mirror of https://github.com/oxen-io/session-ios
You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1635 lines
80 KiB
Swift
1635 lines
80 KiB
Swift
import PromiseKit
|
|
import SessionSnodeKit
|
|
import Sodium
|
|
import Curve25519Kit
|
|
|
|
@objc(SNOpenGroupAPI)
|
|
public final class OpenGroupAPI: NSObject {
|
|
|
|
// MARK: - Settings
|
|
|
|
public static let defaultServer = "http://116.203.70.33"
|
|
public static let defaultServerPublicKey = "a03c383cf63c3c4efe67acc52112a6dd734b3a946b9545f488aaa93da7991238"
|
|
|
|
public static let workQueue = DispatchQueue(label: "OpenGroupAPI.workQueue", qos: .userInitiated) // It's important that this is a serial queue
|
|
|
|
// MARK: - Polling State
|
|
|
|
private static var hasPerformedInitialPoll: Atomic<[String: Bool]> = Atomic([:])
|
|
private static var timeSinceLastPoll: Atomic<[String: TimeInterval]> = Atomic([:])
|
|
private static var lastPollTime: Atomic<TimeInterval> = Atomic(.greatestFiniteMagnitude)
|
|
|
|
private static let timeSinceLastOpen: Atomic<TimeInterval> = {
|
|
guard let lastOpen = UserDefaults.standard[.lastOpen] else { return Atomic(.greatestFiniteMagnitude) }
|
|
|
|
return Atomic(Date().timeIntervalSince(lastOpen))
|
|
}()
|
|
|
|
|
|
// TODO: Remove these
|
|
private static var legacyAuthTokenPromises: Atomic<[String: Promise<String>]> = Atomic([:])
|
|
private static var legacyHasUpdatedLastOpenDate = false
|
|
private static var legacyGroupImagePromises: [String: Promise<Data>] = [:]
|
|
|
|
|
|
// MARK: - Batching & Polling
|
|
|
|
/// This is a convenience method which calls `/batch` with a pre-defined set of requests used to update an Open
|
|
/// Group, currently this will retrieve:
|
|
/// - Capabilities for the server
|
|
/// - For each room:
|
|
/// - Poll Info
|
|
/// - Messages (includes additions and deletions)
|
|
/// - Inbox for the server
|
|
/// - Outbox for the server
|
|
public static func poll(_ server: String, using dependencies: Dependencies = Dependencies()) -> Promise<[Endpoint: (OnionRequestResponseInfoType, Codable?)]> {
|
|
// Store a local copy of the cached state for this server
|
|
let hadPerformedInitialPoll: Bool = (hasPerformedInitialPoll.wrappedValue[server] == true)
|
|
let originalTimeSinceLastPoll: TimeInterval = (timeSinceLastPoll.wrappedValue[server] ?? min(lastPollTime.wrappedValue, timeSinceLastOpen.wrappedValue))
|
|
let maybeLastInboxMessageId: Int64? = dependencies.storage.getOpenGroupInboxLatestMessageId(for: server)
|
|
let maybeLastOutboxMessageId: Int64? = dependencies.storage.getOpenGroupOutboxLatestMessageId(for: server)
|
|
let lastInboxMessageId: Int64 = (maybeLastInboxMessageId ?? 0)
|
|
let lastOutboxMessageId: Int64 = (maybeLastOutboxMessageId ?? 0)
|
|
|
|
// Update the cached state for this server
|
|
hasPerformedInitialPoll.mutate { $0[server] = true }
|
|
lastPollTime.mutate { $0 = min($0, timeSinceLastOpen.wrappedValue)}
|
|
UserDefaults.standard[.lastOpen] = Date()
|
|
|
|
// Generate the requests
|
|
let requestResponseType: [BatchRequestInfoType] = [
|
|
BatchRequestInfo(
|
|
request: Request<NoBody>(
|
|
server: server,
|
|
endpoint: .capabilities
|
|
),
|
|
responseType: Capabilities.self
|
|
)
|
|
]
|
|
.appending(
|
|
// Per-room requests
|
|
dependencies.storage.getAllOpenGroups().values
|
|
.filter { $0.server == server.lowercased() } // Note: The `OpenGroup` type converts to lowercase in init
|
|
.flatMap { openGroup -> [BatchRequestInfoType] in
|
|
let lastSeqNo: Int64? = dependencies.storage.getOpenGroupSequenceNumber(for: openGroup.room, on: server)
|
|
let targetSeqNo: Int64 = (lastSeqNo ?? 0)
|
|
let shouldRetrieveRecentMessages: Bool = (
|
|
lastSeqNo == nil || (
|
|
// If it's the first poll for this launch and it's been longer than
|
|
// 'maxInactivityPeriod' then just retrieve recent messages instead
|
|
// of trying to get all messages since the last one retrieved
|
|
!hadPerformedInitialPoll &&
|
|
originalTimeSinceLastPoll > OpenGroupAPI.Poller.maxInactivityPeriod
|
|
)
|
|
)
|
|
|
|
return [
|
|
BatchRequestInfo(
|
|
request: Request<NoBody>(
|
|
server: server,
|
|
endpoint: .roomPollInfo(openGroup.room, openGroup.infoUpdates)
|
|
),
|
|
responseType: RoomPollInfo.self
|
|
),
|
|
BatchRequestInfo(
|
|
request: Request<NoBody>(
|
|
server: server,
|
|
endpoint: (shouldRetrieveRecentMessages ?
|
|
.roomMessagesRecent(openGroup.room) :
|
|
.roomMessagesSince(openGroup.room, seqNo: targetSeqNo)
|
|
)
|
|
),
|
|
responseType: [Message].self
|
|
)
|
|
]
|
|
}
|
|
)
|
|
.appending([
|
|
// Inbox
|
|
BatchRequestInfo(
|
|
request: Request<NoBody>(
|
|
server: server,
|
|
endpoint: (maybeLastInboxMessageId == nil ?
|
|
.inbox :
|
|
.inboxSince(id: lastInboxMessageId)
|
|
)
|
|
),
|
|
responseType: [DirectMessage]?.self // 'inboxSince' will return a `304` with an empty response if no messages
|
|
),
|
|
|
|
// Outbox
|
|
BatchRequestInfo(
|
|
request: Request<NoBody>(
|
|
server: server,
|
|
endpoint: (maybeLastOutboxMessageId == nil ?
|
|
.outbox :
|
|
.outboxSince(id: lastOutboxMessageId)
|
|
)
|
|
),
|
|
responseType: [DirectMessage]?.self // 'outboxSince' will return a `304` with an empty response if no messages
|
|
)
|
|
])
|
|
|
|
return batch(server, requests: requestResponseType, using: dependencies)
|
|
}
|
|
|
|
/// Submits multiple requests wrapped up in a single request, runs them all, then returns the result of each one
|
|
///
|
|
/// Requests are performed independently, that is, if one fails the others will still be attempted - there is no guarantee on the order in which requests will be
|
|
/// carried out (for sequential, related requests invoke via `/sequence` instead)
|
|
///
|
|
/// For contained subrequests that specify a body (i.e. POST or PUT requests) exactly one of `json`, `b64`, or `bytes` must be provided with the request body.
|
|
private static func batch(_ server: String, requests: [BatchRequestInfoType], using dependencies: Dependencies = Dependencies()) -> Promise<[Endpoint: (OnionRequestResponseInfoType, Codable?)]> {
|
|
let requestBody: BatchRequest = requests.map { $0.toSubRequest() }
|
|
let responseTypes = requests.map { $0.responseType }
|
|
|
|
let request: Request = Request(
|
|
method: .post,
|
|
server: server,
|
|
endpoint: .batch,
|
|
body: requestBody
|
|
)
|
|
|
|
return send(request, using: dependencies)
|
|
.decoded(as: responseTypes, on: OpenGroupAPI.workQueue, error: Error.parsingFailed, using: dependencies)
|
|
.map { result in
|
|
result.enumerated()
|
|
.reduce(into: [:]) { prev, next in
|
|
prev[requests[next.offset].endpoint] = next.element
|
|
}
|
|
}
|
|
}
|
|
|
|
/// This is like `/batch`, except that it guarantees to perform requests sequentially in the order provided and will stop processing requests if the previous request
|
|
/// returned a non-`2xx` response
|
|
///
|
|
/// For example, this can be used to ban and delete all of a user's messages by sequencing the ban followed by the `delete_all`: if the ban fails (e.g. because
|
|
/// permission is denied) then the `delete_all` will not occur. The batch body and response are identical to the `/batch` endpoint; requests that are not
|
|
/// carried out because of an earlier failure will have a response code of `412` (Precondition Failed)."
|
|
///
|
|
/// Like `/batch`, responses are returned in the same order as requests, but unlike `/batch` there may be fewer elements in the response list (if requests were
|
|
/// stopped because of a non-2xx response) - In such a case, the final, non-2xx response is still included as the final response value
|
|
private static func sequence(_ server: String, requests: [BatchRequestInfoType], using dependencies: Dependencies = Dependencies()) -> Promise<[Endpoint: (OnionRequestResponseInfoType, Codable?)]> {
|
|
let requestBody: BatchRequest = requests.map { $0.toSubRequest() }
|
|
let responseTypes = requests.map { $0.responseType }
|
|
|
|
let request: Request = Request(
|
|
method: .post,
|
|
server: server,
|
|
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)
|
|
.map { result in
|
|
result.enumerated()
|
|
.reduce(into: [:]) { prev, next in
|
|
prev[requests[next.offset].endpoint] = next.element
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Capabilities
|
|
|
|
/// Return the list of server features/capabilities
|
|
///
|
|
/// Optionally takes a `required` parameter containing a comma-separated list of capabilites; if any are not satisfied a 412 (Precondition Failed) response
|
|
/// will be returned with missing requested capabilities in the `missing` key
|
|
///
|
|
/// Eg. `GET /capabilities` could return `{"capabilities": ["sogs", "batch"]}` `GET /capabilities?required=magic,batch`
|
|
/// could return: `{"capabilities": ["sogs", "batch"], "missing": ["magic"]}`
|
|
public static func capabilities(on server: String, using dependencies: Dependencies = Dependencies()) -> Promise<(OnionRequestResponseInfoType, Capabilities)> {
|
|
let request: Request = Request<NoBody>(
|
|
server: server,
|
|
endpoint: .capabilities,
|
|
queryParameters: [:] // TODO: Add any requirements '.required'.
|
|
)
|
|
|
|
// 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)
|
|
}
|
|
|
|
// MARK: - Room
|
|
|
|
/// Returns a list of available rooms on the server
|
|
///
|
|
/// Rooms to which the user does not have access (e.g. because they are banned, or the room has restricted access permissions) are not included
|
|
public static func rooms(for server: String, using dependencies: Dependencies = Dependencies()) -> Promise<(OnionRequestResponseInfoType, [Room])> {
|
|
let request: Request = Request<NoBody>(
|
|
server: server,
|
|
endpoint: .rooms
|
|
)
|
|
|
|
return send(request, using: dependencies)
|
|
.decoded(as: [Room].self, on: OpenGroupAPI.workQueue, error: Error.parsingFailed, using: dependencies)
|
|
}
|
|
|
|
/// Returns the details of a single room
|
|
public static func room(for roomToken: String, on server: String, using dependencies: Dependencies = Dependencies()) -> Promise<(OnionRequestResponseInfoType, Room)> {
|
|
let request: Request = Request<NoBody>(
|
|
server: server,
|
|
endpoint: .room(roomToken)
|
|
)
|
|
|
|
return send(request, using: dependencies)
|
|
.decoded(as: Room.self, on: OpenGroupAPI.workQueue, error: Error.parsingFailed, using: dependencies)
|
|
}
|
|
|
|
/// Polls a room for metadata updates
|
|
///
|
|
/// The endpoint polls room metadata for this room, always including the instantaneous room details (such as the user's permission and current
|
|
/// number of active users), and including the full room metadata if the room's info_updated counter has changed from the provided value
|
|
///
|
|
/// **Note:** This is the direct request to retrieve room updates so should be retrieved automatically from the `poll()` method, in order to call
|
|
/// this directly remove the `@available` line and make sure to route the response of this method to the `OpenGroupManager.handlePollInfo`
|
|
/// method to ensure things are processed correctly
|
|
@available(*, unavailable, message: "Avoid using this directly, use the pre-build `poll()` method instead")
|
|
public static func roomPollInfo(lastUpdated: Int64, for roomToken: String, on server: String, using dependencies: Dependencies = Dependencies()) -> Promise<(OnionRequestResponseInfoType, RoomPollInfo)> {
|
|
let request: Request = Request<NoBody>(
|
|
server: server,
|
|
endpoint: .roomPollInfo(roomToken, lastUpdated)
|
|
)
|
|
|
|
return send(request, using: dependencies)
|
|
.decoded(as: RoomPollInfo.self, on: OpenGroupAPI.workQueue, error: Error.parsingFailed, using: dependencies)
|
|
}
|
|
|
|
// MARK: - Messages
|
|
|
|
/// Posts a new message to a room
|
|
public static func send(
|
|
_ plaintext: Data,
|
|
to roomToken: String,
|
|
on server: String,
|
|
whisperTo: String?,
|
|
whisperMods: Bool,
|
|
using dependencies: Dependencies = Dependencies()
|
|
) -> Promise<(OnionRequestResponseInfoType, Message)> {
|
|
guard let signResult: (publicKey: String, signature: Bytes) = sign(plaintext.bytes, for: server, using: dependencies) else {
|
|
return Promise(error: Error.signingFailed)
|
|
}
|
|
|
|
let requestBody: SendMessageRequest = SendMessageRequest(
|
|
data: plaintext,
|
|
signature: Data(signResult.signature),
|
|
whisperTo: whisperTo,
|
|
whisperMods: whisperMods,
|
|
fileIds: nil // TODO: Add support for 'fileIds'.
|
|
)
|
|
|
|
let request = Request(
|
|
method: .post,
|
|
server: server,
|
|
endpoint: .roomMessage(roomToken),
|
|
body: requestBody
|
|
)
|
|
|
|
return send(request, using: dependencies)
|
|
.decoded(as: Message.self, on: OpenGroupAPI.workQueue, error: Error.parsingFailed, using: dependencies)
|
|
}
|
|
|
|
/// Returns a single message by ID
|
|
public static func message(_ id: Int64, in roomToken: String, on server: String, using dependencies: Dependencies = Dependencies()) -> Promise<(OnionRequestResponseInfoType, Message)> {
|
|
let request: Request = Request<NoBody>(
|
|
server: server,
|
|
endpoint: .roomMessageIndividual(roomToken, id: id)
|
|
)
|
|
|
|
return send(request, using: dependencies)
|
|
.decoded(as: Message.self, on: OpenGroupAPI.workQueue, error: Error.parsingFailed, using: dependencies)
|
|
}
|
|
|
|
/// Edits a message, replacing its existing content with new content and a new signature
|
|
///
|
|
/// **Note:** This edit may only be initiated by the creator of the post, and the poster must currently have write permissions in the room
|
|
public static func messageUpdate(
|
|
_ id: Int64,
|
|
plaintext: Data,
|
|
fileIds: [Int64]?,
|
|
in roomToken: String,
|
|
on server: String,
|
|
using dependencies: Dependencies = Dependencies()
|
|
) -> Promise<(OnionRequestResponseInfoType, Data?)> {
|
|
guard let signResult: (publicKey: String, signature: Bytes) = sign(plaintext.bytes, for: server, using: dependencies) else {
|
|
return Promise(error: Error.signingFailed)
|
|
}
|
|
|
|
let requestBody: UpdateMessageRequest = UpdateMessageRequest(
|
|
data: plaintext,
|
|
signature: Data(signResult.signature),
|
|
fileIds: fileIds
|
|
)
|
|
|
|
let request: Request = Request(
|
|
method: .put,
|
|
server: server,
|
|
endpoint: .roomMessageIndividual(roomToken, id: id),
|
|
body: requestBody
|
|
)
|
|
|
|
// TODO: Handle custom response info?
|
|
return send(request, using: dependencies)
|
|
}
|
|
|
|
// TODO: Need to test this once the API has been implemented
|
|
public static func messageDelete(
|
|
_ id: Int64,
|
|
in roomToken: String,
|
|
on server: String,
|
|
using dependencies: Dependencies = Dependencies()
|
|
) -> Promise<(OnionRequestResponseInfoType, Data?)> {
|
|
let request: Request = Request<NoBody>(
|
|
method: .delete,
|
|
server: server,
|
|
endpoint: .roomMessageIndividual(roomToken, id: id)
|
|
)
|
|
|
|
// TODO: Handle custom response info? Need to let the OpenGroupManager know to delete the message?
|
|
// TODO: !!!! This is currently broken - looks like there isn't currently a DELETE endpoint (but there should be)
|
|
return send(request, using: dependencies)
|
|
.map { response in
|
|
print("RAWR")
|
|
return response
|
|
}
|
|
}
|
|
|
|
/// **Note:** This is the direct request to retrieve recent messages so should be retrieved automatically from the `poll()` method, in order to call
|
|
/// this directly remove the `@available` line and make sure to route the response of this method to the `OpenGroupManager.handleMessages`
|
|
/// method to ensure things are processed correctly
|
|
@available(*, unavailable, message: "Avoid using this directly, use the pre-build `poll()` method instead")
|
|
public static func recentMessages(in roomToken: String, on server: String, using dependencies: Dependencies = Dependencies()) -> Promise<(OnionRequestResponseInfoType, [Message])> {
|
|
let request: Request = Request<NoBody>(
|
|
server: server,
|
|
endpoint: .roomMessagesRecent(roomToken)
|
|
// TODO: Limit?.
|
|
// queryParameters: [ .limit: 50 ]
|
|
)
|
|
|
|
return send(request, using: dependencies)
|
|
.decoded(as: [Message].self, on: OpenGroupAPI.workQueue, error: Error.parsingFailed, 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
|
|
/// remove the `@available` line and make sure to route the response of this method to the `OpenGroupManager.handleMessages`
|
|
/// method to ensure things are processed correctly
|
|
@available(*, unavailable, message: "Avoid using this directly, use the pre-build `poll()` method instead")
|
|
public static func messagesBefore(messageId: Int64, in roomToken: String, on server: String, using dependencies: Dependencies = Dependencies()) -> Promise<(OnionRequestResponseInfoType, [Message])> {
|
|
// TODO: Do we need to be able to load old messages?
|
|
let request: Request = Request<NoBody>(
|
|
server: server,
|
|
endpoint: .roomMessagesBefore(roomToken, id: messageId)
|
|
// TODO: Limit?.
|
|
// queryParameters: [ .limit: 50 ]
|
|
)
|
|
|
|
return send(request, using: dependencies)
|
|
.decoded(as: [Message].self, on: OpenGroupAPI.workQueue, error: Error.parsingFailed, using: dependencies)
|
|
}
|
|
|
|
/// **Note:** This is the direct request to retrieve messages since a given message `seqNo` so should be retrieved automatically from the
|
|
/// `poll()` method, in order to call this directly remove the `@available` line and make sure to route the response of this method to the
|
|
/// `OpenGroupManager.handleMessages` method to ensure things are processed correctly
|
|
@available(*, unavailable, message: "Avoid using this directly, use the pre-build `poll()` method instead")
|
|
public static func messagesSince(seqNo: Int64, in roomToken: String, on server: String, using dependencies: Dependencies = Dependencies()) -> Promise<(OnionRequestResponseInfoType, [Message])> {
|
|
let request: Request = Request<NoBody>(
|
|
server: server,
|
|
endpoint: .roomMessagesSince(roomToken, seqNo: seqNo)
|
|
// TODO: Limit?.
|
|
// queryParameters: [ .limit: 50 ]
|
|
)
|
|
|
|
return send(request, using: dependencies)
|
|
.decoded(as: [Message].self, on: OpenGroupAPI.workQueue, error: Error.parsingFailed, using: dependencies)
|
|
}
|
|
|
|
// MARK: - Pinning
|
|
|
|
/// Adds a pinned message to this room
|
|
///
|
|
/// **Note:** Existing pinned messages are not removed: the new message is added to the pinned message list (If you want to remove existing
|
|
/// pins then build a sequence request that first calls .../unpin/all)
|
|
///
|
|
/// The user must have admin (not just moderator) permissions in the room in order to pin messages
|
|
///
|
|
/// Pinned messages that are already pinned will be re-pinned (that is, their pin timestamp and pinning admin user will be updated) - because pinned
|
|
/// messages are returned in pinning-order this allows admins to order multiple pinned messages in a room by re-pinning (via this endpoint) in the
|
|
/// order in which pinned messages should be displayed
|
|
public static func pinMessage(id: Int64, in roomToken: String, on server: String, using dependencies: Dependencies = Dependencies()) -> Promise<OnionRequestResponseInfoType> {
|
|
let request: Request = Request<NoBody>(
|
|
method: .post,
|
|
server: server,
|
|
endpoint: .roomPinMessage(roomToken, id: id)
|
|
)
|
|
|
|
return send(request, using: dependencies)
|
|
.map { responseInfo, _ in responseInfo }
|
|
}
|
|
|
|
/// Remove a message from this room's pinned message list
|
|
///
|
|
/// The user must have `admin` (not just `moderator`) permissions in the room
|
|
public static func unpinMessage(id: Int64, in roomToken: String, on server: String, using dependencies: Dependencies = Dependencies()) -> Promise<OnionRequestResponseInfoType> {
|
|
let request: Request = Request<NoBody>(
|
|
method: .post,
|
|
server: server,
|
|
endpoint: .roomUnpinMessage(roomToken, id: id)
|
|
)
|
|
|
|
return send(request, using: dependencies)
|
|
.map { responseInfo, _ in responseInfo }
|
|
}
|
|
|
|
/// Removes _all_ pinned messages from this room
|
|
///
|
|
/// The user must have `admin` (not just `moderator`) permissions in the room
|
|
public static func unpinAll(in roomToken: String, on server: String, using dependencies: Dependencies = Dependencies()) -> Promise<OnionRequestResponseInfoType> {
|
|
let request: Request = Request<NoBody>(
|
|
method: .post,
|
|
server: server,
|
|
endpoint: .roomUnpinAll(roomToken)
|
|
)
|
|
|
|
return send(request, using: dependencies)
|
|
.map { responseInfo, _ in responseInfo }
|
|
}
|
|
|
|
// MARK: - Files
|
|
|
|
public static func uploadFile(_ bytes: [UInt8], 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: .roomFile(roomToken),
|
|
headers: [ .fileName: fileName ].compactMapValues { $0 },
|
|
body: bytes
|
|
)
|
|
|
|
return send(request, using: dependencies)
|
|
.decoded(as: FileUploadResponse.self, on: OpenGroupAPI.workQueue, error: Error.parsingFailed, 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: [ .fileName: fileName ].compactMapValues { $0 },
|
|
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>(
|
|
server: server,
|
|
endpoint: .roomFileIndividual(roomToken, fileId)
|
|
)
|
|
|
|
return send(request, using: dependencies)
|
|
.map { responseInfo, maybeData in
|
|
guard let data: Data = maybeData else { throw 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>(
|
|
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)
|
|
}
|
|
|
|
// MARK: - Inbox/Outbox (Message Requests)
|
|
|
|
/// Retrieves all of the user's current DMs (up to limit)
|
|
///
|
|
/// **Note:** This is the direct request to retrieve DMs for a specific Open Group so should be retrieved automatically from the `poll()`
|
|
/// method, in order to call this directly remove the `@available` line and make sure to route the response of this method to the
|
|
/// `OpenGroupManager.handleDirectMessages` method to ensure things are processed correctly
|
|
@available(*, unavailable, message: "Avoid using this directly, use the pre-build `poll()` method instead")
|
|
public static func inbox(on server: String, using dependencies: Dependencies = Dependencies()) -> Promise<(OnionRequestResponseInfoType, [DirectMessage]?)> {
|
|
let request: Request = Request<NoBody>(
|
|
server: server,
|
|
endpoint: .inbox
|
|
)
|
|
|
|
return send(request, using: dependencies)
|
|
.decoded(as: [DirectMessage]?.self, on: OpenGroupAPI.workQueue, error: Error.parsingFailed, 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
|
|
///
|
|
/// **Note:** This is the direct request to retrieve messages requests for a specific Open Group since a given messages so should be retrieved
|
|
/// automatically from the `poll()` method, in order to call this directly remove the `@available` line and make sure to route the response
|
|
/// of this method to the `OpenGroupManager.handleDirectMessages` method to ensure things are processed correctly
|
|
@available(*, unavailable, message: "Avoid using this directly, use the pre-build `poll()` method instead")
|
|
public static func inboxSince(id: Int64, on server: String, using dependencies: Dependencies = Dependencies()) -> Promise<(OnionRequestResponseInfoType, [DirectMessage]?)> {
|
|
let request: Request = Request<NoBody>(
|
|
server: server,
|
|
endpoint: .inboxSince(id: id)
|
|
)
|
|
|
|
return send(request, using: dependencies)
|
|
.decoded(as: [DirectMessage]?.self, on: OpenGroupAPI.workQueue, error: Error.parsingFailed, using: dependencies)
|
|
}
|
|
|
|
/// Delivers a direct message to a user via their blinded Session ID
|
|
///
|
|
/// The body of this request is a JSON object containing a message key with a value of the encrypted-then-base64-encoded message to deliver
|
|
public static func send(_ ciphertext: Data, toInboxFor blindedSessionId: String, on server: String/*, with serverPublicKey: String*/, using dependencies: Dependencies = Dependencies()) -> Promise<(OnionRequestResponseInfoType, Data?)> {
|
|
let requestBody: SendDirectMessageRequest = SendDirectMessageRequest(
|
|
message: ciphertext
|
|
)
|
|
|
|
let request: Request = Request(
|
|
method: .post,
|
|
server: server,
|
|
endpoint: .inboxFor(sessionId: blindedSessionId),
|
|
body: requestBody
|
|
)
|
|
|
|
return send(request, using: dependencies)
|
|
}
|
|
|
|
/// Retrieves all of the user's sent DMs (up to limit)
|
|
///
|
|
/// **Note:** This is the direct request to retrieve DMs sent by the user for a specific Open Group so should be retrieved automatically
|
|
/// from the `poll()` method, in order to call this directly remove the `@available` line and make sure to route the response of
|
|
/// this method to the `OpenGroupManager.handleDirectMessages` method to ensure things are processed correctly
|
|
@available(*, unavailable, message: "Avoid using this directly, use the pre-build `poll()` method instead")
|
|
public static func outbox(on server: String, using dependencies: Dependencies = Dependencies()) -> Promise<(OnionRequestResponseInfoType, [DirectMessage]?)> {
|
|
let request: Request = Request<NoBody>(
|
|
server: server,
|
|
endpoint: .outbox
|
|
)
|
|
|
|
return send(request, using: dependencies)
|
|
.decoded(as: [DirectMessage]?.self, on: OpenGroupAPI.workQueue, error: Error.parsingFailed, 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
|
|
///
|
|
/// **Note:** This is the direct request to retrieve messages requests sent by the user for a specific Open Group since a given messages so
|
|
/// should be retrieved automatically from the `poll()` method, in order to call this directly remove the `@available` line and make sure
|
|
/// to route the response of this method to the `OpenGroupManager.handleDirectMessages` method to ensure things are processed correctly
|
|
@available(*, unavailable, message: "Avoid using this directly, use the pre-build `poll()` method instead")
|
|
public static func outboxSince(id: Int64, on server: String, using dependencies: Dependencies = Dependencies()) -> Promise<(OnionRequestResponseInfoType, [DirectMessage]?)> {
|
|
let request: Request = Request<NoBody>(
|
|
server: server,
|
|
endpoint: .outboxSince(id: id)
|
|
)
|
|
|
|
return send(request, using: dependencies)
|
|
.decoded(as: [DirectMessage]?.self, on: OpenGroupAPI.workQueue, error: Error.parsingFailed, using: dependencies)
|
|
}
|
|
|
|
// MARK: - Users
|
|
|
|
/// Applies a ban of a user from specific rooms, or from the server globally
|
|
///
|
|
/// The invoking user must have `moderator` (or `admin`) permission in all given rooms when specifying rooms, and must be a
|
|
/// `globalModerator` (or `globalAdmin`) if using the global parameter
|
|
///
|
|
/// **Note:** The user's messages are not deleted by this request - In order to ban and delete all messages use the `/sequence` endpoint to
|
|
/// bundle a `/user/.../ban` with a `/user/.../deleteMessages` request
|
|
///
|
|
/// - Parameters:
|
|
/// - sessionId: The sessionId (either standard or blinded) of the user whose messages should be deleted
|
|
///
|
|
/// - timeout: Value specifying a time limit on the ban, in seconds
|
|
///
|
|
/// The applied ban will expire and be removed after the given interval - If omitted (or `null`) then the ban is permanent
|
|
///
|
|
/// If this endpoint is called multiple times then the timeout of the last call takes effect (eg. a permanent ban can be replaced
|
|
/// with a time-limited ban by calling the endpoint again with a timeout value, and vice versa)
|
|
///
|
|
/// - roomTokens: List of one or more room tokens from which the user should be banned from
|
|
///
|
|
/// The invoking user **must** be a moderator of all of the given rooms.
|
|
///
|
|
/// This may be set to the single-element list `["*"]` to ban the user from all rooms in which the current user has moderator
|
|
/// permissions (the call will succeed if the calling user is a moderator in at least one channel)
|
|
///
|
|
/// **Note:** You can ban from all rooms on a server by providing a `nil` value for this parameter (the invoking user must be a
|
|
/// global moderator in order to add a global ban)
|
|
///
|
|
/// - server: The server to delete messages from
|
|
///
|
|
/// - dependencies: Injected dependencies (used for unit testing)
|
|
public static func userBan(
|
|
_ sessionId: String,
|
|
for timeout: TimeInterval? = nil,
|
|
from roomTokens: [String]? = nil,
|
|
on server: String,
|
|
using dependencies: Dependencies = Dependencies()
|
|
) -> Promise<(OnionRequestResponseInfoType, Data?)> {
|
|
let requestBody: UserBanRequest = UserBanRequest(
|
|
rooms: roomTokens,
|
|
global: (roomTokens == nil ? true : nil),
|
|
timeout: timeout
|
|
)
|
|
|
|
let request: Request = Request(
|
|
method: .post,
|
|
server: server,
|
|
endpoint: .userBan(sessionId),
|
|
body: requestBody
|
|
)
|
|
|
|
return send(request, using: dependencies)
|
|
}
|
|
|
|
/// Removes a user ban from specific rooms, or from the server globally
|
|
///
|
|
/// The invoking user must have `moderator` (or `admin`) permission in all given rooms when specifying rooms, and must be a global server `moderator`
|
|
/// (or `admin`) if using the `global` parameter
|
|
///
|
|
/// **Note:** Room and global bans are independent: if a user is banned globally and has a room-specific ban then removing the global ban does not remove
|
|
/// the room specific ban, and removing the room-specific ban does not remove the global ban (to fully unban a user globally and from all rooms, submit a
|
|
/// `/sequence` request with a global unban followed by a "rooms": ["*"] unban)
|
|
///
|
|
/// - Parameters:
|
|
/// - sessionId: The sessionId (either standard or blinded) of the user whose messages should be deleted
|
|
///
|
|
/// - roomTokens: List of one or more room tokens from which the user should be unbanned from
|
|
///
|
|
/// The invoking user **must** be a moderator of all of the given rooms.
|
|
///
|
|
/// This may be set to the single-element list `["*"]` to unban the user from all rooms in which the current user has moderator
|
|
/// permissions (the call will succeed if the calling user is a moderator in at least one channel)
|
|
///
|
|
/// **Note:** You can ban from all rooms on a server by providing a `nil` value for this parameter
|
|
///
|
|
/// - server: The server to delete messages from
|
|
///
|
|
/// - dependencies: Injected dependencies (used for unit testing)
|
|
public static func userUnban(
|
|
_ sessionId: String,
|
|
from roomTokens: [String]?,
|
|
on server: String,
|
|
using dependencies: Dependencies = Dependencies()
|
|
) -> Promise<(OnionRequestResponseInfoType, Data?)> {
|
|
let requestBody: UserUnbanRequest = UserUnbanRequest(
|
|
rooms: roomTokens,
|
|
global: (roomTokens == nil ? true : nil)
|
|
)
|
|
|
|
let request: Request = Request(
|
|
method: .post,
|
|
server: server,
|
|
endpoint: .userUnban(sessionId),
|
|
body: requestBody
|
|
)
|
|
|
|
return send(request, using: dependencies)
|
|
}
|
|
|
|
/// Appoints or removes a moderator or admin
|
|
///
|
|
/// This endpoint is used to appoint or remove moderator/admin permissions either for specific rooms or for server-wide global moderator permissions
|
|
///
|
|
/// Admins/moderators of rooms can only be appointed or removed by a user who has admin permissions in the room (including global admins)
|
|
///
|
|
/// Global admins/moderators may only be appointed by a global admin
|
|
///
|
|
/// The admin/moderator paramters interact as follows:
|
|
/// - **admin=true, moderator omitted:** This adds admin permissions, which automatically also implies moderator permissions
|
|
/// - **admin=true, moderator=true:** Exactly the same as above
|
|
/// - **admin=false, moderator=true:** Removes any existing admin permissions from the rooms (or globally), if present, and adds
|
|
/// moderator permissions to the rooms/globally (if not already present)
|
|
/// - **admin=false, moderator omitted:** This removes admin permissions but leaves moderator permissions, if present (this
|
|
/// effectively "downgrades" an admin to a moderator). Unlike the above this does **not** add moderator permissions to matching rooms
|
|
/// if not already present
|
|
/// - **moderator=true, admin omitted:** Adds moderator permissions to the given rooms (or globally), if not already present. If
|
|
/// the user already has admin permissions this does nothing (that is, admin permission is *not* removed, unlike the above)
|
|
/// - **moderator=false, admin omitted:** This removes moderator **and** admin permissions from all given rooms (or globally)
|
|
/// - **moderator=false, admin=false:** Exactly the same as above
|
|
/// - **moderator=false, admin=true:** This combination is **not permitted** (because admin permissions imply moderator
|
|
/// permissions) and will result in Bad Request error if given
|
|
///
|
|
/// - Parameters:
|
|
/// - sessionId: The sessionId (either standard or blinded) of the user to modify the permissions of
|
|
///
|
|
/// - moderator: Value indicating that this user should have moderator permissions added (true), removed (false), or left alone (null)
|
|
///
|
|
/// - admin: Value indicating that this user should have admin permissions added (true), removed (false), or left alone (null)
|
|
///
|
|
/// Granting admin permission automatically includes granting moderator permission (and thus it is an error to use admin=true with
|
|
/// moderator=false)
|
|
///
|
|
/// - visible: Value indicating whether the moderator/admin should be made publicly visible as a moderator/admin of the room(s)
|
|
/// (if true) or hidden (false)
|
|
///
|
|
/// Hidden moderators/admins still have all the same permissions as visible moderators/admins, but are visible only to other
|
|
/// moderators/admins; regular users in the room will not know their moderator status
|
|
///
|
|
/// - roomTokens: List of one or more room tokens to which the permission changes should be applied
|
|
///
|
|
/// The invoking user **must** be an admin of all of the given rooms.
|
|
///
|
|
/// This may be set to the single-element list `["*"]` to add or remove the moderator from all rooms in which the current user has admin
|
|
/// permissions (the call will succeed if the calling user is an admin in at least one channel)
|
|
///
|
|
/// **Note:** You can specify a change to global permisisons by providing a `nil` value for this parameter
|
|
///
|
|
/// - server: The server to perform the permission changes on
|
|
///
|
|
/// - dependencies: Injected dependencies (used for unit testing)
|
|
public static func userModeratorUpdate(
|
|
_ sessionId: String,
|
|
moderator: Bool? = nil,
|
|
admin: Bool? = nil,
|
|
visible: Bool,
|
|
for roomTokens: [String]?,
|
|
on server: String,
|
|
using dependencies: Dependencies = Dependencies()
|
|
) -> Promise<(OnionRequestResponseInfoType, Data?)> {
|
|
guard (moderator != nil && admin == nil) || (moderator == nil && admin != nil) else { return Promise(error: Error.generic) }
|
|
|
|
let requestBody: UserModeratorRequest = UserModeratorRequest(
|
|
rooms: roomTokens,
|
|
global: (roomTokens == nil ? true : nil),
|
|
moderator: moderator,
|
|
admin: admin,
|
|
visible: visible
|
|
)
|
|
|
|
let request: Request = Request(
|
|
method: .post,
|
|
server: server,
|
|
endpoint: .userModerator(sessionId),
|
|
body: requestBody
|
|
)
|
|
|
|
return send(request, using: dependencies)
|
|
}
|
|
|
|
// TODO: Need to test this once the API has been implemented
|
|
// TODO: Update docs to align with the API documentation once implemented
|
|
/// Deletes all messages from a given sessionId within the provided rooms (or globally) on a server
|
|
///
|
|
/// - Parameters:
|
|
/// - sessionId: The sessionId (either standard or blinded) of the user whose messages should be deleted
|
|
///
|
|
/// - roomTokens: List of one or more room tokens from which the messages should be deleted
|
|
///
|
|
/// The invoking user **must** be an admin of all of the given rooms.
|
|
///
|
|
/// This may be set to the single-element list `["*"]` to add or remove the moderator from all rooms in which the current user has admin
|
|
/// permissions (the call will succeed if the calling user is an admin in at least one channel)
|
|
///
|
|
/// **Note:** You can delete messages from all rooms on a server by providing a `nil` value for this parameter
|
|
///
|
|
/// - server: The server to delete messages from
|
|
///
|
|
/// - dependencies: Injected dependencies (used for unit testing)
|
|
public static func userDeleteMessages(
|
|
_ sessionId: String,
|
|
for roomTokens: [String]?,
|
|
on server: String,
|
|
using dependencies: Dependencies = Dependencies()
|
|
) -> Promise<(OnionRequestResponseInfoType, UserDeleteMessagesResponse)> {
|
|
let requestBody: UserDeleteMessagesRequest = UserDeleteMessagesRequest(
|
|
rooms: roomTokens,
|
|
global: (roomTokens == nil ? true : nil)
|
|
)
|
|
|
|
let request: Request = Request(
|
|
method: .post,
|
|
server: server,
|
|
endpoint: .userDeleteMessages(sessionId),
|
|
body: requestBody
|
|
)
|
|
|
|
return send(request, using: dependencies)
|
|
.decoded(as: UserDeleteMessagesResponse.self, on: OpenGroupAPI.workQueue, error: Error.parsingFailed, using: dependencies)
|
|
}
|
|
|
|
// TODO: Need to test this once the API has been implemented
|
|
/// This is a convenience method which constructs a `/sequence` of the `userBan` and `userDeleteMessages` requests, refer to those
|
|
/// methods for the documented behaviour of each method
|
|
public static func userBanAndDeleteAllMessage(
|
|
_ sessionId: String,
|
|
for roomTokens: [String]?,
|
|
on server: String,
|
|
using dependencies: Dependencies = Dependencies()
|
|
) -> Promise<[OnionRequestResponseInfoType]> {
|
|
let banRequestBody: UserBanRequest = UserBanRequest(
|
|
rooms: roomTokens,
|
|
global: (roomTokens == nil ? true : nil),
|
|
timeout: nil
|
|
)
|
|
let deleteMessageRequestBody: UserDeleteMessagesRequest = UserDeleteMessagesRequest(
|
|
rooms: roomTokens,
|
|
global: (roomTokens == nil ? true : nil)
|
|
)
|
|
|
|
// Generate the requests
|
|
let requestResponseType: [BatchRequestInfoType] = [
|
|
BatchRequestInfo(
|
|
request: Request(
|
|
method: .post,
|
|
server: server,
|
|
endpoint: .userBan(sessionId),
|
|
body: banRequestBody
|
|
)
|
|
),
|
|
BatchRequestInfo(
|
|
request: Request(
|
|
method: .post,
|
|
server: server,
|
|
endpoint: .userDeleteMessages(sessionId),
|
|
body: deleteMessageRequestBody
|
|
),
|
|
responseType: UserDeleteMessagesResponse.self
|
|
)
|
|
]
|
|
|
|
return sequence(server, requests: requestResponseType, using: dependencies)
|
|
.map { results in
|
|
// TODO: Handle deletions...???? Hand off to OpenGroupAPIManager?
|
|
return results.values.map { responseInfo, _ in responseInfo }
|
|
}
|
|
}
|
|
|
|
// MARK: - Authentication
|
|
|
|
/// Sign a message to be sent to SOGS (handles both un-blinded and blinded signing based on the server capabilities)
|
|
private static func sign(_ messageBytes: Bytes, for serverName: String, using dependencies: Dependencies = Dependencies()) -> (publicKey: String, signature: Bytes)? {
|
|
guard let userEdKeyPair: Box.KeyPair = dependencies.storage.getUserED25519KeyPair() else { return nil }
|
|
guard let serverPublicKey: String = dependencies.storage.getOpenGroupPublicKey(for: serverName) else {
|
|
return nil
|
|
}
|
|
|
|
let server: Server? = dependencies.storage.getOpenGroupServer(name: serverName)
|
|
|
|
// Check if the server supports blinded keys, if so then sign using the blinded key
|
|
if server?.capabilities.capabilities.contains(.blind) == true {
|
|
guard let blindedKeyPair: Box.KeyPair = dependencies.sodium.blindedKeyPair(serverPublicKey: serverPublicKey, edKeyPair: userEdKeyPair, genericHash: dependencies.genericHash) else {
|
|
return nil
|
|
}
|
|
|
|
guard let signatureResult: Bytes = dependencies.sodium.sogsSignature(message: messageBytes, secretKey: userEdKeyPair.secretKey, blindedSecretKey: blindedKeyPair.secretKey, blindedPublicKey: blindedKeyPair.publicKey) else {
|
|
return nil
|
|
}
|
|
|
|
return (
|
|
publicKey: SessionId(.blinded, publicKey: blindedKeyPair.publicKey).hexString,
|
|
signature: signatureResult
|
|
)
|
|
}
|
|
|
|
// Otherwise fall back to sign using the unblinded key
|
|
guard let signatureResult: Bytes = dependencies.sign.signature(message: messageBytes, secretKey: userEdKeyPair.secretKey) else {
|
|
return nil
|
|
}
|
|
|
|
return (
|
|
publicKey: SessionId(.unblinded, publicKey: userEdKeyPair.publicKey).hexString,
|
|
signature: signatureResult
|
|
)
|
|
}
|
|
|
|
/// Sign a request to be sent to SOGS (handles both un-blinded and blinded signing based on the server capabilities)
|
|
private static func sign(_ request: URLRequest, for serverName: String, with serverPublicKey: String, using dependencies: Dependencies = Dependencies()) -> URLRequest? {
|
|
guard let url: URL = request.url else { return nil }
|
|
|
|
var updatedRequest: URLRequest = request
|
|
let path: String = url.path
|
|
.appending(url.query.map { value in "?\(value)" })
|
|
let method: String = (request.httpMethod ?? "GET")
|
|
let timestamp: Int = Int(floor(dependencies.date.timeIntervalSince1970))
|
|
let nonce: Data = Data(dependencies.nonceGenerator16.nonce())
|
|
|
|
guard let serverPublicKeyData: Data = serverPublicKey.dataFromHex() else { return nil }
|
|
guard let timestampBytes: Bytes = "\(timestamp)".data(using: .ascii)?.bytes else { return nil }
|
|
|
|
/// 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
|
|
}
|
|
|
|
return dependencies.genericHash.hash(message: body.bytes, outputLength: 64)
|
|
}()
|
|
|
|
/// Generate the signature message
|
|
/// "ServerPubkey || Nonce || Timestamp || Method || Path || Blake2b Hash(Body)
|
|
/// `ServerPubkey`
|
|
/// `Nonce`
|
|
/// `Timestamp` is the bytes of an ascii decimal string
|
|
/// `Method`
|
|
/// `Path`
|
|
/// `Body` is a Blake2b hash of the data (if there is a body)
|
|
let messageBytes: Bytes = serverPublicKeyData.bytes
|
|
.appending(nonce.bytes)
|
|
.appending(timestampBytes)
|
|
.appending(method.bytes)
|
|
.appending(path.bytes)
|
|
.appending(bodyHash ?? [])
|
|
|
|
/// Sign the above message
|
|
guard let signResult: (publicKey: String, signature: Bytes) = sign(messageBytes, for: serverName, using: dependencies) else {
|
|
return nil
|
|
}
|
|
|
|
updatedRequest.allHTTPHeaderFields = (request.allHTTPHeaderFields ?? [:])
|
|
.updated(with: [
|
|
Header.sogsPubKey.rawValue: signResult.publicKey,
|
|
Header.sogsTimestamp.rawValue: "\(timestamp)",
|
|
Header.sogsNonce.rawValue: nonce.base64EncodedString(),
|
|
Header.sogsSignature.rawValue: signResult.signature.toBase64()
|
|
])
|
|
|
|
return updatedRequest
|
|
}
|
|
|
|
// 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()
|
|
|
|
do {
|
|
urlRequest.httpBody = try request.bodyData()
|
|
}
|
|
catch {
|
|
return Promise(error: Error.parsingFailed)
|
|
}
|
|
|
|
if request.useOnionRouting {
|
|
guard let publicKey = dependencies.storage.getOpenGroupPublicKey(for: request.server) else {
|
|
return Promise(error: Error.noPublicKey)
|
|
}
|
|
|
|
if request.isAuthRequired {
|
|
// Attempt to sign the request with the new auth
|
|
guard let signedRequest: URLRequest = sign(urlRequest, for: request.server, with: publicKey, using: dependencies) else {
|
|
return Promise(error: Error.signingFailed)
|
|
}
|
|
|
|
// TODO: 'removeAuthToken' as a migration??? (would previously do this when getting a `401`)
|
|
return dependencies.api.sendOnionRequest(signedRequest, to: request.server, with: publicKey)
|
|
}
|
|
|
|
return dependencies.api.sendOnionRequest(urlRequest, to: request.server, with: publicKey)
|
|
}
|
|
|
|
preconditionFailure("It's currently not allowed to send non onion routed requests.")
|
|
}
|
|
|
|
// MARK: -
|
|
// MARK: -
|
|
// MARK: - Legacy Requests (To be removed)
|
|
// TODO: Remove the legacy requests (should be unused once we release - just here for testing)
|
|
|
|
public static var legacyDefaultRoomsPromise: Promise<[LegacyRoomInfo]>?
|
|
|
|
// MARK: -- Legacy Auth
|
|
|
|
@available(*, deprecated, message: "Use request signing instead")
|
|
private static func legacyGetAuthToken(for room: String, on server: String) -> Promise<String> {
|
|
let storage = SNMessagingKitConfiguration.shared.storage
|
|
|
|
if let authToken: String = storage.getAuthToken(for: room, on: server) {
|
|
return Promise.value(authToken)
|
|
}
|
|
|
|
if let authTokenPromise: Promise<String> = legacyAuthTokenPromises.wrappedValue["\(server).\(room)"] {
|
|
return authTokenPromise
|
|
}
|
|
|
|
let promise: Promise<String> = legacyRequestNewAuthToken(for: room, on: server)
|
|
.then(on: OpenGroupAPI.workQueue) { legacyClaimAuthToken($0, for: room, on: server) }
|
|
.then(on: OpenGroupAPI.workQueue) { authToken -> Promise<String> in
|
|
let (promise, seal) = Promise<String>.pending()
|
|
storage.write(with: { transaction in
|
|
storage.setAuthToken(for: room, on: server, to: authToken, using: transaction)
|
|
}, completion: {
|
|
seal.fulfill(authToken)
|
|
})
|
|
return promise
|
|
}
|
|
|
|
promise
|
|
.done(on: OpenGroupAPI.workQueue) { _ in
|
|
legacyAuthTokenPromises.wrappedValue["\(server).\(room)"] = nil
|
|
}
|
|
.catch(on: OpenGroupAPI.workQueue) { _ in
|
|
legacyAuthTokenPromises.wrappedValue["\(server).\(room)"] = nil
|
|
}
|
|
|
|
legacyAuthTokenPromises.wrappedValue["\(server).\(room)"] = promise
|
|
return promise
|
|
}
|
|
|
|
@available(*, deprecated, message: "Use request signing instead")
|
|
public static func legacyRequestNewAuthToken(for room: String, on server: String) -> Promise<String> {
|
|
SNLog("Requesting auth token for server: \(server).")
|
|
guard let userKeyPair: ECKeyPair = SNMessagingKitConfiguration.shared.storage.getUserKeyPair() else {
|
|
return Promise(error: Error.generic)
|
|
}
|
|
|
|
let request: Request = Request(
|
|
server: server,
|
|
room: room,
|
|
endpoint: .legacyAuthTokenChallenge(legacyAuth: true),
|
|
queryParameters: [
|
|
.publicKey: getUserHexEncodedPublicKey()
|
|
],
|
|
isAuthRequired: false
|
|
)
|
|
|
|
return legacySend(request).map(on: OpenGroupAPI.workQueue) { _, maybeData in
|
|
guard let data: Data = maybeData else { throw Error.parsingFailed }
|
|
let response = try data.decoded(as: LegacyAuthTokenResponse.self, customError: Error.parsingFailed)
|
|
let symmetricKey = try AESGCM.generateSymmetricKey(x25519PublicKey: response.challenge.ephemeralPublicKey, x25519PrivateKey: userKeyPair.privateKey)
|
|
|
|
guard let tokenAsData = try? AESGCM.decrypt(response.challenge.ciphertext, with: symmetricKey) else {
|
|
throw Error.decryptionFailed
|
|
}
|
|
|
|
return tokenAsData.toHexString()
|
|
}
|
|
}
|
|
|
|
@available(*, deprecated, message: "Use request signing instead")
|
|
public static func legacyClaimAuthToken(_ authToken: String, for room: String, on server: String) -> Promise<String> {
|
|
let requestBody: LegacyPublicKeyBody = LegacyPublicKeyBody(publicKey: getUserHexEncodedPublicKey())
|
|
|
|
guard let body: Data = try? JSONEncoder().encode(requestBody) else {
|
|
return Promise(error: HTTP.Error.invalidJSON)
|
|
}
|
|
|
|
let request: Request = Request(
|
|
method: .post,
|
|
server: server,
|
|
room: room,
|
|
endpoint: .legacyAuthTokenClaim(legacyAuth: true),
|
|
headers: [
|
|
// Set explicitly here because is isn't in the database yet at this point
|
|
.authorization: authToken
|
|
],
|
|
body: body,
|
|
isAuthRequired: false
|
|
)
|
|
|
|
return legacySend(request).map(on: OpenGroupAPI.workQueue) { _ in authToken }
|
|
}
|
|
|
|
/// Should be called when leaving a group.
|
|
@available(*, deprecated, message: "Use request signing instead")
|
|
public static func legacyDeleteAuthToken(for room: String, on server: String) -> Promise<Void> {
|
|
let request: Request = Request(
|
|
method: .delete,
|
|
server: server,
|
|
room: room,
|
|
endpoint: .legacyAuthToken(legacyAuth: true)
|
|
)
|
|
|
|
return legacySend(request).map(on: OpenGroupAPI.workQueue) { _ in
|
|
let storage = SNMessagingKitConfiguration.shared.storage
|
|
|
|
storage.write { transaction in
|
|
storage.removeAuthToken(for: room, on: server, using: transaction)
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: -- Legacy Requests
|
|
|
|
@available(*, deprecated, message: "Use poll or batch instead")
|
|
public static func legacyCompactPoll(_ server: String) -> Promise<LegacyCompactPollResponse> {
|
|
let storage: SessionMessagingKitStorageProtocol = SNMessagingKitConfiguration.shared.storage
|
|
let rooms: [String] = storage.getAllOpenGroups().values
|
|
.filter { $0.server == server }
|
|
.map { $0.room }
|
|
var getAuthTokenPromises: [String: Promise<String>] = [:]
|
|
let useMessageLimit = (hasPerformedInitialPoll[server] != true && timeSinceLastOpen > OpenGroupAPI.Poller.maxInactivityPeriod)
|
|
|
|
hasPerformedInitialPoll[server] = true
|
|
|
|
if !legacyHasUpdatedLastOpenDate {
|
|
UserDefaults.standard[.lastOpen] = Date()
|
|
legacyHasUpdatedLastOpenDate = true
|
|
}
|
|
|
|
for room in rooms {
|
|
getAuthTokenPromises[room] = legacyGetAuthToken(for: room, on: server)
|
|
}
|
|
|
|
let requestBody: LegacyCompactPollBody = LegacyCompactPollBody(
|
|
requests: rooms
|
|
.map { roomId -> LegacyCompactPollBody.Room in
|
|
LegacyCompactPollBody.Room(
|
|
id: roomId,
|
|
fromMessageServerId: (useMessageLimit ? nil :
|
|
storage.getLastMessageServerID(for: roomId, on: server)
|
|
),
|
|
fromDeletionServerId: (useMessageLimit ? nil :
|
|
storage.getLastDeletionServerID(for: roomId, on: server)
|
|
),
|
|
legacyAuthToken: nil
|
|
)
|
|
}
|
|
)
|
|
|
|
return when(fulfilled: [Promise<String>](getAuthTokenPromises.values))
|
|
.then(on: OpenGroupAPI.workQueue) { _ -> Promise<LegacyCompactPollResponse> in
|
|
let requestBodyWithAuthTokens: LegacyCompactPollBody = LegacyCompactPollBody(
|
|
requests: requestBody.requests.compactMap { oldRoom -> LegacyCompactPollBody.Room? in
|
|
guard let authToken: String = getAuthTokenPromises[oldRoom.id]?.value else { return nil }
|
|
|
|
return LegacyCompactPollBody.Room(
|
|
id: oldRoom.id,
|
|
fromMessageServerId: oldRoom.fromMessageServerId,
|
|
fromDeletionServerId: oldRoom.fromDeletionServerId,
|
|
legacyAuthToken: authToken
|
|
)
|
|
}
|
|
)
|
|
|
|
guard let body: Data = try? JSONEncoder().encode(requestBodyWithAuthTokens) else {
|
|
return Promise(error: HTTP.Error.invalidJSON)
|
|
}
|
|
|
|
let request = Request(
|
|
method: .post,
|
|
server: server,
|
|
endpoint: .legacyCompactPoll(legacyAuth: true),
|
|
body: body,
|
|
isAuthRequired: false
|
|
)
|
|
|
|
return legacySend(request)
|
|
.then(on: OpenGroupAPI.workQueue) { _, maybeData -> Promise<LegacyCompactPollResponse> in
|
|
guard let data: Data = maybeData else { throw Error.parsingFailed }
|
|
let response: LegacyCompactPollResponse = try data.decoded(as: LegacyCompactPollResponse.self, customError: Error.parsingFailed)
|
|
|
|
return when(
|
|
fulfilled: response.results
|
|
.compactMap { (result: LegacyCompactPollResponse.Result) -> Promise<[LegacyDeletion]>? in
|
|
// A 401 means that we didn't provide a (valid) auth token for a route that
|
|
// required one. We use this as an indication that the token we're using has
|
|
// expired. Note that a 403 has a different meaning; it means that we provided
|
|
// a valid token but it doesn't have a high enough permission level for the
|
|
// route in question.
|
|
guard result.statusCode != 401 else {
|
|
storage.writeSync { transaction in
|
|
storage.removeAuthToken(for: result.room, on: server, using: transaction)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
return legacyProcess(messages: result.messages, for: result.room, on: server)
|
|
.then(on: OpenGroupAPI.workQueue) { _ -> Promise<[LegacyDeletion]> in
|
|
legacyProcess(deletions: result.deletions, for: result.room, on: server)
|
|
}
|
|
}
|
|
).then(on: OpenGroupAPI.workQueue) { _ in Promise.value(response) }
|
|
}
|
|
}
|
|
}
|
|
|
|
@available(*, deprecated, message: "Use getDefaultRoomsIfNeeded instead")
|
|
public static func legacyGetDefaultRoomsIfNeeded() {
|
|
Storage.shared.write(
|
|
with: { transaction in
|
|
Storage.shared.setOpenGroupPublicKey(for: defaultServer, to: defaultServerPublicKey, using: transaction)
|
|
},
|
|
completion: {
|
|
let promise = attempt(maxRetryCount: 8, recoveringOn: DispatchQueue.main) {
|
|
OpenGroupAPI.legacyGetAllRooms(from: defaultServer)
|
|
}
|
|
_ = promise.done(on: OpenGroupAPI.workQueue) { items in
|
|
items.forEach { legacyGetGroupImage(for: $0.id, on: defaultServer).retainUntilComplete() }
|
|
}
|
|
promise.catch(on: OpenGroupAPI.workQueue) { _ in
|
|
OpenGroupAPI.legacyDefaultRoomsPromise = nil
|
|
}
|
|
legacyDefaultRoomsPromise = promise
|
|
}
|
|
)
|
|
}
|
|
|
|
@available(*, deprecated, message: "Use rooms(for:) instead")
|
|
public static func legacyGetAllRooms(from server: String) -> Promise<[LegacyRoomInfo]> {
|
|
let request: Request = Request(
|
|
server: server,
|
|
endpoint: .legacyRooms,
|
|
isAuthRequired: false
|
|
)
|
|
|
|
return legacySend(request)
|
|
.map(on: OpenGroupAPI.workQueue) { _, maybeData in
|
|
guard let data: Data = maybeData else { throw Error.parsingFailed }
|
|
let response: LegacyRoomsResponse = try data.decoded(as: LegacyRoomsResponse.self, customError: Error.parsingFailed)
|
|
|
|
return response.rooms
|
|
}
|
|
}
|
|
|
|
@available(*, deprecated, message: "Use room(for:on:) instead")
|
|
public static func legacyGetRoomInfo(for room: String, on server: String) -> Promise<LegacyRoomInfo> {
|
|
let request: Request = Request(
|
|
server: server,
|
|
room: room,
|
|
endpoint: .legacyRoomInfo(room),
|
|
isAuthRequired: false
|
|
)
|
|
|
|
return legacySend(request)
|
|
.map(on: OpenGroupAPI.workQueue) { _, maybeData in
|
|
guard let data: Data = maybeData else { throw Error.parsingFailed }
|
|
let response: LegacyGetInfoResponse = try data.decoded(as: LegacyGetInfoResponse.self, customError: Error.parsingFailed)
|
|
|
|
return response.room
|
|
}
|
|
}
|
|
|
|
@available(*, deprecated, message: "Use roomImage(_:for:on:) instead")
|
|
public static func legacyGetGroupImage(for room: String, on server: String) -> Promise<Data> {
|
|
// Normally the image for a given group is stored with the group thread, so it's only
|
|
// fetched once. However, on the join open group screen we show images for groups the
|
|
// user * hasn't * joined yet. We don't want to re-fetch these images every time the
|
|
// user opens the app because that could slow the app down or be data-intensive. So
|
|
// instead we assume that these images don't change that often and just fetch them once
|
|
// a week. We also assume that they're all fetched at the same time as well, so that
|
|
// we only need to maintain one date in user defaults. On top of all of this we also
|
|
// don't double up on fetch requests by storing the existing request as a promise if
|
|
// there is one.
|
|
let lastOpenGroupImageUpdate: Date? = UserDefaults.standard[.lastOpenGroupImageUpdate]
|
|
let now: Date = Date()
|
|
let timeSinceLastUpdate: TimeInterval = (given(lastOpenGroupImageUpdate) { now.timeIntervalSince($0) } ?? .greatestFiniteMagnitude)
|
|
let updateInterval: TimeInterval = (7 * 24 * 60 * 60)
|
|
|
|
if let data = Storage.shared.getOpenGroupImage(for: room, on: server), server == defaultServer, timeSinceLastUpdate < updateInterval {
|
|
return Promise.value(data)
|
|
}
|
|
|
|
if let promise = legacyGroupImagePromises["\(server).\(room)"] {
|
|
return promise
|
|
}
|
|
|
|
let request: Request = Request(
|
|
server: server,
|
|
room: room,
|
|
endpoint: .legacyRoomImage(room),
|
|
isAuthRequired: false
|
|
)
|
|
|
|
let promise: Promise<Data> = legacySend(request).map(on: OpenGroupAPI.workQueue) { _, maybeData in
|
|
guard let data: Data = maybeData else { throw Error.parsingFailed }
|
|
let response: LegacyFileDownloadResponse = try data.decoded(as: LegacyFileDownloadResponse.self, customError: Error.parsingFailed)
|
|
|
|
if server == defaultServer {
|
|
Storage.shared.write { transaction in
|
|
Storage.shared.setOpenGroupImage(to: response.data, for: room, on: server, using: transaction)
|
|
}
|
|
UserDefaults.standard[.lastOpenGroupImageUpdate] = now
|
|
}
|
|
|
|
return response.data
|
|
}
|
|
legacyGroupImagePromises["\(server).\(room)"] = promise
|
|
|
|
return promise
|
|
}
|
|
|
|
@available(*, deprecated, message: "Use room(for:on:) instead")
|
|
public static func legacyGetMemberCount(for room: String, on server: String) -> Promise<UInt64> {
|
|
let request: Request = Request(
|
|
server: server,
|
|
room: room,
|
|
endpoint: .legacyMemberCount(legacyAuth: true)
|
|
)
|
|
|
|
return legacySend(request)
|
|
.map(on: OpenGroupAPI.workQueue) { _, maybeData in
|
|
guard let data: Data = maybeData else { throw Error.parsingFailed }
|
|
let response: LegacyMemberCountResponse = try data.decoded(as: LegacyMemberCountResponse.self, customError: Error.parsingFailed)
|
|
|
|
let storage = SNMessagingKitConfiguration.shared.storage
|
|
storage.write { transaction in
|
|
storage.setUserCount(to: response.memberCount, forOpenGroupWithID: "\(server).\(room)", using: transaction)
|
|
}
|
|
|
|
return response.memberCount
|
|
}
|
|
}
|
|
|
|
// MARK: - Legacy File Storage
|
|
|
|
@available(*, deprecated, message: "Use uploadFile(_:fileName:to:on:) instead")
|
|
public static func legacyUpload(_ file: Data, to room: String, on server: String) -> 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, room: room, endpoint: .legacyFiles, body: body)
|
|
|
|
return legacySend(request).map(on: OpenGroupAPI.workQueue) { _, maybeData in
|
|
guard let data: Data = maybeData else { throw Error.parsingFailed }
|
|
let response: LegacyFileUploadResponse = try data.decoded(as: LegacyFileUploadResponse.self, customError: Error.parsingFailed)
|
|
|
|
return response.fileId
|
|
}
|
|
}
|
|
|
|
@available(*, deprecated, message: "Use downloadFile(_:from:on:) instead")
|
|
public static func legacyDownload(_ file: UInt64, from room: String, on server: String) -> Promise<Data> {
|
|
let request = Request(server: server, room: room, endpoint: .legacyFile(file))
|
|
|
|
return legacySend(request).map(on: OpenGroupAPI.workQueue) { _, maybeData in
|
|
guard let data: Data = maybeData else { throw Error.parsingFailed }
|
|
let response: LegacyFileDownloadResponse = try data.decoded(as: LegacyFileDownloadResponse.self, customError: Error.parsingFailed)
|
|
|
|
return response.data
|
|
}
|
|
}
|
|
|
|
// MARK: - Legacy Message Sending & Receiving
|
|
|
|
@available(*, deprecated, message: "Use send(_:to:on:whisperTo:whisperMods:with:) instead")
|
|
public static func legacySend(_ message: LegacyOpenGroupMessageV2, to room: String, on server: String, with publicKey: String) -> Promise<LegacyOpenGroupMessageV2> {
|
|
guard let signedMessage = message.sign(with: publicKey) else { return Promise(error: Error.signingFailed) }
|
|
guard let body: Data = try? JSONEncoder().encode(signedMessage) else {
|
|
return Promise(error: Error.parsingFailed)
|
|
}
|
|
let request = Request(method: .post, server: server, room: room, endpoint: .legacyMessages, body: body)
|
|
|
|
return legacySend(request).map(on: OpenGroupAPI.workQueue) { _, maybeData in
|
|
guard let data: Data = maybeData else { throw Error.parsingFailed }
|
|
let message: LegacyOpenGroupMessageV2 = try data.decoded(as: LegacyOpenGroupMessageV2.self, customError: Error.parsingFailed)
|
|
Storage.shared.write { transaction in
|
|
Storage.shared.addReceivedMessageTimestamp(message.sentTimestamp, using: transaction)
|
|
}
|
|
return message
|
|
}
|
|
}
|
|
|
|
@available(*, deprecated, message: "Use recentMessages(in:on:) or messagesSince(seqNo:in:on:) instead")
|
|
public static func legacyGetMessages(for room: String, on server: String) -> Promise<[LegacyOpenGroupMessageV2]> {
|
|
let storage = SNMessagingKitConfiguration.shared.storage
|
|
let request: Request = Request(
|
|
server: server,
|
|
room: room,
|
|
endpoint: .legacyMessages,
|
|
queryParameters: [
|
|
.fromServerId: storage.getLastMessageServerID(for: room, on: server).map { String($0) }
|
|
].compactMapValues { $0 }
|
|
)
|
|
|
|
return legacySend(request).then(on: OpenGroupAPI.workQueue) { _, maybeData -> Promise<[LegacyOpenGroupMessageV2]> in
|
|
guard let data: Data = maybeData else { throw Error.parsingFailed }
|
|
let messages: [LegacyOpenGroupMessageV2] = try data.decoded(as: [LegacyOpenGroupMessageV2].self, customError: Error.parsingFailed)
|
|
|
|
return legacyProcess(messages: messages, for: room, on: server)
|
|
}
|
|
}
|
|
|
|
// MARK: - Legacy Message Deletion
|
|
|
|
// TODO: No delete method????.
|
|
@available(*, deprecated, message: "Use v4 endpoint instead")
|
|
public static func legacyDeleteMessage(with serverID: Int64, from room: String, on server: String) -> Promise<Void> {
|
|
let request: Request = Request(
|
|
method: .delete,
|
|
server: server,
|
|
room: room,
|
|
endpoint: .legacyMessagesForServer(serverID)
|
|
)
|
|
|
|
return legacySend(request).map(on: OpenGroupAPI.workQueue) { _ in }
|
|
}
|
|
|
|
@available(*, deprecated, message: "Use v4 endpoint instead")
|
|
public static func legacyGetDeletedMessages(for room: String, on server: String) -> Promise<[LegacyDeletion]> {
|
|
let storage = SNMessagingKitConfiguration.shared.storage
|
|
|
|
let request: Request = Request(
|
|
server: server,
|
|
room: room,
|
|
endpoint: .legacyDeletedMessages,
|
|
queryParameters: [
|
|
.fromServerId: storage.getLastDeletionServerID(for: room, on: server).map { String($0) }
|
|
].compactMapValues { $0 }
|
|
)
|
|
|
|
return legacySend(request).then(on: OpenGroupAPI.workQueue) { _, maybeData -> Promise<[LegacyDeletion]> in
|
|
guard let data: Data = maybeData else { throw Error.parsingFailed }
|
|
let response: LegacyDeletedMessagesResponse = try data.decoded(as: LegacyDeletedMessagesResponse.self, customError: Error.parsingFailed)
|
|
|
|
return legacyProcess(deletions: response.deletions, for: room, on: server)
|
|
}
|
|
}
|
|
|
|
// MARK: - Legacy Moderation
|
|
|
|
@available(*, deprecated, message: "Use v4 endpoint instead")
|
|
public static func legacyGetModerators(for room: String, on server: String) -> Promise<[String]> {
|
|
let request: Request = Request(
|
|
server: server,
|
|
room: room,
|
|
endpoint: .legacyModerators
|
|
)
|
|
|
|
return legacySend(request)
|
|
.map(on: OpenGroupAPI.workQueue) { _, maybeData in
|
|
guard let data: Data = maybeData else { throw Error.parsingFailed }
|
|
let response: LegacyModeratorsResponse = try data.decoded(as: LegacyModeratorsResponse.self, customError: Error.parsingFailed)
|
|
|
|
if var x = self.moderators[server] {
|
|
x[room] = Set(response.moderators)
|
|
self.moderators[server] = x
|
|
}
|
|
else {
|
|
self.moderators[server] = [room: Set(response.moderators)]
|
|
}
|
|
|
|
return response.moderators
|
|
}
|
|
}
|
|
|
|
@available(*, deprecated, message: "Use v4 endpoint instead")
|
|
public static func legacyBan(_ publicKey: String, from room: String, on server: String) -> Promise<Void> {
|
|
let requestBody: LegacyPublicKeyBody = LegacyPublicKeyBody(publicKey: getUserHexEncodedPublicKey())
|
|
|
|
guard let body: Data = try? JSONEncoder().encode(requestBody) else {
|
|
return Promise(error: HTTP.Error.invalidJSON)
|
|
}
|
|
|
|
let request: Request = Request(
|
|
method: .post,
|
|
server: server,
|
|
room: room,
|
|
endpoint: .legacyBlockList,
|
|
body: body
|
|
)
|
|
|
|
return legacySend(request).map(on: OpenGroupAPI.workQueue) { _ in }
|
|
}
|
|
|
|
@available(*, deprecated, message: "Use v4 endpoint instead")
|
|
public static func legacyBanAndDeleteAllMessages(_ publicKey: String, from room: String, on server: String) -> Promise<Void> {
|
|
let requestBody: LegacyPublicKeyBody = LegacyPublicKeyBody(publicKey: getUserHexEncodedPublicKey())
|
|
|
|
guard let body: Data = try? JSONEncoder().encode(requestBody) else {
|
|
return Promise(error: HTTP.Error.invalidJSON)
|
|
}
|
|
|
|
let request: Request = Request(
|
|
method: .post,
|
|
server: server,
|
|
room: room,
|
|
endpoint: .legacyBanAndDeleteAll,
|
|
body: body
|
|
)
|
|
|
|
return legacySend(request).map(on: OpenGroupAPI.workQueue) { _ in }
|
|
}
|
|
|
|
@available(*, deprecated, message: "Use v4 endpoint instead")
|
|
public static func legacyUnban(_ publicKey: String, from room: String, on server: String) -> Promise<Void> {
|
|
let request: Request = Request(
|
|
method: .delete,
|
|
server: server,
|
|
room: room,
|
|
endpoint: .legacyBlockListIndividual(publicKey)
|
|
)
|
|
|
|
return legacySend(request).map(on: OpenGroupAPI.workQueue) { _ in }
|
|
}
|
|
|
|
// MARK: - Processing
|
|
// TODO: Move these methods to the OpenGroupManager? (seems odd for them to be in the API)
|
|
|
|
@available(*, deprecated, message: "Use v4 endpoint instead")
|
|
private static func legacyProcess(messages: [LegacyOpenGroupMessageV2]?, for room: String, on server: String) -> Promise<[LegacyOpenGroupMessageV2]> {
|
|
guard let messages: [LegacyOpenGroupMessageV2] = messages, !messages.isEmpty else { return Promise.value([]) }
|
|
|
|
let storage = SNMessagingKitConfiguration.shared.storage
|
|
let serverID: Int64 = (messages.compactMap { $0.serverID }.max() ?? 0)
|
|
let lastMessageServerID: Int64 = (storage.getLastMessageServerID(for: room, on: server) ?? 0)
|
|
|
|
if serverID > lastMessageServerID {
|
|
let (promise, seal) = Promise<[LegacyOpenGroupMessageV2]>.pending()
|
|
|
|
storage.write(
|
|
with: { transaction in
|
|
storage.setLastMessageServerID(for: room, on: server, to: serverID, using: transaction)
|
|
},
|
|
completion: {
|
|
seal.fulfill(messages)
|
|
}
|
|
)
|
|
|
|
return promise
|
|
}
|
|
|
|
return Promise.value(messages)
|
|
}
|
|
|
|
@available(*, deprecated, message: "Use v4 endpoint instead")
|
|
private static func legacyProcess(deletions: [LegacyDeletion]?, for room: String, on server: String) -> Promise<[LegacyDeletion]> {
|
|
guard let deletions: [LegacyDeletion] = deletions else { return Promise.value([]) }
|
|
|
|
let storage = SNMessagingKitConfiguration.shared.storage
|
|
let serverID: Int64 = (deletions.compactMap { $0.id }.max() ?? 0)
|
|
let lastDeletionServerID: Int64 = (storage.getLastDeletionServerID(for: room, on: server) ?? 0)
|
|
|
|
if serverID > lastDeletionServerID {
|
|
let (promise, seal) = Promise<[LegacyDeletion]>.pending()
|
|
|
|
storage.write(
|
|
with: { transaction in
|
|
storage.setLastDeletionServerID(for: room, on: server, to: serverID, using: transaction)
|
|
},
|
|
completion: {
|
|
seal.fulfill(deletions)
|
|
}
|
|
)
|
|
|
|
return promise
|
|
}
|
|
|
|
return Promise.value(deletions)
|
|
}
|
|
|
|
// MARK: - Legacy Convenience
|
|
|
|
@available(*, deprecated, message: "Use v4 endpoint instead")
|
|
private static func legacySend(_ request: Request, through api: OnionRequestAPIType.Type = OnionRequestAPI.self) -> 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()
|
|
urlRequest.httpBody = request.body
|
|
|
|
if request.useOnionRouting {
|
|
guard let publicKey = SNMessagingKitConfiguration.shared.storage.getOpenGroupPublicKey(for: request.server) else {
|
|
return Promise(error: Error.noPublicKey)
|
|
}
|
|
|
|
if request.isAuthRequired {
|
|
// Because legacy auth happens on a per-room basis, we need to have a room to
|
|
// make an authenticated request
|
|
guard let room = request.room else {
|
|
return api.sendOnionRequest(urlRequest, to: request.server, using: .v3, with: publicKey)
|
|
}
|
|
|
|
return legacyGetAuthToken(for: room, on: request.server)
|
|
.then(on: OpenGroupAPI.workQueue) { authToken -> Promise<(OnionRequestResponseInfoType, Data?)> in
|
|
urlRequest.setValue(authToken, forHTTPHeaderField: Header.authorization.rawValue)
|
|
|
|
let promise = api.sendOnionRequest(urlRequest, to: request.server, using: .v3, with: publicKey)
|
|
promise.catch(on: OpenGroupAPI.workQueue) { error in
|
|
// A 401 means that we didn't provide a (valid) auth token for a route
|
|
// that required one. We use this as an indication that the token we're
|
|
// using has expired. Note that a 403 has a different meaning; it means
|
|
// that we provided a valid token but it doesn't have a high enough
|
|
// permission level for the route in question.
|
|
if case OnionRequestAPI.Error.httpRequestFailedAtDestination(let statusCode, _, _) = error, statusCode == 401 {
|
|
let storage = SNMessagingKitConfiguration.shared.storage
|
|
|
|
storage.writeSync { transaction in
|
|
storage.removeAuthToken(for: room, on: request.server, using: transaction)
|
|
}
|
|
}
|
|
}
|
|
|
|
return promise
|
|
}
|
|
}
|
|
|
|
return api.sendOnionRequest(urlRequest, to: request.server, using: .v3, with: publicKey)
|
|
}
|
|
|
|
preconditionFailure("It's currently not allowed to send non onion routed requests.")
|
|
}
|
|
}
|