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:
commit
c5a6cc9a52
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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],
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
Loading…
Reference in New Issue