mirror of
https://github.com/oxen-io/session-ios.git
synced 2023-12-13 21:30:14 +01:00
Got the '/inbox' APIs and encryption/decryption/validation working
Added a few types to make the code more readable Added the inbox request to the polling Added a couple of properties to the TSContactThread to indicate the originating open group to support SOGS DMs Added code to store the latest message id for an open group inbox Added a bunch of documentation from the API docs into the OpenGroupAPI (and associated models) Updated the OpenGroupAPI to match the latest docs Fixed the incorrect structure of the SendDirectMessageRequest Fixed an incorrect inbox endpoint path Tweaked the batch response handling so it wouldn't fail to parse all responses if a single one failed Renamed IdPrefix to SessionId.Prefix and cleaned up the type to be more readable & self-documenting
This commit is contained in:
parent
faa8918cd4
commit
dbead5e3c8
40 changed files with 1221 additions and 324 deletions
|
@ -774,7 +774,7 @@
|
|||
FCB11D8C1A129A76002F93FB /* CoreMedia.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FCB11D8B1A129A76002F93FB /* CoreMedia.framework */; };
|
||||
FD5D200F27AA2B6000FEA984 /* MessageRequestResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD5D200E27AA2B6000FEA984 /* MessageRequestResponse.swift */; };
|
||||
FD5D201127AA331F00FEA984 /* ConfigurationMessage+Convenience.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD5D201027AA331F00FEA984 /* ConfigurationMessage+Convenience.swift */; };
|
||||
FD5D201E27B0D87C00FEA984 /* IdPrefix.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD5D201D27B0D87C00FEA984 /* IdPrefix.swift */; };
|
||||
FD5D201E27B0D87C00FEA984 /* SessionId.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD5D201D27B0D87C00FEA984 /* SessionId.swift */; };
|
||||
FD5D202027B0E67900FEA984 /* String+Encoding.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD5D201F27B0E67800FEA984 /* String+Encoding.swift */; };
|
||||
FD659AC027A7649600F12C02 /* MessageRequestsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD659ABF27A7649600F12C02 /* MessageRequestsViewController.swift */; };
|
||||
FD705A8C278CDB5600F16121 /* SAEScreenLockViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD705A8B278CDB5600F16121 /* SAEScreenLockViewController.swift */; };
|
||||
|
@ -793,7 +793,7 @@
|
|||
FD88BADB27A750F200BBC442 /* MessageRequestsMigration.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD88BADA27A750F200BBC442 /* MessageRequestsMigration.swift */; };
|
||||
FDC4380927B31D4E00C60D73 /* Error.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4380827B31D4E00C60D73 /* Error.swift */; };
|
||||
FDC4380B27B31D7E00C60D73 /* Request.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4380A27B31D7E00C60D73 /* Request.swift */; };
|
||||
FDC4381527B329CE00C60D73 /* NonceGenerator16Byte.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4381427B329CE00C60D73 /* NonceGenerator16Byte.swift */; };
|
||||
FDC4381527B329CE00C60D73 /* NonceGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4381427B329CE00C60D73 /* NonceGenerator.swift */; };
|
||||
FDC4381727B32EC700C60D73 /* Personalization.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4381627B32EC700C60D73 /* Personalization.swift */; };
|
||||
FDC4381A27B34EBA00C60D73 /* LegacyCompactPollBody.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4381927B34EBA00C60D73 /* LegacyCompactPollBody.swift */; };
|
||||
FDC4381C27B354AC00C60D73 /* LegacyPublicKeyBody.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4381B27B354AC00C60D73 /* LegacyPublicKeyBody.swift */; };
|
||||
|
@ -837,7 +837,6 @@
|
|||
FDC4389D27BA01F000C60D73 /* TestStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4389C27BA01F000C60D73 /* TestStorage.swift */; };
|
||||
FDC438A427BB107F00C60D73 /* UserBanRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC438A327BB107F00C60D73 /* UserBanRequest.swift */; };
|
||||
FDC438A627BB113A00C60D73 /* UserUnbanRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC438A527BB113A00C60D73 /* UserUnbanRequest.swift */; };
|
||||
FDC438A827BB11CD00C60D73 /* UserPermissionsRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC438A727BB11CD00C60D73 /* UserPermissionsRequest.swift */; };
|
||||
FDC438AA27BB12BB00C60D73 /* UserModeratorRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC438A927BB12BB00C60D73 /* UserModeratorRequest.swift */; };
|
||||
FDC438AC27BB145200C60D73 /* UserDeleteMessagesRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC438AB27BB145200C60D73 /* UserDeleteMessagesRequest.swift */; };
|
||||
FDC438AE27BB148700C60D73 /* UserDeleteMessagesResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC438AD27BB148700C60D73 /* UserDeleteMessagesResponse.swift */; };
|
||||
|
@ -1915,7 +1914,7 @@
|
|||
FCB11D8B1A129A76002F93FB /* CoreMedia.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreMedia.framework; path = System/Library/Frameworks/CoreMedia.framework; sourceTree = SDKROOT; };
|
||||
FD5D200E27AA2B6000FEA984 /* MessageRequestResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageRequestResponse.swift; sourceTree = "<group>"; };
|
||||
FD5D201027AA331F00FEA984 /* ConfigurationMessage+Convenience.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ConfigurationMessage+Convenience.swift"; sourceTree = "<group>"; };
|
||||
FD5D201D27B0D87C00FEA984 /* IdPrefix.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IdPrefix.swift; sourceTree = "<group>"; };
|
||||
FD5D201D27B0D87C00FEA984 /* SessionId.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionId.swift; sourceTree = "<group>"; };
|
||||
FD5D201F27B0E67800FEA984 /* String+Encoding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Encoding.swift"; sourceTree = "<group>"; };
|
||||
FD659ABF27A7649600F12C02 /* MessageRequestsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageRequestsViewController.swift; sourceTree = "<group>"; };
|
||||
FD705A8B278CDB5600F16121 /* SAEScreenLockViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SAEScreenLockViewController.swift; sourceTree = "<group>"; };
|
||||
|
@ -1937,7 +1936,7 @@
|
|||
FD9039443F7CB729CF71350E /* Pods-GlobalDependencies-FrameworkAndExtensionDependencies-SessionNotificationServiceExtension.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-GlobalDependencies-FrameworkAndExtensionDependencies-SessionNotificationServiceExtension.debug.xcconfig"; path = "Pods/Target Support Files/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-SessionNotificationServiceExtension/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-SessionNotificationServiceExtension.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
FDC4380827B31D4E00C60D73 /* Error.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Error.swift; sourceTree = "<group>"; };
|
||||
FDC4380A27B31D7E00C60D73 /* Request.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Request.swift; sourceTree = "<group>"; };
|
||||
FDC4381427B329CE00C60D73 /* NonceGenerator16Byte.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NonceGenerator16Byte.swift; sourceTree = "<group>"; };
|
||||
FDC4381427B329CE00C60D73 /* NonceGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NonceGenerator.swift; sourceTree = "<group>"; };
|
||||
FDC4381627B32EC700C60D73 /* Personalization.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Personalization.swift; sourceTree = "<group>"; };
|
||||
FDC4381927B34EBA00C60D73 /* LegacyCompactPollBody.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegacyCompactPollBody.swift; sourceTree = "<group>"; };
|
||||
FDC4381B27B354AC00C60D73 /* LegacyPublicKeyBody.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegacyPublicKeyBody.swift; sourceTree = "<group>"; };
|
||||
|
@ -1979,7 +1978,6 @@
|
|||
FDC4389C27BA01F000C60D73 /* TestStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestStorage.swift; sourceTree = "<group>"; };
|
||||
FDC438A327BB107F00C60D73 /* UserBanRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserBanRequest.swift; sourceTree = "<group>"; };
|
||||
FDC438A527BB113A00C60D73 /* UserUnbanRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserUnbanRequest.swift; sourceTree = "<group>"; };
|
||||
FDC438A727BB11CD00C60D73 /* UserPermissionsRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserPermissionsRequest.swift; sourceTree = "<group>"; };
|
||||
FDC438A927BB12BB00C60D73 /* UserModeratorRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserModeratorRequest.swift; sourceTree = "<group>"; };
|
||||
FDC438AB27BB145200C60D73 /* UserDeleteMessagesRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDeleteMessagesRequest.swift; sourceTree = "<group>"; };
|
||||
FDC438AD27BB148700C60D73 /* UserDeleteMessagesResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDeleteMessagesResponse.swift; sourceTree = "<group>"; };
|
||||
|
@ -2553,7 +2551,7 @@
|
|||
C38EF23D255B6D66007E1867 /* UIView+OWS.h */,
|
||||
C38EF23E255B6D66007E1867 /* UIView+OWS.m */,
|
||||
C38EF2EF255B6DBB007E1867 /* Weak.swift */,
|
||||
FD5D201D27B0D87C00FEA984 /* IdPrefix.swift */,
|
||||
FD5D201D27B0D87C00FEA984 /* SessionId.swift */,
|
||||
);
|
||||
path = General;
|
||||
sourceTree = "<group>";
|
||||
|
@ -3846,7 +3844,7 @@
|
|||
FDC4381F27B36ADC00C60D73 /* Endpoint.swift */,
|
||||
FDC4380827B31D4E00C60D73 /* Error.swift */,
|
||||
FDC4381627B32EC700C60D73 /* Personalization.swift */,
|
||||
FDC4381427B329CE00C60D73 /* NonceGenerator16Byte.swift */,
|
||||
FDC4381427B329CE00C60D73 /* NonceGenerator.swift */,
|
||||
FDC438C027BB4E6800C60D73 /* Dependencies.swift */,
|
||||
FDC438C227BB512200C60D73 /* SodiumProtocols.swift */,
|
||||
);
|
||||
|
@ -3871,7 +3869,6 @@
|
|||
FDC438C827BB706500C60D73 /* SendDirectMessageRequest.swift */,
|
||||
FDC438A327BB107F00C60D73 /* UserBanRequest.swift */,
|
||||
FDC438A527BB113A00C60D73 /* UserUnbanRequest.swift */,
|
||||
FDC438A727BB11CD00C60D73 /* UserPermissionsRequest.swift */,
|
||||
FDC438A927BB12BB00C60D73 /* UserModeratorRequest.swift */,
|
||||
FDC438AB27BB145200C60D73 /* UserDeleteMessagesRequest.swift */,
|
||||
FDC438AD27BB148700C60D73 /* UserDeleteMessagesResponse.swift */,
|
||||
|
@ -5087,7 +5084,7 @@
|
|||
B8F5F58325EC94A6003BF8D4 /* Collection+Subscripting.swift in Sources */,
|
||||
C33FDEF8255A656D00E217F9 /* Promise+Delaying.swift in Sources */,
|
||||
B8BC00C0257D90E30032E807 /* General.swift in Sources */,
|
||||
FD5D201E27B0D87C00FEA984 /* IdPrefix.swift in Sources */,
|
||||
FD5D201E27B0D87C00FEA984 /* SessionId.swift in Sources */,
|
||||
C32C5A24256DB7DB003C73A2 /* SNUserDefaults.swift in Sources */,
|
||||
C3D9E41F25676C870040E4F3 /* OWSPrimaryStorageProtocol.swift in Sources */,
|
||||
C3BBE0A72554D4DE0050F1E3 /* Promise+Retrying.swift in Sources */,
|
||||
|
@ -5211,7 +5208,7 @@
|
|||
FDC4381727B32EC700C60D73 /* Personalization.swift in Sources */,
|
||||
FDC4387827B5C35400C60D73 /* SendMessageRequest.swift in Sources */,
|
||||
C32C5EDC256DF501003C73A2 /* YapDatabaseConnection+OWS.m in Sources */,
|
||||
FDC4381527B329CE00C60D73 /* NonceGenerator16Byte.swift in Sources */,
|
||||
FDC4381527B329CE00C60D73 /* NonceGenerator.swift in Sources */,
|
||||
C3BBE0762554CDA60050F1E3 /* Configuration.swift in Sources */,
|
||||
FDC4387627B5BEF300C60D73 /* FileDownloadResponse.swift in Sources */,
|
||||
C35D76DB26606304009AA5FB /* ThreadUpdateBatcher.swift in Sources */,
|
||||
|
@ -5278,7 +5275,6 @@
|
|||
C32C5AAE256DBE8F003C73A2 /* TSInteraction.m in Sources */,
|
||||
C32C5D23256DD4C0003C73A2 /* Mention.swift in Sources */,
|
||||
C32C5FD6256E0346003C73A2 /* Notification+Thread.swift in Sources */,
|
||||
FDC438A827BB11CD00C60D73 /* UserPermissionsRequest.swift in Sources */,
|
||||
C3BBE0C72554F1570050F1E3 /* FixedWidthInteger+BigEndian.swift in Sources */,
|
||||
FDC438C127BB4E6800C60D73 /* Dependencies.swift in Sources */,
|
||||
FDC4383827B3863200C60D73 /* VersionResponse.swift in Sources */,
|
||||
|
|
|
@ -66,4 +66,5 @@ protocol MessageCellDelegate : AnyObject {
|
|||
func openURL(_ url: URL)
|
||||
func handleReplyButtonTapped(for viewItem: ConversationViewItem)
|
||||
func showUserDetails(for sessionID: String)
|
||||
func startThread(with sessionID: String, openGroupServer: String, openGroupPublicKey: String)
|
||||
}
|
||||
|
|
|
@ -497,12 +497,27 @@ final class VisibleMessageCell : MessageCell, LinkPreviewViewDelegate {
|
|||
let location = gestureRecognizer.location(in: self)
|
||||
if profilePictureView.frame.contains(location) && VisibleMessageCell.shouldShowProfilePicture(for: viewItem) {
|
||||
guard let message = viewItem.interaction as? TSIncomingMessage else { return }
|
||||
guard !message.isOpenGroupMessage else { return } // Do not show user details to prevent spam
|
||||
delegate?.showUserDetails(for: message.authorId)
|
||||
} else if replyButton.frame.contains(location) {
|
||||
|
||||
// For open groups only attempt to start a conversation if the author has a blinded id
|
||||
if message.isOpenGroupMessage {
|
||||
guard SessionId.Prefix(from: message.authorId) == .blinded else { return }
|
||||
guard let openGroup: OpenGroup = Storage.shared.getOpenGroup(for: message.uniqueThreadId) else { return }
|
||||
|
||||
delegate?.startThread(
|
||||
with: message.authorId,
|
||||
openGroupServer: openGroup.server,
|
||||
openGroupPublicKey: openGroup.publicKey
|
||||
)
|
||||
}
|
||||
else {
|
||||
delegate?.showUserDetails(for: message.authorId)
|
||||
}
|
||||
}
|
||||
else if replyButton.frame.contains(location) {
|
||||
UIImpactFeedbackGenerator(style: .heavy).impactOccurred()
|
||||
reply()
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
delegate?.handleViewItemTapped(viewItem, gestureRecognizer: gestureRecognizer)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -34,30 +34,32 @@ extension Storage {
|
|||
if let sender: String = message.sender, let openGroupID: String = openGroupID {
|
||||
guard let userEdKeyPair: Box.KeyPair = Storage.shared.getUserED25519KeyPair() else { return nil }
|
||||
|
||||
switch IdPrefix(with: sender) {
|
||||
case .blinded:
|
||||
let sodium: Sodium = Sodium()
|
||||
let serverNameParts: [String.SubSequence] = openGroupID.split(separator: ".")
|
||||
let serverName: String = serverNameParts[0..<(serverNameParts.count - 1)].joined(separator: ".")
|
||||
|
||||
// Note: This is horrible but it doesn't look like there is going to be a nicer way to do it...
|
||||
guard let serverPublicKey: String = Storage.shared.getOpenGroupPublicKey(for: serverName) else {
|
||||
return nil
|
||||
}
|
||||
guard let blindedKeyPair: Box.KeyPair = sodium.blindedKeyPair(serverPublicKey: serverPublicKey, edKeyPair: userEdKeyPair, genericHash: sodium.genericHash) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
isOutgoingMessage = (sender == IdPrefix.blinded.hexEncodedPublicKey(for: blindedKeyPair.publicKey))
|
||||
|
||||
case .standard, .unblinded:
|
||||
isOutgoingMessage = (
|
||||
message.sender == getUserPublicKey() ||
|
||||
sender == IdPrefix.unblinded.hexEncodedPublicKey(for: userEdKeyPair.publicKey)
|
||||
)
|
||||
|
||||
case .none:
|
||||
isOutgoingMessage = false
|
||||
if let senderSessionId: SessionId = SessionId(from: sender) {
|
||||
switch senderSessionId.prefix {
|
||||
case .blinded:
|
||||
let sodium: Sodium = Sodium()
|
||||
let serverNameParts: [String.SubSequence] = openGroupID.split(separator: ".")
|
||||
let serverName: String = serverNameParts[0..<(serverNameParts.count - 1)].joined(separator: ".")
|
||||
|
||||
// Note: This is horrible but it doesn't look like there is going to be a nicer way to do it...
|
||||
guard let serverPublicKey: String = Storage.shared.getOpenGroupPublicKey(for: serverName) else {
|
||||
return nil
|
||||
}
|
||||
guard let blindedKeyPair: Box.KeyPair = sodium.blindedKeyPair(serverPublicKey: serverPublicKey, edKeyPair: userEdKeyPair, genericHash: sodium.genericHash) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
isOutgoingMessage = (sender == SessionId(.blinded, publicKey: blindedKeyPair.publicKey).hexString)
|
||||
|
||||
case .standard, .unblinded:
|
||||
isOutgoingMessage = (
|
||||
message.sender == getUserPublicKey() ||
|
||||
sender == SessionId(.unblinded, publicKey: userEdKeyPair.publicKey).hexString
|
||||
)
|
||||
}
|
||||
}
|
||||
else {
|
||||
isOutgoingMessage = false
|
||||
}
|
||||
}
|
||||
else {
|
||||
|
|
|
@ -110,6 +110,25 @@ public final class MessageSendJob : NSObject, Job, NSCoding { // NSObject/NSCodi
|
|||
fileIds: fileIds
|
||||
)
|
||||
}
|
||||
else if rawDestination.removePrefix("openGroupInbox(") {
|
||||
guard rawDestination.removeSuffix(")") else { return nil }
|
||||
|
||||
let components = rawDestination
|
||||
.split(separator: ",")
|
||||
.map { String($0).trimmingCharacters(in: .whitespacesAndNewlines) }
|
||||
|
||||
guard components.count == 3 else { return nil }
|
||||
|
||||
let server: String = components[0]
|
||||
let openGroupPublicKey: String = components[1]
|
||||
let blindedPublicKey: String = components[2]
|
||||
|
||||
destination = .openGroupInbox(
|
||||
server: server,
|
||||
openGroupPublicKey: openGroupPublicKey,
|
||||
blindedPublicKey: blindedPublicKey
|
||||
)
|
||||
}
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
|
@ -141,6 +160,9 @@ public final class MessageSendJob : NSObject, Job, NSCoding { // NSObject/NSCodi
|
|||
"openGroupV4(\(room), \(server), \(whisperToString), \(whisperModsString), [\(fileIdString)])",
|
||||
forKey: "destination"
|
||||
)
|
||||
|
||||
case .openGroupInbox(let server, let openGroupPublicKey, let blindedPublicKey):
|
||||
coder.encode("openGroupInbox(\(server), \(openGroupPublicKey), \(blindedPublicKey)", forKey: "destination")
|
||||
}
|
||||
|
||||
coder.encode(id, forKey: "id")
|
||||
|
|
|
@ -12,9 +12,22 @@ public extension Message {
|
|||
whisperMods: Bool = false,
|
||||
fileIds: [Int64]? = nil // TODO: Handle 'fileIds'
|
||||
)
|
||||
case openGroupInbox(server: String, openGroupPublicKey: String, blindedPublicKey: String)
|
||||
|
||||
static func from(_ thread: TSThread) -> Message.Destination {
|
||||
if let thread = thread as? TSContactThread {
|
||||
if SessionId.Prefix(from: thread.contactSessionID()) == .blinded {
|
||||
guard let server: String = thread.originalOpenGroupServer, let publicKey: String = thread.originalOpenGroupPublicKey else {
|
||||
preconditionFailure("Attempting to send message to blinded id without the Open Group information")
|
||||
}
|
||||
|
||||
return .openGroupInbox(
|
||||
server: server,
|
||||
openGroupPublicKey: publicKey,
|
||||
blindedPublicKey: thread.contactSessionID()
|
||||
)
|
||||
}
|
||||
|
||||
return .contact(publicKey: thread.contactSessionID())
|
||||
}
|
||||
|
||||
|
|
|
@ -70,24 +70,39 @@ extension OpenGroupAPI {
|
|||
// MARK: - BatchSubResponse<T>
|
||||
|
||||
struct BatchSubResponse<T: Codable>: Codable {
|
||||
/// The numeric http response code (e.g. 200 for success)
|
||||
let code: Int32
|
||||
|
||||
/// This should always include the content type of the request
|
||||
let headers: [String: String]
|
||||
let body: T
|
||||
|
||||
/// The body of the request; will be plain json if content-type is `application/json`, otherwise it will be base64 encoded data
|
||||
let body: T?
|
||||
|
||||
/// A flag to indicate that there was a body but it failed to parse
|
||||
let failedToParseBody: Bool
|
||||
}
|
||||
|
||||
// MARK: - BatchRequestInfo<T, R>
|
||||
|
||||
struct BatchRequestInfo<T: Encodable, R: Codable>: BatchRequestInfoType {
|
||||
struct BatchRequestInfo<T: Encodable>: BatchRequestInfoType {
|
||||
let request: Request<T>
|
||||
let responseType: Codable.Type
|
||||
|
||||
var endpoint: Endpoint { request.endpoint }
|
||||
|
||||
init(request: Request<T>, responseType: R.Type) {
|
||||
init<R: Codable>(request: Request<T>, responseType: R.Type) {
|
||||
self.request = request
|
||||
self.responseType = BatchSubResponse<R>.self
|
||||
}
|
||||
|
||||
init(request: Request<T>) {
|
||||
self.init(
|
||||
request: request,
|
||||
responseType: NoResponse.self
|
||||
)
|
||||
}
|
||||
|
||||
func toSubRequest() -> BatchSubRequest {
|
||||
return BatchSubRequest(request: request)
|
||||
}
|
||||
|
@ -97,7 +112,21 @@ extension OpenGroupAPI {
|
|||
|
||||
typealias BatchRequest = [BatchSubRequest]
|
||||
typealias BatchResponseTypes = [Codable.Type]
|
||||
typealias BatchResponse = [(OnionRequestResponseInfoType, Codable)]
|
||||
typealias BatchResponse = [(OnionRequestResponseInfoType, Codable?)]
|
||||
}
|
||||
|
||||
extension OpenGroupAPI.BatchSubResponse {
|
||||
init(from decoder: Decoder) throws {
|
||||
let container: KeyedDecodingContainer<CodingKeys> = try decoder.container(keyedBy: CodingKeys.self)
|
||||
let body: T? = try? container.decode(T.self, forKey: .body)
|
||||
|
||||
self = OpenGroupAPI.BatchSubResponse(
|
||||
code: try container.decode(Int32.self, forKey: .code),
|
||||
headers: try container.decode([String: String].self, forKey: .headers),
|
||||
body: body,
|
||||
failedToParseBody: (body == nil && T.self != OpenGroupAPI.NoResponse.self)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - BatchRequestInfoType
|
||||
|
|
|
@ -7,13 +7,24 @@ extension OpenGroupAPI {
|
|||
enum CodingKeys: String, CodingKey {
|
||||
case id
|
||||
case sender
|
||||
case posted = "posted_at"
|
||||
case expires = "expires_at"
|
||||
case base64EncodedData = "data"
|
||||
case base64EncodedMessage = "message"
|
||||
}
|
||||
|
||||
/// The unique integer message id
|
||||
public let id: Int64
|
||||
|
||||
/// The (blinded) Session ID of the sender of the message
|
||||
public let sender: String
|
||||
|
||||
/// Unix timestamp when the message was received by SOGS
|
||||
public let posted: TimeInterval
|
||||
|
||||
/// Unix timestamp when SOGS will expire and delete the message
|
||||
public let expires: TimeInterval
|
||||
public let base64EncodedData: String
|
||||
|
||||
/// The encrypted message body
|
||||
public let base64EncodedMessage: String
|
||||
}
|
||||
}
|
||||
|
|
|
@ -51,10 +51,10 @@ extension OpenGroupAPI.Message {
|
|||
throw OpenGroupAPI.Error.parsingFailed
|
||||
}
|
||||
|
||||
// Verify the signature based on the IdPrefix
|
||||
// Verify the signature based on the SessionId.Prefix type
|
||||
let publicKey: Data = Data(hex: sender.removingIdPrefixIfNeeded())
|
||||
|
||||
switch IdPrefix(with: sender) {
|
||||
switch SessionId.Prefix(from: sender) {
|
||||
case .blinded:
|
||||
guard dependencies.sign.verify(message: data.bytes, publicKey: publicKey.bytes, signature: signature.bytes) else {
|
||||
SNLog("Ignoring message with invalid signature.")
|
||||
|
|
|
@ -10,8 +10,13 @@ extension OpenGroupAPI {
|
|||
case pinnedBy = "pinned_by"
|
||||
}
|
||||
|
||||
/// The numeric message id
|
||||
let id: Int64
|
||||
|
||||
/// The unix timestamp when the message was pinned
|
||||
let pinnedAt: TimeInterval
|
||||
|
||||
/// The session ID of the admin who pinned this message (which is not necessarily the same as the author of the message)
|
||||
let pinnedBy: String
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,15 +6,15 @@ extension OpenGroupAPI {
|
|||
public struct Room: Codable {
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case token
|
||||
case created
|
||||
case name
|
||||
case description
|
||||
case imageId = "image_id"
|
||||
|
||||
case infoUpdates = "info_updates"
|
||||
case messageSequence = "message_sequence"
|
||||
case created
|
||||
|
||||
case activeUsers = "active_users"
|
||||
case activeUsersCutoff = "active_users_cutoff"
|
||||
case imageId = "image_id"
|
||||
case pinnedMessages = "pinned_messages"
|
||||
|
||||
case admin
|
||||
|
@ -29,40 +29,118 @@ extension OpenGroupAPI {
|
|||
|
||||
case read
|
||||
case defaultRead = "default_read"
|
||||
case defaultAccessible = "default_accessible"
|
||||
case write
|
||||
case defaultWrite = "default_write"
|
||||
case upload
|
||||
case defaultUpload = "default_upload"
|
||||
}
|
||||
|
||||
/// The room token as used in a URL, e.g. "sudoku"
|
||||
public let token: String
|
||||
public let created: TimeInterval
|
||||
|
||||
/// The room name typically shown to users, e.g. "Sodoku Solvers"
|
||||
public let name: String
|
||||
|
||||
/// Text description of the room, e.g. "All the best sodoku discussion!"
|
||||
public let description: String?
|
||||
|
||||
/// Monotonic integer counter that increases whenever the room's metadata changes
|
||||
public let infoUpdates: Int64
|
||||
|
||||
/// Monotonic room post counter that increases each time a message is posted, edited, or deleted in this room
|
||||
///
|
||||
/// Note that changes to this field do not imply an update the room's info_updates value, nor vice versa
|
||||
public let messageSequence: Int64
|
||||
|
||||
/// Unix timestamp (as a float) of the room creation time. Note that unlike earlier versions of SOGS, this is a proper
|
||||
/// seconds-since-epoch unix timestamp, not a javascript-style millisecond value
|
||||
public let created: TimeInterval
|
||||
|
||||
/// Number of recently active users in the room over a recent time period (as given in the active_users_cutoff value)
|
||||
///
|
||||
/// Users are considered "active" if they have accessed the room (checking for new messages, etc.) at least once in the given period
|
||||
///
|
||||
/// **Note:** changes to this field do not update the room's info_updates value
|
||||
public let activeUsers: Int64
|
||||
|
||||
/// The length of time (in seconds) of the active_users period. Defaults to a week (604800), but the open group administrator can configure it
|
||||
public let activeUsersCutoff: Int64
|
||||
|
||||
/// File ID of an uploaded file containing the room's image
|
||||
///
|
||||
/// Omitted if there is no image
|
||||
public let imageId: Int64?
|
||||
|
||||
public let infoUpdates: Int64
|
||||
public let messageSequence: Int64
|
||||
public let activeUsers: Int64
|
||||
public let activeUsersCutoff: Int64
|
||||
/// Array of pinned message information (omitted entirely if there are no pinned messages)
|
||||
public let pinnedMessages: [PinnedMessage]?
|
||||
|
||||
/// This flag is `true` if the current user has admin permissions in the room
|
||||
public let admin: Bool
|
||||
|
||||
/// This flag is `true` if the current user is a global admin
|
||||
///
|
||||
/// This is not exclusive of `globalModerator`/`moderator`/`admin` (a global admin will have all four set to `true`)
|
||||
public let globalAdmin: Bool
|
||||
|
||||
/// Array of Session IDs of the room's publicly viewable moderators
|
||||
///
|
||||
/// This does not include room moderator nor hidden admins
|
||||
public let admins: [String]
|
||||
|
||||
/// Array of Session IDs of the room's publicly hidden admins
|
||||
///
|
||||
/// This field is only included if the requesting user has moderator or admin permissions, and is omitted if empty
|
||||
public let hiddenAdmins: [String]?
|
||||
|
||||
/// This flag is `true` if the current user has moderator permissions in the room
|
||||
public let moderator: Bool
|
||||
|
||||
/// This flag is `true` if the current user is a global moderator
|
||||
///
|
||||
/// This is not exclusive of `moderator` (a global moderator will have both flags set to `true`)
|
||||
public let globalModerator: Bool
|
||||
|
||||
/// Array of Session IDs of the room's publicly viewable moderators
|
||||
///
|
||||
/// This does not include room administrators nor hidden moderators
|
||||
public let moderators: [String]
|
||||
|
||||
/// Array of Session IDs of the room's publicly hidden moderators
|
||||
///
|
||||
/// This field is only included if the requesting user has moderator or admin permissions, and is omitted if empty
|
||||
public let hiddenModerators: [String]?
|
||||
|
||||
/// This flag indicates whether the **current** user has permission to read the room's messages
|
||||
///
|
||||
/// **Note:** If this value is `false` the user only has access the room metadata
|
||||
public let read: Bool
|
||||
public let defaultRead: Bool
|
||||
|
||||
/// This field indicates whether new users have read permissions in the room
|
||||
///
|
||||
/// It is included in the response only if the requesting user has moderator or admin permissions
|
||||
public let defaultRead: Bool?
|
||||
|
||||
/// This field indicates whether new users have access permissions in the room
|
||||
///
|
||||
/// It is included in the response only if the requesting user has moderator or admin permissions
|
||||
public let defaultAccessible: Bool?
|
||||
|
||||
/// This flag indicates whether the **current** user has permission to post messages in the room
|
||||
public let write: Bool
|
||||
public let defaultWrite: Bool
|
||||
|
||||
/// This field indicates whether new users have write permissions in the room
|
||||
///
|
||||
/// It is included in the response only if the requesting user has moderator or admin permissions
|
||||
public let defaultWrite: Bool?
|
||||
|
||||
/// This flag indicates whether the **current** user has permission to upload files to the room
|
||||
public let upload: Bool
|
||||
public let defaultUpload: Bool
|
||||
|
||||
/// This field indicates whether new users have upload permissions in the room
|
||||
///
|
||||
/// It is included in the response only if the requesting user has moderator or admin permissions
|
||||
public let defaultUpload: Bool?
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -74,15 +152,15 @@ extension OpenGroupAPI.Room {
|
|||
|
||||
self = OpenGroupAPI.Room(
|
||||
token: try container.decode(String.self, forKey: .token),
|
||||
created: try container.decode(TimeInterval.self, forKey: .created),
|
||||
name: try container.decode(String.self, forKey: .name),
|
||||
description: try? container.decode(String.self, forKey: .description),
|
||||
imageId: try? container.decode(Int64.self, forKey: .imageId),
|
||||
|
||||
infoUpdates: try container.decode(Int64.self, forKey: .infoUpdates),
|
||||
messageSequence: try container.decode(Int64.self, forKey: .messageSequence),
|
||||
created: try container.decode(TimeInterval.self, forKey: .created),
|
||||
|
||||
activeUsers: try container.decode(Int64.self, forKey: .activeUsers),
|
||||
activeUsersCutoff: try container.decode(Int64.self, forKey: .activeUsersCutoff),
|
||||
imageId: try? container.decode(Int64.self, forKey: .imageId),
|
||||
pinnedMessages: try? container.decode([OpenGroupAPI.PinnedMessage].self, forKey: .pinnedMessages),
|
||||
|
||||
admin: ((try? container.decode(Bool.self, forKey: .admin)) ?? false),
|
||||
|
@ -96,12 +174,12 @@ extension OpenGroupAPI.Room {
|
|||
hiddenModerators: try? container.decode([String].self, forKey: .hiddenModerators),
|
||||
|
||||
read: try container.decode(Bool.self, forKey: .read),
|
||||
defaultRead: ((try? container.decode(Bool.self, forKey: .defaultRead)) ?? false),
|
||||
defaultRead: try? container.decode(Bool.self, forKey: .defaultRead),
|
||||
defaultAccessible: try? container.decode(Bool.self, forKey: .defaultAccessible),
|
||||
write: try container.decode(Bool.self, forKey: .write),
|
||||
defaultWrite: ((try? container.decode(Bool.self, forKey: .defaultWrite)) ?? false),
|
||||
defaultWrite: try? container.decode(Bool.self, forKey: .defaultWrite),
|
||||
upload: try container.decode(Bool.self, forKey: .upload),
|
||||
defaultUpload: ((try? container.decode(Bool.self, forKey: .defaultUpload)) ?? false)
|
||||
defaultUpload: try? container.decode(Bool.self, forKey: .defaultUpload)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -17,6 +17,7 @@ extension OpenGroupAPI {
|
|||
|
||||
case read
|
||||
case defaultRead = "default_read"
|
||||
case defaultAccessible = "default_accessible"
|
||||
case write
|
||||
case defaultWrite = "default_write"
|
||||
case upload
|
||||
|
@ -25,22 +26,65 @@ extension OpenGroupAPI {
|
|||
case details
|
||||
}
|
||||
|
||||
/// The room token as used in a URL, e.g. "sudoku"
|
||||
public let token: String?
|
||||
public let activeUsers: Int64?
|
||||
|
||||
public let admin: Bool?
|
||||
public let globalAdmin: Bool?
|
||||
/// Number of recently active users in the room over a recent time period (as given in the active_users_cutoff value)
|
||||
///
|
||||
/// Users are considered "active" if they have accessed the room (checking for new messages, etc.) at least once in the given period
|
||||
///
|
||||
/// **Note:** changes to this field do not update the room's info_updates value
|
||||
public let activeUsers: Int64
|
||||
|
||||
public let moderator: Bool?
|
||||
public let globalModerator: Bool?
|
||||
/// This flag is `true` if the current user has admin permissions in the room
|
||||
public let admin: Bool
|
||||
|
||||
public let read: Bool?
|
||||
/// This flag is `true` if the current user is a global admin
|
||||
///
|
||||
/// This is not exclusive of `globalModerator`/`moderator`/`admin` (a global admin will have all four set to `true`)
|
||||
public let globalAdmin: Bool
|
||||
|
||||
/// This flag is `true` if the current user has moderator permissions in the room
|
||||
public let moderator: Bool
|
||||
|
||||
/// This flag is `true` if the current user is a global moderator
|
||||
///
|
||||
/// This is not exclusive of `moderator` (a global moderator will have both flags set to `true`)
|
||||
public let globalModerator: Bool
|
||||
|
||||
/// This flag indicates whether the **current** user has permission to read the room's messages
|
||||
///
|
||||
/// **Note:** If this value is `false` the user only has access the room metadata
|
||||
public let read: Bool
|
||||
|
||||
/// This field indicates whether new users have read permissions in the room
|
||||
///
|
||||
/// It is included in the response only if the requesting user has moderator or admin permissions
|
||||
public let defaultRead: Bool?
|
||||
public let write: Bool?
|
||||
|
||||
/// This field indicates whether new users have access permissions in the room
|
||||
///
|
||||
/// It is included in the response only if the requesting user has moderator or admin permissions
|
||||
public let defaultAccessible: Bool?
|
||||
|
||||
/// This flag indicates whether the **current** user has permission to post messages in the room
|
||||
public let write: Bool
|
||||
|
||||
/// This field indicates whether new users have write permissions in the room
|
||||
///
|
||||
/// It is included in the response only if the requesting user has moderator or admin permissions
|
||||
public let defaultWrite: Bool?
|
||||
public let upload: Bool?
|
||||
|
||||
/// This flag indicates whether the **current** user has permission to upload files to the room
|
||||
public let upload: Bool
|
||||
|
||||
/// This field indicates whether new users have upload permissions in the room
|
||||
///
|
||||
/// It is included in the response only if the requesting user has moderator or admin permissions
|
||||
public let defaultUpload: Bool?
|
||||
|
||||
/// The full room metadata (as would be returned by the `/rooms/{roomToken}` endpoint)
|
||||
///
|
||||
/// Only populated and different if the `info_updates` counter differs from the provided `info_updated` value
|
||||
public let details: Room?
|
||||
}
|
||||
|
@ -59,6 +103,7 @@ extension OpenGroupAPI.RoomPollInfo {
|
|||
globalModerator: room.globalModerator,
|
||||
read: room.read,
|
||||
defaultRead: room.defaultRead,
|
||||
defaultAccessible: room.defaultAccessible,
|
||||
write: room.write,
|
||||
defaultWrite: room.defaultWrite,
|
||||
upload: room.upload,
|
||||
|
@ -67,3 +112,32 @@ extension OpenGroupAPI.RoomPollInfo {
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Decoding
|
||||
|
||||
extension OpenGroupAPI.RoomPollInfo {
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container: KeyedDecodingContainer<CodingKeys> = try decoder.container(keyedBy: CodingKeys.self)
|
||||
|
||||
self = OpenGroupAPI.RoomPollInfo(
|
||||
token: try container.decode(String.self, forKey: .token),
|
||||
activeUsers: try container.decode(Int64.self, forKey: .activeUsers),
|
||||
|
||||
admin: ((try? container.decode(Bool.self, forKey: .admin)) ?? false),
|
||||
globalAdmin: ((try? container.decode(Bool.self, forKey: .globalAdmin)) ?? false),
|
||||
|
||||
moderator: ((try? container.decode(Bool.self, forKey: .moderator)) ?? false),
|
||||
globalModerator: ((try? container.decode(Bool.self, forKey: .globalModerator)) ?? false),
|
||||
|
||||
read: try container.decode(Bool.self, forKey: .read),
|
||||
defaultRead: try? container.decode(Bool.self, forKey: .defaultRead),
|
||||
defaultAccessible: try? container.decode(Bool.self, forKey: .defaultAccessible),
|
||||
write: try container.decode(Bool.self, forKey: .write),
|
||||
defaultWrite: try? container.decode(Bool.self, forKey: .defaultWrite),
|
||||
upload: try container.decode(Bool.self, forKey: .upload),
|
||||
defaultUpload: try? container.decode(Bool.self, forKey: .defaultUpload),
|
||||
|
||||
details: try? container.decode(OpenGroupAPI.Room.self, forKey: .details)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,14 +4,14 @@ import Foundation
|
|||
|
||||
extension OpenGroupAPI {
|
||||
public struct SendDirectMessageRequest: Codable {
|
||||
let data: Data
|
||||
let message: Data
|
||||
|
||||
// MARK: - Encodable
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container: KeyedEncodingContainer<CodingKeys> = encoder.container(keyedBy: CodingKeys.self)
|
||||
|
||||
try container.encode(data.base64EncodedString(), forKey: .data)
|
||||
try container.encode(message.base64EncodedString(), forKey: .message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,10 +12,34 @@ extension OpenGroupAPI {
|
|||
case fileIds = "files"
|
||||
}
|
||||
|
||||
/// The serialized message body (encoded in base64 when encoding)
|
||||
let data: Data
|
||||
|
||||
/// A 64-byte Ed25519 signature of the message body, signed by the current user's keys (encoded in base64 when
|
||||
/// encoding - ie. 88 base64 chars)
|
||||
let signature: Data
|
||||
|
||||
/// If present this indicates that this message is a whisper that should only be shown to the given user (via their sessionId)
|
||||
let whisperTo: String?
|
||||
let whisperMods: Bool
|
||||
|
||||
/// If `true`, then this message will be visible to moderators but not ordinary users
|
||||
///
|
||||
/// If this and `whisper_to` are used together then the message will be visible to the given user and any room
|
||||
/// moderators (this can be used, for instance, to issue a warning to a user that only the user and other mods can see)
|
||||
///
|
||||
/// **Note:** Only moderators may set this flag
|
||||
let whisperMods: Bool?
|
||||
|
||||
/// Array of file IDs of new files uploaded as attachments of this post
|
||||
///
|
||||
/// This is required to preserve uploads for the default expiry period (15 days, unless otherwise configured by the SOGS
|
||||
/// administrator); uploaded files that are not attached to a post will be deleted much sooner
|
||||
///
|
||||
/// If any of the given file ids are already associated with another message then the association is ignored (i.e. the files remain
|
||||
/// associated with the original message)
|
||||
///
|
||||
/// When submitting a message edit this field must contain the IDs of any newly uploaded files that are part of the edit; existing
|
||||
/// attachment IDs may also be included, but are not required
|
||||
let fileIds: [Int64]?
|
||||
|
||||
// MARK: - Initialization
|
||||
|
@ -24,7 +48,7 @@ extension OpenGroupAPI {
|
|||
data: Data,
|
||||
signature: Data,
|
||||
whisperTo: String? = nil,
|
||||
whisperMods: Bool = false,
|
||||
whisperMods: Bool? = nil,
|
||||
fileIds: [Int64]? = nil
|
||||
) {
|
||||
self.data = data
|
||||
|
@ -42,7 +66,7 @@ extension OpenGroupAPI {
|
|||
try container.encode(data.base64EncodedString(), forKey: .data)
|
||||
try container.encode(signature.base64EncodedString(), forKey: .signature)
|
||||
try container.encodeIfPresent(whisperTo, forKey: .whisperTo)
|
||||
try container.encode(whisperMods, forKey: .whisperMods)
|
||||
try container.encodeIfPresent(whisperMods, forKey: .whisperMods)
|
||||
try container.encodeIfPresent(fileIds, forKey: .fileIds)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,9 +4,25 @@ import Foundation
|
|||
|
||||
extension OpenGroupAPI {
|
||||
public struct UpdateMessageRequest: Codable {
|
||||
/// The serialized message body (encoded in base64 when encoding)
|
||||
let data: Data
|
||||
|
||||
/// A 64-byte Ed25519 signature of the message body, signed by the current user's keys (encoded in base64 when
|
||||
/// encoding - ie. 88 base64 chars)
|
||||
let signature: Data
|
||||
|
||||
/// Array of file IDs of new files uploaded as attachments of this post
|
||||
///
|
||||
/// This is required to preserve uploads for the default expiry period (15 days, unless otherwise configured by the SOGS
|
||||
/// administrator); uploaded files that are not attached to a post will be deleted much sooner
|
||||
///
|
||||
/// If any of the given file ids are already associated with another message then the association is ignored (i.e. the files remain
|
||||
/// associated with the original message)
|
||||
///
|
||||
/// This field must contain the IDs of any newly uploaded files that are part of the edit; existing attachment IDs may also be
|
||||
/// included, but are not required
|
||||
let fileIds: [Int64]?
|
||||
|
||||
// MARK: - Encodable
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
|
|
|
@ -4,8 +4,28 @@ import Foundation
|
|||
|
||||
extension OpenGroupAPI {
|
||||
struct UserBanRequest: Codable {
|
||||
/// List of one or more room tokens from which the user should be banned (the invoking user must be a `moderator`
|
||||
/// of all of the given rooms
|
||||
///
|
||||
/// This may be set to the single-element list ["*"] to ban the user from all rooms in which the invoking user has `moderator`
|
||||
/// permissions (the call will succeed if the calling user is a moderator in at least one channel)
|
||||
///
|
||||
/// Exclusive of `global`
|
||||
let rooms: [String]?
|
||||
|
||||
/// If true then apply the ban at the server-wide global level: the user will be banned from the server entirely—not merely from
|
||||
/// all rooms, but also from calling any other server request (the invoking user must be a global `moderator` in order to add
|
||||
/// a global ban
|
||||
///
|
||||
/// Exclusive of rooms
|
||||
let global: Bool?
|
||||
|
||||
/// Optional value specifying a time limit on the ban, in seconds
|
||||
///
|
||||
/// The applied ban will expire and be removed after the given interval - If omitted (or `null`) then the ban is permanent
|
||||
///
|
||||
/// If this endpoint is called multiple times then the timeout of the last call takes effect (eg. a permanent ban can be replaced
|
||||
/// with a time-limited ban by calling the endpoint again with a timeout value, and vice versa)
|
||||
let timeout: TimeInterval?
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,7 +25,7 @@ extension OpenGroupAPI {
|
|||
/// that may also be present.
|
||||
///
|
||||
/// See the `admin` parameter description for information on how `admin` and `moderator` parameters interact.
|
||||
let moderator: Bool
|
||||
let moderator: Bool?
|
||||
|
||||
/// If `true` then this user will be granted moderator and admin permissions to the given rooms or server. Admin permissions are
|
||||
/// required to appoint new moderators or administrators and to alter room info such as the image, adding/removing pinned messages,
|
||||
|
@ -51,7 +51,7 @@ extension OpenGroupAPI {
|
|||
/// - `moderator=false, admin=false`: exactly the same as above.
|
||||
/// - `moderator=false, admin=true`: this combination is *not* *permitted* (because admin permissions imply moderator
|
||||
/// permissions) and will result in Bad Request error if given.
|
||||
let admin: Bool
|
||||
let admin: Bool?
|
||||
|
||||
/// Whether this user should be a "visible" moderator or admin in the specified rooms (or globally). Visible moderators are identified to all
|
||||
/// room users (e.g. via a special status badge in Session clients).
|
||||
|
|
|
@ -1,13 +0,0 @@
|
|||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
|
||||
extension OpenGroupAPI {
|
||||
struct UserPermissionsRequest: Codable {
|
||||
let rooms: [String]
|
||||
let timeout: TimeInterval
|
||||
let read: Bool
|
||||
let write: Bool
|
||||
let upload: Bool
|
||||
}
|
||||
}
|
|
@ -4,7 +4,18 @@ import Foundation
|
|||
|
||||
extension OpenGroupAPI {
|
||||
struct UserUnbanRequest: Codable {
|
||||
/// List of one or more room tokens from which the user should be banned (the invoking user must be a `moderator`
|
||||
/// of all of the given rooms
|
||||
///
|
||||
/// This may be set to the single-element list ["*"] to ban the user from all rooms in which the invoking user has `moderator`
|
||||
/// permissions (the call will succeed if the calling user is a moderator in at least one channel)
|
||||
///
|
||||
/// Exclusive of `global`
|
||||
let rooms: [String]?
|
||||
|
||||
/// If true then remove a server-wide global ban
|
||||
///
|
||||
/// Exclusive of rooms
|
||||
let global: Bool?
|
||||
}
|
||||
}
|
||||
|
|
|
@ -40,10 +40,13 @@ public final class OpenGroupAPI: NSObject {
|
|||
/// - For each room:
|
||||
/// - Poll Info
|
||||
/// - Messages (includes additions and deletions)
|
||||
public static func poll(_ server: String, using dependencies: Dependencies = Dependencies()) -> Promise<[Endpoint: (OnionRequestResponseInfoType, Codable)]> {
|
||||
/// - Inbox for the server
|
||||
public static func poll(_ server: String, using dependencies: Dependencies = Dependencies()) -> Promise<[Endpoint: (OnionRequestResponseInfoType, Codable?)]> {
|
||||
// Store a local copy of the cached state for this server
|
||||
let hadPerformedInitialPoll: Bool = (hasPerformedInitialPoll[server] == true)
|
||||
let originalTimeSinceLastPoll: TimeInterval = (timeSinceLastPoll[server] ?? min(lastPollTime, timeSinceLastOpen))
|
||||
let maybeLastInboxMessageId: Int64? = dependencies.storage.getOpenGroupInboxLatestMessageId(for: server)
|
||||
let lastInboxMessageId: Int64 = (maybeLastInboxMessageId ?? 0)
|
||||
|
||||
// Update the cached state for this server
|
||||
hasPerformedInitialPoll[server] = true
|
||||
|
@ -100,16 +103,32 @@ public final class OpenGroupAPI: NSObject {
|
|||
]
|
||||
}
|
||||
)
|
||||
.appending(
|
||||
// Inbox
|
||||
BatchRequestInfo(
|
||||
request: Request<NoBody>(
|
||||
server: server,
|
||||
endpoint: (maybeLastInboxMessageId == nil ?
|
||||
.inbox :
|
||||
.inboxSince(id: lastInboxMessageId)
|
||||
)
|
||||
// TODO: Limit?
|
||||
// queryParameters: [ .limit: 256 ]
|
||||
),
|
||||
responseType: [DirectMessage].self
|
||||
)
|
||||
)
|
||||
|
||||
return batch(server, requests: requestResponseType, using: dependencies)
|
||||
}
|
||||
|
||||
/// This is used, for example, to poll multiple rooms on the same server for updates in a single query rather than needing to make multiple requests for each room.
|
||||
/// Submits multiple requests wrapped up in a single request, runs them all, then returns the result of each one
|
||||
///
|
||||
/// No guarantee is made as to the order in which sub-requests are processed; use the `/sequence` instead if you need that.
|
||||
/// Requests are performed independently, that is, if one fails the others will still be attempted - there is no guarantee on the order in which requests will be
|
||||
/// carried out (for sequential, related requests invoke via `/sequence` instead)
|
||||
///
|
||||
/// For contained subrequests that specify a body (i.e. POST or PUT requests) exactly one of `json`, `b64`, or `bytes` must be provided with the request body.
|
||||
private static func batch(_ server: String, requests: [BatchRequestInfoType], using dependencies: Dependencies = Dependencies()) -> Promise<[Endpoint: (OnionRequestResponseInfoType, Codable)]> {
|
||||
private static func batch(_ server: String, requests: [BatchRequestInfoType], using dependencies: Dependencies = Dependencies()) -> Promise<[Endpoint: (OnionRequestResponseInfoType, Codable?)]> {
|
||||
let requestBody: BatchRequest = requests.map { $0.toSubRequest() }
|
||||
let responseTypes = requests.map { $0.responseType }
|
||||
|
||||
|
@ -130,12 +149,16 @@ public final class OpenGroupAPI: NSObject {
|
|||
}
|
||||
}
|
||||
|
||||
/// The requests are guaranteed to be performed sequentially in the order given in the request and will abort if any request does not return a status-`2xx` response.
|
||||
/// This is like `/batch`, except that it guarantees to perform requests sequentially in the order provided and will stop processing requests if the previous request
|
||||
/// returned a non-`2xx` response
|
||||
///
|
||||
/// For example, this can be used to ban and delete all of a user's messages by sequencing the ban followed by the `delete_all`: if the ban fails (e.g. because
|
||||
/// permission is denied) then the `delete_all` will not occur. The batch body and response are identical to the `/batch` endpoint; requests that are not
|
||||
/// carried out because of an earlier failure will have a response code of `412` (Precondition Failed)."
|
||||
private static func sequence(_ server: String, requests: [BatchRequestInfoType], using dependencies: Dependencies = Dependencies()) -> Promise<[Endpoint: (OnionRequestResponseInfoType, Codable)]> {
|
||||
///
|
||||
/// Like `/batch`, responses are returned in the same order as requests, but unlike `/batch` there may be fewer elements in the response list (if requests were
|
||||
/// stopped because of a non-2xx response) - In such a case, the final, non-2xx response is still included as the final response value
|
||||
private static func sequence(_ server: String, requests: [BatchRequestInfoType], using dependencies: Dependencies = Dependencies()) -> Promise<[Endpoint: (OnionRequestResponseInfoType, Codable?)]> {
|
||||
let requestBody: BatchRequest = requests.map { $0.toSubRequest() }
|
||||
let responseTypes = requests.map { $0.responseType }
|
||||
|
||||
|
@ -159,6 +182,13 @@ public final class OpenGroupAPI: NSObject {
|
|||
|
||||
// MARK: - Capabilities
|
||||
|
||||
/// Return the list of server features/capabilities
|
||||
///
|
||||
/// Optionally takes a `required` parameter containing a comma-separated list of capabilites; if any are not satisfied a 412 (Precondition Failed) response
|
||||
/// will be returned with missing requested capabilities in the `missing` key
|
||||
///
|
||||
/// Eg. `GET /capabilities` could return `{"capabilities": ["sogs", "batch"]}` `GET /capabilities?required=magic,batch`
|
||||
/// could return: `{"capabilities": ["sogs", "batch"], "missing": ["magic"]}`
|
||||
public static func capabilities(on server: String, using dependencies: Dependencies = Dependencies()) -> Promise<(OnionRequestResponseInfoType, Capabilities)> {
|
||||
let request: Request = Request<NoBody>(
|
||||
server: server,
|
||||
|
@ -173,6 +203,9 @@ public final class OpenGroupAPI: NSObject {
|
|||
|
||||
// MARK: - Room
|
||||
|
||||
/// Returns a list of available rooms on the server
|
||||
///
|
||||
/// Rooms to which the user does not have access (e.g. because they are banned, or the room has restricted access permissions) are not included
|
||||
public static func rooms(for server: String, using dependencies: Dependencies = Dependencies()) -> Promise<(OnionRequestResponseInfoType, [Room])> {
|
||||
let request: Request = Request<NoBody>(
|
||||
server: server,
|
||||
|
@ -183,6 +216,7 @@ public final class OpenGroupAPI: NSObject {
|
|||
.decoded(as: [Room].self, on: OpenGroupAPI.workQueue, error: Error.parsingFailed, using: dependencies)
|
||||
}
|
||||
|
||||
/// Returns the details of a single room
|
||||
public static func room(for roomToken: String, on server: String, using dependencies: Dependencies = Dependencies()) -> Promise<(OnionRequestResponseInfoType, Room)> {
|
||||
let request: Request = Request<NoBody>(
|
||||
server: server,
|
||||
|
@ -193,6 +227,15 @@ public final class OpenGroupAPI: NSObject {
|
|||
.decoded(as: Room.self, on: OpenGroupAPI.workQueue, error: Error.parsingFailed, using: dependencies)
|
||||
}
|
||||
|
||||
/// Polls a room for metadata updates
|
||||
///
|
||||
/// The endpoint polls room metadata for this room, always including the instantaneous room details (such as the user's permission and current
|
||||
/// number of active users), and including the full room metadata if the room's info_updated counter has changed from the provided value
|
||||
///
|
||||
/// **Note:** This is the direct request to retrieve room updates so should be retrieved automatically from the `poll()` method, in order to call
|
||||
/// this directly remove the `@available` line and make sure to route the response of this method to the `OpenGroupManager.handlePollInfo`
|
||||
/// method to ensure things are processed correctly
|
||||
@available(*, unavailable, message: "Avoid using this directly, use the pre-build `poll()` method instead")
|
||||
public static func roomPollInfo(lastUpdated: Int64, for roomToken: String, on server: String, using dependencies: Dependencies = Dependencies()) -> Promise<(OnionRequestResponseInfoType, RoomPollInfo)> {
|
||||
let request: Request = Request<NoBody>(
|
||||
server: server,
|
||||
|
@ -205,6 +248,7 @@ public final class OpenGroupAPI: NSObject {
|
|||
|
||||
// MARK: - Messages
|
||||
|
||||
/// Posts a new message to a room
|
||||
public static func send(
|
||||
_ plaintext: Data,
|
||||
to roomToken: String,
|
||||
|
@ -236,6 +280,7 @@ public final class OpenGroupAPI: NSObject {
|
|||
.decoded(as: Message.self, on: OpenGroupAPI.workQueue, error: Error.parsingFailed, using: dependencies)
|
||||
}
|
||||
|
||||
/// Returns a single message by ID
|
||||
public static func message(_ id: Int64, in roomToken: String, on server: String, using dependencies: Dependencies = Dependencies()) -> Promise<(OnionRequestResponseInfoType, Message)> {
|
||||
let request: Request = Request<NoBody>(
|
||||
server: server,
|
||||
|
@ -246,9 +291,13 @@ public final class OpenGroupAPI: NSObject {
|
|||
.decoded(as: Message.self, on: OpenGroupAPI.workQueue, error: Error.parsingFailed, using: dependencies)
|
||||
}
|
||||
|
||||
/// Edits a message, replacing its existing content with new content and a new signature
|
||||
///
|
||||
/// **Note:** This edit may only be initiated by the creator of the post, and the poster must currently have write permissions in the room
|
||||
public static func messageUpdate(
|
||||
_ id: Int64,
|
||||
plaintext: Data,
|
||||
fileIds: [Int64]?,
|
||||
in roomToken: String,
|
||||
on server: String,
|
||||
using dependencies: Dependencies = Dependencies()
|
||||
|
@ -259,7 +308,8 @@ public final class OpenGroupAPI: NSObject {
|
|||
|
||||
let requestBody: UpdateMessageRequest = UpdateMessageRequest(
|
||||
data: plaintext,
|
||||
signature: Data(signResult.signature)
|
||||
signature: Data(signResult.signature),
|
||||
fileIds: fileIds
|
||||
)
|
||||
|
||||
let request: Request = Request(
|
||||
|
@ -294,10 +344,10 @@ public final class OpenGroupAPI: NSObject {
|
|||
return response
|
||||
}
|
||||
}
|
||||
|
||||
/// This is the direct request to retrieve recent messages from an Open Group so should be retrieved automatically from the `poll()`
|
||||
/// method, if the logic should change then remove the `@available` line and make sure to route the response of this method to
|
||||
/// the `OpenGroupManager` `handleMessages` method (otherwise the logic may not work correctly)
|
||||
|
||||
/// **Note:** This is the direct request to retrieve recent messages so should be retrieved automatically from the `poll()` method, in order to call
|
||||
/// this directly remove the `@available` line and make sure to route the response of this method to the `OpenGroupManager.handleMessages`
|
||||
/// method to ensure things are processed correctly
|
||||
@available(*, unavailable, message: "Avoid using this directly, use the pre-build `poll()` method instead")
|
||||
public static func recentMessages(in roomToken: String, on server: String, using dependencies: Dependencies = Dependencies()) -> Promise<(OnionRequestResponseInfoType, [Message])> {
|
||||
let request: Request = Request<NoBody>(
|
||||
|
@ -311,9 +361,9 @@ public final class OpenGroupAPI: NSObject {
|
|||
.decoded(as: [Message].self, on: OpenGroupAPI.workQueue, error: Error.parsingFailed, using: dependencies)
|
||||
}
|
||||
|
||||
/// This is the direct request to retrieve recent messages from an Open Group so should be retrieved automatically from the `poll()`
|
||||
/// method, if the logic should change then remove the `@available` line and make sure to route the response of this method to
|
||||
/// the `OpenGroupManager` `handleMessages` method (otherwise the logic may not work correctly)
|
||||
/// **Note:** This is the direct request to retrieve recent messages before a given message and is currently unused, in order to call this directly
|
||||
/// remove the `@available` line and make sure to route the response of this method to the `OpenGroupManager.handleMessages`
|
||||
/// method to ensure things are processed correctly
|
||||
@available(*, unavailable, message: "Avoid using this directly, use the pre-build `poll()` method instead")
|
||||
public static func messagesBefore(messageId: Int64, in roomToken: String, on server: String, using dependencies: Dependencies = Dependencies()) -> Promise<(OnionRequestResponseInfoType, [Message])> {
|
||||
// TODO: Do we need to be able to load old messages?
|
||||
|
@ -328,9 +378,9 @@ public final class OpenGroupAPI: NSObject {
|
|||
.decoded(as: [Message].self, on: OpenGroupAPI.workQueue, error: Error.parsingFailed, using: dependencies)
|
||||
}
|
||||
|
||||
/// This is the direct request to retrieve recent messages from an Open Group so should be retrieved automatically from the `poll()`
|
||||
/// method, if the logic should change then remove the `@available` line and make sure to route the response of this method to
|
||||
/// the `OpenGroupManager` `handleMessages` method (otherwise the logic may not work correctly)
|
||||
/// **Note:** This is the direct request to retrieve messages since a given message `seqNo` so should be retrieved automatically from the
|
||||
/// `poll()` method, in order to call this directly remove the `@available` line and make sure to route the response of this method to the
|
||||
/// `OpenGroupManager.handleMessages` method to ensure things are processed correctly
|
||||
@available(*, unavailable, message: "Avoid using this directly, use the pre-build `poll()` method instead")
|
||||
public static func messagesSince(seqNo: Int64, in roomToken: String, on server: String, using dependencies: Dependencies = Dependencies()) -> Promise<(OnionRequestResponseInfoType, [Message])> {
|
||||
let request: Request = Request<NoBody>(
|
||||
|
@ -346,6 +396,16 @@ public final class OpenGroupAPI: NSObject {
|
|||
|
||||
// MARK: - Pinning
|
||||
|
||||
/// Adds a pinned message to this room
|
||||
///
|
||||
/// **Note:** Existing pinned messages are not removed: the new message is added to the pinned message list (If you want to remove existing
|
||||
/// pins then build a sequence request that first calls .../unpin/all)
|
||||
///
|
||||
/// The user must have admin (not just moderator) permissions in the room in order to pin messages
|
||||
///
|
||||
/// Pinned messages that are already pinned will be re-pinned (that is, their pin timestamp and pinning admin user will be updated) - because pinned
|
||||
/// messages are returned in pinning-order this allows admins to order multiple pinned messages in a room by re-pinning (via this endpoint) in the
|
||||
/// order in which pinned messages should be displayed
|
||||
public static func pinMessage(id: Int64, in roomToken: String, on server: String, using dependencies: Dependencies = Dependencies()) -> Promise<OnionRequestResponseInfoType> {
|
||||
let request: Request = Request<NoBody>(
|
||||
method: .post,
|
||||
|
@ -357,6 +417,9 @@ public final class OpenGroupAPI: NSObject {
|
|||
.map { responseInfo, _ in responseInfo }
|
||||
}
|
||||
|
||||
/// Remove a message from this room's pinned message list
|
||||
///
|
||||
/// The user must have `admin` (not just `moderator`) permissions in the room
|
||||
public static func unpinMessage(id: Int64, in roomToken: String, on server: String, using dependencies: Dependencies = Dependencies()) -> Promise<OnionRequestResponseInfoType> {
|
||||
let request: Request = Request<NoBody>(
|
||||
method: .post,
|
||||
|
@ -368,6 +431,9 @@ public final class OpenGroupAPI: NSObject {
|
|||
.map { responseInfo, _ in responseInfo }
|
||||
}
|
||||
|
||||
/// Removes _all_ pinned messages from this room
|
||||
///
|
||||
/// The user must have `admin` (not just `moderator`) permissions in the room
|
||||
public static func unpinAll(in roomToken: String, on server: String, using dependencies: Dependencies = Dependencies()) -> Promise<OnionRequestResponseInfoType> {
|
||||
let request: Request = Request<NoBody>(
|
||||
method: .post,
|
||||
|
@ -435,7 +501,13 @@ public final class OpenGroupAPI: NSObject {
|
|||
|
||||
// MARK: - Inbox (Message Requests)
|
||||
|
||||
public static func messageRequests(on server: String, using dependencies: Dependencies = Dependencies()) -> Promise<(OnionRequestResponseInfoType, [DirectMessage])> {
|
||||
/// Retrieves all of the user's current DMs (up to limit)
|
||||
///
|
||||
/// **Note:** This is the direct request to retrieve DMs for a specific Open Group so should be retrieved automatically from the `poll()`
|
||||
/// method, in order to call this directly remove the `@available` line and make sure to route the response of this method to the
|
||||
/// `OpenGroupManager.handleInbox` method to ensure things are processed correctly
|
||||
@available(*, unavailable, message: "Avoid using this directly, use the pre-build `poll()` method instead")
|
||||
public static func inbox(on server: String, using dependencies: Dependencies = Dependencies()) -> Promise<(OnionRequestResponseInfoType, [DirectMessage])> {
|
||||
let request: Request = Request<NoBody>(
|
||||
server: server,
|
||||
endpoint: .inbox
|
||||
|
@ -445,7 +517,13 @@ public final class OpenGroupAPI: NSObject {
|
|||
.decoded(as: [DirectMessage].self, on: OpenGroupAPI.workQueue, error: Error.parsingFailed, using: dependencies)
|
||||
}
|
||||
|
||||
public static func messageRequestsSince(id: Int64, on server: String, using dependencies: Dependencies = Dependencies()) -> Promise<(OnionRequestResponseInfoType, [DirectMessage])> {
|
||||
/// Polls for any DMs received since the given id
|
||||
///
|
||||
/// **Note:** This is the direct request to retrieve messages requests for a specific Open Group since a given messages so should be retrieved
|
||||
/// automatically from the `poll()` method, in order to call this directly remove the `@available` line and make sure to route the response
|
||||
/// of this method to the `OpenGroupManager.handleInbox` method to ensure things are processed correctly
|
||||
@available(*, unavailable, message: "Avoid using this directly, use the pre-build `poll()` method instead")
|
||||
public static func inboxSince(id: Int64, on server: String, using dependencies: Dependencies = Dependencies()) -> Promise<(OnionRequestResponseInfoType, [DirectMessage])> {
|
||||
let request: Request = Request<NoBody>(
|
||||
server: server,
|
||||
endpoint: .inboxSince(id: id)
|
||||
|
@ -455,14 +533,12 @@ public final class OpenGroupAPI: NSObject {
|
|||
.decoded(as: [DirectMessage].self, on: OpenGroupAPI.workQueue, error: Error.parsingFailed, using: dependencies)
|
||||
}
|
||||
|
||||
public static func sendMessageRequest(_ plaintext: Data, to blindedSessionId: String, on server: String, with serverPublicKey: String, using dependencies: Dependencies = Dependencies()) -> Promise<(OnionRequestResponseInfoType, [DirectMessage])> {
|
||||
guard let signedMessage: Data = sign(message: plaintext, to: blindedSessionId, on: server, with: serverPublicKey, using: dependencies) else {
|
||||
return Promise(error: Error.signingFailed)
|
||||
}
|
||||
|
||||
/// Delivers a direct message to a user via their blinded Session ID
|
||||
///
|
||||
/// The body of this request is a JSON object containing a message key with a value of the encrypted-then-base64-encoded message to deliver
|
||||
public static func send(_ ciphertext: Data, toInboxFor blindedSessionId: String, on server: String/*, with serverPublicKey: String*/, using dependencies: Dependencies = Dependencies()) -> Promise<(OnionRequestResponseInfoType, Data?)> {
|
||||
let requestBody: SendDirectMessageRequest = SendDirectMessageRequest(
|
||||
data: signedMessage
|
||||
// signature: signedMessage.signature // TODO: Confirm whether this needs a signature??
|
||||
message: ciphertext
|
||||
)
|
||||
|
||||
let request: Request = Request(
|
||||
|
@ -478,7 +554,44 @@ public final class OpenGroupAPI: NSObject {
|
|||
|
||||
// MARK: - Users
|
||||
|
||||
public static func userBan(_ sessionId: String, for timeout: TimeInterval? = nil, from roomTokens: [String]? = nil, on server: String, using dependencies: Dependencies = Dependencies()) -> Promise<(OnionRequestResponseInfoType, Data?)> {
|
||||
/// Applies a ban of a user from specific rooms, or from the server globally
|
||||
///
|
||||
/// The invoking user must have `moderator` (or `admin`) permission in all given rooms when specifying rooms, and must be a
|
||||
/// `globalModerator` (or `globalAdmin`) if using the global parameter
|
||||
///
|
||||
/// **Note:** The user's messages are not deleted by this request - In order to ban and delete all messages use the `/sequence` endpoint to
|
||||
/// bundle a `/user/.../ban` with a `/user/.../deleteMessages` request
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - sessionId: The sessionId (either standard or blinded) of the user whose messages should be deleted
|
||||
///
|
||||
/// - timeout: Value specifying a time limit on the ban, in seconds
|
||||
///
|
||||
/// The applied ban will expire and be removed after the given interval - If omitted (or `null`) then the ban is permanent
|
||||
///
|
||||
/// If this endpoint is called multiple times then the timeout of the last call takes effect (eg. a permanent ban can be replaced
|
||||
/// with a time-limited ban by calling the endpoint again with a timeout value, and vice versa)
|
||||
///
|
||||
/// - roomTokens: List of one or more room tokens from which the user should be banned from
|
||||
///
|
||||
/// The invoking user **must** be a moderator of all of the given rooms.
|
||||
///
|
||||
/// This may be set to the single-element list `["*"]` to ban the user from all rooms in which the current user has moderator
|
||||
/// permissions (the call will succeed if the calling user is a moderator in at least one channel)
|
||||
///
|
||||
/// **Note:** You can ban from all rooms on a server by providing a `nil` value for this parameter (the invoking user must be a
|
||||
/// global moderator in order to add a global ban)
|
||||
///
|
||||
/// - server: The server to delete messages from
|
||||
///
|
||||
/// - dependencies: Injected dependencies (used for unit testing)
|
||||
public static func userBan(
|
||||
_ sessionId: String,
|
||||
for timeout: TimeInterval? = nil,
|
||||
from roomTokens: [String]? = nil,
|
||||
on server: String,
|
||||
using dependencies: Dependencies = Dependencies()
|
||||
) -> Promise<(OnionRequestResponseInfoType, Data?)> {
|
||||
let requestBody: UserBanRequest = UserBanRequest(
|
||||
rooms: roomTokens,
|
||||
global: (roomTokens == nil ? true : nil),
|
||||
|
@ -495,7 +608,36 @@ public final class OpenGroupAPI: NSObject {
|
|||
return send(request, using: dependencies)
|
||||
}
|
||||
|
||||
public static func userUnban(_ sessionId: String, from roomTokens: [String]? = nil, on server: String, using dependencies: Dependencies = Dependencies()) -> Promise<(OnionRequestResponseInfoType, Data?)> {
|
||||
/// Removes a user ban from specific rooms, or from the server globally
|
||||
///
|
||||
/// The invoking user must have `moderator` (or `admin`) permission in all given rooms when specifying rooms, and must be a global server `moderator`
|
||||
/// (or `admin`) if using the `global` parameter
|
||||
///
|
||||
/// **Note:** Room and global bans are independent: if a user is banned globally and has a room-specific ban then removing the global ban does not remove
|
||||
/// the room specific ban, and removing the room-specific ban does not remove the global ban (to fully unban a user globally and from all rooms, submit a
|
||||
/// `/sequence` request with a global unban followed by a "rooms": ["*"] unban)
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - sessionId: The sessionId (either standard or blinded) of the user whose messages should be deleted
|
||||
///
|
||||
/// - roomTokens: List of one or more room tokens from which the user should be unbanned from
|
||||
///
|
||||
/// The invoking user **must** be a moderator of all of the given rooms.
|
||||
///
|
||||
/// This may be set to the single-element list `["*"]` to unban the user from all rooms in which the current user has moderator
|
||||
/// permissions (the call will succeed if the calling user is a moderator in at least one channel)
|
||||
///
|
||||
/// **Note:** You can ban from all rooms on a server by providing a `nil` value for this parameter
|
||||
///
|
||||
/// - server: The server to delete messages from
|
||||
///
|
||||
/// - dependencies: Injected dependencies (used for unit testing)
|
||||
public static func userUnban(
|
||||
_ sessionId: String,
|
||||
from roomTokens: [String]?,
|
||||
on server: String,
|
||||
using dependencies: Dependencies = Dependencies()
|
||||
) -> Promise<(OnionRequestResponseInfoType, Data?)> {
|
||||
let requestBody: UserUnbanRequest = UserUnbanRequest(
|
||||
rooms: roomTokens,
|
||||
global: (roomTokens == nil ? true : nil)
|
||||
|
@ -511,26 +653,68 @@ public final class OpenGroupAPI: NSObject {
|
|||
return send(request, using: dependencies)
|
||||
}
|
||||
|
||||
public static func userPermissionUpdate(_ sessionId: String, read: Bool, write: Bool, upload: Bool, for roomTokens: [String], timeout: TimeInterval, on server: String, using dependencies: Dependencies = Dependencies()) -> Promise<(OnionRequestResponseInfoType, Data?)> {
|
||||
let requestBody: UserPermissionsRequest = UserPermissionsRequest(
|
||||
rooms: roomTokens,
|
||||
timeout: timeout,
|
||||
read: read,
|
||||
write: write,
|
||||
upload: upload
|
||||
)
|
||||
/// Appoints or removes a moderator or admin
|
||||
///
|
||||
/// This endpoint is used to appoint or remove moderator/admin permissions either for specific rooms or for server-wide global moderator permissions
|
||||
///
|
||||
/// Admins/moderators of rooms can only be appointed or removed by a user who has admin permissions in the room (including global admins)
|
||||
///
|
||||
/// Global admins/moderators may only be appointed by a global admin
|
||||
///
|
||||
/// The admin/moderator paramters interact as follows:
|
||||
/// - **admin=true, moderator omitted:** This adds admin permissions, which automatically also implies moderator permissions
|
||||
/// - **admin=true, moderator=true:** Exactly the same as above
|
||||
/// - **admin=false, moderator=true:** Removes any existing admin permissions from the rooms (or globally), if present, and adds
|
||||
/// moderator permissions to the rooms/globally (if not already present)
|
||||
/// - **admin=false, moderator omitted:** This removes admin permissions but leaves moderator permissions, if present (this
|
||||
/// effectively "downgrades" an admin to a moderator). Unlike the above this does **not** add moderator permissions to matching rooms
|
||||
/// if not already present
|
||||
/// - **moderator=true, admin omitted:** Adds moderator permissions to the given rooms (or globally), if not already present. If
|
||||
/// the user already has admin permissions this does nothing (that is, admin permission is *not* removed, unlike the above)
|
||||
/// - **moderator=false, admin omitted:** This removes moderator **and** admin permissions from all given rooms (or globally)
|
||||
/// - **moderator=false, admin=false:** Exactly the same as above
|
||||
/// - **moderator=false, admin=true:** This combination is **not permitted** (because admin permissions imply moderator
|
||||
/// permissions) and will result in Bad Request error if given
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - sessionId: The sessionId (either standard or blinded) of the user to modify the permissions of
|
||||
///
|
||||
/// - moderator: Value indicating that this user should have moderator permissions added (true), removed (false), or left alone (null)
|
||||
///
|
||||
/// - admin: Value indicating that this user should have admin permissions added (true), removed (false), or left alone (null)
|
||||
///
|
||||
/// Granting admin permission automatically includes granting moderator permission (and thus it is an error to use admin=true with
|
||||
/// moderator=false)
|
||||
///
|
||||
/// - visible: Value indicating whether the moderator/admin should be made publicly visible as a moderator/admin of the room(s)
|
||||
/// (if true) or hidden (false)
|
||||
///
|
||||
/// Hidden moderators/admins still have all the same permissions as visible moderators/admins, but are visible only to other
|
||||
/// moderators/admins; regular users in the room will not know their moderator status
|
||||
///
|
||||
/// - roomTokens: List of one or more room tokens to which the permission changes should be applied
|
||||
///
|
||||
/// The invoking user **must** be an admin of all of the given rooms.
|
||||
///
|
||||
/// This may be set to the single-element list `["*"]` to add or remove the moderator from all rooms in which the current user has admin
|
||||
/// permissions (the call will succeed if the calling user is an admin in at least one channel)
|
||||
///
|
||||
/// **Note:** You can specify a change to global permisisons by providing a `nil` value for this parameter
|
||||
///
|
||||
/// - server: The server to perform the permission changes on
|
||||
///
|
||||
/// - dependencies: Injected dependencies (used for unit testing)
|
||||
public static func userModeratorUpdate(
|
||||
_ sessionId: String,
|
||||
moderator: Bool? = nil,
|
||||
admin: Bool? = nil,
|
||||
visible: Bool,
|
||||
for roomTokens: [String]?,
|
||||
on server: String,
|
||||
using dependencies: Dependencies = Dependencies()
|
||||
) -> Promise<(OnionRequestResponseInfoType, Data?)> {
|
||||
guard (moderator != nil && admin == nil) || (moderator == nil && admin != nil) else { return Promise(error: Error.generic) }
|
||||
|
||||
let request: Request = Request(
|
||||
method: .post,
|
||||
server: server,
|
||||
endpoint: .userPermission(sessionId),
|
||||
body: requestBody
|
||||
)
|
||||
|
||||
return send(request, using: dependencies)
|
||||
}
|
||||
|
||||
public static func userModeratorUpdate(_ sessionId: String, moderator: Bool, admin: Bool, visible: Bool, for roomTokens: [String]? = nil, on server: String, using dependencies: Dependencies = Dependencies()) -> Promise<(OnionRequestResponseInfoType, Data?)> {
|
||||
let requestBody: UserModeratorRequest = UserModeratorRequest(
|
||||
rooms: roomTokens,
|
||||
global: (roomTokens == nil ? true : nil),
|
||||
|
@ -549,7 +733,31 @@ public final class OpenGroupAPI: NSObject {
|
|||
return send(request, using: dependencies)
|
||||
}
|
||||
|
||||
public static func userDeleteMessages(_ sessionId: String, for roomTokens: [String]? = nil, on server: String, using dependencies: Dependencies = Dependencies()) -> Promise<(OnionRequestResponseInfoType, UserDeleteMessagesResponse)> {
|
||||
// TODO: Need to test this once the API has been implemented
|
||||
// TODO: Update docs to align with the API documentation once implemented
|
||||
/// Deletes all messages from a given sessionId within the provided rooms (or globally) on a server
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - sessionId: The sessionId (either standard or blinded) of the user whose messages should be deleted
|
||||
///
|
||||
/// - roomTokens: List of one or more room tokens from which the messages should be deleted
|
||||
///
|
||||
/// The invoking user **must** be an admin of all of the given rooms.
|
||||
///
|
||||
/// This may be set to the single-element list `["*"]` to add or remove the moderator from all rooms in which the current user has admin
|
||||
/// permissions (the call will succeed if the calling user is an admin in at least one channel)
|
||||
///
|
||||
/// **Note:** You can delete messages from all rooms on a server by providing a `nil` value for this parameter
|
||||
///
|
||||
/// - server: The server to delete messages from
|
||||
///
|
||||
/// - dependencies: Injected dependencies (used for unit testing)
|
||||
public static func userDeleteMessages(
|
||||
_ sessionId: String,
|
||||
for roomTokens: [String]?,
|
||||
on server: String,
|
||||
using dependencies: Dependencies = Dependencies()
|
||||
) -> Promise<(OnionRequestResponseInfoType, UserDeleteMessagesResponse)> {
|
||||
let requestBody: UserDeleteMessagesRequest = UserDeleteMessagesRequest(
|
||||
rooms: roomTokens,
|
||||
global: (roomTokens == nil ? true : nil)
|
||||
|
@ -566,7 +774,15 @@ public final class OpenGroupAPI: NSObject {
|
|||
.decoded(as: UserDeleteMessagesResponse.self, on: OpenGroupAPI.workQueue, error: Error.parsingFailed, using: dependencies)
|
||||
}
|
||||
|
||||
public static func userBanAndDeleteAllMessage(_ sessionId: String, for roomTokens: [String]? = nil, on server: String, using dependencies: Dependencies = Dependencies()) -> Promise<[OnionRequestResponseInfoType]> {
|
||||
// TODO: Need to test this once the API has been implemented
|
||||
/// This is a convenience method which constructs a `/sequence` of the `userBan` and `userDeleteMessages` requests, refer to those
|
||||
/// methods for the documented behaviour of each method
|
||||
public static func userBanAndDeleteAllMessage(
|
||||
_ sessionId: String,
|
||||
for roomTokens: [String]?,
|
||||
on server: String,
|
||||
using dependencies: Dependencies = Dependencies()
|
||||
) -> Promise<[OnionRequestResponseInfoType]> {
|
||||
let banRequestBody: UserBanRequest = UserBanRequest(
|
||||
rooms: roomTokens,
|
||||
global: (roomTokens == nil ? true : nil),
|
||||
|
@ -585,8 +801,7 @@ public final class OpenGroupAPI: NSObject {
|
|||
server: server,
|
||||
endpoint: .userBan(sessionId),
|
||||
body: banRequestBody
|
||||
),
|
||||
responseType: Data?.self
|
||||
)
|
||||
),
|
||||
BatchRequestInfo(
|
||||
request: Request(
|
||||
|
@ -608,46 +823,8 @@ public final class OpenGroupAPI: NSObject {
|
|||
|
||||
// MARK: - Authentication
|
||||
|
||||
// TODO: This is going to have to work differently (ie. `MessageSender+Encryption`)
|
||||
/// Sign a blinded message request to be sent to a users inbox via SOGS v4
|
||||
private static func sign(message: Data, to blindedSessionId: String, on serverName: String, with serverPublicKey: String, using dependencies: Dependencies = Dependencies()) -> Data? {
|
||||
guard let userEdKeyPair: Box.KeyPair = dependencies.storage.getUserED25519KeyPair() else { return nil }
|
||||
// TODO: Re-do this
|
||||
return nil
|
||||
// guard let blindedKeyPair: BlindedECKeyPair = dependencies.sodium.blindedKeyPair(serverPublicKey: serverPublicKey, edKeyPair: userEdKeyPair, genericHash: dependencies.genericHash) else {
|
||||
// return nil
|
||||
// }
|
||||
// guard let blindedRecipientPublicKey: Data = String(blindedSessionId.suffix(from: blindedSessionId.index(blindedSessionId.startIndex, offsetBy: IdPrefix.blinded.rawValue.count))).dataFromHex() else {
|
||||
// return nil
|
||||
// }
|
||||
//
|
||||
// /// Generate the sharedSecret by "a kB || kA || kB" where
|
||||
// /// a, A are the users private and public keys respectively,
|
||||
// /// kA is the users blinded public key
|
||||
// /// kB is the recipients blinded public key
|
||||
// let maybeSharedSecret: Data? = dependencies.sodium
|
||||
// .sharedEdSecret(userEdKeyPair.secretKey, blindedRecipientPublicKey.bytes)?
|
||||
// .appending(blindedKeyPair.publicKey.bytes)
|
||||
// .appending(blindedRecipientPublicKey.bytes)
|
||||
//
|
||||
// guard let sharedSecret: Data = maybeSharedSecret else { return nil }
|
||||
// guard let intermediateHash: Bytes = dependencies.genericHash.hash(message: sharedSecret.bytes) else { return nil }
|
||||
//
|
||||
// /// Generate the inner message by "message || A" where
|
||||
// /// A is the sender's ed25519 master pubkey (**not** kA blinded pubkey)
|
||||
// let innerMessage: Bytes = (message.bytes + userEdKeyPair.publicKey)
|
||||
// guard let (ciphertext, nonce) = dependencies.aeadXChaCha20Poly1305Ietf.encrypt(message: innerMessage, secretKey: intermediateHash) else {
|
||||
// return nil
|
||||
// }
|
||||
//
|
||||
// /// Generate the final data by "b'\x00' + ciphertext + nonce"
|
||||
// let finalData: Bytes = [0] + ciphertext + nonce
|
||||
//
|
||||
// return Data(finalData)
|
||||
}
|
||||
|
||||
/// Sign a message to be sent to SOGS (handles both un-blinded and blinded signing based on the server capabilities)
|
||||
public static func sign(_ messageBytes: Bytes, for serverName: String, using dependencies: Dependencies = Dependencies()) -> (publicKey: String, signature: Bytes)? {
|
||||
private static func sign(_ messageBytes: Bytes, for serverName: String, using dependencies: Dependencies = Dependencies()) -> (publicKey: String, signature: Bytes)? {
|
||||
guard let userEdKeyPair: Box.KeyPair = dependencies.storage.getUserED25519KeyPair() else { return nil }
|
||||
guard let serverPublicKey: String = dependencies.storage.getOpenGroupPublicKey(for: serverName) else {
|
||||
return nil
|
||||
|
@ -666,7 +843,7 @@ public final class OpenGroupAPI: NSObject {
|
|||
}
|
||||
|
||||
return (
|
||||
publicKey: IdPrefix.blinded.hexEncodedPublicKey(for: blindedKeyPair.publicKey),
|
||||
publicKey: SessionId(.blinded, publicKey: blindedKeyPair.publicKey).hexString,
|
||||
signature: signatureResult
|
||||
)
|
||||
}
|
||||
|
@ -677,7 +854,7 @@ public final class OpenGroupAPI: NSObject {
|
|||
}
|
||||
|
||||
return (
|
||||
publicKey: IdPrefix.unblinded.hexEncodedPublicKey(for: userEdKeyPair.publicKey),
|
||||
publicKey: SessionId(.unblinded, publicKey: userEdKeyPair.publicKey).hexString,
|
||||
signature: signatureResult
|
||||
)
|
||||
}
|
||||
|
@ -691,7 +868,7 @@ public final class OpenGroupAPI: NSObject {
|
|||
.appending(url.query.map { value in "?\(value)" })
|
||||
let method: String = (request.httpMethod ?? "GET")
|
||||
let timestamp: Int = Int(floor(dependencies.date.timeIntervalSince1970))
|
||||
let nonce: Data = Data(dependencies.nonceGenerator.nonce())
|
||||
let nonce: Data = Data(dependencies.nonceGenerator16.nonce())
|
||||
|
||||
guard let serverPublicKeyData: Data = serverPublicKey.dataFromHex() else { return nil }
|
||||
guard let timestampBytes: Bytes = "\(timestamp)".data(using: .ascii)?.bytes else { return nil }
|
||||
|
|
|
@ -130,11 +130,12 @@ public final class OpenGroupManager: NSObject {
|
|||
isBackgroundPoll: Bool,
|
||||
using dependencies: OpenGroupAPI.Dependencies = OpenGroupAPI.Dependencies()
|
||||
) {
|
||||
// Sorting the messages by server ID before importing them fixes an issue where messages that quote older messages can't find those older messages
|
||||
// Sorting the messages by server ID before importing them fixes an issue where messages
|
||||
// that quote older messages can't find those older messages
|
||||
let openGroupID = "\(server).\(roomToken)"
|
||||
let sortedMessages: [OpenGroupAPI.Message] = messages
|
||||
.sorted { lhs, rhs in lhs.seqNo < rhs.seqNo }
|
||||
let seqNo: Int64 = (sortedMessages.last?.seqNo ?? 0)
|
||||
.sorted { lhs, rhs in lhs.id < rhs.id }
|
||||
let seqNo: Int64 = (sortedMessages.map { $0.seqNo }.max() ?? 0)
|
||||
|
||||
dependencies.storage.write { transaction in
|
||||
var messageServerIDsToRemove: [UInt64] = []
|
||||
|
@ -256,7 +257,6 @@ public final class OpenGroupManager: NSObject {
|
|||
imageID: (pollInfo.details?.imageId.map { "\($0)" } ?? existingOpenGroup?.imageID),
|
||||
infoUpdates: ((pollInfo.details?.infoUpdates ?? existingOpenGroup?.infoUpdates) ?? 0)
|
||||
)
|
||||
let existingUserCount: UInt64? = dependencies.storage.getUserCount(forOpenGroupWithID: updatedOpenGroup.id)
|
||||
|
||||
// - Thread changes
|
||||
thread.shouldBeVisible = true
|
||||
|
@ -268,7 +268,7 @@ public final class OpenGroupManager: NSObject {
|
|||
|
||||
// - User Count
|
||||
dependencies.storage.setUserCount(
|
||||
to: ((pollInfo.activeUsers.map { UInt64($0) } ?? existingUserCount) ?? 0),
|
||||
to: UInt64(pollInfo.activeUsers),
|
||||
forOpenGroupWithID: updatedOpenGroup.id,
|
||||
using: transaction
|
||||
)
|
||||
|
@ -313,6 +313,51 @@ public final class OpenGroupManager: NSObject {
|
|||
)
|
||||
}
|
||||
|
||||
internal static func handleInbox(
|
||||
_ messages: [OpenGroupAPI.DirectMessage],
|
||||
on server: String,
|
||||
isBackgroundPoll: Bool,
|
||||
using dependencies: OpenGroupAPI.Dependencies = OpenGroupAPI.Dependencies()
|
||||
) {
|
||||
guard let serverPublicKey: String = dependencies.storage.getOpenGroupPublicKey(for: server) else {
|
||||
SNLog("Couldn't receive inbox message.")
|
||||
return
|
||||
}
|
||||
|
||||
// Sorting the messages by server ID before importing them fixes an issue where messages
|
||||
// that quote older messages can't find those older messages
|
||||
let sortedMessages: [OpenGroupAPI.DirectMessage] = messages
|
||||
.sorted { lhs, rhs in lhs.id < rhs.id }
|
||||
let latestMessageId: Int64 = (sortedMessages.last?.id ?? 0)
|
||||
|
||||
dependencies.storage.write { transaction in
|
||||
// Update the 'latestMessageId' value
|
||||
dependencies.storage.setOpenGroupInboxLatestMessageId(for: server, to: latestMessageId, using: transaction)
|
||||
|
||||
// Process the messages
|
||||
sortedMessages.forEach { message in
|
||||
guard let messageData = Data(base64Encoded: message.base64EncodedMessage) else {
|
||||
SNLog("Couldn't receive inbox message.")
|
||||
return
|
||||
}
|
||||
|
||||
// Note: The `posted` value is in seconds but all messages in the database use milliseconds for timestamps
|
||||
let envelope = SNProtoEnvelope.builder(type: .sessionMessage, timestamp: UInt64(floor(message.posted * 1000)))
|
||||
envelope.setContent(messageData)
|
||||
envelope.setSource(message.sender)
|
||||
|
||||
do {
|
||||
let data = try envelope.buildSerializedData()
|
||||
let (message, proto) = try MessageReceiver.parse(data, openGroupMessageServerID: nil, openGroupServerPublicKey: serverPublicKey, isRetry: false, using: transaction)
|
||||
try MessageReceiver.handle(message, associatedWithProto: proto, openGroupID: nil, isBackgroundPoll: isBackgroundPoll, using: transaction)
|
||||
}
|
||||
catch let error {
|
||||
SNLog("Couldn't receive inbox message due to error: \(error).")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Convenience
|
||||
|
||||
/// This method specifies if the given publicKey is a moderator or an admin within a specified Open Group
|
||||
|
@ -332,7 +377,7 @@ public final class OpenGroupManager: NSObject {
|
|||
}
|
||||
|
||||
// Add the unblinded key as an option
|
||||
targetKeys.append(IdPrefix.unblinded.hexEncodedPublicKey(for: userEdKeyPair.publicKey))
|
||||
targetKeys.append(SessionId(.unblinded, publicKey: userEdKeyPair.publicKey).hexString)
|
||||
|
||||
let server: OpenGroupAPI.Server? = dependencies.storage.getOpenGroupServer(name: server)
|
||||
|
||||
|
@ -343,7 +388,7 @@ public final class OpenGroupManager: NSObject {
|
|||
}
|
||||
|
||||
// Add the blinded key as an option
|
||||
targetKeys.append(IdPrefix.blinded.hexEncodedPublicKey(for: blindedKeyPair.publicKey))
|
||||
targetKeys.append(SessionId(.blinded, publicKey: blindedKeyPair.publicKey).hexString)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -13,7 +13,8 @@ extension OpenGroupAPI {
|
|||
let sign: SignType
|
||||
let genericHash: GenericHashType
|
||||
let ed25519: Ed25519Type.Type
|
||||
let nonceGenerator: NonceGenerator16ByteType
|
||||
let nonceGenerator16: NonceGenerator16ByteType
|
||||
let nonceGenerator24: NonceGenerator24ByteType
|
||||
let date: Date
|
||||
|
||||
public init(
|
||||
|
@ -25,7 +26,8 @@ extension OpenGroupAPI {
|
|||
sign: SignType? = nil,
|
||||
genericHash: GenericHashType? = nil,
|
||||
ed25519: Ed25519Type.Type = Ed25519.self,
|
||||
nonceGenerator: NonceGenerator16ByteType = NonceGenerator16Byte(),
|
||||
nonceGenerator16: NonceGenerator16ByteType = NonceGenerator16Byte(),
|
||||
nonceGenerator24: NonceGenerator24ByteType = NonceGenerator24Byte(),
|
||||
date: Date = Date()
|
||||
) {
|
||||
self.api = api
|
||||
|
@ -35,7 +37,8 @@ extension OpenGroupAPI {
|
|||
self.sign = (sign ?? sodium.getSign())
|
||||
self.genericHash = (genericHash ?? sodium.getGenericHash())
|
||||
self.ed25519 = ed25519
|
||||
self.nonceGenerator = nonceGenerator
|
||||
self.nonceGenerator16 = nonceGenerator16
|
||||
self.nonceGenerator24 = nonceGenerator24
|
||||
self.date = date
|
||||
}
|
||||
|
||||
|
@ -49,7 +52,8 @@ extension OpenGroupAPI {
|
|||
sign: SignType? = nil,
|
||||
genericHash: GenericHashType? = nil,
|
||||
ed25519: Ed25519Type.Type? = nil,
|
||||
nonceGenerator: NonceGenerator16ByteType? = nil,
|
||||
nonceGenerator16: NonceGenerator16ByteType? = nil,
|
||||
nonceGenerator24: NonceGenerator24ByteType? = nil,
|
||||
date: Date? = nil
|
||||
) -> Dependencies {
|
||||
return Dependencies(
|
||||
|
@ -60,7 +64,8 @@ extension OpenGroupAPI {
|
|||
sign: (sign ?? self.sign),
|
||||
genericHash: (genericHash ?? self.genericHash),
|
||||
ed25519: (ed25519 ?? self.ed25519),
|
||||
nonceGenerator: (nonceGenerator ?? self.nonceGenerator),
|
||||
nonceGenerator16: (nonceGenerator16 ?? self.nonceGenerator16),
|
||||
nonceGenerator24: (nonceGenerator24 ?? self.nonceGenerator24),
|
||||
date: (date ?? self.date)
|
||||
)
|
||||
}
|
||||
|
|
|
@ -136,7 +136,7 @@ extension OpenGroupAPI {
|
|||
// Inbox (Message Requests)
|
||||
|
||||
case .inbox: return "inbox"
|
||||
case .inboxSince(let id): return "inbox/\(id)"
|
||||
case .inboxSince(let id): return "inbox/since/\(id)"
|
||||
case .inboxFor(let sessionId): return "inbox/\(sessionId)"
|
||||
|
||||
// Users
|
||||
|
|
|
@ -3,11 +3,15 @@
|
|||
import Sodium
|
||||
|
||||
public protocol NonceGenerator16ByteType {
|
||||
var NonceBytes: Int { get }
|
||||
|
||||
func nonce() -> Array<UInt8>
|
||||
}
|
||||
|
||||
extension NonceGenerator16ByteType {
|
||||
public protocol NonceGenerator24ByteType {
|
||||
var NonceBytes: Int { get }
|
||||
|
||||
func nonce() -> Array<UInt8>
|
||||
}
|
||||
|
||||
extension OpenGroupAPI {
|
||||
|
@ -16,4 +20,10 @@ extension OpenGroupAPI {
|
|||
|
||||
public init() {}
|
||||
}
|
||||
|
||||
public class NonceGenerator24Byte: NonceGenerator, NonceGenerator24ByteType {
|
||||
public var NonceBytes: Int { 24 }
|
||||
|
||||
public init() {}
|
||||
}
|
||||
}
|
|
@ -4,7 +4,10 @@ import Foundation
|
|||
import SessionUtilitiesKit
|
||||
|
||||
extension OpenGroupAPI {
|
||||
struct NoBody: Encodable {}
|
||||
struct Empty: Codable {}
|
||||
|
||||
typealias NoBody = Empty
|
||||
typealias NoResponse = Empty
|
||||
|
||||
struct Request<T: Encodable> {
|
||||
let method: HTTP.Verb
|
||||
|
|
|
@ -9,14 +9,20 @@ public protocol SodiumType {
|
|||
func getAeadXChaCha20Poly1305Ietf() -> AeadXChaCha20Poly1305IetfType
|
||||
func getSign() -> SignType
|
||||
|
||||
func generateBlindingFactor(serverPublicKey: String) -> Bytes?
|
||||
func blindedKeyPair(serverPublicKey: String, edKeyPair: Box.KeyPair, genericHash: GenericHashType) -> Box.KeyPair?
|
||||
func sogsSignature(message: Bytes, secretKey: Bytes, blindedSecretKey ka: Bytes, blindedPublicKey kA: Bytes) -> Bytes?
|
||||
|
||||
func sharedEdSecret(_ firstKeyBytes: [UInt8], _ secondKeyBytes: [UInt8]) -> Bytes?
|
||||
func combineKeys(lhsKeyBytes: Bytes, rhsKeyBytes: Bytes) -> Bytes?
|
||||
func sharedBlindedEncryptionKey(secretKey a: Bytes, otherBlindedPublicKey: Bytes, fromBlindedPublicKey kA: Bytes, toBlindedPublicKey kB: Bytes, genericHash: GenericHashType) -> Bytes?
|
||||
}
|
||||
|
||||
public protocol AeadXChaCha20Poly1305IetfType {
|
||||
func encrypt(message: Bytes, secretKey: Aead.XChaCha20Poly1305Ietf.Key, additionalData: Bytes?) -> (authenticatedCipherText: Bytes, nonce: Aead.XChaCha20Poly1305Ietf.Nonce)?
|
||||
var KeyBytes: Int { get }
|
||||
var ABytes: Int { get }
|
||||
|
||||
func encrypt(message: Bytes, secretKey: Bytes, nonce: Bytes, additionalData: Bytes?) -> Bytes?
|
||||
func decrypt(authenticatedCipherText: Bytes, secretKey: Bytes, nonce: Bytes, additionalData: Bytes?) -> Bytes?
|
||||
}
|
||||
|
||||
public protocol Ed25519Type {
|
||||
|
@ -24,6 +30,9 @@ public protocol Ed25519Type {
|
|||
}
|
||||
|
||||
public protocol SignType {
|
||||
var PublicKeyBytes: Int { get }
|
||||
|
||||
func toX25519(ed25519PublicKey: Bytes) -> Bytes?
|
||||
func signature(message: Bytes, secretKey: Bytes) -> Bytes?
|
||||
func verify(message: Bytes, publicKey: Bytes, signature: Bytes) -> Bool
|
||||
}
|
||||
|
@ -37,8 +46,12 @@ public protocol GenericHashType {
|
|||
// MARK: - Default Values
|
||||
|
||||
extension AeadXChaCha20Poly1305IetfType {
|
||||
func encrypt(message: Bytes, secretKey: Aead.XChaCha20Poly1305Ietf.Key) -> (authenticatedCipherText: Bytes, nonce: Aead.XChaCha20Poly1305Ietf.Nonce)? {
|
||||
return encrypt(message: message, secretKey: secretKey, additionalData: nil)
|
||||
func encrypt(message: Bytes, secretKey: Bytes, nonce: Bytes) -> Bytes? {
|
||||
return encrypt(message: message, secretKey: secretKey, nonce: nonce, additionalData: nil)
|
||||
}
|
||||
|
||||
func decrypt(authenticatedCipherText: Bytes, secretKey: Bytes, nonce: Bytes) -> Bytes? {
|
||||
return decrypt(authenticatedCipherText: authenticatedCipherText, secretKey: secretKey, nonce: nonce, additionalData: nil)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -24,7 +24,65 @@ extension MessageReceiver {
|
|||
guard isValid else { throw Error.invalidSignature }
|
||||
// 4. ) Get the sender's X25519 public key
|
||||
guard let senderX25519PublicKey = sodium.sign.toX25519(ed25519PublicKey: senderED25519PublicKey) else { throw Error.decryptionFailed }
|
||||
// TODO: Need to rework this as it'll be based on the blinded id
|
||||
return (Data(plaintext), SessionId(.standard, publicKey: senderX25519PublicKey).hexString)
|
||||
}
|
||||
|
||||
internal static func decryptWithSessionBlindingProtocol(data: Data, fromBlindedPublicKey: String, with openGroupPublicKey: String, userEd25519KeyPair: Box.KeyPair, using dependencies: OpenGroupAPI.Dependencies = OpenGroupAPI.Dependencies()) throws -> (plaintext: Data, senderX25519PublicKey: String) {
|
||||
guard let blindedKeyPair = dependencies.sodium.blindedKeyPair(serverPublicKey: openGroupPublicKey, edKeyPair: userEd25519KeyPair, genericHash: dependencies.genericHash) else {
|
||||
throw Error.decryptionFailed
|
||||
}
|
||||
|
||||
/// Step one: calculate the shared encryption key, receiving from A to B
|
||||
let kA: Bytes = Data(hex: fromBlindedPublicKey.removingIdPrefixIfNeeded()).bytes
|
||||
guard let dec_key: Bytes = dependencies.sodium.sharedBlindedEncryptionKey(
|
||||
secretKey: userEd25519KeyPair.secretKey,
|
||||
otherBlindedPublicKey: kA,
|
||||
fromBlindedPublicKey: kA,
|
||||
toBlindedPublicKey: blindedKeyPair.publicKey,
|
||||
genericHash: dependencies.genericHash
|
||||
) else {
|
||||
throw Error.decryptionFailed
|
||||
}
|
||||
|
||||
return (Data(plaintext), IdPrefix.standard.rawValue + senderX25519PublicKey.toHexString())
|
||||
/// v, ct, nc = data[0], data[1:-24], data[-24:]
|
||||
let version: UInt8 = data.bytes[0]
|
||||
let ciphertext: Bytes = Bytes(data.bytes[1..<(data.count - dependencies.nonceGenerator24.NonceBytes)])
|
||||
let nonce: Bytes = Bytes(data.bytes[(data.count - dependencies.nonceGenerator24.NonceBytes)..<data.count])
|
||||
|
||||
/// Make sure our encryption version is okay
|
||||
guard version == 0 else { throw Error.decryptionFailed }
|
||||
|
||||
/// Decrypt
|
||||
guard let innerBytes: Bytes = dependencies.aeadXChaCha20Poly1305Ietf.decrypt(authenticatedCipherText: ciphertext, secretKey: dec_key, nonce: nonce) else {
|
||||
throw Error.decryptionFailed
|
||||
}
|
||||
|
||||
/// Ensure the length is correct
|
||||
guard innerBytes.count > dependencies.sign.PublicKeyBytes else { throw Error.decryptionFailed }
|
||||
|
||||
/// Split up: the last 32 bytes are the sender's *unblinded* ed25519 key
|
||||
let plaintext: Bytes = Bytes(innerBytes[
|
||||
0...(innerBytes.count - 1 - dependencies.sign.PublicKeyBytes)
|
||||
])
|
||||
let sender_edpk: Bytes = Bytes(innerBytes[
|
||||
(innerBytes.count - dependencies.sign.PublicKeyBytes)...(innerBytes.count - 1)
|
||||
])
|
||||
|
||||
/// Verify that the inner sender_edpk (A) yields the same outer kA we got with the message
|
||||
guard let blindingFactor: Bytes = dependencies.sodium.generateBlindingFactor(serverPublicKey: openGroupPublicKey) else {
|
||||
throw Error.invalidSignature
|
||||
}
|
||||
guard let sharedSecret: Bytes = dependencies.sodium.combineKeys(lhsKeyBytes: blindingFactor, rhsKeyBytes: sender_edpk) else {
|
||||
throw Error.invalidSignature
|
||||
}
|
||||
guard kA == sharedSecret else { throw Error.invalidSignature }
|
||||
|
||||
/// Get the sender's X25519 public key
|
||||
guard let senderSessionIdBytes: Bytes = dependencies.sign.toX25519(ed25519PublicKey: sender_edpk) else {
|
||||
throw Error.decryptionFailed
|
||||
}
|
||||
|
||||
return (Data(plaintext), SessionId(.standard, publicKey: senderSessionIdBytes).hexString)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -48,66 +48,90 @@ public enum MessageReceiver {
|
|||
}
|
||||
}
|
||||
|
||||
public static func parse(_ data: Data, openGroupMessageServerID: UInt64?, isRetry: Bool = false, using transaction: Any) throws -> (Message, SNProtoContent) {
|
||||
public static func parse(_ data: Data, openGroupMessageServerID: UInt64?, openGroupServerPublicKey: String? = nil, isRetry: Bool = false, using transaction: Any) throws -> (Message, SNProtoContent) {
|
||||
let userPublicKey = SNMessagingKitConfiguration.shared.storage.getUserPublicKey()
|
||||
let isOpenGroupMessage = (openGroupMessageServerID != nil)
|
||||
|
||||
// Parse the envelope
|
||||
let envelope = try SNProtoEnvelope.parseData(data)
|
||||
let storage = SNMessagingKitConfiguration.shared.storage
|
||||
|
||||
// Decrypt the contents
|
||||
guard let ciphertext = envelope.content else { throw Error.noData }
|
||||
|
||||
var plaintext: Data!
|
||||
var sender: String!
|
||||
var groupPublicKey: String? = nil
|
||||
|
||||
if isOpenGroupMessage {
|
||||
(plaintext, sender) = (envelope.content!, envelope.source!)
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
switch envelope.type {
|
||||
case .sessionMessage:
|
||||
guard let userX25519KeyPair = SNMessagingKitConfiguration.shared.storage.getUserKeyPair() else { throw Error.noUserX25519KeyPair }
|
||||
(plaintext, sender) = try decryptWithSessionProtocol(ciphertext: ciphertext, using: userX25519KeyPair)
|
||||
case .closedGroupMessage:
|
||||
guard let hexEncodedGroupPublicKey = envelope.source, SNMessagingKitConfiguration.shared.storage.isClosedGroup(hexEncodedGroupPublicKey) else { throw Error.invalidGroupPublicKey }
|
||||
var encryptionKeyPairs = Storage.shared.getClosedGroupEncryptionKeyPairs(for: hexEncodedGroupPublicKey)
|
||||
guard !encryptionKeyPairs.isEmpty else { throw Error.noGroupKeyPair }
|
||||
// Loop through all known group key pairs in reverse order (i.e. try the latest key pair first (which'll more than
|
||||
// likely be the one we want) but try older ones in case that didn't work)
|
||||
var encryptionKeyPair = encryptionKeyPairs.removeLast()
|
||||
func decrypt() throws {
|
||||
do {
|
||||
(plaintext, sender) = try decryptWithSessionProtocol(ciphertext: ciphertext, using: encryptionKeyPair)
|
||||
} catch {
|
||||
if !encryptionKeyPairs.isEmpty {
|
||||
encryptionKeyPair = encryptionKeyPairs.removeLast()
|
||||
try decrypt()
|
||||
} else {
|
||||
throw error
|
||||
case .sessionMessage:
|
||||
// Default to 'standard' as the old code didn't seem to require an `envelope.source`
|
||||
switch (SessionId.Prefix(from: envelope.source) ?? .standard) {
|
||||
case .standard, .unblinded:
|
||||
guard let userX25519KeyPair = SNMessagingKitConfiguration.shared.storage.getUserKeyPair() else {
|
||||
throw Error.noUserX25519KeyPair
|
||||
}
|
||||
|
||||
(plaintext, sender) = try decryptWithSessionProtocol(ciphertext: ciphertext, using: userX25519KeyPair)
|
||||
|
||||
case .blinded:
|
||||
guard let senderSessionId: String = envelope.source else { throw Error.noData }
|
||||
guard let openGroupServerPublicKey: String = openGroupServerPublicKey else {
|
||||
throw Error.invalidGroupPublicKey
|
||||
}
|
||||
guard let userEd25519KeyPair = SNMessagingKitConfiguration.shared.storage.getUserED25519KeyPair() else {
|
||||
throw Error.noUserED25519KeyPair
|
||||
}
|
||||
|
||||
(plaintext, sender) = try decryptWithSessionBlindingProtocol(
|
||||
data: ciphertext,
|
||||
fromBlindedPublicKey: senderSessionId,
|
||||
with: openGroupServerPublicKey,
|
||||
userEd25519KeyPair: userEd25519KeyPair
|
||||
)
|
||||
}
|
||||
|
||||
case .closedGroupMessage:
|
||||
guard let hexEncodedGroupPublicKey = envelope.source, SNMessagingKitConfiguration.shared.storage.isClosedGroup(hexEncodedGroupPublicKey) else {
|
||||
throw Error.invalidGroupPublicKey
|
||||
}
|
||||
|
||||
var encryptionKeyPairs = Storage.shared.getClosedGroupEncryptionKeyPairs(for: hexEncodedGroupPublicKey)
|
||||
|
||||
guard !encryptionKeyPairs.isEmpty else { throw Error.noGroupKeyPair }
|
||||
|
||||
// Loop through all known group key pairs in reverse order (i.e. try the latest key pair first (which'll more than
|
||||
// likely be the one we want) but try older ones in case that didn't work)
|
||||
var encryptionKeyPair = encryptionKeyPairs.removeLast()
|
||||
|
||||
func decrypt() throws {
|
||||
do {
|
||||
(plaintext, sender) = try decryptWithSessionProtocol(ciphertext: ciphertext, using: encryptionKeyPair)
|
||||
}
|
||||
catch {
|
||||
if !encryptionKeyPairs.isEmpty {
|
||||
encryptionKeyPair = encryptionKeyPairs.removeLast()
|
||||
try decrypt()
|
||||
}
|
||||
else {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
groupPublicKey = envelope.source
|
||||
try decrypt()
|
||||
/*
|
||||
do {
|
||||
groupPublicKey = envelope.source
|
||||
try decrypt()
|
||||
} catch {
|
||||
do {
|
||||
let now = Date()
|
||||
// Don't spam encryption key pair requests
|
||||
let shouldRequestEncryptionKeyPair = given(lastEncryptionKeyPairRequest[groupPublicKey!]) { now.timeIntervalSince($0) > 30 } ?? true
|
||||
if shouldRequestEncryptionKeyPair {
|
||||
try MessageSender.requestEncryptionKeyPair(for: groupPublicKey!, using: transaction as! YapDatabaseReadWriteTransaction)
|
||||
lastEncryptionKeyPairRequest[groupPublicKey!] = now
|
||||
}
|
||||
}
|
||||
throw error // Throw the * decryption * error and not the error generated by requestEncryptionKeyPair (if it generated one)
|
||||
}
|
||||
*/
|
||||
default: throw Error.unknownEnvelopeType
|
||||
|
||||
default: throw Error.unknownEnvelopeType
|
||||
}
|
||||
}
|
||||
|
||||
// Don't process the envelope any further if the sender is blocked
|
||||
guard !isBlocked(sender) else { throw Error.senderBlocked }
|
||||
|
||||
// Parse the proto
|
||||
let proto: SNProtoContent
|
||||
do {
|
||||
|
|
|
@ -8,7 +8,7 @@ extension MessageSender {
|
|||
var members = members
|
||||
let userPublicKey = getUserHexEncodedPublicKey()
|
||||
// Generate the group's public key
|
||||
let groupPublicKey = Curve25519.generateKeyPair().hexEncodedPublicKey // Includes the 'IdPrefix.standard' prefix
|
||||
let groupPublicKey = Curve25519.generateKeyPair().hexEncodedPublicKey // Includes the 'SessionId.Prefix.standard' prefix
|
||||
// Generate the key pair that'll be used for encryption and decryption
|
||||
let encryptionKeyPair = Curve25519.generateKeyPair()
|
||||
// Ensure the current user is included in the member list
|
||||
|
|
|
@ -4,15 +4,59 @@ import Sodium
|
|||
extension MessageSender {
|
||||
|
||||
internal static func encryptWithSessionProtocol(_ plaintext: Data, for recipientHexEncodedX25519PublicKey: String) throws -> Data {
|
||||
guard let userED25519KeyPair = SNMessagingKitConfiguration.shared.storage.getUserED25519KeyPair() else { throw Error.noUserED25519KeyPair }
|
||||
guard let userED25519KeyPair = SNMessagingKitConfiguration.shared.storage.getUserED25519KeyPair() else {
|
||||
throw Error.noUserED25519KeyPair
|
||||
}
|
||||
|
||||
let recipientX25519PublicKey = Data(hex: recipientHexEncodedX25519PublicKey.removingIdPrefixIfNeeded())
|
||||
let sodium = Sodium()
|
||||
|
||||
let verificationData = plaintext + Data(userED25519KeyPair.publicKey) + recipientX25519PublicKey
|
||||
guard let signature = sodium.sign.signature(message: Bytes(verificationData), secretKey: userED25519KeyPair.secretKey) else { throw Error.signingFailed }
|
||||
guard let signature = sodium.sign.signature(message: Bytes(verificationData), secretKey: userED25519KeyPair.secretKey) else {
|
||||
throw Error.signingFailed
|
||||
}
|
||||
|
||||
let plaintextWithMetadata = plaintext + Data(userED25519KeyPair.publicKey) + Data(signature)
|
||||
guard let ciphertext = sodium.box.seal(message: Bytes(plaintextWithMetadata), recipientPublicKey: Bytes(recipientX25519PublicKey)) else { throw Error.encryptionFailed }
|
||||
guard let ciphertext = sodium.box.seal(message: Bytes(plaintextWithMetadata), recipientPublicKey: Bytes(recipientX25519PublicKey)) else {
|
||||
throw Error.encryptionFailed
|
||||
}
|
||||
|
||||
return Data(ciphertext)
|
||||
}
|
||||
|
||||
internal static func encryptWithSessionBlindingProtocol(_ plaintext: Data, for recipientBlindedId: String, openGroupPublicKey: String, using dependencies: OpenGroupAPI.Dependencies = OpenGroupAPI.Dependencies()) throws -> Data {
|
||||
guard SessionId.Prefix(from: recipientBlindedId) == .blinded else { throw Error.signingFailed }
|
||||
guard let userEd25519KeyPair = SNMessagingKitConfiguration.shared.storage.getUserED25519KeyPair() else {
|
||||
throw Error.noUserED25519KeyPair
|
||||
}
|
||||
guard let blindedKeyPair = dependencies.sodium.blindedKeyPair(serverPublicKey: openGroupPublicKey, edKeyPair: userEd25519KeyPair, genericHash: dependencies.genericHash) else {
|
||||
throw Error.signingFailed
|
||||
}
|
||||
|
||||
let recipientBlindedPublicKey = Data(hex: recipientBlindedId.removingIdPrefixIfNeeded())
|
||||
|
||||
/// Step one: calculate the shared encryption key, sending from A to B
|
||||
guard let enc_key: Bytes = dependencies.sodium.sharedBlindedEncryptionKey(
|
||||
secretKey: userEd25519KeyPair.secretKey,
|
||||
otherBlindedPublicKey: recipientBlindedPublicKey.bytes,
|
||||
fromBlindedPublicKey: blindedKeyPair.publicKey,
|
||||
toBlindedPublicKey: recipientBlindedPublicKey.bytes,
|
||||
genericHash: dependencies.genericHash
|
||||
) else {
|
||||
throw Error.signingFailed
|
||||
}
|
||||
|
||||
/// Inner data: msg || A (i.e. the sender's ed25519 master pubkey, *not* kA blinded pubkey)
|
||||
let innerBytes: Bytes = (plaintext.bytes + userEd25519KeyPair.publicKey)
|
||||
|
||||
/// Encrypt using xchacha20-poly1305
|
||||
let nonce: Bytes = dependencies.nonceGenerator24.nonce()
|
||||
|
||||
guard let ciphertext = dependencies.aeadXChaCha20Poly1305Ietf.encrypt(message: innerBytes, secretKey: enc_key, nonce: nonce) else {
|
||||
throw Error.encryptionFailed
|
||||
}
|
||||
|
||||
/// data = b'\x00' + ciphertext + nonce
|
||||
return Data(Bytes(arrayLiteral: 0) + ciphertext + nonce)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -103,6 +103,9 @@ public final class MessageSender : NSObject {
|
|||
|
||||
case .legacyOpenGroup, .openGroup:
|
||||
return sendToOpenGroupDestination(destination, message: message, using: transaction)
|
||||
|
||||
case .openGroupInbox:
|
||||
return sendToOpenGroupInboxDestination(destination, message: message, using: transaction)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -125,7 +128,7 @@ public final class MessageSender : NSObject {
|
|||
switch destination {
|
||||
case .contact(let publicKey): message.recipient = publicKey
|
||||
case .closedGroup(let groupPublicKey): message.recipient = groupPublicKey
|
||||
case .legacyOpenGroup, .openGroup: preconditionFailure()
|
||||
case .legacyOpenGroup, .openGroup, .openGroupInbox: preconditionFailure()
|
||||
}
|
||||
|
||||
let isSelfSend = (message.recipient == userPublicKey)
|
||||
|
@ -183,7 +186,7 @@ public final class MessageSender : NSObject {
|
|||
|
||||
ciphertext = try encryptWithSessionProtocol(plaintext, for: encryptionKeyPair.hexEncodedPublicKey)
|
||||
|
||||
case .legacyOpenGroup, .openGroup: preconditionFailure()
|
||||
case .legacyOpenGroup, .openGroup, .openGroupInbox: preconditionFailure()
|
||||
}
|
||||
}
|
||||
catch {
|
||||
|
@ -205,7 +208,7 @@ public final class MessageSender : NSObject {
|
|||
kind = .closedGroupMessage
|
||||
senderPublicKey = groupPublicKey
|
||||
|
||||
case .legacyOpenGroup, .openGroup: preconditionFailure()
|
||||
case .legacyOpenGroup, .openGroup, .openGroupInbox: preconditionFailure()
|
||||
}
|
||||
|
||||
let wrappedMessage: Data
|
||||
|
@ -302,15 +305,14 @@ public final class MessageSender : NSObject {
|
|||
preconditionFailure()
|
||||
}
|
||||
|
||||
message.sender = IdPrefix.blinded.hexEncodedPublicKey(for: blindedKeyPair.publicKey)
|
||||
message.sender = SessionId(.blinded, publicKey: blindedKeyPair.publicKey).hexString
|
||||
}
|
||||
else {
|
||||
message.sender = IdPrefix.unblinded.hexEncodedPublicKey(for: userEdKeyPair.publicKey)
|
||||
message.sender = SessionId(.unblinded, publicKey: userEdKeyPair.publicKey).hexString
|
||||
}
|
||||
|
||||
switch destination {
|
||||
case .contact(_): preconditionFailure()
|
||||
case .closedGroup(_): preconditionFailure()
|
||||
case .contact, .closedGroup, .openGroupInbox: preconditionFailure()
|
||||
case .legacyOpenGroup(let channel, let server): message.recipient = "\(server).\(channel)"
|
||||
|
||||
case .openGroup(let room, let server, let whisperTo, let whisperMods, _):
|
||||
|
@ -405,6 +407,99 @@ public final class MessageSender : NSObject {
|
|||
|
||||
return promise
|
||||
}
|
||||
|
||||
internal static func sendToOpenGroupInboxDestination(_ destination: Message.Destination, message: Message, using transaction: Any, dependencies: OpenGroupAPI.Dependencies = OpenGroupAPI.Dependencies()) -> Promise<Void> {
|
||||
let (promise, seal) = Promise<Void>.pending()
|
||||
let storage = SNMessagingKitConfiguration.shared.storage
|
||||
let transaction = transaction as! YapDatabaseReadWriteTransaction
|
||||
let userPublicKey = storage.getUserPublicKey()
|
||||
|
||||
guard case .openGroupInbox(let server, let openGroupPublicKey, let recipientBlindedPublicKey) = destination else {
|
||||
preconditionFailure()
|
||||
}
|
||||
|
||||
// Set the timestamp, sender and recipient
|
||||
if message.sentTimestamp == nil { // Visible messages will already have their sent timestamp set
|
||||
message.sentTimestamp = NSDate.millisecondTimestamp()
|
||||
}
|
||||
|
||||
message.sender = userPublicKey
|
||||
message.recipient = recipientBlindedPublicKey
|
||||
|
||||
// Set the failure handler (need it here already for precondition failure handling)
|
||||
func handleFailure(with error: Swift.Error, using transaction: YapDatabaseReadWriteTransaction) {
|
||||
MessageSender.handleFailedMessageSend(message, with: error, using: transaction)
|
||||
seal.reject(error)
|
||||
}
|
||||
|
||||
// Attach the user's profile if needed
|
||||
if let message = message as? VisibleMessage {
|
||||
guard let name = storage.getUser()?.name else {
|
||||
handleFailure(with: Error.noUsername, using: transaction)
|
||||
return promise
|
||||
}
|
||||
|
||||
if let profileKey = storage.getUser()?.profileEncryptionKey?.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
|
||||
}
|
||||
|
||||
// Encrypt the serialized protobuf
|
||||
let ciphertext: Data
|
||||
|
||||
do {
|
||||
ciphertext = try encryptWithSessionBlindingProtocol(plaintext, for: recipientBlindedPublicKey, openGroupPublicKey: openGroupPublicKey, using: dependencies)
|
||||
}
|
||||
catch {
|
||||
SNLog("Couldn't encrypt message for destination: \(destination) due to error: \(error).")
|
||||
handleFailure(with: error, using: transaction)
|
||||
return promise
|
||||
}
|
||||
|
||||
// Send the result
|
||||
|
||||
OpenGroupAPI
|
||||
.send(
|
||||
ciphertext,
|
||||
toInboxFor: recipientBlindedPublicKey,
|
||||
on: server,
|
||||
using: dependencies
|
||||
)
|
||||
.done(on: DispatchQueue.global(qos: .userInitiated)) { responseInfo, data in
|
||||
dependencies.storage.write { transaction in
|
||||
MessageSender.handleSuccessfulMessageSend(message, to: destination, using: transaction)
|
||||
seal.fulfill(())
|
||||
}
|
||||
}
|
||||
.catch(on: DispatchQueue.global(qos: .userInitiated)) { error in
|
||||
dependencies.storage.write { transaction in
|
||||
handleFailure(with: error, using: transaction as! YapDatabaseReadWriteTransaction)
|
||||
}
|
||||
}
|
||||
|
||||
return promise
|
||||
}
|
||||
|
||||
// MARK: Success & Failure Handling
|
||||
public static func handleSuccessfulMessageSend(_ message: Message, to destination: Message.Destination, serverTimestamp: UInt64? = nil, isSyncMessage: Bool = false, using transaction: Any) {
|
||||
|
|
|
@ -83,46 +83,58 @@ extension OpenGroupAPI {
|
|||
return promise
|
||||
}
|
||||
|
||||
private func handlePollResponse(_ response: [OpenGroupAPI.Endpoint: (info: OnionRequestResponseInfoType, data: Codable)], isBackgroundPoll: Bool) {
|
||||
response.forEach { endpoint, response in
|
||||
private func handlePollResponse(_ response: [OpenGroupAPI.Endpoint: (info: OnionRequestResponseInfoType, data: Codable?)], isBackgroundPoll: Bool) {
|
||||
response.forEach { endpoint, endpointResponse in
|
||||
switch endpoint {
|
||||
case .capabilities:
|
||||
guard let responseData: BatchSubResponse<Capabilities> = response.data as? BatchSubResponse<Capabilities> else {
|
||||
guard let responseData: BatchSubResponse<Capabilities> = endpointResponse.data as? BatchSubResponse<Capabilities>, let responseBody: Capabilities = responseData.body else {
|
||||
SNLog("Open group polling failed due to invalid data.")
|
||||
return
|
||||
}
|
||||
|
||||
OpenGroupManager.handleCapabilities(
|
||||
responseData.body,
|
||||
responseBody,
|
||||
on: server
|
||||
)
|
||||
|
||||
case .roomMessagesRecent(let roomToken), .roomMessagesBefore(let roomToken, _), .roomMessagesSince(let roomToken, _):
|
||||
guard let responseData: BatchSubResponse<[Message]> = response.data as? BatchSubResponse<[Message]> else {
|
||||
guard let responseData: BatchSubResponse<[Message]> = endpointResponse.data as? BatchSubResponse<[Message]>, let responseBody: [Message] = responseData.body else {
|
||||
SNLog("Open group polling failed due to invalid data.")
|
||||
return
|
||||
}
|
||||
|
||||
OpenGroupManager.handleMessages(
|
||||
responseData.body,
|
||||
responseBody,
|
||||
for: roomToken,
|
||||
on: server,
|
||||
isBackgroundPoll: isBackgroundPoll
|
||||
)
|
||||
|
||||
case .roomPollInfo(let roomToken, _):
|
||||
guard let responseData: BatchSubResponse<RoomPollInfo> = response.data as? BatchSubResponse<RoomPollInfo> else {
|
||||
guard let responseData: BatchSubResponse<RoomPollInfo> = endpointResponse.data as? BatchSubResponse<RoomPollInfo>, let responseBody: RoomPollInfo = responseData.body else {
|
||||
SNLog("Open group polling failed due to invalid data.")
|
||||
return
|
||||
}
|
||||
|
||||
OpenGroupManager.handlePollInfo(
|
||||
responseData.body,
|
||||
responseBody,
|
||||
publicKey: nil,
|
||||
for: roomToken,
|
||||
on: server
|
||||
)
|
||||
|
||||
case .inbox, .inboxSince:
|
||||
guard let responseData: BatchSubResponse<[DirectMessage]> = endpointResponse.data as? BatchSubResponse<[DirectMessage]>, let responseBody: [DirectMessage] = responseData.body else {
|
||||
SNLog("Open group polling failed due to invalid data.")
|
||||
return
|
||||
}
|
||||
|
||||
OpenGroupManager.handleInbox(
|
||||
responseBody,
|
||||
on: server,
|
||||
isBackgroundPoll: isBackgroundPoll
|
||||
)
|
||||
|
||||
default: break // No custom handling needed
|
||||
}
|
||||
}
|
||||
|
|
|
@ -71,6 +71,12 @@ public protocol SessionMessagingKitStorageProtocol {
|
|||
func getOpenGroupSequenceNumber(for room: String, on server: String) -> Int64?
|
||||
func setOpenGroupSequenceNumber(for room: String, on server: String, to newValue: Int64, using transaction: Any)
|
||||
func removeOpenGroupSequenceNumber(for room: String, on server: String, using transaction: Any)
|
||||
|
||||
// MARK: - -- Open Group Inbox Latest Message Id
|
||||
|
||||
func getOpenGroupInboxLatestMessageId(for server: String) -> Int64?
|
||||
func setOpenGroupInboxLatestMessageId(for server: String, to newValue: Int64, using transaction: Any)
|
||||
func removeOpenGroupInboxLatestMessageId(for server: String, using transaction: Any)
|
||||
|
||||
// MARK: - Message Handling
|
||||
|
||||
|
|
|
@ -47,14 +47,9 @@ extension Sodium {
|
|||
private static let publicKeyLength: Int = Int(crypto_scalarmult_bytes()) // 32
|
||||
private static let secretKeyLength: Int = Int(crypto_sign_secretkeybytes()) // 64
|
||||
|
||||
/// Constructs a "blinded" key pair (`ka, kA`) based on an open group server `publicKey` and an ed25519 `keyPair`
|
||||
public func blindedKeyPair(serverPublicKey: String, edKeyPair: Box.KeyPair, genericHash: GenericHashType) -> Box.KeyPair? {
|
||||
guard edKeyPair.publicKey.count == Sodium.publicKeyLength && edKeyPair.secretKey.count == Sodium.secretKeyLength else {
|
||||
return nil
|
||||
}
|
||||
|
||||
/// 64-byte blake2b hash then reduce to get the blinding factor:
|
||||
/// k = salt.crypto_core_ed25519_scalar_reduce(blake2b(server_pk, digest_size=64).digest())
|
||||
/// 64-byte blake2b hash then reduce to get the blinding factor
|
||||
public func generateBlindingFactor(serverPublicKey: String) -> Bytes? {
|
||||
/// k = salt.crypto_core_ed25519_scalar_reduce(blake2b(server_pk, digest_size=64).digest())
|
||||
guard let serverPubKeyData: Data = serverPublicKey.dataFromHex() else { return nil }
|
||||
guard let serverPublicKeyHashBytes: Bytes = genericHash.hash(message: [UInt8](serverPubKeyData), outputLength: 64) else {
|
||||
return nil
|
||||
|
@ -75,15 +70,18 @@ extension Sodium {
|
|||
/// Ensure the above worked
|
||||
guard kResult == 0 else { return nil }
|
||||
|
||||
/// Calculate k*a. To get 'a' (the Ed25519 private key scalar) we call the sodium function to
|
||||
/// convert to an *x* secret key, which seems wrong--but isn't because converted keys use the
|
||||
/// same secret scalar secret. (And so this is just the most convenient way to get 'a' out of
|
||||
/// a sodium Ed25519 secret key).
|
||||
/// a = s.to_curve25519_private_key().encode()
|
||||
let secretKeyBytes: Bytes = [UInt8](edKeyPair.secretKey)
|
||||
return Data(bytes: kPtr, count: Sodium.scalarLength).bytes
|
||||
}
|
||||
|
||||
/// Calculate k*a. To get 'a' (the Ed25519 private key scalar) we call the sodium function to
|
||||
/// convert to an *x* secret key, which seems wrong--but isn't because converted keys use the
|
||||
/// same secret scalar secret (and so this is just the most convenient way to get 'a' out of
|
||||
/// a sodium Ed25519 secret key)
|
||||
private func generatePrivateKeyScalar(secretKey: Bytes) -> Bytes? {
|
||||
/// a = s.to_curve25519_private_key().encode()
|
||||
let aPtr: UnsafeMutablePointer<UInt8> = UnsafeMutablePointer<UInt8>.allocate(capacity: Sodium.scalarMultLength)
|
||||
|
||||
let aResult = secretKeyBytes.withUnsafeBytes { (secretKeyPtr: UnsafeRawBufferPointer) -> Int32 in
|
||||
let aResult = secretKey.withUnsafeBytes { (secretKeyPtr: UnsafeRawBufferPointer) -> Int32 in
|
||||
guard let secretKeyBaseAddress: UnsafePointer<UInt8> = secretKeyPtr.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
|
||||
return -1
|
||||
}
|
||||
|
@ -94,10 +92,38 @@ extension Sodium {
|
|||
/// Ensure the above worked
|
||||
guard aResult == 0 else { return nil }
|
||||
|
||||
return Data(bytes: aPtr, count: Sodium.scalarMultLength).bytes
|
||||
}
|
||||
|
||||
/// Constructs a "blinded" key pair (`ka, kA`) based on an open group server `publicKey` and an ed25519 `keyPair`
|
||||
public func blindedKeyPair(serverPublicKey: String, edKeyPair: Box.KeyPair, genericHash: GenericHashType) -> Box.KeyPair? {
|
||||
guard edKeyPair.publicKey.count == Sodium.publicKeyLength && edKeyPair.secretKey.count == Sodium.secretKeyLength else {
|
||||
return nil
|
||||
}
|
||||
guard let kBytes: Bytes = generateBlindingFactor(serverPublicKey: serverPublicKey) else { return nil }
|
||||
guard let aBytes: Bytes = generatePrivateKeyScalar(secretKey: edKeyPair.secretKey) else { return nil }
|
||||
|
||||
/// Generate the blinded key pair `ka`, `kA`
|
||||
let kaPtr: UnsafeMutablePointer<UInt8> = UnsafeMutablePointer<UInt8>.allocate(capacity: Sodium.secretKeyLength)
|
||||
let kAPtr: UnsafeMutablePointer<UInt8> = UnsafeMutablePointer<UInt8>.allocate(capacity: Sodium.publicKeyLength)
|
||||
crypto_core_ed25519_scalar_mul(kaPtr, kPtr, aPtr)
|
||||
|
||||
let kaResult = aBytes.withUnsafeBytes { (aPtr: UnsafeRawBufferPointer) -> Int32 in
|
||||
return kBytes.withUnsafeBytes { (kPtr: UnsafeRawBufferPointer) -> Int32 in
|
||||
guard let kBaseAddress: UnsafePointer<UInt8> = kPtr.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
|
||||
return -1
|
||||
}
|
||||
guard let aBaseAddress: UnsafePointer<UInt8> = aPtr.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
|
||||
return -1
|
||||
}
|
||||
|
||||
crypto_core_ed25519_scalar_mul(kaPtr, kBaseAddress, aBaseAddress)
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
/// Ensure the above worked
|
||||
guard kaResult == 0 else { return nil }
|
||||
|
||||
guard crypto_scalarmult_ed25519_base_noclamp(kAPtr, kaPtr) == 0 else { return nil }
|
||||
|
||||
return Box.KeyPair(
|
||||
|
@ -108,7 +134,7 @@ extension Sodium {
|
|||
|
||||
/// Constructs an Ed25519 signature from a root Ed25519 key and a blinded scalar/pubkey pair, with one tweak to the
|
||||
/// construction: we add kA into the hashed value that yields r so that we have domain separation for different blinded
|
||||
/// pubkeys (This doesn't affect verification at all).
|
||||
/// pubkeys (this doesn't affect verification at all)
|
||||
public func sogsSignature(message: Bytes, secretKey: Bytes, blindedSecretKey ka: Bytes, blindedPublicKey kA: Bytes) -> Bytes? {
|
||||
/// H_rh = sha512(s.encode()).digest()[32:]
|
||||
let H_rh: Bytes = Bytes(secretKey.sha512().suffix(32))
|
||||
|
@ -170,25 +196,41 @@ extension Sodium {
|
|||
return (Data(bytes: sig_RPtr, count: Sodium.noClampLength).bytes + Data(bytes: sig_sPtr, count: Sodium.scalarLength).bytes)
|
||||
}
|
||||
|
||||
// TODO: Determine if we still need this? (To generate the `kB` value for the `/inbox` API????)
|
||||
public func sharedEdSecret(_ firstKeyBytes: [UInt8], _ secondKeyBytes: [UInt8]) -> Bytes? {
|
||||
let sharedSecretPtr: UnsafeMutablePointer<UInt8> = UnsafeMutablePointer<UInt8>.allocate(capacity: Sodium.noClampLength)
|
||||
let result = secondKeyBytes.withUnsafeBytes { (secondKeyPtr: UnsafeRawBufferPointer) -> Int32 in
|
||||
return firstKeyBytes.withUnsafeBytes { (firstKeyPtr: UnsafeRawBufferPointer) -> Int32 in
|
||||
guard let firstKeyBaseAddress: UnsafePointer<UInt8> = firstKeyPtr.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
|
||||
/// Combines two keys (`kA`)
|
||||
public func combineKeys(lhsKeyBytes: Bytes, rhsKeyBytes: Bytes) -> Bytes? {
|
||||
let combinedPtr: UnsafeMutablePointer<UInt8> = UnsafeMutablePointer<UInt8>.allocate(capacity: Sodium.noClampLength)
|
||||
|
||||
let result = rhsKeyBytes.withUnsafeBytes { (rhsKeyBytesPtr: UnsafeRawBufferPointer) -> Int32 in
|
||||
return lhsKeyBytes.withUnsafeBytes { (lhsKeyBytesPtr: UnsafeRawBufferPointer) -> Int32 in
|
||||
guard let lhsKeyBytesBaseAddress: UnsafePointer<UInt8> = lhsKeyBytesPtr.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
|
||||
return -1
|
||||
}
|
||||
guard let secondKeyBaseAddress: UnsafePointer<UInt8> = secondKeyPtr.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
|
||||
guard let rhsKeyBytesBaseAddress: UnsafePointer<UInt8> = rhsKeyBytesPtr.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
|
||||
return -1
|
||||
}
|
||||
|
||||
return crypto_scalarmult_ed25519_noclamp(sharedSecretPtr, firstKeyBaseAddress, secondKeyBaseAddress)
|
||||
return crypto_scalarmult_ed25519_noclamp(combinedPtr, lhsKeyBytesBaseAddress, rhsKeyBytesBaseAddress)
|
||||
}
|
||||
}
|
||||
|
||||
/// Ensure the above worked
|
||||
guard result == 0 else { return nil }
|
||||
|
||||
return Data(bytes: sharedSecretPtr, count: Sodium.scalarMultLength).bytes
|
||||
return Data(bytes: combinedPtr, count: Sodium.noClampLength).bytes
|
||||
}
|
||||
|
||||
/// Calculate a shared secret for a message from A to B:
|
||||
///
|
||||
/// BLAKE2b(a kB || kA || kB)
|
||||
///
|
||||
/// The receiver can calulate the same value via:
|
||||
///
|
||||
/// BLAKE2b(b kA || kA || kB)
|
||||
public func sharedBlindedEncryptionKey(secretKey: Bytes, otherBlindedPublicKey: Bytes, fromBlindedPublicKey kA: Bytes, toBlindedPublicKey kB: Bytes, genericHash: GenericHashType) -> Bytes? {
|
||||
guard let aBytes: Bytes = generatePrivateKeyScalar(secretKey: secretKey) else { return nil }
|
||||
guard let combinedKeyBytes: Bytes = combineKeys(lhsKeyBytes: aBytes, rhsKeyBytes: otherBlindedPublicKey) else { return nil }
|
||||
|
||||
return genericHash.hash(message: (combinedKeyBytes + kA + kB), outputLength: 32)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -218,3 +260,25 @@ extension GenericHash {
|
|||
return output
|
||||
}
|
||||
}
|
||||
|
||||
extension AeadXChaCha20Poly1305IetfType {
|
||||
/// This method is the same as the standard AeadXChaCha20Poly1305IetfType `encrypt` method except it allows the
|
||||
/// specification of a nonce which allows for deterministic behaviour with unit testing
|
||||
public func encrypt(message: Bytes, secretKey: Bytes, nonce: Bytes, additionalData: Bytes? = nil) -> Bytes? {
|
||||
guard secretKey.count == KeyBytes else { return nil }
|
||||
|
||||
var authenticatedCipherText = Bytes(repeating: 0, count: message.count + ABytes)
|
||||
var authenticatedCipherTextLen: UInt64 = 0
|
||||
|
||||
let result = crypto_aead_xchacha20poly1305_ietf_encrypt(
|
||||
&authenticatedCipherText, &authenticatedCipherTextLen,
|
||||
message, UInt64(message.count),
|
||||
additionalData, UInt64(additionalData?.count ?? 0),
|
||||
nil, nonce, secretKey
|
||||
)
|
||||
|
||||
guard result == 0 else { return nil }
|
||||
|
||||
return authenticatedCipherText
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,15 +7,19 @@ public extension ECKeyPair {
|
|||
}
|
||||
|
||||
@objc var hexEncodedPublicKey: String {
|
||||
// Prefixing with 'IdPrefix.standard' is necessary for what seems to be a sort of Signal public key versioning system
|
||||
return IdPrefix.standard.rawValue + publicKey.map { String(format: "%02hhx", $0) }.joined()
|
||||
// Prefixing with 'SessionId.Prefix.standard' is necessary for what seems to be a sort of Signal public key versioning system
|
||||
return SessionId(.standard, publicKey: publicKey.bytes).hexString
|
||||
}
|
||||
|
||||
@objc static func isValidHexEncodedPublicKey(candidate: String) -> Bool {
|
||||
// Note: If the logic in here changes ensure it doesn't break `SessionId.Prefix(from:)`
|
||||
// Check that it's a valid hexadecimal encoding
|
||||
guard Hex.isValid(candidate) else { return false }
|
||||
// Check that it has length 66 and a valid prefix
|
||||
guard candidate.count == 66 && IdPrefix.allCases.first(where: { candidate.hasPrefix($0.rawValue) }) != nil else { return false }
|
||||
guard candidate.count == 66 && SessionId.Prefix.allCases.first(where: { candidate.hasPrefix($0.rawValue) }) != nil else {
|
||||
return false
|
||||
}
|
||||
|
||||
// It appears to be a valid public key
|
||||
return true
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@ public extension Data {
|
|||
|
||||
func removingIdPrefixIfNeeded() -> Data {
|
||||
var result = self
|
||||
if result.count == 33 && IdPrefix(with: result.toHexString()) != nil { result.removeFirst() }
|
||||
if result.count == 33 && SessionId.Prefix(from: result.toHexString()) != nil { result.removeFirst() }
|
||||
return result
|
||||
}
|
||||
|
||||
|
@ -29,7 +29,7 @@ public extension Data {
|
|||
|
||||
@objc func removingIdPrefixIfNeeded() -> NSData {
|
||||
var result = self as Data
|
||||
if result.count == 33 && IdPrefix(with: result.toHexString()) != nil { result.removeFirst() }
|
||||
if result.count == 33 && SessionId.Prefix(from: result.toHexString()) != nil { result.removeFirst() }
|
||||
return result as NSData
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,24 +0,0 @@
|
|||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
import Sodium
|
||||
import Curve25519Kit
|
||||
|
||||
public enum IdPrefix: String, CaseIterable {
|
||||
case standard = "05" // Used for identified users, open groups, etc.
|
||||
case blinded = "15" // Used for participants in open groups with blinding enabled
|
||||
case unblinded = "00" // Used for participants in open groups with blinding disabled
|
||||
|
||||
public init?(with sessionId: String) {
|
||||
// TODO: Determine if we want this 'idPrefix' method (would need to validate both `ECKeyPair` and `Box.KeyPair` types)
|
||||
guard ECKeyPair.isValidHexEncodedPublicKey(candidate: sessionId) else { return nil }
|
||||
guard let targetPrefix: IdPrefix = IdPrefix(rawValue: String(sessionId.prefix(2))) else { return nil }
|
||||
|
||||
self = targetPrefix
|
||||
}
|
||||
|
||||
public func hexEncodedPublicKey(for publicKey: Bytes) -> String {
|
||||
|
||||
return self.rawValue + publicKey.map { String(format: "%02hhx", $0) }.joined()
|
||||
}
|
||||
}
|
57
SessionUtilitiesKit/General/SessionId.swift
Normal file
57
SessionUtilitiesKit/General/SessionId.swift
Normal file
|
@ -0,0 +1,57 @@
|
|||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
import Sodium
|
||||
import Curve25519Kit
|
||||
|
||||
public struct SessionId {
|
||||
public enum Prefix: String, CaseIterable {
|
||||
case standard = "05" // Used for identified users, open groups, etc.
|
||||
case blinded = "15" // Used for participants in open groups with blinding enabled
|
||||
case unblinded = "00" // Used for participants in open groups with blinding disabled
|
||||
|
||||
public init?(from stringValue: String?) {
|
||||
guard let stringValue: String = stringValue else { return nil }
|
||||
|
||||
guard stringValue.count > 2 else {
|
||||
guard let targetPrefix: Prefix = Prefix(rawValue: stringValue) else { return nil }
|
||||
self = targetPrefix
|
||||
return
|
||||
}
|
||||
|
||||
guard ECKeyPair.isValidHexEncodedPublicKey(candidate: stringValue) else { return nil }
|
||||
guard let targetPrefix: Prefix = Prefix(rawValue: String(stringValue.prefix(2))) else { return nil }
|
||||
|
||||
self = targetPrefix
|
||||
}
|
||||
}
|
||||
|
||||
public let prefix: Prefix
|
||||
public let publicKey: String
|
||||
|
||||
public var hexString: String {
|
||||
return prefix.rawValue + publicKey
|
||||
}
|
||||
|
||||
// MARK: - Initialization
|
||||
|
||||
public init?(from idString: String?) {
|
||||
guard let idString: String = idString, idString.count > 2 else { return nil }
|
||||
guard let targetPrefix: Prefix = Prefix(from: idString) else { return nil }
|
||||
|
||||
self.prefix = targetPrefix
|
||||
self.publicKey = idString.substring(from: 2)
|
||||
}
|
||||
|
||||
public init?(_ type: Prefix, publicKey: String) {
|
||||
guard ECKeyPair.isValidHexEncodedPublicKey(candidate: publicKey) else { return nil }
|
||||
|
||||
self.prefix = type
|
||||
self.publicKey = publicKey
|
||||
}
|
||||
|
||||
public init(_ type: Prefix, publicKey: Bytes) {
|
||||
self.prefix = type
|
||||
self.publicKey = publicKey.map { String(format: "%02hhx", $0) }.joined()
|
||||
}
|
||||
}
|
|
@ -3,7 +3,7 @@ public extension String {
|
|||
|
||||
func removingIdPrefixIfNeeded() -> String {
|
||||
var result = self
|
||||
if result.count == 66 && IdPrefix(with: result) != nil { result.removeFirst(2) }
|
||||
if result.count == 66 && SessionId.Prefix(from: result) != nil { result.removeFirst(2) }
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
@ -12,7 +12,7 @@ public extension String {
|
|||
|
||||
@objc func removingIdPrefixIfNeeded() -> NSString {
|
||||
var result = self as String
|
||||
if result.count == 66 && IdPrefix(with: result) != nil { result.removeFirst(2) }
|
||||
if result.count == 66 && SessionId.Prefix(from: result) != nil { result.removeFirst(2) }
|
||||
return result as NSString
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@ public final class Identicon : NSObject {
|
|||
@objc public static func generatePlaceholderIcon(seed: String, text: String, size: CGFloat) -> UIImage {
|
||||
let icon = PlaceholderIcon(seed: seed)
|
||||
var content = text
|
||||
if content.count > 2 && IdPrefix(with: content) != nil {
|
||||
if content.count > 2 && SessionId.Prefix(from: content) != nil {
|
||||
content.removeFirst(2)
|
||||
}
|
||||
let layer = icon.generateLayer(with: size, text: content.substring(to: 1))
|
||||
|
|
Loading…
Reference in a new issue