Merge branch 'krille/get-event-from-push' into 'main'

feat: Get event from push notification

See merge request famedly/company/frontend/famedlysdk!1008
This commit is contained in:
Krille Fear 2022-04-14 15:08:46 +00:00
commit c5a6cc9a52
10 changed files with 453 additions and 1 deletions

View File

@ -42,6 +42,7 @@ export 'src/utils/matrix_file.dart';
export 'src/utils/matrix_id_string_extension.dart';
export 'src/utils/matrix_default_localizations.dart';
export 'src/utils/matrix_localizations.dart';
export 'src/utils/push_notification.dart';
export 'src/utils/receipt.dart';
export 'src/utils/sync_update_extension.dart';
export 'src/utils/to_device_event.dart';

View File

@ -967,6 +967,149 @@ class Client extends MatrixApi {
bool _initLock = false;
/// Fetches the corresponding Event object from a notification including a
/// full Room object with the sender User object in it. Returns null if this
/// push notification is not corresponding to an existing event.
/// The client does **not** need to be initialized first. If it is not
/// initalized, it will only fetch the necessary parts of the database. This
/// should make it possible to run this parallel to another client with the
/// same client name.
Future<Event?> getEventByPushNotification(
PushNotification notification, {
bool storeInDatabase = true,
Duration timeoutForServerRequests = const Duration(seconds: 8),
}) async {
// Get access token if necessary:
final database = _database ??= await databaseBuilder?.call(this);
if (!isLogged()) {
if (database == null) {
throw Exception(
'Can not execute getEventByPushNotification() without a database');
}
final clientInfoMap = await database.getClient(clientName);
final token = clientInfoMap?.tryGet<String>('token');
if (token == null) {
throw Exception('Client is not logged in.');
}
accessToken = token;
}
// Check if the notification contains an event at all:
final eventId = notification.eventId;
final roomId = notification.roomId;
if (eventId == null || roomId == null) return null;
// Create the room object:
final room = getRoomById(roomId) ??
await database?.getSingleRoom(this, roomId) ??
Room(
id: roomId,
client: this,
);
final roomName = notification.roomName;
final roomAlias = notification.roomAlias;
if (roomName != null) {
room.setState(Event(
eventId: 'TEMP',
stateKey: '',
type: EventTypes.RoomName,
content: {'name': roomName},
room: room,
senderId: 'UNKNOWN',
originServerTs: DateTime.now(),
));
}
if (roomAlias != null) {
room.setState(Event(
eventId: 'TEMP',
stateKey: '',
type: EventTypes.RoomCanonicalAlias,
content: {'alias': roomAlias},
room: room,
senderId: 'UNKNOWN',
originServerTs: DateTime.now(),
));
}
// Load the event from the notification or from the database or from server:
MatrixEvent? matrixEvent;
final content = notification.content;
final sender = notification.sender;
final type = notification.type;
if (content != null && sender != null && type != null) {
matrixEvent = MatrixEvent(
content: content,
senderId: sender,
type: type,
originServerTs: DateTime.now(),
eventId: eventId,
roomId: roomId,
);
}
matrixEvent ??= await database
?.getEventById(eventId, room)
.timeout(timeoutForServerRequests);
try {
matrixEvent ??= await getOneRoomEvent(roomId, eventId)
.timeout(timeoutForServerRequests);
} on MatrixException catch (_) {
// No access to the MatrixEvent. Search in /notifications
final notificationsResponse = await getNotifications();
matrixEvent ??= notificationsResponse.notifications
.firstWhereOrNull((notification) =>
notification.roomId == roomId &&
notification.event.eventId == eventId)
?.event;
}
if (matrixEvent == null) {
throw Exception('Unable to find event for this push notification!');
}
// Load the sender of this event
try {
await room
.requestUser(matrixEvent.senderId)
.timeout(timeoutForServerRequests);
} catch (e, s) {
Logs().w('Unable to request user for push helper', e, s);
final senderDisplayName = notification.senderDisplayName;
if (senderDisplayName != null && sender != null) {
room.setState(User(sender, displayName: senderDisplayName, room: room));
}
}
// Create Event object and decrypt if necessary
var event = Event.fromMatrixEvent(
matrixEvent,
room,
status: EventStatus.sent,
);
final encryption = this.encryption;
if (event.type == EventTypes.Encrypted && encryption != null) {
event = await encryption.decryptRoomEvent(roomId, event);
if (event.type == EventTypes.Encrypted && _currentSync != null) {
await _currentSync;
event = await encryption.decryptRoomEvent(roomId, event);
}
}
if (storeInDatabase) {
await database?.transaction(() async {
await database.storeEventUpdate(
EventUpdate(
roomID: roomId,
type: EventUpdateType.timeline,
content: event.toJson(),
),
this);
});
}
return event;
}
/// Sets the user credentials and starts the synchronisation.
///
/// Before you can connect you need at least an [accessToken], a [homeserver],

View File

@ -54,6 +54,9 @@ abstract class DatabaseApi {
Future<List<Room>> getRoomList(Client client);
Future<Room?> getSingleRoom(Client client, String roomId,
{bool loadImportantStates = true});
Future<Map<String, BasicEvent>> getAccountData();
/// Stores a RoomUpdate object in the database. Must be called inside of

View File

@ -458,6 +458,29 @@ class FluffyBoxDatabase extends DatabaseApi {
return OutboundGroupSession.fromJson(copyMap(raw), userId);
}
@override
Future<Room?> getSingleRoom(Client client, String roomId,
{bool loadImportantStates = true}) async {
// Get raw room from database:
final roomData = await _roomsBox.get(roomId);
if (roomData == null) return null;
final room = Room.fromJson(copyMap(roomData), client);
// Get important states:
if (loadImportantStates) {
final dbKeys = client.importantStateEvents
.map((state) => TupleKey(roomId, state).toString())
.toList();
final rawStates = await _roomStateBox.getAll(dbKeys);
for (final rawState in rawStates) {
if (rawState == null) continue;
room.setState(Event.fromJson(copyMap(rawState['']), room));
}
}
return room;
}
@override
Future<List<Room>> getRoomList(Client client) =>
runBenchmarked<List<Room>>('Get room list from store', () async {

View File

@ -515,6 +515,31 @@ class FamedlySdkHiveDatabase extends DatabaseApi {
return OutboundGroupSession.fromJson(convertToJson(raw), userId);
}
@override
Future<Room?> getSingleRoom(Client client, String roomId,
{bool loadImportantStates = true}) async {
// Get raw room from database:
final roomData = await _roomsBox.get(roomId);
if (roomData == null) return null;
final room = Room.fromJson(copyMap(roomData), client);
// Get important states:
if (loadImportantStates) {
final dbKeys = client.importantStateEvents
.map((state) => TupleKey(roomId, state).toString())
.toList();
final rawStates = await Future.wait(
dbKeys.map((key) => _roomStateBox.get(key)),
);
for (final rawState in rawStates) {
if (rawState == null) continue;
room.setState(Event.fromJson(copyMap(rawState['']), room));
}
}
return room;
}
@override
Future<List<Room>> getRoomList(Client client) =>
runBenchmarked<List<Room>>('Get room list from hive', () async {

View File

@ -0,0 +1,147 @@
import 'dart:convert';
/// Push Notification object from https://spec.matrix.org/v1.2/push-gateway-api/
class PushNotification {
final Map<String, dynamic>? content;
final PushNotificationCounts? counts;
final List<PushNotificationDevice> devices;
final String? eventId;
final String? prio;
final String? roomAlias;
final String? roomId;
final String? roomName;
final String? sender;
final String? senderDisplayName;
final String? type;
const PushNotification({
this.content,
this.counts,
required this.devices,
this.eventId,
this.prio,
this.roomAlias,
this.roomId,
this.roomName,
this.sender,
this.senderDisplayName,
this.type,
});
/// Generate a Push Notification object from JSON. It also supports a
/// <String, String> map which usually comes from Firebase Cloud Messaging.
factory PushNotification.fromJson(Map<String, dynamic> json) =>
PushNotification(
content: json['content'] is Map
? Map<String, dynamic>.from(json['content'])
: json['content'] is String
? jsonDecode(json['content'])
: null,
counts: json['counts'] is Map
? PushNotificationCounts.fromJson(json['counts'])
: json['counts'] is String
? PushNotificationCounts.fromJson(jsonDecode(json['counts']))
: null,
devices: json['devices'] is List
? (json['devices'] as List)
.map((d) => PushNotificationDevice.fromJson(d))
.toList()
: (jsonDecode(json['devices']) as List)
.map((d) => PushNotificationDevice.fromJson(d))
.toList(),
eventId: json['event_id'],
prio: json['prio'],
roomAlias: json['room_alias'],
roomId: json['room_id'],
roomName: json['room_name'],
sender: json['sender'],
senderDisplayName: json['sender_display_name'],
type: json['type'],
);
Map<String, dynamic> toJson() => {
if (content != null) 'content': content,
if (counts != null) 'counts': counts?.toJson(),
'devices': devices.map((i) => i.toJson()).toList(),
if (eventId != null) 'event_id': eventId,
if (prio != null) 'prio': prio,
if (roomAlias != null) 'room_alias': roomAlias,
if (roomId != null) 'room_id': roomId,
if (roomName != null) 'room_name': roomName,
if (sender != null) 'sender': sender,
if (senderDisplayName != null) 'sender_display_name': senderDisplayName,
if (type != null) 'type': type,
};
}
class PushNotificationCounts {
final int? missedCalls;
final int? unread;
const PushNotificationCounts({
this.missedCalls,
this.unread,
});
factory PushNotificationCounts.fromJson(Map<String, dynamic> json) =>
PushNotificationCounts(
missedCalls: json['missed_calls'],
unread: json['unread'],
);
Map<String, dynamic> toJson() => {
if (missedCalls != null) 'missed_calls': missedCalls,
if (unread != null) 'unread': unread,
};
}
class PushNotificationDevice {
final String appId;
final Map<String, dynamic>? data;
final String pushkey;
final int? pushkeyTs;
final Tweaks? tweaks;
const PushNotificationDevice({
required this.appId,
this.data,
required this.pushkey,
this.pushkeyTs,
this.tweaks,
});
factory PushNotificationDevice.fromJson(Map<String, dynamic> json) =>
PushNotificationDevice(
appId: json['app_id'],
data: json['data'] == null
? null
: Map<String, dynamic>.from(json['data']),
pushkey: json['pushkey'],
pushkeyTs: json['pushkey_ts'],
tweaks: json['tweaks'] == null ? null : Tweaks.fromJson(json['tweaks']),
);
Map<String, dynamic> toJson() => {
'app_id': appId,
if (data != null) 'data': data,
'pushkey': pushkey,
if (pushkeyTs != null) 'pushkey_ts': pushkeyTs,
if (tweaks != null) 'tweaks': tweaks?.toJson(),
};
}
class Tweaks {
final String? sound;
const Tweaks({
this.sound,
});
factory Tweaks.fromJson(Map<String, dynamic> json) => Tweaks(
sound: json['sound'],
);
Map<String, dynamic> toJson() => {
if (sound != null) 'sound': sound,
};
}

View File

@ -917,5 +917,64 @@ void main() {
await Future.delayed(Duration(milliseconds: 200));
expect(hiveClient.isLogged(), true);
});
test('getEventByPushNotification', () async {
final client = Client(
'testclient',
httpClient: FakeMatrixApi(),
databaseBuilder: getDatabase,
)
..accessToken = '1234'
..baseUri = Uri.parse('https://fakeserver.notexisting');
Event? event;
event = await client
.getEventByPushNotification(PushNotification(devices: []));
expect(event, null);
event = await client.getEventByPushNotification(
PushNotification(
devices: [],
eventId: '123',
roomId: '!localpart2:server.abc',
content: {
'msgtype': 'm.text',
'body': 'Hello world',
},
roomAlias: '#testalias:blaaa',
roomName: 'TestRoomName',
sender: '@alicyy:example.com',
senderDisplayName: 'AlicE',
type: 'm.room.message',
),
);
expect(event?.eventId, '123');
expect(event?.body, 'Hello world');
expect(event?.senderId, '@alicyy:example.com');
expect(event?.sender.calcDisplayname(), 'AlicE');
expect(event?.type, 'm.room.message');
expect(event?.messageType, 'm.text');
expect(event?.room.id, '!localpart2:server.abc');
expect(event?.room.name, 'TestRoomName');
expect(event?.room.canonicalAlias, '#testalias:blaaa');
final storedEvent =
await client.database?.getEventById('123', event!.room);
expect(storedEvent?.eventId, event?.eventId);
event = await client.getEventByPushNotification(
PushNotification(
devices: [],
eventId: '1234',
roomId: '!localpart:server.abc',
),
);
expect(event?.eventId, '143273582443PhrSn:example.org');
expect(event?.room.id, '!localpart:server.abc');
expect(event?.body, 'This is an example text message');
expect(event?.messageType, 'm.text');
expect(event?.type, 'm.room.message');
final storedEvent2 = await client.database
?.getEventById('143273582443PhrSn:example.org', event!.room);
expect(storedEvent2?.eventId, event?.eventId);
});
});
}

View File

@ -123,6 +123,11 @@ void testDatabase(
final rooms = await database.getRoomList(client);
expect(rooms.single.id, '!testroom');
});
test('getRoomList', () async {
final room =
await database.getSingleRoom(Client('testclient'), '!testroom');
expect(room?.id, '!testroom');
});
test('getRoomList', () async {
final list = await database.getRoomList(Client('testclient'));
expect(list.single.id, '!testroom');

View File

@ -103,7 +103,8 @@ class FakeMatrixApi extends MockClient {
}
} else if (method == 'GET' &&
action.contains('/client/r0/rooms/') &&
action.contains('/state/m.room.member/')) {
action.contains('/state/m.room.member/') &&
!action.endsWith('%40alicyy%3Aexample.com')) {
res = {'displayname': ''};
} else if (method == 'PUT' &&
action.contains(

View File

@ -0,0 +1,45 @@
import 'dart:convert';
import 'package:matrix/matrix.dart';
import 'package:test/test.dart';
void main() {
group('Push Notification', () {
Logs().level = Level.error;
const json = <String, dynamic>{
'content': {
'body': "I'm floating in a most peculiar way.",
'msgtype': 'm.text'
},
'counts': {'missed_calls': 1, 'unread': 2},
'devices': [
{
'app_id': 'org.matrix.matrixConsole.ios',
'data': {},
'pushkey': 'V2h5IG9uIGVhcnRoIGRpZCB5b3UgZGVjb2RlIHRoaXM/',
'pushkey_ts': 12345678,
'tweaks': {'sound': 'bing'}
}
],
'event_id': '\$3957tyerfgewrf384',
'prio': 'high',
'room_alias': '#exampleroom:matrix.org',
'room_id': '!slw48wfj34rtnrf:example.com',
'room_name': 'Mission Control',
'sender': '@exampleuser:matrix.org',
'sender_display_name': 'Major Tom',
'type': 'm.room.message'
};
test('fromJson and toJson', () async {
expect(PushNotification.fromJson(json).toJson(), json);
});
test('fromJson and toJson with String keys only', () async {
final strJson =
json.map((k, v) => MapEntry(k, v is String ? v : jsonEncode(v)));
expect(PushNotification.fromJson(strJson).toJson(), json);
});
});
}