Delete V1 OGS & file server

This commit is contained in:
Niels Andriesse 2021-05-04 15:46:48 +10:00
parent f552d51423
commit d742fc1548
30 changed files with 66 additions and 1838 deletions

View File

@ -251,7 +251,6 @@
B8C2B2C82563685C00551B4D /* CircleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8C2B2C72563685C00551B4D /* CircleView.swift */; };
B8C2B332256376F000551B4D /* ThreadUtil.m in Sources */ = {isa = PBXBuildFile; fileRef = B8C2B331256376F000551B4D /* ThreadUtil.m */; };
B8C2B3442563782400551B4D /* ThreadUtil.h in Headers */ = {isa = PBXBuildFile; fileRef = B8C2B33B2563770800551B4D /* ThreadUtil.h */; settings = {ATTRIBUTES = (Public, ); }; };
B8CADAE925AFADF400AAFA15 /* OpenGroupManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3AAFFCB25AE92150089E6DD /* OpenGroupManager.swift */; };
B8CCF6352396005F0091D419 /* SpaceMono-Regular.ttf in Resources */ = {isa = PBXBuildFile; fileRef = B8CCF6342396005F0091D419 /* SpaceMono-Regular.ttf */; };
B8CCF63723961D6D0091D419 /* NewDMVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8CCF63623961D6D0091D419 /* NewDMVC.swift */; };
B8CCF63F23975CFB0091D419 /* JoinOpenGroupVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8CCF63E23975CFB0091D419 /* JoinOpenGroupVC.swift */; };
@ -380,7 +379,6 @@
C32C5D83256DD5B6003C73A2 /* SSKKeychainStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDBBC255A581600E217F9 /* SSKKeychainStorage.swift */; };
C32C5D9C256DD6DC003C73A2 /* OWSOutgoingReceiptManager.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDB6F255A580F00E217F9 /* OWSOutgoingReceiptManager.m */; };
C32C5DA5256DD6E5003C73A2 /* OWSOutgoingReceiptManager.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDABD255A580100E217F9 /* OWSOutgoingReceiptManager.h */; settings = {ATTRIBUTES = (Public, ); }; };
C32C5DBE256DD743003C73A2 /* OpenGroupPoller.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDA8C255A57FD00E217F9 /* OpenGroupPoller.swift */; };
C32C5DBF256DD743003C73A2 /* ClosedGroupPoller.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDB34255A580B00E217F9 /* ClosedGroupPoller.swift */; };
C32C5DC0256DD743003C73A2 /* Poller.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDB3A255A580B00E217F9 /* Poller.swift */; };
C32C5DC9256DD935003C73A2 /* ProxiedContentDownloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDAF2255A580500E217F9 /* ProxiedContentDownloader.swift */; };
@ -678,15 +676,8 @@
C3A71D1F25589AC30043A11F /* WebSocketResources.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3A71D1D25589AC30043A11F /* WebSocketResources.pb.swift */; };
C3A71F892558BA9F0043A11F /* Mnemonic.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3A71F882558BA9F0043A11F /* Mnemonic.swift */; };
C3A7211A2558BCA10043A11F /* DiffieHellman.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3A71D662558A0170043A11F /* DiffieHellman.swift */; };
C3A721382558BDFA0043A11F /* OpenGroupMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3A721342558BDF90043A11F /* OpenGroupMessage.swift */; };
C3A721392558BDFA0043A11F /* OpenGroupAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3A721352558BDF90043A11F /* OpenGroupAPI.swift */; };
C3A7213A2558BDFA0043A11F /* OpenGroupInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3A721362558BDFA0043A11F /* OpenGroupInfo.swift */; };
C3A7213B2558BDFA0043A11F /* OpenGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3A721372558BDFA0043A11F /* OpenGroup.swift */; };
C3A721902558C0CD0043A11F /* FileServerAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3A7218F2558C0CD0043A11F /* FileServerAPI.swift */; };
C3A7219A2558C1660043A11F /* AnyPromise+Conversion.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3A721992558C1660043A11F /* AnyPromise+Conversion.swift */; };
C3A7222A2558C1E40043A11F /* DotNetAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3A722292558C1E40043A11F /* DotNetAPI.swift */; };
C3A7225E2558C38D0043A11F /* Promise+Retaining.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3A7225D2558C38D0043A11F /* Promise+Retaining.swift */; };
C3A7229C2558E4310043A11F /* OpenGroupMessage+Conversion.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3A7229B2558E4310043A11F /* OpenGroupMessage+Conversion.swift */; };
C3A76A8D25DB83F90074CB90 /* PermissionMissingModal.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3A76A8C25DB83F90074CB90 /* PermissionMissingModal.swift */; };
C3AABDDF2553ECF00042FF4C /* Array+Description.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A5D12553860800C340D1 /* Array+Description.swift */; };
C3AAFFE825AE975D0089E6DD /* ConfigurationMessage+Convenience.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3AAFFDE25AE96FF0089E6DD /* ConfigurationMessage+Convenience.swift */; };
@ -1321,7 +1312,6 @@
C33FDA87255A57FC00E217F9 /* TypingIndicators.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TypingIndicators.swift; sourceTree = "<group>"; };
C33FDA88255A57FD00E217F9 /* YapDatabaseTransaction+OWS.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "YapDatabaseTransaction+OWS.h"; sourceTree = "<group>"; };
C33FDA8B255A57FD00E217F9 /* AppVersion.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppVersion.m; sourceTree = "<group>"; };
C33FDA8C255A57FD00E217F9 /* OpenGroupPoller.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OpenGroupPoller.swift; sourceTree = "<group>"; };
C33FDA8E255A57FD00E217F9 /* OWSFileSystem.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSFileSystem.m; sourceTree = "<group>"; };
C33FDA90255A57FD00E217F9 /* TSYapDatabaseObject.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TSYapDatabaseObject.m; sourceTree = "<group>"; };
C33FDA96255A57FE00E217F9 /* OWSDispatch.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSDispatch.h; sourceTree = "<group>"; };
@ -1680,18 +1670,10 @@
C3A71D4E25589FF30043A11F /* NSData+messagePadding.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSData+messagePadding.h"; sourceTree = "<group>"; };
C3A71D662558A0170043A11F /* DiffieHellman.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DiffieHellman.swift; sourceTree = "<group>"; };
C3A71F882558BA9F0043A11F /* Mnemonic.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Mnemonic.swift; sourceTree = "<group>"; };
C3A721342558BDF90043A11F /* OpenGroupMessage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OpenGroupMessage.swift; sourceTree = "<group>"; };
C3A721352558BDF90043A11F /* OpenGroupAPI.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OpenGroupAPI.swift; sourceTree = "<group>"; };
C3A721362558BDFA0043A11F /* OpenGroupInfo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OpenGroupInfo.swift; sourceTree = "<group>"; };
C3A721372558BDFA0043A11F /* OpenGroup.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OpenGroup.swift; sourceTree = "<group>"; };
C3A7218F2558C0CD0043A11F /* FileServerAPI.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileServerAPI.swift; sourceTree = "<group>"; };
C3A721992558C1660043A11F /* AnyPromise+Conversion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AnyPromise+Conversion.swift"; sourceTree = "<group>"; };
C3A722292558C1E40043A11F /* DotNetAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DotNetAPI.swift; sourceTree = "<group>"; };
C3A7225D2558C38D0043A11F /* Promise+Retaining.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Promise+Retaining.swift"; sourceTree = "<group>"; };
C3A7229B2558E4310043A11F /* OpenGroupMessage+Conversion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OpenGroupMessage+Conversion.swift"; sourceTree = "<group>"; };
C3A76A8C25DB83F90074CB90 /* PermissionMissingModal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PermissionMissingModal.swift; sourceTree = "<group>"; };
C3AA6BB824CE8F1B002358B6 /* Migrating Translations from Android.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; name = "Migrating Translations from Android.md"; path = "Meta/Translations/Migrating Translations from Android.md"; sourceTree = "<group>"; };
C3AAFFCB25AE92150089E6DD /* OpenGroupManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenGroupManager.swift; sourceTree = "<group>"; };
C3AAFFDE25AE96FF0089E6DD /* ConfigurationMessage+Convenience.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ConfigurationMessage+Convenience.swift"; sourceTree = "<group>"; };
C3AAFFF125AE99710089E6DD /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
C3AECBEA24EF5244005743DE /* fa */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fa; path = fa.lproj/Localizable.strings; sourceTree = "<group>"; };
@ -2460,31 +2442,6 @@
path = Meta;
sourceTree = "<group>";
};
C3227FF4260AAD58006EA627 /* V2 */ = {
isa = PBXGroup;
children = (
C3DB6694260AC923001EFC55 /* OpenGroupV2.swift */,
B88FA7B726045D100049422F /* OpenGroupAPIV2.swift */,
C3DB66CB260AF1F3001EFC55 /* OpenGroupAPIV2+ObjC.swift */,
C3DB66AB260ACA42001EFC55 /* OpenGroupManagerV2.swift */,
C3227FF5260AAD66006EA627 /* OpenGroupMessageV2.swift */,
);
path = V2;
sourceTree = "<group>";
};
C3228005260AAD7E006EA627 /* V1 */ = {
isa = PBXGroup;
children = (
C3A721372558BDFA0043A11F /* OpenGroup.swift */,
C3A721352558BDF90043A11F /* OpenGroupAPI.swift */,
C3A721362558BDFA0043A11F /* OpenGroupInfo.swift */,
C3AAFFCB25AE92150089E6DD /* OpenGroupManager.swift */,
C3A721342558BDF90043A11F /* OpenGroupMessage.swift */,
C3A7229B2558E4310043A11F /* OpenGroupMessage+Conversion.swift */,
);
path = V1;
sourceTree = "<group>";
};
C328252E25CA54F70062D0A7 /* Context Menu */ = {
isa = PBXGroup;
children = (
@ -2532,7 +2489,6 @@
isa = PBXGroup;
children = (
C33FDB34255A580B00E217F9 /* ClosedGroupPoller.swift */,
C33FDA8C255A57FD00E217F9 /* OpenGroupPoller.swift */,
C3DB66C2260ACCE6001EFC55 /* OpenGroupPollerV2.swift */,
C33FDB3A255A580B00E217F9 /* Poller.swift */,
);
@ -3138,8 +3094,11 @@
C3A721332558BDDF0043A11F /* Open Groups */ = {
isa = PBXGroup;
children = (
C3228005260AAD7E006EA627 /* V1 */,
C3227FF4260AAD58006EA627 /* V2 */,
C3DB6694260AC923001EFC55 /* OpenGroupV2.swift */,
B88FA7B726045D100049422F /* OpenGroupAPIV2.swift */,
C3DB66CB260AF1F3001EFC55 /* OpenGroupAPIV2+ObjC.swift */,
C3DB66AB260ACA42001EFC55 /* OpenGroupManagerV2.swift */,
C3227FF5260AAD66006EA627 /* OpenGroupMessageV2.swift */,
);
path = "Open Groups";
sourceTree = "<group>";
@ -3147,7 +3106,6 @@
C3A7215C2558C0AC0043A11F /* File Server */ = {
isa = PBXGroup;
children = (
C3A7218F2558C0CD0043A11F /* FileServerAPI.swift */,
B87EF17026367CF800124B3C /* FileServerAPIV2.swift */,
);
path = "File Server";
@ -3159,7 +3117,6 @@
C33FDB01255A580700E217F9 /* AppReadiness.h */,
C33FDB75255A581000E217F9 /* AppReadiness.m */,
C38EF309255B6DBE007E1867 /* DeviceSleepManager.swift */,
C3A722292558C1E40043A11F /* DotNetAPI.swift */,
C37F53E8255BA9BB002AEA92 /* Environment.h */,
C37F5402255BA9ED002AEA92 /* Environment.m */,
C3BBE0C62554F1570050F1E3 /* FixedWidthInteger+BigEndian.swift */,
@ -4672,8 +4629,6 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
C3A7229C2558E4310043A11F /* OpenGroupMessage+Conversion.swift in Sources */,
C32C5DBE256DD743003C73A2 /* OpenGroupPoller.swift in Sources */,
B8856D08256F10F1001CE70E /* DeviceSleepManager.swift in Sources */,
C3471F4C25553AB000297E91 /* MessageReceiver+Decryption.swift in Sources */,
C300A5D32554B05A00555489 /* TypingIndicator.swift in Sources */,
@ -4698,10 +4653,8 @@
C300A5FC2554B0A000555489 /* MessageReceiver.swift in Sources */,
C32C5A76256DBBCF003C73A2 /* SignalAttachment.swift in Sources */,
C32C5CA4256DD1DC003C73A2 /* TSAccountManager.m in Sources */,
C3A7213B2558BDFA0043A11F /* OpenGroup.swift in Sources */,
C352A3892557876500338F3E /* JobQueue.swift in Sources */,
C3BBE0B52554F0E10050F1E3 /* ProofOfWork.swift in Sources */,
C3A721902558C0CD0043A11F /* FileServerAPI.swift in Sources */,
C32C59C1256DB41F003C73A2 /* TSGroupThread.m in Sources */,
C3A3A08F256E1728004D228D /* FullTextSearchFinder.swift in Sources */,
B8856D1A256F114D001CE70E /* ProximityMonitoringManager.swift in Sources */,
@ -4722,12 +4675,10 @@
C32C5DC0256DD743003C73A2 /* Poller.swift in Sources */,
C3C2A7682553A3D900C340D1 /* VisibleMessage+Contact.swift in Sources */,
C3A3A171256E1D25004D228D /* SSKReachabilityManager.swift in Sources */,
C3A721392558BDFA0043A11F /* OpenGroupAPI.swift in Sources */,
C3A71D0B2558989C0043A11F /* MessageWrapper.swift in Sources */,
B8F5F60325EDE16F003BF8D4 /* DataExtractionNotification.swift in Sources */,
C32C5D24256DD4C0003C73A2 /* MentionsManager.swift in Sources */,
C3A71D1E25589AC30043A11F /* WebSocketProto.swift in Sources */,
C3A721382558BDFA0043A11F /* OpenGroupMessage.swift in Sources */,
C3C2A7852553AAF300C340D1 /* SessionProtos.pb.swift in Sources */,
B8566C63256F55930045A0B9 /* OWSLinkPreview+Conversion.swift in Sources */,
C32C5B3F256DC1DF003C73A2 /* TSQuotedMessage+Conversion.swift in Sources */,
@ -4746,7 +4697,6 @@
C32C5EDC256DF501003C73A2 /* YapDatabaseConnection+OWS.m in Sources */,
C3BBE0762554CDA60050F1E3 /* Configuration.swift in Sources */,
B8B32033258B235D0020074B /* Storage+Contacts.swift in Sources */,
B8CADAE925AFADF400AAFA15 /* OpenGroupManager.swift in Sources */,
B8856D69256F141F001CE70E /* OWSWindowManager.m in Sources */,
C3D9E3BE25676AD70040E4F3 /* TSAttachmentPointer.m in Sources */,
C3ECBF7B257056B700EA7FCE /* Threading.swift in Sources */,
@ -4761,7 +4711,6 @@
C32C599E256DB02B003C73A2 /* TypingIndicators.swift in Sources */,
C3DB66CC260AF1F3001EFC55 /* OpenGroupAPIV2+ObjC.swift in Sources */,
C32C5BEF256DC8EE003C73A2 /* OWSDisappearingMessagesJob.m in Sources */,
C3A7222A2558C1E40043A11F /* DotNetAPI.swift in Sources */,
C34A977425A3E34A00852C71 /* ClosedGroupControlMessage.swift in Sources */,
B88FA7B826045D100049422F /* OpenGroupAPIV2.swift in Sources */,
C32C5E97256DE0CB003C73A2 /* OWSPrimaryStorage.m in Sources */,
@ -4795,7 +4744,6 @@
C32C5F11256DF79A003C73A2 /* SSKIncrementingIdFinder.swift in Sources */,
C32C5DBF256DD743003C73A2 /* ClosedGroupPoller.swift in Sources */,
C32C5EEE256DF54E003C73A2 /* TSDatabaseView.m in Sources */,
C3A7213A2558BDFA0043A11F /* OpenGroupInfo.swift in Sources */,
C352A35B2557824E00338F3E /* AttachmentUploadJob.swift in Sources */,
C3A3A13C256E1B27004D228D /* OWSMediaGalleryFinder.m in Sources */,
C32C5AAE256DBE8F003C73A2 /* TSInteraction.m in Sources */,

View File

@ -7,7 +7,7 @@ extension Storage {
let transaction = transaction as! YapDatabaseReadWriteTransaction
var threadOrNil: TSThread?
if let openGroupID = openGroupID {
if let threadID = Storage.shared.v2GetThreadID(for: openGroupID) ?? Storage.shared.getThreadID(for: openGroupID),
if let threadID = Storage.shared.v2GetThreadID(for: openGroupID),
let thread = TSGroupThread.fetch(uniqueId: threadID, transaction: transaction) {
threadOrNil = thread
}

View File

@ -49,26 +49,6 @@ extension Storage {
// MARK: - Quotes
@objc(getServerIDForQuoteWithID:quoteeHexEncodedPublicKey:threadID:transaction:)
public func getServerID(quoteID: UInt64, quoteeHexEncodedPublicKey: String, threadID: String, transaction: YapDatabaseReadTransaction) -> UInt64 {
guard let message = TSInteraction.interactions(withTimestamp: quoteID, filter: { interaction in
let senderPublicKey: String
if let message = interaction as? TSIncomingMessage {
senderPublicKey = message.authorId
} else if interaction is TSOutgoingMessage {
senderPublicKey = getUserHexEncodedPublicKey()
} else {
return false
}
return (senderPublicKey == quoteeHexEncodedPublicKey) && (interaction.uniqueThreadId == threadID)
}, with: transaction).first as! TSMessage? else { return 0 }
return message.openGroupServerMessageID
}
// MARK: - Authorization
private static let authTokenCollection = "SNAuthTokenCollection"
@ -205,10 +185,6 @@ extension Storage {
(transaction as! YapDatabaseReadWriteTransaction).setObject(messageID, forKey: String(serverID), inCollection: Storage.openGroupMessageIDCollection)
}
public func setLastProfilePictureUploadDate(_ date: Date) {
UserDefaults.standard[.lastProfilePictureUpload] = date
}
public func getOpenGroupImage(for room: String, on server: String) -> Data? {
var result: Data?
Storage.read { transaction in
@ -220,148 +196,4 @@ extension Storage {
public func setOpenGroupImage(to data: Data, for room: String, on server: String, using transaction: Any) {
(transaction as! YapDatabaseReadWriteTransaction).setObject(data, forKey: "\(server).\(room)", inCollection: Storage.openGroupImageCollection)
}
// MARK: - Deprecated
private static let oldOpenGroupUserCountCollection = "LokiPublicChatUserCountCollection"
public func getUserCount(forOpenGroupWithID openGroupID: String) -> Int? {
var result: Int?
Storage.read { transaction in
result = transaction.object(forKey: openGroupID, inCollection: Storage.oldOpenGroupUserCountCollection) as? Int
}
return result
}
public func setUserCount(to newValue: Int, forOpenGroupWithID openGroupID: String, using transaction: Any) {
let transaction = transaction as! YapDatabaseReadWriteTransaction
transaction.setObject(newValue, forKey: openGroupID, inCollection: Storage.oldOpenGroupUserCountCollection)
transaction.addCompletionQueue(.main) {
NotificationCenter.default.post(name: .groupThreadUpdated, object: nil)
}
}
private static let oldOpenGroupCollection = "LokiPublicChatCollection"
@objc public func getAllUserOpenGroups() -> [String:OpenGroup] {
var result = [String:OpenGroup]()
Storage.read { transaction in
transaction.enumerateKeysAndObjects(inCollection: Storage.oldOpenGroupCollection) { threadID, object, _ in
guard let openGroup = object as? OpenGroup else { return }
result[threadID] = openGroup
}
}
return result
}
@objc(getOpenGroupForThreadID:)
public func getOpenGroup(for threadID: String) -> OpenGroup? {
var result: OpenGroup?
Storage.read { transaction in
result = transaction.object(forKey: threadID, inCollection: Storage.oldOpenGroupCollection) as? OpenGroup
}
return result
}
public func getThreadID(for openGroupID: String) -> String? {
var result: String?
Storage.read { transaction in
transaction.enumerateKeysAndObjects(inCollection: Storage.oldOpenGroupCollection, using: { threadID, object, stop in
guard let openGroup = object as? OpenGroup, "\(openGroup.server).\(openGroup.channel)" == openGroupID else { return }
result = threadID
stop.pointee = true
})
}
return result
}
@objc(setOpenGroup:forThreadWithID:using:)
public func setOpenGroup(_ openGroup: OpenGroup, for threadID: String, using transaction: Any) {
(transaction as! YapDatabaseReadWriteTransaction).setObject(openGroup, forKey: threadID, inCollection: Storage.oldOpenGroupCollection)
}
@objc(removeOpenGroupForThreadID:using:)
public func removeOpenGroup(for threadID: String, using transaction: Any) {
(transaction as! YapDatabaseReadWriteTransaction).removeObject(forKey: threadID, inCollection: Storage.oldOpenGroupCollection)
}
private static func getAuthTokenCollection(for server: String) -> String {
return (server == FileServerAPI.server) ? "LokiStorageAuthTokenCollection" : "LokiGroupChatAuthTokenCollection"
}
public func getAuthToken(for server: String) -> String? {
let collection = Storage.getAuthTokenCollection(for: server)
var result: String? = nil
Storage.read { transaction in
result = transaction.object(forKey: server, inCollection: collection) as? String
}
return result
}
public func setAuthToken(for server: String, to newValue: String, using transaction: Any) {
let collection = Storage.getAuthTokenCollection(for: server)
(transaction as! YapDatabaseReadWriteTransaction).setObject(newValue, forKey: server, inCollection: collection)
}
public func removeAuthToken(for server: String, using transaction: Any) {
let collection = Storage.getAuthTokenCollection(for: server)
(transaction as! YapDatabaseReadWriteTransaction).removeObject(forKey: server, inCollection: collection)
}
public static let oldLastMessageServerIDCollection = "LokiGroupChatLastMessageServerIDCollection"
public func getLastMessageServerID(for group: UInt64, on server: String) -> UInt64? {
var result: UInt64? = nil
Storage.read { transaction in
result = transaction.object(forKey: "\(server).\(group)", inCollection: Storage.oldLastMessageServerIDCollection) as? UInt64
}
return result
}
public func setLastMessageServerID(for group: UInt64, on server: String, to newValue: UInt64, using transaction: Any) {
(transaction as! YapDatabaseReadWriteTransaction).setObject(newValue, forKey: "\(server).\(group)", inCollection: Storage.oldLastMessageServerIDCollection)
}
public func removeLastMessageServerID(for group: UInt64, on server: String, using transaction: Any) {
(transaction as! YapDatabaseReadWriteTransaction).removeObject(forKey: "\(server).\(group)", inCollection: Storage.oldLastMessageServerIDCollection)
}
public static let oldLastDeletionServerIDCollection = "LokiGroupChatLastDeletionServerIDCollection"
public func getLastDeletionServerID(for group: UInt64, on server: String) -> UInt64? {
var result: UInt64? = nil
Storage.read { transaction in
result = transaction.object(forKey: "\(server).\(group)", inCollection: Storage.oldLastDeletionServerIDCollection) as? UInt64
}
return result
}
public func setLastDeletionServerID(for group: UInt64, on server: String, to newValue: UInt64, using transaction: Any) {
(transaction as! YapDatabaseReadWriteTransaction).setObject(newValue, forKey: "\(server).\(group)", inCollection: Storage.oldLastDeletionServerIDCollection)
}
public func removeLastDeletionServerID(for group: UInt64, on server: String, using transaction: Any) {
(transaction as! YapDatabaseReadWriteTransaction).removeObject(forKey: "\(server).\(group)", inCollection: Storage.oldLastDeletionServerIDCollection)
}
public func setOpenGroupDisplayName(to displayName: String, for publicKey: String, inOpenGroupWithID openGroupID: String, using transaction: Any) {
let collection = openGroupID
(transaction as! YapDatabaseReadWriteTransaction).setObject(displayName, forKey: publicKey, inCollection: collection)
}
private static let openGroupProfilePictureURLCollection = "LokiPublicChatAvatarURLCollection"
public func getProfilePictureURL(forOpenGroupWithID openGroupID: String) -> String? {
var result: String?
Storage.read { transaction in
result = transaction.object(forKey: openGroupID, inCollection: Storage.openGroupProfilePictureURLCollection) as? String
}
return result
}
public func setProfilePictureURL(to profilePictureURL: String?, forOpenGroupWithID openGroupID: String, using transaction: Any) {
(transaction as! YapDatabaseReadWriteTransaction).setObject(profilePictureURL, forKey: openGroupID, inCollection: Storage.openGroupProfilePictureURLCollection)
}
}

View File

@ -1,76 +0,0 @@
import AFNetworking
import PromiseKit
import SessionSnodeKit
import SessionUtilitiesKit
@objc(SNFileServerAPI)
public final class FileServerAPI : DotNetAPI {
// MARK: Settings
private static let attachmentType = "net.app.core.oembed"
private static let deviceLinkType = "network.loki.messenger.devicemapping"
internal static let publicKey = "62509D59BDEEC404DD0D489C1E15BA8F94FD3D619B01C1BF48A9922BFCB7311C"
public static let maxFileSize = 10_000_000 // 10 MB
/// The file server has a file size limit of `maxFileSize`, which the Service Nodes try to enforce as well. However, the limit applied by the Service Nodes
/// is on the **HTTP request** and not the actual file size. Because the file server expects the file data to be base 64 encoded, the size of the HTTP
/// request for a given file will be at least `ceil(n / 3) * 4` bytes, where n is the file size in bytes. This is the minimum size because there might also
/// be other parameters in the request. On average the multiplier appears to be about 1.5, so when checking whether the file will exceed the file size limit when
/// uploading a file we just divide the size of the file by this number. The alternative would be to actually check the size of the HTTP request but that's only
/// possible after proof of work has been calculated and the onion request encryption has happened, which takes several seconds.
public static let fileSizeORMultiplier: Double = 2
@objc public static let server = "https://file.getsession.org"
@objc public static let fileStorageBucketURL = "https://file-static.lokinet.org"
// MARK: Profile Pictures
@objc(uploadProfilePicture:)
public static func objc_uploadProfilePicture(_ profilePicture: Data) -> AnyPromise {
return AnyPromise.from(uploadProfilePicture(profilePicture))
}
public static func uploadProfilePicture(_ profilePicture: Data) -> Promise<String> {
guard Double(profilePicture.count) < Double(maxFileSize) / fileSizeORMultiplier else { return Promise(error: Error.maxFileSizeExceeded) }
let url = "\(server)/files"
let parameters: JSON = [ "type" : attachmentType, "Content-Type" : "application/binary" ]
var error: NSError?
let request = AFHTTPRequestSerializer().multipartFormRequest(withMethod: "POST", urlString: url, parameters: parameters, constructingBodyWith: { formData in
formData.appendPart(withFileData: profilePicture, name: "content", fileName: UUID().uuidString, mimeType: "application/binary")
}, error: &error)
// Uploads to the Loki File Server shouldn't include any personally identifiable information so use a dummy auth token
request.addValue("Bearer loki", forHTTPHeaderField: "Authorization")
if let error = error {
SNLog("Couldn't upload profile picture due to error: \(error).")
return Promise(error: error)
}
return OnionRequestAPI.sendOnionRequest(request, to: server, using: publicKey).map(on: DispatchQueue.global(qos: .userInitiated)) { json in
guard let data = json["data"] as? JSON, let downloadURL = data["url"] as? String else {
SNLog("Couldn't parse profile picture from: \(json).")
throw Error.parsingFailed
}
SNMessagingKitConfiguration.shared.storage.setLastProfilePictureUploadDate(Date())
return downloadURL
}
}
// MARK: Open Group Server Public Key
public static func getPublicKey(for openGroupServer: String) -> Promise<String> {
guard let host = URL(string: openGroupServer)?.host,
let url = URL(string: "\(server)/loki/v1/getOpenGroupKey/\(host)") else { return Promise(error: DotNetAPI.Error.invalidURL) }
let request = TSRequest(url: url)
let token = "loki" // Tokenless request; use a dummy token
request.allHTTPHeaderFields = [ "Content-Type" : "application/json", "Authorization" : "Bearer \(token)" ]
return OnionRequestAPI.sendOnionRequest(request, to: server, using: publicKey).map(on: DispatchQueue.global(qos: .userInitiated)) { json in
guard let bodyAsString = json["data"] as? String, let bodyAsData = bodyAsString.data(using: .utf8),
let body = try JSONSerialization.jsonObject(with: bodyAsData, options: [ .fragmentsAllowed ]) as? JSON else { throw HTTP.Error.invalidJSON }
guard let base64EncodedPublicKey = body["data"] as? String else {
SNLog("Couldn't parse open group public key from: \(body).")
throw Error.parsingFailed
}
let prefixedPublicKey = Data(base64Encoded: base64EncodedPublicKey)!
let hexEncodedPrefixedPublicKey = prefixedPublicKey.toHexString()
return hexEncodedPrefixedPublicKey.removing05PrefixIfNeeded()
}
}
}

View File

@ -4,20 +4,32 @@ import SessionSnodeKit
@objc(SNFileServerAPIV2)
public final class FileServerAPIV2 : NSObject {
// MARK: Settings
@objc public static let server = "http://88.99.175.227"
public static let serverPublicKey = "7cb31905b55cd5580c686911debf672577b3fb0bff81df4ce2d5c4cb3a7aaa69"
public static let maxFileSize = 10_000_000 // 10 MB
/// The file server has a file size limit of `maxFileSize`, which the Service Nodes try to enforce as well. However, the limit applied by the Service Nodes
/// is on the **HTTP request** and not the actual file size. Because the file server expects the file data to be base 64 encoded, the size of the HTTP
/// request for a given file will be at least `ceil(n / 3) * 4` bytes, where n is the file size in bytes. This is the minimum size because there might also
/// be other parameters in the request. On average the multiplier appears to be about 1.5, so when checking whether the file will exceed the file size limit when
/// uploading a file we just divide the size of the file by this number. The alternative would be to actually check the size of the HTTP request but that's only
/// possible after proof of work has been calculated and the onion request encryption has happened, which takes several seconds.
public static let fileSizeORMultiplier: Double = 2
// MARK: Initialization
private override init() { }
// MARK: Error
public enum Error : LocalizedError {
case parsingFailed
case invalidURL
case maxFileSizeExceeded
public var errorDescription: String? {
switch self {
case .parsingFailed: return "Invalid response."
case .invalidURL: return "Invalid URL."
case .maxFileSizeExceeded: return "Maximum file size exceeded."
}
}
}

View File

@ -80,13 +80,6 @@ public final class AttachmentDownloadJob : NSObject, Job, NSCoding { // NSObject
storage.setAttachmentState(to: .failed, for: pointer, associatedWith: self.tsMessageID, using: transaction)
}, completion: { })
self.handlePermanentFailure(error: error)
} else if let error = error as? DotNetAPI.Error, case .parsingFailed = error {
// No need to retry if the response is invalid. Most likely this means we (incorrectly)
// got a "Cannot GET ..." error from the file server.
storage.write(with: { transaction in
storage.setAttachmentState(to: .failed, for: pointer, associatedWith: self.tsMessageID, using: transaction)
}, completion: { })
self.handlePermanentFailure(error: error)
} else {
self.handleFailure(error: error)
}
@ -100,7 +93,7 @@ public final class AttachmentDownloadJob : NSObject, Job, NSCoding { // NSObject
}.catch(on: DispatchQueue.global()) { error in
handleFailure(error)
}
} else if pointer.downloadURL.contains(FileServerAPIV2.server) {
} else {
guard let fileAsString = pointer.downloadURL.split(separator: "/").last, let file = UInt64(fileAsString) else {
return handleFailure(Error.invalidURL)
}
@ -109,12 +102,6 @@ public final class AttachmentDownloadJob : NSObject, Job, NSCoding { // NSObject
}.catch(on: DispatchQueue.global()) { error in
handleFailure(error)
}
} else { // Legacy
FileServerAPI.downloadAttachment(from: pointer.downloadURL).done(on: DispatchQueue.global(qos: .userInitiated)) { data in
self.handleDownloadedAttachment(data: data, temporaryFilePath: temporaryFilePath, pointer: pointer, failureHandler: handleFailure)
}.catch(on: DispatchQueue.global()) { error in
handleFailure(error)
}
}
}

View File

@ -67,23 +67,8 @@ public final class AttachmentUploadJob : NSObject, Job, NSCoding { // NSObject/N
let storage = SNMessagingKitConfiguration.shared.storage
if let v2OpenGroup = storage.getV2OpenGroup(for: threadID) {
AttachmentUploadJob.upload(stream, using: { data in return OpenGroupAPIV2.upload(data, to: v2OpenGroup.room, on: v2OpenGroup.server) }, encrypt: false, onSuccess: handleSuccess, onFailure: handleFailure)
} else if Features.useV2FileServer && storage.getOpenGroup(for: threadID) == nil {
} else {
AttachmentUploadJob.upload(stream, using: FileServerAPIV2.upload, encrypt: true, onSuccess: handleSuccess, onFailure: handleFailure)
} else { // Legacy
let openGroup = storage.getOpenGroup(for: threadID)
let server = openGroup?.server ?? FileServerAPI.server
// FIXME: A lot of what's currently happening in FileServerAPI should really be happening here
FileServerAPI.uploadAttachment(stream, with: attachmentID, to: server).done(on: DispatchQueue.global(qos: .userInitiated)) { // Intentionally capture self
self.handleSuccess()
}.catch(on: DispatchQueue.global(qos: .userInitiated)) { error in
if let error = error as? Error, case .noAttachment = error {
self.handlePermanentFailure(error: error)
} else if let error = error as? DotNetAPI.Error, !error.isRetryable {
self.handlePermanentFailure(error: error)
} else {
self.handleFailure(error: error)
}
}
}
}
@ -107,8 +92,8 @@ public final class AttachmentUploadJob : NSObject, Job, NSCoding { // NSObject/N
}
// Check the file size
SNLog("File size: \(data.count) bytes.")
if Double(data.count) > Double(FileServerAPI.maxFileSize) / FileServerAPI.fileSizeORMultiplier {
onFailure?(FileServerAPI.Error.maxFileSizeExceeded); return
if Double(data.count) > Double(FileServerAPIV2.maxFileSize) / FileServerAPIV2.fileSizeORMultiplier {
onFailure?(FileServerAPIV2.Error.maxFileSizeExceeded); return
}
// Send the request
stream.isUploaded = false

View File

@ -15,12 +15,8 @@ public extension Message {
let groupPublicKey = LKGroupUtilities.getDecodedGroupID(groupID)
return .closedGroup(groupPublicKey: groupPublicKey)
} else if let thread = thread as? TSGroupThread, thread.isOpenGroup {
if let openGroupV2 = Storage.shared.getV2OpenGroup(for: thread.uniqueId!) {
return .openGroupV2(room: openGroupV2.room, server: openGroupV2.server)
} else {
let openGroup = Storage.shared.getOpenGroup(for: thread.uniqueId!)!
return .openGroup(channel: openGroup.channel, server: openGroup.server)
}
let openGroupV2 = Storage.shared.getV2OpenGroup(for: thread.uniqueId!)!
return .openGroupV2(room: openGroupV2.room, server: openGroupV2.server)
} else {
preconditionFailure("TODO: Handle legacy closed groups.")
}

View File

@ -1,43 +0,0 @@
@objc(SNOpenGroup)
public final class OpenGroup : NSObject, NSCoding {
@objc public let id: String
@objc public let idAsData: Data
@objc public let channel: UInt64
@objc public let server: String
@objc public let displayName: String
@objc public let isDeletable: Bool
@objc public init?(channel: UInt64, server: String, displayName: String, isDeletable: Bool) {
let id = "\(server).\(channel)"
self.id = id
guard let idAsData = id.data(using: .utf8) else { return nil }
self.idAsData = idAsData
self.channel = channel
self.server = server.lowercased()
self.displayName = displayName
self.isDeletable = isDeletable
}
// MARK: Coding
@objc public init?(coder: NSCoder) {
channel = UInt64(coder.decodeInt64(forKey: "channel"))
server = coder.decodeObject(forKey: "server") as! String
let id = "\(server).\(channel)"
self.id = id
guard let idAsData = id.data(using: .utf8) else { return nil }
self.idAsData = idAsData
displayName = coder.decodeObject(forKey: "displayName") as! String
isDeletable = coder.decodeBool(forKey: "isDeletable")
super.init()
}
@objc public func encode(with coder: NSCoder) {
coder.encode(Int64(channel), forKey: "channel")
coder.encode(server, forKey: "server")
coder.encode(displayName, forKey: "displayName")
coder.encode(isDeletable, forKey: "isDeletable")
}
override public var description: String { "\(displayName) (\(server))" }
}

View File

@ -1,519 +0,0 @@
import AFNetworking
import PromiseKit
import SessionSnodeKit
import SessionUtilitiesKit
@objc(SNOpenGroupAPI)
public final class OpenGroupAPI : DotNetAPI {
private static var moderators: [String:[UInt64:Set<String>]] = [:] // Server URL to (channel ID to set of moderator IDs)
public static var displayNameUpdatees: [String:Set<String>] = [:]
// MARK: Settings
private static let attachmentType = "net.app.core.oembed"
private static let channelInfoType = "net.patter-app.settings"
private static let fallbackBatchCount = 64
private static let maxRetryCount: UInt = 4
public static let profilePictureType = "network.loki.messenger.avatar"
@objc public static let openGroupMessageType = "network.loki.messenger.publicChat"
// MARK: Open Group Public Key Validation
public static func getOpenGroupServerPublicKey(for server: String) -> Promise<String> {
if let publicKey = SNMessagingKitConfiguration.shared.storage.getOpenGroupPublicKey(for: server) {
return Promise.value(publicKey)
} else {
return FileServerAPI.getPublicKey(for: server).then(on: DispatchQueue.global(qos: .default)) { publicKey -> Promise<String> in
let url = URL(string: server)!
let request = TSRequest(url: url)
return OnionRequestAPI.sendOnionRequest(request, to: server, using: publicKey, isJSONRequired: false).map(on: DispatchQueue.global(qos: .default)) { _ -> String in
SNMessagingKitConfiguration.shared.storage.writeSync { transaction in
SNMessagingKitConfiguration.shared.storage.setOpenGroupPublicKey(for: server, to: publicKey, using: transaction)
}
return publicKey
}
}
}
}
// MARK: Receiving
@objc(getMessagesForGroup:onServer:)
public static func objc_getMessages(for group: UInt64, on server: String) -> AnyPromise {
return AnyPromise.from(getMessages(for: group, on: server))
}
public static func getMessages(for channel: UInt64, on server: String) -> Promise<[OpenGroupMessage]> {
let storage = SNMessagingKitConfiguration.shared.storage
var queryParameters = "include_annotations=1"
if let lastMessageServerID = storage.getLastMessageServerID(for: channel, on: server) {
queryParameters += "&since_id=\(lastMessageServerID)"
} else {
queryParameters += "&count=\(fallbackBatchCount)&include_deleted=0"
}
return getOpenGroupServerPublicKey(for: server).then(on: DispatchQueue.global(qos: .default)) { serverPublicKey in
getAuthToken(for: server).then(on: DispatchQueue.global(qos: .default)) { token -> Promise<[OpenGroupMessage]> in
let url = URL(string: "\(server)/channels/\(channel)/messages?\(queryParameters)")!
let request = TSRequest(url: url)
request.allHTTPHeaderFields = [ "Content-Type" : "application/json", "Authorization" : "Bearer \(token)" ]
return OnionRequestAPI.sendOnionRequest(request, to: server, using: serverPublicKey).map(on: DispatchQueue.global(qos: .default)) { json in
guard let rawMessages = json["data"] as? [JSON] else {
SNLog("Couldn't parse messages for open group channel with ID: \(channel) on server: \(server) from: \(json).")
throw Error.parsingFailed
}
return rawMessages.compactMap { message in
let isDeleted = (message["is_deleted"] as? Int == 1)
guard !isDeleted else { return nil }
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZ"
guard let annotations = message["annotations"] as? [JSON], let annotation = annotations.first(where: { $0["type"] as? String == openGroupMessageType }), let value = annotation["value"] as? JSON,
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, let dateAsString = message["created_at"] as? String, let date = dateFormatter.date(from: dateAsString) else {
SNLog("Couldn't parse message for open group channel with ID: \(channel) on server: \(server) from: \(message).")
return nil
}
let serverTimestamp = UInt64(date.timeIntervalSince1970) * 1000
var profilePicture: OpenGroupMessage.ProfilePicture? = nil
let displayName = user["name"] as? String ?? NSLocalizedString("Anonymous", comment: "")
if let userAnnotations = user["annotations"] as? [JSON], let profilePictureAnnotation = userAnnotations.first(where: { $0["type"] as? String == profilePictureType }),
let profilePictureValue = profilePictureAnnotation["value"] as? JSON, let profileKeyString = profilePictureValue["profileKey"] as? String, let profileKey = Data(base64Encoded: profileKeyString), let url = profilePictureValue["url"] as? String {
profilePicture = OpenGroupMessage.ProfilePicture(profileKey: profileKey, url: url)
}
let lastMessageServerID = storage.getLastMessageServerID(for: channel, on: server)
if serverID > (lastMessageServerID ?? 0) {
storage.writeSync { transaction in
storage.setLastMessageServerID(for: channel, on: server, to: serverID, using: transaction)
}
}
let quote: OpenGroupMessage.Quote?
if let quoteAsJSON = value["quote"] as? JSON, let quotedMessageTimestamp = quoteAsJSON["id"] as? UInt64, let quoteePublicKey = quoteAsJSON["author"] as? String,
let quotedMessageBody = quoteAsJSON["text"] as? String {
let quotedMessageServerID = message["reply_to"] as? UInt64
quote = OpenGroupMessage.Quote(quotedMessageTimestamp: quotedMessageTimestamp, quoteePublicKey: quoteePublicKey, quotedMessageBody: quotedMessageBody,
quotedMessageServerID: quotedMessageServerID)
} else {
quote = nil
}
let signature = OpenGroupMessage.Signature(data: Data(hex: hexEncodedSignatureData), version: signatureVersion)
let attachmentsAsJSON = annotations.filter { $0["type"] as? String == attachmentType }
let attachments: [OpenGroupMessage.Attachment] = attachmentsAsJSON.compactMap { attachmentAsJSON in
guard let value = attachmentAsJSON["value"] as? JSON, let kindAsString = value["lokiType"] as? String, let kind = OpenGroupMessage.Attachment.Kind(rawValue: kindAsString),
let serverID = value["id"] as? UInt64, let contentType = value["contentType"] as? String, let size = value["size"] as? UInt, let url = value["url"] as? String else { return nil }
let fileName = value["fileName"] as? String ?? UUID().description
let width = value["width"] as? UInt ?? 0
let height = value["height"] as? UInt ?? 0
let flags = (value["flags"] as? UInt) ?? 0
let caption = value["caption"] as? String
let linkPreviewURL = value["linkPreviewUrl"] as? String
let linkPreviewTitle = value["linkPreviewTitle"] as? String
if kind == .linkPreview {
guard linkPreviewURL != nil && linkPreviewTitle != nil else {
SNLog("Ignoring open group message with invalid link preview.")
return nil
}
}
return OpenGroupMessage.Attachment(kind: kind, server: server, serverID: serverID, contentType: contentType, size: size, fileName: fileName, flags: flags,
width: width, height: height, caption: caption, url: url, linkPreviewURL: linkPreviewURL, linkPreviewTitle: linkPreviewTitle)
}
let result = OpenGroupMessage(serverID: serverID, senderPublicKey: hexEncodedPublicKey, displayName: displayName, profilePicture: profilePicture,
body: body, type: openGroupMessageType, timestamp: timestamp, quote: quote, attachments: attachments, signature: signature, serverTimestamp: serverTimestamp)
guard result.hasValidSignature() else {
SNLog("Ignoring open group message with invalid signature.")
return nil
}
let existingMessageID = storage.getIDForMessage(withServerID: result.serverID!)
guard existingMessageID == nil else {
SNLog("Ignoring duplicate open group message.")
return nil
}
return result
}.sorted { $0.serverTimestamp < $1.serverTimestamp}
}
}
}.handlingInvalidAuthTokenIfNeeded(for: server)
}
// MARK: Sending
@objc(sendMessage:toGroup:onServer:)
public static func objc_sendMessage(_ message: OpenGroupMessage, to group: UInt64, on server: String) -> AnyPromise {
return AnyPromise.from(sendMessage(message, to: group, on: server))
}
public static func sendMessage(_ message: OpenGroupMessage, to channel: UInt64, on server: String) -> Promise<OpenGroupMessage> {
SNLog("Sending message to open group channel with ID: \(channel) on server: \(server).")
let storage = SNMessagingKitConfiguration.shared.storage
guard let userKeyPair = storage.getUserKeyPair(),
let userName = storage.getUser()?.name else { return Promise(error: Error.generic) }
let (promise, seal) = Promise<OpenGroupMessage>.pending()
DispatchQueue.global(qos: .userInitiated).async { [privateKey = userKeyPair.privateKey] in
guard let signedMessage = message.sign(with: privateKey) else { return seal.reject(Error.signingFailed) }
attempt(maxRetryCount: maxRetryCount, recoveringOn: DispatchQueue.global(qos: .default)) {
getOpenGroupServerPublicKey(for: server).then(on: DispatchQueue.global(qos: .default)) { serverPublicKey in
getAuthToken(for: server).then(on: DispatchQueue.global(qos: .default)) { token -> Promise<OpenGroupMessage> in
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)" ]
let userName = userName
return OnionRequestAPI.sendOnionRequest(request, to: server, using: serverPublicKey).map(on: DispatchQueue.global(qos: .default)) { json in
// ISO8601DateFormatter doesn't support milliseconds before iOS 11
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZ"
guard 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 {
SNLog("Couldn't parse message for open group channel with ID: \(channel) on server: \(server) from: \(json).")
throw Error.parsingFailed
}
let timestamp = UInt64(date.timeIntervalSince1970) * 1000
return OpenGroupMessage(serverID: serverID, senderPublicKey: userKeyPair.publicKey.toHexString(), displayName: userName, profilePicture: signedMessage.profilePicture, body: body, type: openGroupMessageType, timestamp: timestamp, quote: signedMessage.quote, attachments: signedMessage.attachments, signature: signedMessage.signature, serverTimestamp: timestamp)
}
}
}.handlingInvalidAuthTokenIfNeeded(for: server)
}.done(on: DispatchQueue.global(qos: .default)) { message in
seal.fulfill(message)
}.catch(on: DispatchQueue.global(qos: .default)) { error in
seal.reject(error)
}
}
return promise
}
// MARK: Deletion
public static func getDeletedMessageServerIDs(for channel: UInt64, on server: String) -> Promise<[UInt64]> {
SNLog("Getting deleted messages for open group channel with ID: \(channel) on server: \(server).")
let storage = SNMessagingKitConfiguration.shared.storage
let queryParameters: String
if let lastDeletionServerID = storage.getLastDeletionServerID(for: channel, on: server) {
queryParameters = "since_id=\(lastDeletionServerID)"
} else {
queryParameters = "count=\(fallbackBatchCount)"
}
return getOpenGroupServerPublicKey(for: server).then(on: DispatchQueue.global(qos: .default)) { serverPublicKey in
getAuthToken(for: server).then(on: DispatchQueue.global(qos: .default)) { token -> Promise<[UInt64]> in
let url = URL(string: "\(server)/loki/v1/channel/\(channel)/deletes?\(queryParameters)")!
let request = TSRequest(url: url)
request.allHTTPHeaderFields = [ "Content-Type" : "application/json", "Authorization" : "Bearer \(token)" ]
return OnionRequestAPI.sendOnionRequest(request, to: server, using: serverPublicKey).map(on: DispatchQueue.global(qos: .default)) { json in
guard let body = json["body"] as? JSON, let deletions = body["data"] as? [JSON] else {
SNLog("Couldn't parse deleted messages for open group channel with ID: \(channel) on server: \(server) from: \(json).")
throw Error.parsingFailed
}
return deletions.compactMap { deletion in
guard let serverID = deletion["id"] as? UInt64, let messageServerID = deletion["message_id"] as? UInt64 else {
SNLog("Couldn't parse deleted message for open group channel with ID: \(channel) on server: \(server) from: \(deletion).")
return nil
}
let lastDeletionServerID = storage.getLastDeletionServerID(for: channel, on: server)
if serverID > (lastDeletionServerID ?? 0) {
storage.writeSync { transaction in
storage.setLastDeletionServerID(for: channel, on: server, to: serverID, using: transaction)
}
}
return messageServerID
}
}
}
}.handlingInvalidAuthTokenIfNeeded(for: server)
}
@objc(deleteMessageWithID:forGroup:onServer:isSentByUser:)
public static func objc_deleteMessage(with messageID: UInt, for group: UInt64, on server: String, isSentByUser: Bool) -> AnyPromise {
return AnyPromise.from(deleteMessage(with: messageID, for: group, on: server, wasSentByUser: isSentByUser))
}
public static func deleteMessage(with messageID: UInt, for channel: UInt64, on server: String, wasSentByUser: Bool) -> Promise<Void> {
let isModerationRequest = !wasSentByUser
SNLog("Deleting message with ID: \(messageID) for open group channel with ID: \(channel) on server: \(server) (isModerationRequest = \(isModerationRequest)).")
let urlAsString = wasSentByUser ? "\(server)/channels/\(channel)/messages/\(messageID)" : "\(server)/loki/v1/moderation/message/\(messageID)"
return attempt(maxRetryCount: maxRetryCount, recoveringOn: DispatchQueue.global(qos: .default)) {
getOpenGroupServerPublicKey(for: server).then(on: DispatchQueue.global(qos: .default)) { serverPublicKey in
getAuthToken(for: server).then(on: DispatchQueue.global(qos: .default)) { token -> Promise<Void> in
let url = URL(string: urlAsString)!
let request = TSRequest(url: url, method: "DELETE", parameters: [:])
request.allHTTPHeaderFields = [ "Content-Type" : "application/json", "Authorization" : "Bearer \(token)" ]
return OnionRequestAPI.sendOnionRequest(request, to: server, using: serverPublicKey, isJSONRequired: false).done(on: DispatchQueue.global(qos: .default)) { _ -> Void in
SNLog("Deleted message with ID: \(messageID) on server: \(server).")
}
}
}.handlingInvalidAuthTokenIfNeeded(for: server)
}
}
// MARK: Banning
@objc(banPublicKey:fromServer:)
public static func objc_ban(_ publicKey: String, from server: String) -> AnyPromise {
return AnyPromise.from(ban(publicKey, from: server))
}
public static func ban(_ publicKey: String, from server: String) -> Promise<Void> {
SNLog("Banning user with ID: \(publicKey) from server: \(server).")
return attempt(maxRetryCount: maxRetryCount, recoveringOn: DispatchQueue.global(qos: .default)) {
getOpenGroupServerPublicKey(for: server).then(on: DispatchQueue.global(qos: .default)) { serverPublicKey in
getAuthToken(for: server).then(on: DispatchQueue.global(qos: .default)) { token -> Promise<Void> in
let url = URL(string: "\(server)/loki/v1/moderation/blacklist/@\(publicKey)")!
let request = TSRequest(url: url, method: "POST", parameters: [:])
request.allHTTPHeaderFields = [ "Content-Type" : "application/json", "Authorization" : "Bearer \(token)" ]
let promise = OnionRequestAPI.sendOnionRequest(request, to: server, using: serverPublicKey, isJSONRequired: false)
let _ = promise.done(on: DispatchQueue.global(qos: .default)) { _ -> Void in
SNLog("Banned user with ID: \(publicKey) from server: \(server).")
}
promise.catch(on: DispatchQueue.main) { error in
print(error)
}
return promise.map { _ in }
}
}.handlingInvalidAuthTokenIfNeeded(for: server)
}
}
// MARK: Display Name & Profile Picture
public static func getDisplayNames(for channel: UInt64, on server: String) -> Promise<Void> {
let openGroupID = "\(server).\(channel)"
guard let publicKeys = displayNameUpdatees[openGroupID] else { return Promise.value(()) }
displayNameUpdatees[openGroupID] = []
SNLog("Getting display names for: \(publicKeys).")
return getOpenGroupServerPublicKey(for: server).then(on: DispatchQueue.global(qos: .default)) { serverPublicKey in
getAuthToken(for: server).then(on: DispatchQueue.global(qos: .default)) { token -> Promise<Void> in
let queryParameters = "ids=\(publicKeys.map { "@\($0)" }.joined(separator: ","))&include_user_annotations=1"
let url = URL(string: "\(server)/users?\(queryParameters)")!
let request = TSRequest(url: url)
return OnionRequestAPI.sendOnionRequest(request, to: server, using: serverPublicKey).map(on: DispatchQueue.global(qos: .default)) { json in
guard let data = json["data"] as? [JSON] else {
SNLog("Couldn't parse display names for users: \(publicKeys) from: \(json).")
throw Error.parsingFailed
}
let storage = SNMessagingKitConfiguration.shared.storage
storage.writeSync { transaction in
data.forEach { data in
guard let user = data["user"] as? JSON, let hexEncodedPublicKey = user["username"] as? String, let rawDisplayName = user["name"] as? String else { return }
let endIndex = hexEncodedPublicKey.endIndex
let cutoffIndex = hexEncodedPublicKey.index(endIndex, offsetBy: -8)
let displayName = "\(rawDisplayName) (...\(hexEncodedPublicKey[cutoffIndex..<endIndex]))"
storage.setOpenGroupDisplayName(to: displayName, for: hexEncodedPublicKey, inOpenGroupWithID: "\(server).\(channel)", using: transaction)
}
}
}
}
}.handlingInvalidAuthTokenIfNeeded(for: server)
}
@objc(setDisplayName:on:)
public static func objc_setDisplayName(to newDisplayName: String?, on server: String) -> AnyPromise {
return AnyPromise.from(setDisplayName(to: newDisplayName, on: server))
}
public static func setDisplayName(to newDisplayName: String?, on server: String) -> Promise<Void> {
SNLog("Updating display name on server: \(server).")
let parameters: JSON = [ "name" : (newDisplayName ?? "") ]
return attempt(maxRetryCount: maxRetryCount, recoveringOn: DispatchQueue.global(qos: .default)) {
getOpenGroupServerPublicKey(for: server).then(on: DispatchQueue.global(qos: .default)) { serverPublicKey in
getAuthToken(for: server).then(on: DispatchQueue.global(qos: .default)) { token -> Promise<Void> in
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 OnionRequestAPI.sendOnionRequest(request, to: server, using: serverPublicKey).map(on: DispatchQueue.global(qos: .default)) { _ in }.recover(on: DispatchQueue.global(qos: .default)) { error in
print("Couldn't update display name due to error: \(error).")
throw error
}
}
}.handlingInvalidAuthTokenIfNeeded(for: server)
}
}
@objc(setProfilePictureURL:usingProfileKey:on:)
public static func objc_setProfilePicture(to url: String?, using profileKey: Data, on server: String) -> AnyPromise {
return AnyPromise.from(setProfilePictureURL(to: url, using: profileKey, on: server))
}
public static func setProfilePictureURL(to url: String?, using profileKey: Data, on server: String) -> Promise<Void> {
SNLog("Updating profile picture on server: \(server).")
var annotation: JSON = [ "type" : profilePictureType ]
if let url = url {
annotation["value"] = [ "profileKey" : profileKey.base64EncodedString(), "url" : url ]
}
let parameters: JSON = [ "annotations" : [ annotation ] ]
return attempt(maxRetryCount: maxRetryCount, recoveringOn: DispatchQueue.global(qos: .default)) {
getOpenGroupServerPublicKey(for: server).then(on: DispatchQueue.global(qos: .default)) { serverPublicKey in
getAuthToken(for: server).then(on: DispatchQueue.global(qos: .default)) { token -> Promise<Void> in
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 OnionRequestAPI.sendOnionRequest(request, to: server, using: serverPublicKey).map(on: DispatchQueue.global(qos: .default)) { _ in }.recover(on: DispatchQueue.global(qos: .default)) { error in
SNLog("Couldn't update profile picture due to error: \(error).")
throw error
}
}
}.handlingInvalidAuthTokenIfNeeded(for: server)
}
}
// MARK: Joining & Leaving
@objc(getInfoForChannelWithID:onServer:)
public static func objc_getInfo(for channel: UInt64, on server: String) -> AnyPromise {
return AnyPromise.from(getInfo(for: channel, on: server))
}
public static func getInfo(for channel: UInt64, on server: String) -> Promise<OpenGroupInfo> {
return attempt(maxRetryCount: maxRetryCount, recoveringOn: DispatchQueue.global(qos: .default)) {
getOpenGroupServerPublicKey(for: server).then(on: DispatchQueue.global(qos: .default)) { serverPublicKey in
getAuthToken(for: server).then(on: DispatchQueue.global(qos: .default)) { token -> Promise<OpenGroupInfo> in
let url = URL(string: "\(server)/channels/\(channel)?include_annotations=1")!
let request = TSRequest(url: url)
request.allHTTPHeaderFields = [ "Content-Type" : "application/json", "Authorization" : "Bearer \(token)" ]
return OnionRequestAPI.sendOnionRequest(request, to: server, using: serverPublicKey).map(on: DispatchQueue.global(qos: .default)) { json in
guard let data = json["data"] as? JSON,
let annotations = data["annotations"] as? [JSON],
let annotation = annotations.first,
let info = annotation["value"] as? JSON,
let displayName = info["name"] as? String,
let profilePictureURL = info["avatar"] as? String,
let countInfo = data["counts"] as? JSON,
let memberCount = countInfo["subscribers"] as? Int else {
SNLog("Couldn't parse info for open group channel with ID: \(channel) on server: \(server) from: \(json).")
throw Error.parsingFailed
}
let storage = SNMessagingKitConfiguration.shared.storage
storage.writeSync { transaction in
storage.setUserCount(to: memberCount, forOpenGroupWithID: "\(server).\(channel)", using: transaction)
}
let openGroupInfo = OpenGroupInfo(displayName: displayName, profilePictureURL: profilePictureURL, memberCount: memberCount)
OpenGroupAPI.updateProfileIfNeeded(for: channel, on: server, from: openGroupInfo)
return openGroupInfo
}
}
}.handlingInvalidAuthTokenIfNeeded(for: server)
}
}
public static func updateProfileIfNeeded(for channel: UInt64, on server: String, from info: OpenGroupInfo) {
let openGroupID = "\(server).\(channel)"
SNMessagingKitConfiguration.shared.storage.write { transaction in
let transaction = transaction as! YapDatabaseReadWriteTransaction
// Update user count
Storage.shared.setUserCount(to: info.memberCount, forOpenGroupWithID: openGroupID, using: transaction)
let thread = TSGroupThread.getOrCreateThread(withGroupId: openGroupID.data(using: .utf8)!, groupType: .openGroup, transaction: transaction)
// Update display name if needed
let model = thread.groupModel
if model.groupName != info.displayName {
let newGroupModel = TSGroupModel(title: info.displayName, memberIds: model.groupMemberIds, image: model.groupImage, groupId: model.groupId, groupType: model.groupType, adminIds: model.groupAdminIds)
thread.groupModel = newGroupModel
thread.save(with: transaction)
}
// Download and update profile picture if needed
let oldProfilePictureURL = Storage.shared.getProfilePictureURL(forOpenGroupWithID: openGroupID)
if oldProfilePictureURL != info.profilePictureURL || model.groupImage == nil {
Storage.shared.setProfilePictureURL(to: info.profilePictureURL, forOpenGroupWithID: openGroupID, using: transaction)
if let profilePictureURL = info.profilePictureURL {
var sanitizedServerURL = server
while sanitizedServerURL.hasSuffix("/") { sanitizedServerURL.removeLast() }
var sanitizedProfilePictureURL = profilePictureURL
while sanitizedProfilePictureURL.hasPrefix("/") { sanitizedProfilePictureURL.removeFirst() }
let url = "\(sanitizedServerURL)/\(sanitizedProfilePictureURL)"
FileServerAPI.downloadAttachment(from: url).map2 { rawData in
let attachmentStream: TSAttachmentStream
let data: Data
if let rawImage = UIImage(data: rawData), let jpegData = rawImage.jpegData(compressionQuality: 0.8) {
data = jpegData
} else {
data = rawData
}
attachmentStream = TSAttachmentStream(contentType: OWSMimeTypeImageJpeg, byteCount: UInt32(data.count), sourceFilename: nil, caption: nil, albumMessageId: nil)
try attachmentStream.write(data)
thread.updateAvatar(with: attachmentStream)
}
}
}
}
}
public static func join(_ channel: UInt64, on server: String) -> Promise<Void> {
return attempt(maxRetryCount: maxRetryCount, recoveringOn: DispatchQueue.global(qos: .default)) {
getOpenGroupServerPublicKey(for: server).then(on: DispatchQueue.global(qos: .default)) { serverPublicKey in
getAuthToken(for: server).then(on: DispatchQueue.global(qos: .default)) { token -> Promise<Void> in
let url = URL(string: "\(server)/channels/\(channel)/subscribe")!
let request = TSRequest(url: url, method: "POST", parameters: [:])
request.allHTTPHeaderFields = [ "Content-Type" : "application/json", "Authorization" : "Bearer \(token)" ]
return OnionRequestAPI.sendOnionRequest(request, to: server, using: serverPublicKey).done(on: DispatchQueue.global(qos: .default)) { _ -> Void in
SNLog("Joined channel with ID: \(channel) on server: \(server).")
}
}
}.handlingInvalidAuthTokenIfNeeded(for: server)
}
}
public static func leave(_ channel: UInt64, on server: String) -> Promise<Void> {
return attempt(maxRetryCount: maxRetryCount, recoveringOn: DispatchQueue.global(qos: .default)) {
getOpenGroupServerPublicKey(for: server).then(on: DispatchQueue.global(qos: .default)) { serverPublicKey in
getAuthToken(for: server).then(on: DispatchQueue.global(qos: .default)) { token -> Promise<Void> in
let url = URL(string: "\(server)/channels/\(channel)/subscribe")!
let request = TSRequest(url: url, method: "DELETE", parameters: [:])
request.allHTTPHeaderFields = [ "Content-Type" : "application/json", "Authorization" : "Bearer \(token)" ]
return OnionRequestAPI.sendOnionRequest(request, to: server, using: serverPublicKey).done(on: DispatchQueue.global(qos: .default)) { _ -> Void in
SNLog("Left channel with ID: \(channel) on server: \(server).")
}
}
}.handlingInvalidAuthTokenIfNeeded(for: server)
}
}
// MARK: Reporting
@objc(reportMessageWithID:inChannel:onServer:)
public static func objc_reportMessageWithID(_ messageID: UInt64, in channel: UInt64, on server: String) -> AnyPromise {
return AnyPromise.from(reportMessageWithID(messageID, in: channel, on: server))
}
public static func reportMessageWithID(_ messageID: UInt64, in channel: UInt64, on server: String) -> Promise<Void> {
let url = URL(string: "\(server)/loki/v1/channels/\(channel)/messages/\(messageID)/report")!
let request = TSRequest(url: url, method: "POST", parameters: [:])
// Only used for the Loki Public Chat which doesn't require authentication
return getOpenGroupServerPublicKey(for: server).then(on: DispatchQueue.global(qos: .default)) { serverPublicKey in
OnionRequestAPI.sendOnionRequest(request, to: server, using: serverPublicKey).map(on: DispatchQueue.global(qos: .default)) { _ in }
}
}
// MARK: Moderators
public static func getModerators(for channel: UInt64, on server: String) -> Promise<Set<String>> {
return getOpenGroupServerPublicKey(for: server).then(on: DispatchQueue.global(qos: .default)) { serverPublicKey in
getAuthToken(for: server).then(on: DispatchQueue.global(qos: .default)) { token -> Promise<Set<String>> in
let url = URL(string: "\(server)/loki/v1/channel/\(channel)/get_moderators")!
let request = TSRequest(url: url)
request.allHTTPHeaderFields = [ "Content-Type" : "application/json", "Authorization" : "Bearer \(token)" ]
return OnionRequestAPI.sendOnionRequest(request, to: server, using: serverPublicKey).map(on: DispatchQueue.global(qos: .default)) { json in
guard let moderators = json["moderators"] as? [String] else {
SNLog("Couldn't parse moderators for open group channel with ID: \(channel) on server: \(server) from: \(json).")
throw Error.parsingFailed
}
let moderatorsAsSet = Set(moderators);
if self.moderators.keys.contains(server) {
self.moderators[server]![channel] = moderatorsAsSet
} else {
self.moderators[server] = [ channel : moderatorsAsSet ]
}
return moderatorsAsSet
}
}
}.handlingInvalidAuthTokenIfNeeded(for: server)
}
@objc(isUserModerator:forChannel:onServer:)
public static func isUserModerator(_ hexEncodedPublicString: String, for channel: UInt64, on server: String) -> Bool {
return moderators[server]?[channel]?.contains(hexEncodedPublicString) ?? false
}
}
// MARK: Error Handling
internal extension Promise {
func handlingInvalidAuthTokenIfNeeded(for server: String) -> Promise<T> {
return recover(on: DispatchQueue.global(qos: .userInitiated)) { error -> Promise<T> in
if case OnionRequestAPI.Error.httpRequestFailedAtDestination(let statusCode, _) = error, statusCode == 401 || statusCode == 403 {
SNLog("Auth token for: \(server) expired; dropping it.")
let storage = SNMessagingKitConfiguration.shared.storage
storage.writeSync { transaction in
storage.removeAuthToken(for: server, using: transaction)
}
}
throw error
}
}
}

View File

@ -1,6 +0,0 @@
public struct OpenGroupInfo {
public let displayName: String
public let profilePictureURL: String?
public let memberCount: Int
}

View File

@ -1,101 +0,0 @@
import PromiseKit
@objc(SNOpenGroupManager)
public final class OpenGroupManager : NSObject {
private var pollers: [String:OpenGroupPoller] = [:]
private var isPolling = false
// MARK: Error
public enum Error : LocalizedError {
case invalidURL
public var errorDescription: String? {
switch self {
case .invalidURL: return "Invalid URL."
}
}
}
// MARK: Initialization
@objc public static let shared = OpenGroupManager()
private override init() { }
// MARK: Polling
@objc public func startPolling() {
guard !isPolling else { return }
isPolling = true
let openGroups = Storage.shared.getAllUserOpenGroups()
for (_, openGroup) in openGroups {
if let poller = pollers[openGroup.id] { poller.stop() } // Should never occur
let poller = OpenGroupPoller(for: openGroup)
poller.startIfNeeded()
pollers[openGroup.id] = poller
}
}
@objc public func stopPolling() {
pollers.forEach { (_, openGroupPoller) in openGroupPoller.stop() }
pollers.removeAll()
}
// MARK: Adding & Removing
public func add(with url: String, using transaction: Any) -> Promise<Void> {
let storage = Storage.shared
guard let url = URL(string: url), let scheme = url.scheme, scheme == "https", url.host != nil else {
return Promise(error: Error.invalidURL)
}
let channel: UInt64 = 1
let server = url.absoluteString
let userPublicKey = getUserHexEncodedPublicKey()
let name = storage.getUser()?.name
let profilePictureURL = storage.getUser()?.profilePictureURL
let profileKey = storage.getUser()?.profilePictureEncryptionKey?.keyData
storage.removeLastMessageServerID(for: channel, on: server, using: transaction)
storage.removeLastDeletionServerID(for: channel, on: server, using: transaction)
return OpenGroupAPI.getInfo(for: channel, on: server).done { info in
let openGroup = OpenGroup(channel: channel, server: server, displayName: info.displayName, isDeletable: true)!
let groupID = LKGroupUtilities.getEncodedOpenGroupIDAsData(openGroup.id)
let model = TSGroupModel(title: openGroup.displayName, memberIds: [ userPublicKey ], image: nil, groupId: groupID, groupType: .openGroup, adminIds: [])
storage.write(with: { transaction in
let thread = TSGroupThread.getOrCreateThread(with: model, transaction: transaction as! YapDatabaseReadWriteTransaction)
storage.setOpenGroup(openGroup, for: thread.uniqueId!, using: transaction)
}, completion: {
let _ = OpenGroupAPI.setDisplayName(to: name, on: server)
if let profilePictureURL = profilePictureURL, let profileKey = profileKey {
let _ = OpenGroupAPI.setProfilePictureURL(to: profilePictureURL, using: profileKey, on: server)
}
let _ = OpenGroupAPI.join(channel, on: server)
if let poller = OpenGroupManager.shared.pollers[openGroup.id] {
poller.stop()
OpenGroupManager.shared.pollers[openGroup.id] = nil
}
let poller = OpenGroupPoller(for: openGroup)
poller.startIfNeeded()
OpenGroupManager.shared.pollers[openGroup.id] = poller
})
}
}
public func delete(_ openGroup: OpenGroup, associatedWith thread: TSThread, using transaction: YapDatabaseReadWriteTransaction) {
if let poller = pollers[openGroup.id] {
poller.stop()
pollers[openGroup.id] = nil
}
var messageIDs: Set<String> = []
var messageTimestamps: Set<UInt64> = []
thread.enumerateInteractions(with: transaction) { interaction, _ in
messageIDs.insert(interaction.uniqueId!)
messageTimestamps.insert(interaction.timestamp)
}
SNMessagingKitConfiguration.shared.storage.updateMessageIDCollectionByPruningMessagesWithIDs(messageIDs, using: transaction)
Storage.shared.removeReceivedMessageTimestamps(messageTimestamps, using: transaction)
Storage.shared.removeLastMessageServerID(for: openGroup.channel, on: openGroup.server, using: transaction)
Storage.shared.removeLastDeletionServerID(for: openGroup.channel, on: openGroup.server, using: transaction)
let _ = OpenGroupAPI.leave(openGroup.channel, on: openGroup.server)
Storage.shared.removeOpenGroupPublicKey(for: openGroup.server, using: transaction)
thread.removeAllThreadInteractions(with: transaction)
thread.remove(with: transaction)
Storage.shared.removeOpenGroup(for: thread.uniqueId!, using: transaction)
}
}

View File

@ -1,82 +0,0 @@
internal extension OpenGroupMessage {
static func from(_ message: VisibleMessage, for server: String, using transaction: YapDatabaseReadWriteTransaction) -> OpenGroupMessage? {
let storage = SNMessagingKitConfiguration.shared.storage
guard let userPublicKey = storage.getUserPublicKey() else { return nil }
var attachmentIDs = message.attachmentIDs
// Validation
guard message.isValid else { return nil } // Should be valid at this point
// Quote
let quote: OpenGroupMessage.Quote? = {
if let quote = message.quote {
guard quote.isValid else { return nil }
let quotedMessageBody = quote.text ?? String(quote.timestamp!) // The back-end doesn't accept messages without a body so we use this as a workaround
if let quotedAttachmentID = quote.attachmentID, let index = attachmentIDs.firstIndex(of: quotedAttachmentID) {
attachmentIDs.remove(at: index)
}
// FIXME: For some reason the server always returns a 500 if quotedMessageServerID is set...
return OpenGroupMessage.Quote(quotedMessageTimestamp: quote.timestamp!, quoteePublicKey: quote.publicKey!, quotedMessageBody: quotedMessageBody, quotedMessageServerID: nil)
} else {
return nil
}
}()
// Message
let name = storage.getUser()?.name ?? "Anonymous"
let body = message.text ?? String(message.sentTimestamp!) // The back-end doesn't accept messages without a body so we use this as a workaround
let result = OpenGroupMessage(serverID: nil, senderPublicKey: userPublicKey, displayName: name, profilePicture: nil, body: body,
type: OpenGroupAPI.openGroupMessageType, timestamp: message.sentTimestamp!, quote: quote, attachments: [], signature: nil, serverTimestamp: 0)
// Link preview
if let linkPreview = message.linkPreview {
guard linkPreview.isValid, let attachmentID = linkPreview.attachmentID,
let attachment = TSAttachment.fetch(uniqueId: attachmentID, transaction: transaction) as? TSAttachmentStream else { return nil }
if let index = attachmentIDs.firstIndex(of: attachmentID) {
attachmentIDs.remove(at: index)
}
let fileName = attachment.sourceFilename ?? UUID().uuidString
let width = attachment.shouldHaveImageSize() ? attachment.imageSize().width : 0
let height = attachment.shouldHaveImageSize() ? attachment.imageSize().height : 0
let openGroupLinkPreview = OpenGroupMessage.Attachment(
kind: .linkPreview,
server: server,
serverID: attachment.serverId,
contentType: attachment.contentType,
size: UInt(attachment.byteCount),
fileName: fileName,
flags: 0,
width: UInt(width),
height: UInt(height),
caption: attachment.caption,
url: attachment.downloadURL,
linkPreviewURL: linkPreview.url,
linkPreviewTitle: linkPreview.title
)
result.attachments.append(openGroupLinkPreview)
}
// Attachments
let attachments: [OpenGroupMessage.Attachment] = attachmentIDs.compactMap { attachmentID in
guard let attachment = TSAttachment.fetch(uniqueId: attachmentID, transaction: transaction) as? TSAttachmentStream else { return nil } // Should never occur
let fileName = attachment.sourceFilename ?? UUID().uuidString
let width = attachment.shouldHaveImageSize() ? attachment.imageSize().width : 0
let height = attachment.shouldHaveImageSize() ? attachment.imageSize().height : 0
return OpenGroupMessage.Attachment(
kind: .attachment,
server: server,
serverID: attachment.serverId,
contentType: attachment.contentType,
size: UInt(attachment.byteCount),
fileName: fileName,
flags: 0,
width: UInt(width),
height: UInt(height),
caption: attachment.caption,
url: attachment.downloadURL,
linkPreviewURL: nil,
linkPreviewTitle: nil
)
}
result.attachments += attachments
// Return
return result
}
}

View File

@ -1,196 +0,0 @@
import PromiseKit
import Curve25519Kit
import SessionUtilitiesKit
@objc(SNOpenGroupMessage)
public final class OpenGroupMessage : NSObject {
public let serverID: UInt64?
public let senderPublicKey: String
public let displayName: String
public let profilePicture: ProfilePicture?
public let body: String
/// - Note: Expressed as milliseconds since 00:00:00 UTC on 1 January 1970.
public let timestamp: UInt64
public let type: String
public let quote: Quote?
public var attachments: [Attachment] = []
public let signature: Signature?
/// - Note: Used for sorting.
public let serverTimestamp: UInt64
@objc(serverID)
public var objc_serverID: UInt64 { return serverID ?? 0 }
// MARK: Settings
private let signatureVersion: UInt64 = 1
private let attachmentType = "net.app.core.oembed"
// MARK: Types
public struct ProfilePicture {
public let profileKey: Data
public let url: String
}
public struct Quote {
public let quotedMessageTimestamp: UInt64
public let quoteePublicKey: String
public let quotedMessageBody: String
public let quotedMessageServerID: UInt64?
}
public struct Attachment {
public let kind: Kind
public let server: String
public let serverID: UInt64
public let contentType: String
public let size: UInt
public let fileName: String
public let flags: UInt
public let width: UInt
public let height: UInt
public let caption: String?
public let url: String
/// Guaranteed to be non-`nil` if `kind` is `linkPreview`
public let linkPreviewURL: String?
/// Guaranteed to be non-`nil` if `kind` is `linkPreview`
public let linkPreviewTitle: String?
public enum Kind : String { case attachment, linkPreview = "preview" }
public var dotNETType: String {
if contentType.hasPrefix("image") {
return "photo"
} else if contentType.hasPrefix("video") {
return "video"
} else if contentType.hasPrefix("audio") {
return "audio"
} else {
return "other"
}
}
}
public struct Signature {
public let data: Data
public let version: UInt64
}
// MARK: Initialization
public init(serverID: UInt64?, senderPublicKey: String, displayName: String, profilePicture: ProfilePicture?, body: String,
type: String, timestamp: UInt64, quote: Quote?, attachments: [Attachment], signature: Signature?, serverTimestamp: UInt64) {
self.serverID = serverID
self.senderPublicKey = senderPublicKey
self.displayName = displayName
self.profilePicture = profilePicture
self.body = body
self.type = type
self.timestamp = timestamp
self.quote = quote
self.attachments = attachments
self.signature = signature
self.serverTimestamp = serverTimestamp
super.init()
}
@objc public convenience init(senderPublicKey: String, displayName: String, body: String, type: String, timestamp: UInt64,
quotedMessageTimestamp: UInt64, quoteePublicKey: String?, quotedMessageBody: String, quotedMessageServerID: UInt64,
signatureData: Data?, signatureVersion: UInt64, serverTimestamp: UInt64) {
let quote: Quote?
if quotedMessageTimestamp != 0, let quoteeHexEncodedPublicKey = quoteePublicKey {
let quotedMessageServerID = (quotedMessageServerID != 0) ? quotedMessageServerID : nil
quote = Quote(quotedMessageTimestamp: quotedMessageTimestamp, quoteePublicKey: quoteeHexEncodedPublicKey, quotedMessageBody: quotedMessageBody, quotedMessageServerID: quotedMessageServerID)
} else {
quote = nil
}
let signature: Signature?
if let signatureData = signatureData, signatureVersion != 0 {
signature = Signature(data: signatureData, version: signatureVersion)
} else {
signature = nil
}
self.init(serverID: nil, senderPublicKey: senderPublicKey, displayName: displayName, profilePicture: nil, body: body, type: type, timestamp: timestamp, quote: quote, attachments: [], signature: signature, serverTimestamp: serverTimestamp)
}
// MARK: Crypto
internal func sign(with privateKey: Data) -> OpenGroupMessage? {
guard let data = getValidationData(for: signatureVersion) else {
SNLog("Failed to sign open group message.")
return nil
}
let userKeyPair = SNMessagingKitConfiguration.shared.storage.getUserKeyPair()!
guard let signatureData = try? Ed25519.sign(data, with: userKeyPair) else {
SNLog("Failed to sign open group message.")
return nil
}
let signature = Signature(data: signatureData, version: signatureVersion)
return OpenGroupMessage(serverID: serverID, senderPublicKey: senderPublicKey, displayName: displayName, profilePicture: profilePicture, body: body, type: type, timestamp: timestamp, quote: quote, attachments: attachments, signature: signature, serverTimestamp: serverTimestamp)
}
internal func hasValidSignature() -> Bool {
guard let signature = signature else { return false }
guard let data = getValidationData(for: signature.version) else { return false }
let publicKey = Data(hex: self.senderPublicKey.removing05PrefixIfNeeded())
return (try? Ed25519.verifySignature(signature.data, publicKey: publicKey, data: data)) ?? false
}
// MARK: JSON
internal func toJSON() -> JSON {
var value: JSON = [ "timestamp" : timestamp ]
if let quote = quote {
let quoteAsJSON: JSON = [ "id" : quote.quotedMessageTimestamp, "author" : quote.quoteePublicKey, "text" : quote.quotedMessageBody ]
value["quote"] = quoteAsJSON
}
if let signature = signature {
value["sig"] = signature.data.toHexString()
value["sigver"] = signature.version
}
if let profilePicture = profilePicture {
value["avatar"] = profilePicture;
}
let annotation: JSON = [ "type" : type, "value" : value ]
let attachmentAnnotations: [JSON] = attachments.map { attachment in
var attachmentValue: JSON = [
// Fields required by the .NET API
"version" : 1, "type" : attachment.dotNETType,
// Custom fields
"lokiType" : attachment.kind.rawValue, "server" : attachment.server, "id" : attachment.serverID, "contentType" : attachment.contentType, "size" : attachment.size, "fileName" : attachment.fileName, "width" : attachment.width, "height" : attachment.height, "url" : attachment.url
]
if let caption = attachment.caption {
attachmentValue["caption"] = caption
}
if let linkPreviewURL = attachment.linkPreviewURL {
attachmentValue["linkPreviewUrl"] = linkPreviewURL
}
if let linkPreviewTitle = attachment.linkPreviewTitle {
attachmentValue["linkPreviewTitle"] = linkPreviewTitle
}
return [ "type" : attachmentType, "value" : attachmentValue ]
}
var result: JSON = [ "text" : body, "annotations": [ annotation ] + attachmentAnnotations ]
if let quotedMessageServerID = quote?.quotedMessageServerID {
result["reply_to"] = quotedMessageServerID
}
return result
}
// MARK: Convenience
@objc public func addAttachment(kind: String, server: String, serverID: UInt64, contentType: String, size: UInt,
fileName: String, flags: UInt, width: UInt, height: UInt, caption: String?, url: String, linkPreviewURL: String?, linkPreviewTitle: String?) {
guard let kind = Attachment.Kind(rawValue: kind) else { preconditionFailure() }
let attachment = Attachment(kind: kind, server: server, serverID: serverID, contentType: contentType, size: size, fileName: fileName, flags: flags, width: width, height: height, caption: caption, url: url, linkPreviewURL: linkPreviewURL, linkPreviewTitle: linkPreviewTitle)
attachments.append(attachment)
}
private func getValidationData(for signatureVersion: UInt64) -> Data? {
var string = "\(body.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines))\(timestamp)"
if let quote = quote {
string += "\(quote.quotedMessageTimestamp)\(quote.quoteePublicKey)\(quote.quotedMessageBody.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines))"
if let quotedMessageServerID = quote.quotedMessageServerID {
string += "\(quotedMessageServerID)"
}
}
string += attachments.sorted { $0.serverID < $1.serverID }.map { "\($0.serverID)" }.joined(separator: "")
string += "\(signatureVersion)"
return string.data(using: String.Encoding.utf8)
}
}

View File

@ -30,11 +30,10 @@ public final class MentionsManager : NSObject {
guard let cache = userPublicKeyCache[threadID] else { return [] }
var candidates: [Mention] = []
// Gather candidates
let openGroup = Storage.shared.getOpenGroup(for: threadID)
let openGroupV2 = Storage.shared.getV2OpenGroup(for: threadID)
storage.dbReadConnection.read { transaction in
candidates = cache.compactMap { publicKey in
let context: Contact.Context = (openGroupV2 != nil || openGroup != nil) ? .openGroup : .regular
let context: Contact.Context = (openGroupV2 != nil) ? .openGroup : .regular
let displayNameOrNil = Storage.shared.getContact(with: publicKey)?.displayName(for: context)
guard let displayName = displayNameOrNil else { return nil }
guard !displayName.hasPrefix("Anonymous") else { return nil }

View File

@ -216,8 +216,6 @@ extension MessageReceiver {
for openGroupURL in message.openGroups {
if let (room, server, publicKey) = OpenGroupManagerV2.parseV2OpenGroup(from: openGroupURL) {
OpenGroupManagerV2.shared.add(room: room, server: server, publicKey: publicKey, using: transaction).retainUntilComplete()
} else {
OpenGroupManager.shared.add(with: openGroupURL, using: transaction).retainUntilComplete()
}
}
}
@ -244,16 +242,10 @@ extension MessageReceiver {
message.attachmentIDs = attachmentIDs
var attachmentsToDownload = attachmentIDs
// Update profile if needed
if let newProfile = message.profile {
if let profile = message.profile {
let sessionID = message.sender!
updateProfileIfNeeded(publicKey: sessionID, name: newProfile.displayName, profilePictureURL: newProfile.profilePictureURL,
profileKey: given(newProfile.profileKey) { OWSAES256Key(data: $0)! }, sentTimestamp: message.sentTimestamp!, transaction: transaction)
if let rawDisplayName = newProfile.displayName, let openGroupID = openGroupID {
let endIndex = sessionID.endIndex
let cutoffIndex = sessionID.index(endIndex, offsetBy: -8)
let displayName = "\(rawDisplayName) (...\(sessionID[cutoffIndex..<endIndex]))"
Storage.shared.setOpenGroupDisplayName(to: displayName, for: sessionID, inOpenGroupWithID: openGroupID, using: transaction)
}
updateProfileIfNeeded(publicKey: sessionID, name: profile.displayName, profilePictureURL: profile.profilePictureURL,
profileKey: given(profile.profileKey) { OWSAES256Key(data: $0)! }, sentTimestamp: message.sentTimestamp!, transaction: transaction)
}
// Get or create thread
guard let threadID = storage.getOrCreateThread(for: message.syncTarget ?? message.sender!, groupPublicKey: message.groupPublicKey, openGroupID: openGroupID, using: transaction) else { throw Error.noThread }

View File

@ -285,67 +285,41 @@ public final class MessageSender : NSObject {
#endif
}
guard message.isValid else { handleFailure(with: Error.invalidMessage, using: transaction); return promise }
// There's quite a bit of overlap between the two clauses of this if statement for now, but that'll be fixed
// when we remove support for V1 open groups
if case .openGroup(let channel, let server) = destination {
// The back-end doesn't accept messages without a body so we use this as a workaround
if message.text?.isEmpty != false {
message.text = String(message.sentTimestamp!)
}
// Convert the message to an open group message
guard let openGroupMessage = OpenGroupMessage.from(message, for: server, using: transaction) else { handleFailure(with: Error.invalidMessage, using: transaction); return promise }
// Send the result
OpenGroupAPI.sendMessage(openGroupMessage, to: channel, on: server).done(on: DispatchQueue.global(qos: .userInitiated)) { openGroupMessage in
message.openGroupServerMessageID = openGroupMessage.serverID
storage.write(with: { transaction in
MessageSender.handleSuccessfulMessageSend(message, to: destination, using: transaction)
seal.fulfill(())
}, completion: { })
}.catch(on: DispatchQueue.global(qos: .userInitiated)) { error in
storage.write(with: { transaction in
handleFailure(with: error, using: transaction as! YapDatabaseReadWriteTransaction)
}, completion: { })
}
// Return
return promise
} else if case .openGroupV2(let room, let server) = destination {
// Attach the user's profile
guard let name = storage.getUser()?.name else { handleFailure(with: Error.noUsername, using: transaction); return promise }
if let profileKey = storage.getUser()?.profilePictureEncryptionKey?.keyData, let profilePictureURL = storage.getUser()?.profilePictureURL {
message.profile = VisibleMessage.Profile(displayName: name, profileKey: profileKey, profilePictureURL: profilePictureURL)
} else {
message.profile = VisibleMessage.Profile(displayName: name)
}
// Convert it to protobuf
guard let proto = message.toProto(using: transaction) else { handleFailure(with: Error.protoConversionFailed, using: transaction); return promise }
// Serialize the protobuf
let plaintext: Data
do {
plaintext = (try proto.serializedData() as NSData).paddedMessageBody()
} catch {
SNLog("Couldn't serialize proto due to error: \(error).")
handleFailure(with: error, using: transaction)
return promise
}
// Send the result
let openGroupMessage = OpenGroupMessageV2(serverID: nil, sender: nil, sentTimestamp: message.sentTimestamp!,
base64EncodedData: plaintext.base64EncodedString(), base64EncodedSignature: nil)
OpenGroupAPIV2.send(openGroupMessage, to: room, on: server).done(on: DispatchQueue.global(qos: .userInitiated)) { openGroupMessage in
message.openGroupServerMessageID = given(openGroupMessage.serverID) { UInt64($0) }
storage.write(with: { transaction in
MessageSender.handleSuccessfulMessageSend(message, to: destination, using: transaction)
seal.fulfill(())
}, completion: { })
}.catch(on: DispatchQueue.global(qos: .userInitiated)) { error in
storage.write(with: { transaction in
handleFailure(with: error, using: transaction as! YapDatabaseReadWriteTransaction)
}, completion: { })
}
// Return
return promise
// Attach the user's profile
guard let name = storage.getUser()?.name else { handleFailure(with: Error.noUsername, using: transaction); return promise }
if let profileKey = storage.getUser()?.profilePictureEncryptionKey?.keyData, let profilePictureURL = storage.getUser()?.profilePictureURL {
message.profile = VisibleMessage.Profile(displayName: name, profileKey: profileKey, profilePictureURL: profilePictureURL)
} else {
preconditionFailure()
message.profile = VisibleMessage.Profile(displayName: name)
}
// Convert it to protobuf
guard let proto = message.toProto(using: transaction) else { handleFailure(with: Error.protoConversionFailed, using: transaction); return promise }
// Serialize the protobuf
let plaintext: Data
do {
plaintext = (try proto.serializedData() as NSData).paddedMessageBody()
} catch {
SNLog("Couldn't serialize proto due to error: \(error).")
handleFailure(with: error, using: transaction)
return promise
}
// Send the result
guard case .openGroupV2(let room, let server) = destination else { preconditionFailure() }
let openGroupMessage = OpenGroupMessageV2(serverID: nil, sender: nil, sentTimestamp: message.sentTimestamp!,
base64EncodedData: plaintext.base64EncodedString(), base64EncodedSignature: nil)
OpenGroupAPIV2.send(openGroupMessage, to: room, on: server).done(on: DispatchQueue.global(qos: .userInitiated)) { openGroupMessage in
message.openGroupServerMessageID = given(openGroupMessage.serverID) { UInt64($0) }
storage.write(with: { transaction in
MessageSender.handleSuccessfulMessageSend(message, to: destination, using: transaction)
seal.fulfill(())
}, completion: { })
}.catch(on: DispatchQueue.global(qos: .userInitiated)) { error in
storage.write(with: { transaction in
handleFailure(with: error, using: transaction as! YapDatabaseReadWriteTransaction)
}, completion: { })
}
// Return
return promise
}
// MARK: Success & Failure Handling

View File

@ -1,193 +0,0 @@
import PromiseKit
@objc(LKOpenGroupPoller)
public final class OpenGroupPoller : NSObject {
private let openGroup: OpenGroup
private var pollForNewMessagesTimer: Timer? = nil
private var pollForDeletedMessagesTimer: Timer? = nil
private var pollForModeratorsTimer: Timer? = nil
private var hasStarted = false
private var isPolling = false
private var isMainAppAndActive: Bool {
var isMainAppAndActive = false
if let sharedUserDefaults = UserDefaults(suiteName: "group.com.loki-project.loki-messenger") {
isMainAppAndActive = sharedUserDefaults.bool(forKey: "isMainAppActive")
}
return isMainAppAndActive
}
// MARK: Settings
private let pollForNewMessagesInterval: TimeInterval = 4
private let pollForDeletedMessagesInterval: TimeInterval = 30
private let pollForModeratorsInterval: TimeInterval = 10 * 60
// MARK: Lifecycle
@objc(initForOpenGroup:)
public init(for openGroup: OpenGroup) {
self.openGroup = openGroup
super.init()
}
@objc public func startIfNeeded() {
guard !hasStarted else { return }
guard isMainAppAndActive else { stop(); return }
DispatchQueue.main.async { [weak self] in // Timers don't do well on background queues
guard let strongSelf = self else { return }
strongSelf.pollForNewMessagesTimer = Timer.scheduledTimer(withTimeInterval: strongSelf.pollForNewMessagesInterval, repeats: true) { _ in self?.pollForNewMessages() }
strongSelf.pollForDeletedMessagesTimer = Timer.scheduledTimer(withTimeInterval: strongSelf.pollForDeletedMessagesInterval, repeats: true) { _ in self?.pollForDeletedMessages() }
strongSelf.pollForModeratorsTimer = Timer.scheduledTimer(withTimeInterval: strongSelf.pollForModeratorsInterval, repeats: true) { _ in self?.pollForModerators() }
// Perform initial updates
strongSelf.pollForNewMessages()
strongSelf.pollForDeletedMessages()
strongSelf.pollForModerators()
strongSelf.hasStarted = true
}
}
@objc public func stop() {
pollForNewMessagesTimer?.invalidate()
pollForDeletedMessagesTimer?.invalidate()
pollForModeratorsTimer?.invalidate()
hasStarted = false
}
// MARK: Polling
@objc(pollForNewMessages)
public func objc_pollForNewMessages() -> AnyPromise {
AnyPromise.from(pollForNewMessages())
}
@discardableResult
public func pollForNewMessages() -> Promise<Void> {
guard isMainAppAndActive else { stop(); return Promise.value(()) }
return pollForNewMessages(isBackgroundPoll: false)
}
@discardableResult
public func pollForNewMessages(isBackgroundPoll: Bool) -> Promise<Void> {
guard !self.isPolling else { return Promise.value(()) }
self.isPolling = true
let openGroup = self.openGroup
let (promise, seal) = Promise<Void>.pending()
promise.retainUntilComplete()
OpenGroupAPI.getMessages(for: openGroup.channel, on: openGroup.server).done(on: DispatchQueue.global(qos: .default)) { messages in
self.isPolling = false
// Sorting the messages by timestamp before importing them fixes an issue where messages that quote older messages can't find those older messages
messages.sorted { $0.serverTimestamp < $1.serverTimestamp }.forEach { message in
let senderPublicKey = message.senderPublicKey
let wasSentByCurrentUser = (senderPublicKey == getUserHexEncodedPublicKey())
func generateDisplayName(from rawDisplayName: String) -> String {
let endIndex = senderPublicKey.endIndex
let cutoffIndex = senderPublicKey.index(endIndex, offsetBy: -8)
return "\(rawDisplayName) (...\(senderPublicKey[cutoffIndex..<endIndex]))"
}
let senderDisplayName = Storage.shared.getContact(with: senderPublicKey)?.displayName(for: .openGroup)
?? generateDisplayName(from: NSLocalizedString("Anonymous", comment: ""))
let id = LKGroupUtilities.getEncodedOpenGroupIDAsData(openGroup.id)
// Main message
let dataMessageProto = SNProtoDataMessage.builder()
let body = (message.body == message.timestamp.description) ? "" : message.body // The back-end doesn't accept messages without a body so we use this as a workaround
dataMessageProto.setBody(body)
dataMessageProto.setTimestamp(message.timestamp)
// Attachments
let attachmentProtos: [SNProtoAttachmentPointer] = message.attachments.compactMap { attachment in
guard attachment.kind == .attachment else { return nil }
let attachmentProto = SNProtoAttachmentPointer.builder(id: attachment.serverID)
attachmentProto.setContentType(attachment.contentType)
attachmentProto.setSize(UInt32(attachment.size))
attachmentProto.setFileName(attachment.fileName)
attachmentProto.setFlags(UInt32(attachment.flags))
attachmentProto.setWidth(UInt32(attachment.width))
attachmentProto.setHeight(UInt32(attachment.height))
if let caption = attachment.caption { attachmentProto.setCaption(caption) }
attachmentProto.setUrl(attachment.url)
return try! attachmentProto.build()
}
dataMessageProto.setAttachments(attachmentProtos)
// Link preview
if let linkPreview = message.attachments.first(where: { $0.kind == .linkPreview }) {
let linkPreviewProto = SNProtoDataMessagePreview.builder(url: linkPreview.linkPreviewURL!)
linkPreviewProto.setTitle(linkPreview.linkPreviewTitle!)
let attachmentProto = SNProtoAttachmentPointer.builder(id: linkPreview.serverID)
attachmentProto.setContentType(linkPreview.contentType)
attachmentProto.setSize(UInt32(linkPreview.size))
attachmentProto.setFileName(linkPreview.fileName)
attachmentProto.setFlags(UInt32(linkPreview.flags))
attachmentProto.setWidth(UInt32(linkPreview.width))
attachmentProto.setHeight(UInt32(linkPreview.height))
if let caption = linkPreview.caption { attachmentProto.setCaption(caption) }
attachmentProto.setUrl(linkPreview.url)
linkPreviewProto.setImage(try! attachmentProto.build())
dataMessageProto.setPreview([ try! linkPreviewProto.build() ])
}
// Quote
if let quote = message.quote {
let quoteProto = SNProtoDataMessageQuote.builder(id: quote.quotedMessageTimestamp, author: quote.quoteePublicKey)
if quote.quotedMessageBody != String(quote.quotedMessageTimestamp) { quoteProto.setText(quote.quotedMessageBody) }
dataMessageProto.setQuote(try! quoteProto.build())
}
// Profile
let profileProto = SNProtoDataMessageLokiProfile.builder()
profileProto.setDisplayName(message.displayName)
if let profilePicture = message.profilePicture {
profileProto.setProfilePicture(profilePicture.url)
dataMessageProto.setProfileKey(profilePicture.profileKey)
}
dataMessageProto.setProfile(try! profileProto.build())
// Signal group context
let groupProto = SNProtoGroupContext.builder(id: id, type: .deliver)
groupProto.setName(openGroup.displayName)
dataMessageProto.setGroup(try! groupProto.build())
// Sync target
if wasSentByCurrentUser {
dataMessageProto.setSyncTarget(openGroup.id)
}
// Content
let content = SNProtoContent.builder()
content.setDataMessage(try! dataMessageProto.build())
// Envelope
let envelope = SNProtoEnvelope.builder(type: .sessionMessage, timestamp: message.timestamp)
envelope.setSource(senderPublicKey)
envelope.setSourceDevice(1)
envelope.setContent(try! content.build().serializedData())
envelope.setServerTimestamp(message.serverTimestamp)
SNMessagingKitConfiguration.shared.storage.write { transaction in
Storage.shared.setOpenGroupDisplayName(to: senderDisplayName, for: senderPublicKey, inOpenGroupWithID: openGroup.id, using: transaction)
let messageServerID = message.serverID
let job = MessageReceiveJob(data: try! envelope.buildSerializedData(), openGroupMessageServerID: messageServerID, openGroupID: openGroup.id, isBackgroundPoll: isBackgroundPoll)
if isBackgroundPoll {
job.execute().done(on: DispatchQueue.global(qos: .userInitiated)) {
seal.fulfill(())
}.catch(on: DispatchQueue.global(qos: .userInitiated)) { _ in
seal.fulfill(()) // The promise is just used to keep track of when we're done
}
} else {
SessionMessagingKit.JobQueue.shared.add(job, using: transaction)
seal.fulfill(())
}
}
}
}.catch(on: DispatchQueue.global(qos: .userInitiated)) { _ in
seal.fulfill(()) // The promise is just used to keep track of when we're done
}.retainUntilComplete()
return promise
}
private func pollForDeletedMessages() {
let openGroup = self.openGroup
OpenGroupAPI.getDeletedMessageServerIDs(for: openGroup.channel, on: openGroup.server).done(on: DispatchQueue.global(qos: .default)) { deletedMessageServerIDs in
let deletedMessageIDs = deletedMessageServerIDs.compactMap { Storage.shared.getIDForMessage(withServerID: UInt64($0)) }
SNMessagingKitConfiguration.shared.storage.write { transaction in
deletedMessageIDs.forEach { messageID in
let transaction = transaction as! YapDatabaseReadWriteTransaction
TSMessage.fetch(uniqueId: messageID, transaction: transaction)?.remove(with: transaction)
}
}
}.retainUntilComplete()
}
private func pollForModerators() {
OpenGroupAPI.getModerators(for: openGroup.channel, on: openGroup.server).retainUntilComplete()
}
}

View File

@ -47,7 +47,6 @@ public protocol SessionMessagingKitStorageProtocol {
func getAllV2OpenGroups() -> [String:OpenGroupV2]
func getV2OpenGroup(for threadID: String) -> OpenGroupV2?
func getThreadID(for openGroupID: String) -> String?
func updateMessageIDCollectionByPruningMessagesWithIDs(_ messageIDs: Set<String>, using transaction: Any)
// MARK: - Open Group Public Keys
@ -72,8 +71,6 @@ public protocol SessionMessagingKitStorageProtocol {
func setUserCount(to newValue: UInt64, forV2OpenGroupWithID openGroupID: String, using transaction: Any)
func getIDForMessage(withServerID serverID: UInt64) -> String?
func setIDForMessage(withServerID serverID: UInt64, to messageID: String, using transaction: Any)
func setOpenGroupDisplayName(to displayName: String, for publicKey: String, inOpenGroupWithID openGroupID: String, using transaction: Any)
func setLastProfilePictureUploadDate(_ date: Date) // Stored in user defaults so no transaction is needed
// MARK: - Message Handling
@ -89,23 +86,4 @@ public protocol SessionMessagingKitStorageProtocol {
func setAttachmentState(to state: TSAttachmentPointerState, for pointer: TSAttachmentPointer, associatedWith tsIncomingMessageID: String, using transaction: Any)
/// Also touches the associated message.
func persist(_ stream: TSAttachmentStream, associatedWith tsIncomingMessageID: String, using transaction: Any)
// MARK: - Deprecated
func getAuthToken(for server: String) -> String?
func setAuthToken(for server: String, to newValue: String, using transaction: Any)
func removeAuthToken(for server: String, using transaction: Any)
func getLastMessageServerID(for group: UInt64, on server: String) -> UInt64?
func setLastMessageServerID(for group: UInt64, on server: String, to newValue: UInt64, using transaction: Any)
func removeLastMessageServerID(for group: UInt64, on server: String, using transaction: Any)
func getLastDeletionServerID(for group: UInt64, on server: String) -> UInt64?
func setLastDeletionServerID(for group: UInt64, on server: String, to newValue: UInt64, using transaction: Any)
func removeLastDeletionServerID(for group: UInt64, on server: String, using transaction: Any)
func getAllUserOpenGroups() -> [String:OpenGroup]
func getOpenGroup(for threadID: String) -> OpenGroup?
func setUserCount(to newValue: Int, forOpenGroupWithID openGroupID: String, using transaction: Any)
}

View File

@ -1,226 +0,0 @@
import AFNetworking
import CryptoSwift
import PromiseKit
import SessionSnodeKit
import SessionUtilitiesKit
import SignalCoreKit
/// Base class for `FileServerAPI` and `OpenGroupAPI`.
public class DotNetAPI : NSObject {
// MARK: Settings
private static let attachmentType = "network.loki"
private static let maxRetryCount: UInt = 4
// MARK: Error
public enum Error : LocalizedError {
case generic
case invalidURL
case parsingFailed
case signingFailed
case encryptionFailed
case decryptionFailed
case maxFileSizeExceeded
internal var isRetryable: Bool {
return false
}
public var errorDescription: String? {
switch self {
case .generic: return "An error occurred."
case .invalidURL: return "Invalid URL."
case .parsingFailed: return "Invalid file server response."
case .signingFailed: return "Couldn't sign message."
case .encryptionFailed: return "Couldn't encrypt file."
case .decryptionFailed: return "Couldn't decrypt file."
case .maxFileSizeExceeded: return "Maximum file size exceeded."
}
}
}
// MARK: Lifecycle
override private init() { }
// MARK: Private API
private static func requestNewAuthToken(for server: String) -> Promise<String> {
SNLog("Requesting auth token for server: \(server).")
guard let userKeyPair = SNMessagingKitConfiguration.shared.storage.getUserKeyPair() else { return Promise(error: Error.generic) }
let queryParameters = "pubKey=\(userKeyPair.publicKey.toHexString())"
let url = URL(string: "\(server)/loki/v1/get_challenge?\(queryParameters)")!
let request = TSRequest(url: url)
let serverPublicKeyPromise = (server == FileServerAPI.server) ? Promise.value(FileServerAPI.publicKey)
: OpenGroupAPI.getOpenGroupServerPublicKey(for: server)
return serverPublicKeyPromise.then(on: DispatchQueue.global(qos: .userInitiated)) { serverPublicKey in
OnionRequestAPI.sendOnionRequest(request, to: server, using: serverPublicKey)
}.map(on: DispatchQueue.global(qos: .userInitiated)) { json in
guard let base64EncodedChallenge = json["cipherText64"] as? String, let base64EncodedServerPublicKey = json["serverPubKey64"] as? String,
let challenge = Data(base64Encoded: base64EncodedChallenge), var serverPublicKey = Data(base64Encoded: base64EncodedServerPublicKey) else {
throw Error.parsingFailed
}
// Discard the "05" prefix if needed
if serverPublicKey.count == 33 {
let hexEncodedServerPublicKey = serverPublicKey.toHexString()
let startIndex = hexEncodedServerPublicKey.index(hexEncodedServerPublicKey.startIndex, offsetBy: 2)
serverPublicKey = Data(hex: String(hexEncodedServerPublicKey[startIndex..<hexEncodedServerPublicKey.endIndex]))
}
// The challenge is prefixed by the 16 bit IV
guard let tokenAsData = try? DiffieHellman.decrypt(challenge, publicKey: serverPublicKey, privateKey: userKeyPair.privateKey),
let token = String(bytes: tokenAsData, encoding: .utf8) else {
throw Error.decryptionFailed
}
return token
}
}
private static func submitAuthToken(_ token: String, for server: String) -> Promise<String> {
SNLog("Submitting auth token for server: \(server).")
let url = URL(string: "\(server)/loki/v1/submit_challenge")!
guard let userPublicKey = SNMessagingKitConfiguration.shared.storage.getUserPublicKey() else { return Promise(error: Error.generic) }
let parameters = [ "pubKey" : userPublicKey, "token" : token ]
let request = TSRequest(url: url, method: "POST", parameters: parameters)
let serverPublicKeyPromise = (server == FileServerAPI.server) ? Promise.value(FileServerAPI.publicKey)
: OpenGroupAPI.getOpenGroupServerPublicKey(for: server)
return serverPublicKeyPromise.then(on: DispatchQueue.global(qos: .userInitiated)) { serverPublicKey in
OnionRequestAPI.sendOnionRequest(request, to: server, using: serverPublicKey)
}.map(on: DispatchQueue.global(qos: .userInitiated)) { _ in token }
}
// MARK: Public API
public static func getAuthToken(for server: String) -> Promise<String> {
let storage = SNMessagingKitConfiguration.shared.storage
if let token = storage.getAuthToken(for: server) {
return Promise.value(token)
} else {
return requestNewAuthToken(for: server).then(on: DispatchQueue.global(qos: .userInitiated)) { submitAuthToken($0, for: server) }.map(on: DispatchQueue.global(qos: .userInitiated)) { token in
storage.writeSync { transaction in
storage.setAuthToken(for: server, to: token, using: transaction)
}
return token
}
}
}
@objc(downloadAttachmentFrom:)
public static func objc_downloadAttachment(from url: String) -> AnyPromise {
return AnyPromise.from(downloadAttachment(from: url))
}
public static func downloadAttachment(from urlAsString: String) -> Promise<Data> {
guard let url = URL(string: urlAsString) else { return Promise(error: Error.invalidURL) }
var host = "https://\(url.host!)"
let sanitizedURL: String
if FileServerAPI.fileStorageBucketURL.contains(host) {
sanitizedURL = urlAsString.replacingOccurrences(of: FileServerAPI.fileStorageBucketURL, with: "\(FileServerAPI.server)/loki/v1")
host = FileServerAPI.server
} else {
sanitizedURL = urlAsString.replacingOccurrences(of: host, with: "\(host)/loki/v1")
}
let request: NSMutableURLRequest
do {
request = try AFHTTPRequestSerializer().request(withMethod: "GET", urlString: sanitizedURL, parameters: nil)
} catch {
SNLog("Couldn't download attachment due to error: \(error).")
return Promise(error: error)
}
let serverPublicKeyPromise = FileServerAPI.server.contains(host) ? Promise.value(FileServerAPI.publicKey)
: OpenGroupAPI.getOpenGroupServerPublicKey(for: host)
return attempt(maxRetryCount: maxRetryCount, recoveringOn: DispatchQueue.global(qos: .userInitiated)) {
serverPublicKeyPromise.then(on: DispatchQueue.global(qos: .userInitiated)) { serverPublicKey in
return OnionRequestAPI.sendOnionRequest(request, to: host, using: serverPublicKey, isJSONRequired: false).map(on: DispatchQueue.global(qos: .userInitiated)) { json in
guard let body = json["result"] as? String, let data = Data(base64Encoded: body) else {
SNLog("Couldn't parse attachment from: \(json).")
throw Error.parsingFailed
}
return data
}
}
}
}
@objc(uploadAttachment:withID:toServer:)
public static func objc_uploadAttachment(_ attachment: TSAttachmentStream, with attachmentID: String, to server: String) -> AnyPromise {
return AnyPromise.from(uploadAttachment(attachment, with: attachmentID, to: server))
}
public static func uploadAttachment(_ attachment: TSAttachmentStream, with attachmentID: String, to server: String) -> Promise<Void> {
let isEncryptionRequired = (server == FileServerAPI.server)
return Promise<Void>() { seal in
func proceed(with token: String) {
// Get the attachment
let data: Data
guard let unencryptedAttachmentData = try? attachment.readDataFromFile() else {
SNLog("Couldn't read attachment from disk.")
return seal.reject(Error.generic)
}
// Encrypt the attachment if needed
if isEncryptionRequired {
var encryptionKey = NSData()
var digest = NSData()
guard let encryptedAttachmentData = Cryptography.encryptAttachmentData(unencryptedAttachmentData, shouldPad: false, outKey: &encryptionKey, outDigest: &digest) else {
SNLog("Couldn't encrypt attachment.")
return seal.reject(Error.encryptionFailed)
}
attachment.encryptionKey = encryptionKey as Data
attachment.digest = digest as Data
data = encryptedAttachmentData
} else {
data = unencryptedAttachmentData
}
// Check the file size if needed
SNLog("File size: \(data.count) bytes.")
if Double(data.count) > Double(FileServerAPI.maxFileSize) / FileServerAPI.fileSizeORMultiplier {
return seal.reject(Error.maxFileSizeExceeded)
}
// Create the request
let url = "\(server)/files"
let parameters: JSON = [ "type" : attachmentType, "Content-Type" : "application/binary" ]
var error: NSError?
let request = AFHTTPRequestSerializer().multipartFormRequest(withMethod: "POST", urlString: url, parameters: parameters, constructingBodyWith: { formData in
let uuid = UUID().uuidString
SNLog("File UUID: \(uuid).")
formData.appendPart(withFileData: data, name: "content", fileName: uuid, mimeType: "application/binary")
}, error: &error)
request.addValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
if let error = error {
SNLog("Couldn't upload attachment due to error: \(error).")
return seal.reject(error)
}
// Send the request
let serverPublicKeyPromise = (server == FileServerAPI.server) ? Promise.value(FileServerAPI.publicKey)
: OpenGroupAPI.getOpenGroupServerPublicKey(for: server)
attachment.isUploaded = false
attachment.save()
let _ = serverPublicKeyPromise.then(on: DispatchQueue.global(qos: .userInitiated)) { serverPublicKey in
OnionRequestAPI.sendOnionRequest(request, to: server, using: serverPublicKey)
}.done(on: DispatchQueue.global(qos: .userInitiated)) { json in
// Parse the server ID & download URL
guard let data = json["data"] as? JSON, let serverID = data["id"] as? UInt64, let downloadURL = data["url"] as? String else {
SNLog("Couldn't parse attachment from: \(json).")
return seal.reject(Error.parsingFailed)
}
// Update the attachment
attachment.serverId = serverID
attachment.isUploaded = true
attachment.downloadURL = downloadURL
attachment.save()
seal.fulfill(())
}.catch(on: DispatchQueue.global(qos: .userInitiated)) { error in
seal.reject(error)
}
}
if server == FileServerAPI.server {
DispatchQueue.global(qos: .userInitiated).async {
proceed(with: "loki") // Uploads to the Loki File Server shouldn't include any personally identifiable information so use a dummy auth token
}
} else {
getAuthToken(for: server).done(on: DispatchQueue.global(qos: .userInitiated)) { token in
proceed(with: token)
}.catch(on: DispatchQueue.global(qos: .userInitiated)) { error in
SNLog("Couldn't upload attachment due to error: \(error).")
seal.reject(error)
}
}
}
}
}

View File

@ -3,6 +3,4 @@
public final class Features : NSObject {
public static let useOnionRequests = true
public static let useTestnet = false
@objc public static let useV2OpenGroups = true
@objc public static let useV2FileServer = false
}

View File

@ -9,6 +9,6 @@ public final class Configuration : NSObject {
@objc public static func performMainSetup() {
SNMessagingKit.configure(storage: Storage.shared)
SNSnodeKit.configure(storage: Storage.shared)
SNUtilitiesKit.configure(owsPrimaryStorage: OWSPrimaryStorage.shared(), maxFileSize: UInt(Double(FileServerAPI.maxFileSize) / FileServerAPI.fileSizeORMultiplier))
SNUtilitiesKit.configure(owsPrimaryStorage: OWSPrimaryStorage.shared(), maxFileSize: UInt(Double(FileServerAPIV2.maxFileSize) / FileServerAPIV2.fileSizeORMultiplier))
}
}

View File

@ -27,8 +27,6 @@ extension ConfigurationMessage {
case .openGroup:
if let v2OpenGroup = storage.getV2OpenGroup(for: thread.uniqueId!) {
openGroups.insert("\(v2OpenGroup.server)/\(v2OpenGroup.room)?public_key=\(v2OpenGroup.publicKey)")
} else if let openGroup = storage.getOpenGroup(for: thread.uniqueId!) {
openGroups.insert(openGroup.server)
}
default: break
}

View File

@ -47,17 +47,10 @@ extension MessageSender {
let (promise, seal) = Promise<Void>.pending()
AttachmentUploadJob.upload(stream, using: { data in return OpenGroupAPIV2.upload(data, to: v2OpenGroup.room, on: v2OpenGroup.server) }, encrypt: false, onSuccess: { seal.fulfill(()) }, onFailure: { seal.reject($0) })
return promise
} else if Features.useV2FileServer && storage.getOpenGroup(for: thread.uniqueId!) == nil {
} else {
let (promise, seal) = Promise<Void>.pending()
AttachmentUploadJob.upload(stream, using: FileServerAPIV2.upload, encrypt: true, onSuccess: { seal.fulfill(()) }, onFailure: { seal.reject($0) })
return promise
} else { // Legacy
let openGroup = storage.getOpenGroup(for: thread.uniqueId!)
let server = openGroup?.server ?? FileServerAPI.server
let maxRetryCount: UInt = (openGroup != nil) ? 24 : 8
return attempt(maxRetryCount: maxRetryCount, recoveringOn: DispatchQueue.global(qos: .userInitiated)) {
FileServerAPI.uploadAttachment(stream, with: stream.uniqueId!, to: server)
}
}
}
return when(resolved: attachmentUploadPromises).then(on: DispatchQueue.global(qos: .userInitiated)) { results -> Promise<Void> in

View File

@ -362,12 +362,7 @@ typedef void (^ProfileManagerFailureBlock)(NSError *error);
NSData *encryptedAvatarData = [self encryptProfileData:avatarData profileKey:newProfileKey];
OWSAssertDebug(encryptedAvatarData.length > 0);
AnyPromise *promise;
if (SNFeatures.useV2FileServer) {
promise = [SNFileServerAPIV2 upload:encryptedAvatarData];
} else {
promise = [SNFileServerAPI uploadProfilePicture:encryptedAvatarData];
}
AnyPromise *promise = [SNFileServerAPIV2 upload:encryptedAvatarData];
[promise.thenOn(dispatch_get_main_queue(), ^(NSString *downloadURL) {
[self.localUserProfile updateWithProfileKey:newProfileKey dbConnection:self.dbConnection completion:^{
@ -399,18 +394,6 @@ typedef void (^ProfileManagerFailureBlock)(NSError *error);
avatarUrl:(nullable NSString *)avatarURL
success:(void (^)(void))successBlock
failure:(ProfileManagerFailureBlock)failureBlock {
OWSAssertDebug(successBlock);
OWSAssertDebug(failureBlock);
NSDictionary *publicChats = [LKStorage.shared getAllUserOpenGroups];
NSSet *servers = [NSSet setWithArray:[publicChats.allValues map:^NSString *(SNOpenGroup *publicChat) { return publicChat.server; }]];
for (NSString *server in servers) {
[[SNOpenGroupAPI setDisplayName:localProfileName on:server] retainUntilComplete];
[[SNOpenGroupAPI setProfilePictureURL:avatarURL usingProfileKey:self.localProfileKey.keyData on:server] retainUntilComplete];
}
successBlock();
}
@ -808,13 +791,8 @@ typedef void (^ProfileManagerFailureBlock)(NSError *error);
NSString *profilePictureURL = userProfile.avatarUrlPath;
AnyPromise *promise;
if ([profilePictureURL containsString:SNFileServerAPIV2.server]) {
uint64_t *file = (uint64_t)[[profilePictureURL lastPathComponent] intValue];
promise = [SNFileServerAPIV2 download:file];
} else {
promise = [SNFileServerAPI downloadAttachmentFrom:profilePictureURL];
}
uint64_t *file = (uint64_t)[[profilePictureURL lastPathComponent] intValue];
AnyPromise *promise = [SNFileServerAPIV2 download:file];
[promise.then(^(NSData *data) {
@synchronized(self.currentAvatarDownloads)