session-ios/SessionMessagingKit/Open Groups/OpenGroupMessage.swift

197 lines
9.0 KiB
Swift

import PromiseKit
import Curve25519Kit
import SessionUtilitiesKit
@objc(SNOpenGroupMessage)
public final class OpenGroupMessage : NSObject {
public let serverID: UInt64?
public let senderPublicKey: String
public let displayName: String
public let profilePicture: ProfilePicture?
public let body: String
/// - Note: Expressed as milliseconds since 00:00:00 UTC on 1 January 1970.
public let timestamp: UInt64
public let type: String
public let quote: Quote?
public var attachments: [Attachment] = []
public let signature: Signature?
/// - Note: Used for sorting.
public let serverTimestamp: UInt64
@objc(serverID)
public var objc_serverID: UInt64 { return serverID ?? 0 }
// MARK: Settings
private let signatureVersion: UInt64 = 1
private let attachmentType = "net.app.core.oembed"
// MARK: Types
public struct ProfilePicture {
public let profileKey: Data
public let url: String
}
public struct Quote {
public let quotedMessageTimestamp: UInt64
public let quoteePublicKey: String
public let quotedMessageBody: String
public let quotedMessageServerID: UInt64?
}
public struct Attachment {
public let kind: Kind
public let server: String
public let serverID: UInt64
public let contentType: String
public let size: UInt
public let fileName: String
public let flags: UInt
public let width: UInt
public let height: UInt
public let caption: String?
public let url: String
/// Guaranteed to be non-`nil` if `kind` is `linkPreview`
public let linkPreviewURL: String?
/// Guaranteed to be non-`nil` if `kind` is `linkPreview`
public let linkPreviewTitle: String?
public enum Kind : String { case attachment, linkPreview = "preview" }
public var dotNETType: String {
if contentType.hasPrefix("image") {
return "photo"
} else if contentType.hasPrefix("video") {
return "video"
} else if contentType.hasPrefix("audio") {
return "audio"
} else {
return "other"
}
}
}
public struct Signature {
public let data: Data
public let version: UInt64
}
// MARK: Initialization
public init(serverID: UInt64?, senderPublicKey: String, displayName: String, profilePicture: ProfilePicture?, body: String,
type: String, timestamp: UInt64, quote: Quote?, attachments: [Attachment], signature: Signature?, serverTimestamp: UInt64) {
self.serverID = serverID
self.senderPublicKey = senderPublicKey
self.displayName = displayName
self.profilePicture = profilePicture
self.body = body
self.type = type
self.timestamp = timestamp
self.quote = quote
self.attachments = attachments
self.signature = signature
self.serverTimestamp = serverTimestamp
super.init()
}
@objc public convenience init(senderPublicKey: String, displayName: String, body: String, type: String, timestamp: UInt64,
quotedMessageTimestamp: UInt64, quoteePublicKey: String?, quotedMessageBody: String, quotedMessageServerID: UInt64,
signatureData: Data?, signatureVersion: UInt64, serverTimestamp: UInt64) {
let quote: Quote?
if quotedMessageTimestamp != 0, let quoteeHexEncodedPublicKey = quoteePublicKey {
let quotedMessageServerID = (quotedMessageServerID != 0) ? quotedMessageServerID : nil
quote = Quote(quotedMessageTimestamp: quotedMessageTimestamp, quoteePublicKey: quoteeHexEncodedPublicKey, quotedMessageBody: quotedMessageBody, quotedMessageServerID: quotedMessageServerID)
} else {
quote = nil
}
let signature: Signature?
if let signatureData = signatureData, signatureVersion != 0 {
signature = Signature(data: signatureData, version: signatureVersion)
} else {
signature = nil
}
self.init(serverID: nil, senderPublicKey: senderPublicKey, displayName: displayName, profilePicture: nil, body: body, type: type, timestamp: timestamp, quote: quote, attachments: [], signature: signature, serverTimestamp: serverTimestamp)
}
// MARK: Crypto
internal func sign(with privateKey: Data) -> OpenGroupMessage? {
guard let data = getValidationData(for: signatureVersion) else {
SNLog("Failed to sign open group message.")
return nil
}
let userKeyPair = SNMessagingKitConfiguration.shared.storage.getUserKeyPair()!
guard let signatureData = try? Ed25519.sign(data, with: userKeyPair) else {
SNLog("Failed to sign open group message.")
return nil
}
let signature = Signature(data: signatureData, version: signatureVersion)
return OpenGroupMessage(serverID: serverID, senderPublicKey: senderPublicKey, displayName: displayName, profilePicture: profilePicture, body: body, type: type, timestamp: timestamp, quote: quote, attachments: attachments, signature: signature, serverTimestamp: serverTimestamp)
}
internal func hasValidSignature() -> Bool {
guard let signature = signature else { return false }
guard let data = getValidationData(for: signature.version) else { return false }
let publicKey = Data(hex: self.senderPublicKey.removing05PrefixIfNeeded())
return (try? Ed25519.verifySignature(signature.data, publicKey: publicKey, data: data)) ?? false
}
// MARK: JSON
internal func toJSON() -> JSON {
var value: JSON = [ "timestamp" : timestamp ]
if let quote = quote {
let quoteAsJSON: JSON = [ "id" : quote.quotedMessageTimestamp, "author" : quote.quoteePublicKey, "text" : quote.quotedMessageBody ]
value["quote"] = quoteAsJSON
}
if let signature = signature {
value["sig"] = signature.data.toHexString()
value["sigver"] = signature.version
}
if let profilePicture = profilePicture {
value["avatar"] = profilePicture;
}
let annotation: JSON = [ "type" : type, "value" : value ]
let attachmentAnnotations: [JSON] = attachments.map { attachment in
var attachmentValue: JSON = [
// Fields required by the .NET API
"version" : 1, "type" : attachment.dotNETType,
// Custom fields
"lokiType" : attachment.kind.rawValue, "server" : attachment.server, "id" : attachment.serverID, "contentType" : attachment.contentType, "size" : attachment.size, "fileName" : attachment.fileName, "width" : attachment.width, "height" : attachment.height, "url" : attachment.url
]
if let caption = attachment.caption {
attachmentValue["caption"] = caption
}
if let linkPreviewURL = attachment.linkPreviewURL {
attachmentValue["linkPreviewUrl"] = linkPreviewURL
}
if let linkPreviewTitle = attachment.linkPreviewTitle {
attachmentValue["linkPreviewTitle"] = linkPreviewTitle
}
return [ "type" : attachmentType, "value" : attachmentValue ]
}
var result: JSON = [ "text" : body, "annotations": [ annotation ] + attachmentAnnotations ]
if let quotedMessageServerID = quote?.quotedMessageServerID {
result["reply_to"] = quotedMessageServerID
}
return result
}
// MARK: Convenience
@objc public func addAttachment(kind: String, server: String, serverID: UInt64, contentType: String, size: UInt,
fileName: String, flags: UInt, width: UInt, height: UInt, caption: String?, url: String, linkPreviewURL: String?, linkPreviewTitle: String?) {
guard let kind = Attachment.Kind(rawValue: kind) else { preconditionFailure() }
let attachment = Attachment(kind: kind, server: server, serverID: serverID, contentType: contentType, size: size, fileName: fileName, flags: flags, width: width, height: height, caption: caption, url: url, linkPreviewURL: linkPreviewURL, linkPreviewTitle: linkPreviewTitle)
attachments.append(attachment)
}
private func getValidationData(for signatureVersion: UInt64) -> Data? {
var string = "\(body.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines))\(timestamp)"
if let quote = quote {
string += "\(quote.quotedMessageTimestamp)\(quote.quoteePublicKey)\(quote.quotedMessageBody.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines))"
if let quotedMessageServerID = quote.quotedMessageServerID {
string += "\(quotedMessageServerID)"
}
}
string += attachments.sorted { $0.serverID < $1.serverID }.map { "\($0.serverID)" }.joined(separator: "")
string += "\(signatureVersion)"
return string.data(using: String.Encoding.utf8)
}
}