Delete V1 OGS & file server
This commit is contained in:
parent
f552d51423
commit
d742fc1548
|
@ -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 */,
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.")
|
||||
}
|
||||
|
|
|
@ -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))" }
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,6 +0,0 @@
|
|||
|
||||
public struct OpenGroupInfo {
|
||||
public let displayName: String
|
||||
public let profilePictureURL: String?
|
||||
public let memberCount: Int
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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 }
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in New Issue