File upload working, further code cleanup

Got the updated file upload working
Removed the legacy 'room' header
Consolidated a number of types between SOGS, FileServer and general requests
Updated the OnionRequestAPI to deal with a Data payload (rather than encoding it to a string and then back to data)
This commit is contained in:
Morgan Pretty 2022-03-04 13:33:06 +11:00
parent 8ca00ca578
commit 1c474955de
34 changed files with 534 additions and 496 deletions

View File

@ -793,6 +793,8 @@
FD83B9C527CF3E2A005E1583 /* OpenGroupSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD83B9C427CF3E2A005E1583 /* OpenGroupSpec.swift */; };
FD83B9C727CF3F10005E1583 /* CapabilitiesSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD83B9C627CF3F10005E1583 /* CapabilitiesSpec.swift */; };
FD83B9C927D0487A005E1583 /* SendDirectMessageResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD83B9C827D0487A005E1583 /* SendDirectMessageResponse.swift */; };
FD83B9CC27D179BC005E1583 /* FSEndpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD83B9CB27D179BC005E1583 /* FSEndpoint.swift */; };
FD83B9CE27D17A04005E1583 /* Request.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD83B9CD27D17A04005E1583 /* Request.swift */; };
FD859EF227BF6BA200510D0C /* Data+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD859EF127BF6BA200510D0C /* Data+Utilities.swift */; };
FD859EF427C2F49200510D0C /* TestSodium.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD859EF327C2F49200510D0C /* TestSodium.swift */; };
FD859EF627C2F52C00510D0C /* TestSign.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD859EF527C2F52C00510D0C /* TestSign.swift */; };
@ -802,13 +804,12 @@
FD859F0027C4691300510D0C /* MockDataGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD859EFF27C4691300510D0C /* MockDataGenerator.swift */; };
FD88BAD927A7439C00BBC442 /* MessageRequestsCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD88BAD827A7439C00BBC442 /* MessageRequestsCell.swift */; };
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 */; };
FDC4380927B31D4E00C60D73 /* SOGSError.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4380827B31D4E00C60D73 /* SOGSError.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 */; };
FDC4382027B36ADC00C60D73 /* Endpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4381F27B36ADC00C60D73 /* Endpoint.swift */; };
FDC4382027B36ADC00C60D73 /* SOGSEndpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4381F27B36ADC00C60D73 /* SOGSEndpoint.swift */; };
FDC4382627B37F6900C60D73 /* LegacyDeletedMessagesResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4382527B37F6900C60D73 /* LegacyDeletedMessagesResponse.swift */; };
FDC4382827B37FD300C60D73 /* LegacyModeratorsResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4382727B37FD300C60D73 /* LegacyModeratorsResponse.swift */; };
FDC4382A27B3802D00C60D73 /* LegacyRoomsResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4382927B3802D00C60D73 /* LegacyRoomsResponse.swift */; };
@ -832,7 +833,7 @@
FDC4385D27B4C18900C60D73 /* Room.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4385C27B4C18900C60D73 /* Room.swift */; };
FDC4385F27B4C4A200C60D73 /* PinnedMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4385E27B4C4A200C60D73 /* PinnedMessage.swift */; };
FDC4386127B4CDDF00C60D73 /* FileResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4386027B4CDDF00C60D73 /* FileResponse.swift */; };
FDC4386327B4D94E00C60D73 /* OGMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4386227B4D94E00C60D73 /* OGMessage.swift */; };
FDC4386327B4D94E00C60D73 /* SOGSMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4386227B4D94E00C60D73 /* SOGSMessage.swift */; };
FDC4386527B4DE7600C60D73 /* RoomPollInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4386427B4DE7600C60D73 /* RoomPollInfo.swift */; };
FDC4386727B4E10E00C60D73 /* Capabilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4386627B4E10E00C60D73 /* Capabilities.swift */; };
FDC4386927B4E6B800C60D73 /* String+Utlities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4386827B4E6B700C60D73 /* String+Utlities.swift */; };
@ -1947,6 +1948,8 @@
FD83B9C427CF3E2A005E1583 /* OpenGroupSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenGroupSpec.swift; sourceTree = "<group>"; };
FD83B9C627CF3F10005E1583 /* CapabilitiesSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CapabilitiesSpec.swift; sourceTree = "<group>"; };
FD83B9C827D0487A005E1583 /* SendDirectMessageResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendDirectMessageResponse.swift; sourceTree = "<group>"; };
FD83B9CB27D179BC005E1583 /* FSEndpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FSEndpoint.swift; sourceTree = "<group>"; };
FD83B9CD27D17A04005E1583 /* Request.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Request.swift; sourceTree = "<group>"; };
FD859EEF27BF207700510D0C /* SessionProtos.proto */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.protobuf; path = SessionProtos.proto; sourceTree = "<group>"; };
FD859EF027BF207C00510D0C /* WebSocketResources.proto */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.protobuf; path = WebSocketResources.proto; sourceTree = "<group>"; };
FD859EF127BF6BA200510D0C /* Data+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Data+Utilities.swift"; sourceTree = "<group>"; };
@ -1959,13 +1962,12 @@
FD88BAD827A7439C00BBC442 /* MessageRequestsCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageRequestsCell.swift; sourceTree = "<group>"; };
FD88BADA27A750F200BBC442 /* MessageRequestsMigration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageRequestsMigration.swift; sourceTree = "<group>"; };
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>"; };
FDC4380827B31D4E00C60D73 /* SOGSError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SOGSError.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>"; };
FDC4381F27B36ADC00C60D73 /* Endpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Endpoint.swift; sourceTree = "<group>"; };
FDC4381F27B36ADC00C60D73 /* SOGSEndpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SOGSEndpoint.swift; sourceTree = "<group>"; };
FDC4382527B37F6900C60D73 /* LegacyDeletedMessagesResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegacyDeletedMessagesResponse.swift; sourceTree = "<group>"; };
FDC4382727B37FD300C60D73 /* LegacyModeratorsResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegacyModeratorsResponse.swift; sourceTree = "<group>"; };
FDC4382927B3802D00C60D73 /* LegacyRoomsResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegacyRoomsResponse.swift; sourceTree = "<group>"; };
@ -1989,7 +1991,7 @@
FDC4385C27B4C18900C60D73 /* Room.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Room.swift; sourceTree = "<group>"; };
FDC4385E27B4C4A200C60D73 /* PinnedMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PinnedMessage.swift; sourceTree = "<group>"; };
FDC4386027B4CDDF00C60D73 /* FileResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileResponse.swift; sourceTree = "<group>"; };
FDC4386227B4D94E00C60D73 /* OGMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OGMessage.swift; sourceTree = "<group>"; };
FDC4386227B4D94E00C60D73 /* SOGSMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SOGSMessage.swift; sourceTree = "<group>"; };
FDC4386427B4DE7600C60D73 /* RoomPollInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomPollInfo.swift; sourceTree = "<group>"; };
FDC4386627B4E10E00C60D73 /* Capabilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Capabilities.swift; sourceTree = "<group>"; };
FDC4386827B4E6B700C60D73 /* String+Utlities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Utlities.swift"; sourceTree = "<group>"; };
@ -3420,6 +3422,7 @@
isa = PBXGroup;
children = (
FDC4383227B385B200C60D73 /* Models */,
FD83B9CA27D179AF005E1583 /* Types */,
B87EF17026367CF800124B3C /* FileServerAPIV2.swift */,
);
path = "File Server";
@ -3905,6 +3908,14 @@
path = Models;
sourceTree = "<group>";
};
FD83B9CA27D179AF005E1583 /* Types */ = {
isa = PBXGroup;
children = (
FD83B9CB27D179BC005E1583 /* FSEndpoint.swift */,
);
path = Types;
sourceTree = "<group>";
};
FD88BAD727A7438E00BBC442 /* Views */ = {
isa = PBXGroup;
children = (
@ -3916,9 +3927,8 @@
FDC4380727B31D3A00C60D73 /* Types */ = {
isa = PBXGroup;
children = (
FDC4380A27B31D7E00C60D73 /* Request.swift */,
FDC4381F27B36ADC00C60D73 /* Endpoint.swift */,
FDC4380827B31D4E00C60D73 /* Error.swift */,
FDC4381F27B36ADC00C60D73 /* SOGSEndpoint.swift */,
FDC4380827B31D4E00C60D73 /* SOGSError.swift */,
FDC4381627B32EC700C60D73 /* Personalization.swift */,
FDC4381427B329CE00C60D73 /* NonceGenerator.swift */,
FDC438C027BB4E6800C60D73 /* Dependencies.swift */,
@ -3940,7 +3950,7 @@
FDC4386027B4CDDF00C60D73 /* FileResponse.swift */,
FDC4387727B5C35400C60D73 /* SendMessageRequest.swift */,
FDC438CA27BB7DB100C60D73 /* UpdateMessageRequest.swift */,
FDC4386227B4D94E00C60D73 /* OGMessage.swift */,
FDC4386227B4D94E00C60D73 /* SOGSMessage.swift */,
FDC438C627BB6DF000C60D73 /* DirectMessage.swift */,
FDC438C827BB706500C60D73 /* SendDirectMessageRequest.swift */,
FD83B9C827D0487A005E1583 /* SendDirectMessageResponse.swift */,
@ -3988,6 +3998,7 @@
FDC4385527B484AE00C60D73 /* Models */,
FDC4384E27B4804F00C60D73 /* Header.swift */,
FDC4385027B4807400C60D73 /* QueryParam.swift */,
FD83B9CD27D17A04005E1583 /* Request.swift */,
);
path = "Common Networking";
sourceTree = "<group>";
@ -5287,7 +5298,6 @@
C3D9E3BF25676AD70040E4F3 /* TSAttachmentStream.m in Sources */,
C3C2A7562553A3AB00C340D1 /* VisibleMessage+Quote.swift in Sources */,
B8B32021258B1A650020074B /* Contact.swift in Sources */,
FDC4380B27B31D7E00C60D73 /* Request.swift in Sources */,
FDC4384027B4746D00C60D73 /* LegacyGetInfoResponse.swift in Sources */,
C32C5C89256DD0D2003C73A2 /* Storage+Jobs.swift in Sources */,
C300A5FC2554B0A000555489 /* MessageReceiver.swift in Sources */,
@ -5337,11 +5347,12 @@
B8EB20EE2640F28000773E52 /* VisibleMessage+OpenGroupInvitation.swift in Sources */,
FDC4381C27B354AC00C60D73 /* LegacyPublicKeyBody.swift in Sources */,
FDC4382F27B383AF00C60D73 /* UnregisterResponse.swift in Sources */,
FDC4386327B4D94E00C60D73 /* OGMessage.swift in Sources */,
FDC4386327B4D94E00C60D73 /* SOGSMessage.swift in Sources */,
C3C2A7712553A41E00C340D1 /* ControlMessage.swift in Sources */,
C32C5D19256DD493003C73A2 /* OWSLinkPreview.swift in Sources */,
C32C5CF0256DD3E4003C73A2 /* Storage+Shared.swift in Sources */,
C300A5BD2554B00D00555489 /* ReadReceipt.swift in Sources */,
FD83B9CC27D179BC005E1583 /* FSEndpoint.swift in Sources */,
C32C5AB5256DBE8F003C73A2 /* TSOutgoingMessage+Conversion.swift in Sources */,
FDC4385927B484E800C60D73 /* FileUploadBody.swift in Sources */,
C32C5E5B256DDF45003C73A2 /* OWSStorage.m in Sources */,
@ -5377,8 +5388,8 @@
B8D0A25925E367AC00C1835E /* Notification+MessageReceiver.swift in Sources */,
FDC4387227B5BB3B00C60D73 /* FileUploadResponse.swift in Sources */,
C32C599E256DB02B003C73A2 /* TypingIndicators.swift in Sources */,
FDC4380927B31D4E00C60D73 /* Error.swift in Sources */,
FDC4382027B36ADC00C60D73 /* Endpoint.swift in Sources */,
FDC4380927B31D4E00C60D73 /* SOGSError.swift in Sources */,
FDC4382027B36ADC00C60D73 /* SOGSEndpoint.swift in Sources */,
C3DB66CC260AF1F3001EFC55 /* OpenGroupAPI+ObjC.swift in Sources */,
C32C5BEF256DC8EE003C73A2 /* OWSDisappearingMessagesJob.m in Sources */,
C34A977425A3E34A00852C71 /* ClosedGroupControlMessage.swift in Sources */,
@ -5403,6 +5414,7 @@
C300A5E72554B07300555489 /* ExpirationTimerUpdate.swift in Sources */,
B88A1AC725C90A4700E6D421 /* TypingIndicatorInteraction.swift in Sources */,
C3D9E3C025676AD70040E4F3 /* TSAttachment.m in Sources */,
FD83B9CE27D17A04005E1583 /* Request.swift in Sources */,
FDC4384827B47F4D00C60D73 /* LegacyRoomInfo.swift in Sources */,
C32C598A256D0664003C73A2 /* SNProtoEnvelope+Conversion.swift in Sources */,
FDC438CB27BB7DB100C60D73 /* UpdateMessageRequest.swift in Sources */,

View File

@ -568,7 +568,7 @@ extension ConversationVC : InputViewDelegate, MessageCellDelegate, ContextMenuAc
let attachment = mediaView.attachment
if let pointer = attachment as? TSAttachmentPointer {
if pointer.state == .failed {
// TODO: Tapped a failed incoming attachment
// TODO: Tapped a failed incoming attachment (Note: This is generally a permanent failure - see `AttachmentDownloadJob`)
}
}
guard let stream = attachment as? TSAttachmentStream else { return }

View File

@ -182,7 +182,7 @@ extension OpenGroupSuggestionGrid {
label.text = room.name
if let imageId: Int64 = room.imageId {
if let imageId: UInt64 = room.imageId {
let promise = OpenGroupManager.roomImage(imageId, for: room.token, on: OpenGroupAPI.defaultServer)
imageView.image = given(promise.value) { UIImage(data: $0)! }
imageView.isHidden = (imageView.image == nil)

View File

@ -1,4 +1,5 @@
import UIKit
import SessionUtilitiesKit
final class SettingsVC : BaseVC, AvatarViewHelperDelegate {
private var profilePictureToBeUploaded: UIImage?
@ -382,7 +383,7 @@ final class SettingsVC : BaseVC, AvatarViewHelperDelegate {
DispatchQueue.main.async {
modalActivityIndicator.dismiss {
var isMaxFileSizeExceeded = false
if let error = error as? FileServerAPIV2.Error {
if let error = error as? HTTP.Error {
isMaxFileSizeExceeded = (error == .maxFileSizeExceeded)
}
let title = isMaxFileSizeExceeded ? "Maximum File Size Exceeded" : "Couldn't Update Profile"

View File

@ -37,9 +37,7 @@ extension FileDownloadResponse {
let base64EncodedData: String = try container.decode(String.self, forKey: .base64EncodedData)
guard let data = Data(base64Encoded: base64EncodedData) else {
throw FileServerAPIV2.Error.parsingFailed
}
guard let data = Data(base64Encoded: base64EncodedData) else { throw HTTP.Error.parsingFailed }
self = FileDownloadResponse(
fileName: try container.decode(String.self, forKey: .fileName),

View File

@ -24,9 +24,7 @@ extension LegacyFileDownloadResponse {
let base64EncodedData: String = try container.decode(String.self, forKey: .base64EncodedData)
guard let data = Data(base64Encoded: base64EncodedData) else {
throw FileServerAPIV2.Error.parsingFailed
}
guard let data = Data(base64Encoded: base64EncodedData) else { throw HTTP.Error.parsingFailed }
self = LegacyFileDownloadResponse(
data: data

View File

@ -8,4 +8,5 @@ enum QueryParam: String {
case required = "required"
case limit // For messages - number between 1 and 256 (default is 100)
case platform // For file server session version check
}

View File

@ -0,0 +1,106 @@
import Foundation
import SessionUtilitiesKit
// MARK: - Convenience Types
struct Empty: Codable {}
typealias NoBody = Empty
typealias NoResponse = Empty
protocol EndpointType: Hashable {
var path: String { get }
}
// MARK: - Request
struct Request<T: Encodable, Endpoint: EndpointType> {
let method: HTTP.Verb
let server: String
let endpoint: Endpoint
let queryParameters: [QueryParam: String]
let headers: [Header: String]
/// This is the body value sent during the request
///
/// **Warning:** The `bodyData` value should be used to when making the actual request instead of this as there
/// is custom handling for certain data types
let body: T?
let isAuthRequired: Bool
/// Always `true` under normal circumstances. You might want to disable
/// this when running over Lokinet.
let useOnionRouting: Bool
// MARK: - Initialization
init(
method: HTTP.Verb = .get,
server: String,
endpoint: Endpoint,
queryParameters: [QueryParam: String] = [:],
headers: [Header: String] = [:],
body: T? = nil,
isAuthRequired: Bool = true,
useOnionRouting: Bool = true
) {
self.method = method
self.server = server
self.endpoint = endpoint
self.queryParameters = queryParameters
self.headers = headers
self.body = body
self.isAuthRequired = isAuthRequired
self.useOnionRouting = useOnionRouting
}
// MARK: - Internal Methods
private var url: URL? {
return URL(string: "\(server)\(urlPathAndParamsString)")
}
private func bodyData() throws -> Data? {
// Note: Need to differentiate between JSON, b64 string and bytes body values to ensure they are
// encoded correctly so the server knows how to handle them
switch body {
case let bodyString as String:
// The only acceptable string body is a base64 encoded one
guard let encodedData: Data = Data(base64Encoded: bodyString) else { throw HTTP.Error.parsingFailed }
return encodedData
case let bodyBytes as [UInt8]:
return Data(bodyBytes)
default:
// Having no body is fine so just return nil
guard let body: T = body else { return nil }
return try JSONEncoder().encode(body)
}
}
// MARK: - Request Generation
var urlPathAndParamsString: String {
return [
"/\(endpoint.path)",
queryParameters
.map { key, value in "\(key.rawValue)=\(value)" }
.joined(separator: "&")
]
.compactMap { $0 }
.filter { !$0.isEmpty }
.joined(separator: "?")
}
func generateUrlRequest() throws -> URLRequest {
guard let url: URL = url else { throw HTTP.Error.invalidURL }
var urlRequest: URLRequest = URLRequest(url: url)
urlRequest.httpMethod = method.rawValue
urlRequest.allHTTPHeaderFields = headers.toHTTPHeaders()
urlRequest.httpBody = try bodyData()
return urlRequest
}
}

View File

@ -1,3 +1,4 @@
import YapDatabase
extension Storage {
@ -96,10 +97,14 @@ extension Storage {
public func getMessageSendJob(for messageSendJobID: String) -> MessageSendJob? {
var result: MessageSendJob?
Storage.read { transaction in
result = transaction.object(forKey: messageSendJobID, inCollection: MessageSendJob.collection) as? MessageSendJob
result = self.getMessageSendJob(for: messageSendJobID, using: transaction)
}
return result
}
public func getMessageSendJob(for messageSendJobID: String, using transaction: Any) -> MessageSendJob? {
return (transaction as! YapDatabaseReadTransaction).object(forKey: messageSendJobID, inCollection: MessageSendJob.collection) as? MessageSendJob
}
public func resumeMessageSendJobIfNeeded(_ messageSendJobID: String) {
guard let job = getMessageSendJob(for: messageSendJobID) else { return }

View File

@ -4,7 +4,8 @@ import SessionSnodeKit
@objc(SNFileServerAPIV2)
public final class FileServerAPIV2 : NSObject {
// MARK: Settings
// MARK: - Settings
@objc public static let oldServer = "http://88.99.175.227"
public static let oldServerPublicKey = "7cb31905b55cd5580c686911debf672577b3fb0bff81df4ce2d5c4cb3a7aaa69"
@objc public static let server = "http://filev2.getsession.org"
@ -18,92 +19,12 @@ public final class FileServerAPIV2 : NSObject {
/// possible after proof of work has been calculated and the onion request encryption has happened, which takes several seconds.
public static let fileSizeORMultiplier: Double = 2
// MARK: Initialization
// MARK: - Initialization
private override init() { }
// MARK: Error
public enum Error: LocalizedError {
case parsingFailed
case invalidURL
case maxFileSizeExceeded
public var errorDescription: String? {
switch self {
case .parsingFailed: return "Invalid response."
case .invalidURL: return "Invalid URL."
case .maxFileSizeExceeded: return "Maximum file size exceeded."
}
}
}
// MARK: - File Storage
// MARK: Request
private struct Request {
let verb: HTTP.Verb
let endpoint: String
let queryParameters: [QueryParam: String]
let body: Data?
let headers: [Header: String]
/// Always `true` under normal circumstances. You might want to disable
/// this when running over Lokinet.
let useOnionRouting: Bool
init(verb: HTTP.Verb, endpoint: String, queryParameters: [QueryParam: String] = [:], body: Data? = nil,
headers: [Header: String] = [:], useOnionRouting: Bool = true) {
self.verb = verb
self.endpoint = endpoint
self.queryParameters = queryParameters
self.body = body
self.headers = headers
self.useOnionRouting = useOnionRouting
}
}
// MARK: - Convenience
private static func send(_ request: Request, useOldServer: Bool) -> Promise<Data> {
let server = useOldServer ? oldServer : server
let serverPublicKey = useOldServer ? oldServerPublicKey : serverPublicKey
var urlRequest: URLRequest
// TODO: Combine this 'Request' with the the pattern in OpenGroupServerV2?
switch request.verb {
case .get:
var rawURL = "\(server)/\(request.endpoint)"
if !request.queryParameters.isEmpty {
let queryString = request.queryParameters.map { key, value in "\(key)=\(value)" }.joined(separator: "&")
rawURL += "?\(queryString)"
}
guard let url = URL(string: rawURL) else { return Promise(error: Error.invalidURL) }
urlRequest = URLRequest(url: url)
case .post, .put, .delete:
let rawURL = "\(server)/\(request.endpoint)"
guard let url = URL(string: rawURL) else { return Promise(error: Error.invalidURL) }
urlRequest = URLRequest(url: url)
urlRequest.httpMethod = request.verb.rawValue
urlRequest.httpBody = request.body
}
urlRequest.allHTTPHeaderFields = request.headers.toHTTPHeaders()
guard request.useOnionRouting else {
preconditionFailure("It's currently not allowed to send non onion routed requests.")
}
// TODO: Upgrade this to use the V4 onion requests once supported.
return OnionRequestAPI.sendOnionRequest(urlRequest, to: server, using: .v3, with: serverPublicKey)
.map2 { _, response in
guard let response: Data = response else { throw Error.parsingFailed }
return response
}
}
// MARK: File Storage
@objc(upload:)
public static func objc_upload(file: Data) -> AnyPromise {
return AnyPromise.from(upload(file).map { String($0) })
@ -112,41 +33,72 @@ public final class FileServerAPIV2 : NSObject {
public static func upload(_ file: Data) -> Promise<UInt64> {
let requestBody: FileUploadBody = FileUploadBody(file: file.base64EncodedString())
guard let body: Data = try? JSONEncoder().encode(requestBody) else {
return Promise(error: HTTP.Error.invalidJSON)
}
let request = Request(
method: .post,
server: server,
endpoint: Endpoint.files,
body: requestBody
)
let request = Request(verb: .post, endpoint: "files", body: body)
return send(request, useOldServer: false).map(on: DispatchQueue.global(qos: .userInitiated)) { data in
let response: LegacyFileUploadResponse = try data.decoded(as: LegacyFileUploadResponse.self, customError: Error.parsingFailed)
return response.fileId
}
return send(request, serverPublicKey: serverPublicKey)
.decoded(as: LegacyFileUploadResponse.self, on: .global(qos: .userInitiated), error: HTTP.Error.parsingFailed)
.map { response in response.fileId }
}
@objc(download:useOldServer:)
public static func objc_download(file: String, useOldServer: Bool) -> AnyPromise {
guard let id = UInt64(file) else { return AnyPromise.from(Promise<Data>(error: Error.invalidURL)) }
guard let id = UInt64(file) else { return AnyPromise.from(Promise<Data>(error: HTTP.Error.invalidURL)) }
return AnyPromise.from(download(id, useOldServer: useOldServer))
}
public static func download(_ file: UInt64, useOldServer: Bool) -> Promise<Data> {
let request = Request(verb: .get, endpoint: "files/\(file)")
let serverPublicKey: String = (useOldServer ? oldServerPublicKey : serverPublicKey)
let request = Request<NoBody, Endpoint>(
server: (useOldServer ? oldServer : server),
endpoint: .file(fileId: file)
)
return send(request, useOldServer: useOldServer).map(on: DispatchQueue.global(qos: .userInitiated)) { data in
let response: LegacyFileDownloadResponse = try data.decoded(as: LegacyFileDownloadResponse.self, customError: Error.parsingFailed)
return response.data
}
return send(request, serverPublicKey: serverPublicKey)
.decoded(as: LegacyFileDownloadResponse.self, on: .global(qos: .userInitiated), error: HTTP.Error.parsingFailed)
.map { response in response.data }
}
public static func getVersion(_ platform: String) -> Promise<String> {
let request = Request(verb: .get, endpoint: "session_version?platform=\(platform)")
let request = Request<NoBody, Endpoint>(
server: server,
endpoint: .sessionVersion,
queryParameters: [
.platform: platform
]
)
return send(request, useOldServer: false).map(on: DispatchQueue.global(qos: .userInitiated)) { data in
let response: VersionResponse = try data.decoded(as: VersionResponse.self, customError: Error.parsingFailed)
return response.version
return send(request, serverPublicKey: serverPublicKey)
.decoded(as: VersionResponse.self, on: .global(qos: .userInitiated), error: HTTP.Error.parsingFailed)
.map { response in response.version }
}
// MARK: - Convenience
private static func send<T: Encodable>(_ request: Request<T, Endpoint>, serverPublicKey: String) -> Promise<Data> {
guard request.useOnionRouting else {
preconditionFailure("It's currently not allowed to send non onion routed requests.")
}
let urlRequest: URLRequest
do {
urlRequest = try request.generateUrlRequest()
}
catch {
return Promise(error: error)
}
// TODO: Rename file to be 'FileServerAPI' (drop the 'V2')
return OnionRequestAPI.sendOnionRequest(urlRequest, to: request.server, with: serverPublicKey)
.map2 { _, response in
guard let response: Data = response else { throw HTTP.Error.parsingFailed }
return response
}
}
}

View File

@ -0,0 +1,19 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
extension FileServerAPIV2 {
public enum Endpoint: EndpointType {
case files
case file(fileId: UInt64)
case sessionVersion
var path: String {
switch self {
case .files: return "files"
case .file(let fileId): return "files/\(fileId)"
case .sessionVersion: return "session_version"
}
}
}
}

View File

@ -98,7 +98,7 @@ public final class AttachmentDownloadJob : NSObject, Job, NSCoding { // NSObject
}
}
if let tsMessage = TSMessage.fetch(uniqueId: tsMessageID), let openGroup = storage.getOpenGroup(for: tsMessage.uniqueThreadId) {
guard let fileAsString = pointer.downloadURL.split(separator: "/").last, let file = UInt64(fileAsString) else {
guard let fileAsString = pointer.downloadURL.split(separator: "/").last, let fileId = UInt64(fileAsString) else {
return handleFailure(Error.invalidURL)
}
// TODO: Upgrade this to use the non-legacy version

View File

@ -66,25 +66,33 @@ public final class AttachmentUploadJob : NSObject, Job, NSCoding { // NSObject/N
guard let stream = TSAttachment.fetch(uniqueId: attachmentID) as? TSAttachmentStream else {
return handleFailure(error: Error.noAttachment)
}
guard !stream.isUploaded else { return handleSuccess() } // Should never occur
guard !stream.isUploaded else { return handleSuccess(stream.serverId) } // Should never occur
let storage = SNMessagingKitConfiguration.shared.storage
if let openGroup = storage.getOpenGroup(for: threadID) {
AttachmentUploadJob.upload(
stream,
using: { data in
// TODO: Upgrade this to use the non-legacy version
return OpenGroupAPI.legacyUpload(data, to: openGroup.room, on: openGroup.server)
OpenGroupAPI.uploadFile(data.bytes, to: openGroup.room, on: openGroup.server)
.map { _, response -> UInt64 in response.id }
},
encrypt: false,
onSuccess: handleSuccess,
onSuccess: { [weak self] fileId in self?.handleSuccess(fileId) },
onFailure: handleFailure
)
}
else {
AttachmentUploadJob.upload(
stream,
using: FileServerAPIV2.upload,
encrypt: true,
onSuccess: { [weak self] fileId in self?.handleSuccess(fileId) },
onFailure: handleFailure
)
} else {
AttachmentUploadJob.upload(stream, using: FileServerAPIV2.upload, encrypt: true, onSuccess: handleSuccess, onFailure: handleFailure)
}
}
public static func upload(_ stream: TSAttachmentStream, using upload: (Data) -> Promise<UInt64>, encrypt: Bool, onSuccess: (() -> Void)?, onFailure: ((Swift.Error) -> Void)?) {
public static func upload(_ stream: TSAttachmentStream, using upload: (Data) -> Promise<UInt64>, encrypt: Bool, onSuccess: ((UInt64) -> Void)?, onFailure: ((Swift.Error) -> Void)?) {
// Get the attachment
guard var data = try? stream.readDataFromFile() else {
SNLog("Couldn't read attachment from disk.")
@ -105,37 +113,74 @@ public final class AttachmentUploadJob : NSObject, Job, NSCoding { // NSObject/N
// Check the file size
SNLog("File size: \(data.count) bytes.")
if Double(data.count) > Double(FileServerAPIV2.maxFileSize) / FileServerAPIV2.fileSizeORMultiplier {
onFailure?(FileServerAPIV2.Error.maxFileSizeExceeded); return
onFailure?(HTTP.Error.maxFileSizeExceeded)
return
}
// Send the request
stream.isUploaded = false
stream.save()
upload(data).done(on: DispatchQueue.global(qos: .userInitiated)) { fileID in
let downloadURL = "\(FileServerAPIV2.server)/files/\(fileID)"
stream.serverId = fileID
upload(data).done(on: DispatchQueue.global(qos: .userInitiated)) { fileId in
let downloadURL = "\(FileServerAPIV2.server)/files/\(fileId)"
stream.serverId = fileId
stream.isUploaded = true
stream.downloadURL = downloadURL
stream.save()
onSuccess?()
onSuccess?(fileId)
}.catch { error in
onFailure?(error)
}
}
private func handleSuccess() {
private func handleSuccess(_ fileId: UInt64) {
SNLog("Attachment uploaded successfully.")
delegate?.handleJobSucceeded(self)
SNMessagingKitConfiguration.shared.storage.resumeMessageSendJobIfNeeded(messageSendJobID)
Storage.shared.write(with: { transaction in
var message: TSMessage?
let transaction = transaction as! YapDatabaseReadWriteTransaction
TSDatabaseSecondaryIndexes.enumerateMessages(withTimestamp: self.message.sentTimestamp!, with: { _, key, _ in
message = TSMessage.fetch(uniqueId: key, transaction: transaction)
}, using: transaction)
if let message = message {
MessageInvalidator.invalidate(message, with: transaction)
let messageSendJobId: String = messageSendJobID
Storage.shared.write(
with: { transaction in
// Get the existing MessageSendJob and replace it with one that has it's destination updated
// to include the returned fileId
if let oldJob: MessageSendJob = SNMessagingKitConfiguration.shared.storage.getMessageSendJob(for: messageSendJobId, using: transaction) {
switch oldJob.destination {
case .openGroup(let roomToken, let server, let whisperTo, let whisperMods, let oldFileIds):
let job: MessageSendJob = MessageSendJob(
message: oldJob.message,
destination: .openGroup(
roomToken: roomToken,
server: server,
whisperTo: whisperTo,
whisperMods: whisperMods,
fileIds: (oldFileIds ?? []) + [fileId]
)
)
job.id = oldJob.id // Use the existing id so it gets overwritten
job.delegate = oldJob.delegate
job.failureCount = oldJob.failureCount
// This method just writes the job directly and doesn't generate a new id (as we want)
SNMessagingKitConfiguration.shared.storage.persist(job, using: transaction)
default: break
}
}
},
completion: {
SNMessagingKitConfiguration.shared.storage.resumeMessageSendJobIfNeeded(messageSendJobId)
Storage.shared.write(with: { transaction in
var message: TSMessage?
let transaction = transaction as! YapDatabaseReadWriteTransaction
TSDatabaseSecondaryIndexes.enumerateMessages(withTimestamp: self.message.sentTimestamp!, with: { _, key, _ in
message = TSMessage.fetch(uniqueId: key, transaction: transaction)
}, using: transaction)
if let message = message {
MessageInvalidator.invalidate(message, with: transaction)
}
}, completion: { })
}
}, completion: { })
)
}
private func handlePermanentFailure(error: Swift.Error) {

View File

@ -6,7 +6,7 @@ public final class MessageSendJob : NSObject, Job, NSCoding { // NSObject/NSCodi
public let message: Message
public let destination: Message.Destination
public var delegate: JobDelegate?
public var id: String?
public var id: String? // This should only get set in either `JobQueue` or `AttachmentUploadJob`
public var failureCount: UInt = 0
// MARK: - Settings
@ -100,7 +100,7 @@ public final class MessageSendJob : NSObject, Job, NSCoding { // NSObject/NSCodi
.replacingOccurrences(of: "]", with: "")
.split(separator: "|")
.map { String($0) }
let fileIds: [Int64]? = (fileIdStrings.isEmpty ? nil : fileIdStrings.compactMap { Int64($0) })
let fileIds: [UInt64]? = (fileIdStrings.isEmpty ? nil : fileIdStrings.compactMap { UInt64($0) })
destination = .openGroup(
roomToken: roomToken,

View File

@ -10,11 +10,11 @@ public extension Message {
server: String,
whisperTo: String? = nil,
whisperMods: Bool = false,
fileIds: [Int64]? = nil // TODO: Handle 'fileIds'
fileIds: [UInt64]? = nil
)
case openGroupInbox(server: String, openGroupPublicKey: String, blindedPublicKey: String)
static func from(_ thread: TSThread) -> Message.Destination {
static func from(_ thread: TSThread, fileIds: [UInt64]? = nil) -> 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 {
@ -40,7 +40,7 @@ public extension Message {
if let thread = thread as? TSGroupThread, thread.isOpenGroup {
let openGroup: OpenGroup = Storage.shared.getOpenGroup(for: thread.uniqueId!)!
return .openGroup(roomToken: openGroup.room, server: openGroup.server)
return .openGroup(roomToken: openGroup.room, server: openGroup.server, fileIds: fileIds)
}
preconditionFailure("TODO: Handle legacy closed groups.")

View File

@ -28,7 +28,7 @@ extension OpenGroupAPI {
private let b64: String?
private let bytes: [UInt8]?
init<T: Encodable>(request: Request<T>) {
init<T: Encodable>(request: Request<T, Endpoint>) {
self.method = request.method
self.path = request.urlPathAndParamsString
self.headers = (request.headers.isEmpty ? nil : request.headers.toHTTPHeaders())
@ -86,17 +86,17 @@ extension OpenGroupAPI {
// MARK: - BatchRequestInfo<T, R>
struct BatchRequestInfo<T: Encodable>: BatchRequestInfoType {
let request: Request<T>
let request: Request<T, Endpoint>
let responseType: Codable.Type
var endpoint: Endpoint { request.endpoint }
init<R: Codable>(request: Request<T>, responseType: R.Type) {
init<R: Codable>(request: Request<T, Endpoint>, responseType: R.Type) {
self.request = request
self.responseType = BatchSubResponse<R>.self
}
init(request: Request<T>) {
init(request: Request<T, Endpoint>) {
self.init(
request: request,
responseType: NoResponse.self
@ -124,7 +124,7 @@ extension 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 && !(T.self is ExpressibleByNilLiteral.Type))
failedToParseBody: (body == nil && T.self != NoResponse.self && !(T.self is ExpressibleByNilLiteral.Type))
)
}
}
@ -143,31 +143,31 @@ protocol BatchRequestInfoType {
// MARK: - Convenience
public extension Decodable {
static func decoded(from data: Data, customError: Error, using dependencies: OpenGroupAPI.Dependencies = OpenGroupAPI.Dependencies()) throws -> Self {
return try data.decoded(as: Self.self, customError: customError, using: dependencies)
static func decoded(from data: Data, using dependencies: OpenGroupAPI.Dependencies = OpenGroupAPI.Dependencies()) throws -> Self {
return try data.decoded(as: Self.self, using: dependencies)
}
}
extension Promise where T == (OnionRequestResponseInfoType, Data?) {
func decoded(as types: OpenGroupAPI.BatchResponseTypes, on queue: DispatchQueue? = nil, error: Error, using dependencies: OpenGroupAPI.Dependencies = OpenGroupAPI.Dependencies()) -> Promise<OpenGroupAPI.BatchResponse> {
func decoded(as types: OpenGroupAPI.BatchResponseTypes, on queue: DispatchQueue? = nil, using dependencies: OpenGroupAPI.Dependencies = OpenGroupAPI.Dependencies()) -> Promise<OpenGroupAPI.BatchResponse> {
self.map(on: queue) { responseInfo, maybeData -> OpenGroupAPI.BatchResponse in
// Need to split the data into an array of data so each item can be Decoded correctly
guard let data: Data = maybeData else { throw OpenGroupAPI.Error.parsingFailed }
guard let data: Data = maybeData else { throw HTTP.Error.parsingFailed }
guard let jsonObject: Any = try? JSONSerialization.jsonObject(with: data, options: [.fragmentsAllowed]) else {
throw OpenGroupAPI.Error.parsingFailed
throw HTTP.Error.parsingFailed
}
guard let anyArray: [Any] = jsonObject as? [Any] else { throw OpenGroupAPI.Error.parsingFailed }
guard let anyArray: [Any] = jsonObject as? [Any] else { throw HTTP.Error.parsingFailed }
let dataArray: [Data] = anyArray.compactMap { try? JSONSerialization.data(withJSONObject: $0) }
guard dataArray.count == types.count else { throw OpenGroupAPI.Error.parsingFailed }
guard dataArray.count == types.count else { throw HTTP.Error.parsingFailed }
do {
return try zip(dataArray, types)
.map { data, type in try type.decoded(from: data, customError: error, using: dependencies) }
.map { data, type in try type.decoded(from: data, using: dependencies) }
.map { data in (responseInfo, data) }
}
catch _ {
throw error
catch {
throw HTTP.Error.parsingFailed
}
}
}

View File

@ -28,7 +28,7 @@ extension OpenGroupAPI.LegacyAuthTokenResponse.Challenge {
let base64EncodedEphemeralPublicKey: String = try container.decode(String.self, forKey: .ephemeralPublicKey)
guard let ciphertext = Data(base64Encoded: base64EncodedCiphertext), let ephemeralPublicKey = Data(base64Encoded: base64EncodedEphemeralPublicKey) else {
throw OpenGroupAPI.Error.parsingFailed
throw HTTP.Error.parsingFailed
}
self = OpenGroupAPI.LegacyAuthTokenResponse.Challenge(

View File

@ -49,7 +49,7 @@ extension LegacyOpenGroupMessageV2 {
// Validate the message signature
guard let data = Data(base64Encoded: base64EncodedData), let signature = Data(base64Encoded: base64EncodedSignature) else {
throw OpenGroupAPI.Error.parsingFailed
throw HTTP.Error.parsingFailed
}
let publicKey = Data(hex: sender.removingIdPrefixIfNeeded())
@ -57,7 +57,7 @@ extension LegacyOpenGroupMessageV2 {
guard isValid else {
SNLog("Ignoring message with invalid signature.")
throw OpenGroupAPI.Error.parsingFailed
throw HTTP.Error.parsingFailed
}
self = LegacyOpenGroupMessageV2(

View File

@ -70,7 +70,7 @@ extension OpenGroupAPI {
/// File ID of an uploaded file containing the room's image
///
/// Omitted if there is no image
public let imageId: Int64?
public let imageId: UInt64?
/// Array of pinned message information (omitted entirely if there are no pinned messages)
public let pinnedMessages: [PinnedMessage]?
@ -160,7 +160,7 @@ extension OpenGroupAPI.Room {
activeUsers: try container.decode(Int64.self, forKey: .activeUsers),
activeUsersCutoff: try container.decode(Int64.self, forKey: .activeUsersCutoff),
imageId: try? container.decode(Int64.self, forKey: .imageId),
imageId: try? container.decode(UInt64.self, forKey: .imageId),
pinnedMessages: try? container.decode([OpenGroupAPI.PinnedMessage].self, forKey: .pinnedMessages),
admin: ((try? container.decode(Bool.self, forKey: .admin)) ?? false),

View File

@ -45,10 +45,10 @@ extension OpenGroupAPI.Message {
// If we have data and a signature (ie. the message isn't a deletion) then validate the signature
if let base64EncodedData: String = maybeBase64EncodedData, let base64EncodedSignature: String = maybeBase64EncodedSignature {
guard let sender: String = maybeSender, let data = Data(base64Encoded: base64EncodedData), let signature = Data(base64Encoded: base64EncodedSignature) else {
throw OpenGroupAPI.Error.parsingFailed
throw HTTP.Error.parsingFailed
}
guard let dependencies: OpenGroupAPI.Dependencies = decoder.userInfo[OpenGroupAPI.Dependencies.userInfoKey] as? OpenGroupAPI.Dependencies else {
throw OpenGroupAPI.Error.parsingFailed
throw HTTP.Error.parsingFailed
}
// Verify the signature based on the SessionId.Prefix type
@ -58,18 +58,18 @@ extension OpenGroupAPI.Message {
case .blinded:
guard dependencies.sign.verify(message: data.bytes, publicKey: publicKey.bytes, signature: signature.bytes) else {
SNLog("Ignoring message with invalid signature.")
throw OpenGroupAPI.Error.parsingFailed
throw HTTP.Error.parsingFailed
}
case .standard, .unblinded:
guard (try? dependencies.ed25519.verifySignature(signature, publicKey: publicKey, data: data)) == true else {
SNLog("Ignoring message with invalid signature.")
throw OpenGroupAPI.Error.parsingFailed
throw HTTP.Error.parsingFailed
}
case .none:
SNLog("Ignoring message with invalid sender.")
throw OpenGroupAPI.Error.parsingFailed
throw HTTP.Error.parsingFailed
}
}

View File

@ -40,7 +40,7 @@ extension OpenGroupAPI {
///
/// 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]?
let fileIds: [UInt64]?
// MARK: - Initialization
@ -49,7 +49,7 @@ extension OpenGroupAPI {
signature: Data,
whisperTo: String? = nil,
whisperMods: Bool? = nil,
fileIds: [Int64]? = nil
fileIds: [UInt64]? = nil
) {
self.data = data
self.signature = signature

View File

@ -59,7 +59,7 @@ public final class OpenGroupAPI: NSObject {
// Generate the requests
let requestResponseType: [BatchRequestInfoType] = [
BatchRequestInfo(
request: Request<NoBody>(
request: Request<NoBody, Endpoint>(
server: server,
endpoint: .capabilities
),
@ -85,14 +85,14 @@ public final class OpenGroupAPI: NSObject {
return [
BatchRequestInfo(
request: Request<NoBody>(
request: Request<NoBody, Endpoint>(
server: server,
endpoint: .roomPollInfo(openGroup.room, openGroup.infoUpdates)
),
responseType: RoomPollInfo.self
),
BatchRequestInfo(
request: Request<NoBody>(
request: Request<NoBody, Endpoint>(
server: server,
endpoint: (shouldRetrieveRecentMessages ?
.roomMessagesRecent(openGroup.room) :
@ -107,7 +107,7 @@ public final class OpenGroupAPI: NSObject {
.appending([
// Inbox
BatchRequestInfo(
request: Request<NoBody>(
request: Request<NoBody, Endpoint>(
server: server,
endpoint: (maybeLastInboxMessageId == nil ?
.inbox :
@ -119,7 +119,7 @@ public final class OpenGroupAPI: NSObject {
// Outbox
BatchRequestInfo(
request: Request<NoBody>(
request: Request<NoBody, Endpoint>(
server: server,
endpoint: (maybeLastOutboxMessageId == nil ?
.outbox :
@ -146,12 +146,12 @@ public final class OpenGroupAPI: NSObject {
let request: Request = Request(
method: .post,
server: server,
endpoint: .batch,
endpoint: Endpoint.batch,
body: requestBody
)
return send(request, using: dependencies)
.decoded(as: responseTypes, on: OpenGroupAPI.workQueue, error: Error.parsingFailed, using: dependencies)
.decoded(as: responseTypes, on: OpenGroupAPI.workQueue, using: dependencies)
.map { result in
result.enumerated()
.reduce(into: [:]) { prev, next in
@ -176,13 +176,13 @@ public final class OpenGroupAPI: NSObject {
let request: Request = Request(
method: .post,
server: server,
endpoint: .sequence,
endpoint: Endpoint.sequence,
body: requestBody
)
// TODO: Handle a `412` response (ie. a required capability isn't supported)
return send(request, using: dependencies)
.decoded(as: responseTypes, on: OpenGroupAPI.workQueue, error: Error.parsingFailed, using: dependencies)
.decoded(as: responseTypes, on: OpenGroupAPI.workQueue, using: dependencies)
.map { result in
result.enumerated()
.reduce(into: [:]) { prev, next in
@ -201,7 +201,7 @@ public final class OpenGroupAPI: NSObject {
/// 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>(
let request: Request = Request<NoBody, Endpoint>(
server: server,
endpoint: .capabilities,
queryParameters: [:] // TODO: Add any requirements '.required'.
@ -209,7 +209,7 @@ public final class OpenGroupAPI: NSObject {
// TODO: Handle a `412` response (ie. a required capability isn't supported)
return send(request, using: dependencies)
.decoded(as: Capabilities.self, on: OpenGroupAPI.workQueue, error: Error.parsingFailed, using: dependencies)
.decoded(as: Capabilities.self, on: OpenGroupAPI.workQueue, using: dependencies)
}
// MARK: - Room
@ -218,13 +218,13 @@ public final class OpenGroupAPI: NSObject {
///
/// 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>(
let request: Request = Request<NoBody, Endpoint>(
server: server,
endpoint: .rooms
)
return send(request, using: dependencies)
.decoded(as: [Room].self, on: OpenGroupAPI.workQueue, error: Error.parsingFailed, using: dependencies)
.decoded(as: [Room].self, on: OpenGroupAPI.workQueue, using: dependencies)
}
/// Returns the details of a single room
@ -234,13 +234,13 @@ public final class OpenGroupAPI: NSObject {
/// method to ensure things are processed correctly
@available(*, unavailable, message: "Avoid using this directly, use the pre-built `poll()` method instead")
public static func room(for roomToken: String, on server: String, using dependencies: Dependencies = Dependencies()) -> Promise<(OnionRequestResponseInfoType, Room)> {
let request: Request = Request<NoBody>(
let request: Request = Request<NoBody, Endpoint>(
server: server,
endpoint: .room(roomToken)
)
return send(request, using: dependencies)
.decoded(as: Room.self, on: OpenGroupAPI.workQueue, error: Error.parsingFailed, using: dependencies)
.decoded(as: Room.self, on: OpenGroupAPI.workQueue, using: dependencies)
}
/// Polls a room for metadata updates
@ -253,13 +253,13 @@ public final class OpenGroupAPI: NSObject {
/// method to ensure things are processed correctly
@available(*, unavailable, message: "Avoid using this directly, use the pre-built `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>(
let request: Request = Request<NoBody, Endpoint>(
server: server,
endpoint: .roomPollInfo(roomToken, lastUpdated)
)
return send(request, using: dependencies)
.decoded(as: RoomPollInfo.self, on: OpenGroupAPI.workQueue, error: Error.parsingFailed, using: dependencies)
.decoded(as: RoomPollInfo.self, on: OpenGroupAPI.workQueue, using: dependencies)
}
/// This is a convenience method which constructs a `/sequence` of the `capabilities` and `room` requests, refer to those
@ -272,7 +272,7 @@ public final class OpenGroupAPI: NSObject {
let requestResponseType: [BatchRequestInfoType] = [
// Get the latest capabilities for the server (in case it's a new server or the cached ones are stale)
BatchRequestInfo(
request: Request<NoBody>(
request: Request<NoBody, Endpoint>(
server: server,
endpoint: .capabilities
),
@ -281,7 +281,7 @@ public final class OpenGroupAPI: NSObject {
// And the room info
BatchRequestInfo(
request: Request<NoBody>(
request: Request<NoBody, Endpoint>(
server: server,
endpoint: .room(roomToken)
),
@ -298,14 +298,14 @@ public final class OpenGroupAPI: NSObject {
switch endpoint {
case .capabilities:
guard let responseData: BatchSubResponse<Capabilities> = endpointResponse.data as? BatchSubResponse<Capabilities>, let responseBody: Capabilities = responseData.body else {
throw Error.parsingFailed
throw HTTP.Error.parsingFailed
}
capabilities = (endpointResponse.info, responseBody)
case .room:
guard let responseData: OpenGroupAPI.BatchSubResponse<OpenGroupAPI.Room> = endpointResponse.data as? OpenGroupAPI.BatchSubResponse<OpenGroupAPI.Room>, let responseBody: OpenGroupAPI.Room = responseData.body else {
throw Error.parsingFailed
throw HTTP.Error.parsingFailed
}
room = (endpointResponse.info, responseBody)
@ -315,7 +315,7 @@ public final class OpenGroupAPI: NSObject {
}
guard let capabilities: (OnionRequestResponseInfoType, Capabilities?) = capabilities, let room: (OnionRequestResponseInfoType, Room?) = room else {
throw Error.parsingFailed
throw HTTP.Error.parsingFailed
}
return (capabilities, room)
@ -331,6 +331,7 @@ public final class OpenGroupAPI: NSObject {
on server: String,
whisperTo: String?,
whisperMods: Bool,
fileIds: [UInt64]?,
using dependencies: Dependencies = Dependencies()
) -> Promise<(OnionRequestResponseInfoType, Message)> {
guard let signResult: (publicKey: String, signature: Bytes) = sign(plaintext.bytes, for: server, using: dependencies) else {
@ -342,18 +343,18 @@ public final class OpenGroupAPI: NSObject {
signature: Data(signResult.signature),
whisperTo: whisperTo,
whisperMods: whisperMods,
fileIds: nil // TODO: Add support for 'fileIds'.
fileIds: fileIds
)
let request = Request(
method: .post,
server: server,
endpoint: .roomMessage(roomToken),
endpoint: Endpoint.roomMessage(roomToken),
body: requestBody
)
return send(request, using: dependencies)
.decoded(as: Message.self, on: OpenGroupAPI.workQueue, error: Error.parsingFailed, using: dependencies)
.decoded(as: Message.self, on: OpenGroupAPI.workQueue, using: dependencies)
.map { response, message in
// Store the 'message.posted' timestamp to prevent the sent message getting duplicated when it is later retrieved
dependencies.storage.write { transaction in
@ -367,13 +368,13 @@ public final class OpenGroupAPI: NSObject {
/// 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>(
let request: Request = Request<NoBody, Endpoint>(
server: server,
endpoint: .roomMessageIndividual(roomToken, id: id)
)
return send(request, using: dependencies)
.decoded(as: Message.self, on: OpenGroupAPI.workQueue, error: Error.parsingFailed, using: dependencies)
.decoded(as: Message.self, on: OpenGroupAPI.workQueue, using: dependencies)
}
/// Edits a message, replacing its existing content with new content and a new signature
@ -400,7 +401,7 @@ public final class OpenGroupAPI: NSObject {
let request: Request = Request(
method: .put,
server: server,
endpoint: .roomMessageIndividual(roomToken, id: id),
endpoint: Endpoint.roomMessageIndividual(roomToken, id: id),
body: requestBody
)
@ -414,7 +415,7 @@ public final class OpenGroupAPI: NSObject {
on server: String,
using dependencies: Dependencies = Dependencies()
) -> Promise<(OnionRequestResponseInfoType, Data?)> {
let request: Request = Request<NoBody>(
let request: Request = Request<NoBody, Endpoint>(
method: .delete,
server: server,
endpoint: .roomMessageIndividual(roomToken, id: id)
@ -428,7 +429,7 @@ public final class OpenGroupAPI: NSObject {
/// method to ensure things are processed correctly
@available(*, unavailable, message: "Avoid using this directly, use the pre-built `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>(
let request: Request = Request<NoBody, Endpoint>(
server: server,
endpoint: .roomMessagesRecent(roomToken)
// TODO: Limit?.
@ -436,7 +437,7 @@ public final class OpenGroupAPI: NSObject {
)
return send(request, using: dependencies)
.decoded(as: [Message].self, on: OpenGroupAPI.workQueue, error: Error.parsingFailed, using: dependencies)
.decoded(as: [Message].self, on: OpenGroupAPI.workQueue, using: dependencies)
}
/// **Note:** This is the direct request to retrieve recent messages before a given message and is currently unused, in order to call this directly
@ -445,7 +446,7 @@ public final class OpenGroupAPI: NSObject {
@available(*, unavailable, message: "Avoid using this directly, use the pre-built `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?
let request: Request = Request<NoBody>(
let request: Request = Request<NoBody, Endpoint>(
server: server,
endpoint: .roomMessagesBefore(roomToken, id: messageId)
// TODO: Limit?.
@ -453,7 +454,7 @@ public final class OpenGroupAPI: NSObject {
)
return send(request, using: dependencies)
.decoded(as: [Message].self, on: OpenGroupAPI.workQueue, error: Error.parsingFailed, using: dependencies)
.decoded(as: [Message].self, on: OpenGroupAPI.workQueue, using: dependencies)
}
/// **Note:** This is the direct request to retrieve messages since a given message `seqNo` so should be retrieved automatically from the
@ -461,7 +462,7 @@ public final class OpenGroupAPI: NSObject {
/// `OpenGroupManager.handleMessages` method to ensure things are processed correctly
@available(*, unavailable, message: "Avoid using this directly, use the pre-built `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>(
let request: Request = Request<NoBody, Endpoint>(
server: server,
endpoint: .roomMessagesSince(roomToken, seqNo: seqNo)
// TODO: Limit?.
@ -469,7 +470,7 @@ public final class OpenGroupAPI: NSObject {
)
return send(request, using: dependencies)
.decoded(as: [Message].self, on: OpenGroupAPI.workQueue, error: Error.parsingFailed, using: dependencies)
.decoded(as: [Message].self, on: OpenGroupAPI.workQueue, using: dependencies)
}
// MARK: - Pinning
@ -485,7 +486,7 @@ public final class OpenGroupAPI: NSObject {
/// 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>(
let request: Request = Request<NoBody, Endpoint>(
method: .post,
server: server,
endpoint: .roomPinMessage(roomToken, id: id)
@ -499,7 +500,7 @@ public final class OpenGroupAPI: NSObject {
///
/// 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>(
let request: Request = Request<NoBody, Endpoint>(
method: .post,
server: server,
endpoint: .roomUnpinMessage(roomToken, id: id)
@ -513,7 +514,7 @@ public final class OpenGroupAPI: NSObject {
///
/// 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>(
let request: Request = Request<NoBody, Endpoint>(
method: .post,
server: server,
endpoint: .roomUnpinAll(roomToken)
@ -529,7 +530,7 @@ public final class OpenGroupAPI: NSObject {
let request: Request = Request(
method: .post,
server: server,
endpoint: .roomFile(roomToken),
endpoint: Endpoint.roomFile(roomToken),
headers: [
.contentDisposition: [ "attachment", fileName.map { "filename=\"\($0)\"" } ]
.compactMap{ $0 }
@ -540,50 +541,31 @@ public final class OpenGroupAPI: NSObject {
)
return send(request, using: dependencies)
.decoded(as: FileUploadResponse.self, on: OpenGroupAPI.workQueue, error: Error.parsingFailed, using: dependencies)
.decoded(as: FileUploadResponse.self, on: OpenGroupAPI.workQueue, using: dependencies)
}
/// Warning: This approach is less efficient as it expects the data to be base64Encoded (with is 33% larger than binary), please use the binary approach
/// whenever possible
public static func uploadFile(_ base64EncodedString: String, fileName: String? = nil, to roomToken: String, on server: String, using dependencies: Dependencies = Dependencies()) -> Promise<(OnionRequestResponseInfoType, FileUploadResponse)> {
let request: Request = Request(
method: .post,
server: server,
endpoint: .roomFileJson(roomToken),
headers: [
.contentDisposition: [ "attachment", fileName.map { "filename=\"\($0)\"" } ]
.compactMap{ $0 }
.joined(separator: "; "),
],
body: base64EncodedString
)
return send(request, using: dependencies)
.decoded(as: FileUploadResponse.self, on: OpenGroupAPI.workQueue, error: Error.parsingFailed, using: dependencies)
}
public static func downloadFile(_ fileId: Int64, from roomToken: String, on server: String, using dependencies: Dependencies = Dependencies()) -> Promise<(OnionRequestResponseInfoType, Data)> {
let request: Request = Request<NoBody>(
public static func downloadFile(_ fileId: UInt64, from roomToken: String, on server: String, using dependencies: Dependencies = Dependencies()) -> Promise<(OnionRequestResponseInfoType, Data)> {
let request: Request = Request<NoBody, Endpoint>(
server: server,
endpoint: .roomFileIndividual(roomToken, fileId)
)
return send(request, using: dependencies)
.map { responseInfo, maybeData in
guard let data: Data = maybeData else { throw Error.parsingFailed }
guard let data: Data = maybeData else { throw HTTP.Error.parsingFailed }
return (responseInfo, data)
}
}
public static func downloadFileJson(_ fileId: Int64, from roomToken: String, on server: String, using dependencies: Dependencies = Dependencies()) -> Promise<(OnionRequestResponseInfoType, FileDownloadResponse)> {
let request: Request = Request<NoBody>(
public static func downloadFileJson(_ fileId: UInt64, from roomToken: String, on server: String, using dependencies: Dependencies = Dependencies()) -> Promise<(OnionRequestResponseInfoType, FileDownloadResponse)> {
let request: Request = Request<NoBody, Endpoint>(
server: server,
endpoint: .roomFileIndividualJson(roomToken, fileId)
)
// TODO: This endpoint is getting rewritten to return just data (properties would come through as headers).
return send(request, using: dependencies)
.decoded(as: FileDownloadResponse.self, on: OpenGroupAPI.workQueue, error: Error.parsingFailed, using: dependencies)
.decoded(as: FileDownloadResponse.self, on: OpenGroupAPI.workQueue, using: dependencies)
}
// MARK: - Inbox/Outbox (Message Requests)
@ -595,13 +577,13 @@ public final class OpenGroupAPI: NSObject {
/// `OpenGroupManager.handleDirectMessages` method to ensure things are processed correctly
@available(*, unavailable, message: "Avoid using this directly, use the pre-built `poll()` method instead")
public static func inbox(on server: String, using dependencies: Dependencies = Dependencies()) -> Promise<(OnionRequestResponseInfoType, [DirectMessage]?)> {
let request: Request = Request<NoBody>(
let request: Request = Request<NoBody, Endpoint>(
server: server,
endpoint: .inbox
)
return send(request, using: dependencies)
.decoded(as: [DirectMessage]?.self, on: OpenGroupAPI.workQueue, error: Error.parsingFailed, using: dependencies)
.decoded(as: [DirectMessage]?.self, on: OpenGroupAPI.workQueue, using: dependencies)
}
/// Polls for any DMs received since the given id, this method will return a `304` with an empty response if there are no messages
@ -611,13 +593,13 @@ public final class OpenGroupAPI: NSObject {
/// of this method to the `OpenGroupManager.handleDirectMessages` method to ensure things are processed correctly
@available(*, unavailable, message: "Avoid using this directly, use the pre-built `poll()` method instead")
public static func inboxSince(id: Int64, on server: String, using dependencies: Dependencies = Dependencies()) -> Promise<(OnionRequestResponseInfoType, [DirectMessage]?)> {
let request: Request = Request<NoBody>(
let request: Request = Request<NoBody, Endpoint>(
server: server,
endpoint: .inboxSince(id: id)
)
return send(request, using: dependencies)
.decoded(as: [DirectMessage]?.self, on: OpenGroupAPI.workQueue, error: Error.parsingFailed, using: dependencies)
.decoded(as: [DirectMessage]?.self, on: OpenGroupAPI.workQueue, using: dependencies)
}
/// Delivers a direct message to a user via their blinded Session ID
@ -631,12 +613,12 @@ public final class OpenGroupAPI: NSObject {
let request: Request = Request(
method: .post,
server: server,
endpoint: .inboxFor(sessionId: blindedSessionId),
endpoint: Endpoint.inboxFor(sessionId: blindedSessionId),
body: requestBody
)
return send(request, using: dependencies)
.decoded(as: SendDirectMessageResponse.self, on: OpenGroupAPI.workQueue, error: Error.parsingFailed, using: dependencies)
.decoded(as: SendDirectMessageResponse.self, on: OpenGroupAPI.workQueue, using: dependencies)
.map { response, message in
// Store the 'message.posted' timestamp to prevent the sent message getting duplicated when it is later retrieved
dependencies.storage.write { transaction in
@ -655,13 +637,13 @@ public final class OpenGroupAPI: NSObject {
/// this method to the `OpenGroupManager.handleDirectMessages` method to ensure things are processed correctly
@available(*, unavailable, message: "Avoid using this directly, use the pre-built `poll()` method instead")
public static func outbox(on server: String, using dependencies: Dependencies = Dependencies()) -> Promise<(OnionRequestResponseInfoType, [DirectMessage]?)> {
let request: Request = Request<NoBody>(
let request: Request = Request<NoBody, Endpoint>(
server: server,
endpoint: .outbox
)
return send(request, using: dependencies)
.decoded(as: [DirectMessage]?.self, on: OpenGroupAPI.workQueue, error: Error.parsingFailed, using: dependencies)
.decoded(as: [DirectMessage]?.self, on: OpenGroupAPI.workQueue, using: dependencies)
}
/// Polls for any DMs sent since the given id, this method will return a `304` with an empty response if there are no messages
@ -671,13 +653,13 @@ public final class OpenGroupAPI: NSObject {
/// to route the response of this method to the `OpenGroupManager.handleDirectMessages` method to ensure things are processed correctly
@available(*, unavailable, message: "Avoid using this directly, use the pre-built `poll()` method instead")
public static func outboxSince(id: Int64, on server: String, using dependencies: Dependencies = Dependencies()) -> Promise<(OnionRequestResponseInfoType, [DirectMessage]?)> {
let request: Request = Request<NoBody>(
let request: Request = Request<NoBody, Endpoint>(
server: server,
endpoint: .outboxSince(id: id)
)
return send(request, using: dependencies)
.decoded(as: [DirectMessage]?.self, on: OpenGroupAPI.workQueue, error: Error.parsingFailed, using: dependencies)
.decoded(as: [DirectMessage]?.self, on: OpenGroupAPI.workQueue, using: dependencies)
}
// MARK: - Users
@ -729,7 +711,7 @@ public final class OpenGroupAPI: NSObject {
let request: Request = Request(
method: .post,
server: server,
endpoint: .userBan(sessionId),
endpoint: Endpoint.userBan(sessionId),
body: requestBody
)
@ -774,7 +756,7 @@ public final class OpenGroupAPI: NSObject {
let request: Request = Request(
method: .post,
server: server,
endpoint: .userUnban(sessionId),
endpoint: Endpoint.userUnban(sessionId),
body: requestBody
)
@ -841,7 +823,9 @@ public final class OpenGroupAPI: NSObject {
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) }
guard (moderator != nil && admin == nil) || (moderator == nil && admin != nil) else {
return Promise(error: HTTP.Error.generic)
}
let requestBody: UserModeratorRequest = UserModeratorRequest(
rooms: roomTokens,
@ -854,7 +838,7 @@ public final class OpenGroupAPI: NSObject {
let request: Request = Request(
method: .post,
server: server,
endpoint: .userModerator(sessionId),
endpoint: Endpoint.userModerator(sessionId),
body: requestBody
)
@ -894,12 +878,12 @@ public final class OpenGroupAPI: NSObject {
let request: Request = Request(
method: .post,
server: server,
endpoint: .userDeleteMessages(sessionId),
endpoint: Endpoint.userDeleteMessages(sessionId),
body: requestBody
)
return send(request, using: dependencies)
.decoded(as: UserDeleteMessagesResponse.self, on: OpenGroupAPI.workQueue, error: Error.parsingFailed, using: dependencies)
.decoded(as: UserDeleteMessagesResponse.self, on: OpenGroupAPI.workQueue, using: dependencies)
}
// TODO: Need to test this once the API has been implemented
@ -1003,11 +987,7 @@ public final class OpenGroupAPI: NSObject {
/// Get a hash of any body content
let bodyHash: Bytes? = {
// Note: We need the `!body.isEmpty` check because of the default `Data()` value when trying to
// init data from the httpBodyStream
guard let body: Data = (request.httpBody ?? request.httpBodyStream.map { ((try? Data(from: $0)) ?? Data()) }), !body.isEmpty else {
return nil
}
guard let body: Data = request.httpBody else { return nil }
return dependencies.genericHash.hash(message: body.bytes, outputLength: 64)
}()
@ -1045,20 +1025,14 @@ public final class OpenGroupAPI: NSObject {
// MARK: - Convenience
private static func send<T: Encodable>(_ request: Request<T>, using dependencies: Dependencies = Dependencies()) -> Promise<(OnionRequestResponseInfoType, Data?)> {
guard let url: URL = request.url else { return Promise(error: Error.invalidURL) }
var urlRequest: URLRequest = URLRequest(url: url)
urlRequest.httpMethod = request.method.rawValue
urlRequest.allHTTPHeaderFields = request.headers
.setting(.room, request.room) // TODO: Is this needed anymore? Add at the request level?.
.toHTTPHeaders()
private static func send<T: Encodable>(_ request: Request<T, Endpoint>, using dependencies: Dependencies = Dependencies()) -> Promise<(OnionRequestResponseInfoType, Data?)> {
let urlRequest: URLRequest
do {
urlRequest.httpBody = try request.bodyData()
urlRequest = try request.generateUrlRequest()
}
catch {
return Promise(error: Error.parsingFailed)
return Promise(error: error)
}
if request.useOnionRouting {

View File

@ -46,7 +46,7 @@ public final class OpenGroupManager: NSObject {
let groupId: Data = LKGroupUtilities.getEncodedOpenGroupIDAsData("\(server).\(roomToken)")
if OpenGroupManager.shared.pollers[server] != nil && TSGroupThread.fetch(uniqueId: TSGroupThread.threadId(fromGroupId: groupId), transaction: transaction) != nil {
SNLog("Ignoring join open group attempt, user initiated: \(!isConfigMessage)")
SNLog("Ignoring join open group attempt (already joined), user initiated: \(!isConfigMessage)")
return Promise.value(())
}
@ -63,7 +63,7 @@ public final class OpenGroupManager: NSObject {
.done(on: DispatchQueue.global(qos: .userInitiated)) { (capabilitiesResponse: (info: OnionRequestResponseInfoType, data: OpenGroupAPI.Capabilities?), roomResponse: (info: OnionRequestResponseInfoType, data: OpenGroupAPI.Room?)) in
guard let capabilities: OpenGroupAPI.Capabilities = capabilitiesResponse.data, let room: OpenGroupAPI.Room = roomResponse.data else {
SNLog("Failed to join open group due to invalid data.")
seal.reject(OpenGroupAPI.Error.generic)
seal.reject(HTTP.Error.generic)
return
}
@ -254,7 +254,7 @@ public final class OpenGroupManager: NSObject {
}
// - Room image (if there is one)
if let imageId: Int64 = pollInfo.details?.imageId {
if let imageId: UInt64 = pollInfo.details?.imageId {
OpenGroupManager.roomImage(imageId, for: roomToken, on: server)
.done(on: DispatchQueue.global(qos: .userInitiated)) { data in
dependencies.storage.write { transaction in
@ -493,8 +493,8 @@ public final class OpenGroupManager: NSObject {
OpenGroupManager.defaultRoomsPromise?
.done(on: OpenGroupAPI.workQueue) { items in
items
.compactMap { room -> (Int64, String)? in
guard let imageId: Int64 = room.imageId else { return nil}
.compactMap { room -> (UInt64, String)? in
guard let imageId: UInt64 = room.imageId else { return nil}
return (imageId, room.token)
}
@ -511,7 +511,7 @@ public final class OpenGroupManager: NSObject {
}
public static func roomImage(
_ fileId: Int64,
_ fileId: UInt64,
for roomToken: String,
on server: String,
using dependencies: OpenGroupAPI.Dependencies = OpenGroupAPI.Dependencies()

View File

@ -1,94 +0,0 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import SessionUtilitiesKit
extension OpenGroupAPI {
struct Empty: Codable {}
typealias NoBody = Empty
typealias NoResponse = Empty
struct Request<T: Encodable> {
let method: HTTP.Verb
let server: String
let room: String? // TODO: Remove this?
let endpoint: Endpoint
let queryParameters: [QueryParam: String]
let headers: [Header: String]
/// This is the body value sent during the request
///
/// **Warning:** The `bodyData` value should be used to when making the actual request instead of this as there
/// is custom handling for certain data types
let body: T?
let isAuthRequired: Bool
/// Always `true` under normal circumstances. You might want to disable
/// this when running over Lokinet.
let useOnionRouting: Bool
// MARK: - Initialization
init(
method: HTTP.Verb = .get,
server: String,
room: String? = nil,
endpoint: Endpoint,
queryParameters: [QueryParam: String] = [:],
headers: [Header: String] = [:],
body: T? = nil,
isAuthRequired: Bool = true,
useOnionRouting: Bool = true
) {
self.method = method
self.server = server
self.room = room
self.endpoint = endpoint
self.queryParameters = queryParameters
self.headers = headers
self.body = body
self.isAuthRequired = isAuthRequired
self.useOnionRouting = useOnionRouting
}
// MARK: - Convenience
var url: URL? {
return URL(string: "\(server)\(urlPathAndParamsString)")
}
var urlPathAndParamsString: String {
return [
"/\(endpoint.path)",
queryParameters
.map { key, value in "\(key.rawValue)=\(value)" }
.joined(separator: "&")
]
.compactMap { $0 }
.filter { !$0.isEmpty }
.joined(separator: "?")
}
func bodyData() throws -> Data? {
// Note: Need to differentiate between JSON, b64 string and bytes body values to ensure they are
// encoded correctly so the server knows how to handle them
switch body {
case let bodyString as String:
// The only acceptable string body is a base64 encoded one
guard let encodedData: Data = Data(base64Encoded: bodyString) else {
throw OpenGroupAPI.Error.parsingFailed
}
return encodedData
case let bodyBytes as [UInt8]:
return Data(bodyBytes)
default:
// Having no body is fine so just return nil
guard let body: T = body else { return nil }
return try JSONEncoder().encode(body)
}
}
}
}

View File

@ -3,7 +3,7 @@
import Foundation
extension OpenGroupAPI {
public enum Endpoint: Hashable {
public enum Endpoint: EndpointType {
// Utility
case onion
@ -34,9 +34,8 @@ extension OpenGroupAPI {
// Files
case roomFile(String)
case roomFileJson(String)
case roomFileIndividual(String, Int64)
case roomFileIndividualJson(String, Int64)
case roomFileIndividual(String, UInt64)
case roomFileIndividualJson(String, UInt64)
// Inbox/Outbox (Message Requests)
@ -126,7 +125,6 @@ extension OpenGroupAPI {
// Files
case .roomFile(let roomToken): return "room/\(roomToken)/file"
case .roomFileJson(let roomToken): return "room/\(roomToken)/fileJSON"
case .roomFileIndividual(let roomToken, let fileId):
// Note: The 'fileName' value is ignored by the server and is only used to distinguish
// this from the 'Json' variant

View File

@ -4,20 +4,14 @@ import Foundation
extension OpenGroupAPI {
public enum Error: LocalizedError {
case generic
case parsingFailed
case decryptionFailed
case signingFailed
case invalidURL
case noPublicKey
public var errorDescription: String? {
switch self {
case .generic: return "An error occurred."
case .parsingFailed: return "Invalid response."
case .decryptionFailed: return "Couldn't decrypt response."
case .signingFailed: return "Couldn't sign message."
case .invalidURL: return "Invalid URL."
case .noPublicKey: return "Couldn't find server public key."
}
}

View File

@ -379,7 +379,7 @@ public final class MessageSender : NSObject {
// Send the result
guard case .openGroup(let room, let server, let whisperTo, let whisperMods, _) = destination else {
guard case .openGroup(let room, let server, let whisperTo, let whisperMods, let fileIds) = destination else {
preconditionFailure()
}
@ -389,7 +389,8 @@ public final class MessageSender : NSObject {
to: room,
on: server,
whisperTo: whisperTo,
whisperMods: whisperMods
whisperMods: whisperMods,
fileIds: fileIds
)
.done(on: DispatchQueue.global(qos: .userInitiated)) { responseInfo, data in
message.openGroupServerMessageID = UInt64(data.id)

View File

@ -45,6 +45,7 @@ public protocol SessionMessagingKitStorageProtocol {
func getAllPendingJobs(of type: Job.Type) -> [Job]
func getAttachmentUploadJob(for attachmentID: String) -> AttachmentUploadJob?
func getMessageSendJob(for messageSendJobID: String) -> MessageSendJob?
func getMessageSendJob(for messageSendJobID: String, using transaction: Any) -> MessageSendJob?
func resumeMessageSendJobIfNeeded(_ messageSendJobID: String)
func isJobCanceled(_ job: Job) -> Bool

View File

@ -13,13 +13,16 @@ extension Promise where T == Data {
}
extension Promise where T == (OnionRequestResponseInfoType, Data?) {
func decoded<R: Decodable>(as type: R.Type, on queue: DispatchQueue? = nil, error: Error? = nil, using dependencies: OpenGroupAPI.Dependencies = OpenGroupAPI.Dependencies()) -> Promise<(OnionRequestResponseInfoType, R)> {
func decoded<R: Decodable>(as type: R.Type, on queue: DispatchQueue? = nil, using dependencies: OpenGroupAPI.Dependencies = OpenGroupAPI.Dependencies()) -> Promise<(OnionRequestResponseInfoType, R)> {
self.map(on: queue) { responseInfo, maybeData -> (OnionRequestResponseInfoType, R) in
guard let data: Data = maybeData else {
throw OpenGroupAPI.Error.parsingFailed
}
guard let data: Data = maybeData else { throw HTTP.Error.parsingFailed }
return (responseInfo, try data.decoded(as: type, customError: error, using: dependencies))
do {
return (responseInfo, try data.decoded(as: type, using: dependencies))
}
catch {
throw HTTP.Error.parsingFailed
}
}
}
}

View File

@ -252,7 +252,7 @@ class OpenGroupAPISpec: QuickSpec {
expect(error?.localizedDescription)
.toEventually(
equal(OpenGroupAPI.Error.parsingFailed.localizedDescription),
equal(HTTP.Error.parsingFailed.localizedDescription),
timeout: .milliseconds(100)
)
@ -272,7 +272,7 @@ class OpenGroupAPISpec: QuickSpec {
expect(error?.localizedDescription)
.toEventually(
equal(OpenGroupAPI.Error.parsingFailed.localizedDescription),
equal(HTTP.Error.parsingFailed.localizedDescription),
timeout: .milliseconds(100)
)
@ -292,7 +292,7 @@ class OpenGroupAPISpec: QuickSpec {
expect(error?.localizedDescription)
.toEventually(
equal(OpenGroupAPI.Error.parsingFailed.localizedDescription),
equal(HTTP.Error.parsingFailed.localizedDescription),
timeout: .milliseconds(100)
)
@ -312,7 +312,7 @@ class OpenGroupAPISpec: QuickSpec {
expect(error?.localizedDescription)
.toEventually(
equal(OpenGroupAPI.Error.parsingFailed.localizedDescription),
equal(HTTP.Error.parsingFailed.localizedDescription),
timeout: .milliseconds(100)
)
@ -364,7 +364,7 @@ class OpenGroupAPISpec: QuickSpec {
expect(error?.localizedDescription)
.toEventually(
equal(OpenGroupAPI.Error.parsingFailed.localizedDescription),
equal(HTTP.Error.parsingFailed.localizedDescription),
timeout: .milliseconds(100)
)
@ -413,7 +413,7 @@ class OpenGroupAPISpec: QuickSpec {
expect(error?.localizedDescription)
.toEventually(
equal(OpenGroupAPI.Error.parsingFailed.localizedDescription),
equal(HTTP.Error.parsingFailed.localizedDescription),
timeout: .milliseconds(100)
)
@ -449,7 +449,8 @@ class OpenGroupAPISpec: QuickSpec {
expect(requestData?.urlString).to(equal("testServer/room/testRoom/file"))
expect(requestData?.httpMethod).to(equal("POST"))
expect(requestData?.headers).to(haveCount(4))
expect(requestData?.headers.keys).toNot(contain(Header.fileName.rawValue))
expect(requestData?.headers[Header.contentDisposition.rawValue])
.toNot(contain("filename"))
}
it("adds a fileName header when provided") {
@ -477,7 +478,7 @@ class OpenGroupAPISpec: QuickSpec {
expect(requestData?.urlString).to(equal("testServer/room/testRoom/file"))
expect(requestData?.httpMethod).to(equal("POST"))
expect(requestData?.headers).to(haveCount(5))
expect(requestData?.headers[Header.fileName.rawValue]).to(equal("TestFileName"))
expect(requestData?.headers[Header.contentDisposition.rawValue]).to(contain("TestFileName"))
}
}

View File

@ -14,24 +14,22 @@ internal extension OnionRequestAPI {
}
/// Encrypts `payload` for `destination` and returns the result. Use this to build the core of an onion request.
static func encrypt(_ payload: String, for destination: Destination, with version: Version) -> Promise<AESGCM.EncryptionResult> {
static func encrypt(_ payload: Data, for destination: Destination, with version: Version) -> Promise<AESGCM.EncryptionResult> {
let (promise, seal) = Promise<AESGCM.EncryptionResult>.pending()
DispatchQueue.global(qos: .userInitiated).async {
do {
guard let payloadAsData: Data = payload.data(using: .utf8) else { throw Error.invalidRequestInfo }
let data: Data
switch version {
case .v2, .v3:
// Wrapping is only needed for snode requests
switch destination {
case .snode: data = try encode(ciphertext: payloadAsData, json: [ "headers" : "" ])
case .server: data = payloadAsData
case .snode: data = try encode(ciphertext: payload, json: [ "headers" : "" ])
case .server: data = payload
}
case .v4:
data = payloadAsData
data = payload
}
let result = try encrypt(data, for: destination)

View File

@ -249,7 +249,7 @@ public enum OnionRequestAPI: OnionRequestAPIType {
}
/// Builds an onion around `payload` and returns the result.
private static func buildOnion(around payload: String, targetedAt destination: Destination, version: Version) -> Promise<OnionBuildingResult> {
private static func buildOnion(around payload: Data, targetedAt destination: Destination, version: Version) -> Promise<OnionBuildingResult> {
var guardSnode: Snode!
var targetSnodeSymmetricKey: Data! // Needed by invoke(_:on:with:) to decrypt the response sent back by the destination
var encryptionResult: AESGCM.EncryptionResult!
@ -287,7 +287,7 @@ public enum OnionRequestAPI: OnionRequestAPIType {
public static func sendOnionRequest(to snode: Snode, invoking method: Snode.Method, with parameters: JSON, using version: Version, associatedWith publicKey: String?) -> Promise<Data> {
let payloadJson: JSON = [ "method": method.rawValue, "params": parameters ]
guard let jsonData: Data = try? JSONSerialization.data(withJSONObject: payloadJson, options: []), let payload: String = String(data: jsonData, encoding: .utf8) else {
guard let payload: Data = try? JSONSerialization.data(withJSONObject: payloadJson, options: []) else {
return Promise(error: HTTP.Error.invalidJSON)
}
@ -316,7 +316,7 @@ public enum OnionRequestAPI: OnionRequestAPIType {
let scheme: String? = url.scheme
let port: UInt16? = url.port.map { UInt16($0) }
guard let payload: String = generatePayload(for: request, with: version) else {
guard let payload: Data = generatePayload(for: request, with: version) else {
return Promise(error: Error.invalidRequestInfo)
}
@ -328,7 +328,7 @@ public enum OnionRequestAPI: OnionRequestAPIType {
return promise
}
public static func sendOnionRequest(with payload: String, to destination: Destination, version: Version) -> Promise<(OnionRequestResponseInfoType, Data?)> {
public static func sendOnionRequest(with payload: Data, to destination: Destination, version: Version) -> Promise<(OnionRequestResponseInfoType, Data?)> {
let (promise, seal) = Promise<(OnionRequestResponseInfoType, Data?)>.pending()
var guardSnode: Snode?
Threading.workQueue.async { // Avoid race conditions on `guardSnodes` and `paths`
@ -451,7 +451,7 @@ public enum OnionRequestAPI: OnionRequestAPIType {
// MARK: - Version Handling
private static func generatePayload(for request: URLRequest, with version: Version) -> String? {
private static func generatePayload(for request: URLRequest, with version: Version) -> Data? {
guard let url = request.url else { return nil }
switch version {
@ -475,10 +475,6 @@ public enum OnionRequestAPI: OnionRequestAPIType {
headers["Content-Type"] = "application/json" // Assume data is JSON
bodyAsString = (String(data: body, encoding: .utf8) ?? "null")
}
else if let inputStream: InputStream = request.httpBodyStream, let body: Data = try? Data(from: inputStream) {
headers["Content-Type"] = request.allHTTPHeaderFields!["Content-Type"]
bodyAsString = "{ \"fileUpload\" : \"\(String(data: body.base64EncodedData(), encoding: .utf8) ?? "null")\" }"
}
else {
bodyAsString = "null"
}
@ -492,7 +488,7 @@ public enum OnionRequestAPI: OnionRequestAPIType {
guard let jsonData: Data = try? JSONSerialization.data(withJSONObject: payload, options: []) else { return nil }
return String(data: jsonData, encoding: .utf8)
return jsonData
// V4 Onion Requests have a very different structure
case .v4:
@ -502,12 +498,12 @@ public enum OnionRequestAPI: OnionRequestAPIType {
.appending(url.query.map { value in "?\(value)" })
let requestInfo: RequestInfo = RequestInfo(
method: (request.httpMethod ?? "GET"), // Default (if nil) is 'GET'
method: (request.httpMethod ?? "GET"), // The default (if nil) is 'GET'
endpoint: endpoint,
headers: (request.allHTTPHeaderFields ?? [:])
.setting(
"Content-Type",
(request.httpBody == nil && request.httpBodyStream == nil ? nil :
(request.httpBody == nil ? nil :
// Default to JSON if not defined
((request.allHTTPHeaderFields ?? [:])["Content-Type"] ?? "application/json")
)
@ -515,26 +511,17 @@ public enum OnionRequestAPI: OnionRequestAPIType {
.removingValue(forKey: "User-Agent")
)
guard let requestInfoData: Data = try? JSONEncoder().encode(requestInfo), let requestInfoString: String = String(data: requestInfoData, encoding: .ascii) else {
/// Generate the Bencoded payload in the form `l{requestInfoLength}:{requestInfo}{bodyLength}:{body}e`
guard let requestInfoData: Data = try? JSONEncoder().encode(requestInfo) else { return nil }
guard let prefixData: Data = "l\(requestInfoData.count):".data(using: .ascii), let suffixData: Data = "e".data(using: .ascii) else {
return nil
}
if let body: Data = request.httpBody {
guard let bodyString: String = String(data: body, encoding: .ascii) else {
return nil
}
return "l\(requestInfoString.count):\(requestInfoString)\(bodyString.count):\(bodyString)e"
}
else if let inputStream: InputStream = request.httpBodyStream, let body: Data = try? Data(from: inputStream), let bodyString: String = String(data: body, encoding: .ascii) {
// TODO: Handle this properly
// headers["Content-Type"] = request.allHTTPHeaderFields!["Content-Type"]
// bodyAsString = "{ \"fileUpload\" : \"\(String(data: body.base64EncodedData(), encoding: .utf8) ?? "null")\" }"
return "l\(requestInfoString.count):\(requestInfoString)\(bodyString.count):\(bodyString)e"
}
else {
return "l\(requestInfoString.count):\(requestInfoString)e"
if let body: Data = request.httpBody, let bodyCountData: Data = "\(body.count):".data(using: .ascii) {
return (prefixData + requestInfoData + bodyCountData + body + suffixData)
}
return (prefixData + requestInfoData + suffixData)
}
}
@ -653,12 +640,6 @@ public enum OnionRequestAPI: OnionRequestAPIType {
return seal.fulfill((responseInfo, nil))
}
// TODO: Is this going to be done anymore...???
// if let timestamp = body["t"] as? Int64 {
// let offset = timestamp - Int64(NSDate.millisecondTimestamp())
// SnodeAPI.clockOffset = offset
// }
// Extract the response data as well
let dataString: String = String(responseString.suffix(from: infoStringEndIndex))
let dataStringParts: [String.SubSequence] = dataString.split(separator: ":")

View File

@ -78,18 +78,23 @@ public enum HTTP {
// MARK: - Error
public enum Error : LocalizedError {
public enum Error: LocalizedError, Equatable {
case generic
case httpRequestFailed(statusCode: UInt, data: Data?)
case invalidURL
case invalidJSON
case parsingFailed
case invalidResponse
case maxFileSizeExceeded
case httpRequestFailed(statusCode: UInt, data: Data?)
public var errorDescription: String? {
switch self {
case .generic: return "An error occurred."
case .httpRequestFailed(let statusCode, _): return "HTTP request failed with status code: \(statusCode)."
case .invalidURL: return "Invalid URL."
case .invalidJSON: return "Invalid JSON."
case .invalidResponse: return "Invalid Response"
case .parsingFailed, .invalidResponse: return "Invalid response."
case .maxFileSizeExceeded: return "Maximum file size exceeded."
case .httpRequestFailed(let statusCode, _): return "HTTP request failed with status code: \(statusCode)."
}
}
}

View File

@ -39,79 +39,108 @@ extension MessageSender {
}
public static func sendNonDurably(_ message: VisibleMessage, with attachmentIDs: [String], in thread: TSThread, using transaction: YapDatabaseReadWriteTransaction) -> Promise<Void> {
let attachments = attachmentIDs.compactMap { TSAttachment.fetch(uniqueId: $0, transaction: transaction) as? TSAttachmentStream }
let attachments = attachmentIDs.compactMap {
TSAttachment.fetch(uniqueId: $0, transaction: transaction) as? TSAttachmentStream
}
let attachmentsToUpload = attachments.filter { !$0.isUploaded }
let attachmentUploadPromises: [Promise<Void>] = attachmentsToUpload.map { stream in
let attachmentUploadPromises: [Promise<UInt64>] = attachmentsToUpload.map { stream in
let storage = SNMessagingKitConfiguration.shared.storage
if let openGroup = storage.getOpenGroup(for: thread.uniqueId!) {
let (promise, seal) = Promise<Void>.pending()
if let threadId: String = thread.uniqueId, let openGroup = storage.getOpenGroup(for: threadId) {
let (promise, seal) = Promise<UInt64>.pending()
AttachmentUploadJob.upload(
stream,
using: { data in
// TODO: Update to non-legacy version.
OpenGroupAPI.legacyUpload(
data,
to: openGroup.room,
on: openGroup.server
)
OpenGroupAPI
.uploadFile(
data.bytes,
to: openGroup.room,
on: openGroup.server
)
.map { _, response -> UInt64 in response.id }
},
encrypt: false,
onSuccess: { seal.fulfill(()) },
onSuccess: { fileId in seal.fulfill(fileId) },
onFailure: { seal.reject($0) }
)
return promise
} else {
let (promise, seal) = Promise<Void>.pending()
AttachmentUploadJob.upload(stream, using: FileServerAPIV2.upload, encrypt: true, onSuccess: { seal.fulfill(()) }, onFailure: { seal.reject($0) })
return promise
}
let (promise, seal) = Promise<UInt64>.pending()
AttachmentUploadJob.upload(
stream,
using: FileServerAPIV2.upload,
encrypt: true,
onSuccess: { fileId in seal.fulfill(fileId) },
onFailure: { seal.reject($0) }
)
return promise
}
return when(resolved: attachmentUploadPromises).then(on: DispatchQueue.global(qos: .userInitiated)) { results -> Promise<Void> in
let errors = results.compactMap { result -> Swift.Error? in
if case .rejected(let error) = result { return error } else { return nil }
return when(resolved: attachmentUploadPromises)
.then(on: DispatchQueue.global(qos: .userInitiated)) { results -> Promise<Void> in
let errors = results.compactMap { result -> Swift.Error? in
if case .rejected(let error) = result { return error } else { return nil }
}
if let error = errors.first { return Promise(error: error) }
let fileIds: [UInt64] = results.compactMap { result -> UInt64? in
switch result {
case .fulfilled(let fileId): return fileId
default: return nil
}
}
return sendNonDurably(message, in: thread, with: fileIds, using: transaction)
}
if let error = errors.first { return Promise(error: error) }
return sendNonDurably(message, in: thread, using: transaction)
}
}
public static func sendNonDurably(_ message: Message, in thread: TSThread, using transaction: YapDatabaseReadWriteTransaction) -> Promise<Void> {
public static func sendNonDurably(_ message: Message, in thread: TSThread, with fileIds: [UInt64]? = nil, using transaction: YapDatabaseReadWriteTransaction) -> Promise<Void> {
message.threadID = thread.uniqueId!
let destination = Message.Destination.from(thread)
let destination = Message.Destination.from(thread, fileIds: fileIds)
return MessageSender.send(message, to: destination, using: transaction)
}
public static func sendNonDurably(_ message: VisibleMessage, with attachments: [SignalAttachment], in thread: TSThread) -> Promise<Void> {
Storage.writeSync{ transaction in
Storage.writeSync { transaction in
prep(attachments, for: message, using: transaction)
}
let attachments = message.attachmentIDs.compactMap { TSAttachment.fetch(uniqueId: $0) as? TSAttachmentStream }
let attachmentsToUpload = attachments.filter { !$0.isUploaded }
let attachmentUploadPromises: [Promise<Void>] = attachmentsToUpload.map { stream in
let attachmentUploadPromises: [Promise<UInt64>] = attachmentsToUpload.map { stream in
let storage = SNMessagingKitConfiguration.shared.storage
if let openGroup = storage.getOpenGroup(for: thread.uniqueId!) {
let (promise, seal) = Promise<Void>.pending()
let (promise, seal) = Promise<UInt64>.pending()
AttachmentUploadJob.upload(
stream,
using: { data in
// TODO: Update to non-legacy version
OpenGroupAPI.legacyUpload(
data,
to: openGroup.room,
on: openGroup.server
)
OpenGroupAPI
.uploadFile(
data.bytes,
to: openGroup.room,
on: openGroup.server
)
.map { _, response in response.id }
},
encrypt: false,
onSuccess: { seal.fulfill(()) },
onSuccess: { fileId in seal.fulfill(fileId) },
onFailure: { seal.reject($0) }
)
return promise
} else {
let (promise, seal) = Promise<Void>.pending()
AttachmentUploadJob.upload(stream, using: FileServerAPIV2.upload, encrypt: true, onSuccess: { seal.fulfill(()) }, onFailure: { seal.reject($0) })
return promise
}
let (promise, seal) = Promise<UInt64>.pending()
AttachmentUploadJob.upload(
stream,
using: FileServerAPIV2.upload,
encrypt: true,
onSuccess: { fileId in seal.fulfill(fileId) },
onFailure: { seal.reject($0) }
)
return promise
}
let (promise, seal) = Promise<Void>.pending()
let results = when(resolved: attachmentUploadPromises).wait()
@ -119,13 +148,23 @@ extension MessageSender {
if case .rejected(let error) = result { return error } else { return nil }
}
if let error = errors.first { seal.reject(error) }
Storage.write{ transaction in
sendNonDurably(message, in: thread, using: transaction).done {
seal.fulfill(())
}.catch { error in
seal.reject(error)
let fileIds: [UInt64] = results.compactMap { result -> UInt64? in
switch result {
case .fulfilled(let fileId): return fileId
default: return nil
}
}
Storage.write { transaction in
sendNonDurably(message, in: thread, with: fileIds, using: transaction)
.done {
seal.fulfill(())
}
.catch { error in
seal.reject(error)
}
}
return promise
}
}