2019-06-09 11:57:33 +00:00
/ *
* Copyright ( c ) 2019 Zender & Kurtz GbR .
*
* Authors:
* Christian Pauly < krille @ famedly . com >
* Marcel Radzio < mtrnord @ famedly . com >
*
* This file is part of famedlysdk .
*
* famedlysdk is free software: you can redistribute it and / or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation , either version 3 of the License , or
* ( at your option ) any later version .
*
* famedlysdk is distributed in the hope that it will be useful ,
* but WITHOUT ANY WARRANTY ; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE . See the
* GNU General Public License for more details .
*
* You should have received a copy of the GNU General Public License
2019-06-21 07:46:53 +00:00
* along with famedlysdk . If not , see < http: //www.gnu.org/licenses/>.
2019-06-09 11:57:33 +00:00
* /
2020-01-04 10:29:38 +00:00
import ' dart:async ' ;
2020-02-15 07:48:41 +00:00
import ' dart:convert ' ;
2020-01-04 10:29:38 +00:00
2020-02-04 13:41:13 +00:00
import ' package:famedlysdk/famedlysdk.dart ' ;
Update lib/src/client.dart, lib/src/user.dart, lib/src/timeline.dart, lib/src/room.dart, lib/src/presence.dart, lib/src/event.dart, lib/src/utils/profile.dart, lib/src/utils/receipt.dart, test/client_test.dart, test/event_test.dart, test/presence_test.dart, test/room_test.dart, test/timeline_test.dart, test/user_test.dart files
2020-01-04 17:56:17 +00:00
import ' package:famedlysdk/src/client.dart ' ;
import ' package:famedlysdk/src/event.dart ' ;
import ' package:famedlysdk/src/room_account_data.dart ' ;
import ' package:famedlysdk/src/sync/event_update.dart ' ;
import ' package:famedlysdk/src/sync/room_update.dart ' ;
import ' package:famedlysdk/src/utils/matrix_exception.dart ' ;
import ' package:famedlysdk/src/utils/matrix_file.dart ' ;
import ' package:famedlysdk/src/utils/mx_content.dart ' ;
2020-02-15 07:48:41 +00:00
import ' package:famedlysdk/src/utils/session_key.dart ' ;
2019-09-09 13:22:02 +00:00
import ' package:mime_type/mime_type.dart ' ;
2020-02-15 07:48:41 +00:00
import ' package:olm/olm.dart ' as olm ;
2019-07-12 09:26:07 +00:00
Update lib/src/client.dart, lib/src/user.dart, lib/src/timeline.dart, lib/src/room.dart, lib/src/presence.dart, lib/src/event.dart, lib/src/utils/profile.dart, lib/src/utils/receipt.dart, test/client_test.dart, test/event_test.dart, test/presence_test.dart, test/room_test.dart, test/timeline_test.dart, test/user_test.dart files
2020-01-04 17:56:17 +00:00
import ' ./user.dart ' ;
import ' timeline.dart ' ;
import ' utils/states_map.dart ' ;
2019-06-09 10:16:48 +00:00
2020-01-18 14:49:15 +00:00
enum PushRuleState { notify , mentions_only , dont_notify }
enum JoinRules { public , knock , invite , private }
enum GuestAccess { can_join , forbidden }
enum HistoryVisibility { invited , joined , shared , world_readable }
2019-06-09 12:33:25 +00:00
/// Represents a Matrix room.
2019-06-09 10:16:48 +00:00
class Room {
2019-06-11 08:51:45 +00:00
/// The full qualified Matrix ID for the room in the format '!localid:server.abc'.
final String id ;
/// Membership status of the user for this room.
2019-07-12 09:26:07 +00:00
Membership membership ;
2019-06-11 08:51:45 +00:00
/// The count of unread notifications.
2019-06-09 10:16:48 +00:00
int notificationCount ;
2019-06-11 08:51:45 +00:00
/// The count of highlighted notifications.
2019-06-09 10:16:48 +00:00
int highlightCount ;
2019-06-11 08:51:45 +00:00
2019-09-03 14:34:38 +00:00
/// A token that can be supplied to the from parameter of the rooms/{roomId}/messages endpoint.
2019-06-11 08:51:45 +00:00
String prev_batch ;
2019-09-03 14:34:38 +00:00
/// The users which can be used to generate a room name if the room does not have one.
/// Required if the room's m.room.name or m.room.canonical_alias state events are unset or empty.
2019-08-08 12:31:47 +00:00
List < String > mHeroes = [ ] ;
2019-09-03 14:34:38 +00:00
/// The number of users with membership of join, including the client's own user ID.
2019-08-07 08:17:03 +00:00
int mJoinedMemberCount ;
2019-09-03 14:34:38 +00:00
/// The number of users with membership of invite.
2019-08-07 08:17:03 +00:00
int mInvitedMemberCount ;
2019-11-20 13:02:23 +00:00
StatesMap states = StatesMap ( ) ;
2019-06-11 08:51:45 +00:00
2019-10-20 09:44:14 +00:00
/// Key-Value store for ephemerals.
Map < String , RoomAccountData > ephemerals = { } ;
2019-09-03 14:34:38 +00:00
/// Key-Value store for private account data only visible for this user.
2019-08-07 10:27:02 +00:00
Map < String , RoomAccountData > roomAccountData = { } ;
2019-08-07 08:32:18 +00:00
2020-02-15 07:48:41 +00:00
olm . OutboundGroupSession get outboundGroupSession = > _outboundGroupSession ;
olm . OutboundGroupSession _outboundGroupSession ;
2020-02-27 08:41:49 +00:00
List < String > _outboundGroupSessionDevices ;
2020-02-15 07:48:41 +00:00
/// Clears the existing outboundGroupSession, tries to create a new one and
/// stores it as an ingoingGroupSession in the [sessionKeys]. Then sends the
/// new session encrypted with olm to all non-blocked devices using
/// to-device-messaging.
Future < void > createOutboundGroupSession ( ) async {
2020-02-27 08:41:49 +00:00
await clearOutboundGroupSession ( wipe: true ) ;
List < DeviceKeys > deviceKeys = await getUserDeviceKeys ( ) ;
_outboundGroupSessionDevices = [ ] ;
for ( DeviceKeys keys in deviceKeys ) {
_outboundGroupSessionDevices . add ( keys . deviceId ) ;
}
_outboundGroupSessionDevices . sort ( ) ;
2020-02-15 07:48:41 +00:00
try {
_outboundGroupSession = olm . OutboundGroupSession ( ) ;
_outboundGroupSession . create ( ) ;
} catch ( e ) {
_outboundGroupSession = null ;
print ( " [LibOlm] Unable to create new outboundGroupSession: " +
e . toString ( ) ) ;
}
if ( _outboundGroupSession = = null ) return ;
2020-02-18 07:42:52 +00:00
await _storeOutboundGroupSession ( ) ;
2020-02-15 07:48:41 +00:00
// Add as an inboundSession to the [sessionKeys].
Map < String , dynamic > rawSession = {
" algorithm " : " m.megolm.v1.aes-sha2 " ,
" room_id " : this . id ,
" session_id " : _outboundGroupSession . session_id ( ) ,
" session_key " : _outboundGroupSession . session_key ( ) ,
} ;
setSessionKey ( rawSession [ " session_id " ] , rawSession ) ;
try {
await client . sendToDevice ( deviceKeys , " m.room_key " , rawSession ) ;
} catch ( e ) {
print (
" [LibOlm] Unable to send the session key to the participating devices: " +
e . toString ( ) ) ;
await clearOutboundGroupSession ( ) ;
}
return ;
}
2020-02-18 07:42:52 +00:00
Future < void > _storeOutboundGroupSession ( ) async {
2020-02-18 09:23:55 +00:00
if ( _outboundGroupSession = = null ) return ;
2020-02-18 07:42:52 +00:00
await client . storeAPI ? . setItem (
" /clients/ ${ client . deviceID } /rooms/ ${ this . id } /outbound_group_session " ,
_outboundGroupSession . pickle ( client . userID ) ) ;
2020-02-27 08:41:49 +00:00
await client . storeAPI ? . setItem (
" /clients/ ${ client . deviceID } /rooms/ ${ this . id } /outbound_group_session_devices " ,
json . encode ( _outboundGroupSessionDevices ) ) ;
2020-02-18 07:42:52 +00:00
return ;
}
2020-02-27 08:41:49 +00:00
/// Clears the existing outboundGroupSession but first checks if the participating
/// devices have been changed. Returns false if the session has not been cleared because
/// it wasn't necessary.
Future < bool > clearOutboundGroupSession ( { bool wipe = false } ) async {
if ( ! wipe & & this . _outboundGroupSessionDevices ! = null ) {
List < DeviceKeys > deviceKeys = await getUserDeviceKeys ( ) ;
List < String > outboundGroupSessionDevices = [ ] ;
for ( DeviceKeys keys in deviceKeys ) {
outboundGroupSessionDevices . add ( keys . deviceId ) ;
}
outboundGroupSessionDevices . sort ( ) ;
if ( outboundGroupSessionDevices . toString ( ) = =
this . _outboundGroupSessionDevices . toString ( ) ) {
return false ;
}
}
this . _outboundGroupSessionDevices = = null ;
2020-02-15 07:48:41 +00:00
await client . storeAPI ? . setItem (
" /clients/ ${ client . deviceID } /rooms/ ${ this . id } /outbound_group_session " ,
null ) ;
this . _outboundGroupSession ? . free ( ) ;
this . _outboundGroupSession = null ;
2020-02-27 08:41:49 +00:00
return true ;
2020-02-15 07:48:41 +00:00
}
/// Key-Value store of session ids to the session keys. Only m.megolm.v1.aes-sha2
/// session keys are supported. They are stored as a Map with the following keys:
/// {
/// "algorithm": "m.megolm.v1.aes-sha2",
/// "room_id": "!Cuyf34gef24t:localhost",
/// "session_id": "X3lUlvLELLYxeTx4yOVu6UDpasGEVO0Jbu+QFnm0cKQ",
/// "session_key": "AgAAAADxKHa9uFxcXzwYoNueL5Xqi69IkD4sni8LlfJL7qNBEY..."
/// }
Map < String , SessionKey > get sessionKeys = > _sessionKeys ;
Map < String , SessionKey > _sessionKeys = { } ;
/// Add a new session key to the [sessionKeys].
2020-02-21 15:05:19 +00:00
void setSessionKey ( String sessionId , Map < String , dynamic > content ,
{ bool forwarded = false } ) {
2020-02-15 07:48:41 +00:00
if ( sessionKeys . containsKey ( sessionId ) ) return ;
olm . InboundGroupSession inboundGroupSession ;
if ( content [ " algorithm " ] = = " m.megolm.v1.aes-sha2 " ) {
try {
inboundGroupSession = olm . InboundGroupSession ( ) ;
2020-02-21 15:05:19 +00:00
if ( forwarded ) {
inboundGroupSession . import_session ( content [ " session_key " ] ) ;
} else {
inboundGroupSession . create ( content [ " session_key " ] ) ;
}
2020-02-15 07:48:41 +00:00
} catch ( e ) {
inboundGroupSession = null ;
print ( " [LibOlm] Could not create new InboundGroupSession: " +
e . toString ( ) ) ;
}
}
_sessionKeys [ sessionId ] = SessionKey (
content: content ,
inboundGroupSession: inboundGroupSession ,
indexes: { } ,
key: client . userID ,
) ;
2020-02-18 09:23:55 +00:00
if ( _fullyRestored ) {
client . storeAPI ? . setItem (
" /clients/ ${ client . deviceID } /rooms/ ${ this . id } /session_keys " ,
json . encode ( sessionKeys ) ) ;
}
2020-02-24 08:10:35 +00:00
if ( getState ( " m.room.encrypted " ) ! = null ) {
final Event decrypted = getState ( " m.room.encrypted " ) . decrypted ;
if ( decrypted . type ! = EventTypes . Encrypted ) {
setState ( decrypted ) ;
}
}
2020-02-21 15:05:19 +00:00
onSessionKeyReceived . add ( sessionId ) ;
2020-02-15 07:48:41 +00:00
}
2020-01-02 14:09:49 +00:00
/// Returns the [Event] for the given [typeKey] and optional [stateKey].
2019-11-20 13:42:08 +00:00
/// If no [stateKey] is provided, it defaults to an empty string.
2020-01-02 14:09:49 +00:00
Event getState ( String typeKey , [ String stateKey = " " ] ) = >
2019-11-20 13:42:08 +00:00
states . states [ typeKey ] ! = null ? states . states [ typeKey ] [ stateKey ] : null ;
2019-11-21 14:10:24 +00:00
/// Adds the [state] to this room and overwrites a state with the same
/// typeKey/stateKey key pair if there is one.
2020-01-02 14:09:49 +00:00
void setState ( Event state ) {
2020-02-21 08:44:05 +00:00
// Decrypt if necessary
if ( state . type = = EventTypes . Encrypted ) {
try {
state = decryptGroupMessage ( state ) ;
} catch ( e ) {
print ( " [LibOlm] Could not decrypt room state: " + e . toString ( ) ) ;
}
}
2020-02-15 07:48:41 +00:00
// Check if this is a member change and we need to clear the outboundGroupSession.
if ( encrypted & &
outboundGroupSession ! = null & &
state . type = = EventTypes . RoomMember ) {
User newUser = state . asUser ;
User oldUser = getState ( " m.room.member " , newUser . id ) ? . asUser ;
if ( oldUser = = null | | oldUser . membership ! = newUser . membership ) {
clearOutboundGroupSession ( ) ;
}
}
2020-02-24 08:10:35 +00:00
if ( ( getState ( state . typeKey ) ? . time ? . millisecondsSinceEpoch ? ? 0 ) >
2020-02-24 09:34:28 +00:00
( state . time ? . millisecondsSinceEpoch ? ? 1 ) ) {
2020-02-24 08:10:35 +00:00
return ;
}
2020-01-02 14:33:26 +00:00
if ( ! states . states . containsKey ( state . typeKey ) ) {
2019-11-21 14:10:24 +00:00
states . states [ state . typeKey ] = { } ;
2020-01-02 14:33:26 +00:00
}
2019-11-21 14:10:24 +00:00
states . states [ state . typeKey ] [ state . stateKey ? ? " " ] = state ;
2019-11-20 13:42:08 +00:00
}
2019-06-11 08:51:45 +00:00
/// ID of the fully read marker event.
2019-09-03 15:57:27 +00:00
String get fullyRead = > roomAccountData [ " m.fully_read " ] ! = null
? roomAccountData [ " m.fully_read " ] . content [ " event_id " ]
: " " ;
2019-06-11 08:51:45 +00:00
2020-01-04 10:29:38 +00:00
/// If something changes, this callback will be triggered. Will return the
/// room id.
final StreamController < String > onUpdate = StreamController . broadcast ( ) ;
2019-09-03 11:24:44 +00:00
2020-02-21 15:05:19 +00:00
/// If there is a new session key received, this will be triggered with
/// the session ID.
final StreamController < String > onSessionKeyReceived =
StreamController . broadcast ( ) ;
2019-08-07 08:17:03 +00:00
/// The name of the room if set by a participant.
2019-11-26 12:46:46 +00:00
String get name = > states [ " m.room.name " ] ! = null
? states [ " m.room.name " ] . content [ " name " ]
: " " ;
2019-08-07 08:17:03 +00:00
/// The topic of the room if set by a participant.
String get topic = > states [ " m.room.topic " ] ! = null
? states [ " m.room.topic " ] . content [ " topic " ]
: " " ;
/// The avatar of the room if set by a participant.
MxContent get avatar {
2020-01-02 14:33:26 +00:00
if ( states [ " m.room.avatar " ] ! = null ) {
2019-08-08 09:41:42 +00:00
return MxContent ( states [ " m.room.avatar " ] . content [ " url " ] ) ;
2020-01-02 14:33:26 +00:00
}
if ( mHeroes ! = null & & mHeroes . length = = 1 & & states [ mHeroes [ 0 ] ] ! = null ) {
2019-08-08 09:41:42 +00:00
return states [ mHeroes [ 0 ] ] . asUser . avatarUrl ;
2020-01-02 14:33:26 +00:00
}
2019-11-26 12:46:46 +00:00
if ( membership = = Membership . invite & &
getState ( " m.room.member " , client . userID ) ! = null ) {
return getState ( " m.room.member " , client . userID ) . sender . avatarUrl ;
2019-09-30 08:19:28 +00:00
}
2019-08-07 08:17:03 +00:00
return MxContent ( " " ) ;
}
2019-06-11 08:51:45 +00:00
/// The address in the format: #roomname:homeserver.org.
2019-08-07 08:17:03 +00:00
String get canonicalAlias = > states [ " m.room.canonical_alias " ] ! = null
2019-08-08 07:58:37 +00:00
? states [ " m.room.canonical_alias " ] . content [ " alias " ]
2019-08-07 08:17:03 +00:00
: " " ;
2019-06-11 08:51:45 +00:00
2019-08-08 08:31:39 +00:00
/// If this room is a direct chat, this is the matrix ID of the user.
/// Returns null otherwise.
String get directChatMatrixID {
2020-01-02 14:33:26 +00:00
String returnUserId ;
2019-08-08 08:31:39 +00:00
if ( client . directChats is Map < String , dynamic > ) {
client . directChats . forEach ( ( String userId , dynamic roomIds ) {
if ( roomIds is List < dynamic > ) {
2020-01-02 14:33:26 +00:00
for ( int i = 0 ; i < roomIds . length ; i + + ) {
2019-08-08 08:31:39 +00:00
if ( roomIds [ i ] = = this . id ) {
returnUserId = userId ;
break ;
}
2020-01-02 14:33:26 +00:00
}
2019-08-08 08:31:39 +00:00
}
} ) ;
}
return returnUserId ;
}
2019-06-11 08:51:45 +00:00
2019-08-29 08:49:07 +00:00
/// Wheither this is a direct chat or not
bool get isDirectChat = > directChatMatrixID ! = null ;
2019-06-11 08:51:45 +00:00
/// Must be one of [all, mention]
String notificationSettings ;
2019-08-29 07:50:04 +00:00
Event get lastEvent {
2020-01-02 14:09:49 +00:00
DateTime lastTime = DateTime . fromMillisecondsSinceEpoch ( 0 ) ;
2020-01-14 11:27:26 +00:00
Event lastEvent = getState ( " m.room.message " ) ;
2020-01-02 14:33:26 +00:00
if ( lastEvent = = null ) {
2019-11-20 13:02:23 +00:00
states . forEach ( ( final String key , final entry ) {
if ( ! entry . containsKey ( " " ) ) return ;
2020-01-02 14:09:49 +00:00
final Event state = entry [ " " ] ;
if ( state . time ! = null & &
state . time . millisecondsSinceEpoch >
lastTime . millisecondsSinceEpoch ) {
2019-11-20 13:02:23 +00:00
lastTime = state . time ;
2020-01-14 11:27:26 +00:00
lastEvent = state ;
2019-11-13 14:08:27 +00:00
}
2019-11-20 13:02:23 +00:00
} ) ;
2020-01-02 14:33:26 +00:00
}
2019-08-29 07:50:04 +00:00
return lastEvent ;
}
2019-06-11 08:51:45 +00:00
2019-10-20 09:44:14 +00:00
/// Returns a list of all current typing users.
List < User > get typingUsers {
if ( ! ephemerals . containsKey ( " m.typing " ) ) return [ ] ;
List < dynamic > typingMxid = ephemerals [ " m.typing " ] . content [ " user_ids " ] ;
List < User > typingUsers = [ ] ;
2020-01-02 14:33:26 +00:00
for ( int i = 0 ; i < typingMxid . length ; i + + ) {
2019-11-15 11:08:43 +00:00
typingUsers . add ( getUserByMXIDSync ( typingMxid [ i ] ) ) ;
2020-01-02 14:33:26 +00:00
}
2019-10-20 09:44:14 +00:00
return typingUsers ;
}
2019-06-11 08:51:45 +00:00
/// Your current client instance.
final Client client ;
2019-06-09 10:16:48 +00:00
Room ( {
2019-06-11 08:51:45 +00:00
this . id ,
2019-08-08 11:00:56 +00:00
this . membership = Membership . join ,
this . notificationCount = 0 ,
this . highlightCount = 0 ,
2019-06-28 09:42:57 +00:00
this . prev_batch = " " ,
2019-06-11 08:51:45 +00:00
this . client ,
2019-08-07 08:17:03 +00:00
this . notificationSettings ,
2019-08-08 11:00:56 +00:00
this . mHeroes = const [ ] ,
this . mInvitedMemberCount = 0 ,
this . mJoinedMemberCount = 0 ,
this . roomAccountData = const { } ,
2019-06-09 10:16:48 +00:00
} ) ;
2019-09-26 09:30:07 +00:00
/// The default count of how much events should be requested when requesting the
/// history of this room.
static const int DefaultHistoryCount = 100 ;
2019-08-06 09:47:09 +00:00
/// Calculates the displayname. First checks if there is a name, then checks for a canonical alias and
/// then generates a name from the heroes.
String get displayname {
2019-11-26 12:46:46 +00:00
if ( name ! = null & & name . isNotEmpty ) return name ;
2019-08-06 09:47:09 +00:00
if ( canonicalAlias ! = null & &
2020-01-02 14:33:26 +00:00
canonicalAlias . isNotEmpty & &
canonicalAlias . length > 3 ) {
2020-02-10 11:33:18 +00:00
return canonicalAlias . localpart ;
2020-01-02 14:33:26 +00:00
}
2019-11-29 16:19:32 +00:00
List < String > heroes = [ ] ;
if ( mHeroes ! = null & &
2020-01-02 14:33:26 +00:00
mHeroes . isNotEmpty & &
2019-11-29 16:19:32 +00:00
mHeroes . any ( ( h ) = > h . isNotEmpty ) ) {
heroes = mHeroes ;
} else {
if ( states [ " m.room.member " ] is Map < String , dynamic > ) {
for ( var entry in states [ " m.room.member " ] . entries ) {
2020-01-02 14:09:49 +00:00
Event state = entry . value ;
2019-11-29 16:19:32 +00:00
if ( state . type = = EventTypes . RoomMember & &
state . stateKey ! = client ? . userID ) heroes . add ( state . stateKey ) ;
}
}
}
2020-01-02 14:33:26 +00:00
if ( heroes . isNotEmpty ) {
2019-08-06 09:47:09 +00:00
String displayname = " " ;
2019-11-29 16:19:32 +00:00
for ( int i = 0 ; i < heroes . length ; i + + ) {
if ( heroes [ i ] . isEmpty ) continue ;
2019-12-05 16:42:12 +00:00
displayname + = getUserByMXIDSync ( heroes [ i ] ) . calcDisplayname ( ) + " , " ;
2019-11-26 12:46:46 +00:00
}
2019-08-06 09:47:09 +00:00
return displayname . substring ( 0 , displayname . length - 2 ) ;
}
2019-11-26 12:46:46 +00:00
if ( membership = = Membership . invite & &
getState ( " m.room.member " , client . userID ) ! = null ) {
return getState ( " m.room.member " , client . userID ) . sender . calcDisplayname ( ) ;
}
2019-08-06 09:47:09 +00:00
return " Empty chat " ;
}
2019-06-11 08:51:45 +00:00
/// The last message sent to this room.
String get lastMessage {
2020-01-02 14:33:26 +00:00
if ( lastEvent ! = null ) {
2020-01-14 11:27:26 +00:00
return lastEvent . body ;
2020-01-02 14:33:26 +00:00
} else {
2019-06-11 11:44:25 +00:00
return " " ;
2020-01-02 14:33:26 +00:00
}
2019-06-11 08:51:45 +00:00
}
/// When the last message received.
2020-01-02 14:09:49 +00:00
DateTime get timeCreated {
2020-01-02 14:33:26 +00:00
if ( lastEvent ! = null ) {
2019-06-21 10:18:54 +00:00
return lastEvent . time ;
2020-01-02 14:33:26 +00:00
}
return DateTime . now ( ) ;
2019-06-09 10:16:48 +00:00
}
2019-12-29 10:28:33 +00:00
/// Call the Matrix API to change the name of this room. Returns the event ID of the
/// new m.room.name event.
Future < String > setName ( String newName ) async {
2020-01-02 14:09:49 +00:00
final Map < String , dynamic > resp = await client . jsonRequest (
2019-07-12 09:26:07 +00:00
type: HTTPType . PUT ,
2019-07-26 11:32:18 +00:00
action: " /client/r0/rooms/ ${ id } /state/m.room.name " ,
2019-06-09 10:16:48 +00:00
data: { " name " : newName } ) ;
2019-12-29 10:28:33 +00:00
return resp [ " event_id " ] ;
2019-06-09 10:16:48 +00:00
}
2019-06-11 08:51:45 +00:00
/// Call the Matrix API to change the topic of this room.
2019-12-29 10:28:33 +00:00
Future < String > setDescription ( String newName ) async {
2020-01-02 14:09:49 +00:00
final Map < String , dynamic > resp = await client . jsonRequest (
2019-07-12 09:26:07 +00:00
type: HTTPType . PUT ,
2019-07-26 11:32:18 +00:00
action: " /client/r0/rooms/ ${ id } /state/m.room.topic " ,
2019-06-09 10:16:48 +00:00
data: { " topic " : newName } ) ;
2019-12-29 10:28:33 +00:00
return resp [ " event_id " ] ;
2019-06-09 10:16:48 +00:00
}
2020-02-11 11:06:54 +00:00
Future < String > sendTextEvent ( String message ,
{ String txid , Event inReplyTo } ) = >
sendEvent ( { " msgtype " : " m.text " , " body " : message } ,
txid: txid , inReplyTo: inReplyTo ) ;
2019-09-09 13:22:02 +00:00
2019-12-18 11:46:25 +00:00
/// Sends a [file] to this room after uploading it. The [msgType] is optional
/// and will be detected by the mimetype of the file.
Future < String > sendFileEvent ( MatrixFile file ,
2020-02-11 11:06:54 +00:00
{ String msgType = " m.file " , String txid , Event inReplyTo } ) async {
2019-12-18 11:46:25 +00:00
if ( msgType = = " m.image " ) return sendImageEvent ( file ) ;
if ( msgType = = " m.audio " ) return sendVideoEvent ( file ) ;
if ( msgType = = " m.video " ) return sendAudioEvent ( file ) ;
2019-10-02 11:33:01 +00:00
String fileName = file . path . split ( " / " ) . last ;
2019-09-09 13:22:02 +00:00
2020-01-02 14:09:49 +00:00
final String uploadResp = await client . upload ( file ) ;
2019-09-09 13:22:02 +00:00
// Send event
Map < String , dynamic > content = {
" msgtype " : msgType ,
" body " : fileName ,
" filename " : fileName ,
" url " : uploadResp ,
" info " : {
2019-12-18 11:46:25 +00:00
" mimetype " : mime ( file . path ) ,
" size " : file . size ,
}
} ;
2020-02-11 11:06:54 +00:00
return await sendEvent ( content , txid: txid , inReplyTo: inReplyTo ) ;
2019-12-18 11:46:25 +00:00
}
Future < String > sendAudioEvent ( MatrixFile file ,
2020-02-11 11:06:54 +00:00
{ String txid , int width , int height , Event inReplyTo } ) async {
2019-12-18 11:46:25 +00:00
String fileName = file . path . split ( " / " ) . last ;
2020-01-02 14:09:49 +00:00
final String uploadResp = await client . upload ( file ) ;
2019-12-18 11:46:25 +00:00
Map < String , dynamic > content = {
" msgtype " : " m.audio " ,
" body " : fileName ,
" filename " : fileName ,
" url " : uploadResp ,
" info " : {
" mimetype " : mime ( fileName ) ,
" size " : file . size ,
2019-09-09 13:22:02 +00:00
}
} ;
2020-02-11 11:06:54 +00:00
return await sendEvent ( content , txid: txid , inReplyTo: inReplyTo ) ;
2019-09-09 13:22:02 +00:00
}
2019-10-18 11:05:07 +00:00
Future < String > sendImageEvent ( MatrixFile file ,
2020-02-11 11:06:54 +00:00
{ String txid , int width , int height , Event inReplyTo } ) async {
2019-10-02 11:33:01 +00:00
String fileName = file . path . split ( " / " ) . last ;
2020-01-02 14:09:49 +00:00
final String uploadResp = await client . upload ( file ) ;
2019-09-09 13:22:02 +00:00
Map < String , dynamic > content = {
" msgtype " : " m.image " ,
" body " : fileName ,
" url " : uploadResp ,
2019-09-30 12:03:34 +00:00
" info " : {
2019-10-18 11:05:07 +00:00
" size " : file . size ,
2019-10-02 11:33:01 +00:00
" mimetype " : mime ( fileName ) ,
" w " : width ,
" h " : height ,
2019-09-30 12:03:34 +00:00
} ,
2019-09-09 13:22:02 +00:00
} ;
2020-02-11 11:06:54 +00:00
return await sendEvent ( content , txid: txid , inReplyTo: inReplyTo ) ;
2019-09-09 13:22:02 +00:00
}
2019-12-18 11:46:25 +00:00
Future < String > sendVideoEvent ( MatrixFile file ,
2020-01-02 14:33:26 +00:00
{ String txid ,
2019-12-18 11:46:25 +00:00
int videoWidth ,
int videoHeight ,
int duration ,
MatrixFile thumbnail ,
int thumbnailWidth ,
2020-02-11 11:06:54 +00:00
int thumbnailHeight ,
Event inReplyTo } ) async {
2019-12-18 11:46:25 +00:00
String fileName = file . path . split ( " / " ) . last ;
2020-01-02 14:09:49 +00:00
final String uploadResp = await client . upload ( file ) ;
2019-12-18 11:46:25 +00:00
Map < String , dynamic > content = {
" msgtype " : " m.video " ,
" body " : fileName ,
" url " : uploadResp ,
" info " : {
" size " : file . size ,
" mimetype " : mime ( fileName ) ,
} ,
} ;
if ( videoWidth ! = null ) {
content [ " info " ] [ " w " ] = videoWidth ;
}
if ( thumbnailHeight ! = null ) {
content [ " info " ] [ " h " ] = thumbnailHeight ;
}
if ( duration ! = null ) {
content [ " info " ] [ " duration " ] = duration ;
}
if ( thumbnail ! = null ) {
String thumbnailName = file . path . split ( " / " ) . last ;
2020-01-02 14:09:49 +00:00
final String thumbnailUploadResp = await client . upload ( file ) ;
2019-12-18 11:46:25 +00:00
content [ " info " ] [ " thumbnail_url " ] = thumbnailUploadResp ;
content [ " info " ] [ " thumbnail_info " ] = {
" size " : thumbnail . size ,
" mimetype " : mime ( thumbnailName ) ,
} ;
if ( thumbnailWidth ! = null ) {
content [ " info " ] [ " thumbnail_info " ] [ " w " ] = thumbnailWidth ;
}
if ( thumbnailHeight ! = null ) {
content [ " info " ] [ " thumbnail_info " ] [ " h " ] = thumbnailHeight ;
}
}
2020-02-11 11:06:54 +00:00
return await sendEvent ( content , txid: txid , inReplyTo: inReplyTo ) ;
2019-12-18 11:46:25 +00:00
}
2020-02-11 11:06:54 +00:00
Future < String > sendEvent ( Map < String , dynamic > content ,
{ String txid , Event inReplyTo } ) async {
2020-02-15 12:21:03 +00:00
final String type = " m.room.message " ;
2020-02-18 10:49:02 +00:00
final String sendType = ( this . encrypted & & client . encryptionEnabled )
? " m.room.encrypted "
: type ;
2019-06-27 07:25:25 +00:00
// Create new transaction id
2019-06-26 14:36:34 +00:00
String messageID ;
2019-06-11 15:16:01 +00:00
final int now = DateTime . now ( ) . millisecondsSinceEpoch ;
2019-06-26 14:36:34 +00:00
if ( txid = = null ) {
messageID = " msg $ now " ;
2020-01-02 14:33:26 +00:00
} else {
2019-06-26 14:36:34 +00:00
messageID = txid ;
2020-01-02 14:33:26 +00:00
}
2019-06-11 15:16:01 +00:00
2020-02-11 11:06:54 +00:00
if ( inReplyTo ! = null ) {
String replyText = " < ${ inReplyTo . senderId } > " + inReplyTo . body ;
List < String > replyTextLines = replyText . split ( " \n " ) ;
for ( int i = 0 ; i < replyTextLines . length ; i + + ) {
replyTextLines [ i ] = " > " + replyTextLines [ i ] ;
}
replyText = replyTextLines . join ( " \n " ) ;
content [ " format " ] = " org.matrix.custom.html " ;
2020-02-15 07:48:41 +00:00
content [ " formatted_body " ] =
' <mx-reply><blockquote><a href="https://matrix.to/#/ ${ inReplyTo . room . id } / ${ inReplyTo . eventId } ">In reply to</a> <a href="https://matrix.to/#/ ${ inReplyTo . senderId } "> ${ inReplyTo . senderId } </a><br> ${ inReplyTo . body } </blockquote></mx-reply> ${ content [ " formatted_body " ] ? ? content [ " body " ] } ' ;
2020-02-11 11:06:54 +00:00
content [ " body " ] = replyText + " \n \n ${ content [ " body " ] ? ? " " } " ;
content [ " m.relates_to " ] = {
" m.in_reply_to " : {
" event_id " : inReplyTo . eventId ,
} ,
} ;
}
2019-06-26 14:36:34 +00:00
// Display a *sending* event and store it.
2019-06-12 09:46:57 +00:00
EventUpdate eventUpdate =
2019-06-18 10:33:40 +00:00
EventUpdate ( type: " timeline " , roomID: id , eventType: type , content: {
2019-06-12 09:46:57 +00:00
" type " : type ,
2019-06-27 07:44:37 +00:00
" event_id " : messageID ,
2019-06-12 09:46:57 +00:00
" sender " : client . userID ,
" status " : 0 ,
" origin_server_ts " : now ,
2019-09-09 13:22:02 +00:00
" content " : content
2019-06-12 09:46:57 +00:00
} ) ;
2020-01-02 14:09:49 +00:00
client . onEvent . add ( eventUpdate ) ;
2019-06-26 14:36:34 +00:00
await client . store ? . transaction ( ( ) {
2019-06-11 15:16:01 +00:00
client . store . storeEventUpdate ( eventUpdate ) ;
2019-07-12 09:26:07 +00:00
return ;
2019-06-11 15:16:01 +00:00
} ) ;
2019-06-26 14:36:34 +00:00
// Send the text and on success, store and display a *sent* event.
2019-12-29 10:28:33 +00:00
try {
2020-02-15 07:48:41 +00:00
final Map < String , dynamic > response = await client . jsonRequest (
type: HTTPType . PUT ,
2020-02-15 12:21:03 +00:00
action: " /client/r0/rooms/ ${ id } /send/ $ sendType / $ messageID " ,
2020-02-18 10:49:02 +00:00
data: client . encryptionEnabled
? await encryptGroupMessagePayload ( content )
: content ) ;
2020-02-15 07:48:41 +00:00
final String res = response [ " event_id " ] ;
2019-12-29 10:28:33 +00:00
eventUpdate . content [ " status " ] = 1 ;
2019-07-23 09:09:13 +00:00
eventUpdate . content [ " unsigned " ] = { " transaction_id " : messageID } ;
2019-12-29 10:28:33 +00:00
eventUpdate . content [ " event_id " ] = res ;
2020-01-02 14:09:49 +00:00
client . onEvent . add ( eventUpdate ) ;
2019-06-26 18:03:20 +00:00
await client . store ? . transaction ( ( ) {
client . store . storeEventUpdate ( eventUpdate ) ;
2019-07-12 09:26:07 +00:00
return ;
2019-06-26 18:03:20 +00:00
} ) ;
2019-12-29 10:28:33 +00:00
return res ;
} catch ( exception ) {
2020-02-15 07:48:41 +00:00
print ( " [Client] Error while sending: " + exception . toString ( ) ) ;
2019-12-29 10:28:33 +00:00
// On error, set status to -1
eventUpdate . content [ " status " ] = - 1 ;
2019-07-23 09:09:13 +00:00
eventUpdate . content [ " unsigned " ] = { " transaction_id " : messageID } ;
2020-01-02 14:09:49 +00:00
client . onEvent . add ( eventUpdate ) ;
2019-06-26 18:03:20 +00:00
await client . store ? . transaction ( ( ) {
client . store . storeEventUpdate ( eventUpdate ) ;
2019-07-12 09:26:07 +00:00
return ;
2019-06-26 18:03:20 +00:00
} ) ;
2019-06-11 15:16:01 +00:00
}
return null ;
2019-06-09 10:16:48 +00:00
}
2019-09-30 08:19:28 +00:00
/// Call the Matrix API to join this room if the user is not already a member.
/// If this room is intended to be a direct chat, the direct chat flag will
/// automatically be set.
2019-12-29 10:28:33 +00:00
Future < void > join ( ) async {
try {
2020-01-02 14:09:49 +00:00
await client . jsonRequest (
2019-12-29 10:28:33 +00:00
type: HTTPType . POST , action: " /client/r0/rooms/ ${ id } /join " ) ;
2020-01-28 08:15:53 +00:00
final Event invitation = getState ( " m.room.member " , client . userID ) ;
if ( invitation ! = null & &
invitation . content [ " is_direct " ] is bool & &
invitation . content [ " is_direct " ] ) {
await addToDirectChat ( invitation . sender . id ) ;
2020-01-02 14:33:26 +00:00
}
2019-12-29 10:28:33 +00:00
} on MatrixException catch ( exception ) {
if ( exception . errorMessage = = " No known servers " ) {
2020-01-02 14:33:26 +00:00
await client . store ? . forgetRoom ( id ) ;
2020-01-02 14:09:49 +00:00
client . onRoomUpdate . add (
2019-11-13 13:56:20 +00:00
RoomUpdate (
id: id ,
membership: Membership . leave ,
notification_count: 0 ,
highlight_count: 0 ) ,
) ;
}
2019-12-29 10:28:33 +00:00
rethrow ;
2019-11-13 13:56:20 +00:00
}
2019-09-30 08:19:28 +00:00
}
/// Call the Matrix API to leave this room. If this room is set as a direct
/// chat, this will be removed too.
2019-12-29 10:28:33 +00:00
Future < void > leave ( ) async {
2019-09-30 08:19:28 +00:00
if ( directChatMatrixID ! = " " ) await removeFromDirectChat ( ) ;
2020-01-02 14:09:49 +00:00
await client . jsonRequest (
2019-07-12 09:26:07 +00:00
type: HTTPType . POST , action: " /client/r0/rooms/ ${ id } /leave " ) ;
2019-12-29 10:28:33 +00:00
return ;
2019-06-09 10:16:48 +00:00
}
2019-06-11 08:51:45 +00:00
/// Call the Matrix API to forget this room if you already left it.
2019-12-29 10:28:33 +00:00
Future < void > forget ( ) async {
2020-01-02 14:33:26 +00:00
await client . store ? . forgetRoom ( id ) ;
2020-01-02 14:09:49 +00:00
await client . jsonRequest (
2019-07-12 09:26:07 +00:00
type: HTTPType . POST , action: " /client/r0/rooms/ ${ id } /forget " ) ;
2019-12-29 10:28:33 +00:00
return ;
2019-06-09 10:16:48 +00:00
}
2019-06-11 08:51:45 +00:00
/// Call the Matrix API to kick a user from this room.
2019-12-29 10:28:33 +00:00
Future < void > kick ( String userID ) async {
2020-01-02 14:09:49 +00:00
await client . jsonRequest (
2019-07-12 09:26:07 +00:00
type: HTTPType . POST ,
2019-06-11 11:44:25 +00:00
action: " /client/r0/rooms/ ${ id } /kick " ,
2019-06-09 10:16:48 +00:00
data: { " user_id " : userID } ) ;
2019-12-29 10:28:33 +00:00
return ;
2019-06-09 10:16:48 +00:00
}
2019-06-11 08:51:45 +00:00
/// Call the Matrix API to ban a user from this room.
2019-12-29 10:28:33 +00:00
Future < void > ban ( String userID ) async {
2020-01-02 14:09:49 +00:00
await client . jsonRequest (
2019-07-12 09:26:07 +00:00
type: HTTPType . POST ,
2019-06-11 11:44:25 +00:00
action: " /client/r0/rooms/ ${ id } /ban " ,
2019-06-09 10:16:48 +00:00
data: { " user_id " : userID } ) ;
2019-12-29 10:28:33 +00:00
return ;
2019-06-09 10:16:48 +00:00
}
2019-06-11 08:51:45 +00:00
/// Call the Matrix API to unban a banned user from this room.
2019-12-29 10:28:33 +00:00
Future < void > unban ( String userID ) async {
2020-01-02 14:09:49 +00:00
await client . jsonRequest (
2019-07-12 09:26:07 +00:00
type: HTTPType . POST ,
2019-06-11 11:44:25 +00:00
action: " /client/r0/rooms/ ${ id } /unban " ,
2019-06-09 10:16:48 +00:00
data: { " user_id " : userID } ) ;
2019-12-29 10:28:33 +00:00
return ;
2019-06-09 10:16:48 +00:00
}
2019-08-08 09:41:42 +00:00
/// Set the power level of the user with the [userID] to the value [power].
2019-12-29 10:28:33 +00:00
/// Returns the event ID of the new state event. If there is no known
/// power level event, there might something broken and this returns null.
Future < String > setPower ( String userID , int power ) async {
2019-08-08 09:41:42 +00:00
if ( states [ " m.room.power_levels " ] = = null ) return null ;
2019-12-05 09:06:23 +00:00
Map < String , dynamic > powerMap = { }
. . addAll ( states [ " m.room.power_levels " ] . content ) ;
if ( powerMap [ " users " ] = = null ) powerMap [ " users " ] = { } ;
powerMap [ " users " ] [ userID ] = power ;
2019-06-11 11:32:14 +00:00
2020-01-02 14:09:49 +00:00
final Map < String , dynamic > resp = await client . jsonRequest (
2019-07-12 09:26:07 +00:00
type: HTTPType . PUT ,
2019-07-26 08:05:08 +00:00
action: " /client/r0/rooms/ $ id /state/m.room.power_levels " ,
2019-12-05 09:06:23 +00:00
data: powerMap ) ;
2019-12-29 10:28:33 +00:00
return resp [ " event_id " ] ;
2019-06-11 11:32:14 +00:00
}
2019-06-11 08:51:45 +00:00
/// Call the Matrix API to invite a user to this room.
2019-12-29 10:28:33 +00:00
Future < void > invite ( String userID ) async {
2020-01-02 14:09:49 +00:00
await client . jsonRequest (
2019-07-12 09:26:07 +00:00
type: HTTPType . POST ,
2019-06-11 11:44:25 +00:00
action: " /client/r0/rooms/ ${ id } /invite " ,
2019-06-09 10:16:48 +00:00
data: { " user_id " : userID } ) ;
2019-12-29 10:28:33 +00:00
return ;
2019-06-09 10:16:48 +00:00
}
2019-09-26 09:30:07 +00:00
/// Request more previous events from the server. [historyCount] defines how much events should
/// be received maximum. When the request is answered, [onHistoryReceived] will be triggered **before**
/// the historical events will be published in the onEvent stream.
Future < void > requestHistory (
{ int historyCount = DefaultHistoryCount , onHistoryReceived } ) async {
2020-01-02 14:09:49 +00:00
final dynamic resp = await client . jsonRequest (
2019-07-12 09:26:07 +00:00
type: HTTPType . GET ,
2019-06-28 09:42:57 +00:00
action:
2020-01-02 14:09:49 +00:00
" /client/r0/rooms/ $ id /messages?from= ${ prev_batch } &dir=b&limit= $ historyCount &filter= ${ Client . syncFilters } " ) ;
2019-06-11 11:44:25 +00:00
2019-09-26 09:30:07 +00:00
if ( onHistoryReceived ! = null ) onHistoryReceived ( ) ;
2019-06-28 10:32:33 +00:00
prev_batch = resp [ " end " ] ;
2020-01-02 14:33:26 +00:00
await client . store ? . storeRoomPrevBatch ( this ) ;
2019-06-28 10:32:33 +00:00
2019-06-12 09:46:57 +00:00
if ( ! ( resp [ " chunk " ] is List < dynamic > & &
resp [ " chunk " ] . length > 0 & &
2019-06-11 11:44:25 +00:00
resp [ " end " ] is String ) ) return ;
2019-08-29 10:28:50 +00:00
if ( resp [ " state " ] is List < dynamic > ) {
2020-01-02 14:33:26 +00:00
await client . store ? . transaction ( ( ) {
2019-08-29 10:28:50 +00:00
for ( int i = 0 ; i < resp [ " state " ] . length ; i + + ) {
EventUpdate eventUpdate = EventUpdate (
type: " state " ,
roomID: id ,
eventType: resp [ " state " ] [ i ] [ " type " ] ,
content: resp [ " state " ] [ i ] ,
2020-02-21 08:44:05 +00:00
) . decrypt ( this ) ;
client . onEvent . add ( eventUpdate ) ;
2019-08-29 10:28:50 +00:00
client . store . storeEventUpdate ( eventUpdate ) ;
}
return ;
} ) ;
if ( client . store = = null ) {
for ( int i = 0 ; i < resp [ " state " ] . length ; i + + ) {
EventUpdate eventUpdate = EventUpdate (
type: " state " ,
roomID: id ,
eventType: resp [ " state " ] [ i ] [ " type " ] ,
content: resp [ " state " ] [ i ] ,
2020-02-21 08:44:05 +00:00
) . decrypt ( this ) ;
client . onEvent . add ( eventUpdate ) ;
2019-08-29 10:28:50 +00:00
}
}
}
2019-06-11 11:44:25 +00:00
List < dynamic > history = resp [ " chunk " ] ;
2020-01-02 14:33:26 +00:00
await client . store ? . transaction ( ( ) {
2019-06-11 11:44:25 +00:00
for ( int i = 0 ; i < history . length ; i + + ) {
EventUpdate eventUpdate = EventUpdate (
2019-06-28 06:39:43 +00:00
type: " history " ,
2019-06-11 11:44:25 +00:00
roomID: id ,
2019-06-28 06:39:43 +00:00
eventType: history [ i ] [ " type " ] ,
2019-06-11 11:44:25 +00:00
content: history [ i ] ,
2020-02-21 08:44:05 +00:00
) . decrypt ( this ) ;
client . onEvent . add ( eventUpdate ) ;
2019-06-11 11:44:25 +00:00
client . store . storeEventUpdate ( eventUpdate ) ;
2020-01-24 09:34:38 +00:00
client . store . setRoomPrevBatch ( id , resp [ " end " ] ) ;
2019-06-11 11:44:25 +00:00
}
2019-07-12 09:26:07 +00:00
return ;
2019-06-11 11:44:25 +00:00
} ) ;
2019-06-28 06:39:43 +00:00
if ( client . store = = null ) {
for ( int i = 0 ; i < history . length ; i + + ) {
EventUpdate eventUpdate = EventUpdate (
type: " history " ,
roomID: id ,
eventType: history [ i ] [ " type " ] ,
content: history [ i ] ,
2020-02-21 08:44:05 +00:00
) . decrypt ( this ) ;
client . onEvent . add ( eventUpdate ) ;
2019-06-28 06:39:43 +00:00
}
}
2020-01-02 14:09:49 +00:00
client . onRoomUpdate . add (
2019-10-24 09:39:39 +00:00
RoomUpdate (
id: id ,
membership: membership ,
prev_batch: resp [ " end " ] ,
notification_count: notificationCount ,
highlight_count: highlightCount ,
) ,
) ;
2019-06-11 11:44:25 +00:00
}
2019-06-09 10:16:48 +00:00
2019-12-29 10:28:33 +00:00
/// Sets this room as a direct chat for this user if not already.
Future < void > addToDirectChat ( String userID ) async {
2019-08-08 09:41:42 +00:00
Map < String , dynamic > directChats = client . directChats ;
2020-01-02 14:33:26 +00:00
if ( directChats . containsKey ( userID ) ) {
if ( ! directChats [ userID ] . contains ( id ) ) {
directChats [ userID ] . add ( id ) ;
} else {
return ;
} // Is already in direct chats
} else {
2019-06-12 09:46:57 +00:00
directChats [ userID ] = [ id ] ;
2020-01-02 14:33:26 +00:00
}
2019-06-12 09:46:57 +00:00
2020-01-02 14:09:49 +00:00
await client . jsonRequest (
2019-07-12 09:26:07 +00:00
type: HTTPType . PUT ,
2019-06-12 09:46:57 +00:00
action: " /client/r0/user/ ${ client . userID } /account_data/m.direct " ,
data: directChats ) ;
2019-12-29 10:28:33 +00:00
return ;
2019-06-12 09:46:57 +00:00
}
2019-12-29 10:28:33 +00:00
/// Removes this room from all direct chat tags.
Future < void > removeFromDirectChat ( ) async {
2019-09-30 08:19:28 +00:00
Map < String , dynamic > directChats = client . directChats ;
if ( directChats . containsKey ( directChatMatrixID ) & &
2020-01-02 14:33:26 +00:00
directChats [ directChatMatrixID ] . contains ( id ) ) {
2019-09-30 08:19:28 +00:00
directChats [ directChatMatrixID ] . remove ( id ) ;
2020-01-02 14:33:26 +00:00
} else {
return ;
} // Nothing to do here
2019-09-30 08:19:28 +00:00
2020-01-02 14:09:49 +00:00
await client . jsonRequest (
2019-09-30 08:19:28 +00:00
type: HTTPType . PUT ,
action: " /client/r0/user/ ${ client . userID } /account_data/m.direct " ,
data: directChats ) ;
2019-12-29 10:28:33 +00:00
return ;
2019-09-30 08:19:28 +00:00
}
2019-06-26 14:39:52 +00:00
/// Sends *m.fully_read* and *m.read* for the given event ID.
2019-12-29 10:28:33 +00:00
Future < void > sendReadReceipt ( String eventID ) async {
2019-12-17 11:07:25 +00:00
this . notificationCount = 0 ;
2020-01-02 14:33:26 +00:00
await client ? . store ? . resetNotificationCount ( this . id ) ;
await client . jsonRequest (
2019-07-12 09:26:07 +00:00
type: HTTPType . POST ,
2019-06-11 12:13:30 +00:00
action: " /client/r0/rooms/ $ id /read_markers " ,
2019-06-12 09:46:57 +00:00
data: {
" m.fully_read " : eventID ,
" m.read " : eventID ,
} ) ;
2019-12-29 10:28:33 +00:00
return ;
2019-06-11 12:13:30 +00:00
}
2020-02-15 07:48:41 +00:00
void restoreGroupSessionKeys ( ) async {
// Restore the inbound and outbound session keys
if ( client . encryptionEnabled & & client . storeAPI ! = null ) {
final String outboundGroupSessionPickle = await client . storeAPI . getItem (
" /clients/ ${ client . deviceID } /rooms/ ${ this . id } /outbound_group_session " ) ;
if ( outboundGroupSessionPickle ! = null ) {
try {
this . _outboundGroupSession = olm . OutboundGroupSession ( ) ;
this
. _outboundGroupSession
. unpickle ( client . userID , outboundGroupSessionPickle ) ;
} catch ( e ) {
this . _outboundGroupSession = null ;
print ( " [LibOlm] Unable to unpickle outboundGroupSession: " +
e . toString ( ) ) ;
}
}
2020-02-27 08:41:49 +00:00
final String outboundGroupSessionDevicesString = await client . storeAPI
. getItem (
" /clients/ ${ client . deviceID } /rooms/ ${ this . id } /outbound_group_session_devices " ) ;
if ( outboundGroupSessionDevicesString ! = null ) {
this . _outboundGroupSessionDevices =
List < String > . from ( json . decode ( outboundGroupSessionDevicesString ) ) ;
}
2020-02-15 07:48:41 +00:00
final String sessionKeysPickle = await client . storeAPI
. getItem ( " /clients/ ${ client . deviceID } /rooms/ ${ this . id } /session_keys " ) ;
if ( sessionKeysPickle ? . isNotEmpty ? ? false ) {
final Map < String , dynamic > map = json . decode ( sessionKeysPickle ) ;
2020-02-20 15:29:15 +00:00
if ( this . _sessionKeys = = null ) this . _sessionKeys = { } ;
2020-02-15 07:48:41 +00:00
for ( var entry in map . entries ) {
try {
this . _sessionKeys [ entry . key ] =
SessionKey . fromJson ( entry . value , client . userID ) ;
} catch ( e ) {
print ( " [LibOlm] Could not unpickle inboundGroupSession: " +
e . toString ( ) ) ;
}
}
}
}
2020-02-20 15:29:15 +00:00
await client . storeAPI ? . setItem (
" /clients/ ${ client . deviceID } /rooms/ ${ this . id } /session_keys " ,
json . encode ( sessionKeys ) ) ;
2020-02-18 09:23:55 +00:00
_fullyRestored = true ;
2020-02-15 07:48:41 +00:00
}
2020-02-18 09:23:55 +00:00
bool _fullyRestored = false ;
2019-08-07 07:50:40 +00:00
/// Returns a Room from a json String which comes normally from the store. If the
/// state are also given, the method will await them.
2019-06-12 09:46:57 +00:00
static Future < Room > getRoomFromTableRow (
2019-08-07 07:50:40 +00:00
Map < String , dynamic > row , Client matrix ,
2019-08-07 08:46:59 +00:00
{ Future < List < Map < String , dynamic > > > states ,
Future < List < Map < String , dynamic > > > roomAccountData } ) async {
2019-08-07 08:17:03 +00:00
Room newRoom = Room (
2019-08-28 11:06:41 +00:00
id: row [ " room_id " ] ,
2019-08-08 09:41:42 +00:00
membership: Membership . values
. firstWhere ( ( e ) = > e . toString ( ) = = ' Membership. ' + row [ " membership " ] ) ,
2019-06-09 10:16:48 +00:00
notificationCount: row [ " notification_count " ] ,
highlightCount: row [ " highlight_count " ] ,
2019-06-11 10:21:45 +00:00
notificationSettings: row [ " notification_settings " ] ,
prev_batch: row [ " prev_batch " ] ,
2019-08-06 09:47:09 +00:00
mInvitedMemberCount: row [ " invited_member_count " ] ,
mJoinedMemberCount: row [ " joined_member_count " ] ,
mHeroes: row [ " heroes " ] ? . split ( " , " ) ? ? [ ] ,
2019-06-11 08:51:45 +00:00
client: matrix ,
2019-08-08 09:41:42 +00:00
roomAccountData: { } ,
2019-06-09 10:16:48 +00:00
) ;
2019-08-07 08:17:03 +00:00
2020-02-15 07:48:41 +00:00
// Restore the inbound and outbound session keys
await newRoom . restoreGroupSessionKeys ( ) ;
2019-08-07 08:17:03 +00:00
if ( states ! = null ) {
List < Map < String , dynamic > > rawStates = await states ;
for ( int i = 0 ; i < rawStates . length ; i + + ) {
2020-01-02 14:09:49 +00:00
Event newState = Event . fromJson ( rawStates [ i ] , newRoom ) ;
2019-11-22 08:53:48 +00:00
newRoom . setState ( newState ) ;
2019-08-07 08:17:03 +00:00
}
}
2019-08-07 08:46:59 +00:00
Map < String , RoomAccountData > newRoomAccountData = { } ;
if ( roomAccountData ! = null ) {
List < Map < String , dynamic > > rawRoomAccountData = await roomAccountData ;
for ( int i = 0 ; i < rawRoomAccountData . length ; i + + ) {
RoomAccountData newData =
RoomAccountData . fromJson ( rawRoomAccountData [ i ] , newRoom ) ;
newRoomAccountData [ newData . typeKey ] = newData ;
}
newRoom . roomAccountData = newRoomAccountData ;
}
2019-06-09 10:16:48 +00:00
2019-08-07 08:46:59 +00:00
return newRoom ;
2019-06-09 10:16:48 +00:00
}
2019-06-26 14:39:52 +00:00
/// Creates a timeline from the store. Returns a [Timeline] object.
2019-06-25 10:06:26 +00:00
Future < Timeline > getTimeline (
{ onTimelineUpdateCallback onUpdate ,
onTimelineInsertCallback onInsert } ) async {
2020-01-23 10:43:01 +00:00
List < Event > events =
client . store ! = null ? await client . store . getEventList ( this ) : [ ] ;
2020-02-21 08:44:05 +00:00
// Try again to decrypt encrypted events and update the database.
if ( this . encrypted & & client . store ! = null ) {
await client . store . transaction ( ( ) {
for ( int i = 0 ; i < events . length ; i + + ) {
2020-02-21 08:56:40 +00:00
if ( events [ i ] . type = = EventTypes . Encrypted & &
events [ i ] . content [ " body " ] = = DecryptError . UNKNOWN_SESSION ) {
2020-02-21 08:44:05 +00:00
events [ i ] = events [ i ] . decrypted ;
2020-02-21 08:56:40 +00:00
if ( events [ i ] . type ! = EventTypes . Encrypted ) {
2020-02-21 08:44:05 +00:00
client . store . storeEventUpdate (
EventUpdate (
eventType: events [ i ] . typeKey ,
content: events [ i ] . toJson ( ) ,
roomID: events [ i ] . roomId ,
type: " timeline " ,
) ,
) ;
}
}
2020-02-15 07:48:41 +00:00
}
2020-02-21 08:44:05 +00:00
} ) ;
2020-02-15 07:48:41 +00:00
}
2020-02-21 08:44:05 +00:00
2020-01-23 10:43:01 +00:00
Timeline timeline = Timeline (
2019-06-21 10:18:54 +00:00
room: this ,
events: events ,
onUpdate: onUpdate ,
onInsert: onInsert ,
) ;
2020-01-23 10:43:01 +00:00
if ( client . store = = null ) {
prev_batch = " " ;
await requestHistory ( ) ;
}
return timeline ;
2019-06-21 10:18:54 +00:00
}
2019-09-02 10:09:30 +00:00
/// Returns all participants for this room. With lazy loading this
/// list may not be complete. User [requestParticipants] in this
/// case.
List < User > getParticipants ( ) {
List < User > userList = [ ] ;
2019-11-20 13:02:23 +00:00
if ( states [ " m.room.member " ] is Map < String , dynamic > ) {
for ( var entry in states [ " m.room.member " ] . entries ) {
2020-01-02 14:09:49 +00:00
Event state = entry . value ;
2019-11-20 13:02:23 +00:00
if ( state . type = = EventTypes . RoomMember ) userList . add ( state . asUser ) ;
}
}
2019-09-02 10:09:30 +00:00
return userList ;
}
2019-06-11 08:51:45 +00:00
/// Request the full list of participants from the server. The local list
/// from the store is not complete if the client uses lazy loading.
2019-06-18 10:06:55 +00:00
Future < List < User > > requestParticipants ( ) async {
2020-02-04 13:41:13 +00:00
if ( participantListComplete ) return getParticipants ( ) ;
2019-06-09 10:16:48 +00:00
List < User > participants = [ ] ;
2020-01-02 14:09:49 +00:00
dynamic res = await client . jsonRequest (
2019-07-12 09:26:07 +00:00
type: HTTPType . GET , action: " /client/r0/rooms/ ${ id } /members " ) ;
2019-06-09 10:16:48 +00:00
for ( num i = 0 ; i < res [ " chunk " ] . length ; i + + ) {
2020-01-02 14:09:49 +00:00
User newUser = Event . fromJson ( res [ " chunk " ] [ i ] , this ) . asUser ;
2020-02-04 13:41:13 +00:00
if ( ! [ Membership . leave , Membership . ban ] . contains ( newUser . membership ) ) {
participants . add ( newUser ) ;
setState ( newUser ) ;
}
2019-06-09 10:16:48 +00:00
}
2019-06-21 10:18:54 +00:00
return participants ;
2019-06-09 10:16:48 +00:00
}
2019-06-28 08:59:00 +00:00
2020-02-04 13:41:13 +00:00
/// Checks if the local participant list of joined and invited users is complete.
bool get participantListComplete {
List < User > knownParticipants = getParticipants ( ) ;
knownParticipants . removeWhere (
( u ) = > ! [ Membership . join , Membership . invite ] . contains ( u . membership ) ) ;
return knownParticipants . length = =
( this . mJoinedMemberCount ? ? 0 ) + ( this . mInvitedMemberCount ? ? 0 ) ;
}
2019-11-15 11:08:43 +00:00
/// Returns the [User] object for the given [mxID] or requests it from
/// the homeserver and waits for a response.
2019-07-29 14:16:20 +00:00
Future < User > getUserByMXID ( String mxID ) async {
2019-08-08 09:41:42 +00:00
if ( states [ mxID ] ! = null ) return states [ mxID ] . asUser ;
2019-11-15 11:08:43 +00:00
return requestUser ( mxID ) ;
}
/// Returns the [User] object for the given [mxID] or requests it from
/// the homeserver and returns a default [User] object while waiting.
User getUserByMXIDSync ( String mxID ) {
2020-01-02 14:33:26 +00:00
if ( states [ mxID ] ! = null ) {
2019-11-15 11:08:43 +00:00
return states [ mxID ] . asUser ;
2020-01-02 14:33:26 +00:00
} else {
2019-12-29 10:28:33 +00:00
try {
requestUser ( mxID ) ;
} catch ( _ ) { }
2019-11-15 11:08:43 +00:00
return User ( mxID , room: this ) ;
}
}
Set < String > _requestingMatrixIds = Set ( ) ;
/// Requests a missing [User] for this room. Important for clients using
/// lazy loading.
Future < User > requestUser ( String mxID ) async {
if ( mxID = = null | | ! _requestingMatrixIds . add ( mxID ) ) return null ;
2019-12-29 10:28:33 +00:00
Map < String , dynamic > resp ;
try {
2020-01-02 14:09:49 +00:00
resp = await client . jsonRequest (
2019-12-29 10:28:33 +00:00
type: HTTPType . GET ,
action: " /client/r0/rooms/ $ id /state/m.room.member/ $ mxID " ) ;
} catch ( exception ) {
2019-11-15 11:08:43 +00:00
_requestingMatrixIds . remove ( mxID ) ;
2019-12-29 10:28:33 +00:00
rethrow ;
2019-11-15 11:08:43 +00:00
}
final User user = User ( mxID ,
2019-09-17 12:21:16 +00:00
displayName: resp [ " displayname " ] ,
avatarUrl: resp [ " avatar_url " ] ,
room: this ) ;
2019-11-15 11:08:43 +00:00
states [ mxID ] = user ;
2020-01-02 14:33:26 +00:00
if ( client . store ! = null ) {
await client . store . transaction ( ( ) {
2019-11-15 11:08:43 +00:00
client . store . storeEventUpdate (
EventUpdate (
content: resp ,
roomID: id ,
type: " state " ,
eventType: " m.room.member " ) ,
) ;
return ;
} ) ;
2020-01-02 14:33:26 +00:00
}
2020-01-04 10:29:38 +00:00
if ( onUpdate ! = null ) onUpdate . add ( id ) ;
2019-11-15 11:08:43 +00:00
_requestingMatrixIds . remove ( mxID ) ;
return user ;
2019-07-29 14:16:20 +00:00
}
2019-11-29 11:12:04 +00:00
/// Searches for the event on the server. Returns null if not found.
2019-06-28 08:59:00 +00:00
Future < Event > getEventById ( String eventID ) async {
2020-01-02 14:09:49 +00:00
final dynamic resp = await client . jsonRequest (
2019-07-12 09:26:07 +00:00
type: HTTPType . GET , action: " /client/r0/rooms/ $ id /event/ $ eventID " ) ;
2019-08-07 08:17:03 +00:00
return Event . fromJson ( resp , this ) ;
2019-06-28 08:59:00 +00:00
}
2019-08-07 09:23:57 +00:00
2019-09-03 15:57:27 +00:00
/// Returns the power level of the given user ID.
2019-08-08 09:41:42 +00:00
int getPowerLevelByUserId ( String userId ) {
2019-08-07 09:23:57 +00:00
int powerLevel = 0 ;
2020-01-02 14:09:49 +00:00
Event powerLevelState = states [ " m.room.power_levels " ] ;
2019-08-07 09:23:57 +00:00
if ( powerLevelState = = null ) return powerLevel ;
2020-01-02 14:33:26 +00:00
if ( powerLevelState . content [ " users_default " ] is int ) {
2019-08-07 09:23:57 +00:00
powerLevel = powerLevelState . content [ " users_default " ] ;
2020-01-02 14:33:26 +00:00
}
2019-08-08 09:41:42 +00:00
if ( powerLevelState . content [ " users " ] is Map < String , dynamic > & &
2020-01-02 14:33:26 +00:00
powerLevelState . content [ " users " ] [ userId ] ! = null ) {
2019-08-08 09:41:42 +00:00
powerLevel = powerLevelState . content [ " users " ] [ userId ] ;
2020-01-02 14:33:26 +00:00
}
2019-08-07 09:23:57 +00:00
return powerLevel ;
}
2019-08-08 09:41:42 +00:00
/// Returns the user's own power level.
int get ownPowerLevel = > getPowerLevelByUserId ( client . userID ) ;
2019-08-07 09:23:57 +00:00
/// Returns the power levels from all users for this room or null if not given.
Map < String , int > get powerLevels {
2020-01-02 14:09:49 +00:00
Event powerLevelState = states [ " m.room.power_levels " ] ;
2020-01-02 14:33:26 +00:00
if ( powerLevelState . content [ " users " ] is Map < String , int > ) {
2019-08-07 09:23:57 +00:00
return powerLevelState . content [ " users " ] ;
2020-01-02 14:33:26 +00:00
}
2019-08-07 09:23:57 +00:00
return null ;
}
2019-09-09 13:22:02 +00:00
2019-12-29 10:28:33 +00:00
/// Uploads a new user avatar for this room. Returns the event ID of the new
/// m.room.avatar event.
Future < String > setAvatar ( MatrixFile file ) async {
2020-01-02 14:09:49 +00:00
final String uploadResp = await client . upload ( file ) ;
final Map < String , dynamic > setAvatarResp = await client . jsonRequest (
type: HTTPType . PUT ,
action: " /client/r0/rooms/ $ id /state/m.room.avatar/ " ,
data: { " url " : uploadResp } ) ;
2019-09-09 13:22:02 +00:00
return setAvatarResp [ " event_id " ] ;
}
2019-11-26 06:38:44 +00:00
bool _hasPermissionFor ( String action ) {
if ( getState ( " m.room.power_levels " ) = = null | |
2020-01-05 08:15:25 +00:00
getState ( " m.room.power_levels " ) . content [ action ] = = null ) return true ;
2019-11-26 06:38:44 +00:00
return ownPowerLevel > = getState ( " m.room.power_levels " ) . content [ action ] ;
}
/// The level required to ban a user.
bool get canBan = > _hasPermissionFor ( " ban " ) ;
/// The default level required to send message events. Can be overridden by the events key.
bool get canSendDefaultMessages = > _hasPermissionFor ( " events_default " ) ;
/// The level required to invite a user.
bool get canInvite = > _hasPermissionFor ( " invite " ) ;
/// The level required to kick a user.
bool get canKick = > _hasPermissionFor ( " kick " ) ;
/// The level required to redact an event.
bool get canRedact = > _hasPermissionFor ( " redact " ) ;
/// The default level required to send state events. Can be overridden by the events key.
bool get canSendDefaultStates = > _hasPermissionFor ( " state_default " ) ;
bool get canChangePowerLevel = > canSendEvent ( " m.room.power_levels " ) ;
bool canSendEvent ( String eventType ) {
2020-01-05 08:15:25 +00:00
if ( getState ( " m.room.power_levels " ) = = null ) return true ;
2019-12-05 09:06:23 +00:00
if ( getState ( " m.room.power_levels " ) . content [ " events " ] = = null | |
2020-01-02 14:33:26 +00:00
getState ( " m.room.power_levels " ) . content [ " events " ] [ eventType ] = = null ) {
2019-11-26 06:38:44 +00:00
return eventType = = " m.room.message "
? canSendDefaultMessages
: canSendDefaultStates ;
2020-01-02 14:33:26 +00:00
}
2019-11-26 06:38:44 +00:00
return ownPowerLevel > =
getState ( " m.room.power_levels " ) . content [ " events " ] [ eventType ] ;
}
2019-12-04 09:58:47 +00:00
/// Returns the [PushRuleState] for this room, based on the m.push_rules stored in
/// the account_data.
PushRuleState get pushRuleState {
if ( ! client . accountData . containsKey ( " m.push_rules " ) | |
2020-01-02 14:33:26 +00:00
! ( client . accountData [ " m.push_rules " ] . content [ " global " ] is Map ) ) {
2019-12-04 09:58:47 +00:00
return PushRuleState . notify ;
2020-01-02 14:33:26 +00:00
}
2019-12-04 09:58:47 +00:00
final Map < String , dynamic > globalPushRules =
client . accountData [ " m.push_rules " ] . content [ " global " ] ;
if ( globalPushRules = = null ) return PushRuleState . notify ;
if ( globalPushRules [ " override " ] is List ) {
for ( var i = 0 ; i < globalPushRules [ " override " ] . length ; i + + ) {
if ( globalPushRules [ " override " ] [ i ] [ " rule_id " ] = = id ) {
if ( globalPushRules [ " override " ] [ i ] [ " actions " ]
. indexOf ( " dont_notify " ) ! =
- 1 ) {
return PushRuleState . dont_notify ;
}
break ;
}
}
}
if ( globalPushRules [ " room " ] is List ) {
for ( var i = 0 ; i < globalPushRules [ " room " ] . length ; i + + ) {
if ( globalPushRules [ " room " ] [ i ] [ " rule_id " ] = = id ) {
if ( globalPushRules [ " room " ] [ i ] [ " actions " ] . indexOf ( " dont_notify " ) ! =
- 1 ) {
return PushRuleState . mentions_only ;
}
break ;
}
}
}
return PushRuleState . notify ;
}
/// Sends a request to the homeserver to set the [PushRuleState] for this room.
/// Returns ErrorResponse if something goes wrong.
Future < dynamic > setPushRuleState ( PushRuleState newState ) async {
if ( newState = = pushRuleState ) return null ;
dynamic resp ;
switch ( newState ) {
// All push notifications should be sent to the user
case PushRuleState . notify:
2020-01-02 14:33:26 +00:00
if ( pushRuleState = = PushRuleState . dont_notify ) {
2020-01-02 14:09:49 +00:00
resp = await client . jsonRequest (
2019-12-04 09:58:47 +00:00
type: HTTPType . DELETE ,
action: " /client/r0/pushrules/global/override/ $ id " ,
data: { } ) ;
2020-01-02 14:33:26 +00:00
} else if ( pushRuleState = = PushRuleState . mentions_only ) {
2020-01-02 14:09:49 +00:00
resp = await client . jsonRequest (
2019-12-04 09:58:47 +00:00
type: HTTPType . DELETE ,
action: " /client/r0/pushrules/global/room/ $ id " ,
data: { } ) ;
2020-01-02 14:33:26 +00:00
}
2019-12-04 09:58:47 +00:00
break ;
// Only when someone mentions the user, a push notification should be sent
case PushRuleState . mentions_only:
if ( pushRuleState = = PushRuleState . dont_notify ) {
2020-01-02 14:09:49 +00:00
resp = await client . jsonRequest (
2019-12-04 09:58:47 +00:00
type: HTTPType . DELETE ,
action: " /client/r0/pushrules/global/override/ $ id " ,
data: { } ) ;
2020-01-02 14:09:49 +00:00
resp = await client . jsonRequest (
2019-12-04 09:58:47 +00:00
type: HTTPType . PUT ,
action: " /client/r0/pushrules/global/room/ $ id " ,
data: {
" actions " : [ " dont_notify " ]
} ) ;
2020-01-02 14:33:26 +00:00
} else if ( pushRuleState = = PushRuleState . notify ) {
2020-01-02 14:09:49 +00:00
resp = await client . jsonRequest (
2019-12-04 09:58:47 +00:00
type: HTTPType . PUT ,
action: " /client/r0/pushrules/global/room/ $ id " ,
data: {
" actions " : [ " dont_notify " ]
} ) ;
2020-01-02 14:33:26 +00:00
}
2019-12-04 09:58:47 +00:00
break ;
// No push notification should be ever sent for this room.
case PushRuleState . dont_notify:
if ( pushRuleState = = PushRuleState . mentions_only ) {
2020-01-02 14:09:49 +00:00
resp = await client . jsonRequest (
2019-12-04 09:58:47 +00:00
type: HTTPType . DELETE ,
action: " /client/r0/pushrules/global/room/ $ id " ,
data: { } ) ;
}
2020-01-02 14:09:49 +00:00
resp = await client . jsonRequest (
2019-12-04 09:58:47 +00:00
type: HTTPType . PUT ,
action: " /client/r0/pushrules/global/override/ $ id " ,
data: {
" actions " : [ " dont_notify " ] ,
" conditions " : [
{ " key " : " room_id " , " kind " : " event_match " , " pattern " : id }
]
} ) ;
}
return resp ;
}
2019-12-12 12:19:18 +00:00
/// Redacts this event. Returns [ErrorResponse] on error.
Future < dynamic > redactEvent ( String eventId ,
{ String reason , String txid } ) async {
// Create new transaction id
String messageID ;
final int now = DateTime . now ( ) . millisecondsSinceEpoch ;
if ( txid = = null ) {
messageID = " msg $ now " ;
2020-01-02 14:33:26 +00:00
} else {
2019-12-12 12:19:18 +00:00
messageID = txid ;
2020-01-02 14:33:26 +00:00
}
2019-12-12 12:19:18 +00:00
Map < String , dynamic > data = { } ;
if ( reason ! = null ) data [ " reason " ] = reason ;
2020-01-02 14:09:49 +00:00
final dynamic resp = await client . jsonRequest (
2019-12-12 12:19:18 +00:00
type: HTTPType . PUT ,
action: " /client/r0/rooms/ $ id /redact/ $ eventId / $ messageID " ,
data: data ) ;
return resp ;
}
2019-12-16 11:55:13 +00:00
Future < dynamic > sendTypingInfo ( bool isTyping , { int timeout } ) {
2019-12-17 11:07:25 +00:00
Map < String , dynamic > data = {
2019-12-16 11:55:13 +00:00
" typing " : isTyping ,
} ;
if ( timeout ! = null ) data [ " timeout " ] = timeout ;
2020-01-02 14:09:49 +00:00
return client . jsonRequest (
2019-12-16 11:55:13 +00:00
type: HTTPType . PUT ,
action: " /client/r0/rooms/ ${ this . id } /typing/ ${ client . userID } " ,
data: data ,
) ;
}
2020-01-04 18:36:17 +00:00
/// This is sent by the caller when they wish to establish a call.
/// [callId] is a unique identifier for the call.
/// [version] is the version of the VoIP specification this message adheres to. This specification is version 0.
/// [lifetime] is the time in milliseconds that the invite is valid for. Once the invite age exceeds this value,
/// clients should discard it. They should also no longer show the call as awaiting an answer in the UI.
/// [type] The type of session description. Must be 'offer'.
/// [sdp] The SDP text of the session description.
Future < String > inviteToCall ( String callId , int lifetime , String sdp ,
{ String type = " offer " , int version = 0 , String txid } ) async {
if ( txid = = null ) txid = " txid ${ DateTime . now ( ) . millisecondsSinceEpoch } " ;
final Map < String , dynamic > response = await client . jsonRequest (
type: HTTPType . PUT ,
action: " /client/r0/rooms/ $ id /send/m.call.invite/ $ txid " ,
data: {
" call_id " : callId ,
" lifetime " : lifetime ,
" offer " : { " sdp " : sdp , " type " : type } ,
" version " : version ,
} ,
) ;
return response [ " event_id " ] ;
}
/// This is sent by callers after sending an invite and by the callee after answering.
/// Its purpose is to give the other party additional ICE candidates to try using to communicate.
///
/// [callId] The ID of the call this event relates to.
///
/// [version] The version of the VoIP specification this messages adheres to. This specification is version 0.
///
/// [candidates] Array of objects describing the candidates. Example:
///
/// ```
/// [
/// {
/// "candidate": "candidate:863018703 1 udp 2122260223 10.9.64.156 43670 typ host generation 0",
/// "sdpMLineIndex": 0,
/// "sdpMid": "audio"
/// }
/// ],
/// ```
Future < String > sendCallCandidates (
String callId ,
List < Map < String , dynamic > > candidates , {
int version = 0 ,
String txid ,
} ) async {
if ( txid = = null ) txid = " txid ${ DateTime . now ( ) . millisecondsSinceEpoch } " ;
final Map < String , dynamic > response = await client . jsonRequest (
type: HTTPType . PUT ,
action: " /client/r0/rooms/ $ id /send/m.call.candidates/ $ txid " ,
data: {
" call_id " : callId ,
" candidates " : candidates ,
" version " : version ,
} ,
) ;
return response [ " event_id " ] ;
}
/// This event is sent by the callee when they wish to answer the call.
/// [callId] is a unique identifier for the call.
/// [version] is the version of the VoIP specification this message adheres to. This specification is version 0.
/// [type] The type of session description. Must be 'answer'.
/// [sdp] The SDP text of the session description.
Future < String > answerCall ( String callId , String sdp ,
{ String type = " answer " , int version = 0 , String txid } ) async {
if ( txid = = null ) txid = " txid ${ DateTime . now ( ) . millisecondsSinceEpoch } " ;
final Map < String , dynamic > response = await client . jsonRequest (
type: HTTPType . PUT ,
action: " /client/r0/rooms/ $ id /send/m.call.answer/ $ txid " ,
data: {
" call_id " : callId ,
" answer " : { " sdp " : sdp , " type " : type } ,
" version " : version ,
} ,
) ;
return response [ " event_id " ] ;
}
/// This event is sent by the callee when they wish to answer the call.
/// [callId] The ID of the call this event relates to.
/// [version] is the version of the VoIP specification this message adheres to. This specification is version 0.
Future < String > hangupCall ( String callId ,
{ int version = 0 , String txid } ) async {
if ( txid = = null ) txid = " txid ${ DateTime . now ( ) . millisecondsSinceEpoch } " ;
final Map < String , dynamic > response = await client . jsonRequest (
type: HTTPType . PUT ,
action: " /client/r0/rooms/ $ id /send/m.call.hangup/ $ txid " ,
data: {
" call_id " : callId ,
" version " : version ,
} ,
) ;
return response [ " event_id " ] ;
}
2019-12-04 09:58:47 +00:00
2020-01-18 14:49:15 +00:00
/// Returns all aliases for this room.
List < String > get aliases {
List < String > aliases = [ ] ;
for ( Event aliasEvent in states . states [ " m.room.aliases " ] . values ) {
if ( aliasEvent . content [ " aliases " ] is List ) {
aliases . addAll ( aliasEvent . content [ " aliases " ] ) ;
}
}
return aliases ;
}
/// A room may be public meaning anyone can join the room without any prior action. Alternatively,
/// it can be invite meaning that a user who wishes to join the room must first receive an invite
/// to the room from someone already inside of the room. Currently, knock and private are reserved
/// keywords which are not implemented.
JoinRules get joinRules = > getState ( " m.room.join_rules " ) ! = null
? JoinRules . values . firstWhere (
( r ) = >
r . toString ( ) . replaceAll ( " JoinRules. " , " " ) = =
getState ( " m.room.join_rules " ) . content [ " join_rule " ] ,
orElse: ( ) = > null )
: null ;
/// Changes the join rules. You should check first if the user is able to change it.
Future < void > setJoinRules ( JoinRules joinRules ) async {
await client . jsonRequest (
type: HTTPType . PUT ,
action: " /client/r0/rooms/ $ id /state/m.room.join_rules/ " ,
data: {
" join_rule " : joinRules . toString ( ) . replaceAll ( " JoinRules. " , " " ) ,
} ,
) ;
return ;
}
/// Whether the user has the permission to change the join rules.
bool get canChangeJoinRules = > canSendEvent ( " m.room.join_rules " ) ;
/// This event controls whether guest users are allowed to join rooms. If this event
/// is absent, servers should act as if it is present and has the guest_access value "forbidden".
GuestAccess get guestAccess = > getState ( " m.room.guest_access " ) ! = null
? GuestAccess . values . firstWhere (
( r ) = >
r . toString ( ) . replaceAll ( " GuestAccess. " , " " ) = =
getState ( " m.room.guest_access " ) . content [ " guest_access " ] ,
orElse: ( ) = > GuestAccess . forbidden )
: GuestAccess . forbidden ;
/// Changes the guest access. You should check first if the user is able to change it.
Future < void > setGuestAccess ( GuestAccess guestAccess ) async {
await client . jsonRequest (
type: HTTPType . PUT ,
action: " /client/r0/rooms/ $ id /state/m.room.guest_access/ " ,
data: {
" guest_access " : guestAccess . toString ( ) . replaceAll ( " GuestAccess. " , " " ) ,
} ,
) ;
return ;
}
/// Whether the user has the permission to change the guest access.
bool get canChangeGuestAccess = > canSendEvent ( " m.room.guest_access " ) ;
/// This event controls whether a user can see the events that happened in a room from before they joined.
HistoryVisibility get historyVisibility = >
getState ( " m.room.history_visibility " ) ! = null
? HistoryVisibility . values . firstWhere (
( r ) = >
r . toString ( ) . replaceAll ( " HistoryVisibility. " , " " ) = =
getState ( " m.room.history_visibility " )
. content [ " history_visibility " ] ,
orElse: ( ) = > null )
: null ;
/// Changes the history visibility. You should check first if the user is able to change it.
Future < void > setHistoryVisibility ( HistoryVisibility historyVisibility ) async {
await client . jsonRequest (
type: HTTPType . PUT ,
action: " /client/r0/rooms/ $ id /state/m.room.history_visibility/ " ,
data: {
" history_visibility " :
historyVisibility . toString ( ) . replaceAll ( " HistoryVisibility. " , " " ) ,
} ,
) ;
return ;
}
/// Whether the user has the permission to change the history visibility.
bool get canChangeHistoryVisibility = >
canSendEvent ( " m.room.history_visibility " ) ;
2020-02-04 13:41:13 +00:00
/// Returns the encryption algorithm. Currently only `m.megolm.v1.aes-sha2` is supported.
/// Returns null if there is no encryption algorithm.
String get encryptionAlgorithm = > getState ( " m.room.encryption " ) ! = null
? getState ( " m.room.encryption " ) . content [ " algorithm " ] . toString ( )
: null ;
/// Checks if this room is encrypted.
bool get encrypted = > encryptionAlgorithm ! = null ;
Future < void > enableEncryption ( { int algorithmIndex = 0 } ) async {
if ( encrypted ) throw ( " Encryption is already enabled! " ) ;
final String algorithm =
Client . supportedGroupEncryptionAlgorithms [ algorithmIndex ] ;
await client . jsonRequest (
type: HTTPType . PUT ,
action: " /client/r0/rooms/ $ id /state/m.room.encryption/ " ,
data: {
" algorithm " : algorithm ,
} ,
) ;
return ;
}
2020-02-15 07:48:41 +00:00
/// Returns all known device keys for all participants in this room.
2020-02-04 13:41:13 +00:00
Future < List < DeviceKeys > > getUserDeviceKeys ( ) async {
List < DeviceKeys > deviceKeys = [ ] ;
List < User > users = await requestParticipants ( ) ;
for ( final userDeviceKeyEntry in client . userDeviceKeys . entries ) {
if ( users . indexWhere ( ( u ) = > u . id = = userDeviceKeyEntry . key ) = = - 1 ) {
continue ;
}
for ( DeviceKeys deviceKeyEntry
in userDeviceKeyEntry . value . deviceKeys . values ) {
deviceKeys . add ( deviceKeyEntry ) ;
}
}
return deviceKeys ;
}
2020-02-15 07:48:41 +00:00
/// Encrypts the given json payload and creates a send-ready m.room.encrypted
/// payload. This will create a new outgoingGroupSession if necessary.
Future < Map < String , dynamic > > encryptGroupMessagePayload (
Map < String , dynamic > payload ,
{ String type = " m.room.message " } ) async {
2020-02-19 09:59:21 +00:00
if ( ! this . encrypted | | ! client . encryptionEnabled ) return payload ;
2020-02-15 07:48:41 +00:00
if ( this . encryptionAlgorithm ! = " m.megolm.v1.aes-sha2 " ) {
throw ( " Unknown encryption algorithm " ) ;
}
if ( _outboundGroupSession = = null ) {
await createOutboundGroupSession ( ) ;
}
final Map < String , dynamic > payloadContent = {
" content " : payload ,
" type " : type ,
" room_id " : id ,
} ;
Map < String , dynamic > encryptedPayload = {
" algorithm " : " m.megolm.v1.aes-sha2 " ,
" ciphertext " : _outboundGroupSession . encrypt ( json . encode ( payloadContent ) ) ,
" device_id " : client . deviceID ,
" sender_key " : client . identityKey ,
" session_id " : _outboundGroupSession . session_id ( ) ,
} ;
2020-02-18 07:42:52 +00:00
await _storeOutboundGroupSession ( ) ;
2020-02-15 07:48:41 +00:00
return encryptedPayload ;
}
/// Decrypts the given [event] with one of the available ingoingGroupSessions.
2020-02-21 08:44:05 +00:00
/// Returns a m.bad.encrypted event if it fails and does nothing if the event
/// was not encrypted.
2020-02-15 07:48:41 +00:00
Event decryptGroupMessage ( Event event ) {
2020-02-19 07:54:56 +00:00
if ( event . type ! = EventTypes . Encrypted ) return event ;
2020-02-18 07:02:17 +00:00
Map < String , dynamic > decryptedPayload ;
try {
2020-02-18 07:42:52 +00:00
if ( ! client . encryptionEnabled ) {
2020-02-21 08:56:40 +00:00
throw ( DecryptError . NOT_ENABLED ) ;
2020-02-18 07:42:52 +00:00
}
2020-02-18 07:02:17 +00:00
if ( event . content [ " algorithm " ] ! = " m.megolm.v1.aes-sha2 " ) {
2020-02-21 08:56:40 +00:00
throw ( DecryptError . UNKNOWN_ALGORITHM ) ;
2020-02-18 07:02:17 +00:00
}
final String sessionId = event . content [ " session_id " ] ;
if ( ! sessionKeys . containsKey ( sessionId ) ) {
2020-02-21 08:56:40 +00:00
throw ( DecryptError . UNKNOWN_SESSION ) ;
2020-02-18 07:02:17 +00:00
}
final olm . DecryptResult decryptResult = sessionKeys [ sessionId ]
. inboundGroupSession
. decrypt ( event . content [ " ciphertext " ] ) ;
final String messageIndexKey =
event . eventId + event . time . millisecondsSinceEpoch . toString ( ) ;
if ( sessionKeys [ sessionId ] . indexes . containsKey ( messageIndexKey ) & &
sessionKeys [ sessionId ] . indexes [ messageIndexKey ] ! =
decryptResult . message_index ) {
2020-02-18 07:42:52 +00:00
if ( ( _outboundGroupSession ? . session_id ( ) ? ? " " ) = = sessionId ) {
clearOutboundGroupSession ( ) ;
}
2020-02-21 08:56:40 +00:00
throw ( DecryptError . CHANNEL_CORRUPTED ) ;
2020-02-18 07:02:17 +00:00
}
sessionKeys [ sessionId ] . indexes [ messageIndexKey ] =
decryptResult . message_index ;
2020-02-18 07:42:52 +00:00
_storeOutboundGroupSession ( ) ;
2020-02-18 07:02:17 +00:00
// TODO: The client should check that the sender's fingerprint key matches the keys.ed25519 property of the event which established the Megolm session when marking the event as verified.
2020-02-15 07:48:41 +00:00
2020-02-18 07:02:17 +00:00
decryptedPayload = json . decode ( decryptResult . plaintext ) ;
} catch ( exception ) {
2020-02-21 08:56:40 +00:00
if ( exception . toString ( ) = = DecryptError . UNKNOWN_SESSION ) {
2020-02-21 08:44:05 +00:00
decryptedPayload = {
" content " : event . content ,
" type " : " m.room.encrypted " ,
} ;
decryptedPayload [ " content " ] [ " body " ] = exception . toString ( ) ;
decryptedPayload [ " content " ] [ " msgtype " ] = " m.bad.encrypted " ;
} else {
decryptedPayload = {
" content " : {
" msgtype " : " m.bad.encrypted " ,
" body " : exception . toString ( ) ,
} ,
" type " : " m.room.encrypted " ,
} ;
}
2020-02-18 07:02:17 +00:00
}
2020-02-15 07:48:41 +00:00
return Event (
content: decryptedPayload [ " content " ] ,
typeKey: decryptedPayload [ " type " ] ,
senderId: event . senderId ,
eventId: event . eventId ,
roomId: event . roomId ,
room: event . room ,
time: event . time ,
unsigned: event . unsigned ,
stateKey: event . stateKey ,
prevContent: event . prevContent ,
status: event . status ,
) ;
}
2020-01-18 14:49:15 +00:00
}
2020-02-21 08:56:40 +00:00
abstract class DecryptError {
static const String NOT_ENABLED = " Encryption is not enabled in your client. " ;
static const String UNKNOWN_ALGORITHM = " Unknown encryption algorithm. " ;
static const String UNKNOWN_SESSION =
" The sender has not sent us the session key. " ;
static const String CHANNEL_CORRUPTED =
" The secure channel with the sender was corrupted. " ;
}