2019-08-19 04:07:09 +02:00
import PromiseKit
@objc ( LKGroupChatAPI )
public final class LokiGroupChatAPI : NSObject {
2019-08-27 02:56:02 +02:00
private static let storage = OWSPrimaryStorage . shared ( )
2019-08-19 04:07:09 +02:00
2019-08-27 02:56:02 +02:00
// MARK: S e t t i n g s
private static let fallbackBatchCount = 40
private static let maxRetryCount : UInt = 4
// MARK: P u b l i c C h a t
@objc public static let publicChatServer = " https://chat.lokinet.org "
2019-08-19 05:41:23 +02:00
@objc public static let publicChatMessageType = " network.loki.messenger.publicChat "
2019-09-02 05:27:12 +02:00
@objc public static let publicChatServerID : UInt64 = 1
2019-08-22 05:14:35 +02:00
2019-09-12 05:59:57 +02:00
// M a r k : M o d e r a t o r s
typealias ModeratorArray = Set < String >
typealias ChannelDictionary = [ UInt64 : ModeratorArray ]
typealias ServerMapping = [ String : ChannelDictionary ]
// A m a p p i n g f r o m s e r v e r t o c h a n n e l t o m o d e r a t o r
private static var moderators = ServerMapping ( )
2019-08-27 02:56:02 +02:00
// MARK: C o n v e n i e n c e
private static var userDisplayName : String {
return SSKEnvironment . shared . contactsManager . displayName ( forPhoneIdentifier : userHexEncodedPublicKey ) ? ? " Anonymous "
}
private static var userKeyPair : ECKeyPair {
return OWSIdentityManager . shared ( ) . identityKeyPair ( ) !
}
private static var userHexEncodedPublicKey : String {
return userKeyPair . hexEncodedPublicKey
}
2019-08-22 04:34:24 +02:00
2019-08-27 02:56:02 +02:00
// MARK: E r r o r
2019-08-20 07:34:59 +02:00
public enum Error : Swift . Error {
2019-09-02 05:27:12 +02:00
case parsingFailed , decryptionFailed
2019-08-20 07:54:12 +02:00
}
2019-08-27 02:56:02 +02:00
// MARK: D a t a b a s e
private static let authTokenCollection = " LokiGroupChatAuthTokenCollection "
private static let lastMessageServerIDCollection = " LokiGroupChatLastMessageServerIDCollection "
2019-08-30 07:27:59 +02:00
private static let lastDeletionServerIDCollection = " LokiGroupChatLastDeletionServerIDCollection "
2019-08-27 02:56:02 +02:00
private static func getAuthTokenFromDatabase ( for server : String ) -> String ? {
var result : String ? = nil
storage . dbReadConnection . read { transaction in
result = transaction . object ( forKey : server , inCollection : authTokenCollection ) as ! String ?
}
return result
}
private static func setAuthToken ( for server : String , to newValue : String ) {
storage . dbReadWriteConnection . readWrite { transaction in
transaction . setObject ( newValue , forKey : server , inCollection : authTokenCollection )
}
}
2019-08-28 02:04:15 +02:00
private static func getLastMessageServerID ( for group : UInt64 , on server : String ) -> UInt ? {
2019-08-27 02:56:02 +02:00
var result : UInt ? = nil
storage . dbReadConnection . read { transaction in
result = transaction . object ( forKey : " \( server ) . \( group ) " , inCollection : lastMessageServerIDCollection ) as ! UInt ?
}
return result
}
2019-08-28 02:04:15 +02:00
private static func setLastMessageServerID ( for group : UInt64 , on server : String , to newValue : UInt64 ) {
2019-08-27 02:56:02 +02:00
storage . dbReadWriteConnection . readWrite { transaction in
transaction . setObject ( newValue , forKey : " \( server ) . \( group ) " , inCollection : lastMessageServerIDCollection )
}
}
2019-08-30 07:27:59 +02:00
private static func getLastDeletionServerID ( for group : UInt64 , on server : String ) -> UInt ? {
2019-08-27 02:56:02 +02:00
var result : UInt ? = nil
storage . dbReadConnection . read { transaction in
2019-08-30 07:27:59 +02:00
result = transaction . object ( forKey : " \( server ) . \( group ) " , inCollection : lastDeletionServerIDCollection ) as ! UInt ?
2019-08-27 02:56:02 +02:00
}
return result
}
2019-08-30 07:27:59 +02:00
private static func setLastDeletionServerID ( for group : UInt64 , on server : String , to newValue : UInt64 ) {
2019-08-27 02:56:02 +02:00
storage . dbReadWriteConnection . readWrite { transaction in
2019-08-30 07:27:59 +02:00
transaction . setObject ( newValue , forKey : " \( server ) . \( group ) " , inCollection : lastDeletionServerIDCollection )
2019-08-27 02:56:02 +02:00
}
}
// MARK: P r i v a t e A P I
private static func requestNewAuthToken ( for server : String ) -> Promise < String > {
print ( " [Loki] Requesting group chat auth token for server: \( server ) . " )
let queryParameters = " pubKey= \( userHexEncodedPublicKey ) "
let url = URL ( string : " \( server ) /loki/v1/get_challenge? \( queryParameters ) " ) !
2019-08-22 04:34:24 +02:00
let request = TSRequest ( url : url )
2019-08-20 07:54:12 +02:00
return TSNetworkManager . shared ( ) . makePromise ( request : request ) . map { $0 . responseObject } . map { rawResponse in
2019-08-23 08:14:19 +02:00
guard let json = rawResponse as ? JSON , let base64EncodedChallenge = json [ " cipherText64 " ] as ? String , let base64EncodedServerPublicKey = json [ " serverPubKey64 " ] as ? String ,
let challenge = Data ( base64Encoded : base64EncodedChallenge ) , var serverPublicKey = Data ( base64Encoded : base64EncodedServerPublicKey ) else {
2019-09-02 05:27:12 +02:00
throw Error . parsingFailed
2019-08-22 07:33:19 +02:00
}
2019-08-23 08:14:19 +02:00
// D i s c a r d t h e " 0 5 " p r e f i x i f n e e d e d
if ( serverPublicKey . count = = 33 ) {
let hexEncodedServerPublicKey = serverPublicKey . hexadecimalString
serverPublicKey = Data . data ( fromHex : hexEncodedServerPublicKey . substring ( from : 2 ) ) !
2019-08-23 02:27:22 +02:00
}
2019-08-23 08:14:19 +02:00
// T h e c h a l l e n g e i s p r e f i x e d b y t h e 1 6 b i t I V
guard let tokenAsData = try ? DiffieHellman . decrypt ( challenge , publicKey : serverPublicKey , privateKey : userKeyPair . privateKey ) ,
2019-08-27 02:56:02 +02:00
let token = String ( bytes : tokenAsData , encoding : . utf8 ) else {
2019-09-02 05:27:12 +02:00
throw Error . decryptionFailed
2019-08-22 07:33:19 +02:00
}
return token
2019-08-20 07:54:12 +02:00
}
2019-08-20 07:34:59 +02:00
}
2019-08-27 02:56:02 +02:00
private static func submitAuthToken ( _ token : String , for server : String ) -> Promise < String > {
print ( " [Loki] Submitting group chat auth token for server: \( server ) . " )
let url = URL ( string : " \( server ) /loki/v1/submit_challenge " ) !
2019-08-22 04:34:24 +02:00
let parameters = [ " pubKey " : userHexEncodedPublicKey , " token " : token ]
let request = TSRequest ( url : url , method : " POST " , parameters : parameters )
2019-08-22 05:14:35 +02:00
return TSNetworkManager . shared ( ) . makePromise ( request : request ) . map { _ in token }
}
2019-08-27 02:56:02 +02:00
private static func getAuthToken ( for server : String ) -> Promise < String > {
if let token = getAuthTokenFromDatabase ( for : server ) {
2019-08-23 08:14:19 +02:00
return Promise . value ( token )
} else {
2019-08-27 02:56:02 +02:00
return requestNewAuthToken ( for : server ) . then { submitAuthToken ( $0 , for : server ) } . map { token -> String in
setAuthToken ( for : server , to : token )
2019-08-22 05:14:35 +02:00
return token
}
}
2019-08-22 04:34:24 +02:00
}
2019-08-27 02:56:02 +02:00
// MARK: P u b l i c A P I
2019-08-28 02:04:15 +02:00
public static func getMessages ( for group : UInt64 , on server : String ) -> Promise < [ LokiGroupMessage ] > {
2019-08-27 02:56:02 +02:00
print ( " [Loki] Getting messages for group chat with ID: \( group ) on server: \( server ) . " )
var queryParameters = " include_annotations=1 "
if let lastMessageServerID = getLastMessageServerID ( for : group , on : server ) {
queryParameters += " &since_id= \( lastMessageServerID ) "
} else {
queryParameters += " &count=- \( fallbackBatchCount ) "
}
let url = URL ( string : " \( server ) /channels/ \( group ) /messages? \( queryParameters ) " ) !
2019-08-19 04:07:09 +02:00
let request = TSRequest ( url : url )
return TSNetworkManager . shared ( ) . makePromise ( request : request ) . map { $0 . responseObject } . map { rawResponse in
guard let json = rawResponse as ? JSON , let rawMessages = json [ " data " ] as ? [ JSON ] else {
2019-08-27 02:56:02 +02:00
print ( " [Loki] Couldn't parse messages for group chat with ID: \( group ) on server: \( server ) from: \( rawResponse ) . " )
2019-09-02 05:27:12 +02:00
throw Error . parsingFailed
2019-08-19 04:07:09 +02:00
}
return rawMessages . flatMap { message in
2019-09-11 03:53:47 +02:00
let isDeleted = ( message [ " is_deleted " ] as ? Int = = 1 )
guard ! isDeleted else { return nil }
2019-08-19 04:07:09 +02:00
guard let annotations = message [ " annotations " ] as ? [ JSON ] , let annotation = annotations . first , let value = annotation [ " value " ] as ? JSON ,
2019-08-28 02:04:15 +02:00
let serverID = message [ " id " ] as ? UInt64 , let body = message [ " text " ] as ? String , let hexEncodedPublicKey = value [ " source " ] as ? String , let displayName = value [ " from " ] as ? String ,
2019-08-23 08:14:19 +02:00
let timestamp = value [ " timestamp " ] as ? UInt64 else {
2019-08-27 02:56:02 +02:00
print ( " [Loki] Couldn't parse message for group chat with ID: \( group ) on server: \( server ) from: \( message ) . " )
2019-08-19 04:07:09 +02:00
return nil
}
2019-08-27 02:56:02 +02:00
let lastMessageServerID = getLastMessageServerID ( for : group , on : server )
if serverID > ( lastMessageServerID ? ? 0 ) { setLastMessageServerID ( for : group , on : server , to : serverID ) }
2019-09-11 03:53:47 +02:00
let quote : LokiGroupMessage . Quote ?
if let quoteAsJSON = value [ " quote " ] as ? JSON , let quotedMessageTimestamp = quoteAsJSON [ " id " ] as ? UInt64 , let quoteeHexEncodedPublicKey = quoteAsJSON [ " author " ] as ? String , let quotedMessageBody = quoteAsJSON [ " text " ] as ? String {
quote = LokiGroupMessage . Quote ( quotedMessageTimestamp : quotedMessageTimestamp , quoteeHexEncodedPublicKey : quoteeHexEncodedPublicKey , quotedMessageBody : quotedMessageBody )
} else {
quote = nil
}
return LokiGroupMessage ( serverID : serverID , hexEncodedPublicKey : hexEncodedPublicKey , displayName : displayName , body : body , type : publicChatMessageType , timestamp : timestamp , quote : quote )
2019-08-19 04:07:09 +02:00
}
}
}
2019-08-28 02:04:15 +02:00
public static func sendMessage ( _ message : LokiGroupMessage , to group : UInt64 , on server : String ) -> Promise < LokiGroupMessage > {
2019-08-27 02:56:02 +02:00
return getAuthToken ( for : server ) . then { token -> Promise < LokiGroupMessage > in
print ( " [Loki] Sending message to group chat with ID: \( group ) on server: \( server ) . " )
let url = URL ( string : " \( server ) /channels/ \( group ) /messages " ) !
2019-08-22 05:14:35 +02:00
let parameters = message . toJSON ( )
let request = TSRequest ( url : url , method : " POST " , parameters : parameters )
request . allHTTPHeaderFields = [ " Content-Type " : " application/json " , " Authorization " : " Bearer \( token ) " ]
let displayName = userDisplayName
return TSNetworkManager . shared ( ) . makePromise ( request : request ) . map { $0 . responseObject } . map { rawResponse in
2019-08-23 08:14:19 +02:00
// I S O 8 6 0 1 D a t e F o r m a t t e r d o e s n ' t s u p p o r t m i l l i s e c o n d s b e f o r e i O S 1 1
2019-08-23 02:27:22 +02:00
let dateFormatter = DateFormatter ( )
dateFormatter . dateFormat = " yyyy-MM-dd'T'HH:mm:ss.SSSZ "
2019-08-28 02:04:15 +02:00
guard let json = rawResponse as ? JSON , let messageAsJSON = json [ " data " ] as ? JSON , let serverID = messageAsJSON [ " id " ] as ? UInt64 , let body = messageAsJSON [ " text " ] as ? String ,
2019-08-27 02:56:02 +02:00
let dateAsString = messageAsJSON [ " created_at " ] as ? String , let date = dateFormatter . date ( from : dateAsString ) else {
2019-08-27 03:58:12 +02:00
print ( " [Loki] Couldn't parse message for group chat with ID: \( group ) on server: \( server ) from: \( rawResponse ) . " )
2019-09-02 05:27:12 +02:00
throw Error . parsingFailed
2019-08-22 05:14:35 +02:00
}
let timestamp = UInt64 ( date . timeIntervalSince1970 ) * 1000
2019-09-11 06:07:51 +02:00
return LokiGroupMessage ( serverID : serverID , hexEncodedPublicKey : userHexEncodedPublicKey , displayName : displayName , body : body , type : publicChatMessageType , timestamp : timestamp , quote : message . quote )
2019-08-19 04:07:09 +02:00
}
2019-08-27 02:56:02 +02:00
} . recover { error -> Promise < LokiGroupMessage > in
if let error = error as ? NetworkManagerError , error . statusCode = = 401 {
print ( " [Loki] Group chat auth token for: \( server ) expired; dropping it. " )
storage . dbReadWriteConnection . removeObject ( forKey : server , inCollection : authTokenCollection )
}
throw error
2019-09-04 07:55:17 +02:00
} . retryingIfNeeded ( maxRetryCount : maxRetryCount ) . map { message in
Analytics . shared . track ( " Group Message Sent " )
return message
} . recover { error -> Promise < LokiGroupMessage > in
Analytics . shared . track ( " Failed to Send Group Message " )
throw error
}
2019-08-27 02:56:02 +02:00
}
2019-08-28 02:29:14 +02:00
public static func getDeletedMessageServerIDs ( for group : UInt64 , on server : String ) -> Promise < [ UInt64 ] > {
2019-08-27 02:56:02 +02:00
print ( " [Loki] Getting deleted messages for group chat with ID: \( group ) on server: \( server ) . " )
2019-08-30 07:27:59 +02:00
let queryParameters : String
if let lastDeletionServerID = getLastDeletionServerID ( for : group , on : server ) {
queryParameters = " since_id= \( lastDeletionServerID ) "
} else {
queryParameters = " count= \( fallbackBatchCount ) "
}
let url = URL ( string : " \( server ) /loki/v1/channel/ \( group ) /deletes? \( queryParameters ) " ) !
2019-08-27 02:56:02 +02:00
let request = TSRequest ( url : url )
return TSNetworkManager . shared ( ) . makePromise ( request : request ) . map { $0 . responseObject } . map { rawResponse in
2019-08-30 07:27:59 +02:00
guard let json = rawResponse as ? JSON , let deletions = json [ " data " ] as ? [ JSON ] else {
2019-08-27 02:56:02 +02:00
print ( " [Loki] Couldn't parse deleted messages for group chat with ID: \( group ) on server: \( server ) from: \( rawResponse ) . " )
2019-09-02 05:27:12 +02:00
throw Error . parsingFailed
2019-08-27 02:56:02 +02:00
}
2019-08-30 07:27:59 +02:00
return deletions . flatMap { deletion in
guard let serverID = deletion [ " id " ] as ? UInt64 , let messageServerID = deletion [ " message_id " ] as ? UInt64 else {
print ( " [Loki] Couldn't parse deleted message for group chat with ID: \( group ) on server: \( server ) from: \( deletion ) . " )
2019-08-27 02:56:02 +02:00
return nil
2019-08-23 08:25:12 +02:00
}
2019-08-30 07:27:59 +02:00
let lastDeletionServerID = getLastDeletionServerID ( for : group , on : server )
if serverID > ( lastDeletionServerID ? ? 0 ) { setLastDeletionServerID ( for : group , on : server , to : serverID ) }
return messageServerID
2019-08-27 02:56:02 +02:00
}
}
2019-08-19 04:07:09 +02:00
}
2019-09-02 05:27:12 +02:00
public static func deleteMessage ( with messageID : UInt , for group : UInt64 , on server : String , isSentByUser : Bool ) -> Promise < Void > {
2019-08-28 08:07:14 +02:00
return getAuthToken ( for : server ) . then { token -> Promise < Void > in
2019-09-02 05:27:12 +02:00
let isModerationRequest = ! isSentByUser
print ( " [Loki] Deleting message with ID: \( messageID ) for group chat with ID: \( group ) on server: \( server ) (isModerationRequest = \( isModerationRequest ) ). " )
let urlAsString = isSentByUser ? " \( server ) /channels/ \( group ) /messages/ \( messageID ) " : " \( server ) /loki/v1/moderation/message/ \( messageID ) "
let url = URL ( string : urlAsString ) !
2019-08-28 08:07:14 +02:00
let request = TSRequest ( url : url , method : " DELETE " , parameters : [ : ] )
request . allHTTPHeaderFields = [ " Content-Type " : " application/json " , " Authorization " : " Bearer \( token ) " ]
2019-09-02 05:27:12 +02:00
return TSNetworkManager . shared ( ) . makePromise ( request : request ) . done { result -> Void in
print ( " [Loki] Deleted message with ID: \( messageID ) on server: \( server ) . " )
2019-09-02 07:44:24 +02:00
} . retryingIfNeeded ( maxRetryCount : maxRetryCount )
2019-08-28 08:07:14 +02:00
}
}
2019-09-02 05:27:12 +02:00
public static func userHasModerationPermission ( for group : UInt64 , on server : String ) -> Promise < Bool > {
2019-08-29 04:49:06 +02:00
return getAuthToken ( for : server ) . then { token -> Promise < Bool > in
let url = URL ( string : " \( server ) /loki/v1/user_info " ) !
let request = TSRequest ( url : url )
request . allHTTPHeaderFields = [ " Content-Type " : " application/json " , " Authorization " : " Bearer \( token ) " ]
return TSNetworkManager . shared ( ) . makePromise ( request : request ) . map { $0 . responseObject } . map { rawResponse in
guard let json = rawResponse as ? JSON , let data = json [ " data " ] as ? JSON else {
2019-09-02 05:27:12 +02:00
print ( " [Loki] Couldn't parse moderation permission for group chat with ID: \( group ) on server: \( server ) from: \( rawResponse ) . " )
throw Error . parsingFailed
2019-08-29 04:49:06 +02:00
}
return data [ " moderator_status " ] as ? Bool ? ? false
}
}
}
2019-09-12 06:09:35 +02:00
@objc ( isUserModerator : forGroup : onServer : )
2019-09-12 05:59:57 +02:00
public static func isUserModerator ( user hexEncodedPublicString : String , for group : UInt64 , on server : String ) -> Bool {
return self . moderators [ server ] ? [ group ] ? . contains ( hexEncodedPublicString ) ? ? false
}
public static func getModerators ( for group : UInt64 , on server : String ) -> Promise < Set < String > > {
let url = URL ( string : " \( server ) /loki/v1/channel/ \( group ) /get_moderators " ) !
let request = TSRequest ( url : url )
return TSNetworkManager . shared ( ) . makePromise ( request : request ) . map { $0 . responseObject } . map { rawResponse in
guard let json = rawResponse as ? JSON , let moderators = json [ " moderators " ] as ? [ String ] else {
print ( " [Loki] Couldn't parse moderators for group chat with ID: \( group ) on server: \( server ) from: \( rawResponse ) . " )
throw Error . parsingFailed
}
let moderatorSet = Set ( moderators ) ;
// U p d a t e o u r c a c h e
if ( self . moderators . keys . contains ( server ) ) {
self . moderators [ server ] ! [ group ] = moderatorSet
} else {
self . moderators [ server ] = [ group : moderatorSet ]
}
return moderatorSet
}
}
2019-08-27 02:56:02 +02:00
// MARK: P u b l i c A P I ( O b j - C )
@objc ( getMessagesForGroup : onServer : )
2019-08-28 02:04:15 +02:00
public static func objc_getMessages ( for group : UInt64 , on server : String ) -> AnyPromise {
2019-08-27 02:56:02 +02:00
return AnyPromise . from ( getMessages ( for : group , on : server ) )
2019-08-19 04:07:09 +02:00
}
2019-08-27 02:56:02 +02:00
@objc ( sendMessage : toGroup : onServer : )
2019-08-28 02:04:15 +02:00
public static func objc_sendMessage ( _ message : LokiGroupMessage , to group : UInt64 , on server : String ) -> AnyPromise {
2019-08-27 02:56:02 +02:00
return AnyPromise . from ( sendMessage ( message , to : group , on : server ) )
2019-08-19 04:07:09 +02:00
}
2019-08-28 08:07:14 +02:00
2019-09-02 05:27:12 +02:00
@objc ( deleteMessageWithID : forGroup : onServer : isSentByUser : )
public static func objc_deleteMessage ( with messageID : UInt , for group : UInt64 , on server : String , isSentByUser : Bool ) -> AnyPromise {
return AnyPromise . from ( deleteMessage ( with : messageID , for : group , on : server , isSentByUser : isSentByUser ) )
2019-08-28 08:07:14 +02:00
}
2019-08-19 04:07:09 +02:00
}