2019-06-09 11:57:33 +00:00
/ *
2020-06-03 10:16:01 +00:00
* Famedly Matrix SDK
* Copyright ( C ) 2019 , 2020 Famedly GmbH
2019-06-09 11:57:33 +00:00
*
2020-06-03 10:16:01 +00:00
* This program is free software: you can redistribute it and / or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation , either version 3 of the
* License , or ( at your option ) any later version .
2019-06-09 11:57:33 +00:00
*
2020-06-03 10:16:01 +00:00
* This program 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 Affero General Public License for more details .
2019-06-09 11:57:33 +00:00
*
2020-06-03 10:16:01 +00:00
* You should have received a copy of the GNU Affero General Public License
* along with this program . If not , see < https: //www.gnu.org/licenses/>.
2019-06-09 11:57:33 +00:00
* /
2019-06-09 10:16:48 +00:00
import ' dart:async ' ;
2020-05-02 05:02:11 +00:00
import ' dart:convert ' ;
2019-06-09 10:16:48 +00:00
import ' dart:core ' ;
2019-07-12 09:26:07 +00:00
2020-05-02 05:02:11 +00:00
import ' package:http/http.dart ' as http ;
2020-08-13 08:40:39 +00:00
import ' ../encryption.dart ' ;
import ' ../famedlysdk.dart ' ;
2020-07-20 11:19:57 +00:00
import ' database/database.dart ' show Database ;
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 ' event.dart ' ;
2020-05-02 05:02:11 +00:00
import ' room.dart ' ;
2020-07-20 11:19:57 +00:00
import ' user.dart ' ;
2020-08-13 08:40:39 +00:00
import ' utils/device_keys_list.dart ' ;
2020-06-03 10:16:01 +00:00
import ' utils/event_update.dart ' ;
2020-08-13 08:40:39 +00:00
import ' utils/logs.dart ' ;
import ' utils/matrix_file.dart ' ;
2020-06-03 10:16:01 +00:00
import ' utils/room_update.dart ' ;
2020-08-13 08:40:39 +00:00
import ' utils/to_device_event.dart ' ;
2019-06-09 10:16:48 +00:00
2020-01-03 13:21:15 +00:00
typedef RoomSorter = int Function ( Room a , Room b ) ;
2019-09-02 08:33:32 +00:00
2020-01-02 14:09:49 +00:00
enum LoginState { logged , loggedOut }
2019-06-09 12:33:25 +00:00
/// Represents a Matrix client to communicate with a
2019-06-09 10:16:48 +00:00
/// [Matrix](https://matrix.org) homeserver and is the entry point for this
/// SDK.
2020-08-11 16:11:51 +00:00
class Client extends MatrixApi {
2020-05-15 18:40:17 +00:00
int _id ;
int get id = > _id ;
2019-06-09 10:16:48 +00:00
2020-05-15 18:40:17 +00:00
Database database ;
2020-01-24 16:42:51 +00:00
2020-05-20 08:24:48 +00:00
bool enableE2eeRecovery ;
2020-08-11 16:11:51 +00:00
@ deprecated
MatrixApi get api = > this ;
2020-06-03 10:16:01 +00:00
2020-06-04 11:39:51 +00:00
Encryption encryption ;
2020-06-05 20:03:28 +00:00
Set < KeyVerificationMethod > verificationMethods ;
2020-07-01 09:09:31 +00:00
Set < String > importantStateEvents ;
2020-09-16 08:18:13 +00:00
Set < String > roomPreviewLastEvents ;
2020-09-21 10:28:13 +00:00
int sendMessageTimeoutSeconds ;
2020-05-20 08:24:48 +00:00
/// Create a client
2020-09-16 08:18:13 +00:00
/// [clientName] = unique identifier of this client
/// [database]: The database instance to use
/// [enableE2eeRecovery]: Enable additional logic to try to recover from bad e2ee sessions
/// [verificationMethods]: A set of all the verification methods this client can handle. Includes:
2020-05-30 11:55:09 +00:00
/// KeyVerificationMethod.numbers: Compare numbers. Most basic, should be supported
/// KeyVerificationMethod.emoji: Compare emojis
2020-09-16 08:18:13 +00:00
/// [importantStateEvents]: A set of all the important state events to load when the client connects.
2020-07-01 09:09:31 +00:00
/// To speed up performance only a set of state events is loaded on startup, those that are
/// needed to display a room list. All the remaining state events are automatically post-loaded
/// when opening the timeline of a room or manually by calling `room.postLoad()`.
/// This set will always include the following state events:
/// - m.room.name
/// - m.room.avatar
/// - m.room.message
/// - m.room.encrypted
/// - m.room.encryption
/// - m.room.canonical_alias
/// - m.room.tombstone
/// - *some* m.room.member events, where needed
2020-09-16 08:18:13 +00:00
/// [roomPreviewLastEvents]: The event types that should be used to calculate the last event
/// in a room for the room list.
2020-08-06 06:55:35 +00:00
Client (
this . clientName , {
this . database ,
this . enableE2eeRecovery = false ,
this . verificationMethods ,
http . Client httpClient ,
this . importantStateEvents ,
2020-09-16 08:18:13 +00:00
this . roomPreviewLastEvents ,
2020-08-06 06:55:35 +00:00
this . pinUnreadRooms = false ,
2020-09-21 10:28:13 +00:00
this . sendMessageTimeoutSeconds = 60 ,
2020-08-06 06:55:35 +00:00
@ deprecated bool debug ,
} ) {
2020-06-05 20:03:28 +00:00
verificationMethods ? ? = < KeyVerificationMethod > { } ;
2020-09-16 08:18:13 +00:00
importantStateEvents ? ? = { } ;
2020-07-01 09:09:31 +00:00
importantStateEvents . addAll ( [
2020-07-02 08:32:11 +00:00
EventTypes . RoomName ,
EventTypes . RoomAvatar ,
EventTypes . Message ,
EventTypes . Encrypted ,
EventTypes . Encryption ,
EventTypes . RoomCanonicalAlias ,
EventTypes . RoomTombstone ,
2020-07-01 09:09:31 +00:00
] ) ;
2020-09-16 08:18:13 +00:00
roomPreviewLastEvents ? ? = { } ;
roomPreviewLastEvents . addAll ( [
EventTypes . Message ,
EventTypes . Encrypted ,
EventTypes . Sticker ,
] ) ;
2020-08-14 16:22:31 +00:00
this . httpClient = httpClient ? ? http . Client ( ) ;
2019-06-09 10:16:48 +00:00
}
/// The required name for this client.
final String clientName ;
/// The Matrix ID of the current logged user.
2020-01-02 14:09:49 +00:00
String get userID = > _userID ;
String _userID ;
2019-06-09 10:16:48 +00:00
/// This points to the position in the synchronization history.
String prevBatch ;
/// The device ID is an unique identifier for this device.
2020-01-02 14:09:49 +00:00
String get deviceID = > _deviceID ;
String _deviceID ;
2019-06-09 10:16:48 +00:00
/// The device name is a human readable identifier for this device.
2020-01-02 14:09:49 +00:00
String get deviceName = > _deviceName ;
String _deviceName ;
2019-06-09 10:16:48 +00:00
/// Returns the current login state.
2020-08-11 16:11:51 +00:00
bool isLogged ( ) = > accessToken ! = null ;
2019-06-09 10:16:48 +00:00
2019-08-07 10:06:28 +00:00
/// A list of all rooms the user is participating or invited.
2020-01-02 14:09:49 +00:00
List < Room > get rooms = > _rooms ;
List < Room > _rooms = [ ] ;
2019-08-07 09:38:51 +00:00
2020-02-15 07:48:41 +00:00
/// Whether this client supports end-to-end encryption using olm.
2020-06-04 11:39:51 +00:00
bool get encryptionEnabled = > encryption ! = null & & encryption . enabled ;
2020-02-15 07:48:41 +00:00
2020-03-16 10:38:03 +00:00
/// Whether this client is able to encrypt and decrypt files.
2020-06-04 11:39:51 +00:00
bool get fileEncryptionEnabled = > encryptionEnabled & & true ;
String get identityKey = > encryption ? . identityKey ? ? ' ' ;
String get fingerprintKey = > encryption ? . fingerprintKey ? ? ' ' ;
2020-03-16 10:38:03 +00:00
2020-05-26 13:58:14 +00:00
/// Wheather this session is unknown to others
2020-05-27 15:37:14 +00:00
bool get isUnknownSession = >
! userDeviceKeys . containsKey ( userID ) | |
! userDeviceKeys [ userID ] . deviceKeys . containsKey ( deviceID ) | |
! userDeviceKeys [ userID ] . deviceKeys [ deviceID ] . signed ;
2020-05-26 13:58:14 +00:00
2020-01-04 13:51:00 +00:00
/// Warning! This endpoint is for testing only!
set rooms ( List < Room > newList ) {
2020-08-06 09:35:02 +00:00
Logs . warning ( ' Warning! This endpoint is for testing only! ' ) ;
2020-01-04 13:51:00 +00:00
_rooms = newList ;
}
2019-08-07 10:06:28 +00:00
/// Key/Value store of account data.
2020-06-03 10:16:01 +00:00
Map < String , BasicEvent > accountData = { } ;
2019-08-07 10:06:28 +00:00
/// Presences of users by a given matrix ID
Map < String , Presence > presences = { } ;
2020-05-29 06:49:37 +00:00
int _transactionCounter = 0 ;
2020-06-03 10:16:01 +00:00
2020-05-29 06:49:37 +00:00
String generateUniqueTransactionId ( ) {
_transactionCounter + + ;
return ' ${ clientName } - ${ _transactionCounter } - ${ DateTime . now ( ) . millisecondsSinceEpoch } ' ;
}
2020-01-02 14:09:49 +00:00
Room getRoomByAlias ( String alias ) {
2020-03-30 09:08:38 +00:00
for ( var i = 0 ; i < rooms . length ; i + + ) {
2020-01-02 14:09:49 +00:00
if ( rooms [ i ] . canonicalAlias = = alias ) return rooms [ i ] ;
}
return null ;
}
Room getRoomById ( String id ) {
2020-03-30 09:08:38 +00:00
for ( var j = 0 ; j < rooms . length ; j + + ) {
2020-01-02 14:09:49 +00:00
if ( rooms [ j ] . id = = id ) return rooms [ j ] ;
}
return null ;
}
2019-08-08 07:58:37 +00:00
Map < String , dynamic > get directChats = >
2020-03-30 09:08:38 +00:00
accountData [ ' m.direct ' ] ! = null ? accountData [ ' m.direct ' ] . content : { } ;
2019-08-07 10:06:28 +00:00
/// Returns the (first) room ID from the store which is a private chat with the user [userId].
/// Returns null if there is none.
2019-08-29 09:12:14 +00:00
String getDirectChatFromUserId ( String userId ) {
2020-03-30 09:08:38 +00:00
if ( accountData [ ' m.direct ' ] ! = null & &
accountData [ ' m.direct ' ] . content [ userId ] is List < dynamic > & &
accountData [ ' m.direct ' ] . content [ userId ] . length > 0 ) {
2020-06-22 06:48:30 +00:00
for ( final roomId in accountData [ ' m.direct ' ] . content [ userId ] ) {
final room = getRoomById ( roomId ) ;
if ( room ! = null & & room . membership = = Membership . join ) {
return roomId ;
}
2020-01-02 14:33:26 +00:00
}
2019-08-29 09:12:14 +00:00
}
2020-03-30 09:08:38 +00:00
for ( var i = 0 ; i < rooms . length ; i + + ) {
if ( rooms [ i ] . membership = = Membership . invite & &
rooms [ i ] . states [ userID ] ? . senderId = = userId & &
rooms [ i ] . states [ userID ] . content [ ' is_direct ' ] = = true ) {
return rooms [ i ] . id ;
2020-01-02 14:33:26 +00:00
}
}
2019-08-29 09:12:14 +00:00
return null ;
}
2019-08-07 10:06:28 +00:00
2020-05-16 06:42:56 +00:00
/// Gets discovery information about the domain. The file may include additional keys.
Future < WellKnownInformations > getWellKnownInformationsByUserId (
String MatrixIdOrDomain ,
) async {
final response = await http
. get ( ' https:// ${ MatrixIdOrDomain . domain } /.well-known/matrix/client ' ) ;
2020-10-06 20:36:40 +00:00
var wellKnown = WellKnownInformations . fromJson ( json . decode ( response . body ) ) ;
2020-10-14 19:06:13 +00:00
if ( Uri . parse ( wellKnown . mHomeserver . baseUrl ) . host ! =
MatrixIdOrDomain . domain ) {
2020-10-14 20:07:36 +00:00
try {
final response = await http . get (
' https:// ${ Uri . parse ( wellKnown . mHomeserver . baseUrl ) . host } /.well-known/matrix/client ' ) ;
if ( response . statusCode = = 200 ) {
wellKnown =
WellKnownInformations . fromJson ( json . decode ( response . body ) ) ;
}
} catch ( _ ) { }
2020-06-13 19:39:18 +00:00
}
return wellKnown ;
2020-05-16 06:42:56 +00:00
}
2020-10-14 19:06:13 +00:00
Future < WellKnownInformations > getWellKnownInformationsByDomain (
dynamic serverUrl ) async {
2020-10-06 20:36:40 +00:00
var homeserver = ( serverUrl is Uri ) ? serverUrl : Uri . parse ( serverUrl ) ;
2020-10-14 19:06:13 +00:00
final response =
await http . get ( ' https:// ${ homeserver . host } /.well-known/matrix/client ' ) ;
2020-10-06 20:36:40 +00:00
var wellKnown = WellKnownInformations . fromJson ( json . decode ( response . body ) ) ;
2020-06-13 19:39:18 +00:00
if ( Uri . parse ( wellKnown . mHomeserver . baseUrl ) . host ! = homeserver . host ) {
2020-10-14 20:07:36 +00:00
try {
final response = await http . get (
' https:// ${ Uri . parse ( wellKnown . mHomeserver . baseUrl ) . host } /.well-known/matrix/client ' ) ;
if ( response . statusCode = = 200 ) {
wellKnown =
WellKnownInformations . fromJson ( json . decode ( response . body ) ) ;
}
} catch ( _ ) { }
2020-10-14 19:06:13 +00:00
}
2020-06-13 19:39:18 +00:00
return wellKnown ;
}
2020-10-23 09:34:08 +00:00
@ Deprecated ( ' Use [checkHomeserver] instead. ' )
2020-06-03 10:16:01 +00:00
Future < bool > checkServer ( dynamic serverUrl ) async {
2019-12-29 10:28:33 +00:00
try {
2020-10-23 09:34:08 +00:00
await checkHomeserver ( serverUrl ) ;
} catch ( _ ) {
return false ;
}
return true ;
}
/// Checks the supported versions of the Matrix protocol and the supported
/// login types. Throws an exception if the server is not compatible with the
/// client and sets [homeserver] to [serverUrl] if it is. Supports the types [Uri]
/// and [String].
Future < void > checkHomeserver ( dynamic homeserverUrl ,
{ Set < String > supportedLoginTypes = supportedLoginTypes } ) async {
try {
if ( homeserverUrl is Uri ) {
homeserver = homeserverUrl ;
2020-07-27 07:40:25 +00:00
} else {
// URLs allow to have whitespace surrounding them, see https://www.w3.org/TR/2011/WD-html5-20110525/urls.html
// As we want to strip a trailing slash, though, we have to trim the url ourself
// and thus can't let Uri.parse() deal with it.
2020-10-23 09:34:08 +00:00
homeserverUrl = homeserverUrl . trim ( ) ;
2020-07-27 07:40:25 +00:00
// strip a trailing slash
2020-10-23 09:34:08 +00:00
if ( homeserverUrl . endsWith ( ' / ' ) ) {
homeserverUrl = homeserverUrl . substring ( 0 , homeserverUrl . length - 1 ) ;
2020-07-27 07:40:25 +00:00
}
2020-10-23 09:34:08 +00:00
homeserver = Uri . parse ( homeserverUrl ) ;
2020-07-27 07:40:25 +00:00
}
2020-08-11 16:11:51 +00:00
final versions = await requestSupportedVersions ( ) ;
2019-12-29 10:28:33 +00:00
2020-10-23 09:34:08 +00:00
if ( ! versions . versions
. any ( ( version ) = > supportedVersions . contains ( version ) ) ) {
throw Exception (
' Server supports the versions: ${ versions . versions . toString ( ) } but this application is only compatible with ${ supportedVersions . toString ( ) } . ' ) ;
2019-06-09 10:16:48 +00:00
}
2020-08-11 16:11:51 +00:00
final loginTypes = await requestLoginTypes ( ) ;
2020-10-23 09:34:08 +00:00
if ( ! loginTypes . flows . any ( ( f ) = > supportedLoginTypes . contains ( f . type ) ) ) {
throw Exception (
' Server supports the Login Types: ${ loginTypes . flows . map ( ( f ) = > f . toJson ) . toList ( ) . toString ( ) } but this application is only compatible with ${ supportedLoginTypes . toString ( ) } . ' ) ;
2019-06-09 10:16:48 +00:00
}
2020-06-03 10:16:01 +00:00
2020-10-23 09:34:08 +00:00
return ;
2019-12-29 10:28:33 +00:00
} catch ( _ ) {
2020-08-11 16:11:51 +00:00
homeserver = null ;
2019-12-29 10:28:33 +00:00
rethrow ;
2019-06-09 10:16:48 +00:00
}
}
2020-01-14 15:16:24 +00:00
/// Checks to see if a username is available, and valid, for the server.
/// Returns the fully-qualified Matrix user ID (MXID) that has been registered.
2020-10-23 09:34:08 +00:00
/// You have to call [checkHomeserver] first to set a homeserver.
2020-08-11 16:11:51 +00:00
@ override
Future < LoginResponse > register ( {
2020-01-14 15:16:24 +00:00
String username ,
String password ,
String deviceId ,
String initialDeviceDisplayName ,
bool inhibitLogin ,
2020-08-11 16:11:51 +00:00
Map < String , dynamic > auth ,
String kind ,
2020-01-14 15:16:24 +00:00
} ) async {
2020-08-11 16:11:51 +00:00
final response = await super . register (
2020-06-03 10:16:01 +00:00
username: username ,
password: password ,
auth: auth ,
deviceId: deviceId ,
initialDeviceDisplayName: initialDeviceDisplayName ,
inhibitLogin: inhibitLogin ,
) ;
2020-01-14 15:16:24 +00:00
// Connect if there is an access token in the response.
2020-06-03 10:16:01 +00:00
if ( response . accessToken = = null | |
response . deviceId = = null | |
response . userId = = null ) {
throw ' Registered but token, device ID or user ID is null. ' ;
}
await connect (
newToken: response . accessToken ,
newUserID: response . userId ,
2020-08-11 16:11:51 +00:00
newHomeserver: homeserver ,
2020-06-03 10:16:01 +00:00
newDeviceName: initialDeviceDisplayName ? ? ' ' ,
newDeviceID: response . deviceId ) ;
2020-08-11 16:11:51 +00:00
return response ;
2020-01-14 15:16:24 +00:00
}
2019-06-09 10:16:48 +00:00
/// Handles the login and allows the client to call all APIs which require
2019-12-29 10:28:33 +00:00
/// authentication. Returns false if the login was not successful. Throws
/// MatrixException if login was not successful.
2020-10-23 09:34:08 +00:00
/// You have to call [checkHomeserver] first to set a homeserver.
2020-08-11 16:11:51 +00:00
@ override
Future < LoginResponse > login ( {
String type = ' m.login.password ' ,
String userIdentifierType = ' m.id.user ' ,
String user ,
String medium ,
String address ,
String password ,
String token ,
2020-01-14 15:16:24 +00:00
String deviceId ,
2020-08-11 16:11:51 +00:00
String initialDeviceDisplayName ,
2020-01-14 15:16:24 +00:00
} ) async {
2020-08-11 16:11:51 +00:00
final loginResp = await super . login (
type: type ,
userIdentifierType: userIdentifierType ,
user: user ,
2020-06-03 10:16:01 +00:00
password: password ,
deviceId: deviceId ,
initialDeviceDisplayName: initialDeviceDisplayName ,
2020-08-11 16:11:51 +00:00
medium: medium ,
address: address ,
token: token ,
2020-06-03 10:16:01 +00:00
) ;
2020-03-30 09:08:38 +00:00
2020-06-03 10:16:01 +00:00
// Connect if there is an access token in the response.
if ( loginResp . accessToken = = null | |
loginResp . deviceId = = null | |
loginResp . userId = = null ) {
2020-08-11 16:11:51 +00:00
throw Exception ( ' Registered but token, device ID or user ID is null. ' ) ;
2020-06-03 10:16:01 +00:00
}
await connect (
newToken: loginResp . accessToken ,
newUserID: loginResp . userId ,
2020-08-11 16:11:51 +00:00
newHomeserver: homeserver ,
2020-06-03 10:16:01 +00:00
newDeviceName: initialDeviceDisplayName ? ? ' ' ,
newDeviceID: loginResp . deviceId ,
) ;
2020-08-11 16:11:51 +00:00
return loginResp ;
2019-06-09 10:16:48 +00:00
}
/// Sends a logout command to the homeserver and clears all local data,
/// including all persistent data from the store.
2020-08-11 16:11:51 +00:00
@ override
2019-06-09 10:16:48 +00:00
Future < void > logout ( ) async {
2019-12-29 10:28:33 +00:00
try {
2020-08-11 16:11:51 +00:00
await super . logout ( ) ;
2020-08-06 09:35:02 +00:00
} catch ( e , s ) {
Logs . error ( e , s ) ;
2019-12-29 10:28:33 +00:00
rethrow ;
} finally {
2020-03-30 09:08:38 +00:00
await clear ( ) ;
2019-12-29 10:28:33 +00:00
}
2019-06-09 10:16:48 +00:00
}
2020-08-21 09:02:20 +00:00
/// Sends a logout command to the homeserver and clears all local data,
/// including all persistent data from the store.
@ override
Future < void > logoutAll ( ) async {
try {
await super . logoutAll ( ) ;
} catch ( e , s ) {
Logs . error ( e , s ) ;
2019-12-29 10:28:33 +00:00
rethrow ;
} finally {
2020-03-30 09:08:38 +00:00
await clear ( ) ;
2019-12-29 10:28:33 +00:00
}
2019-06-09 10:16:48 +00:00
}
2020-01-18 14:49:15 +00:00
/// Returns the user's own displayname and avatar url. In Matrix it is possible that
/// one user can have different displaynames and avatar urls in different rooms. So
/// this endpoint first checks if the profile is the same in all rooms. If not, the
/// profile will be requested from the homserver.
Future < Profile > get ownProfile async {
if ( rooms . isNotEmpty ) {
2020-03-30 09:08:38 +00:00
var profileSet = < Profile > { } ;
for ( var room in rooms ) {
2020-01-18 14:49:15 +00:00
final user = room . getUserByMXIDSync ( userID ) ;
profileSet . add ( Profile . fromJson ( user . content ) ) ;
}
if ( profileSet . length = = 1 ) return profileSet . first ;
}
return getProfileFromUserId ( userID ) ;
}
2020-05-18 11:45:49 +00:00
final Map < String , Profile > _profileCache = { } ;
/// Get the combined profile information for this user.
/// If [getFromRooms] is true then the profile will first be searched from the
/// room memberships. This is unstable if the given user makes use of different displaynames
/// and avatars per room, which is common for some bots and bridges.
/// If [cache] is true then
/// the profile get cached for this session. Please note that then the profile may
/// become outdated if the user changes the displayname or avatar in this session.
Future < Profile > getProfileFromUserId ( String userId ,
{ bool cache = true , bool getFromRooms = true } ) async {
if ( getFromRooms ) {
final room = rooms . firstWhere (
( Room room ) = >
room
. getParticipants ( )
. indexWhere ( ( User user ) = > user . id = = userId ) ! =
- 1 ,
orElse: ( ) = > null ) ;
if ( room ! = null ) {
final user =
room . getParticipants ( ) . firstWhere ( ( User user ) = > user . id = = userId ) ;
return Profile ( user . displayName , user . avatarUrl ) ;
}
}
if ( cache & & _profileCache . containsKey ( userId ) ) {
return _profileCache [ userId ] ;
}
2020-08-11 16:11:51 +00:00
final profile = await requestProfile ( userId ) ;
2020-05-18 11:45:49 +00:00
_profileCache [ userId ] = profile ;
return profile ;
2019-11-30 09:36:30 +00:00
}
2019-12-19 11:26:21 +00:00
Future < List < Room > > get archive async {
2020-03-30 09:08:38 +00:00
var archiveList = < Room > [ ] ;
2020-08-11 16:11:51 +00:00
final syncResp = await sync (
2020-06-03 10:16:01 +00:00
filter: ' {"room":{"include_leave":true,"timeline":{"limit":10}}} ' ,
timeout: 0 ,
) ;
2020-08-11 16:11:51 +00:00
if ( syncResp . rooms . leave is Map < String , dynamic > ) {
for ( var entry in syncResp . rooms . leave . entries ) {
2020-06-03 10:16:01 +00:00
final id = entry . key ;
final room = entry . value ;
2020-03-30 09:08:38 +00:00
var leftRoom = Room (
2019-12-19 11:26:21 +00:00
id: id ,
membership: Membership . leave ,
client: this ,
2020-06-03 10:16:01 +00:00
roomAccountData:
room . accountData ? . asMap ( ) ? . map ( ( k , v ) = > MapEntry ( v . type , v ) ) ? ?
< String , BasicRoomEvent > { } ,
2019-12-19 11:26:21 +00:00
mHeroes: [ ] ) ;
2020-06-03 10:16:01 +00:00
if ( room . timeline ? . events ! = null ) {
for ( var event in room . timeline . events ) {
leftRoom . setState ( Event . fromMatrixEvent ( event , leftRoom ) ) ;
2019-12-19 11:26:21 +00:00
}
}
2020-06-03 10:16:01 +00:00
if ( room . state ! = null ) {
for ( var event in room . state ) {
leftRoom . setState ( Event . fromMatrixEvent ( event , leftRoom ) ) ;
2019-12-19 11:26:21 +00:00
}
}
archiveList . add ( leftRoom ) ;
}
}
2019-11-29 16:19:32 +00:00
return archiveList ;
2019-09-19 14:00:17 +00:00
}
2019-12-29 10:28:33 +00:00
/// Uploads a new user avatar for this user.
Future < void > setAvatar ( MatrixFile file ) async {
2020-08-11 16:11:51 +00:00
final uploadResp = await upload ( file . bytes , file . name ) ;
await setAvatarUrl ( userID , Uri . parse ( uploadResp ) ) ;
2019-12-29 10:28:33 +00:00
return ;
2019-09-09 13:22:02 +00:00
}
2020-01-14 11:27:26 +00:00
/// Returns the push rules for the logged in user.
2020-06-03 10:16:01 +00:00
PushRuleSet get pushRules = > accountData . containsKey ( ' m.push_rules ' )
? PushRuleSet . fromJson ( accountData [ ' m.push_rules ' ] . content )
2020-01-14 11:27:26 +00:00
: null ;
2020-10-23 09:34:08 +00:00
static const Set < String > supportedVersions = { ' r0.5.0 ' , ' r0.6.0 ' } ;
static const Set < String > supportedLoginTypes = { ' m.login.password ' } ;
static const String syncFilters =
' {"room":{"state":{"lazy_load_members":true}}} ' ;
static const String messagesFilters = ' {"lazy_load_members":true} ' ;
2020-02-04 13:41:13 +00:00
static const List < String > supportedDirectEncryptionAlgorithms = [
2020-03-30 09:08:38 +00:00
' m.olm.v1.curve25519-aes-sha2 '
2020-02-04 13:41:13 +00:00
] ;
static const List < String > supportedGroupEncryptionAlgorithms = [
2020-03-30 09:08:38 +00:00
' m.megolm.v1.aes-sha2 '
2020-02-04 13:41:13 +00:00
] ;
2020-04-23 08:18:33 +00:00
static const int defaultThumbnailSize = 256 ;
2020-01-02 14:09:49 +00:00
/// The newEvent signal is the most important signal in this concept. Every time
/// the app receives a new synchronization, this event is called for every signal
/// to update the GUI. For example, for a new message, it is called:
/// onRoomEvent( "m.room.message", "!chat_id:server.com", "timeline", {sender: "@bob:server.com", body: "Hello world"} )
2020-01-02 14:33:26 +00:00
final StreamController < EventUpdate > onEvent = StreamController . broadcast ( ) ;
2020-01-02 14:09:49 +00:00
/// Outside of the events there are updates for the global chat states which
/// are handled by this signal:
final StreamController < RoomUpdate > onRoomUpdate =
2020-01-02 14:33:26 +00:00
StreamController . broadcast ( ) ;
2020-01-02 14:09:49 +00:00
2020-02-15 07:48:41 +00:00
/// The onToDeviceEvent is called when there comes a new to device event. It is
/// already decrypted if necessary.
final StreamController < ToDeviceEvent > onToDeviceEvent =
StreamController . broadcast ( ) ;
2020-01-02 14:09:49 +00:00
/// Called when the login state e.g. user gets logged out.
final StreamController < LoginState > onLoginStateChanged =
2020-01-02 14:33:26 +00:00
StreamController . broadcast ( ) ;
2020-01-02 14:09:49 +00:00
2020-06-01 18:24:41 +00:00
/// Synchronization erros are coming here.
2020-08-26 07:38:14 +00:00
final StreamController < SdkError > onSyncError = StreamController . broadcast ( ) ;
2020-06-01 18:24:41 +00:00
2020-05-22 13:51:45 +00:00
/// Synchronization erros are coming here.
final StreamController < ToDeviceEventDecryptionError > onOlmError =
StreamController . broadcast ( ) ;
2020-01-02 14:09:49 +00:00
/// This is called once, when the first sync has received.
2020-01-02 14:33:26 +00:00
final StreamController < bool > onFirstSync = StreamController . broadcast ( ) ;
2020-01-02 14:09:49 +00:00
/// When a new sync response is coming in, this gives the complete payload.
2020-06-03 10:16:01 +00:00
final StreamController < SyncUpdate > onSync = StreamController . broadcast ( ) ;
2020-01-02 14:09:49 +00:00
2020-01-04 10:29:38 +00:00
/// Callback will be called on presences.
final StreamController < Presence > onPresence = StreamController . broadcast ( ) ;
/// Callback will be called on account data updates.
2020-06-03 10:16:01 +00:00
final StreamController < BasicEvent > onAccountData =
2020-01-04 10:29:38 +00:00
StreamController . broadcast ( ) ;
2020-01-04 18:36:17 +00:00
/// Will be called on call invites.
final StreamController < Event > onCallInvite = StreamController . broadcast ( ) ;
/// Will be called on call hangups.
final StreamController < Event > onCallHangup = StreamController . broadcast ( ) ;
/// Will be called on call candidates.
final StreamController < Event > onCallCandidates = StreamController . broadcast ( ) ;
/// Will be called on call answers.
final StreamController < Event > onCallAnswer = StreamController . broadcast ( ) ;
2020-02-21 15:05:19 +00:00
/// Will be called when another device is requesting session keys for a room.
final StreamController < RoomKeyRequest > onRoomKeyRequest =
StreamController . broadcast ( ) ;
2020-05-17 13:25:42 +00:00
/// Will be called when another device is requesting verification with this device.
2020-05-18 11:45:49 +00:00
final StreamController < KeyVerification > onKeyVerificationRequest =
StreamController . broadcast ( ) ;
2020-05-17 13:25:42 +00:00
2020-01-02 14:09:49 +00:00
/// How long should the app wait until it retrys the synchronisation after
/// an error?
int syncErrorTimeoutSec = 3 ;
/// Sets the user credentials and starts the synchronisation.
///
/// Before you can connect you need at least an [accessToken], a [homeserver],
/// a [userID], a [deviceID], and a [deviceName].
///
/// You get this informations
/// by logging in to your Matrix account, using the [login API](https://matrix.org/docs/spec/client_server/r0.4.0.html#post-matrix-client-r0-login).
///
/// To log in you can use [jsonRequest()] after you have set the [homeserver]
/// to a valid url. For example:
///
/// ```
/// final resp = await matrix
2020-06-03 10:16:01 +00:00
/// .jsonRequest(type: RequestType.POST, action: "/client/r0/login", data: {
2020-01-02 14:09:49 +00:00
/// "type": "m.login.password",
/// "user": "test",
/// "password": "1234",
2020-08-11 16:11:51 +00:00
/// "initial_device_display_name": "Matrix Client"
2020-01-02 14:09:49 +00:00
/// });
/// ```
///
/// Returns:
///
/// ```
/// {
/// "user_id": "@cheeky_monkey:matrix.org",
/// "access_token": "abc123",
/// "device_id": "GHTYAJCE"
/// }
/// ```
///
/// Sends [LoginState.logged] to [onLoginStateChanged].
2020-02-04 13:41:13 +00:00
void connect ( {
String newToken ,
2020-06-03 10:16:01 +00:00
Uri newHomeserver ,
2020-02-04 13:41:13 +00:00
String newUserID ,
String newDeviceName ,
String newDeviceID ,
String newPrevBatch ,
2020-02-15 07:48:41 +00:00
String newOlmAccount ,
2020-02-04 13:41:13 +00:00
} ) async {
2020-05-15 18:40:17 +00:00
String olmAccount ;
if ( database ! = null ) {
final account = await database . getClient ( clientName ) ;
if ( account ! = null ) {
_id = account . clientId ;
2020-08-11 16:11:51 +00:00
homeserver = Uri . parse ( account . homeserverUrl ) ;
accessToken = account . token ;
2020-05-15 18:40:17 +00:00
_userID = account . userId ;
_deviceID = account . deviceId ;
_deviceName = account . deviceName ;
prevBatch = account . prevBatch ;
olmAccount = account . olmAccount ;
}
}
2020-08-11 16:11:51 +00:00
accessToken = newToken ? ? accessToken ;
homeserver = newHomeserver ? ? homeserver ;
2020-05-15 18:40:17 +00:00
_userID = newUserID ? ? _userID ;
_deviceID = newDeviceID ? ? _deviceID ;
_deviceName = newDeviceName ? ? _deviceName ;
prevBatch = newPrevBatch ? ? prevBatch ;
olmAccount = newOlmAccount ? ? olmAccount ;
2020-08-11 16:11:51 +00:00
if ( accessToken = = null | | homeserver = = null | | _userID = = null ) {
2020-05-15 18:40:17 +00:00
// we aren't logged in
2020-06-04 11:39:51 +00:00
encryption ? . dispose ( ) ;
encryption = null ;
2020-05-16 08:03:59 +00:00
onLoginStateChanged . add ( LoginState . loggedOut ) ;
2020-05-15 18:40:17 +00:00
return ;
}
2020-01-02 14:09:49 +00:00
2020-08-17 12:25:48 +00:00
encryption ? . dispose ( ) ;
2020-08-06 06:55:35 +00:00
encryption =
Encryption ( client: this , enableE2eeRecovery: enableE2eeRecovery ) ;
2020-06-04 11:39:51 +00:00
await encryption . init ( olmAccount ) ;
2020-02-15 07:48:41 +00:00
2020-05-15 18:40:17 +00:00
if ( database ! = null ) {
if ( id ! = null ) {
await database . updateClient (
2020-08-11 16:11:51 +00:00
homeserver . toString ( ) ,
accessToken ,
2020-05-16 06:42:56 +00:00
_userID ,
_deviceID ,
_deviceName ,
prevBatch ,
2020-06-04 11:39:51 +00:00
encryption ? . pickledOlmAccount ,
2020-05-16 06:42:56 +00:00
id ,
2020-05-15 18:40:17 +00:00
) ;
} else {
_id = await database . insertClient (
2020-05-16 06:42:56 +00:00
clientName ,
2020-08-11 16:11:51 +00:00
homeserver . toString ( ) ,
accessToken ,
2020-05-16 06:42:56 +00:00
_userID ,
_deviceID ,
_deviceName ,
prevBatch ,
2020-06-04 11:39:51 +00:00
encryption ? . pickledOlmAccount ,
2020-05-15 18:40:17 +00:00
) ;
2020-01-24 16:42:51 +00:00
}
2020-06-05 20:03:28 +00:00
_userDeviceKeys = await database . getUserDeviceKeys ( this ) ;
2020-05-15 18:40:17 +00:00
_rooms = await database . getRoomList ( this , onlyLeft: false ) ;
_sortRooms ( ) ;
accountData = await database . getAccountData ( id ) ;
2020-06-30 11:41:52 +00:00
presences . clear ( ) ;
2020-01-02 14:09:49 +00:00
}
onLoginStateChanged . add ( LoginState . logged ) ;
2020-08-06 09:35:02 +00:00
Logs . success (
2020-08-11 16:11:51 +00:00
' Successfully connected as ${ userID . localpart } with ${ homeserver . toString ( ) } ' ,
2020-08-06 09:35:02 +00:00
) ;
2020-01-02 14:09:49 +00:00
2020-09-04 11:10:09 +00:00
// Always do a _sync after login, even if backgroundSync is set to off
2020-01-02 14:33:26 +00:00
return _sync ( ) ;
2020-01-02 14:09:49 +00:00
}
2020-05-29 06:49:37 +00:00
/// Used for testing only
void setUserId ( String s ) {
_userID = s ;
}
2020-01-02 14:09:49 +00:00
/// Resets all settings and stops the synchronisation.
void clear ( ) {
2020-05-15 18:40:17 +00:00
database ? . clear ( id ) ;
2020-08-11 16:11:51 +00:00
_id = accessToken =
homeserver = _userID = _deviceID = _deviceName = prevBatch = null ;
2020-05-04 06:19:15 +00:00
_rooms = [ ] ;
2020-06-04 11:39:51 +00:00
encryption ? . dispose ( ) ;
encryption = null ;
2020-01-02 14:09:49 +00:00
onLoginStateChanged . add ( LoginState . loggedOut ) ;
}
2020-09-04 11:10:09 +00:00
bool _backgroundSync = true ;
Future < void > _currentSync , _retryDelay = Future . value ( ) ;
bool get syncPending = > _currentSync ! = null ;
2020-01-02 14:09:49 +00:00
2020-09-04 11:10:09 +00:00
/// Controls the background sync (automatically looping forever if turned on).
set backgroundSync ( bool enabled ) {
_backgroundSync = enabled ;
if ( _backgroundSync ) {
_sync ( ) ;
}
}
/// Immediately start a sync and wait for completion.
/// If there is an active sync already, wait for the active sync instead.
Future < void > oneShotSync ( ) {
return _sync ( ) ;
}
2020-01-02 14:09:49 +00:00
2020-09-04 11:10:09 +00:00
Future < void > _sync ( ) {
if ( _currentSync = = null ) {
_currentSync = _innerSync ( ) ;
_currentSync . whenComplete ( ( ) {
_currentSync = null ;
if ( _backgroundSync & & isLogged ( ) & & ! _disposed ) {
_sync ( ) ;
}
} ) ;
}
return _currentSync ;
}
Future < void > _innerSync ( ) async {
await _retryDelay ;
_retryDelay = Future . delayed ( Duration ( seconds: syncErrorTimeoutSec ) ) ;
if ( ! isLogged ( ) | | _disposed ) return null ;
2020-01-02 14:09:49 +00:00
try {
2020-09-04 11:10:09 +00:00
final syncResp = await sync (
2020-06-03 10:16:01 +00:00
filter: syncFilters ,
since: prevBatch ,
timeout: prevBatch ! = null ? 30000 : null ,
2020-09-04 11:10:09 +00:00
) ;
2020-05-18 14:01:14 +00:00
if ( _disposed ) return ;
2020-05-19 07:49:37 +00:00
if ( database ! = null ) {
2020-08-21 15:20:26 +00:00
_currentTransaction = database . transaction ( ( ) async {
2020-05-19 07:49:37 +00:00
await handleSync ( syncResp ) ;
2020-06-03 10:16:01 +00:00
if ( prevBatch ! = syncResp . nextBatch ) {
await database . storePrevBatch ( syncResp . nextBatch , id ) ;
2020-05-19 07:49:37 +00:00
}
} ) ;
2020-08-21 15:20:26 +00:00
await _currentTransaction ;
2020-05-19 07:49:37 +00:00
} else {
await handleSync ( syncResp ) ;
}
2020-05-18 14:01:14 +00:00
if ( _disposed ) return ;
2020-03-30 09:08:38 +00:00
if ( prevBatch = = null ) {
onFirstSync . add ( true ) ;
2020-06-03 10:16:01 +00:00
prevBatch = syncResp . nextBatch ;
2020-01-06 20:21:25 +00:00
_sortRooms ( ) ;
}
2020-06-03 10:16:01 +00:00
prevBatch = syncResp . nextBatch ;
2020-10-06 09:48:34 +00:00
await database ? . deleteOldFiles (
DateTime . now ( ) . subtract ( Duration ( days: 30 ) ) . millisecondsSinceEpoch ) ;
2020-02-20 07:28:15 +00:00
await _updateUserDeviceKeys ( ) ;
2020-06-04 11:39:51 +00:00
if ( encryptionEnabled ) {
encryption . onSync ( ) ;
}
2020-09-04 11:10:09 +00:00
_retryDelay = Future . value ( ) ;
2020-10-15 07:08:49 +00:00
} on MatrixException catch ( e , s ) {
onSyncError . add ( SdkError ( exception: e , stackTrace: s ) ) ;
if ( e . error = = MatrixError . M_UNKNOWN_TOKEN ) {
Logs . warning ( ' The user has been logged out! ' ) ;
clear ( ) ;
}
2020-06-01 18:24:41 +00:00
} catch ( e , s ) {
2020-09-04 11:10:09 +00:00
if ( ! isLogged ( ) | | _disposed ) return ;
2020-08-06 09:35:02 +00:00
Logs . error ( ' Error during processing events: ' + e . toString ( ) , s ) ;
2020-08-26 07:38:14 +00:00
onSyncError . add ( SdkError (
2020-06-01 18:24:41 +00:00
exception: e is Exception ? e : Exception ( e ) , stackTrace: s ) ) ;
2020-01-02 14:09:49 +00:00
}
}
2020-02-15 07:48:41 +00:00
/// Use this method only for testing utilities!
2020-07-21 07:34:30 +00:00
Future < void > handleSync ( SyncUpdate sync , { bool sortAtTheEnd = false } ) async {
2020-06-03 10:16:01 +00:00
if ( sync . toDevice ! = null ) {
2020-06-04 11:39:51 +00:00
await _handleToDeviceEvents ( sync . toDevice ) ;
2020-06-03 10:16:01 +00:00
}
if ( sync . rooms ! = null ) {
if ( sync . rooms . join ! = null ) {
2020-07-21 07:34:30 +00:00
await _handleRooms ( sync . rooms . join , Membership . join ,
sortAtTheEnd: sortAtTheEnd ) ;
2020-01-02 14:33:26 +00:00
}
2020-06-03 10:16:01 +00:00
if ( sync . rooms . invite ! = null ) {
2020-07-21 07:34:30 +00:00
await _handleRooms ( sync . rooms . invite , Membership . invite ,
sortAtTheEnd: sortAtTheEnd ) ;
2020-01-02 14:33:26 +00:00
}
2020-06-03 10:16:01 +00:00
if ( sync . rooms . leave ! = null ) {
2020-07-21 07:34:30 +00:00
await _handleRooms ( sync . rooms . leave , Membership . leave ,
sortAtTheEnd: sortAtTheEnd ) ;
2020-01-02 14:33:26 +00:00
}
2020-09-27 08:54:54 +00:00
_sortRooms ( ) ;
2020-01-02 14:09:49 +00:00
}
2020-06-03 10:16:01 +00:00
if ( sync . presence ! = null ) {
for ( final newPresence in sync . presence ) {
presences [ newPresence . senderId ] = newPresence ;
onPresence . add ( newPresence ) ;
}
2020-01-02 14:09:49 +00:00
}
2020-06-03 10:16:01 +00:00
if ( sync . accountData ! = null ) {
for ( final newAccountData in sync . accountData ) {
if ( database ! = null ) {
2020-06-25 07:16:59 +00:00
await database . storeAccountData (
2020-06-03 10:16:01 +00:00
id ,
newAccountData . type ,
2020-06-26 16:46:54 +00:00
jsonEncode ( newAccountData . content ) ,
2020-06-03 10:16:01 +00:00
) ;
}
accountData [ newAccountData . type ] = newAccountData ;
if ( onAccountData ! = null ) onAccountData . add ( newAccountData ) ;
}
2020-01-02 14:09:49 +00:00
}
2020-06-03 10:16:01 +00:00
if ( sync . deviceLists ! = null ) {
await _handleDeviceListsEvents ( sync . deviceLists ) ;
2020-02-04 13:41:13 +00:00
}
2020-06-04 11:39:51 +00:00
if ( sync . deviceOneTimeKeysCount ! = null & & encryptionEnabled ) {
encryption . handleDeviceOneTimeKeysCount ( sync . deviceOneTimeKeysCount ) ;
2020-04-02 08:39:00 +00:00
}
2020-01-02 14:09:49 +00:00
onSync . add ( sync ) ;
}
2020-06-03 10:16:01 +00:00
Future < void > _handleDeviceListsEvents ( DeviceListsUpdate deviceLists ) async {
if ( deviceLists . changed is List ) {
for ( final userId in deviceLists . changed ) {
2020-02-04 13:41:13 +00:00
if ( _userDeviceKeys . containsKey ( userId ) ) {
_userDeviceKeys [ userId ] . outdated = true ;
2020-05-15 18:40:17 +00:00
if ( database ! = null ) {
2020-05-19 07:49:37 +00:00
await database . storeUserDeviceKeysInfo ( id , userId , true ) ;
2020-05-15 18:40:17 +00:00
}
2020-02-04 13:41:13 +00:00
}
}
2020-06-03 10:16:01 +00:00
for ( final userId in deviceLists . left ) {
2020-02-04 13:41:13 +00:00
if ( _userDeviceKeys . containsKey ( userId ) ) {
_userDeviceKeys . remove ( userId ) ;
}
}
}
}
2020-06-04 11:39:51 +00:00
Future < void > _handleToDeviceEvents ( List < BasicEventWithSender > events ) async {
2020-03-30 09:08:38 +00:00
for ( var i = 0 ; i < events . length ; i + + ) {
2020-06-03 10:16:01 +00:00
var toDeviceEvent = ToDeviceEvent . fromJson ( events [ i ] . toJson ( ) ) ;
2020-06-04 11:39:51 +00:00
if ( toDeviceEvent . type = = EventTypes . Encrypted & & encryptionEnabled ) {
2020-02-15 07:48:41 +00:00
try {
2020-06-04 11:39:51 +00:00
toDeviceEvent = await encryption . decryptToDeviceEvent ( toDeviceEvent ) ;
2020-05-22 13:51:45 +00:00
} catch ( e , s ) {
2020-08-06 09:35:02 +00:00
Logs . error (
' [LibOlm] Could not decrypt to device event from ${ toDeviceEvent . sender } with content: ${ toDeviceEvent . content } \n ${ e . toString ( ) } ' ,
s ) ;
2020-05-22 13:51:45 +00:00
onOlmError . add (
ToDeviceEventDecryptionError (
2020-06-01 18:24:41 +00:00
exception: e is Exception ? e : Exception ( e ) ,
2020-05-22 13:51:45 +00:00
stackTrace: s ,
toDeviceEvent: toDeviceEvent ,
) ,
) ;
2020-06-03 10:16:01 +00:00
toDeviceEvent = ToDeviceEvent . fromJson ( events [ i ] . toJson ( ) ) ;
2020-02-15 07:48:41 +00:00
}
}
2020-06-04 11:39:51 +00:00
if ( encryptionEnabled ) {
await encryption . handleToDeviceEvent ( toDeviceEvent ) ;
2020-05-29 06:49:37 +00:00
}
2020-02-15 07:48:41 +00:00
onToDeviceEvent . add ( toDeviceEvent ) ;
}
}
2020-05-22 10:12:18 +00:00
Future < void > _handleRooms (
2020-07-21 07:34:30 +00:00
Map < String , SyncRoomUpdate > rooms , Membership membership ,
{ bool sortAtTheEnd = false } ) async {
2020-05-19 07:49:37 +00:00
for ( final entry in rooms . entries ) {
final id = entry . key ;
final room = entry . value ;
2020-01-02 14:09:49 +00:00
2020-06-03 10:16:01 +00:00
var update = RoomUpdate . fromSyncRoomUpdate ( room , id ) ;
2020-05-19 08:15:23 +00:00
if ( database ! = null ) {
2020-10-17 10:03:54 +00:00
// TODO: This method seems to be rather slow for some updates
// Perhaps don't dynamically build that one query?
2020-05-19 08:15:23 +00:00
await database . storeRoomUpdate ( this . id , update , getRoomById ( id ) ) ;
}
2020-01-02 14:09:49 +00:00
_updateRoomsByRoomUpdate ( update ) ;
2020-05-15 18:40:17 +00:00
final roomObj = getRoomById ( id ) ;
2020-06-03 10:16:01 +00:00
if ( update . limitedTimeline & & roomObj ! = null ) {
2020-05-15 18:40:17 +00:00
roomObj . resetSortOrder ( ) ;
}
2020-01-02 14:09:49 +00:00
onRoomUpdate . add ( update ) ;
2020-05-15 18:40:17 +00:00
var handledEvents = false ;
2020-05-16 06:42:56 +00:00
2020-01-02 14:09:49 +00:00
/// Handle now all room events and save them in the database
2020-06-03 10:16:01 +00:00
if ( room is JoinedRoomUpdate ) {
if ( room . state ? . isNotEmpty ? ? false ) {
2020-10-17 10:03:54 +00:00
// TODO: This method seems to be comperatively slow for some updates
2020-06-03 10:16:01 +00:00
await _handleRoomEvents (
2020-10-22 10:21:20 +00:00
id ,
room . state . map ( ( i ) = > i . toJson ( ) ) . toList ( ) ,
EventUpdateType . state ) ;
2020-06-03 10:16:01 +00:00
handledEvents = true ;
}
if ( room . timeline ? . events ? . isNotEmpty ? ? false ) {
2020-07-21 07:34:30 +00:00
await _handleRoomEvents (
id ,
room . timeline . events . map ( ( i ) = > i . toJson ( ) ) . toList ( ) ,
2020-10-22 10:21:20 +00:00
sortAtTheEnd ? EventUpdateType . history : EventUpdateType . timeline ,
2020-07-21 07:34:30 +00:00
sortAtTheEnd: sortAtTheEnd ) ;
2020-06-03 10:16:01 +00:00
handledEvents = true ;
}
if ( room . ephemeral ? . isNotEmpty ? ? false ) {
2020-10-17 10:03:54 +00:00
// TODO: This method seems to be comperatively slow for some updates
2020-06-03 10:16:01 +00:00
await _handleEphemerals (
id , room . ephemeral . map ( ( i ) = > i . toJson ( ) ) . toList ( ) ) ;
}
if ( room . accountData ? . isNotEmpty ? ? false ) {
2020-10-22 10:21:20 +00:00
await _handleRoomEvents (
id ,
room . accountData . map ( ( i ) = > i . toJson ( ) ) . toList ( ) ,
EventUpdateType . accountData ) ;
2020-06-03 10:16:01 +00:00
}
2020-01-02 14:33:26 +00:00
}
2020-06-03 10:16:01 +00:00
if ( room is LeftRoomUpdate ) {
if ( room . timeline ? . events ? . isNotEmpty ? ? false ) {
2020-10-22 10:21:20 +00:00
await _handleRoomEvents (
id ,
room . timeline . events . map ( ( i ) = > i . toJson ( ) ) . toList ( ) ,
EventUpdateType . timeline ) ;
2020-06-03 10:16:01 +00:00
handledEvents = true ;
}
if ( room . accountData ? . isNotEmpty ? ? false ) {
2020-10-22 10:21:20 +00:00
await _handleRoomEvents (
id ,
room . accountData . map ( ( i ) = > i . toJson ( ) ) . toList ( ) ,
EventUpdateType . accountData ) ;
2020-06-03 10:16:01 +00:00
}
if ( room . state ? . isNotEmpty ? ? false ) {
await _handleRoomEvents (
2020-10-22 10:21:20 +00:00
id ,
room . state . map ( ( i ) = > i . toJson ( ) ) . toList ( ) ,
EventUpdateType . state ) ;
2020-06-03 10:16:01 +00:00
handledEvents = true ;
}
2020-01-02 14:33:26 +00:00
}
2020-06-03 10:16:01 +00:00
if ( room is InvitedRoomUpdate & &
( room . inviteState ? . isNotEmpty ? ? false ) ) {
2020-10-22 10:21:20 +00:00
await _handleRoomEvents (
id ,
room . inviteState . map ( ( i ) = > i . toJson ( ) ) . toList ( ) ,
EventUpdateType . inviteState ) ;
2020-05-15 18:40:17 +00:00
}
if ( handledEvents & & database ! = null & & roomObj ! = null ) {
2020-05-19 07:49:37 +00:00
await roomObj . updateSortOrder ( ) ;
2020-01-02 14:33:26 +00:00
}
2020-05-19 07:49:37 +00:00
}
2020-01-02 14:09:49 +00:00
}
2020-05-19 07:49:37 +00:00
Future < void > _handleEphemerals ( String id , List < dynamic > events ) async {
2020-01-02 14:09:49 +00:00
for ( num i = 0 ; i < events . length ; i + + ) {
2020-10-22 10:21:20 +00:00
await _handleEvent ( events [ i ] , id , EventUpdateType . ephemeral ) ;
2020-01-02 14:09:49 +00:00
// Receipt events are deltas between two states. We will create a
// fake room account data event for this and store the difference
// there.
2020-03-30 09:08:38 +00:00
if ( events [ i ] [ ' type ' ] = = ' m.receipt ' ) {
var room = getRoomById ( id ) ;
room ? ? = Room ( id: id ) ;
2020-01-02 14:09:49 +00:00
2020-03-30 09:08:38 +00:00
var receiptStateContent =
room . roomAccountData [ ' m.receipt ' ] ? . content ? ? { } ;
for ( var eventEntry in events [ i ] [ ' content ' ] . entries ) {
2020-01-02 14:09:49 +00:00
final String eventID = eventEntry . key ;
2020-03-30 09:08:38 +00:00
if ( events [ i ] [ ' content ' ] [ eventID ] [ ' m.read ' ] ! = null ) {
2020-01-02 14:09:49 +00:00
final Map < String , dynamic > userTimestampMap =
2020-03-30 09:08:38 +00:00
events [ i ] [ ' content ' ] [ eventID ] [ ' m.read ' ] ;
2020-01-02 14:09:49 +00:00
for ( var userTimestampMapEntry in userTimestampMap . entries ) {
2020-03-30 09:08:38 +00:00
final mxid = userTimestampMapEntry . key ;
2020-01-02 14:09:49 +00:00
// Remove previous receipt event from this user
2020-05-16 06:42:56 +00:00
if ( receiptStateContent [ eventID ] is Map < String , dynamic > & &
receiptStateContent [ eventID ] [ ' m.read ' ]
is Map < String , dynamic > & &
receiptStateContent [ eventID ] [ ' m.read ' ] . containsKey ( mxid ) ) {
2020-05-15 18:40:17 +00:00
receiptStateContent [ eventID ] [ ' m.read ' ] . remove ( mxid ) ;
2020-01-02 14:09:49 +00:00
}
if ( userTimestampMap [ mxid ] is Map < String , dynamic > & &
2020-03-30 09:08:38 +00:00
userTimestampMap [ mxid ] . containsKey ( ' ts ' ) ) {
2020-01-02 14:09:49 +00:00
receiptStateContent [ mxid ] = {
2020-03-30 09:08:38 +00:00
' event_id ' : eventID ,
' ts ' : userTimestampMap [ mxid ] [ ' ts ' ] ,
2020-01-02 14:09:49 +00:00
} ;
}
}
}
}
2020-03-30 09:08:38 +00:00
events [ i ] [ ' content ' ] = receiptStateContent ;
2020-10-22 10:21:20 +00:00
await _handleEvent ( events [ i ] , id , EventUpdateType . accountData ) ;
2020-01-02 14:09:49 +00:00
}
}
}
2020-05-22 10:12:18 +00:00
Future < void > _handleRoomEvents (
2020-10-22 10:21:20 +00:00
String chat_id , List < dynamic > events , EventUpdateType type ,
2020-07-21 07:34:30 +00:00
{ bool sortAtTheEnd = false } ) async {
2020-01-02 14:09:49 +00:00
for ( num i = 0 ; i < events . length ; i + + ) {
2020-07-21 07:34:30 +00:00
await _handleEvent ( events [ i ] , chat_id , type , sortAtTheEnd: sortAtTheEnd ) ;
2020-01-02 14:09:49 +00:00
}
}
2020-05-22 10:12:18 +00:00
Future < void > _handleEvent (
2020-10-22 10:21:20 +00:00
Map < String , dynamic > event , String roomID , EventUpdateType type ,
2020-07-21 07:34:30 +00:00
{ bool sortAtTheEnd = false } ) async {
2020-03-30 09:08:38 +00:00
if ( event [ ' type ' ] is String & & event [ ' content ' ] is Map < String , dynamic > ) {
2020-02-04 13:41:13 +00:00
// The client must ignore any new m.room.encryption event to prevent
// man-in-the-middle attacks!
2020-05-15 18:40:17 +00:00
final room = getRoomById ( roomID ) ;
2020-05-16 06:42:56 +00:00
if ( room = = null | |
2020-06-03 10:16:01 +00:00
( event [ ' type ' ] = = EventTypes . Encryption & &
2020-05-22 10:12:18 +00:00
room . encrypted & &
event [ ' content ' ] [ ' algorithm ' ] ! =
2020-06-03 10:16:01 +00:00
room . getState ( EventTypes . Encryption ) ? . content [ ' algorithm ' ] ) ) {
2020-02-04 13:41:13 +00:00
return ;
}
2020-05-15 18:40:17 +00:00
// ephemeral events aren't persisted and don't need a sort order - they are
// expected to be processed as soon as they come in
2020-10-22 10:21:20 +00:00
final sortOrder = type ! = EventUpdateType . ephemeral
2020-07-21 07:34:30 +00:00
? ( sortAtTheEnd ? room . oldSortOrder : room . newSortOrder )
: 0.0 ;
2020-03-30 09:08:38 +00:00
var update = EventUpdate (
eventType: event [ ' type ' ] ,
2020-01-02 14:09:49 +00:00
roomID: roomID ,
type: type ,
content: event ,
2020-05-15 18:40:17 +00:00
sortOrder: sortOrder ,
2020-01-02 14:09:49 +00:00
) ;
2020-06-04 11:39:51 +00:00
if ( event [ ' type ' ] = = EventTypes . Encrypted & & encryptionEnabled ) {
update = await update . decrypt ( room ) ;
2020-05-19 07:58:59 +00:00
}
2020-07-02 08:32:11 +00:00
if ( event [ ' type ' ] = = EventTypes . Message & &
! room . isDirectChat & &
database ! = null & &
room . getState ( EventTypes . RoomMember , event [ ' sender ' ] ) = = null ) {
// In order to correctly render room list previews we need to fetch the member from the database
final user = await database . getUser ( id , event [ ' sender ' ] , room ) ;
if ( user ! = null ) {
room . setState ( user ) ;
}
}
2020-10-22 10:21:20 +00:00
if ( type ! = EventUpdateType . ephemeral & & database ! = null ) {
2020-05-19 07:49:37 +00:00
await database . storeEventUpdate ( id , update ) ;
2020-02-15 07:48:41 +00:00
}
_updateRoomsByEventUpdate ( update ) ;
2020-06-05 20:03:28 +00:00
if ( encryptionEnabled ) {
await encryption . handleEventUpdate ( update ) ;
2020-05-27 19:35:00 +00:00
}
2020-01-02 14:09:49 +00:00
onEvent . add ( update ) ;
2020-01-04 18:36:17 +00:00
2020-08-15 14:05:11 +00:00
final rawUnencryptedEvent = update . content ;
2020-10-22 10:21:20 +00:00
if ( prevBatch ! = null & & type = = EventUpdateType . timeline ) {
2020-08-18 08:07:47 +00:00
if ( rawUnencryptedEvent [ ' type ' ] = = EventTypes . CallInvite ) {
onCallInvite
. add ( Event . fromJson ( rawUnencryptedEvent , room , sortOrder ) ) ;
} else if ( rawUnencryptedEvent [ ' type ' ] = = EventTypes . CallHangup ) {
onCallHangup
. add ( Event . fromJson ( rawUnencryptedEvent , room , sortOrder ) ) ;
} else if ( rawUnencryptedEvent [ ' type ' ] = = EventTypes . CallAnswer ) {
onCallAnswer
. add ( Event . fromJson ( rawUnencryptedEvent , room , sortOrder ) ) ;
} else if ( rawUnencryptedEvent [ ' type ' ] = = EventTypes . CallCandidates ) {
onCallCandidates
. add ( Event . fromJson ( rawUnencryptedEvent , room , sortOrder ) ) ;
}
2020-01-04 18:36:17 +00:00
}
2020-01-02 14:09:49 +00:00
}
}
void _updateRoomsByRoomUpdate ( RoomUpdate chatUpdate ) {
// Update the chat list item.
// Search the room in the rooms
num j = 0 ;
for ( j = 0 ; j < rooms . length ; j + + ) {
if ( rooms [ j ] . id = = chatUpdate . id ) break ;
}
2020-03-30 09:08:38 +00:00
final found = ( j < rooms . length & & rooms [ j ] . id = = chatUpdate . id ) ;
final isLeftRoom = chatUpdate . membership = = Membership . leave ;
2020-01-02 14:09:49 +00:00
// Does the chat already exist in the list rooms?
if ( ! found & & ! isLeftRoom ) {
2020-03-30 09:08:38 +00:00
var position = chatUpdate . membership = = Membership . invite ? 0 : j ;
2020-01-02 14:09:49 +00:00
// Add the new chat to the list
2020-03-30 09:08:38 +00:00
var newRoom = Room (
2020-01-02 14:09:49 +00:00
id: chatUpdate . id ,
membership: chatUpdate . membership ,
prev_batch: chatUpdate . prev_batch ,
highlightCount: chatUpdate . highlight_count ,
notificationCount: chatUpdate . notification_count ,
mHeroes: chatUpdate . summary ? . mHeroes ,
mJoinedMemberCount: chatUpdate . summary ? . mJoinedMemberCount ,
mInvitedMemberCount: chatUpdate . summary ? . mInvitedMemberCount ,
roomAccountData: { } ,
client: this ,
) ;
rooms . insert ( position , newRoom ) ;
}
// If the membership is "leave" then remove the item and stop here
else if ( found & & isLeftRoom ) {
rooms . removeAt ( j ) ;
}
// Update notification, highlight count and/or additional informations
else if ( found & &
chatUpdate . membership ! = Membership . leave & &
( rooms [ j ] . membership ! = chatUpdate . membership | |
rooms [ j ] . notificationCount ! = chatUpdate . notification_count | |
rooms [ j ] . highlightCount ! = chatUpdate . highlight_count | |
chatUpdate . summary ! = null ) ) {
rooms [ j ] . membership = chatUpdate . membership ;
rooms [ j ] . notificationCount = chatUpdate . notification_count ;
rooms [ j ] . highlightCount = chatUpdate . highlight_count ;
2020-01-02 14:33:26 +00:00
if ( chatUpdate . prev_batch ! = null ) {
2020-01-02 14:09:49 +00:00
rooms [ j ] . prev_batch = chatUpdate . prev_batch ;
2020-01-02 14:33:26 +00:00
}
2020-01-02 14:09:49 +00:00
if ( chatUpdate . summary ! = null ) {
2020-01-02 14:33:26 +00:00
if ( chatUpdate . summary . mHeroes ! = null ) {
2020-01-02 14:09:49 +00:00
rooms [ j ] . mHeroes = chatUpdate . summary . mHeroes ;
2020-01-02 14:33:26 +00:00
}
if ( chatUpdate . summary . mJoinedMemberCount ! = null ) {
2020-01-02 14:09:49 +00:00
rooms [ j ] . mJoinedMemberCount = chatUpdate . summary . mJoinedMemberCount ;
2020-01-02 14:33:26 +00:00
}
if ( chatUpdate . summary . mInvitedMemberCount ! = null ) {
2020-01-02 14:09:49 +00:00
rooms [ j ] . mInvitedMemberCount = chatUpdate . summary . mInvitedMemberCount ;
2020-01-02 14:33:26 +00:00
}
2020-01-02 14:09:49 +00:00
}
2020-01-04 10:29:38 +00:00
if ( rooms [ j ] . onUpdate ! = null ) rooms [ j ] . onUpdate . add ( rooms [ j ] . id ) ;
2020-01-02 14:09:49 +00:00
}
}
void _updateRoomsByEventUpdate ( EventUpdate eventUpdate ) {
2020-10-22 10:21:20 +00:00
if ( eventUpdate . type = = EventUpdateType . history ) return ;
2020-09-16 08:18:13 +00:00
final room = getRoomById ( eventUpdate . roomID ) ;
if ( room = = null ) return ;
switch ( eventUpdate . type ) {
2020-10-22 10:21:20 +00:00
case EventUpdateType . timeline:
case EventUpdateType . state:
case EventUpdateType . inviteState:
2020-09-16 08:18:13 +00:00
var stateEvent =
Event . fromJson ( eventUpdate . content , room , eventUpdate . sortOrder ) ;
var prevState = room . getState ( stateEvent . type , stateEvent . stateKey ) ;
2020-09-04 07:48:35 +00:00
if ( prevState ! = null & & prevState . sortOrder > stateEvent . sortOrder ) {
2020-09-16 08:18:13 +00:00
Logs . warning ( '''
2020-09-18 08:17:08 +00:00
A new $ { eventUpdate . type } event of the type $ { stateEvent . type } has arrived with a previews
sort order $ { stateEvent . sortOrder } than the current $ { stateEvent . type } event with a
2020-09-16 08:18:13 +00:00
sort order of $ { prevState . sortOrder } . This should never happen . . . ''' );
2020-09-04 07:48:35 +00:00
return ;
}
2020-09-16 08:18:13 +00:00
if ( stateEvent . type = = EventTypes . Redaction ) {
final String redacts = eventUpdate . content [ ' redacts ' ] ;
room . states . states . forEach (
( String key , Map < String , Event > states ) = > states . forEach (
( String key , Event state ) {
if ( state . eventId = = redacts ) {
state . setRedactionEvent ( stateEvent ) ;
}
} ,
) ,
) ;
} else {
room . setState ( stateEvent ) ;
}
break ;
2020-10-22 10:21:20 +00:00
case EventUpdateType . accountData:
2020-09-16 08:18:13 +00:00
room . roomAccountData [ eventUpdate . eventType ] =
BasicRoomEvent . fromJson ( eventUpdate . content ) ;
break ;
2020-10-22 10:21:20 +00:00
case EventUpdateType . ephemeral:
2020-09-16 08:18:13 +00:00
room . ephemerals [ eventUpdate . eventType ] =
BasicRoomEvent . fromJson ( eventUpdate . content ) ;
break ;
2020-10-22 10:21:20 +00:00
case EventUpdateType . history:
break ;
2020-01-02 14:09:49 +00:00
}
2020-09-16 08:18:13 +00:00
room . onUpdate . add ( room . id ) ;
2020-01-02 14:09:49 +00:00
}
2020-01-03 13:21:15 +00:00
bool _sortLock = false ;
2020-01-02 14:09:49 +00:00
2020-07-20 07:46:46 +00:00
/// If [true] then unread rooms are pinned at the top of the room list.
bool pinUnreadRooms ;
2020-01-03 13:21:15 +00:00
/// The compare function how the rooms should be sorted internally. By default
/// rooms are sorted by timestamp of the last m.room.message event or the last
/// event if there is no known message.
2020-07-20 07:46:46 +00:00
RoomSorter get sortRoomsBy = > ( a , b ) = > ( a . isFavourite ! = b . isFavourite )
? ( a . isFavourite ? - 1 : 1 )
: ( pinUnreadRooms & & a . notificationCount ! = b . notificationCount )
? b . notificationCount . compareTo ( a . notificationCount )
2020-06-25 07:27:01 +00:00
: b . timeCreated . millisecondsSinceEpoch
. compareTo ( a . timeCreated . millisecondsSinceEpoch ) ;
2020-01-03 13:21:15 +00:00
2020-03-30 09:08:38 +00:00
void _sortRooms ( ) {
2020-01-06 20:21:25 +00:00
if ( prevBatch = = null | | _sortLock | | rooms . length < 2 ) return ;
2020-01-03 13:21:15 +00:00
_sortLock = true ;
rooms ? . sort ( sortRoomsBy ) ;
_sortLock = false ;
2020-01-02 14:09:49 +00:00
}
2020-01-12 10:30:05 +00:00
2020-02-04 13:41:13 +00:00
/// A map of known device keys per user.
Map < String , DeviceKeysList > get userDeviceKeys = > _userDeviceKeys ;
Map < String , DeviceKeysList > _userDeviceKeys = { } ;
2020-08-17 12:25:48 +00:00
/// Gets user device keys by its curve25519 key. Returns null if it isn't found
DeviceKeys getUserDeviceKeysByCurve25519Key ( String senderKey ) {
for ( final user in userDeviceKeys . values ) {
final device = user . deviceKeys . values
. firstWhere ( ( e ) = > e . curve25519Key = = senderKey , orElse: ( ) = > null ) ;
if ( device ! = null ) {
return device ;
}
}
return null ;
}
2020-02-04 13:41:13 +00:00
Future < Set < String > > _getUserIdsInEncryptedRooms ( ) async {
2020-03-30 09:08:38 +00:00
var userIds = < String > { } ;
for ( var i = 0 ; i < rooms . length ; i + + ) {
2020-02-04 13:41:13 +00:00
if ( rooms [ i ] . encrypted ) {
2020-08-01 16:18:30 +00:00
try {
var userList = await rooms [ i ] . requestParticipants ( ) ;
for ( var user in userList ) {
if ( [ Membership . join , Membership . invite ]
. contains ( user . membership ) ) {
userIds . add ( user . id ) ;
}
2020-02-19 09:56:41 +00:00
}
2020-08-06 09:35:02 +00:00
} catch ( e , s ) {
Logs . error ( ' [E2EE] Failed to fetch participants: ' + e . toString ( ) , s ) ;
2020-02-04 13:41:13 +00:00
}
}
}
return userIds ;
}
2020-09-18 08:17:08 +00:00
final Map < String , DateTime > _keyQueryFailures = { } ;
2020-02-04 13:41:13 +00:00
Future < void > _updateUserDeviceKeys ( ) async {
2020-02-18 09:23:55 +00:00
try {
2020-03-30 09:08:38 +00:00
if ( ! isLogged ( ) ) return ;
2020-05-15 18:40:17 +00:00
final dbActions = < Future < dynamic > Function ( ) > [ ] ;
2020-03-30 09:08:38 +00:00
var trackedUserIds = await _getUserIdsInEncryptedRooms ( ) ;
trackedUserIds . add ( userID ) ;
2020-02-18 09:23:55 +00:00
// Remove all userIds we no longer need to track the devices of.
_userDeviceKeys
. removeWhere ( ( String userId , v ) = > ! trackedUserIds . contains ( userId ) ) ;
// Check if there are outdated device key lists. Add it to the set.
2020-03-30 09:08:38 +00:00
var outdatedLists = < String , dynamic > { } ;
for ( var userId in trackedUserIds ) {
2020-02-18 09:23:55 +00:00
if ( ! userDeviceKeys . containsKey ( userId ) ) {
2020-06-15 08:26:50 +00:00
_userDeviceKeys [ userId ] = DeviceKeysList ( userId , this ) ;
2020-02-18 09:23:55 +00:00
}
2020-03-30 09:08:38 +00:00
var deviceKeysList = userDeviceKeys [ userId ] ;
2020-09-18 08:17:08 +00:00
if ( deviceKeysList . outdated & &
( ! _keyQueryFailures . containsKey ( userId . domain ) | |
DateTime . now ( )
. subtract ( Duration ( minutes: 5 ) )
. isAfter ( _keyQueryFailures [ userId . domain ] ) ) ) {
2020-02-18 09:23:55 +00:00
outdatedLists [ userId ] = [ ] ;
}
2020-02-04 13:41:13 +00:00
}
2020-02-18 09:23:55 +00:00
if ( outdatedLists . isNotEmpty ) {
2020-02-19 09:24:54 +00:00
// Request the missing device key lists from the server.
2020-08-21 09:02:20 +00:00
if ( ! isLogged ( ) ) return ;
2020-08-11 16:11:51 +00:00
final response = await requestDeviceKeys ( outdatedLists , timeout: 10000 ) ;
2020-02-20 07:28:15 +00:00
2020-06-03 10:16:01 +00:00
for ( final rawDeviceKeyListEntry in response . deviceKeys . entries ) {
final userId = rawDeviceKeyListEntry . key ;
2020-06-04 15:51:49 +00:00
if ( ! userDeviceKeys . containsKey ( userId ) ) {
2020-06-15 08:26:50 +00:00
_userDeviceKeys [ userId ] = DeviceKeysList ( userId , this ) ;
2020-06-04 15:51:49 +00:00
}
2020-03-30 09:08:38 +00:00
final oldKeys =
2020-02-20 07:28:15 +00:00
Map < String , DeviceKeys > . from ( _userDeviceKeys [ userId ] . deviceKeys ) ;
2020-02-18 09:23:55 +00:00
_userDeviceKeys [ userId ] . deviceKeys = { } ;
for ( final rawDeviceKeyEntry in rawDeviceKeyListEntry . value . entries ) {
2020-06-03 10:16:01 +00:00
final deviceId = rawDeviceKeyEntry . key ;
2020-02-19 09:24:54 +00:00
// Set the new device key for this device
2020-06-06 11:47:37 +00:00
final entry =
DeviceKeys . fromMatrixDeviceKeys ( rawDeviceKeyEntry . value , this ) ;
2020-06-05 20:03:28 +00:00
if ( entry . isValid ) {
// is this a new key or the same one as an old one?
// better store an update - the signatures might have changed!
if ( ! oldKeys . containsKey ( deviceId ) | |
oldKeys [ deviceId ] . ed25519Key = = entry . ed25519Key ) {
if ( oldKeys . containsKey ( deviceId ) ) {
// be sure to save the verified status
entry . setDirectVerified ( oldKeys [ deviceId ] . directVerified ) ;
entry . blocked = oldKeys [ deviceId ] . blocked ;
entry . validSignatures = oldKeys [ deviceId ] . validSignatures ;
}
2020-05-20 07:37:32 +00:00
_userDeviceKeys [ userId ] . deviceKeys [ deviceId ] = entry ;
if ( deviceId = = deviceID & &
2020-06-05 20:03:28 +00:00
entry . ed25519Key = = fingerprintKey ) {
2020-05-22 10:12:18 +00:00
// Always trust the own device
2020-05-22 11:18:36 +00:00
entry . setDirectVerified ( true ) ;
2020-05-20 07:37:32 +00:00
}
2020-05-21 13:20:33 +00:00
} else {
// This shouldn't ever happen. The same device ID has gotten
// a new public key. So we ignore the update. TODO: ask krille
// if we should instead use the new key with unknown verified / blocked status
2020-05-22 11:18:45 +00:00
_userDeviceKeys [ userId ] . deviceKeys [ deviceId ] =
oldKeys [ deviceId ] ;
2020-05-20 07:37:32 +00:00
}
2020-06-05 20:03:28 +00:00
}
if ( database ! = null ) {
dbActions . add ( ( ) = > database . storeUserDeviceKey (
id ,
userId ,
deviceId ,
json . encode ( entry . toJson ( ) ) ,
entry . directVerified ,
entry . blocked ,
) ) ;
2020-02-18 09:23:55 +00:00
}
2020-05-15 18:40:17 +00:00
}
2020-05-21 13:20:33 +00:00
// delete old/unused entries
2020-05-15 18:40:17 +00:00
if ( database ! = null ) {
for ( final oldDeviceKeyEntry in oldKeys . entries ) {
final deviceId = oldDeviceKeyEntry . key ;
if ( ! _userDeviceKeys [ userId ] . deviceKeys . containsKey ( deviceId ) ) {
// we need to remove an old key
2020-05-16 06:42:56 +00:00
dbActions . add (
( ) = > database . removeUserDeviceKey ( id , userId , deviceId ) ) ;
2020-05-15 18:40:17 +00:00
}
}
2020-02-15 12:33:03 +00:00
}
2020-02-18 09:23:55 +00:00
_userDeviceKeys [ userId ] . outdated = false ;
2020-05-15 18:40:17 +00:00
if ( database ! = null ) {
2020-05-16 06:42:56 +00:00
dbActions
. add ( ( ) = > database . storeUserDeviceKeysInfo ( id , userId , false ) ) ;
2020-05-15 18:40:17 +00:00
}
2020-02-04 13:41:13 +00:00
}
2020-05-21 13:20:33 +00:00
// next we parse and persist the cross signing keys
2020-06-05 20:03:28 +00:00
final crossSigningTypes = {
' master ' : response . masterKeys ,
' self_signing ' : response . selfSigningKeys ,
' user_signing ' : response . userSigningKeys ,
} ;
for ( final crossSigningKeysEntry in crossSigningTypes . entries ) {
final keyType = crossSigningKeysEntry . key ;
final keys = crossSigningKeysEntry . value ;
if ( keys = = null ) {
2020-05-21 13:20:33 +00:00
continue ;
}
2020-06-05 20:03:28 +00:00
for ( final crossSigningKeyListEntry in keys . entries ) {
final userId = crossSigningKeyListEntry . key ;
if ( ! userDeviceKeys . containsKey ( userId ) ) {
2020-06-15 08:26:50 +00:00
_userDeviceKeys [ userId ] = DeviceKeysList ( userId , this ) ;
2020-06-05 20:03:28 +00:00
}
2020-06-06 11:47:37 +00:00
final oldKeys = Map < String , CrossSigningKey > . from (
_userDeviceKeys [ userId ] . crossSigningKeys ) ;
2020-05-21 13:20:33 +00:00
_userDeviceKeys [ userId ] . crossSigningKeys = { } ;
2020-06-05 20:03:28 +00:00
// add the types we aren't handling atm back
2020-05-21 13:20:33 +00:00
for ( final oldEntry in oldKeys . entries ) {
2020-06-05 20:03:28 +00:00
if ( ! oldEntry . value . usage . contains ( keyType ) ) {
2020-05-22 11:18:45 +00:00
_userDeviceKeys [ userId ] . crossSigningKeys [ oldEntry . key ] =
oldEntry . value ;
2020-05-21 13:20:33 +00:00
}
}
2020-06-06 11:47:37 +00:00
final entry = CrossSigningKey . fromMatrixCrossSigningKey (
crossSigningKeyListEntry . value , this ) ;
2020-05-21 13:20:33 +00:00
if ( entry . isValid ) {
final publicKey = entry . publicKey ;
2020-05-22 11:18:45 +00:00
if ( ! oldKeys . containsKey ( publicKey ) | |
oldKeys [ publicKey ] . ed25519Key = = entry . ed25519Key ) {
2020-05-21 13:20:33 +00:00
if ( oldKeys . containsKey ( publicKey ) ) {
// be sure to save the verification status
2020-05-21 14:51:15 +00:00
entry . setDirectVerified ( oldKeys [ publicKey ] . directVerified ) ;
2020-05-21 13:20:33 +00:00
entry . blocked = oldKeys [ publicKey ] . blocked ;
entry . validSignatures = oldKeys [ publicKey ] . validSignatures ;
}
_userDeviceKeys [ userId ] . crossSigningKeys [ publicKey ] = entry ;
} else {
// This shouldn't ever happen. The same device ID has gotten
// a new public key. So we ignore the update. TODO: ask krille
// if we should instead use the new key with unknown verified / blocked status
2020-05-22 11:18:45 +00:00
_userDeviceKeys [ userId ] . crossSigningKeys [ publicKey ] =
oldKeys [ publicKey ] ;
2020-05-21 13:20:33 +00:00
}
if ( database ! = null ) {
dbActions . add ( ( ) = > database . storeUserCrossSigningKey (
2020-05-22 11:18:45 +00:00
id ,
userId ,
publicKey ,
json . encode ( entry . toJson ( ) ) ,
entry . directVerified ,
entry . blocked ,
) ) ;
2020-05-21 13:20:33 +00:00
}
}
_userDeviceKeys [ userId ] . outdated = false ;
if ( database ! = null ) {
2020-05-22 11:18:45 +00:00
dbActions . add (
( ) = > database . storeUserDeviceKeysInfo ( id , userId , false ) ) ;
2020-05-21 13:20:33 +00:00
}
}
}
2020-09-18 08:17:08 +00:00
// now process all the failures
if ( response . failures ! = null ) {
for ( final failureDomain in response . failures . keys ) {
_keyQueryFailures [ failureDomain ] = DateTime . now ( ) ;
}
2020-05-15 18:40:17 +00:00
}
2020-02-04 13:41:13 +00:00
}
2020-07-30 07:57:45 +00:00
if ( dbActions . isNotEmpty ) {
await database ? . transaction ( ( ) async {
for ( final f in dbActions ) {
await f ( ) ;
}
} ) ;
}
2020-08-06 09:35:02 +00:00
} catch ( e , s ) {
Logs . error (
' [LibOlm] Unable to update user device keys: ' + e . toString ( ) , s ) ;
2020-02-04 13:41:13 +00:00
}
}
2020-02-15 07:48:41 +00:00
2020-08-11 16:11:51 +00:00
/// Send an (unencrypted) to device [message] of a specific [eventType] to all
/// devices of a set of [users].
Future < void > sendToDevicesOfUserIds (
Set < String > users ,
String eventType ,
Map < String , dynamic > message , {
String messageId ,
} ) async {
// Send with send-to-device messaging
var data = < String , Map < String , Map < String , dynamic > > > { } ;
for ( var user in users ) {
data [ user ] = { } ;
data [ user ] [ ' * ' ] = message ;
2020-02-04 13:41:13 +00:00
}
2020-08-11 16:11:51 +00:00
await sendToDevice (
eventType , messageId ? ? generateUniqueTransactionId ( ) , data ) ;
return ;
2020-02-04 13:41:13 +00:00
}
2020-02-15 07:48:41 +00:00
2020-02-21 15:05:19 +00:00
/// Sends an encrypted [message] of this [type] to these [deviceKeys]. To send
/// the request to all devices of the current user, pass an empty list to [deviceKeys].
2020-08-11 16:11:51 +00:00
Future < void > sendToDeviceEncrypted (
2020-02-22 08:08:01 +00:00
List < DeviceKeys > deviceKeys ,
2020-08-11 16:11:51 +00:00
String eventType ,
2020-02-22 08:08:01 +00:00
Map < String , dynamic > message , {
2020-08-11 16:11:51 +00:00
String messageId ,
2020-05-23 15:04:27 +00:00
bool onlyVerified = false ,
2020-02-22 08:08:01 +00:00
} ) async {
2020-08-11 16:11:51 +00:00
if ( ! encryptionEnabled ) return ;
2020-05-23 15:04:27 +00:00
// Don't send this message to blocked devices, and if specified onlyVerified
// then only send it to verified devices
2020-02-21 15:05:19 +00:00
if ( deviceKeys . isNotEmpty ) {
deviceKeys . removeWhere ( ( DeviceKeys deviceKeys ) = >
2020-05-23 15:04:27 +00:00
deviceKeys . blocked | |
deviceKeys . deviceId = = deviceID | |
( onlyVerified & & ! deviceKeys . verified ) ) ;
2020-02-21 15:05:19 +00:00
if ( deviceKeys . isEmpty ) return ;
}
2020-02-15 07:48:41 +00:00
// Send with send-to-device messaging
2020-06-03 10:16:01 +00:00
var data = < String , Map < String , Map < String , dynamic > > > { } ;
2020-08-11 16:11:51 +00:00
data =
await encryption . encryptToDeviceMessage ( deviceKeys , eventType , message ) ;
eventType = EventTypes . Encrypted ;
await sendToDevice (
eventType , messageId ? ? generateUniqueTransactionId ( ) , data ) ;
2020-02-15 07:48:41 +00:00
}
2020-03-23 10:47:55 +00:00
/// Whether all push notifications are muted using the [.m.rule.master]
/// rule of the push rules: https://matrix.org/docs/spec/client_server/r0.6.0#m-rule-master
bool get allPushNotificationsMuted {
2020-03-30 09:08:38 +00:00
if ( ! accountData . containsKey ( ' m.push_rules ' ) | |
! ( accountData [ ' m.push_rules ' ] . content [ ' global ' ] is Map ) ) {
2020-03-23 10:47:55 +00:00
return false ;
}
final Map < String , dynamic > globalPushRules =
2020-03-30 09:08:38 +00:00
accountData [ ' m.push_rules ' ] . content [ ' global ' ] ;
2020-03-23 10:47:55 +00:00
if ( globalPushRules = = null ) return false ;
2020-03-30 09:08:38 +00:00
if ( globalPushRules [ ' override ' ] is List ) {
for ( var i = 0 ; i < globalPushRules [ ' override ' ] . length ; i + + ) {
if ( globalPushRules [ ' override ' ] [ i ] [ ' rule_id ' ] = = ' .m.rule.master ' ) {
return globalPushRules [ ' override ' ] [ i ] [ ' enabled ' ] ;
2020-03-23 10:47:55 +00:00
}
}
}
return false ;
}
Future < void > setMuteAllPushNotifications ( bool muted ) async {
2020-08-11 16:11:51 +00:00
await enablePushRule (
2020-06-03 10:16:01 +00:00
' global ' ,
PushRuleKind . override ,
' .m.rule.master ' ,
muted ,
2020-03-23 10:47:55 +00:00
) ;
return ;
}
2020-04-28 14:23:01 +00:00
/// Changes the password. You should either set oldPasswort or another authentication flow.
2020-08-11 16:11:51 +00:00
@ override
2020-04-28 14:23:01 +00:00
Future < void > changePassword ( String newPassword ,
{ String oldPassword , Map < String , dynamic > auth } ) async {
try {
2020-06-03 10:16:01 +00:00
if ( oldPassword ! = null ) {
auth = {
' type ' : ' m.login.password ' ,
' user ' : userID ,
' password ' : oldPassword ,
} ;
}
2020-08-11 16:11:51 +00:00
await super . changePassword ( newPassword , auth: auth ) ;
2020-04-28 14:23:01 +00:00
} on MatrixException catch ( matrixException ) {
if ( ! matrixException . requireAdditionalAuthentication ) {
rethrow ;
}
if ( matrixException . authenticationFlows . length ! = 1 | |
! matrixException . authenticationFlows . first . stages
. contains ( ' m.login.password ' ) ) {
rethrow ;
}
if ( oldPassword = = null ) {
rethrow ;
}
return changePassword (
newPassword ,
auth: {
' type ' : ' m.login.password ' ,
' user ' : userID ,
' identifier ' : { ' type ' : ' m.id.user ' , ' user ' : userID } ,
' password ' : oldPassword ,
' session ' : matrixException . session ,
} ,
) ;
} catch ( _ ) {
rethrow ;
}
}
2020-05-18 14:01:14 +00:00
2020-09-19 10:39:19 +00:00
/// Clear all local cached messages and perform a new clean sync.
Future < void > clearLocalCachedMessages ( ) async {
prevBatch = null ;
rooms . forEach ( ( r ) = > r . prev_batch = null ) ;
await database ? . clearCache ( id ) ;
}
/// A list of mxids of users who are ignored.
2020-09-19 13:05:43 +00:00
List < String > get ignoredUsers = > ( accountData
. containsKey ( ' m.ignored_user_list ' ) & &
2020-09-20 08:35:25 +00:00
accountData [ ' m.ignored_user_list ' ] . content [ ' ignored_users ' ] is Map )
2020-09-19 13:05:43 +00:00
? List < String > . from (
2020-09-20 08:35:25 +00:00
accountData [ ' m.ignored_user_list ' ] . content [ ' ignored_users ' ] . keys )
2020-09-19 13:05:43 +00:00
: [ ] ;
2020-09-19 10:39:19 +00:00
/// Ignore another user. This will clear the local cached messages to
/// hide all previous messages from this user.
Future < void > ignoreUser ( String userId ) async {
if ( ! userId . isValidMatrixId ) {
throw Exception ( ' $ userId is not a valid mxid! ' ) ;
}
2020-09-20 08:35:25 +00:00
await setAccountData ( userID , ' m.ignored_user_list ' , {
' ignored_users ' : Map . fromEntries (
( ignoredUsers . . add ( userId ) ) . map ( ( key ) = > MapEntry ( key , { } ) ) ) ,
} ) ;
2020-09-19 10:39:19 +00:00
await clearLocalCachedMessages ( ) ;
return ;
}
/// Unignore a user. This will clear the local cached messages and request
/// them again from the server to avoid gaps in the timeline.
Future < void > unignoreUser ( String userId ) async {
if ( ! userId . isValidMatrixId ) {
throw Exception ( ' $ userId is not a valid mxid! ' ) ;
}
if ( ! ignoredUsers . contains ( userId ) ) {
throw Exception ( ' $ userId is not in the ignore list! ' ) ;
}
2020-09-20 08:35:25 +00:00
await setAccountData ( userID , ' m.ignored_user_list ' , {
' ignored_users ' : Map . fromEntries (
( ignoredUsers . . remove ( userId ) ) . map ( ( key ) = > MapEntry ( key , { } ) ) ) ,
} ) ;
2020-09-19 10:39:19 +00:00
await clearLocalCachedMessages ( ) ;
return ;
}
2020-05-18 14:01:14 +00:00
bool _disposed = false ;
2020-08-21 15:20:26 +00:00
Future _currentTransaction = Future . sync ( ( ) = > { } ) ;
2020-05-18 14:01:14 +00:00
/// Stops the synchronization and closes the database. After this
/// you can safely make this Client instance null.
2020-05-19 08:05:17 +00:00
Future < void > dispose ( { bool closeDatabase = false } ) async {
2020-05-18 14:01:14 +00:00
_disposed = true ;
2020-08-21 15:20:26 +00:00
try {
await _currentTransaction ;
} catch ( _ ) {
// No-OP
}
2020-08-17 12:25:48 +00:00
encryption ? . dispose ( ) ;
encryption = null ;
2020-10-02 09:25:31 +00:00
try {
if ( closeDatabase ) await database ? . close ( ) ;
} catch ( error , stacktrace ) {
Logs . warning ( ' Failed to close database: ' + error . toString ( ) , stacktrace ) ;
}
2020-05-18 14:01:14 +00:00
database = null ;
return ;
}
2019-06-09 10:16:48 +00:00
}
2020-06-01 18:24:41 +00:00
2020-08-26 07:38:14 +00:00
class SdkError {
2020-06-01 18:24:41 +00:00
Exception exception ;
StackTrace stackTrace ;
2020-08-26 07:38:14 +00:00
SdkError ( { this . exception , this . stackTrace } ) ;
2020-06-01 18:24:41 +00:00
}