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-02 14:09:49 +00:00
import ' dart:convert ' ;
2020-03-16 10:38:03 +00:00
import ' dart:typed_data ' ;
2020-01-02 14:09:49 +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/utils/receipt.dart ' ;
2020-03-16 10:38:03 +00:00
import ' package:http/http.dart ' as http ;
import ' package:matrix_file_e2ee/matrix_file_e2ee.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 ' ./room.dart ' ;
2020-05-06 10:13:30 +00:00
import ' utils/matrix_localizations.dart ' ;
2020-05-15 18:40:17 +00:00
import ' ./database/database.dart ' show DbRoomState , DbEvent ;
2019-07-11 20:17:40 +00:00
2020-01-04 09:31:27 +00:00
/// All data exchanged over Matrix is expressed as an "event". Typically each client action (e.g. sending a message) correlates with exactly one event.
2020-01-02 14:09:49 +00:00
class Event {
/// The Matrix ID for this event in the format '$localpart:server.abc'. Please not
/// that account data, presence and other events may not have an eventId.
final String eventId ;
/// The json payload of the content. The content highly depends on the type.
Map < String , dynamic > content ;
/// The type String of this event. For example 'm.room.message'.
final String typeKey ;
/// The ID of the room this event belongs to.
final String roomId ;
/// The user who has sent this event if it is not a global account data event.
final String senderId ;
2020-03-30 09:08:38 +00:00
User get sender = > room . getUserByMXIDSync ( senderId ? ? ' @unknown ' ) ;
2020-01-02 14:09:49 +00:00
/// The time this event has received at the server. May be null for events like
/// account data.
final DateTime time ;
/// Optional additional content for this event.
Map < String , dynamic > unsigned ;
/// The room this event belongs to. May be null.
final Room room ;
/// Optional. The previous content for this state.
/// This will be present only for state events appearing in the timeline.
/// If this is not a state event, or there is no previous content, this key will be null.
Map < String , dynamic > prevContent ;
/// Optional. This key will only be present for state events. A unique key which defines
/// the overwriting semantics for this piece of room state.
final String stateKey ;
2019-06-11 09:23:57 +00:00
/// The status of this event.
/// -1=ERROR
/// 0=SENDING
/// 1=SENT
2020-01-02 14:09:49 +00:00
/// 2=TIMELINE
/// 3=ROOM_STATE
2019-06-11 09:23:57 +00:00
int status ;
2019-08-29 07:12:55 +00:00
static const int defaultStatus = 2 ;
2020-01-02 14:09:49 +00:00
static const Map < String , int > STATUS_TYPE = {
2020-03-30 09:08:38 +00:00
' ERROR ' : - 1 ,
' SENDING ' : 0 ,
' SENT ' : 1 ,
' TIMELINE ' : 2 ,
' ROOM_STATE ' : 3 ,
2020-01-02 14:09:49 +00:00
} ;
/// Optional. The event that redacted this event, if any. Otherwise null.
Event get redactedBecause = >
2020-03-30 09:08:38 +00:00
unsigned ! = null & & unsigned . containsKey ( ' redacted_because ' )
? Event . fromJson ( unsigned [ ' redacted_because ' ] , room )
2020-01-02 14:09:49 +00:00
: null ;
bool get redacted = > redactedBecause ! = null ;
User get stateKeyUser = > room . getUserByMXIDSync ( stateKey ) ;
2019-08-29 07:12:55 +00:00
2020-05-15 18:40:17 +00:00
double sortOrder ;
2019-06-21 07:46:53 +00:00
Event (
2019-08-29 07:12:55 +00:00
{ this . status = defaultStatus ,
2020-01-02 14:09:49 +00:00
this . content ,
this . typeKey ,
this . eventId ,
this . roomId ,
this . senderId ,
this . time ,
this . unsigned ,
this . prevContent ,
this . stateKey ,
2020-05-15 18:40:17 +00:00
this . room ,
this . sortOrder = 0.0 } ) ;
2020-01-02 14:09:49 +00:00
static Map < String , dynamic > getMapFromPayload ( dynamic payload ) {
2020-01-02 14:33:26 +00:00
if ( payload is String ) {
2020-01-02 14:09:49 +00:00
try {
return json . decode ( payload ) ;
} catch ( e ) {
return { } ;
}
2020-01-02 14:33:26 +00:00
}
2020-01-02 14:09:49 +00:00
if ( payload is Map < String , dynamic > ) return payload ;
return { } ;
}
2019-08-07 07:52:36 +00:00
/// Get a State event from a table row or from the event stream.
2020-05-16 07:47:19 +00:00
factory Event . fromJson ( Map < String , dynamic > jsonPayload , Room room ,
[ double sortOrder ] ) {
2020-03-30 09:08:38 +00:00
final content = Event . getMapFromPayload ( jsonPayload [ ' content ' ] ) ;
final unsigned = Event . getMapFromPayload ( jsonPayload [ ' unsigned ' ] ) ;
final prevContent = Event . getMapFromPayload ( jsonPayload [ ' prev_content ' ] ) ;
2019-08-07 07:52:36 +00:00
return Event (
2020-01-02 14:09:49 +00:00
status: jsonPayload [ ' status ' ] ? ? defaultStatus ,
stateKey: jsonPayload [ ' state_key ' ] ,
prevContent: prevContent ,
content: content ,
typeKey: jsonPayload [ ' type ' ] ,
eventId: jsonPayload [ ' event_id ' ] ,
roomId: jsonPayload [ ' room_id ' ] ,
senderId: jsonPayload [ ' sender ' ] ,
time: jsonPayload . containsKey ( ' origin_server_ts ' )
? DateTime . fromMillisecondsSinceEpoch ( jsonPayload [ ' origin_server_ts ' ] )
: DateTime . now ( ) ,
unsigned: unsigned ,
room: room ,
2020-05-15 18:40:17 +00:00
sortOrder: sortOrder ? ? 0.0 ,
) ;
}
/// Get an event from either DbRoomState or DbEvent
factory Event . fromDb ( dynamic dbEntry , Room room ) {
if ( ! ( dbEntry is DbRoomState | | dbEntry is DbEvent ) ) {
2020-05-16 07:47:19 +00:00
throw ( ' Unknown db type ' ) ;
2020-05-15 18:40:17 +00:00
}
final content = Event . getMapFromPayload ( dbEntry . content ) ;
final unsigned = Event . getMapFromPayload ( dbEntry . unsigned ) ;
final prevContent = Event . getMapFromPayload ( dbEntry . prevContent ) ;
return Event (
status: ( dbEntry is DbEvent ? dbEntry . status : null ) ? ? defaultStatus ,
stateKey: dbEntry . stateKey ,
prevContent: prevContent ,
content: content ,
typeKey: dbEntry . type ,
eventId: dbEntry . eventId ,
roomId: dbEntry . roomId ,
senderId: dbEntry . sender ,
time: dbEntry . originServerTs ? ? DateTime . now ( ) ,
unsigned: unsigned ,
room: room ,
sortOrder: dbEntry . sortOrder ? ? 0.0 ,
2020-01-02 14:09:49 +00:00
) ;
}
Map < String , dynamic > toJson ( ) {
2020-03-30 09:08:38 +00:00
final data = < String , dynamic > { } ;
if ( stateKey ! = null ) data [ ' state_key ' ] = stateKey ;
if ( prevContent ! = null & & prevContent . isNotEmpty ) {
data [ ' prev_content ' ] = prevContent ;
2020-01-02 14:33:26 +00:00
}
2020-03-30 09:08:38 +00:00
data [ ' content ' ] = content ;
data [ ' type ' ] = typeKey ;
data [ ' event_id ' ] = eventId ;
data [ ' room_id ' ] = roomId ;
data [ ' sender ' ] = senderId ;
data [ ' origin_server_ts ' ] = time . millisecondsSinceEpoch ;
if ( unsigned ! = null & & unsigned . isNotEmpty ) {
data [ ' unsigned ' ] = unsigned ;
2020-01-02 14:33:26 +00:00
}
2020-01-02 14:09:49 +00:00
return data ;
}
/// The unique key of this event. For events with a [stateKey], it will be the
/// stateKey. Otherwise it will be the [type] as a string.
@ deprecated
String get key = > stateKey = = null | | stateKey . isEmpty ? typeKey : stateKey ;
User get asUser = > User . fromState (
stateKey: stateKey ,
prevContent: prevContent ,
content: content ,
typeKey: typeKey ,
eventId: eventId ,
roomId: roomId ,
senderId: senderId ,
time: time ,
unsigned: unsigned ,
room: room ) ;
/// Get the real type.
EventTypes get type {
switch ( typeKey ) {
2020-03-30 09:08:38 +00:00
case ' m.room.avatar ' :
2020-01-02 14:09:49 +00:00
return EventTypes . RoomAvatar ;
2020-03-30 09:08:38 +00:00
case ' m.room.name ' :
2020-01-02 14:09:49 +00:00
return EventTypes . RoomName ;
2020-03-30 09:08:38 +00:00
case ' m.room.topic ' :
2020-01-02 14:09:49 +00:00
return EventTypes . RoomTopic ;
2020-03-30 09:08:38 +00:00
case ' m.room.aliases ' :
2020-01-02 14:09:49 +00:00
return EventTypes . RoomAliases ;
2020-03-30 09:08:38 +00:00
case ' m.room.canonical_alias ' :
2020-01-02 14:09:49 +00:00
return EventTypes . RoomCanonicalAlias ;
2020-03-30 09:08:38 +00:00
case ' m.room.create ' :
2020-01-02 14:09:49 +00:00
return EventTypes . RoomCreate ;
2020-03-30 09:08:38 +00:00
case ' m.room.redaction ' :
2020-01-02 14:09:49 +00:00
return EventTypes . Redaction ;
2020-03-30 09:08:38 +00:00
case ' m.room.join_rules ' :
2020-01-02 14:09:49 +00:00
return EventTypes . RoomJoinRules ;
2020-03-30 09:08:38 +00:00
case ' m.room.member ' :
2020-01-02 14:09:49 +00:00
return EventTypes . RoomMember ;
2020-03-30 09:08:38 +00:00
case ' m.room.power_levels ' :
2020-01-02 14:09:49 +00:00
return EventTypes . RoomPowerLevels ;
2020-03-30 09:08:38 +00:00
case ' m.room.guest_access ' :
2020-01-02 14:09:49 +00:00
return EventTypes . GuestAccess ;
2020-03-30 09:08:38 +00:00
case ' m.room.history_visibility ' :
2020-01-02 14:09:49 +00:00
return EventTypes . HistoryVisibility ;
2020-03-30 09:08:38 +00:00
case ' m.sticker ' :
2020-01-04 09:31:27 +00:00
return EventTypes . Sticker ;
2020-03-30 09:08:38 +00:00
case ' m.room.message ' :
2020-01-04 09:31:27 +00:00
return EventTypes . Message ;
2020-03-30 09:08:38 +00:00
case ' m.room.encrypted ' :
2020-01-04 18:36:17 +00:00
return EventTypes . Encrypted ;
2020-03-30 09:08:38 +00:00
case ' m.room.encryption ' :
2020-01-04 18:36:17 +00:00
return EventTypes . Encryption ;
2020-05-16 07:47:19 +00:00
case ' m.room.tombsone ' :
return EventTypes . RoomTombstone ;
2020-03-30 09:08:38 +00:00
case ' m.call.invite ' :
2020-01-04 18:36:17 +00:00
return EventTypes . CallInvite ;
2020-03-30 09:08:38 +00:00
case ' m.call.answer ' :
2020-01-04 18:36:17 +00:00
return EventTypes . CallAnswer ;
2020-03-30 09:08:38 +00:00
case ' m.call.candidates ' :
2020-01-04 18:36:17 +00:00
return EventTypes . CallCandidates ;
2020-03-30 09:08:38 +00:00
case ' m.call.hangup ' :
2020-01-04 18:36:17 +00:00
return EventTypes . CallHangup ;
2020-01-02 14:09:49 +00:00
}
return EventTypes . Unknown ;
}
2020-01-04 09:31:27 +00:00
///
MessageTypes get messageType {
2020-03-30 09:08:38 +00:00
switch ( content [ ' msgtype ' ] ? ? ' m.text ' ) {
case ' m.text ' :
if ( content . containsKey ( ' m.relates_to ' ) ) {
2020-01-04 09:31:27 +00:00
return MessageTypes . Reply ;
}
return MessageTypes . Text ;
2020-03-30 09:08:38 +00:00
case ' m.notice ' :
2020-01-04 09:31:27 +00:00
return MessageTypes . Notice ;
2020-03-30 09:08:38 +00:00
case ' m.emote ' :
2020-01-04 09:31:27 +00:00
return MessageTypes . Emote ;
2020-03-30 09:08:38 +00:00
case ' m.image ' :
2020-01-04 09:31:27 +00:00
return MessageTypes . Image ;
2020-03-30 09:08:38 +00:00
case ' m.video ' :
2020-01-04 09:31:27 +00:00
return MessageTypes . Video ;
2020-03-30 09:08:38 +00:00
case ' m.audio ' :
2020-01-04 09:31:27 +00:00
return MessageTypes . Audio ;
2020-03-30 09:08:38 +00:00
case ' m.file ' :
2020-01-04 09:31:27 +00:00
return MessageTypes . File ;
2020-03-30 09:08:38 +00:00
case ' m.sticker ' :
2020-01-04 09:31:27 +00:00
return MessageTypes . Sticker ;
2020-03-30 09:08:38 +00:00
case ' m.location ' :
2020-01-04 09:31:27 +00:00
return MessageTypes . Location ;
2020-03-30 09:08:38 +00:00
case ' m.bad.encrypted ' :
2020-02-18 07:02:17 +00:00
return MessageTypes . BadEncrypted ;
2020-01-04 09:31:27 +00:00
default :
if ( type = = EventTypes . Message ) {
return MessageTypes . Text ;
}
return MessageTypes . None ;
}
}
2020-01-02 14:09:49 +00:00
void setRedactionEvent ( Event redactedBecause ) {
unsigned = {
2020-03-30 09:08:38 +00:00
' redacted_because ' : redactedBecause . toJson ( ) ,
2020-01-02 14:09:49 +00:00
} ;
prevContent = null ;
2020-03-30 09:08:38 +00:00
var contentKeyWhiteList = < String > [ ] ;
2020-01-02 14:09:49 +00:00
switch ( type ) {
case EventTypes . RoomMember:
2020-03-30 09:08:38 +00:00
contentKeyWhiteList . add ( ' membership ' ) ;
2020-01-02 14:09:49 +00:00
break ;
case EventTypes . RoomCreate:
2020-03-30 09:08:38 +00:00
contentKeyWhiteList . add ( ' creator ' ) ;
2020-01-02 14:09:49 +00:00
break ;
case EventTypes . RoomJoinRules:
2020-03-30 09:08:38 +00:00
contentKeyWhiteList . add ( ' join_rule ' ) ;
2020-01-02 14:09:49 +00:00
break ;
case EventTypes . RoomPowerLevels:
2020-03-30 09:08:38 +00:00
contentKeyWhiteList . add ( ' ban ' ) ;
contentKeyWhiteList . add ( ' events ' ) ;
contentKeyWhiteList . add ( ' events_default ' ) ;
contentKeyWhiteList . add ( ' kick ' ) ;
contentKeyWhiteList . add ( ' redact ' ) ;
contentKeyWhiteList . add ( ' state_default ' ) ;
contentKeyWhiteList . add ( ' users ' ) ;
contentKeyWhiteList . add ( ' users_default ' ) ;
2020-01-02 14:09:49 +00:00
break ;
case EventTypes . RoomAliases:
2020-03-30 09:08:38 +00:00
contentKeyWhiteList . add ( ' aliases ' ) ;
2020-01-02 14:09:49 +00:00
break ;
case EventTypes . HistoryVisibility:
2020-03-30 09:08:38 +00:00
contentKeyWhiteList . add ( ' history_visibility ' ) ;
2020-01-02 14:09:49 +00:00
break ;
default :
break ;
}
2020-03-30 09:08:38 +00:00
var toRemoveList = < String > [ ] ;
2020-01-02 14:09:49 +00:00
for ( var entry in content . entries ) {
2020-01-02 14:33:26 +00:00
if ( ! contentKeyWhiteList . contains ( entry . key ) ) {
2020-01-02 14:09:49 +00:00
toRemoveList . add ( entry . key ) ;
}
}
toRemoveList . forEach ( ( s ) = > content . remove ( s ) ) ;
2019-08-07 07:52:36 +00:00
}
2019-06-09 10:16:48 +00:00
2019-06-11 09:23:57 +00:00
/// Returns the body of this event if it has a body.
2020-03-30 09:08:38 +00:00
String get text = > content [ ' body ' ] ? ? ' ' ;
2019-06-11 09:23:57 +00:00
/// Returns the formatted boy of this event if it has a formatted body.
2020-03-30 09:08:38 +00:00
String get formattedText = > content [ ' formatted_body ' ] ? ? ' ' ;
2019-06-11 09:23:57 +00:00
2020-03-30 09:08:38 +00:00
@ Deprecated ( ' Use [body] instead. ' )
2020-01-14 11:27:26 +00:00
String getBody ( ) = > body ;
2019-06-11 09:23:57 +00:00
/// Use this to get the body.
2020-01-14 11:27:26 +00:00
String get body {
2020-03-30 09:08:38 +00:00
if ( redacted ) return ' Redacted ' ;
if ( text ! = ' ' ) return text ;
if ( formattedText ! = ' ' ) return formattedText ;
return ' $ type ' ;
2019-06-09 10:16:48 +00:00
}
2019-10-20 09:44:14 +00:00
/// Returns a list of [Receipt] instances for this event.
List < Receipt > get receipts {
2020-03-30 09:08:38 +00:00
if ( ! ( room . roomAccountData . containsKey ( ' m.receipt ' ) ) ) return [ ] ;
var receiptsList = < Receipt > [ ] ;
for ( var entry in room . roomAccountData [ ' m.receipt ' ] . content . entries ) {
if ( entry . value [ ' event_id ' ] = = eventId ) {
2020-01-02 14:09:49 +00:00
receiptsList . add ( Receipt ( room . getUserByMXIDSync ( entry . key ) ,
2020-03-30 09:08:38 +00:00
DateTime . fromMillisecondsSinceEpoch ( entry . value [ ' ts ' ] ) ) ) ;
2020-01-02 14:33:26 +00:00
}
2019-10-20 09:44:14 +00:00
}
return receiptsList ;
}
2019-06-27 08:12:39 +00:00
/// Removes this event if the status is < 1. This event will just be removed
2019-07-24 08:13:02 +00:00
/// from the database and the timelines. Returns false if not removed.
2019-07-24 08:48:13 +00:00
Future < bool > remove ( ) async {
2019-06-27 08:12:39 +00:00
if ( status < 1 ) {
2020-05-15 18:40:17 +00:00
await room . client . database ? . removeEvent ( room . client . id , eventId , room . id ) ;
2019-06-27 08:33:43 +00:00
2020-01-02 14:09:49 +00:00
room . client . onEvent . add ( EventUpdate (
2019-06-27 08:12:39 +00:00
roomID: room . id ,
2020-03-30 09:08:38 +00:00
type: ' timeline ' ,
2019-08-07 07:52:36 +00:00
eventType: typeKey ,
2019-06-27 08:20:47 +00:00
content: {
2020-03-30 09:08:38 +00:00
' event_id ' : eventId ,
' status ' : - 2 ,
' content ' : { ' body ' : ' Removed... ' }
2020-05-15 18:40:17 +00:00
} ,
sortOrder: sortOrder ) ) ;
2019-07-24 08:13:02 +00:00
return true ;
2019-06-27 08:12:39 +00:00
}
2019-07-24 08:13:02 +00:00
return false ;
2019-06-27 08:12:39 +00:00
}
/// Try to send this event again. Only works with events of status -1.
2019-06-28 08:38:21 +00:00
Future < String > sendAgain ( { String txid } ) async {
if ( status ! = - 1 ) return null ;
2020-01-02 14:33:26 +00:00
await remove ( ) ;
2020-03-30 09:08:38 +00:00
final eventID = await room . sendTextEvent ( text , txid: txid ) ;
2019-06-28 08:38:21 +00:00
return eventID ;
2019-06-27 08:12:39 +00:00
}
2019-11-26 06:38:44 +00:00
/// Whether the client is allowed to redact this event.
bool get canRedact = > senderId = = room . client . userID | | room . canRedact ;
2019-12-12 12:19:18 +00:00
/// Redacts this event. Returns [ErrorResponse] on error.
Future < dynamic > redact ( { String reason , String txid } ) = >
room . redactEvent ( eventId , reason: reason , txid: txid ) ;
2020-02-11 11:06:54 +00:00
/// Whether this event is in reply to another event.
bool get isReply = >
content [ ' m.relates_to ' ] is Map < String , dynamic > & &
content [ ' m.relates_to ' ] [ ' m.in_reply_to ' ] is Map < String , dynamic > & &
content [ ' m.relates_to ' ] [ ' m.in_reply_to ' ] [ ' event_id ' ] is String & &
( content [ ' m.relates_to ' ] [ ' m.in_reply_to ' ] [ ' event_id ' ] as String )
2020-02-11 11:28:26 +00:00
. isNotEmpty ;
2020-02-11 11:06:54 +00:00
/// Searches for the reply event in the given timeline.
Future < Event > getReplyEvent ( Timeline timeline ) async {
if ( ! isReply ) return null ;
2020-02-18 07:02:17 +00:00
final String replyEventId =
content [ ' m.relates_to ' ] [ ' m.in_reply_to ' ] [ ' event_id ' ] ;
2020-02-11 11:06:54 +00:00
return await timeline . getEventById ( replyEventId ) ;
}
2020-02-21 08:44:05 +00:00
2020-05-17 07:54:34 +00:00
Future < void > loadSession ( ) {
2020-05-22 10:12:18 +00:00
return room . loadInboundGroupSessionKeyForEvent ( this ) ;
2020-05-17 07:54:34 +00:00
}
2020-02-21 08:44:05 +00:00
/// Trys to decrypt this event. Returns a m.bad.encrypted event
/// if it fails and does nothing if the event was not encrypted.
Event get decrypted = > room . decryptGroupMessage ( this ) ;
2020-02-21 15:05:19 +00:00
/// If this event is encrypted and the decryption was not successful because
/// the session is unknown, this requests the session key from other devices
/// in the room. If the event is not encrypted or the decryption failed because
/// of a different error, this throws an exception.
Future < void > requestKey ( ) async {
2020-03-30 09:08:38 +00:00
if ( type ! = EventTypes . Encrypted | |
messageType ! = MessageTypes . BadEncrypted | |
content [ ' body ' ] ! = DecryptError . UNKNOWN_SESSION ) {
throw ( ' Session key not unknown ' ) ;
2020-02-21 15:05:19 +00:00
}
2020-05-19 09:34:11 +00:00
await room . requestSessionKey ( content [ ' session_id ' ] , content [ ' sender_key ' ] ) ;
2020-02-21 15:05:19 +00:00
return ;
}
2020-03-16 10:38:03 +00:00
2020-04-17 14:11:13 +00:00
bool get hasThumbnail = >
content [ ' info ' ] is Map < String , dynamic > & &
( content [ ' info ' ] . containsKey ( ' thumbnail_url ' ) | |
content [ ' info ' ] . containsKey ( ' thumbnail_file ' ) ) ;
2020-03-16 10:38:03 +00:00
/// Downloads (and decryptes if necessary) the attachment of this
/// event and returns it as a [MatrixFile]. If this event doesn't
2020-04-17 14:11:13 +00:00
/// contain an attachment, this throws an error. Set [getThumbnail] to
/// true to download the thumbnail instead.
Future < MatrixFile > downloadAndDecryptAttachment (
{ bool getThumbnail = false } ) async {
2020-03-30 09:08:38 +00:00
if ( ! [ EventTypes . Message , EventTypes . Sticker ] . contains ( type ) ) {
2020-03-16 10:38:03 +00:00
throw ( " This event has the type ' $ typeKey ' and so it can't contain an attachment. " ) ;
}
2020-04-17 14:11:13 +00:00
if ( ! getThumbnail & &
! content . containsKey ( ' url ' ) & &
! content . containsKey ( ' file ' ) ) {
2020-03-16 10:38:03 +00:00
throw ( " This event hasn't any attachment. " ) ;
}
2020-04-17 14:11:13 +00:00
if ( getThumbnail & & ! hasThumbnail ) {
throw ( " This event hasn't any thumbnail. " ) ;
}
final isEncrypted = getThumbnail
? ! content [ ' info ' ] . containsKey ( ' thumbnail_url ' )
: ! content . containsKey ( ' url ' ) ;
2020-03-16 10:38:03 +00:00
if ( isEncrypted & & ! room . client . encryptionEnabled ) {
2020-03-30 09:08:38 +00:00
throw ( ' Encryption is not enabled in your Client. ' ) ;
2020-03-16 10:38:03 +00:00
}
2020-04-17 14:11:13 +00:00
var mxContent = getThumbnail
2020-04-24 07:24:06 +00:00
? Uri . parse ( isEncrypted
2020-04-17 14:11:13 +00:00
? content [ ' info ' ] [ ' thumbnail_file ' ] [ ' url ' ]
: content [ ' info ' ] [ ' thumbnail_url ' ] )
2020-04-24 07:24:06 +00:00
: Uri . parse ( isEncrypted ? content [ ' file ' ] [ ' url ' ] : content [ ' url ' ] ) ;
2020-03-16 10:38:03 +00:00
Uint8List uint8list ;
// Is this file storeable?
2020-04-17 14:11:13 +00:00
final infoMap =
getThumbnail ? content [ ' info ' ] [ ' thumbnail_info ' ] : content [ ' info ' ] ;
2020-05-15 18:40:17 +00:00
final storeable = room . client . database ! = null & &
2020-04-17 14:11:13 +00:00
infoMap is Map < String , dynamic > & &
infoMap [ ' size ' ] is int & &
2020-05-16 07:47:19 +00:00
infoMap [ ' size ' ] < = room . client . database . maxFileSize ;
2020-03-16 10:38:03 +00:00
if ( storeable ) {
2020-05-15 18:40:17 +00:00
uint8list = await room . client . database . getFile ( mxContent . toString ( ) ) ;
2020-03-16 10:38:03 +00:00
}
// Download the file
if ( uint8list = = null ) {
uint8list =
( await http . get ( mxContent . getDownloadLink ( room . client ) ) ) . bodyBytes ;
2020-03-31 15:47:19 +00:00
if ( storeable ) {
2020-05-16 07:47:19 +00:00
await room . client . database
. storeFile ( mxContent . toString ( ) , uint8list , DateTime . now ( ) ) ;
2020-03-31 15:47:19 +00:00
}
2020-03-16 10:38:03 +00:00
}
// Decrypt the file
if ( isEncrypted ) {
2020-04-17 14:11:13 +00:00
final fileMap =
getThumbnail ? content [ ' info ' ] [ ' thumbnail_file ' ] : content [ ' file ' ] ;
if ( ! fileMap [ ' key ' ] [ ' key_ops ' ] . contains ( ' decrypt ' ) ) {
2020-03-16 10:38:03 +00:00
throw ( " Missing 'decrypt' in 'key_ops'. " ) ;
}
2020-03-30 09:08:38 +00:00
final encryptedFile = EncryptedFile ( ) ;
2020-03-16 10:38:03 +00:00
encryptedFile . data = uint8list ;
2020-04-17 14:11:13 +00:00
encryptedFile . iv = fileMap [ ' iv ' ] ;
encryptedFile . k = fileMap [ ' key ' ] [ ' k ' ] ;
encryptedFile . sha256 = fileMap [ ' hashes ' ] [ ' sha256 ' ] ;
2020-03-16 10:38:03 +00:00
uint8list = await decryptFile ( encryptedFile ) ;
}
2020-03-30 09:08:38 +00:00
return MatrixFile ( bytes: uint8list , path: ' / $ body ' ) ;
2020-03-16 10:38:03 +00:00
}
2020-05-06 10:13:30 +00:00
/// Returns a localized String representation of this event. For a
/// room list you may find [withSenderNamePrefix] useful. Set [hideReply] to
/// crop all lines starting with '>'.
String getLocalizedBody ( MatrixLocalizations i18n ,
{ bool withSenderNamePrefix = false , bool hideReply = false } ) {
if ( redacted ) {
return i18n . removedBy ( redactedBecause . sender . calcDisplayname ( ) ) ;
}
var localizedBody = body ;
final senderName = sender . calcDisplayname ( ) ;
switch ( type ) {
case EventTypes . Sticker:
localizedBody = i18n . sentASticker ( senderName ) ;
break ;
case EventTypes . Redaction:
localizedBody = i18n . redactedAnEvent ( senderName ) ;
break ;
case EventTypes . RoomAliases:
localizedBody = i18n . changedTheRoomAliases ( senderName ) ;
break ;
case EventTypes . RoomCanonicalAlias:
localizedBody = i18n . changedTheRoomInvitationLink ( senderName ) ;
break ;
case EventTypes . RoomCreate:
localizedBody = i18n . createdTheChat ( senderName ) ;
break ;
2020-05-16 07:47:19 +00:00
case EventTypes . RoomTombstone:
localizedBody = i18n . roomHasBeenUpgraded ;
break ;
2020-05-06 10:13:30 +00:00
case EventTypes . RoomJoinRules:
var joinRules = JoinRules . values . firstWhere (
( r ) = >
r . toString ( ) . replaceAll ( ' JoinRules. ' , ' ' ) = =
content [ ' join_rule ' ] ,
orElse: ( ) = > null ) ;
if ( joinRules = = null ) {
localizedBody = i18n . changedTheJoinRules ( senderName ) ;
} else {
localizedBody = i18n . changedTheJoinRulesTo (
senderName , joinRules . getLocalizedString ( i18n ) ) ;
}
break ;
case EventTypes . RoomMember:
var text = ' Failed to parse member event ' ;
final targetName = stateKeyUser . calcDisplayname ( ) ;
// Has the membership changed?
final newMembership = content [ ' membership ' ] ? ? ' ' ;
final oldMembership = unsigned [ ' prev_content ' ] is Map < String , dynamic >
? unsigned [ ' prev_content ' ] [ ' membership ' ] ? ? ' '
: ' ' ;
if ( newMembership ! = oldMembership ) {
if ( oldMembership = = ' invite ' & & newMembership = = ' join ' ) {
text = i18n . acceptedTheInvitation ( targetName ) ;
} else if ( oldMembership = = ' invite ' & & newMembership = = ' leave ' ) {
if ( stateKey = = senderId ) {
text = i18n . rejectedTheInvitation ( targetName ) ;
} else {
text = i18n . hasWithdrawnTheInvitationFor ( senderName , targetName ) ;
}
} else if ( oldMembership = = ' leave ' & & newMembership = = ' join ' ) {
text = i18n . joinedTheChat ( targetName ) ;
} else if ( oldMembership = = ' join ' & & newMembership = = ' ban ' ) {
text = i18n . kickedAndBanned ( senderName , targetName ) ;
} else if ( oldMembership = = ' join ' & &
newMembership = = ' leave ' & &
stateKey ! = senderId ) {
text = i18n . kicked ( senderName , targetName ) ;
} else if ( oldMembership = = ' join ' & &
newMembership = = ' leave ' & &
stateKey = = senderId ) {
text = i18n . userLeftTheChat ( targetName ) ;
} else if ( oldMembership = = ' invite ' & & newMembership = = ' ban ' ) {
text = i18n . bannedUser ( senderName , targetName ) ;
} else if ( oldMembership = = ' leave ' & & newMembership = = ' ban ' ) {
text = i18n . bannedUser ( senderName , targetName ) ;
} else if ( oldMembership = = ' ban ' & & newMembership = = ' leave ' ) {
text = i18n . unbannedUser ( senderName , targetName ) ;
} else if ( newMembership = = ' invite ' ) {
text = i18n . invitedUser ( senderName , targetName ) ;
} else if ( newMembership = = ' join ' ) {
text = i18n . joinedTheChat ( targetName ) ;
}
} else if ( newMembership = = ' join ' ) {
final newAvatar = content [ ' avatar_url ' ] ? ? ' ' ;
final oldAvatar = unsigned [ ' prev_content ' ] is Map < String , dynamic >
? unsigned [ ' prev_content ' ] [ ' avatar_url ' ] ? ? ' '
: ' ' ;
final newDisplayname = content [ ' displayname ' ] ? ? ' ' ;
final oldDisplayname =
unsigned [ ' prev_content ' ] is Map < String , dynamic >
? unsigned [ ' prev_content ' ] [ ' displayname ' ] ? ? ' '
: ' ' ;
// Has the user avatar changed?
if ( newAvatar ! = oldAvatar ) {
text = i18n . changedTheProfileAvatar ( targetName ) ;
}
// Has the user avatar changed?
else if ( newDisplayname ! = oldDisplayname ) {
text = i18n . changedTheDisplaynameTo ( targetName , newDisplayname ) ;
}
}
localizedBody = text ;
break ;
case EventTypes . RoomPowerLevels:
localizedBody = i18n . changedTheChatPermissions ( senderName ) ;
break ;
case EventTypes . RoomName:
localizedBody = i18n . changedTheChatNameTo ( senderName , content [ ' name ' ] ) ;
break ;
case EventTypes . RoomTopic:
localizedBody =
i18n . changedTheChatDescriptionTo ( senderName , content [ ' topic ' ] ) ;
break ;
case EventTypes . RoomAvatar:
localizedBody = i18n . changedTheChatAvatar ( senderName ) ;
break ;
case EventTypes . GuestAccess:
var guestAccess = GuestAccess . values . firstWhere (
( r ) = >
r . toString ( ) . replaceAll ( ' GuestAccess. ' , ' ' ) = =
content [ ' guest_access ' ] ,
orElse: ( ) = > null ) ;
if ( guestAccess = = null ) {
localizedBody = i18n . changedTheGuestAccessRules ( senderName ) ;
} else {
localizedBody = i18n . changedTheGuestAccessRulesTo (
senderName , guestAccess . getLocalizedString ( i18n ) ) ;
}
break ;
case EventTypes . HistoryVisibility:
var historyVisibility = HistoryVisibility . values . firstWhere (
( r ) = >
r . toString ( ) . replaceAll ( ' HistoryVisibility. ' , ' ' ) = =
content [ ' history_visibility ' ] ,
orElse: ( ) = > null ) ;
if ( historyVisibility = = null ) {
localizedBody = i18n . changedTheHistoryVisibility ( senderName ) ;
} else {
localizedBody = i18n . changedTheHistoryVisibilityTo (
senderName , historyVisibility . getLocalizedString ( i18n ) ) ;
}
break ;
case EventTypes . Encryption:
localizedBody = i18n . activatedEndToEndEncryption ( senderName ) ;
if ( ! room . client . encryptionEnabled ) {
localizedBody + = ' . ' + i18n . needPantalaimonWarning ;
}
break ;
case EventTypes . Encrypted:
case EventTypes . Message:
switch ( messageType ) {
case MessageTypes . Image:
localizedBody = i18n . sentAPicture ( senderName ) ;
break ;
case MessageTypes . File:
localizedBody = i18n . sentAFile ( senderName ) ;
break ;
case MessageTypes . Audio:
localizedBody = i18n . sentAnAudio ( senderName ) ;
break ;
case MessageTypes . Video:
localizedBody = i18n . sentAVideo ( senderName ) ;
break ;
case MessageTypes . Location:
localizedBody = i18n . sharedTheLocation ( senderName ) ;
break ;
case MessageTypes . Sticker:
localizedBody = i18n . sentASticker ( senderName ) ;
break ;
case MessageTypes . Emote:
localizedBody = ' * $ body ' ;
break ;
case MessageTypes . BadEncrypted:
String errorText ;
switch ( body ) {
case DecryptError . CHANNEL_CORRUPTED:
errorText = i18n . channelCorruptedDecryptError + ' . ' ;
break ;
case DecryptError . NOT_ENABLED:
errorText = i18n . encryptionNotEnabled + ' . ' ;
break ;
case DecryptError . UNKNOWN_ALGORITHM:
errorText = i18n . unknownEncryptionAlgorithm + ' . ' ;
break ;
case DecryptError . UNKNOWN_SESSION:
errorText = i18n . noPermission + ' . ' ;
break ;
default :
errorText = body ;
break ;
}
localizedBody = i18n . couldNotDecryptMessage ( errorText ) ;
break ;
case MessageTypes . Text:
case MessageTypes . Notice:
case MessageTypes . None:
case MessageTypes . Reply:
localizedBody = body ;
break ;
}
break ;
default :
localizedBody = i18n . unknownEvent ( typeKey ) ;
}
// Hide reply fallback
if ( hideReply ) {
localizedBody = localizedBody . replaceFirst (
RegExp ( r'^>( \*)? <[^>]+>[^\n\r]+\r?\n(> [^\n]*\r?\n)*\r?\n' ) , ' ' ) ;
}
// Add the sender name prefix
if ( withSenderNamePrefix & &
type = = EventTypes . Message & &
textOnlyMessageTypes . contains ( messageType ) ) {
final senderNameOrYou =
senderId = = room . client . userID ? i18n . you : senderName ;
localizedBody = ' $ senderNameOrYou : $ localizedBody ' ;
}
return localizedBody ;
}
static const Set < MessageTypes > textOnlyMessageTypes = {
MessageTypes . Text ,
MessageTypes . Reply ,
MessageTypes . Notice ,
MessageTypes . Emote ,
MessageTypes . None ,
} ;
2019-06-09 10:16:48 +00:00
}
2020-01-02 14:09:49 +00:00
2020-01-04 09:31:27 +00:00
enum MessageTypes {
2020-01-02 14:09:49 +00:00
Text ,
Emote ,
Notice ,
Image ,
Video ,
Audio ,
File ,
Location ,
Reply ,
2020-01-04 09:31:27 +00:00
Sticker ,
2020-02-18 07:02:17 +00:00
BadEncrypted ,
2020-01-04 09:31:27 +00:00
None ,
}
enum EventTypes {
Message ,
Sticker ,
Redaction ,
2020-01-02 14:09:49 +00:00
RoomAliases ,
RoomCanonicalAlias ,
RoomCreate ,
RoomJoinRules ,
RoomMember ,
RoomPowerLevels ,
RoomName ,
RoomTopic ,
RoomAvatar ,
2020-05-16 07:47:19 +00:00
RoomTombstone ,
2020-01-02 14:09:49 +00:00
GuestAccess ,
HistoryVisibility ,
2020-01-04 18:36:17 +00:00
Encryption ,
Encrypted ,
CallInvite ,
CallAnswer ,
CallCandidates ,
CallHangup ,
2020-01-02 14:09:49 +00:00
Unknown ,
}