famedlysdk/lib/src/Client.dart

1015 lines
35 KiB
Dart
Raw Normal View History

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
* along with famedlysdk. If not, see <http://www.gnu.org/licenses/>.
2019-06-09 11:57:33 +00:00
*/
2019-06-09 10:16:48 +00:00
import 'dart:async';
import 'dart:core';
2019-12-19 11:26:21 +00:00
import 'package:famedlysdk/famedlysdk.dart';
2019-08-07 10:06:28 +00:00
import 'package:famedlysdk/src/AccountData.dart';
import 'package:famedlysdk/src/Presence.dart';
2019-10-02 11:33:01 +00:00
import 'package:famedlysdk/src/StoreAPI.dart';
2019-08-07 10:06:28 +00:00
import 'package:famedlysdk/src/sync/UserUpdate.dart';
2019-10-18 11:05:07 +00:00
import 'package:famedlysdk/src/utils/MatrixFile.dart';
2020-01-02 14:33:26 +00:00
import 'package:pedantic/pedantic.dart';
2019-06-21 11:30:39 +00:00
import 'Room.dart';
2020-01-02 14:09:49 +00:00
import 'Event.dart';
import 'User.dart';
2019-11-30 09:36:30 +00:00
import 'utils/Profile.dart';
2020-01-02 14:09:49 +00:00
import 'dart:convert';
import 'package:famedlysdk/src/Room.dart';
import 'package:http/http.dart' as http;
import 'package:mime_type/mime_type.dart';
import 'sync/EventUpdate.dart';
import 'sync/RoomUpdate.dart';
import 'sync/UserUpdate.dart';
import 'utils/MatrixException.dart';
2019-06-09 10:16:48 +00:00
typedef AccountDataEventCB = void Function(AccountData accountData);
typedef PresenceCB = void Function(Presence presence);
2020-01-03 13:21:15 +00:00
typedef RoomSorter = int Function(Room a, Room b);
2020-01-02 14:09:49 +00:00
enum HTTPType { GET, POST, PUT, DELETE }
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.
class Client {
/// Handles the connection for this client.
2020-01-02 14:09:49 +00:00
@deprecated
Client get connection => this;
2019-06-09 10:16:48 +00:00
/// Optional persistent store for all data.
2019-10-02 11:33:01 +00:00
StoreAPI store;
2019-06-09 10:16:48 +00:00
2019-10-02 11:33:01 +00:00
Client(this.clientName, {this.debug = false, this.store}) {
if (this.clientName != "testclient") store = null; //Store(this);
2020-01-02 14:09:49 +00:00
this.onLoginStateChanged.stream.listen((loginState) {
2019-06-09 10:16:48 +00:00
print("LoginState: ${loginState.toString()}");
});
}
2019-06-12 11:43:14 +00:00
/// Whether debug prints should be displayed.
final bool debug;
2019-06-09 10:16:48 +00:00
/// The required name for this client.
final String clientName;
/// The homeserver this client is communicating with.
2020-01-02 14:09:49 +00:00
String get homeserver => _homeserver;
String _homeserver;
2019-06-09 10:16:48 +00:00
/// 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 is the access token for the matrix client. When it is undefined, then
/// the user needs to sign in first.
2020-01-02 14:09:49 +00:00
String get accessToken => _accessToken;
String _accessToken;
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
/// Which version of the matrix specification does this server support?
2020-01-02 14:09:49 +00:00
List<String> get matrixVersions => _matrixVersions;
List<String> _matrixVersions;
2019-06-09 10:16:48 +00:00
/// Wheither the server supports lazy load members.
2020-01-02 14:09:49 +00:00
bool get lazyLoadMembers => _lazyLoadMembers;
bool _lazyLoadMembers = false;
2019-06-09 10:16:48 +00:00
/// Returns the current login state.
bool isLogged() => accessToken != null;
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
2019-08-07 10:06:28 +00:00
/// Key/Value store of account data.
Map<String, AccountData> accountData = {};
/// Presences of users by a given matrix ID
Map<String, Presence> presences = {};
2019-08-08 08:31:39 +00:00
/// Callback will be called on account data updates.
AccountDataEventCB onAccountData;
/// Callback will be called on presences.
PresenceCB onPresence;
2020-01-02 14:09:49 +00:00
Room getRoomByAlias(String alias) {
for (int i = 0; i < rooms.length; i++) {
if (rooms[i].canonicalAlias == alias) return rooms[i];
}
return null;
}
Room getRoomById(String id) {
for (int j = 0; j < rooms.length; j++) {
if (rooms[j].id == id) return rooms[j];
}
return null;
}
2019-08-07 10:06:28 +00:00
void handleUserUpdate(UserUpdate userUpdate) {
if (userUpdate.type == "account_data") {
AccountData newAccountData = AccountData.fromJson(userUpdate.content);
accountData[newAccountData.typeKey] = newAccountData;
2019-08-08 08:31:39 +00:00
if (onAccountData != null) onAccountData(newAccountData);
2019-08-07 10:06:28 +00:00
}
if (userUpdate.type == "presence") {
Presence newPresence = Presence.fromJson(userUpdate.content);
2019-08-08 08:31:39 +00:00
presences[newPresence.sender] = newPresence;
if (onPresence != null) onPresence(newPresence);
2019-08-07 10:06:28 +00:00
}
}
2019-08-08 07:58:37 +00:00
Map<String, dynamic> get directChats =>
2019-08-07 10:06:28 +00:00
accountData["m.direct"] != null ? accountData["m.direct"].content : {};
/// 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) {
if (accountData["m.direct"] != null &&
accountData["m.direct"].content[userId] is List<dynamic> &&
accountData["m.direct"].content[userId].length > 0) {
2020-01-02 14:33:26 +00:00
if (getRoomById(accountData["m.direct"].content[userId][0]) != null) {
2020-01-02 14:09:49 +00:00
return accountData["m.direct"].content[userId][0];
2020-01-02 14:33:26 +00:00
}
2019-08-29 09:12:14 +00:00
(accountData["m.direct"].content[userId] as List<dynamic>)
.remove(accountData["m.direct"].content[userId][0]);
2020-01-02 14:09:49 +00:00
this.jsonRequest(
2019-08-29 09:12:14 +00:00
type: HTTPType.PUT,
action: "/client/r0/user/${userID}/account_data/m.direct",
data: directChats);
return getDirectChatFromUserId(userId);
}
2020-01-02 14:33:26 +00:00
for (int i = 0; i < this.rooms.length; i++) {
2020-01-02 14:09:49 +00:00
if (this.rooms[i].membership == Membership.invite &&
this.rooms[i].states[userID]?.senderId == userId &&
2020-01-02 14:33:26 +00:00
this.rooms[i].states[userID].content["is_direct"] == true) {
2020-01-02 14:09:49 +00:00
return this.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
2019-06-09 10:16:48 +00:00
/// Checks the supported versions of the Matrix protocol and the supported
/// login types. Returns false if the server is not compatible with the
/// client. Automatically sets [matrixVersions] and [lazyLoadMembers].
2019-12-29 10:28:33 +00:00
/// Throws FormatException, TimeoutException and MatrixException on error.
2019-06-09 10:16:48 +00:00
Future<bool> checkServer(serverUrl) async {
2019-12-29 10:28:33 +00:00
try {
2020-01-02 14:09:49 +00:00
_homeserver = serverUrl;
final versionResp = await this
.jsonRequest(type: HTTPType.GET, action: "/client/versions");
2019-12-29 10:28:33 +00:00
final List<String> versions = List<String>.from(versionResp["versions"]);
for (int i = 0; i < versions.length; i++) {
2020-01-02 14:33:26 +00:00
if (versions[i] == "r0.5.0") {
2019-12-29 10:28:33 +00:00
break;
2020-01-02 14:33:26 +00:00
} else if (i == versions.length - 1) {
2019-12-29 10:28:33 +00:00
return false;
}
2019-06-09 10:16:48 +00:00
}
2020-01-02 14:09:49 +00:00
_matrixVersions = versions;
2019-06-09 10:16:48 +00:00
2019-12-29 10:28:33 +00:00
if (versionResp.containsKey("unstable_features") &&
versionResp["unstable_features"].containsKey("m.lazy_load_members")) {
2020-01-02 14:09:49 +00:00
_lazyLoadMembers = versionResp["unstable_features"]
2019-12-29 10:28:33 +00:00
["m.lazy_load_members"]
? true
: false;
}
2019-06-09 10:16:48 +00:00
2020-01-02 14:09:49 +00:00
final loginResp = await this
.jsonRequest(type: HTTPType.GET, action: "/client/r0/login");
2019-06-09 10:16:48 +00:00
2019-12-29 10:28:33 +00:00
final List<dynamic> flows = loginResp["flows"];
2019-06-09 10:16:48 +00:00
2019-12-29 10:28:33 +00:00
for (int i = 0; i < flows.length; i++) {
if (flows[i].containsKey("type") &&
2020-01-02 14:33:26 +00:00
flows[i]["type"] == "m.login.password") {
2019-12-29 10:28:33 +00:00
break;
2020-01-02 14:33:26 +00:00
} else if (i == flows.length - 1) {
2019-12-29 10:28:33 +00:00
return false;
}
2019-06-09 10:16:48 +00:00
}
2019-12-29 10:28:33 +00:00
return true;
} catch (_) {
2020-01-02 14:09:49 +00:00
this._homeserver = this._matrixVersions = null;
2019-12-29 10:28:33 +00:00
rethrow;
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.
2019-06-09 10:16:48 +00:00
Future<bool> login(String username, String password) async {
2020-01-02 14:09:49 +00:00
final loginResp = await jsonRequest(
type: HTTPType.POST,
action: "/client/r0/login",
data: {
"type": "m.login.password",
"user": username,
"identifier": {
"type": "m.id.user",
"user": username,
},
"password": password,
"initial_device_display_name": "Famedly Talk"
});
2019-06-09 10:16:48 +00:00
final userID = loginResp["user_id"];
final accessToken = loginResp["access_token"];
if (userID == null || accessToken == null) {
2019-12-29 10:28:33 +00:00
return false;
2019-06-09 10:16:48 +00:00
}
2020-01-02 14:09:49 +00:00
await this.connect(
2019-06-09 10:16:48 +00:00
newToken: accessToken,
newUserID: userID,
newHomeserver: homeserver,
newDeviceName: "",
newDeviceID: "",
newMatrixVersions: matrixVersions,
newLazyLoadMembers: lazyLoadMembers);
return true;
}
/// Sends a logout command to the homeserver and clears all local data,
/// including all persistent data from the store.
Future<void> logout() async {
2019-12-29 10:28:33 +00:00
try {
2020-01-02 14:09:49 +00:00
await this.jsonRequest(type: HTTPType.POST, action: "/client/r0/logout");
2019-12-29 10:28:33 +00:00
} catch (exception) {
rethrow;
} finally {
2020-01-02 14:09:49 +00:00
await this.clear();
2019-12-29 10:28:33 +00:00
}
2019-06-09 10:16:48 +00:00
}
2019-11-30 09:36:30 +00:00
/// Get the combined profile information for this user. This API may be used to
/// fetch the user's own profile information or other users; either locally
/// or on remote homeservers.
Future<Profile> getProfileFromUserId(String userId) async {
2020-01-02 14:09:49 +00:00
final dynamic resp = await this.jsonRequest(
2019-11-30 09:36:30 +00:00
type: HTTPType.GET, action: "/client/r0/profile/${userId}");
return Profile.fromJson(resp);
}
2019-12-19 11:26:21 +00:00
Future<List<Room>> get archive async {
List<Room> archiveList = [];
2019-11-29 16:19:32 +00:00
String syncFilters =
2019-12-19 11:26:21 +00:00
'{"room":{"include_leave":true,"timeline":{"limit":10}}}';
2019-11-29 16:19:32 +00:00
String action = "/client/r0/sync?filter=$syncFilters&timeout=0";
2020-01-02 14:09:49 +00:00
final sync = await this.jsonRequest(type: HTTPType.GET, action: action);
2019-12-29 10:28:33 +00:00
if (sync["rooms"]["leave"] is Map<String, dynamic>) {
2019-12-19 11:26:21 +00:00
for (var entry in sync["rooms"]["leave"].entries) {
final String id = entry.key;
final dynamic room = entry.value;
print(id);
print(room.toString());
Room leftRoom = Room(
id: id,
membership: Membership.leave,
client: this,
roomAccountData: {},
mHeroes: []);
if (room["account_data"] is Map<String, dynamic> &&
room["account_data"]["events"] is List<dynamic>) {
for (dynamic event in room["account_data"]["events"]) {
leftRoom.roomAccountData[event["type"]] =
RoomAccountData.fromJson(event, leftRoom);
}
}
if (room["timeline"] is Map<String, dynamic> &&
room["timeline"]["events"] is List<dynamic>) {
for (dynamic event in room["timeline"]["events"]) {
2020-01-02 14:09:49 +00:00
leftRoom.setState(Event.fromJson(event, leftRoom));
2019-12-19 11:26:21 +00:00
}
}
if (room["state"] is Map<String, dynamic> &&
room["state"]["events"] is List<dynamic>) {
for (dynamic event in room["state"]["events"]) {
2020-01-02 14:09:49 +00:00
leftRoom.setState(Event.fromJson(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
}
Future<dynamic> joinRoomById(String id) async {
2020-01-02 14:09:49 +00:00
return await this
.jsonRequest(type: HTTPType.POST, action: "/client/r0/join/$id");
}
/// Loads the contact list for this user excluding the user itself.
/// Currently the contacts are found by discovering the contacts of
/// the famedlyContactDiscovery room, which is
2019-07-26 12:00:12 +00:00
/// defined by the autojoin room feature in Synapse.
Future<List<User>> loadFamedlyContacts() async {
List<User> contacts = [];
2020-01-02 14:09:49 +00:00
Room contactDiscoveryRoom =
this.getRoomByAlias("#famedlyContactDiscovery:${userID.split(":")[1]}");
2020-01-02 14:33:26 +00:00
if (contactDiscoveryRoom != null) {
contacts = await contactDiscoveryRoom.requestParticipants();
2020-01-02 14:33:26 +00:00
} else {
2019-10-14 16:50:10 +00:00
Map<String, bool> userMap = {};
2020-01-02 14:09:49 +00:00
for (int i = 0; i < this.rooms.length; i++) {
List<User> roomUsers = this.rooms[i].getParticipants();
2019-10-14 16:50:10 +00:00
for (int j = 0; j < roomUsers.length; j++) {
if (userMap[roomUsers[j].id] != true) contacts.add(roomUsers[j]);
userMap[roomUsers[j].id] = true;
}
2019-10-14 13:20:03 +00:00
}
}
2019-07-26 12:00:12 +00:00
return contacts;
}
@Deprecated('Please use [createRoom] instead!')
Future<String> createGroup(List<User> users) => createRoom(invite: users);
/// Creates a new group chat and invites the given Users and returns the new
/// created room ID. If [params] are provided, invite will be ignored. For the
/// moment please look at https://matrix.org/docs/spec/client_server/r0.5.0#post-matrix-client-r0-createroom
/// to configure [params].
Future<String> createRoom(
{List<User> invite, Map<String, dynamic> params}) async {
List<String> inviteIDs = [];
2020-01-02 14:33:26 +00:00
if (params == null && invite != null) {
for (int i = 0; i < invite.length; i++) {
inviteIDs.add(invite[i].id);
}
}
2019-12-29 10:28:33 +00:00
try {
2020-01-02 14:09:49 +00:00
final dynamic resp = await this.jsonRequest(
2019-12-29 10:28:33 +00:00
type: HTTPType.POST,
action: "/client/r0/createRoom",
data: params == null
? {
"invite": inviteIDs,
}
: params);
return resp["room_id"];
} catch (e) {
rethrow;
2019-06-14 06:09:37 +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-01-02 14:09:49 +00:00
final uploadResp = await this.upload(file);
await this.jsonRequest(
2019-09-09 13:22:02 +00:00
type: HTTPType.PUT,
action: "/client/r0/profile/$userID/avatar_url",
data: {"avatar_url": uploadResp});
2019-12-29 10:28:33 +00:00
return;
2019-09-09 13:22:02 +00:00
}
/// Fetches the pushrules for the logged in user.
/// These are needed for notifications on Android
2020-01-02 14:09:49 +00:00
Future<PushRules> getPushrules() async {
final dynamic resp = await this.jsonRequest(
type: HTTPType.GET,
action: "/client/r0/pushrules/",
);
2020-01-02 14:09:49 +00:00
return PushRules.fromJson(resp);
}
/// This endpoint allows the creation, modification and deletion of pushers for this user ID.
2020-01-02 14:09:49 +00:00
Future<void> setPushers(String pushKey, String kind, String appId,
String appDisplayName, String deviceDisplayName, String lang, String url,
{bool append, String profileTag, String format}) async {
Map<String, dynamic> data = {
"lang": lang,
"kind": kind,
"app_display_name": appDisplayName,
"device_display_name": deviceDisplayName,
"profile_tag": profileTag,
"app_id": appId,
"pushkey": pushKey,
"data": {"url": url}
};
if (format != null) data["data"]["format"] = format;
if (profileTag != null) data["profile_tag"] = profileTag;
if (append != null) data["append"] = append;
await this.jsonRequest(
type: HTTPType.POST,
action: "/client/r0/pushers/set",
2020-01-02 14:09:49 +00:00
data: data,
);
2019-12-29 10:28:33 +00:00
return;
}
2020-01-02 14:09:49 +00:00
static String syncFilters = '{"room":{"state":{"lazy_load_members":true}}}';
http.Client httpClient = http.Client();
/// 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
/// Outside of rooms there are account updates like account_data or presences.
2020-01-02 14:33:26 +00:00
final StreamController<UserUpdate> onUserEvent = 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
/// Synchronization erros are coming here.
final StreamController<MatrixException> onError =
2020-01-02 14:33:26 +00:00
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-01-02 14:33:26 +00:00
final StreamController<dynamic> onSync = StreamController.broadcast();
2020-01-02 14:09:49 +00:00
/// Matrix synchronisation is done with https long polling. This needs a
/// timeout which is usually 30 seconds.
int syncTimeoutSec = 30;
/// 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
/// .jsonRequest(type: HTTPType.POST, action: "/client/r0/login", data: {
/// "type": "m.login.password",
/// "user": "test",
/// "password": "1234",
/// "initial_device_display_name": "Fluffy Matrix Client"
/// });
/// ```
///
/// Returns:
///
/// ```
/// {
/// "user_id": "@cheeky_monkey:matrix.org",
/// "access_token": "abc123",
/// "device_id": "GHTYAJCE"
/// }
/// ```
///
/// Sends [LoginState.logged] to [onLoginStateChanged].
void connect(
{String newToken,
String newHomeserver,
String newUserID,
String newDeviceName,
String newDeviceID,
List<String> newMatrixVersions,
bool newLazyLoadMembers,
String newPrevBatch}) async {
this._accessToken = newToken;
this._homeserver = newHomeserver;
this._userID = newUserID;
this._deviceID = newDeviceID;
this._deviceName = newDeviceName;
this._matrixVersions = newMatrixVersions;
this._lazyLoadMembers = newLazyLoadMembers;
this.prevBatch = newPrevBatch;
if (this.store != null) {
2020-01-02 14:33:26 +00:00
await this.store.storeClient();
2020-01-02 14:09:49 +00:00
this._rooms = await this.store.getRoomList(onlyLeft: false);
2020-01-03 13:21:15 +00:00
this._sortRooms();
2020-01-02 14:09:49 +00:00
this.accountData = await this.store.getAccountData();
this.presences = await this.store.getPresences();
}
_userEventSub ??= onUserEvent.stream.listen(this.handleUserUpdate);
onLoginStateChanged.add(LoginState.logged);
2020-01-02 14:33:26 +00:00
return _sync();
2020-01-02 14:09:49 +00:00
}
StreamSubscription _userEventSub;
/// Resets all settings and stops the synchronisation.
void clear() {
this.store?.clear();
this._accessToken = this._homeserver = this._userID = this._deviceID = this
._deviceName =
this._matrixVersions = this._lazyLoadMembers = this.prevBatch = null;
onLoginStateChanged.add(LoginState.loggedOut);
}
/// Used for all Matrix json requests using the [c2s API](https://matrix.org/docs/spec/client_server/r0.4.0.html).
///
/// Throws: TimeoutException, FormatException, MatrixException
///
/// You must first call [this.connect()] or set [this.homeserver] before you can use
/// this! For example to send a message to a Matrix room with the id
/// '!fjd823j:example.com' you call:
///
/// ```
/// final resp = await jsonRequest(
/// type: HTTPType.PUT,
/// action: "/r0/rooms/!fjd823j:example.com/send/m.room.message/$txnId",
/// data: {
/// "msgtype": "m.text",
/// "body": "hello"
/// }
/// );
/// ```
///
Future<Map<String, dynamic>> jsonRequest(
{HTTPType type,
String action,
dynamic data = "",
int timeout,
String contentType = "application/json"}) async {
2020-01-02 14:33:26 +00:00
if (this.isLogged() == false && this.homeserver == null) {
2020-01-02 14:09:49 +00:00
throw ("No homeserver specified.");
2020-01-02 14:33:26 +00:00
}
2020-01-02 14:09:49 +00:00
if (timeout == null) timeout = syncTimeoutSec + 5;
dynamic json;
if (data is Map) data.removeWhere((k, v) => v == null);
(!(data is String)) ? json = jsonEncode(data) : json = data;
if (data is List<int> || action.startsWith("/media/r0/upload")) json = data;
final url = "${this.homeserver}/_matrix${action}";
Map<String, String> headers = {};
2020-01-02 14:33:26 +00:00
if (type == HTTPType.PUT || type == HTTPType.POST) {
2020-01-02 14:09:49 +00:00
headers["Content-Type"] = contentType;
2020-01-02 14:33:26 +00:00
}
if (this.isLogged()) {
2020-01-02 14:09:49 +00:00
headers["Authorization"] = "Bearer ${this.accessToken}";
2020-01-02 14:33:26 +00:00
}
2020-01-02 14:09:49 +00:00
2020-01-02 14:33:26 +00:00
if (this.debug) {
2020-01-02 14:09:49 +00:00
print(
"[REQUEST ${type.toString().split('.').last}] Action: $action, Data: $data");
2020-01-02 14:33:26 +00:00
}
2020-01-02 14:09:49 +00:00
http.Response resp;
Map<String, dynamic> jsonResp = {};
try {
switch (type.toString().split('.').last) {
case "GET":
resp = await httpClient
.get(url, headers: headers)
.timeout(Duration(seconds: timeout));
break;
case "POST":
resp = await httpClient
.post(url, body: json, headers: headers)
.timeout(Duration(seconds: timeout));
break;
case "PUT":
resp = await httpClient
.put(url, body: json, headers: headers)
.timeout(Duration(seconds: timeout));
break;
case "DELETE":
resp = await httpClient
.delete(url, headers: headers)
.timeout(Duration(seconds: timeout));
break;
}
2020-01-03 13:27:49 +00:00
jsonResp = jsonDecode(String.fromCharCodes(resp.body.runes))
2020-01-02 14:09:49 +00:00
as Map<String, dynamic>; // May throw FormatException
if (jsonResp.containsKey("errcode") && jsonResp["errcode"] is String) {
// The server has responsed with an matrix related error.
MatrixException exception = MatrixException(resp);
if (exception.error == MatrixError.M_UNKNOWN_TOKEN) {
// The token is no longer valid. Need to sign off....
onError.add(exception);
clear();
}
throw exception;
}
if (this.debug) print("[RESPONSE] ${jsonResp.toString()}");
} on ArgumentError catch (exception) {
print(exception);
// Ignore this error
} catch (_) {
print(_);
rethrow;
}
return jsonResp;
}
/// Uploads a file with the name [fileName] as base64 encoded to the server
/// and returns the mxc url as a string.
Future<String> upload(MatrixFile file) async {
dynamic fileBytes;
2020-01-02 14:33:26 +00:00
if (this.homeserver != "https://fakeServer.notExisting") {
2020-01-02 14:09:49 +00:00
fileBytes = file.bytes;
2020-01-02 14:33:26 +00:00
}
2020-01-02 14:09:49 +00:00
String fileName = file.path.split("/").last.toLowerCase();
String mimeType = mime(file.path);
print("[UPLOADING] $fileName, type: $mimeType, size: ${fileBytes?.length}");
final Map<String, dynamic> resp = await jsonRequest(
type: HTTPType.POST,
action: "/media/r0/upload?filename=$fileName",
data: fileBytes,
contentType: mimeType);
return resp["content_uri"];
}
Future<dynamic> _syncRequest;
Future<void> _sync() async {
if (this.isLogged() == false) return;
String action = "/client/r0/sync?filter=$syncFilters";
if (this.prevBatch != null) {
action += "&timeout=30000";
action += "&since=${this.prevBatch}";
}
try {
_syncRequest = jsonRequest(type: HTTPType.GET, action: action);
final int hash = _syncRequest.hashCode;
final syncResp = await _syncRequest;
if (hash != _syncRequest.hashCode) return;
2020-01-02 14:33:26 +00:00
if (this.store != null) {
2020-01-02 14:09:49 +00:00
await this.store.transaction(() {
handleSync(syncResp);
this.store.storePrevBatch(syncResp);
return;
});
2020-01-02 14:33:26 +00:00
} else {
2020-01-02 14:09:49 +00:00
await handleSync(syncResp);
2020-01-02 14:33:26 +00:00
}
2020-01-02 14:09:49 +00:00
if (this.prevBatch == null) this.onFirstSync.add(true);
this.prevBatch = syncResp["next_batch"];
2020-01-02 14:33:26 +00:00
if (hash == _syncRequest.hashCode) unawaited(_sync());
2020-01-02 14:09:49 +00:00
} on MatrixException catch (exception) {
onError.add(exception);
await Future.delayed(Duration(seconds: syncErrorTimeoutSec), _sync);
} catch (exception) {
await Future.delayed(Duration(seconds: syncErrorTimeoutSec), _sync);
}
}
void handleSync(dynamic sync) {
if (sync["rooms"] is Map<String, dynamic>) {
2020-01-02 14:33:26 +00:00
if (sync["rooms"]["join"] is Map<String, dynamic>) {
2020-01-02 14:09:49 +00:00
_handleRooms(sync["rooms"]["join"], Membership.join);
2020-01-02 14:33:26 +00:00
}
if (sync["rooms"]["invite"] is Map<String, dynamic>) {
2020-01-02 14:09:49 +00:00
_handleRooms(sync["rooms"]["invite"], Membership.invite);
2020-01-02 14:33:26 +00:00
}
if (sync["rooms"]["leave"] is Map<String, dynamic>) {
2020-01-02 14:09:49 +00:00
_handleRooms(sync["rooms"]["leave"], Membership.leave);
2020-01-02 14:33:26 +00:00
}
2020-01-02 14:09:49 +00:00
}
if (sync["presence"] is Map<String, dynamic> &&
sync["presence"]["events"] is List<dynamic>) {
_handleGlobalEvents(sync["presence"]["events"], "presence");
}
if (sync["account_data"] is Map<String, dynamic> &&
sync["account_data"]["events"] is List<dynamic>) {
_handleGlobalEvents(sync["account_data"]["events"], "account_data");
}
if (sync["to_device"] is Map<String, dynamic> &&
sync["to_device"]["events"] is List<dynamic>) {
_handleGlobalEvents(sync["to_device"]["events"], "to_device");
}
onSync.add(sync);
}
void _handleRooms(Map<String, dynamic> rooms, Membership membership) {
rooms.forEach((String id, dynamic room) async {
// calculate the notification counts, the limitedTimeline and prevbatch
num highlight_count = 0;
num notification_count = 0;
String prev_batch = "";
bool limitedTimeline = false;
if (room["unread_notifications"] is Map<String, dynamic>) {
2020-01-02 14:33:26 +00:00
if (room["unread_notifications"]["highlight_count"] is num) {
2020-01-02 14:09:49 +00:00
highlight_count = room["unread_notifications"]["highlight_count"];
2020-01-02 14:33:26 +00:00
}
if (room["unread_notifications"]["notification_count"] is num) {
2020-01-02 14:09:49 +00:00
notification_count =
room["unread_notifications"]["notification_count"];
2020-01-02 14:33:26 +00:00
}
2020-01-02 14:09:49 +00:00
}
if (room["timeline"] is Map<String, dynamic>) {
2020-01-02 14:33:26 +00:00
if (room["timeline"]["limited"] is bool) {
2020-01-02 14:09:49 +00:00
limitedTimeline = room["timeline"]["limited"];
2020-01-02 14:33:26 +00:00
}
if (room["timeline"]["prev_batch"] is String) {
2020-01-02 14:09:49 +00:00
prev_batch = room["timeline"]["prev_batch"];
2020-01-02 14:33:26 +00:00
}
2020-01-02 14:09:49 +00:00
}
RoomSummary summary;
if (room["summary"] is Map<String, dynamic>) {
summary = RoomSummary.fromJson(room["summary"]);
}
RoomUpdate update = RoomUpdate(
id: id,
membership: membership,
notification_count: notification_count,
highlight_count: highlight_count,
limitedTimeline: limitedTimeline,
prev_batch: prev_batch,
summary: summary,
);
_updateRoomsByRoomUpdate(update);
2020-01-02 14:33:26 +00:00
unawaited(this.store?.storeRoomUpdate(update));
2020-01-02 14:09:49 +00:00
onRoomUpdate.add(update);
/// Handle now all room events and save them in the database
if (room["state"] is Map<String, dynamic> &&
2020-01-02 14:33:26 +00:00
room["state"]["events"] is List<dynamic>) {
2020-01-02 14:09:49 +00:00
_handleRoomEvents(id, room["state"]["events"], "state");
2020-01-02 14:33:26 +00:00
}
2020-01-02 14:09:49 +00:00
if (room["invite_state"] is Map<String, dynamic> &&
2020-01-02 14:33:26 +00:00
room["invite_state"]["events"] is List<dynamic>) {
2020-01-02 14:09:49 +00:00
_handleRoomEvents(id, room["invite_state"]["events"], "invite_state");
2020-01-02 14:33:26 +00:00
}
2020-01-02 14:09:49 +00:00
if (room["timeline"] is Map<String, dynamic> &&
2020-01-02 14:33:26 +00:00
room["timeline"]["events"] is List<dynamic>) {
2020-01-02 14:09:49 +00:00
_handleRoomEvents(id, room["timeline"]["events"], "timeline");
2020-01-02 14:33:26 +00:00
}
2020-01-02 14:09:49 +00:00
if (room["ephemeral"] is Map<String, dynamic> &&
2020-01-02 14:33:26 +00:00
room["ephemeral"]["events"] is List<dynamic>) {
2020-01-02 14:09:49 +00:00
_handleEphemerals(id, room["ephemeral"]["events"]);
2020-01-02 14:33:26 +00:00
}
2020-01-02 14:09:49 +00:00
if (room["account_data"] is Map<String, dynamic> &&
2020-01-02 14:33:26 +00:00
room["account_data"]["events"] is List<dynamic>) {
2020-01-02 14:09:49 +00:00
_handleRoomEvents(id, room["account_data"]["events"], "account_data");
2020-01-02 14:33:26 +00:00
}
2020-01-02 14:09:49 +00:00
});
}
void _handleEphemerals(String id, List<dynamic> events) {
for (num i = 0; i < events.length; i++) {
_handleEvent(events[i], id, "ephemeral");
// Receipt events are deltas between two states. We will create a
// fake room account data event for this and store the difference
// there.
if (events[i]["type"] == "m.receipt") {
Room room = this.getRoomById(id);
if (room == null) room = Room(id: id);
Map<String, dynamic> receiptStateContent =
room.roomAccountData["m.receipt"]?.content ?? {};
for (var eventEntry in events[i]["content"].entries) {
final String eventID = eventEntry.key;
if (events[i]["content"][eventID]["m.read"] != null) {
final Map<String, dynamic> userTimestampMap =
events[i]["content"][eventID]["m.read"];
for (var userTimestampMapEntry in userTimestampMap.entries) {
final String mxid = userTimestampMapEntry.key;
// Remove previous receipt event from this user
for (var entry in receiptStateContent.entries) {
if (entry.value["m.read"] is Map<String, dynamic> &&
entry.value["m.read"].containsKey(mxid)) {
entry.value["m.read"].remove(mxid);
break;
}
}
if (userTimestampMap[mxid] is Map<String, dynamic> &&
userTimestampMap[mxid].containsKey("ts")) {
receiptStateContent[mxid] = {
"event_id": eventID,
"ts": userTimestampMap[mxid]["ts"],
};
}
}
}
}
events[i]["content"] = receiptStateContent;
_handleEvent(events[i], id, "account_data");
}
}
}
void _handleRoomEvents(String chat_id, List<dynamic> events, String type) {
for (num i = 0; i < events.length; i++) {
_handleEvent(events[i], chat_id, type);
}
}
void _handleGlobalEvents(List<dynamic> events, String type) {
2020-01-02 14:33:26 +00:00
for (int i = 0; i < events.length; i++) {
2020-01-02 14:09:49 +00:00
if (events[i]["type"] is String &&
events[i]["content"] is Map<String, dynamic>) {
UserUpdate update = UserUpdate(
eventType: events[i]["type"],
type: type,
content: events[i],
);
this.store?.storeUserEventUpdate(update);
onUserEvent.add(update);
}
2020-01-02 14:33:26 +00:00
}
2020-01-02 14:09:49 +00:00
}
void _handleEvent(Map<String, dynamic> event, String roomID, String type) {
if (event["type"] is String && event["content"] is Map<String, dynamic>) {
EventUpdate update = EventUpdate(
eventType: event["type"],
roomID: roomID,
type: type,
content: event,
);
_updateRoomsByEventUpdate(update);
this.store?.storeEventUpdate(update);
onEvent.add(update);
}
}
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;
}
final bool found = (j < rooms.length && rooms[j].id == chatUpdate.id);
final bool isLeftRoom = chatUpdate.membership == Membership.leave;
// Does the chat already exist in the list rooms?
if (!found && !isLeftRoom) {
num position = chatUpdate.membership == Membership.invite ? 0 : j;
// Add the new chat to the list
Room newRoom = Room(
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
}
if (rooms[j].onUpdate != null) rooms[j].onUpdate();
}
2020-01-03 13:21:15 +00:00
_sortRooms();
2020-01-02 14:09:49 +00:00
}
void _updateRoomsByEventUpdate(EventUpdate eventUpdate) {
if (eventUpdate.type == "history") return;
// Search the room in the rooms
num j = 0;
for (j = 0; j < rooms.length; j++) {
if (rooms[j].id == eventUpdate.roomID) break;
}
final bool found = (j < rooms.length && rooms[j].id == eventUpdate.roomID);
if (!found) return;
if (eventUpdate.type == "timeline" ||
eventUpdate.type == "state" ||
eventUpdate.type == "invite_state") {
Event stateEvent = Event.fromJson(eventUpdate.content, rooms[j]);
if (stateEvent.type == EventTypes.Redaction) {
final String redacts = eventUpdate.content["redacts"];
rooms[j].states.states.forEach(
(String key, Map<String, Event> states) => states.forEach(
(String key, Event state) {
if (state.eventId == redacts) {
state.setRedactionEvent(stateEvent);
}
},
),
);
} else {
Event prevState =
rooms[j].getState(stateEvent.typeKey, stateEvent.stateKey);
if (prevState != null &&
prevState.time.millisecondsSinceEpoch >
stateEvent.time.millisecondsSinceEpoch) return;
rooms[j].setState(stateEvent);
}
} else if (eventUpdate.type == "account_data") {
rooms[j].roomAccountData[eventUpdate.eventType] =
RoomAccountData.fromJson(eventUpdate.content, rooms[j]);
} else if (eventUpdate.type == "ephemeral") {
rooms[j].ephemerals[eventUpdate.eventType] =
RoomAccountData.fromJson(eventUpdate.content, rooms[j]);
}
if (rooms[j].onUpdate != null) rooms[j].onUpdate();
2020-01-03 13:21:15 +00:00
if (eventUpdate.type == "timeline") _sortRooms();
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-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.
RoomSorter sortRoomsBy = (a, b) => b.timeCreated.millisecondsSinceEpoch
.compareTo(a.timeCreated.millisecondsSinceEpoch);
_sortRooms() {
2020-01-02 14:09:49 +00:00
if (prevBatch == null) return;
2020-01-03 13:21:15 +00:00
if (_sortLock || rooms.length < 2) return;
_sortLock = true;
rooms?.sort(sortRoomsBy);
_sortLock = false;
2020-01-02 14:09:49 +00:00
}
2019-06-09 10:16:48 +00:00
}