pull/57/head
Niels Andriesse 6 years ago
parent 131c27d089
commit b4af9d16d5

@ -1537,7 +1537,7 @@ static NSTimeInterval launchStartedAt;
- (void)createGroupChatsIfNeeded
{
// Setup our default public chats
for (LKGroupChat *chat in LKGroupChat.defaultChats) {
for (LKPublicChat *chat in LKPublicChat.defaultChats) {
NSString *userDefaultsKey = [@"isGroupChatSetUp." stringByAppendingString:chat.id];
BOOL isChatSetUp = [NSUserDefaults.standardUserDefaults boolForKey:userDefaultsKey];
if (!isChatSetUp || !chat.isDeletable) {

@ -131,7 +131,7 @@ private extension MentionCandidateSelectionView {
let profilePicture = OWSContactAvatarBuilder(signalId: mentionCandidate.hexEncodedPublicKey, colorName: .blue, diameter: 36).build()
profilePictureImageView.image = profilePicture
if let publicChatServer = publicChatServer, let publicChatServerID = publicChatServerID {
let isUserModerator = LokiGroupChatAPI.isUserModerator(mentionCandidate.hexEncodedPublicKey, for: publicChatServerID, on: publicChatServer)
let isUserModerator = LokiPublicChatAPI.isUserModerator(mentionCandidate.hexEncodedPublicKey, for: publicChatServerID, on: publicChatServer)
moderatorIconImageView.isHidden = !isUserModerator
} else {
moderatorIconImageView.isHidden = true

@ -88,8 +88,8 @@ final class NewPublicChatVC : OWSViewController {
let displayName = OWSProfileManager.shared().localProfileName()
LokiPublicChatManager.shared.addChat(server: urlAsString, channel: channelID)
.done(on: .main) { [weak self] _ in
let _ = LokiGroupChatAPI.getMessages(for: channelID, on: urlAsString)
let _ = LokiGroupChatAPI.setDisplayName(to: displayName, on: urlAsString)
let _ = LokiPublicChatAPI.getMessages(for: channelID, on: urlAsString)
let _ = LokiPublicChatAPI.setDisplayName(to: displayName, on: urlAsString)
self?.presentingViewController!.dismiss(animated: true, completion: nil)
}
.catch(on: .main) { [weak self] _ in

@ -9,9 +9,9 @@ public final class MentionUtilities : NSObject {
}
@objc public static func highlightMentions(in string: String, isOutgoingMessage: Bool, threadID: String, attributes: [NSAttributedString.Key:Any]) -> NSAttributedString {
var groupChat: LokiGroupChat?
var publicChat: LokiPublicChat?
OWSPrimaryStorage.shared().dbReadConnection.read { transaction in
groupChat = LokiDatabaseUtilities.getGroupChat(for: threadID, in: transaction)
publicChat = LokiDatabaseUtilities.getPublicChat(for: threadID, in: transaction)
}
var string = string
let regex = try! NSRegularExpression(pattern: "@[0-9a-fA-F]*", options: [])
@ -26,8 +26,8 @@ public final class MentionUtilities : NSObject {
if hexEncodedPublicKey == OWSIdentityManager.shared().identityKeyPair()!.hexEncodedPublicKey {
userDisplayName = OWSProfileManager.shared().localProfileName()
} else {
if let groupChat = groupChat {
userDisplayName = DisplayNameUtilities.getGroupChatDisplayName(for: hexEncodedPublicKey, in: groupChat.channel, on: groupChat.server)
if let publicChat = publicChat {
userDisplayName = DisplayNameUtilities.getPublicChatDisplayName(for: hexEncodedPublicKey, in: publicChat.channel, on: publicChat.server)
} else {
userDisplayName = DisplayNameUtilities.getPrivateChatDisplayName(for: hexEncodedPublicKey)
}

@ -299,15 +299,17 @@ NS_ASSUME_NONNULL_BEGIN
[self.contentView addSubview:self.avatarView];
if (self.viewItem.isGroupThread && !self.viewItem.isRSSFeed) {
__block LKGroupChat *groupChat;
__block LKPublicChat *publicChat;
[OWSPrimaryStorage.sharedManager.dbReadConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
groupChat = [LKDatabaseUtilities getGroupChatForThreadID:self.viewItem.interaction.uniqueThreadId transaction: transaction];
publicChat = [LKDatabaseUtilities getPublicChatForThreadID:self.viewItem.interaction.uniqueThreadId transaction: transaction];
}];
BOOL isModerator = [LKGroupChatAPI isUserModerator:incomingMessage.authorId forGroup:groupChat.channel onServer:groupChat.server];
if (publicChat != nil) {
BOOL isModerator = [LKPublicChatAPI isUserModerator:incomingMessage.authorId forGroup:publicChat.channel onServer:publicChat.server];
UIImage *moderatorIcon = [UIImage imageNamed:@"Crown"];
self.moderatorIconImageView.image = moderatorIcon;
self.moderatorIconImageView.hidden = !isModerator;
}
}
[self.contentView addSubview:self.moderatorIconImageView];

@ -554,9 +554,9 @@ const CGFloat kRemotelySourcedContentRowSpacing = 3;
if (quotedAuthor == self.quotedMessage.authorId) {
[OWSPrimaryStorage.sharedManager.dbReadConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
LKGroupChat *groupChat = [LKDatabaseUtilities getGroupChatForThreadID:self.quotedMessage.threadId transaction:transaction];
if (groupChat != nil) {
quotedAuthor = [LKDisplayNameUtilities getGroupChatDisplayNameFor:self.quotedMessage.authorId in:groupChat.channel on:groupChat.server using:transaction];;
LKPublicChat *publicChat = [LKDatabaseUtilities getPublicChatForThreadID:self.quotedMessage.threadId transaction:transaction];
if (publicChat != nil) {
quotedAuthor = [LKDisplayNameUtilities getPublicChatDisplayNameFor:self.quotedMessage.authorId in:publicChat.channel on:publicChat.server using:transaction];
} else {
quotedAuthor = [LKDisplayNameUtilities getPrivateChatDisplayNameFor:self.quotedMessage.authorId];
}

@ -1093,12 +1093,14 @@ const CGFloat kMaxTextViewHeight = 98;
- (void)showMentionCandidateSelectionViewFor:(NSArray<LKMention *> *)mentionCandidates in:(TSThread *)thread
{
__block LKGroupChat *groupChat;
__block LKPublicChat *publicChat;
[OWSPrimaryStorage.sharedManager.dbReadConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
groupChat = [LKDatabaseUtilities getGroupChatForThreadID:thread.uniqueId transaction:transaction];
publicChat = [LKDatabaseUtilities getPublicChatForThreadID:thread.uniqueId transaction:transaction];
}];
self.mentionCandidateSelectionView.publicChatServer = groupChat.server;
[self.mentionCandidateSelectionView setPublicChatServerID:groupChat.channel];
if (publicChat != nil) {
self.mentionCandidateSelectionView.publicChatServer = publicChat.server;
[self.mentionCandidateSelectionView setPublicChatServerID:publicChat.channel];
}
self.mentionCandidateSelectionView.mentionCandidates = mentionCandidates;
self.mentionCandidateSelectionViewSizeConstraint.constant = 6 + MIN(mentionCandidates.count, 4) * 52;
[self setNeedsLayout];

@ -1187,15 +1187,15 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType)
TSMessage *message = (TSMessage *)self.interaction;
if (!message.isGroupChatMessage) return;
__block LKGroupChat *groupChat;
__block LKPublicChat *publicChat;
[self.primaryStorage.dbReadConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
groupChat = [LKDatabaseUtilities getGroupChatForThreadID:groupThread.uniqueId transaction: transaction];
publicChat = [LKDatabaseUtilities getPublicChatForThreadID:groupThread.uniqueId transaction: transaction];
}];
if (groupChat == nil) return;
if (publicChat == nil) return;
// Delete the message
BOOL isSentByUser = (interationType == OWSInteractionType_OutgoingMessage);
[[LKGroupChatAPI deleteMessageWithID:message.groupChatServerID forGroup:groupChat.channel onServer:groupChat.server isSentByUser:isSentByUser].catch(^(NSError *error) {
[[LKPublicChatAPI deleteMessageWithID:message.groupChatServerID forGroup:publicChat.channel onServer:publicChat.server isSentByUser:isSentByUser].catch(^(NSError *error) {
// Roll back
[self.interaction save];
}) retainUntilComplete];
@ -1263,15 +1263,15 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType)
if (!message.isGroupChatMessage) return false;
// Ensure we have the details needed to contact the server
__block LKGroupChat *groupChat;
__block LKPublicChat *publicChat;
[self.primaryStorage.dbReadConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
groupChat = [LKDatabaseUtilities getGroupChatForThreadID:groupThread.uniqueId transaction: transaction];
publicChat = [LKDatabaseUtilities getPublicChatForThreadID:groupThread.uniqueId transaction: transaction];
}];
if (groupChat == nil) return false;
if (publicChat == nil) return false;
// Only allow deletion on incoming messages if the user has moderation permission
if (interationType == OWSInteractionType_IncomingMessage) {
BOOL isModerator = [LKGroupChatAPI isUserModerator:self.userHexEncodedPublicKey forGroup:groupChat.channel onServer:groupChat.server];
BOOL isModerator = [LKPublicChatAPI isUserModerator:self.userHexEncodedPublicKey forGroup:publicChat.channel onServer:publicChat.server];
if (!isModerator) return false;
}

@ -173,10 +173,10 @@ NS_ASSUME_NONNULL_BEGIN
+ (void)updatePublicChatMapping
{
[OWSPrimaryStorage.dbReadWriteConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction * _Nonnull transaction) {
for (LKGroupChat *chat in LKGroupChat.defaultChats) {
for (LKPublicChat *chat in LKPublicChat.defaultChats) {
TSGroupThread *thread = [TSGroupThread threadWithGroupId:chat.idAsData transaction:transaction];
if (thread != nil) {
[LKDatabaseUtilities setGroupChat:chat threadID:thread.uniqueId transaction:transaction];
[LKDatabaseUtilities setPublicChat:chat threadID:thread.uniqueId transaction:transaction];
}
}
}];

@ -535,15 +535,15 @@ typedef void (^ProfileManagerFailureBlock)(NSError *error);
OWSAssertDebug(successBlock);
OWSAssertDebug(failureBlock);
__block NSDictionary *groupChats;
__block NSDictionary *publicChats;
[SSKEnvironment.shared.primaryStorage.dbReadConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
groupChats = [LKDatabaseUtilities getAllGroupChats:transaction];
publicChats = [LKDatabaseUtilities getAllPublicChats:transaction];
}];
NSSet *servers = [NSSet setWithArray:[groupChats.allValues map:^NSString *(LKGroupChat *groupChat) { return groupChat.server; }]];
NSSet *servers = [NSSet setWithArray:[publicChats.allValues map:^NSString *(LKPublicChat *publicChat) { return publicChat.server; }]];
for (NSString *server in servers) {
[[LKGroupChatAPI setDisplayName:localProfileName on:server] retainUntilComplete];
[[LKPublicChatAPI setDisplayName:localProfileName on:server] retainUntilComplete];
}
successBlock();

@ -309,15 +309,15 @@ public final class LokiAPI : NSObject {
guard let cache = userHexEncodedPublicKeyCache[threadID] else { return [] }
var candidates: [Mention] = []
// Gather candidates
var groupChat: LokiGroupChat?
var publicChat: LokiPublicChat?
storage.dbReadConnection.read { transaction in
groupChat = LokiDatabaseUtilities.getGroupChat(for: threadID, in: transaction)
publicChat = LokiDatabaseUtilities.getPublicChat(for: threadID, in: transaction)
}
storage.dbReadConnection.read { transaction in
candidates = cache.flatMap { hexEncodedPublicKey in
let uncheckedDisplayName: String?
if let groupChat = groupChat {
uncheckedDisplayName = DisplayNameUtilities.getGroupChatDisplayName(for: hexEncodedPublicKey, in: groupChat.channel, on: groupChat.server)
if let publicChat = publicChat {
uncheckedDisplayName = DisplayNameUtilities.getPublicChatDisplayName(for: hexEncodedPublicKey, in: publicChat.channel, on: publicChat.server)
} else {
uncheckedDisplayName = DisplayNameUtilities.getPrivateChatDisplayName(for: hexEncodedPublicKey)
}

@ -1,6 +1,6 @@
@objc(LKGroupChat)
public final class LokiGroupChat : NSObject, NSCoding {
@objc(LKPublicChat)
public final class LokiPublicChat : NSObject, NSCoding {
@objc public let id: String
@objc public let idAsData: Data
@objc public let channel: UInt64
@ -8,11 +8,11 @@ public final class LokiGroupChat : NSObject, NSCoding {
@objc public let displayName: String
@objc public let isDeletable: Bool
@objc public static var defaultChats: [LokiGroupChat] {
var chats = [LokiGroupChat(channel: UInt64(1), server: "https://chat.lokinet.org", displayName: NSLocalizedString("Loki Public Chat", comment: ""), isDeletable: true)!]
@objc public static var defaultChats: [LokiPublicChat] {
var chats = [LokiPublicChat(channel: UInt64(1), server: "https://chat.lokinet.org", displayName: NSLocalizedString("Loki Public Chat", comment: ""), isDeletable: true)!]
#if DEBUG
chats.append(LokiGroupChat(channel: UInt64(1), server: "https://chat-dev.lokinet.org", displayName: "Loki Dev Chat", isDeletable: true)!)
chats.append(LokiPublicChat(channel: UInt64(1), server: "https://chat-dev.lokinet.org", displayName: "Loki Dev Chat", isDeletable: true)!)
#endif
return chats

@ -1,7 +1,7 @@
import PromiseKit
@objc(LKGroupChatAPI)
public final class LokiGroupChatAPI : LokiDotNetAPI {
@objc(LKPublicChatAPI)
public final class LokiPublicChatAPI : LokiDotNetAPI {
private static var moderators: [String:[UInt64:Set<String>]] = [:] // Server URL to (channel ID to set of moderator IDs)
// MARK: Settings
@ -9,8 +9,8 @@ public final class LokiGroupChatAPI : LokiDotNetAPI {
private static let maxRetryCount: UInt = 8
// MARK: Public Chat
@objc public static let publicChatMessageType = "network.loki.messenger.publicChat"
@objc private static let channelInfoType = "net.patter-app.settings"
@objc public static let publicChatMessageType = "network.loki.messenger.publicChat"
// MARK: Convenience
private static var userDisplayName: String {
@ -18,9 +18,9 @@ public final class LokiGroupChatAPI : LokiDotNetAPI {
}
// MARK: Database
override internal class var authTokenCollection: String { "LokiGroupChatAuthTokenCollection" }
private static let lastMessageServerIDCollection = "LokiGroupChatLastMessageServerIDCollection"
private static let lastDeletionServerIDCollection = "LokiGroupChatLastDeletionServerIDCollection"
override internal class var authTokenCollection: String { "LokiGroupChatAuthTokenCollection" } // Should ideally be LokiPublicChatAuthTokenCollection
private static let lastMessageServerIDCollection = "LokiGroupChatLastMessageServerIDCollection" // Should ideally be LokiPublicChatLastMessageServerIDCollection
private static let lastDeletionServerIDCollection = "LokiGroupChatLastDeletionServerIDCollection" // Should ideally be LokiPublicChatLastDeletionServerIDCollection
private static func getLastMessageServerID(for group: UInt64, on server: String) -> UInt? {
var result: UInt? = nil
@ -63,19 +63,19 @@ public final class LokiGroupChatAPI : LokiDotNetAPI {
}
// MARK: Public API
public static func getMessages(for group: UInt64, on server: String) -> Promise<[LokiGroupMessage]> {
print("[Loki] Getting messages for group chat with ID: \(group) on server: \(server).")
public static func getMessages(for channel: UInt64, on server: String) -> Promise<[LokiPublicChatMessage]> {
print("[Loki] Getting messages for public chat channel with ID: \(channel) on server: \(server).")
var queryParameters = "include_annotations=1"
if let lastMessageServerID = getLastMessageServerID(for: group, on: server) {
if let lastMessageServerID = getLastMessageServerID(for: channel, on: server) {
queryParameters += "&since_id=\(lastMessageServerID)"
} else {
queryParameters += "&count=-\(fallbackBatchCount)"
}
let url = URL(string: "\(server)/channels/\(group)/messages?\(queryParameters)")!
let url = URL(string: "\(server)/channels/\(channel)/messages?\(queryParameters)")!
let request = TSRequest(url: url)
return TSNetworkManager.shared().makePromise(request: request).map { $0.responseObject }.map { rawResponse in
guard let json = rawResponse as? JSON, let rawMessages = json["data"] as? [JSON] else {
print("[Loki] Couldn't parse messages for group chat with ID: \(group) on server: \(server) from: \(rawResponse).")
print("[Loki] Couldn't parse messages for public chat channel with ID: \(channel) on server: \(server) from: \(rawResponse).")
throw Error.parsingFailed
}
return rawMessages.flatMap { message in
@ -85,23 +85,23 @@ public final class LokiGroupChatAPI : LokiDotNetAPI {
let serverID = message["id"] as? UInt64, let hexEncodedSignatureData = value["sig"] as? String, let signatureVersion = value["sigver"] as? UInt64,
let body = message["text"] as? String, let user = message["user"] as? JSON, let hexEncodedPublicKey = user["username"] as? String,
let timestamp = value["timestamp"] as? UInt64 else {
print("[Loki] Couldn't parse message for group chat with ID: \(group) on server: \(server) from: \(message).")
print("[Loki] Couldn't parse message for public chat channel with ID: \(channel) on server: \(server) from: \(message).")
return nil
}
let displayName = user["name"] as? String ?? NSLocalizedString("Anonymous", comment: "")
let lastMessageServerID = getLastMessageServerID(for: group, on: server)
if serverID > (lastMessageServerID ?? 0) { setLastMessageServerID(for: group, on: server, to: serverID) }
let quote: LokiGroupMessage.Quote?
let lastMessageServerID = getLastMessageServerID(for: channel, on: server)
if serverID > (lastMessageServerID ?? 0) { setLastMessageServerID(for: channel, on: server, to: serverID) }
let quote: LokiPublicChatMessage.Quote?
if let quoteAsJSON = value["quote"] as? JSON, let quotedMessageTimestamp = quoteAsJSON["id"] as? UInt64, let quoteeHexEncodedPublicKey = quoteAsJSON["author"] as? String, let quotedMessageBody = quoteAsJSON["text"] as? String {
let quotedMessageServerID = message["reply_to"] as? UInt64
quote = LokiGroupMessage.Quote(quotedMessageTimestamp: quotedMessageTimestamp, quoteeHexEncodedPublicKey: quoteeHexEncodedPublicKey, quotedMessageBody: quotedMessageBody, quotedMessageServerID: quotedMessageServerID)
quote = LokiPublicChatMessage.Quote(quotedMessageTimestamp: quotedMessageTimestamp, quoteeHexEncodedPublicKey: quoteeHexEncodedPublicKey, quotedMessageBody: quotedMessageBody, quotedMessageServerID: quotedMessageServerID)
} else {
quote = nil
}
let signature = LokiGroupMessage.Signature(data: Data(hex: hexEncodedSignatureData), version: signatureVersion)
let result = LokiGroupMessage(serverID: serverID, hexEncodedPublicKey: hexEncodedPublicKey, displayName: displayName, body: body, type: publicChatMessageType, timestamp: timestamp, quote: quote, signature: signature)
let signature = LokiPublicChatMessage.Signature(data: Data(hex: hexEncodedSignatureData), version: signatureVersion)
let result = LokiPublicChatMessage(serverID: serverID, hexEncodedPublicKey: hexEncodedPublicKey, displayName: displayName, body: body, type: publicChatMessageType, timestamp: timestamp, quote: quote, signature: signature)
guard result.hasValidSignature() else {
print("[Loki] Ignoring group chat message with invalid signature.")
print("[Loki] Ignoring public chat message with invalid signature.")
return nil
}
return result
@ -109,11 +109,11 @@ public final class LokiGroupChatAPI : LokiDotNetAPI {
}
}
public static func sendMessage(_ message: LokiGroupMessage, to group: UInt64, on server: String) -> Promise<LokiGroupMessage> {
public static func sendMessage(_ message: LokiPublicChatMessage, to channel: UInt64, on server: String) -> Promise<LokiPublicChatMessage> {
guard let signedMessage = message.sign(with: userKeyPair.privateKey) else { return Promise(error: Error.signingFailed) }
return getAuthToken(for: server).then { token -> Promise<LokiGroupMessage> in
print("[Loki] Sending message to group chat with ID: \(group) on server: \(server).")
let url = URL(string: "\(server)/channels/\(group)/messages")!
return getAuthToken(for: server).then { token -> Promise<LokiPublicChatMessage> in
print("[Loki] Sending message to public chat channel with ID: \(channel) on server: \(server).")
let url = URL(string: "\(server)/channels/\(channel)/messages")!
let parameters = signedMessage.toJSON()
let request = TSRequest(url: url, method: "POST", parameters: parameters)
request.allHTTPHeaderFields = [ "Content-Type" : "application/json", "Authorization" : "Bearer \(token)" ]
@ -124,13 +124,13 @@ public final class LokiGroupChatAPI : LokiDotNetAPI {
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZ"
guard let json = rawResponse as? JSON, let messageAsJSON = json["data"] as? JSON, let serverID = messageAsJSON["id"] as? UInt64, let body = messageAsJSON["text"] as? String,
let dateAsString = messageAsJSON["created_at"] as? String, let date = dateFormatter.date(from: dateAsString) else {
print("[Loki] Couldn't parse message for group chat with ID: \(group) on server: \(server) from: \(rawResponse).")
print("[Loki] Couldn't parse message for public chat channel with ID: \(channel) on server: \(server) from: \(rawResponse).")
throw Error.parsingFailed
}
let timestamp = UInt64(date.timeIntervalSince1970) * 1000
return LokiGroupMessage(serverID: serverID, hexEncodedPublicKey: userHexEncodedPublicKey, displayName: displayName, body: body, type: publicChatMessageType, timestamp: timestamp, quote: signedMessage.quote, signature: signedMessage.signature)
return LokiPublicChatMessage(serverID: serverID, hexEncodedPublicKey: userHexEncodedPublicKey, displayName: displayName, body: body, type: publicChatMessageType, timestamp: timestamp, quote: signedMessage.quote, signature: signedMessage.signature)
}
}.recover { error -> Promise<LokiGroupMessage> in
}.recover { error -> Promise<LokiPublicChatMessage> in
if let error = error as? NetworkManagerError, error.statusCode == 401 {
print("[Loki] Group chat auth token for: \(server) expired; dropping it.")
storage.dbReadWriteConnection.removeObject(forKey: server, inCollection: authTokenCollection)
@ -139,44 +139,44 @@ public final class LokiGroupChatAPI : LokiDotNetAPI {
}.retryingIfNeeded(maxRetryCount: maxRetryCount).map { message in
Analytics.shared.track("Group Message Sent")
return message
}.recover { error -> Promise<LokiGroupMessage> in
}.recover { error -> Promise<LokiPublicChatMessage> in
Analytics.shared.track("Failed to Send Group Message")
throw error
}
}
public static func getDeletedMessageServerIDs(for group: UInt64, on server: String) -> Promise<[UInt64]> {
print("[Loki] Getting deleted messages for group chat with ID: \(group) on server: \(server).")
public static func getDeletedMessageServerIDs(for channel: UInt64, on server: String) -> Promise<[UInt64]> {
print("[Loki] Getting deleted messages for public chat channel with ID: \(channel) on server: \(server).")
let queryParameters: String
if let lastDeletionServerID = getLastDeletionServerID(for: group, on: server) {
if let lastDeletionServerID = getLastDeletionServerID(for: channel, on: server) {
queryParameters = "since_id=\(lastDeletionServerID)"
} else {
queryParameters = "count=\(fallbackBatchCount)"
}
let url = URL(string: "\(server)/loki/v1/channel/\(group)/deletes?\(queryParameters)")!
let url = URL(string: "\(server)/loki/v1/channel/\(channel)/deletes?\(queryParameters)")!
let request = TSRequest(url: url)
return TSNetworkManager.shared().makePromise(request: request).map { $0.responseObject }.map { rawResponse in
guard let json = rawResponse as? JSON, let deletions = json["data"] as? [JSON] else {
print("[Loki] Couldn't parse deleted messages for group chat with ID: \(group) on server: \(server) from: \(rawResponse).")
print("[Loki] Couldn't parse deleted messages for public chat channel with ID: \(channel) on server: \(server) from: \(rawResponse).")
throw Error.parsingFailed
}
return deletions.flatMap { deletion in
guard let serverID = deletion["id"] as? UInt64, let messageServerID = deletion["message_id"] as? UInt64 else {
print("[Loki] Couldn't parse deleted message for group chat with ID: \(group) on server: \(server) from: \(deletion).")
print("[Loki] Couldn't parse deleted message for public chat channel with ID: \(channel) on server: \(server) from: \(deletion).")
return nil
}
let lastDeletionServerID = getLastDeletionServerID(for: group, on: server)
if serverID > (lastDeletionServerID ?? 0) { setLastDeletionServerID(for: group, on: server, to: serverID) }
let lastDeletionServerID = getLastDeletionServerID(for: channel, on: server)
if serverID > (lastDeletionServerID ?? 0) { setLastDeletionServerID(for: channel, on: server, to: serverID) }
return messageServerID
}
}
}
public static func deleteMessage(with messageID: UInt, for group: UInt64, on server: String, isSentByUser: Bool) -> Promise<Void> {
public static func deleteMessage(with messageID: UInt, for channel: UInt64, on server: String, isSentByUser: Bool) -> Promise<Void> {
return getAuthToken(for: server).then { token -> Promise<Void> in
let isModerationRequest = !isSentByUser
print("[Loki] Deleting message with ID: \(messageID) for group chat with ID: \(group) on server: \(server) (isModerationRequest = \(isModerationRequest)).")
let urlAsString = isSentByUser ? "\(server)/channels/\(group)/messages/\(messageID)" : "\(server)/loki/v1/moderation/message/\(messageID)"
print("[Loki] Deleting message with ID: \(messageID) for public chat channel with ID: \(channel) on server: \(server) (isModerationRequest = \(isModerationRequest)).")
let urlAsString = isSentByUser ? "\(server)/channels/\(channel)/messages/\(messageID)" : "\(server)/loki/v1/moderation/message/\(messageID)"
let url = URL(string: urlAsString)!
let request = TSRequest(url: url, method: "DELETE", parameters: [:])
request.allHTTPHeaderFields = [ "Content-Type" : "application/json", "Authorization" : "Bearer \(token)" ]
@ -186,27 +186,27 @@ public final class LokiGroupChatAPI : LokiDotNetAPI {
}
}
public static func getModerators(for group: UInt64, on server: String) -> Promise<Set<String>> {
let url = URL(string: "\(server)/loki/v1/channel/\(group)/get_moderators")!
public static func getModerators(for channel: UInt64, on server: String) -> Promise<Set<String>> {
let url = URL(string: "\(server)/loki/v1/channel/\(channel)/get_moderators")!
let request = TSRequest(url: url)
return TSNetworkManager.shared().makePromise(request: request).map { $0.responseObject }.map { rawResponse in
guard let json = rawResponse as? JSON, let moderators = json["moderators"] as? [String] else {
print("[Loki] Couldn't parse moderators for group chat with ID: \(group) on server: \(server) from: \(rawResponse).")
print("[Loki] Couldn't parse moderators for public chat channel with ID: \(channel) on server: \(server) from: \(rawResponse).")
throw Error.parsingFailed
}
let moderatorAsSet = Set(moderators);
if self.moderators.keys.contains(server) {
self.moderators[server]![group] = moderatorAsSet
self.moderators[server]![channel] = moderatorAsSet
} else {
self.moderators[server] = [ group : moderatorAsSet ]
self.moderators[server] = [ channel : moderatorAsSet ]
}
return moderatorAsSet
}
}
@objc (isUserModerator:forGroup:onServer:)
public static func isUserModerator(_ hexEncodedPublicString: String, for group: UInt64, on server: String) -> Bool {
return moderators[server]?[group]?.contains(hexEncodedPublicString) ?? false
public static func isUserModerator(_ hexEncodedPublicString: String, for channel: UInt64, on server: String) -> Bool {
return moderators[server]?[channel]?.contains(hexEncodedPublicString) ?? false
}
public static func setDisplayName(to newDisplayName: String?, on server: String) -> Promise<Void> {
@ -216,27 +216,27 @@ public final class LokiGroupChatAPI : LokiDotNetAPI {
let url = URL(string: "\(server)/users/me")!
let request = TSRequest(url: url, method: "PATCH", parameters: parameters)
request.allHTTPHeaderFields = [ "Content-Type" : "application/json", "Authorization" : "Bearer \(token)" ]
return TSNetworkManager.shared().makePromise(request: request).retryingIfNeeded(maxRetryCount: 3).map { _ in }.recover { error in
return TSNetworkManager.shared().makePromise(request: request).map { _ in }.recover { error in
print("Couldn't update display name due to error: \(error).")
throw error
}
}
}.retryingIfNeeded(maxRetryCount: 3)
}
public static func getChannelInfo(_ channel: UInt64, on server: String) -> Promise<String> {
public static func getInfo(for channel: UInt64, on server: String) -> Promise<LokiPublicChatInfo> {
let url = URL(string: "\(server)/channels/\(channel)?include_annotations=1")!
let request = TSRequest(url: url)
return TSNetworkManager.shared().makePromise(request: request).map { $0.responseObject }.map { rawResponse in
guard let json = rawResponse as? JSON,
let data = json["data"] as? JSON,
let annotations = data["annotations"] as? [JSON],
let infoAnnotation = annotations.first,
let info = infoAnnotation["value"] as? JSON,
let name = info["name"] as? String else {
print("[Loki] Couldn't parse info for group chat with ID: \(channel) on server: \(server) from: \(rawResponse).")
let annotation = annotations.first,
let info = annotation["value"] as? JSON,
let displayName = info["name"] as? String else {
print("[Loki] Couldn't parse info for public chat channel with ID: \(channel) on server: \(server) from: \(rawResponse).")
throw Error.parsingFailed
}
return name
return LokiPublicChatInfo(displayName: displayName)
}
}
@ -252,7 +252,7 @@ public final class LokiGroupChatAPI : LokiDotNetAPI {
}
@objc(sendMessage:toGroup:onServer:)
public static func objc_sendMessage(_ message: LokiGroupMessage, to group: UInt64, on server: String) -> AnyPromise {
public static func objc_sendMessage(_ message: LokiPublicChatMessage, to group: UInt64, on server: String) -> AnyPromise {
return AnyPromise.from(sendMessage(message, to: group, on: server))
}

@ -0,0 +1,4 @@
public struct LokiPublicChatInfo {
public let displayName: String
}

@ -11,7 +11,7 @@ public final class LokiPublicChatManager: NSObject {
@objc public static let shared = LokiPublicChatManager()
private var chats: [String: LokiGroupChat] = [:]
private var chats: [String: LokiPublicChat] = [:]
private var pollers: [String: GroupChatPoller] = [:]
private var isPolling = false
@ -29,11 +29,11 @@ public final class LokiPublicChatManager: NSObject {
}
@objc public func startPollersIfNeeded() {
for (threadID, groupChat) in chats {
for (threadID, publicChat) in chats {
if let poller = pollers[threadID] {
poller.startIfNeeded()
} else {
let poller = GroupChatPoller(for: groupChat)
let poller = GroupChatPoller(for: publicChat)
poller.startIfNeeded()
pollers[threadID] = poller
}
@ -46,7 +46,7 @@ public final class LokiPublicChatManager: NSObject {
isPolling = false
}
public func addChat(server: String, channel: UInt64) -> Promise<LokiGroupChat> {
public func addChat(server: String, channel: UInt64) -> Promise<LokiPublicChat> {
if let existingChat = getChat(server: server, channel: channel) {
if let chat = self.addChat(server: server, channel: channel, name: existingChat.displayName) {
return Promise.value(chat)
@ -55,18 +55,18 @@ public final class LokiPublicChatManager: NSObject {
}
}
return LokiGroupChatAPI.getAuthToken(for: server).then { token in
return LokiGroupChatAPI.getChannelInfo(channel, on: server)
}.map { channelInfo -> LokiGroupChat in
guard let chat = self.addChat(server: server, channel: channel, name: channelInfo) else { throw Error.chatCreationFailed }
return LokiPublicChatAPI.getAuthToken(for: server).then { token in
return LokiPublicChatAPI.getInfo(for: channel, on: server)
}.map { channelInfo -> LokiPublicChat in
guard let chat = self.addChat(server: server, channel: channel, name: channelInfo.displayName) else { throw Error.chatCreationFailed }
return chat
}
}
@discardableResult
@objc(addChatWithServer:channel:name:)
public func addChat(server: String, channel: UInt64, name: String) -> LokiGroupChat? {
guard let chat = LokiGroupChat(channel: channel, server: server, displayName: name, isDeletable: true) else { return nil }
public func addChat(server: String, channel: UInt64, name: String) -> LokiPublicChat? {
guard let chat = LokiPublicChat(channel: channel, server: server, displayName: name, isDeletable: true) else { return nil }
let model = TSGroupModel(title: chat.displayName, memberIds: [ourHexEncodedPublicKey!, chat.server], image: nil, groupId: chat.idAsData)
// Store the group chat mapping
@ -85,7 +85,7 @@ public final class LokiPublicChatManager: NSObject {
}
// Save the group chat
LokiDatabaseUtilities.setGroupChat(chat, for: thread.uniqueId!, in: transaction)
LokiDatabaseUtilities.setPublicChat(chat, for: thread.uniqueId!, in: transaction)
}
// Update chats and pollers
@ -101,7 +101,7 @@ public final class LokiPublicChatManager: NSObject {
private func refreshChatsAndPollers() {
storage.dbReadConnection.read { transaction in
let newChats = LokiDatabaseUtilities.getAllGroupChats(in: transaction)
let newChats = LokiDatabaseUtilities.getAllPublicChats(in: transaction)
// Remove any chats that don't exist in the database
let removedChatThreadIds = self.chats.keys.filter { !newChats.keys.contains($0) }
@ -124,18 +124,18 @@ public final class LokiPublicChatManager: NSObject {
// Reset the last message cache
if let chat = self.chats[threadId] {
LokiGroupChatAPI.resetLastMessageCache(for: chat.channel, on: chat.server)
LokiPublicChatAPI.resetLastMessageCache(for: chat.channel, on: chat.server)
}
// Remove the chat from the db
storage.dbReadWriteConnection.readWrite { transaction in
LokiDatabaseUtilities.removeGroupChat(for: threadId, in: transaction)
LokiDatabaseUtilities.removePublicChat(for: threadId, in: transaction)
}
refreshChatsAndPollers()
}
private func getChat(server: String, channel: UInt64) -> LokiGroupChat? {
private func getChat(server: String, channel: UInt64) -> LokiPublicChat? {
return chats.values.first { chat in
return chat.server == server && chat.channel == channel
}

@ -1,7 +1,7 @@
import PromiseKit
@objc(LKGroupMessage)
public final class LokiGroupMessage : NSObject {
public final class LokiPublicChatMessage : NSObject {
public let serverID: UInt64?
public let hexEncodedPublicKey: String
public let displayName: String
@ -62,18 +62,18 @@ public final class LokiGroupMessage : NSObject {
}
// MARK: Crypto
internal func sign(with privateKey: Data) -> LokiGroupMessage? {
internal func sign(with privateKey: Data) -> LokiPublicChatMessage? {
guard let data = getValidationData(for: signatureVersion) else {
print("[Loki] Failed to sign group chat message.")
print("[Loki] Failed to sign public chat message.")
return nil
}
let userKeyPair = OWSIdentityManager.shared().identityKeyPair()!
guard let signatureData = try? Ed25519.sign(data, with: userKeyPair) else {
print("[Loki] Failed to sign group chat message.")
print("[Loki] Failed to sign public chat message.")
return nil
}
let signature = Signature(data: signatureData, version: signatureVersion)
return LokiGroupMessage(serverID: serverID, hexEncodedPublicKey: hexEncodedPublicKey, displayName: displayName, body: body, type: type, timestamp: timestamp, quote: quote, signature: signature)
return LokiPublicChatMessage(serverID: serverID, hexEncodedPublicKey: hexEncodedPublicKey, displayName: displayName, body: body, type: type, timestamp: timestamp, quote: quote, signature: signature)
}
internal func hasValidSignature() -> Bool {

@ -1,7 +1,7 @@
@objc(LKGroupChatPoller)
@objc(LKPublicChatPoller)
public final class GroupChatPoller : NSObject {
private let group: LokiGroupChat
private let group: LokiPublicChat
private var pollForNewMessagesTimer: Timer? = nil
private var pollForDeletedMessagesTimer: Timer? = nil
private var pollForModeratorsTimer: Timer? = nil
@ -15,7 +15,7 @@ public final class GroupChatPoller : NSObject {
// MARK: Lifecycle
@objc(initForGroup:)
public init(for group: LokiGroupChat) {
public init(for group: LokiPublicChat) {
self.group = group
super.init()
}
@ -45,7 +45,7 @@ public final class GroupChatPoller : NSObject {
let group = self.group
let userHexEncodedPublicKey = self.userHexEncodedPublicKey
// Processing logic for incoming messages
func processIncomingMessage(_ message: LokiGroupMessage) {
func processIncomingMessage(_ message: LokiPublicChatMessage) {
let senderHexEncodedPublicKey = message.hexEncodedPublicKey
let endIndex = senderHexEncodedPublicKey.endIndex
let cutoffIndex = senderHexEncodedPublicKey.index(endIndex, offsetBy: -8)
@ -80,7 +80,7 @@ public final class GroupChatPoller : NSObject {
}
}
// Processing logic for outgoing messages
func processOutgoingMessage(_ message: LokiGroupMessage) {
func processOutgoingMessage(_ message: LokiPublicChatMessage) {
guard let messageServerID = message.serverID else { return }
let storage = OWSPrimaryStorage.shared()
var isDuplicate = false
@ -102,7 +102,7 @@ public final class GroupChatPoller : NSObject {
storage.dbReadWriteConnection.readWrite { transaction in
message.update(withSentRecipient: group.server, wasSentByUD: false, transaction: transaction)
message.saveGroupChatServerID(messageServerID, in: transaction)
guard let messageID = message.uniqueId else { return print("[Loki] Failed to save group message.") }
guard let messageID = message.uniqueId else { return print("[Loki] Failed to save public chat message.") }
storage.setIDForMessageWithServerID(UInt(messageServerID), to: messageID, in: transaction)
}
if let linkPreviewURL = OWSLinkPreview.previewUrl(forMessageBodyText: message.body, selectedRange: nil) {
@ -110,7 +110,7 @@ public final class GroupChatPoller : NSObject {
}
}
// Poll
let _ = LokiGroupChatAPI.getMessages(for: group.channel, on: group.server).done(on: .main) { messages in
let _ = LokiPublicChatAPI.getMessages(for: group.channel, on: group.server).done(on: .main) { messages in
messages.forEach { message in
if message.hexEncodedPublicKey != userHexEncodedPublicKey {
processIncomingMessage(message)
@ -123,7 +123,7 @@ public final class GroupChatPoller : NSObject {
private func pollForDeletedMessages() {
let group = self.group
let _ = LokiGroupChatAPI.getDeletedMessageServerIDs(for: group.channel, on: group.server).done { deletedMessageServerIDs in
let _ = LokiPublicChatAPI.getDeletedMessageServerIDs(for: group.channel, on: group.server).done { deletedMessageServerIDs in
let storage = OWSPrimaryStorage.shared()
storage.dbReadWriteConnection.readWrite { transaction in
let deletedMessageIDs = deletedMessageServerIDs.compactMap { storage.getIDForMessage(withServerID: UInt($0), in: transaction) }
@ -135,6 +135,6 @@ public final class GroupChatPoller : NSObject {
}
private func pollForModerators() {
let _ = LokiGroupChatAPI.getModerators(for: group.channel, on: group.server)
let _ = LokiPublicChatAPI.getModerators(for: group.channel, on: group.server)
}
}

@ -27,31 +27,31 @@ public final class LokiDatabaseUtilities : NSObject {
return OWSPrimaryStorage.shared().getMasterHexEncodedPublicKey(for: slaveHexEncodedPublicKey, in: transaction)
}
// MARK: Group Chats
private static let groupChatCollection = "LokiGroupChatCollection"
// MARK: Public Chats
private static let publicChatCollection = "LokiPublicChatCollection"
@objc(getAllGroupChats:)
public static func getAllGroupChats(in transaction: YapDatabaseReadTransaction) -> [String:LokiGroupChat] {
var result = [String:LokiGroupChat]()
transaction.enumerateKeysAndObjects(inCollection: groupChatCollection) { threadID, object, _ in
guard let groupChat = object as? LokiGroupChat else { return }
result[threadID] = groupChat
@objc(getAllPublicChats:)
public static func getAllPublicChats(in transaction: YapDatabaseReadTransaction) -> [String:LokiPublicChat] {
var result = [String:LokiPublicChat]()
transaction.enumerateKeysAndObjects(inCollection: publicChatCollection) { threadID, object, _ in
guard let publicChat = object as? LokiPublicChat else { return }
result[threadID] = publicChat
}
return result
}
@objc(getGroupChatForThreadID:transaction:)
public static func getGroupChat(for threadID: String, in transaction: YapDatabaseReadTransaction) -> LokiGroupChat? {
return transaction.object(forKey: threadID, inCollection: groupChatCollection) as? LokiGroupChat
@objc(getPublicChatForThreadID:transaction:)
public static func getPublicChat(for threadID: String, in transaction: YapDatabaseReadTransaction) -> LokiPublicChat? {
return transaction.object(forKey: threadID, inCollection: publicChatCollection) as? LokiPublicChat
}
@objc(setGroupChat:threadID:transaction:)
public static func setGroupChat(_ groupChat: LokiGroupChat, for threadID: String, in transaction: YapDatabaseReadWriteTransaction) {
transaction.setObject(groupChat, forKey: threadID, inCollection: groupChatCollection)
@objc(setPublicChat:threadID:transaction:)
public static func setPublicChat(_ publicChat: LokiPublicChat, for threadID: String, in transaction: YapDatabaseReadWriteTransaction) {
transaction.setObject(publicChat, forKey: threadID, inCollection: publicChatCollection)
}
@objc(removeGroupChatForThreadID:transaction:)
public static func removeGroupChat(for threadID: String, in transaction: YapDatabaseReadWriteTransaction) {
transaction.removeObject(forKey: threadID, inCollection: groupChatCollection)
@objc(removePublicChatForThreadID:transaction:)
public static func removePublicChat(for threadID: String, in transaction: YapDatabaseReadWriteTransaction) {
transaction.removeObject(forKey: threadID, inCollection: publicChatCollection)
}
}

@ -20,15 +20,15 @@ public final class DisplayNameUtilities : NSObject {
}
}
@objc public static func getGroupChatDisplayName(for hexEncodedPublicKey: String, in channel: UInt64, on server: String) -> String? {
@objc public static func getPublicChatDisplayName(for hexEncodedPublicKey: String, in channel: UInt64, on server: String) -> String? {
var result: String?
OWSPrimaryStorage.shared().dbReadConnection.read { transaction in
result = getGroupChatDisplayName(for: hexEncodedPublicKey, in: channel, on: server, using: transaction)
result = getPublicChatDisplayName(for: hexEncodedPublicKey, in: channel, on: server, using: transaction)
}
return result
}
@objc public static func getGroupChatDisplayName(for hexEncodedPublicKey: String, in channel: UInt64, on server: String, using transaction: YapDatabaseReadTransaction) -> String? {
@objc public static func getPublicChatDisplayName(for hexEncodedPublicKey: String, in channel: UInt64, on server: String, using transaction: YapDatabaseReadTransaction) -> String? {
if hexEncodedPublicKey == userHexEncodedPublicKey {
return userDisplayName
} else {

@ -46,8 +46,8 @@ typedef NS_ENUM(NSInteger, LKMessageFriendRequestStatus) {
@property (nonatomic, readonly) BOOL hasFriendRequestStatusMessage;
@property (nonatomic) BOOL isP2P;
// Group chat
@property (nonatomic) uint64_t groupChatServerID;
@property (nonatomic, readonly) BOOL isGroupChatMessage;
@property (nonatomic) uint64_t groupChatServerID; // Should ideally be publicChatServerID
@property (nonatomic, readonly) BOOL isGroupChatMessage; // Should ideally be isPublicChatMessage
- (instancetype)initInteractionWithTimestamp:(uint64_t)timestamp inThread:(TSThread *)thread NS_UNAVAILABLE;

@ -504,8 +504,12 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
[recipientIds addObject:self.tsAccountManager.localNumber];
} else if (thread.isGroupThread) {
[self.primaryStorage.dbReadConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
LKGroupChat *groupChat = [LKDatabaseUtilities getGroupChatForThreadID:thread.uniqueId transaction:transaction];
[recipientIds addObject:groupChat.server];
LKPublicChat *publicChat = [LKDatabaseUtilities getPublicChatForThreadID:thread.uniqueId transaction:transaction];
if (publicChat != nil) {
[recipientIds addObject:publicChat.server];
} else {
// TODO: Handle
}
}];
} else if ([thread isKindOfClass:[TSContactThread class]]) {
NSString *recipientContactId = ((TSContactThread *)thread).contactIdentifier;
@ -1188,11 +1192,11 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
[self messageSendDidFail:messageSend deviceMessages:deviceMessages statusCode:statusCode error:error responseData:responseData];
};
__block LKGroupChat *groupChat;
__block LKPublicChat *publicChat;
[OWSPrimaryStorage.sharedManager.dbReadConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
groupChat = [LKDatabaseUtilities getGroupChatForThreadID:message.uniqueThreadId transaction: transaction];
publicChat = [LKDatabaseUtilities getPublicChatForThreadID:message.uniqueThreadId transaction: transaction];
}];
if (groupChat != nil) {
if (publicChat != nil) {
NSString *userHexEncodedPublicKey = OWSIdentityManager.sharedManager.identityKeyPair.hexEncodedPublicKey;
NSString *displayName = SSKEnvironment.shared.profileManager.localProfileName;
if (displayName == nil) { displayName = @"Anonymous"; }
@ -1205,9 +1209,9 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
quotedMessageServerID = [LKDatabaseUtilities getServerIDForQuoteWithID:quoteID quoteeHexEncodedPublicKey:quoteeHexEncodedPublicKey threadID:messageSend.thread.uniqueId transaction:transaction];
}];
}
LKGroupMessage *groupMessage = [[LKGroupMessage alloc] initWithHexEncodedPublicKey:userHexEncodedPublicKey displayName:displayName body:message.body type:LKGroupChatAPI.publicChatMessageType
LKGroupMessage *groupMessage = [[LKGroupMessage alloc] initWithHexEncodedPublicKey:userHexEncodedPublicKey displayName:displayName body:message.body type:LKPublicChatAPI.publicChatMessageType
timestamp:message.timestamp quotedMessageTimestamp:quoteID quoteeHexEncodedPublicKey:quoteeHexEncodedPublicKey quotedMessageBody:quote.body quotedMessageServerID:quotedMessageServerID signatureData:nil signatureVersion:0];
[[LKGroupChatAPI sendMessage:groupMessage toGroup:groupChat.channel onServer:groupChat.server]
[[LKPublicChatAPI sendMessage:groupMessage toGroup:publicChat.channel onServer:publicChat.server]
.thenOn(OWSDispatch.sendingQueue, ^(LKGroupMessage *groupMessage) {
[self.dbConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
[message saveGroupChatServerID:groupMessage.serverID in:transaction];

Loading…
Cancel
Save