Merge branch 'database-refactor' into add-documents-section

pull/646/head
ryanzhao 4 years ago
commit b0d78754c4

@ -1461,10 +1461,15 @@ final class ConversationVC: BaseVC, OWSConversationSettingsViewDelegate, Convers
var lastSize: CGSize = .zero
self.tableView.afterNextLayoutSubviews(
when: { [weak self] a, b, updatedContentSize in
guard (CACurrentMediaTime() - initialUpdateTime) < 2 && lastSize != updatedContentSize else {
return true
}
when: { [weak self] numSections, numRowInSections, updatedContentSize in
// If too much time has passed or the section/row count doesn't match then
// just stop the callback
guard
(CACurrentMediaTime() - initialUpdateTime) < 2 &&
lastSize != updatedContentSize &&
numSections > targetIndexPath.section &&
numRowInSections[targetIndexPath.section] > targetIndexPath.row
else { return true }
lastSize = updatedContentSize

@ -245,7 +245,7 @@ extension OpenGroupSuggestionGrid {
label.text = room.name
// Only continue if we have a room image
guard let imageId: Int64 = room.imageId else {
guard let imageId: String = room.imageId else {
imageView.isHidden = true
return
}

@ -141,7 +141,7 @@ public enum SMKLegacy {
internal var sender: String?
internal var groupPublicKey: String?
internal var openGroupServerMessageID: UInt64?
internal var openGroupServerTimestamp: UInt64?
internal var openGroupServerTimestamp: UInt64? // Not used for anything
internal var serverHash: String?
// MARK: NSCoding
@ -175,7 +175,6 @@ public enum SMKLegacy {
result.sender = self.sender
result.groupPublicKey = self.groupPublicKey
result.openGroupServerMessageId = self.openGroupServerMessageID
result.openGroupServerTimestamp = self.openGroupServerTimestamp
result.serverHash = self.serverHash
return result

@ -41,11 +41,11 @@ public final class FileServerAPI: NSObject {
.decoded(as: FileUploadResponse.self, on: .global(qos: .userInitiated))
}
public static func download(_ file: Int64, useOldServer: Bool) -> Promise<Data> {
public static func download(_ fileId: String, useOldServer: Bool) -> Promise<Data> {
let serverPublicKey: String = (useOldServer ? oldServerPublicKey : serverPublicKey)
let request = Request<NoBody, Endpoint>(
server: (useOldServer ? oldServer : server),
endpoint: .fileIndividual(fileId: file)
endpoint: .fileIndividual(fileId: fileId)
)
return send(request, serverPublicKey: serverPublicKey)

@ -5,7 +5,7 @@ import Foundation
extension FileServerAPI {
public enum Endpoint: EndpointType {
case file
case fileIndividual(fileId: Int64)
case fileIndividual(fileId: String)
case sessionVersion
var path: String {

@ -87,8 +87,10 @@ public enum AttachmentDownloadJob: JobExecutor {
let downloadPromise: Promise<Data> = {
guard
let downloadUrl: String = attachment.downloadUrl,
let fileAsString: String = downloadUrl.split(separator: "/").last.map({ String($0) }),
let file: Int64 = Int64(fileAsString)
let fileId: String = downloadUrl
.split(separator: "/")
.last
.map({ String($0) })
else {
return Promise(error: AttachmentDownloadError.invalidUrl)
}
@ -98,13 +100,13 @@ public enum AttachmentDownloadJob: JobExecutor {
return nil // Not an open group so just use standard FileServer upload
}
return OpenGroupAPI.downloadFile(db, fileId: file, from: openGroup.roomToken, on: openGroup.server)
return OpenGroupAPI.downloadFile(db, fileId: fileId, from: openGroup.roomToken, on: openGroup.server)
.map { _, data in data }
})
return (
maybeOpenGroupDownloadPromise ??
FileServerAPI.download(file, useOldServer: downloadUrl.contains(FileServerAPI.oldServer))
FileServerAPI.download(fileId, useOldServer: downloadUrl.contains(FileServerAPI.oldServer))
)
}()

@ -14,7 +14,6 @@ public class Message: Codable {
public var sender: String?
public var groupPublicKey: String?
public var openGroupServerMessageId: UInt64?
public var openGroupServerTimestamp: UInt64?
public var serverHash: String?
public var ttl: UInt64 { 14 * 24 * 60 * 60 * 1000 }
@ -41,7 +40,6 @@ public class Message: Codable {
sender: String? = nil,
groupPublicKey: String? = nil,
openGroupServerMessageId: UInt64? = nil,
openGroupServerTimestamp: UInt64? = nil,
serverHash: String? = nil
) {
self.id = id
@ -52,7 +50,6 @@ public class Message: Codable {
self.sender = sender
self.groupPublicKey = groupPublicKey
self.openGroupServerMessageId = openGroupServerMessageId
self.openGroupServerTimestamp = openGroupServerTimestamp
self.serverHash = serverHash
}

@ -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: String?
/// Array of pinned message information (omitted entirely if there are no pinned messages)
public let pinnedMessages: [PinnedMessage]?
@ -150,6 +150,12 @@ extension OpenGroupAPI.Room {
public init(from decoder: Decoder) throws {
let container: KeyedDecodingContainer<CodingKeys> = try decoder.container(keyedBy: CodingKeys.self)
// This logic is to future-proof the transition from int-based to string-based image ids
let maybeImageId: String? = (
((try? container.decode(Int64.self, forKey: .imageId)).map { "\($0)" }) ??
(try? container.decode(String.self, forKey: .imageId))
)
self = OpenGroupAPI.Room(
token: try container.decode(String.self, forKey: .token),
name: try container.decode(String.self, forKey: .name),
@ -160,7 +166,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: maybeImageId,
pinnedMessages: try? container.decode([OpenGroupAPI.PinnedMessage].self, forKey: .pinnedMessages),
admin: ((try? container.decode(Bool.self, forKey: .admin)) ?? false),

@ -700,7 +700,7 @@ public enum OpenGroupAPI {
public static func downloadFile(
_ db: Database,
fileId: Int64,
fileId: String,
from roomToken: String,
on server: String,
using dependencies: SMKDependencies = SMKDependencies()

@ -446,10 +446,10 @@ public final class OpenGroupManager: NSObject {
/// Start downloading the room image (if we don't have one or it's been updated)
if
let imageId: Int64 = pollInfo.details?.imageId,
let imageId: String = pollInfo.details?.imageId,
(
openGroup.imageData == nil ||
openGroup.imageId != "\(imageId)"
openGroup.imageId != imageId
)
{
OpenGroupManager.roomImage(db, fileId: imageId, for: roomToken, on: server, using: dependencies)
@ -786,7 +786,7 @@ public final class OpenGroupManager: NSObject {
.done(on: OpenGroupAPI.workQueue) { items in
dependencies.storage.writeAsync { db in
items
.compactMap { room -> (Int64, String)? in
.compactMap { room -> (String, String)? in
// Try to insert an inactive version of the OpenGroup (use 'insert' rather than 'save'
// as we want it to fail if the room already exists)
do {
@ -797,7 +797,7 @@ public final class OpenGroupManager: NSObject {
isActive: false,
name: room.name,
roomDescription: room.roomDescription,
imageId: room.imageId.map { "\($0)" },
imageId: room.imageId,
imageData: nil,
userCount: room.activeUsers,
infoUpdates: room.infoUpdates,
@ -809,7 +809,7 @@ public final class OpenGroupManager: NSObject {
}
catch {}
guard let imageId: Int64 = room.imageId else { return nil }
guard let imageId: String = room.imageId else { return nil }
return (imageId, room.token)
}
@ -845,7 +845,7 @@ public final class OpenGroupManager: NSObject {
public static func roomImage(
_ db: Database,
fileId: Int64,
fileId: String,
for roomToken: String,
on server: String,
using dependencies: OGMDependencies = OGMDependencies()

@ -35,7 +35,7 @@ extension OpenGroupAPI {
// Files
case roomFile(String)
case roomFileIndividual(String, Int64)
case roomFileIndividual(String, String)
// Inbox/Outbox (Message Requests)

@ -146,7 +146,6 @@ public enum MessageReceiver {
message.sentTimestamp = envelope.timestamp
message.receivedTimestamp = UInt64((Date().timeIntervalSince1970) * 1000)
message.groupPublicKey = groupPublicKey
message.openGroupServerTimestamp = (isOpenGroupMessage ? envelope.serverTimestamp : nil)
message.openGroupServerMessageId = openGroupMessageServerId.map { UInt64($0) }
// Validate

@ -339,11 +339,17 @@ public final class MessageSender {
.joined(separator: ".")
}
// Note: It's possible to send a message and then delete the open group you sent the message to
// which would go into this case, so rather than handling it as an invalid state we just want to
// error in a non-retryable way
guard
let openGroup: OpenGroup = try? OpenGroup.fetchOne(db, id: threadId),
let userEdKeyPair: Box.KeyPair = Identity.fetchUserEd25519KeyPair(db),
case .openGroup(let roomToken, let server, let whisperTo, let whisperMods, let fileIds) = destination
else { preconditionFailure() }
else {
seal.reject(MessageSenderError.invalidMessage)
return promise
}
message.sender = {
let capabilities: [Capability.Variant] = (try? Capability

@ -145,15 +145,15 @@ public struct ProfileManager {
// Download already in flight; ignore
return
}
guard
let profileUrlStringAtStart: String = profile.profilePictureUrl,
let profileUrlAtStart: URL = URL(string: profileUrlStringAtStart)
else {
guard let profileUrlStringAtStart: String = profile.profilePictureUrl else {
SNLog("Skipping downloading avatar for \(profile.id) because url is not set")
return
}
guard
let fileId: Int64 = Int64(profileUrlAtStart.lastPathComponent),
let fileId: String = profileUrlStringAtStart
.split(separator: "/")
.last
.map({ String($0) }),
let profileKeyAtStart: OWSAES256Key = profile.profileEncryptionKey,
profileKeyAtStart.keyData.count > 0
else {

@ -14,8 +14,11 @@ public enum OnionRequestAPIError: LocalizedError {
public var errorDescription: String? {
switch self {
case .httpRequestFailedAtDestination(let statusCode, _, let destination):
case .httpRequestFailedAtDestination(let statusCode, let data, let destination):
if statusCode == 429 { return "Rate limited." }
if let errorResponse: String = String(data: data, encoding: .utf8) {
return "HTTP request failed at destination (\(destination)) with status code: \(statusCode), error body: \(errorResponse)."
}
return "HTTP request failed at destination (\(destination)) with status code: \(statusCode)."

@ -10,4 +10,5 @@ public enum SnodeAPIEndpoint: String {
case oxenDaemonRPCCall = "oxend_request"
case getInfo = "info"
case clearAllData = "delete_all"
case expire = "expire"
}

@ -650,8 +650,11 @@ public enum OnionRequestAPI: OnionRequestAPIType {
}
if let bodyAsString = json["body"] as? String {
guard let bodyAsData = bodyAsString.data(using: .utf8), let body = try JSONSerialization.jsonObject(with: bodyAsData, options: [ .fragmentsAllowed ]) as? JSON else {
return seal.reject(HTTP.Error.invalidJSON)
guard let bodyAsData = bodyAsString.data(using: .utf8) else {
return seal.reject(HTTP.Error.invalidResponse)
}
guard let body = try? JSONSerialization.jsonObject(with: bodyAsData, options: [ .fragmentsAllowed ]) as? JSON else {
return seal.reject(OnionRequestAPIError.httpRequestFailedAtDestination(statusCode: UInt(statusCode), data: bodyAsData, destination: destination))
}
if let timestamp = body["t"] as? Int64 {

@ -719,6 +719,99 @@ public final class SnodeAPI {
return promise
}
// MARK: Edit
public static func updateExpiry(
publicKey: String,
edKeyPair: Box.KeyPair,
updatedExpiryMs: UInt64,
serverHashes: [String]
) -> Promise<[String: (hashes: [String], expiry: UInt64)]> {
let publicKey = (Features.useTestnet ? publicKey.removingIdPrefixIfNeeded() : publicKey)
return attempt(maxRetryCount: maxRetryCount, recoveringOn: Threading.workQueue) {
getSwarm(for: publicKey)
.then2 { swarm -> Promise<[String: (hashes: [String], expiry: UInt64)]> in
// "expire" || expiry || messages[0] || ... || messages[N]
let verificationBytes = SnodeAPIEndpoint.expire.rawValue.bytes
.appending(contentsOf: "\(updatedExpiryMs)".data(using: .ascii)?.bytes)
.appending(contentsOf: serverHashes.joined().bytes)
guard
let snode = swarm.randomElement(),
let signature = sodium.sign.signature(
message: verificationBytes,
secretKey: edKeyPair.secretKey
)
else {
throw SnodeAPIError.signingFailed
}
let parameters: JSON = [
"pubkey" : publicKey,
"pubkey_ed25519" : edKeyPair.publicKey.toHexString(),
"expiry": updatedExpiryMs,
"messages": serverHashes,
"signature": signature.toBase64()
]
return attempt(maxRetryCount: maxRetryCount, recoveringOn: Threading.workQueue) {
invoke(.expire, on: snode, associatedWith: publicKey, parameters: parameters)
.map2 { responseData -> [String: (hashes: [String], expiry: UInt64)] in
guard let responseJson: JSON = try? JSONSerialization.jsonObject(with: responseData, options: [ .fragmentsAllowed ]) as? JSON else {
throw HTTP.Error.invalidJSON
}
guard let swarm = responseJson["swarm"] as? JSON else { throw HTTP.Error.invalidJSON }
var result: [String: (hashes: [String], expiry: UInt64)] = [:]
for (snodePublicKey, rawJSON) in swarm {
guard let json = rawJSON as? JSON else { throw HTTP.Error.invalidJSON }
guard (json["failed"] as? Bool ?? false) == false else {
if let reason = json["reason"] as? String, let statusCode = json["code"] as? String {
SNLog("Couldn't delete data from: \(snodePublicKey) due to error: \(reason) (\(statusCode)).")
}
else {
SNLog("Couldn't delete data from: \(snodePublicKey).")
}
result[snodePublicKey] = ([], 0)
continue
}
guard
let hashes: [String] = json["updated"] as? [String],
let expiryApplied: UInt64 = json["expiry"] as? UInt64,
let signature: String = json["signature"] as? String
else {
throw HTTP.Error.invalidJSON
}
// The signature format is ( PUBKEY_HEX || EXPIRY || RMSG[0] || ... || RMSG[N] || UMSG[0] || ... || UMSG[M] )
let verificationBytes = publicKey.bytes
.appending(contentsOf: "\(expiryApplied)".data(using: .ascii)?.bytes)
.appending(contentsOf: serverHashes.joined().bytes)
.appending(contentsOf: hashes.joined().bytes)
let isValid = sodium.sign.verify(
message: verificationBytes,
publicKey: Bytes(Data(hex: snodePublicKey)),
signature: Bytes(Data(base64Encoded: signature)!)
)
// Ensure the signature is valid
guard isValid else {
throw SnodeAPIError.signatureVerificationFailed
}
result[snodePublicKey] = (hashes, expiryApplied)
}
return result
}
}
}
}
}
// MARK: Delete
public static func deleteMessage(publicKey: String, serverHashes: [String]) -> Promise<[String: Bool]> {
@ -732,10 +825,16 @@ public final class SnodeAPI {
return attempt(maxRetryCount: maxRetryCount, recoveringOn: Threading.workQueue) {
getSwarm(for: publicKey)
.then2 { swarm -> Promise<[String: Bool]> in
// "delete" || messages...
let verificationBytes = SnodeAPIEndpoint.deleteMessage.rawValue.bytes
.appending(contentsOf: serverHashes.joined().bytes)
guard
let snode = swarm.randomElement(),
let verificationData = (SnodeAPIEndpoint.deleteMessage.rawValue + serverHashes.joined()).data(using: String.Encoding.utf8),
let signature = sodium.sign.signature(message: Bytes(verificationData), secretKey: userED25519KeyPair.secretKey)
let signature = sodium.sign.signature(
message: verificationBytes,
secretKey: userED25519KeyPair.secretKey
)
else {
throw SnodeAPIError.signingFailed
}
@ -771,15 +870,11 @@ public final class SnodeAPI {
}
// The signature format is ( PUBKEY_HEX || RMSG[0] || ... || RMSG[N] || DMSG[0] || ... || DMSG[M] )
let verificationData = [
userX25519PublicKey,
serverHashes.joined(),
hashes.joined()
]
.joined()
.data(using: String.Encoding.utf8)!
let verificationBytes = userX25519PublicKey.bytes
.appending(contentsOf: serverHashes.joined().bytes)
.appending(contentsOf: hashes.joined().bytes)
let isValid = sodium.sign.verify(
message: Bytes(verificationData),
message: verificationBytes,
publicKey: Bytes(Data(hex: snodePublicKey)),
signature: Bytes(Data(base64Encoded: signature)!)
)

@ -411,7 +411,7 @@ public class PagedDatabaseObserver<ObservedTable, T>: TransactionObserver where
guard targetIndex > halfPageSize else { return 0 }
guard targetIndex < (totalCount - halfPageSize) else {
return (totalCount - currentPageInfo.pageSize)
return max(0, (totalCount - currentPageInfo.pageSize))
}
return (targetIndex - halfPageSize)

@ -7,8 +7,8 @@ import Curve25519Kit
public struct SessionId {
public enum Prefix: String, CaseIterable {
case standard = "05" // Used for identified users, open groups, etc.
case blinded = "15" // Used for participants in open groups with blinding enabled
case unblinded = "00" // Used for participants in open groups with blinding disabled
case blinded = "15" // Used for authentication and participants in open groups with blinding enabled
case unblinded = "00" // Used for authentication in open groups with blinding disabled
public init?(from stringValue: String?) {
guard let stringValue: String = stringValue else { return nil }

Loading…
Cancel
Save