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.
		
		
		
		
		
			
		
			
				
	
	
		
			197 lines
		
	
	
		
			9.0 KiB
		
	
	
	
		
			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)
 | 
						|
    }
 | 
						|
}
 |