2021-04-26 03:14:45 +02:00
package org.session.libsession.messaging.open_groups
2020-12-02 06:38:12 +01:00
import nl.komponents.kovenant.Kovenant
import nl.komponents.kovenant.Promise
import nl.komponents.kovenant.deferred
import nl.komponents.kovenant.functional.map
import nl.komponents.kovenant.then
2021-04-26 03:14:45 +02:00
import org.session.libsession.messaging.MessagingModuleConfiguration
2021-04-27 06:48:57 +02:00
import org.session.libsession.messaging.file_server.FileServerAPI
2021-03-31 06:15:50 +02:00
import org.session.libsession.messaging.utilities.DotNetAPI
2021-04-27 06:48:57 +02:00
import org.session.libsession.utilities.DownloadUtilities
2020-12-02 06:38:12 +01:00
import org.session.libsignal.service.loki.utilities.retryIfNeeded
2021-03-31 06:15:50 +02:00
import org.session.libsignal.utilities.*
2021-02-01 02:10:48 +01:00
import org.session.libsignal.utilities.Base64
2021-03-31 06:15:50 +02:00
import org.session.libsignal.utilities.logging.Log
2020-12-02 06:38:12 +01:00
import java.io.ByteArrayOutputStream
2021-04-23 08:09:47 +02:00
import java.io.IOException
2020-12-02 06:38:12 +01:00
import java.text.SimpleDateFormat
import java.util.*
object OpenGroupAPI : DotNetAPI ( ) {
private val moderators : HashMap < String , HashMap < Long , Set < String > > > = hashMapOf ( ) // Server URL to (channel ID to set of moderator IDs)
2021-01-27 00:54:25 +01:00
val sharedContext = Kovenant . createContext ( )
2020-12-02 06:38:12 +01:00
// region Settings
private val fallbackBatchCount = 64
private val maxRetryCount = 8
// endregion
// region Convenience
private val channelInfoType = " net.patter-app.settings "
private val attachmentType = " net.app.core.oembed "
@JvmStatic
2021-01-27 00:54:25 +01:00
val openGroupMessageType = " network.loki.messenger.publicChat "
2020-12-02 06:38:12 +01:00
@JvmStatic
2021-01-27 00:54:25 +01:00
val profilePictureType = " network.loki.messenger.avatar "
2020-12-02 06:38:12 +01:00
fun getDefaultChats ( ) : List < OpenGroup > {
return listOf ( ) // Don't auto-join any open groups right now
}
2021-03-11 04:45:08 +01:00
@JvmStatic
2021-01-27 00:54:25 +01:00
fun isUserModerator ( hexEncodedPublicKey : String , channel : Long , server : String ) : Boolean {
2020-12-02 06:38:12 +01:00
if ( moderators [ server ] != null && moderators [ server ] !! [ channel ] != null ) {
return moderators [ server ] !! [ channel ] !! . contains ( hexEncodedPublicKey )
}
return false
}
// endregion
// region Public API
2021-01-27 00:54:25 +01:00
fun getMessages ( channel : Long , server : String ) : Promise < List < OpenGroupMessage > , Exception > {
2020-12-02 06:38:12 +01:00
Log . d ( " Loki " , " Getting messages for open group with ID: $channel on server: $server . " )
2021-04-26 03:14:45 +02:00
val storage = MessagingModuleConfiguration . shared . storage
2020-12-02 06:38:12 +01:00
val parameters = mutableMapOf < String , Any > ( " include_annotations " to 1 )
val lastMessageServerID = storage . getLastMessageServerID ( channel , server )
if ( lastMessageServerID != null ) {
parameters [ " since_id " ] = lastMessageServerID
} else {
parameters [ " count " ] = fallbackBatchCount
parameters [ " include_deleted " ] = 0
}
return execute ( HTTPVerb . GET , server , " channels/ $channel /messages " , parameters = parameters ) . then ( sharedContext ) { json ->
try {
val data = json [ " data " ] as List < Map < * , * > >
val messages = data . mapNotNull { message ->
try {
val isDeleted = message [ " is_deleted " ] as ? Boolean ?: false
if ( isDeleted ) { return @mapNotNull null }
// Ignore messages without annotations
if ( message [ " annotations " ] == null ) { return @mapNotNull null }
val annotation = ( message [ " annotations " ] as List < Map < * , * > > ) . find {
( ( it [ " type " ] as ? String ?: " " ) == openGroupMessageType ) && it [ " value " ] != null
} ?: return @mapNotNull null
val value = annotation [ " value " ] as Map < * , * >
val serverID = message [ " id " ] as ? Long ?: ( message [ " id " ] as ? Int ) ?. toLong ( ) ?: ( message [ " id " ] as String ) . toLong ( )
val user = message [ " user " ] as Map < * , * >
val publicKey = user [ " username " ] as String
val displayName = user [ " name " ] as ? String ?: " Anonymous "
var profilePicture : OpenGroupMessage . ProfilePicture ? = null
if ( user [ " annotations " ] != null ) {
val profilePictureAnnotation = ( user [ " annotations " ] as List < Map < * , * > > ) . find {
( ( it [ " type " ] as ? String ?: " " ) == profilePictureType ) && it [ " value " ] != null
}
val profilePictureAnnotationValue = profilePictureAnnotation ?. get ( " value " ) as ? Map < * , * >
if ( profilePictureAnnotationValue != null && profilePictureAnnotationValue [ " profileKey " ] != null && profilePictureAnnotationValue [ " url " ] != null ) {
try {
val profileKey = Base64 . decode ( profilePictureAnnotationValue [ " profileKey " ] as String )
val url = profilePictureAnnotationValue [ " url " ] as String
profilePicture = OpenGroupMessage . ProfilePicture ( profileKey , url )
} catch ( e : Exception ) { }
}
}
@Suppress ( " NAME_SHADOWING " ) val body = message [ " text " ] as String
val timestamp = value [ " timestamp " ] as ? Long ?: ( value [ " timestamp " ] as ? Int ) ?. toLong ( ) ?: ( value [ " timestamp " ] as String ) . toLong ( )
var quote : OpenGroupMessage . Quote ? = null
if ( value [ " quote " ] != null ) {
val replyTo = message [ " reply_to " ] as ? Long ?: ( message [ " reply_to " ] as ? Int ) ?. toLong ( ) ?: ( message [ " reply_to " ] as String ) . toLong ( )
val quoteAnnotation = value [ " quote " ] as ? Map < * , * >
val quoteTimestamp = quoteAnnotation ?. get ( " id " ) as ? Long ?: ( quoteAnnotation ?. get ( " id " ) as ? Int ) ?. toLong ( ) ?: ( quoteAnnotation ?. get ( " id " ) as ? String ) ?. toLong ( ) ?: 0L
val author = quoteAnnotation ?. get ( " author " ) as ? String
val text = quoteAnnotation ?. get ( " text " ) as ? String
quote = if ( quoteTimestamp > 0L && author != null && text != null ) OpenGroupMessage . Quote ( quoteTimestamp , author , text , replyTo ) else null
}
val attachmentsAsJSON = ( message [ " annotations " ] as List < Map < * , * > > ) . filter {
( ( it [ " type " ] as ? String ?: " " ) == attachmentType ) && it [ " value " ] != null
}
val attachments = attachmentsAsJSON . mapNotNull { it [ " value " ] as ? Map < * , * > } . mapNotNull { attachmentAsJSON ->
try {
val kindAsString = attachmentAsJSON [ " lokiType " ] as String
val kind = OpenGroupMessage . Attachment . Kind . values ( ) . first { it . rawValue == kindAsString }
val id = attachmentAsJSON [ " id " ] as ? Long ?: ( attachmentAsJSON [ " id " ] as ? Int ) ?. toLong ( ) ?: ( attachmentAsJSON [ " id " ] as String ) . toLong ( )
val contentType = attachmentAsJSON [ " contentType " ] as String
val size = attachmentAsJSON [ " size " ] as ? Int ?: ( attachmentAsJSON [ " size " ] as ? Long ) ?. toInt ( ) ?: ( attachmentAsJSON [ " size " ] as String ) . toInt ( )
2021-03-11 04:05:48 +01:00
val fileName = attachmentAsJSON [ " fileName " ] as ? String
2020-12-02 06:38:12 +01:00
val flags = 0
val url = attachmentAsJSON [ " url " ] as String
val caption = attachmentAsJSON [ " caption " ] as ? String
val linkPreviewURL = attachmentAsJSON [ " linkPreviewUrl " ] as ? String
val linkPreviewTitle = attachmentAsJSON [ " linkPreviewTitle " ] as ? String
if ( kind == OpenGroupMessage . Attachment . Kind . LinkPreview && ( linkPreviewURL == null || linkPreviewTitle == null ) ) {
null
} else {
OpenGroupMessage . Attachment ( kind , server , id , contentType , size , fileName , flags , 0 , 0 , caption , url , linkPreviewURL , linkPreviewTitle )
}
} catch ( e : Exception ) {
Log . d ( " Loki " , " Couldn't parse attachment due to error: $e . " )
null
}
}
// Set the last message server ID here to avoid the situation where a message doesn't have a valid signature and this function is called over and over
@Suppress ( " NAME_SHADOWING " ) val lastMessageServerID = storage . getLastMessageServerID ( channel , server )
if ( serverID > lastMessageServerID ?: 0 ) { storage . setLastMessageServerID ( channel , server , serverID ) }
val hexEncodedSignature = value [ " sig " ] as String
val signatureVersion = value [ " sigver " ] as ? Long ?: ( value [ " sigver " ] as ? Int ) ?. toLong ( ) ?: ( value [ " sigver " ] as String ) . toLong ( )
val signature = OpenGroupMessage . Signature ( Hex . fromStringCondensed ( hexEncodedSignature ) , signatureVersion )
val format = SimpleDateFormat ( " yyyy-MM-dd'T'HH:mm:ss.SSS'Z' " , Locale . US )
format . timeZone = TimeZone . getTimeZone ( " GMT " )
val dateAsString = message [ " created_at " ] as String
val serverTimestamp = format . parse ( dateAsString ) . time
// Verify the message
2021-01-08 01:11:23 +01:00
val groupMessage = OpenGroupMessage ( serverID , publicKey , displayName , body , timestamp , openGroupMessageType , quote , attachments . toMutableList ( ) , profilePicture , signature , serverTimestamp )
2020-12-02 06:38:12 +01:00
if ( groupMessage . hasValidSignature ( ) ) groupMessage else null
} catch ( exception : Exception ) {
Log . d ( " Loki " , " Couldn't parse message for open group with ID: $channel on server: $server from: ${JsonUtil.toJson(message)} . Exception: ${exception.message} " )
return @mapNotNull null
}
} . sortedBy { it . serverTimestamp }
messages
} catch ( exception : Exception ) {
Log . d ( " Loki " , " Couldn't parse messages for open group with ID: $channel on server: $server . " )
throw exception
}
}
}
2021-03-31 06:15:50 +02:00
@JvmStatic
2021-01-27 00:54:25 +01:00
fun getDeletedMessageServerIDs ( channel : Long , server : String ) : Promise < List < Long > , Exception > {
2020-12-02 06:38:12 +01:00
Log . d ( " Loki " , " Getting deleted messages for open group with ID: $channel on server: $server . " )
2021-04-26 03:14:45 +02:00
val storage = MessagingModuleConfiguration . shared . storage
2020-12-02 06:38:12 +01:00
val parameters = mutableMapOf < String , Any > ( )
val lastDeletionServerID = storage . getLastDeletionServerID ( channel , server )
if ( lastDeletionServerID != null ) {
parameters [ " since_id " ] = lastDeletionServerID
} else {
parameters [ " count " ] = fallbackBatchCount
}
return execute ( HTTPVerb . GET , server , " loki/v1/channel/ $channel /deletes " , parameters = parameters ) . then ( sharedContext ) { json ->
try {
val deletedMessageServerIDs = ( json [ " data " ] as List < Map < * , * > > ) . mapNotNull { deletion ->
try {
val serverID = deletion [ " id " ] as ? Long ?: ( deletion [ " id " ] as ? Int ) ?. toLong ( ) ?: ( deletion [ " id " ] as String ) . toLong ( )
val messageServerID = deletion [ " message_id " ] as ? Long ?: ( deletion [ " message_id " ] as ? Int ) ?. toLong ( ) ?: ( deletion [ " message_id " ] as String ) . toLong ( )
@Suppress ( " NAME_SHADOWING " ) val lastDeletionServerID = storage . getLastDeletionServerID ( channel , server )
if ( serverID > ( lastDeletionServerID ?: 0 ) ) { storage . setLastDeletionServerID ( channel , server , serverID ) }
messageServerID
} catch ( exception : Exception ) {
Log . d ( " Loki " , " Couldn't parse deleted message for open group with ID: $channel on server: $server . Exception: ${exception.message} " )
return @mapNotNull null
}
}
deletedMessageServerIDs
} catch ( exception : Exception ) {
Log . d ( " Loki " , " Couldn't parse deleted messages for open group with ID: $channel on server: $server . " )
throw exception
}
}
}
2021-03-31 06:15:50 +02:00
@JvmStatic
2021-01-27 00:54:25 +01:00
fun sendMessage ( message : OpenGroupMessage , channel : Long , server : String ) : Promise < OpenGroupMessage , Exception > {
2020-12-02 06:38:12 +01:00
val deferred = deferred < OpenGroupMessage , Exception > ( )
2021-04-26 03:14:45 +02:00
val storage = MessagingModuleConfiguration . shared . storage
2020-12-02 06:38:12 +01:00
val userKeyPair = storage . getUserKeyPair ( ) ?: throw Error . Generic
val userDisplayName = storage . getUserDisplayName ( ) ?: throw Error . Generic
2021-01-27 00:54:25 +01:00
ThreadUtils . queue {
2021-01-13 07:11:30 +01:00
val signedMessage = message . sign ( userKeyPair . second )
2020-12-02 06:38:12 +01:00
if ( signedMessage == null ) {
deferred . reject ( Error . SigningFailed )
} else {
retryIfNeeded ( maxRetryCount ) {
Log . d ( " Loki " , " Sending message to open group with ID: $channel on server: $server . " )
val parameters = signedMessage . toJSON ( )
execute ( HTTPVerb . POST , server , " channels/ $channel /messages " , parameters = parameters ) . then ( sharedContext ) { json ->
try {
val data = json [ " data " ] as Map < * , * >
val serverID = ( data [ " id " ] as ? Long ) ?: ( data [ " id " ] as ? Int ) ?. toLong ( ) ?: ( data [ " id " ] as String ) . toLong ( )
val text = data [ " text " ] as String
val format = SimpleDateFormat ( " yyyy-MM-dd'T'HH:mm:ss.SSS'Z' " , Locale . US )
format . timeZone = TimeZone . getTimeZone ( " GMT " )
val dateAsString = data [ " created_at " ] as String
val timestamp = format . parse ( dateAsString ) . time
2021-03-02 04:13:12 +01:00
OpenGroupMessage ( serverID , userKeyPair . first , userDisplayName , text , timestamp , openGroupMessageType , message . quote , message . attachments , null , signedMessage . signature , timestamp )
2020-12-02 06:38:12 +01:00
} catch ( exception : Exception ) {
Log . d ( " Loki " , " Couldn't parse message for open group with ID: $channel on server: $server . " )
throw exception
}
}
} . success {
deferred . resolve ( it )
} . fail {
deferred . reject ( it )
}
}
2021-01-27 00:54:25 +01:00
}
2020-12-02 06:38:12 +01:00
return deferred . promise
}
2021-01-27 00:54:25 +01:00
fun deleteMessage ( messageServerID : Long , channel : Long , server : String , isSentByUser : Boolean ) : Promise < Long , Exception > {
2020-12-02 06:38:12 +01:00
return retryIfNeeded ( maxRetryCount ) {
val isModerationRequest = !is SentByUser
Log . d ( " Loki " , " Deleting message with ID: $messageServerID from open group with ID: $channel on server: $server (isModerationRequest = $isModerationRequest ). " )
val endpoint = if ( isSentByUser ) " channels/ $channel /messages/ $messageServerID " else " loki/v1/moderation/message/ $messageServerID "
execute ( HTTPVerb . DELETE , server , endpoint , isJSONRequired = false ) . then {
Log . d ( " Loki " , " Deleted message with ID: $messageServerID from open group with ID: $channel on server: $server . " )
messageServerID
}
}
}
2021-03-11 04:45:08 +01:00
@JvmStatic
2021-01-27 00:54:25 +01:00
fun deleteMessages ( messageServerIDs : List < Long > , channel : Long , server : String , isSentByUser : Boolean ) : Promise < List < Long > , Exception > {
2020-12-02 06:38:12 +01:00
return retryIfNeeded ( maxRetryCount ) {
val isModerationRequest = !is SentByUser
val parameters = mapOf ( " ids " to messageServerIDs . joinToString ( " , " ) )
Log . d ( " Loki " , " Deleting messages with IDs: ${messageServerIDs.joinToString()} from open group with ID: $channel on server: $server (isModerationRequest = $isModerationRequest ). " )
val endpoint = if ( isSentByUser ) " loki/v1/messages " else " loki/v1/moderation/messages "
execute ( HTTPVerb . DELETE , server , endpoint , parameters = parameters , isJSONRequired = false ) . then { json ->
Log . d ( " Loki " , " Deleted messages with IDs: $messageServerIDs from open group with ID: $channel on server: $server . " )
messageServerIDs
}
}
}
2021-03-31 06:15:50 +02:00
@JvmStatic
2021-01-27 00:54:25 +01:00
fun getModerators ( channel : Long , server : String ) : Promise < Set < String > , Exception > {
2020-12-02 06:38:12 +01:00
return execute ( HTTPVerb . GET , server , " loki/v1/channel/ $channel /get_moderators " ) . then ( sharedContext ) { json ->
try {
@Suppress ( " UNCHECKED_CAST " ) val moderators = json [ " moderators " ] as ? List < String >
val moderatorsAsSet = moderators . orEmpty ( ) . toSet ( )
if ( this . moderators [ server ] != null ) {
this . moderators [ server ] !! [ channel ] = moderatorsAsSet
} else {
this . moderators [ server ] = hashMapOf ( channel to moderatorsAsSet )
}
moderatorsAsSet
} catch ( exception : Exception ) {
Log . d ( " Loki " , " Couldn't parse moderators for open group with ID: $channel on server: $server . " )
throw exception
}
}
}
2021-03-31 06:15:50 +02:00
@JvmStatic
2021-01-27 00:54:25 +01:00
fun getChannelInfo ( channel : Long , server : String ) : Promise < OpenGroupInfo , Exception > {
2020-12-02 06:38:12 +01:00
return retryIfNeeded ( maxRetryCount ) {
val parameters = mapOf ( " include_annotations " to 1 )
execute ( HTTPVerb . GET , server , " /channels/ $channel " , parameters = parameters ) . then ( sharedContext ) { json ->
try {
val data = json [ " data " ] as Map < * , * >
val annotations = data [ " annotations " ] as List < Map < * , * > >
val annotation = annotations . find { ( it [ " type " ] as ? String ?: " " ) == channelInfoType } ?: throw Error . ParsingFailed
val info = annotation [ " value " ] as Map < * , * >
val displayName = info [ " name " ] as String
val countInfo = data [ " counts " ] as Map < * , * >
val memberCount = countInfo [ " subscribers " ] as ? Int ?: ( countInfo [ " subscribers " ] as ? Long ) ?. toInt ( ) ?: ( countInfo [ " subscribers " ] as String ) . toInt ( )
val profilePictureURL = info [ " avatar " ] as String
val publicChatInfo = OpenGroupInfo ( displayName , profilePictureURL , memberCount )
2021-04-26 03:14:45 +02:00
MessagingModuleConfiguration . shared . storage . setUserCount ( channel , server , memberCount )
2020-12-02 06:38:12 +01:00
publicChatInfo
} catch ( exception : Exception ) {
Log . d ( " Loki " , " Couldn't parse info for open group with ID: $channel on server: $server . " )
throw exception
}
}
}
}
2021-03-31 06:15:50 +02:00
@JvmStatic
2021-01-27 00:54:25 +01:00
fun updateProfileIfNeeded ( channel : Long , server : String , groupID : String , info : OpenGroupInfo , isForcedUpdate : Boolean ) {
2021-04-26 03:14:45 +02:00
val storage = MessagingModuleConfiguration . shared . storage
2020-12-02 06:38:12 +01:00
storage . setUserCount ( channel , server , info . memberCount )
storage . updateTitle ( groupID , info . displayName )
// Download and update profile picture if needed
val oldProfilePictureURL = storage . getOpenGroupProfilePictureURL ( channel , server )
if ( isForcedUpdate || oldProfilePictureURL != info . profilePictureURL ) {
val profilePictureAsByteArray = downloadOpenGroupProfilePicture ( server , info . profilePictureURL ) ?: return
storage . updateProfilePicture ( groupID , profilePictureAsByteArray )
storage . setOpenGroupProfilePictureURL ( channel , server , info . profilePictureURL )
}
}
2021-03-31 06:15:50 +02:00
@JvmStatic
2021-01-27 00:54:25 +01:00
fun downloadOpenGroupProfilePicture ( server : String , endpoint : String ) : ByteArray ? {
2020-12-02 06:38:12 +01:00
val url = " ${server.removeSuffix("/")} / ${endpoint.removePrefix("/")} "
Log . d ( " Loki " , " Downloading open group profile picture from \" $url \" . " )
val outputStream = ByteArrayOutputStream ( )
try {
2021-04-27 06:48:57 +02:00
DownloadUtilities . downloadFile ( outputStream , url , FileServerAPI . maxFileSize , null )
2020-12-02 06:38:12 +01:00
Log . d ( " Loki " , " Open group profile picture was successfully loaded from \" $url \" " )
return outputStream . toByteArray ( )
} catch ( e : Exception ) {
Log . d ( " Loki " , " Failed to download open group profile picture from \" $url \" due to error: $e . " )
return null
} finally {
outputStream . close ( )
}
}
2021-03-31 06:15:50 +02:00
@JvmStatic
2021-01-27 00:54:25 +01:00
fun join ( channel : Long , server : String ) : Promise < Unit , Exception > {
2020-12-02 06:38:12 +01:00
return retryIfNeeded ( maxRetryCount ) {
execute ( HTTPVerb . POST , server , " /channels/ $channel /subscribe " ) . then {
Log . d ( " Loki " , " Joined channel with ID: $channel on server: $server . " )
}
}
}
2021-03-31 06:15:50 +02:00
@JvmStatic
2021-01-27 00:54:25 +01:00
fun leave ( channel : Long , server : String ) : Promise < Unit , Exception > {
2020-12-02 06:38:12 +01:00
return retryIfNeeded ( maxRetryCount ) {
execute ( HTTPVerb . DELETE , server , " /channels/ $channel /subscribe " ) . then {
Log . d ( " Loki " , " Left channel with ID: $channel on server: $server . " )
}
}
}
2021-03-11 04:45:08 +01:00
@JvmStatic
2021-01-27 00:54:25 +01:00
fun ban ( publicKey : String , server : String ) : Promise < Unit , Exception > {
return retryIfNeeded ( maxRetryCount ) {
execute ( HTTPVerb . POST , server , " /loki/v1/moderation/blacklist/@ $publicKey " ) . then {
Log . d ( " Loki " , " Banned user with ID: $publicKey from $server " )
}
}
}
2021-03-31 06:15:50 +02:00
@JvmStatic
2021-01-27 00:54:25 +01:00
fun getDisplayNames ( publicKeys : Set < String > , server : String ) : Promise < Map < String , String > , Exception > {
2020-12-02 06:38:12 +01:00
return getUserProfiles ( publicKeys , server , false ) . map ( sharedContext ) { json ->
val mapping = mutableMapOf < String , String > ( )
for ( user in json ) {
if ( user [ " username " ] != null ) {
val publicKey = user [ " username " ] as String
val displayName = user [ " name " ] as ? String ?: " Anonymous "
mapping [ publicKey ] = displayName
}
}
mapping
}
}
2021-03-31 06:15:50 +02:00
@JvmStatic
2021-01-27 00:54:25 +01:00
fun setDisplayName ( newDisplayName : String ? , server : String ) : Promise < Unit , Exception > {
2020-12-02 06:38:12 +01:00
Log . d ( " Loki " , " Updating display name on server: $server . " )
val parameters = mapOf ( " name " to ( newDisplayName ?: " " ) )
return execute ( HTTPVerb . PATCH , server , " users/me " , parameters = parameters ) . map { Unit }
}
2021-03-31 06:15:50 +02:00
@JvmStatic
2021-01-27 00:54:25 +01:00
fun setProfilePicture ( server : String , profileKey : ByteArray , url : String ? ) : Promise < Unit , Exception > {
2020-12-02 06:38:12 +01:00
return setProfilePicture ( server , Base64 . encodeBytes ( profileKey ) , url )
}
2021-01-27 00:54:25 +01:00
fun setProfilePicture ( server : String , profileKey : String , url : String ? ) : Promise < Unit , Exception > {
2020-12-02 06:38:12 +01:00
Log . d ( " Loki " , " Updating profile picture on server: $server . " )
val value = when ( url ) {
null -> null
else -> mapOf ( " profileKey " to profileKey , " url " to url )
}
// TODO: This may actually completely replace the annotations, have to double check it
return setSelfAnnotation ( server , profilePictureType , value ) . map { Unit } . fail {
Log . d ( " Loki " , " Failed to update profile picture due to error: $it . " )
}
}
// endregion
}