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
|
|
|
*/
|
|
|
|
|
2019-06-09 10:16:48 +00:00
|
|
|
import 'dart:async';
|
|
|
|
import 'dart:core';
|
2019-07-12 09:26:07 +00:00
|
|
|
|
2019-08-07 10:06:28 +00:00
|
|
|
import 'package:famedlysdk/src/AccountData.dart';
|
|
|
|
import 'package:famedlysdk/src/Presence.dart';
|
|
|
|
import 'package:famedlysdk/src/sync/UserUpdate.dart';
|
|
|
|
|
2019-06-09 10:16:48 +00:00
|
|
|
import 'Connection.dart';
|
2019-06-21 11:30:39 +00:00
|
|
|
import 'Room.dart';
|
2019-07-12 09:26:07 +00:00
|
|
|
import 'RoomList.dart';
|
2019-06-09 10:16:48 +00:00
|
|
|
import 'Store.dart';
|
2019-06-11 11:09:26 +00:00
|
|
|
import 'User.dart';
|
2019-07-12 09:26:07 +00:00
|
|
|
import 'requests/SetPushersRequest.dart';
|
|
|
|
import 'responses/ErrorResponse.dart';
|
2019-06-21 07:41:09 +00:00
|
|
|
import 'responses/PushrulesResponse.dart';
|
2019-06-09 10:16:48 +00:00
|
|
|
|
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.
|
|
|
|
Connection connection;
|
|
|
|
|
|
|
|
/// Optional persistent store for all data.
|
|
|
|
Store store;
|
|
|
|
|
2019-06-12 11:43:14 +00:00
|
|
|
Client(this.clientName, {this.debug = false}) {
|
2019-06-09 10:16:48 +00:00
|
|
|
connection = Connection(this);
|
|
|
|
|
2019-06-11 11:09:26 +00:00
|
|
|
if (this.clientName != "testclient") store = Store(this);
|
2019-06-09 10:16:48 +00:00
|
|
|
connection.onLoginStateChanged.stream.listen((loginState) {
|
|
|
|
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.
|
|
|
|
String homeserver;
|
|
|
|
|
|
|
|
/// The Matrix ID of the current logged user.
|
|
|
|
String userID;
|
|
|
|
|
|
|
|
/// This is the access token for the matrix client. When it is undefined, then
|
|
|
|
/// the user needs to sign in first.
|
|
|
|
String accessToken;
|
|
|
|
|
|
|
|
/// This points to the position in the synchronization history.
|
|
|
|
String prevBatch;
|
|
|
|
|
|
|
|
/// The device ID is an unique identifier for this device.
|
|
|
|
String deviceID;
|
|
|
|
|
|
|
|
/// The device name is a human readable identifier for this device.
|
|
|
|
String deviceName;
|
|
|
|
|
|
|
|
/// Which version of the matrix specification does this server support?
|
|
|
|
List<String> matrixVersions;
|
|
|
|
|
|
|
|
/// Wheither the server supports lazy load members.
|
|
|
|
bool lazyLoadMembers = false;
|
|
|
|
|
|
|
|
/// 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.
|
2019-08-07 09:38:51 +00:00
|
|
|
RoomList roomList;
|
|
|
|
|
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;
|
|
|
|
|
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) {
|
|
|
|
if (roomList.getRoomById(accountData["m.direct"].content[userId][0]) !=
|
|
|
|
null) return accountData["m.direct"].content[userId][0];
|
|
|
|
(accountData["m.direct"].content[userId] as List<dynamic>)
|
|
|
|
.remove(accountData["m.direct"].content[userId][0]);
|
|
|
|
connection.jsonRequest(
|
|
|
|
type: HTTPType.PUT,
|
|
|
|
action: "/client/r0/user/${userID}/account_data/m.direct",
|
|
|
|
data: directChats);
|
|
|
|
return getDirectChatFromUserId(userId);
|
|
|
|
}
|
|
|
|
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].
|
|
|
|
Future<bool> checkServer(serverUrl) async {
|
|
|
|
homeserver = serverUrl;
|
|
|
|
|
2019-07-12 09:26:07 +00:00
|
|
|
final versionResp = await connection.jsonRequest(
|
|
|
|
type: HTTPType.GET, action: "/client/versions");
|
2019-06-09 10:16:48 +00:00
|
|
|
if (versionResp is ErrorResponse) {
|
|
|
|
connection.onError.add(ErrorResponse(errcode: "NO_RESPONSE", error: ""));
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
final List<String> versions = List<String>.from(versionResp["versions"]);
|
|
|
|
|
|
|
|
if (versions == null) {
|
|
|
|
connection.onError.add(ErrorResponse(errcode: "NO_RESPONSE", error: ""));
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
for (int i = 0; i < versions.length; i++) {
|
2019-08-06 09:47:09 +00:00
|
|
|
if (versions[i] == "r0.5.0")
|
2019-06-09 10:16:48 +00:00
|
|
|
break;
|
|
|
|
else if (i == versions.length - 1) {
|
|
|
|
connection.onError.add(ErrorResponse(errcode: "NO_SUPPORT", error: ""));
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
matrixVersions = versions;
|
|
|
|
|
|
|
|
if (versionResp.containsKey("unstable_features") &&
|
|
|
|
versionResp["unstable_features"].containsKey("m.lazy_load_members")) {
|
|
|
|
lazyLoadMembers = versionResp["unstable_features"]["m.lazy_load_members"]
|
|
|
|
? true
|
|
|
|
: false;
|
|
|
|
}
|
|
|
|
|
2019-07-12 09:26:07 +00:00
|
|
|
final loginResp = await connection.jsonRequest(
|
|
|
|
type: HTTPType.GET, action: "/client/r0/login");
|
2019-06-09 10:16:48 +00:00
|
|
|
if (loginResp is ErrorResponse) {
|
|
|
|
connection.onError.add(loginResp);
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
final List<dynamic> flows = loginResp["flows"];
|
|
|
|
|
|
|
|
for (int i = 0; i < flows.length; i++) {
|
|
|
|
if (flows[i].containsKey("type") &&
|
|
|
|
flows[i]["type"] == "m.login.password")
|
|
|
|
break;
|
|
|
|
else if (i == flows.length - 1) {
|
|
|
|
connection.onError.add(ErrorResponse(errcode: "NO_SUPPORT", error: ""));
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Handles the login and allows the client to call all APIs which require
|
|
|
|
/// authentication. Returns false if the login was not successful.
|
|
|
|
Future<bool> login(String username, String password) async {
|
2019-06-11 11:09:26 +00:00
|
|
|
final loginResp = await connection
|
2019-07-12 09:26:07 +00:00
|
|
|
.jsonRequest(type: HTTPType.POST, action: "/client/r0/login", data: {
|
2019-06-09 10:16:48 +00:00
|
|
|
"type": "m.login.password",
|
|
|
|
"user": username,
|
|
|
|
"identifier": {
|
|
|
|
"type": "m.id.user",
|
|
|
|
"user": username,
|
|
|
|
},
|
|
|
|
"password": password,
|
|
|
|
"initial_device_display_name": "Famedly Talk"
|
|
|
|
});
|
|
|
|
|
|
|
|
if (loginResp is ErrorResponse) {
|
|
|
|
connection.onError.add(loginResp);
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
final userID = loginResp["user_id"];
|
|
|
|
final accessToken = loginResp["access_token"];
|
|
|
|
if (userID == null || accessToken == null) {
|
|
|
|
connection.onError.add(ErrorResponse(errcode: "NO_SUPPORT", error: ""));
|
|
|
|
}
|
|
|
|
|
|
|
|
await connection.connect(
|
|
|
|
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-06-11 11:09:26 +00:00
|
|
|
final dynamic resp = await connection.jsonRequest(
|
2019-07-12 09:26:07 +00:00
|
|
|
type: HTTPType.POST, action: "/client/r0/logout/all");
|
2019-06-14 06:09:37 +00:00
|
|
|
if (resp is ErrorResponse) connection.onError.add(resp);
|
2019-06-09 10:16:48 +00:00
|
|
|
|
|
|
|
await connection.clear();
|
|
|
|
}
|
|
|
|
|
2019-06-21 11:30:39 +00:00
|
|
|
/// Loads the Rooms from the [store] and creates a new [RoomList] object.
|
|
|
|
Future<RoomList> getRoomList(
|
|
|
|
{bool onlyLeft = false,
|
|
|
|
bool onlyDirect = false,
|
|
|
|
bool onlyGroups = false,
|
2019-06-25 10:06:26 +00:00
|
|
|
onRoomListUpdateCallback onUpdate,
|
|
|
|
onRoomListInsertCallback onInsert,
|
|
|
|
onRoomListRemoveCallback onRemove}) async {
|
2019-06-21 11:30:39 +00:00
|
|
|
List<Room> rooms = await store.getRoomList(
|
|
|
|
onlyLeft: onlyLeft, onlyGroups: onlyGroups, onlyDirect: onlyDirect);
|
|
|
|
return RoomList(
|
|
|
|
client: this,
|
|
|
|
onlyLeft: onlyLeft,
|
|
|
|
onlyDirect: onlyDirect,
|
|
|
|
onlyGroups: onlyGroups,
|
|
|
|
onUpdate: onUpdate,
|
|
|
|
onInsert: onInsert,
|
|
|
|
onRemove: onRemove,
|
|
|
|
rooms: rooms);
|
|
|
|
}
|
|
|
|
|
2019-07-12 09:26:07 +00:00
|
|
|
Future<dynamic> joinRoomById(String id) async {
|
|
|
|
return await connection.jsonRequest(
|
|
|
|
type: HTTPType.POST, action: "/client/r0/join/$id");
|
|
|
|
}
|
|
|
|
|
2019-07-26 12:46:23 +00:00
|
|
|
/// 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.
|
2019-07-26 12:46:23 +00:00
|
|
|
Future<List<User>> loadFamedlyContacts() async {
|
2019-07-26 14:07:29 +00:00
|
|
|
List<User> contacts = [];
|
2019-08-07 10:06:28 +00:00
|
|
|
Room contactDiscoveryRoom = roomList
|
2019-07-26 12:00:12 +00:00
|
|
|
.getRoomByAlias("#famedlyContactDiscovery:${userID.split(":")[1]}");
|
2019-07-26 14:07:29 +00:00
|
|
|
if (contactDiscoveryRoom != null)
|
|
|
|
contacts = await contactDiscoveryRoom.requestParticipants();
|
|
|
|
else
|
2019-08-08 07:58:37 +00:00
|
|
|
contacts = await store?.loadContacts();
|
2019-07-26 12:00:12 +00:00
|
|
|
return contacts;
|
|
|
|
}
|
|
|
|
|
2019-06-11 11:09:26 +00:00
|
|
|
/// Creates a new group chat and invites the given Users and returns the new
|
|
|
|
/// created room ID.
|
|
|
|
Future<String> createGroup(List<User> users) async {
|
|
|
|
List<String> inviteIDs = [];
|
|
|
|
for (int i = 0; i < users.length; i++) inviteIDs.add(users[i].id);
|
|
|
|
|
2019-06-14 06:09:37 +00:00
|
|
|
final dynamic resp = await connection.jsonRequest(
|
2019-07-12 09:26:07 +00:00
|
|
|
type: HTTPType.POST,
|
2019-06-11 11:09:26 +00:00
|
|
|
action: "/client/r0/createRoom",
|
|
|
|
data: {"invite": inviteIDs, "preset": "private_chat"});
|
|
|
|
|
2019-06-14 06:09:37 +00:00
|
|
|
if (resp is ErrorResponse) {
|
|
|
|
connection.onError.add(resp);
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
2019-06-11 11:09:26 +00:00
|
|
|
return resp["room_id"];
|
|
|
|
}
|
2019-06-21 07:41:09 +00:00
|
|
|
|
|
|
|
/// Fetches the pushrules for the logged in user.
|
|
|
|
/// These are needed for notifications on Android
|
|
|
|
Future<PushrulesResponse> getPushrules() async {
|
|
|
|
final dynamic resp = await connection.jsonRequest(
|
2019-07-12 09:26:07 +00:00
|
|
|
type: HTTPType.GET,
|
2019-07-18 16:47:59 +00:00
|
|
|
action: "/client/r0/pushrules/",
|
2019-06-21 07:41:09 +00:00
|
|
|
);
|
|
|
|
|
|
|
|
if (resp is ErrorResponse) {
|
|
|
|
connection.onError.add(resp);
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
return PushrulesResponse.fromJson(resp);
|
|
|
|
}
|
2019-06-25 13:33:56 +00:00
|
|
|
|
|
|
|
/// This endpoint allows the creation, modification and deletion of pushers for this user ID.
|
2019-07-24 07:59:29 +00:00
|
|
|
Future<dynamic> setPushers(SetPushersRequest data) async {
|
2019-06-25 13:33:56 +00:00
|
|
|
final dynamic resp = await connection.jsonRequest(
|
2019-07-12 09:26:07 +00:00
|
|
|
type: HTTPType.POST,
|
2019-06-25 13:33:56 +00:00
|
|
|
action: "/client/r0/pushers/set",
|
2019-07-18 15:21:19 +00:00
|
|
|
data: data.toJson(),
|
2019-06-25 13:33:56 +00:00
|
|
|
);
|
|
|
|
|
|
|
|
if (resp is ErrorResponse) {
|
|
|
|
connection.onError.add(resp);
|
|
|
|
}
|
|
|
|
|
2019-07-24 07:59:29 +00:00
|
|
|
return resp;
|
2019-06-25 13:33:56 +00:00
|
|
|
}
|
2019-06-09 10:16:48 +00:00
|
|
|
}
|
2019-08-08 08:31:39 +00:00
|
|
|
|
|
|
|
typedef AccountDataEventCB = void Function(AccountData accountData);
|
|
|
|
typedef PresenceCB = void Function(Presence presence);
|