famedlysdk/lib/src/Room.dart

511 lines
17 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 'package:famedlysdk/src/Client.dart';
import 'package:famedlysdk/src/Event.dart';
2019-06-09 10:16:48 +00:00
import 'package:famedlysdk/src/responses/ErrorResponse.dart';
2019-06-11 11:44:25 +00:00
import 'package:famedlysdk/src/sync/EventUpdate.dart';
import 'package:famedlysdk/src/utils/ChatTime.dart';
import 'package:famedlysdk/src/utils/MxContent.dart';
2019-06-09 11:57:33 +00:00
import './User.dart';
import 'Connection.dart';
2019-06-21 10:18:54 +00:00
import 'Timeline.dart';
2019-06-09 10:16:48 +00:00
2019-06-09 12:33:25 +00:00
/// Represents a Matrix room.
2019-06-09 10:16:48 +00:00
class Room {
2019-06-11 08:51:45 +00:00
/// The full qualified Matrix ID for the room in the format '!localid:server.abc'.
final String id;
/// Membership status of the user for this room.
Membership membership;
2019-06-11 08:51:45 +00:00
/// The name of the room if set by a participant.
2019-06-09 10:16:48 +00:00
String name;
2019-06-11 08:51:45 +00:00
2019-07-23 13:03:16 +00:00
/// Whether this room has a name or the name is generated by member names.
bool hasName = false;
2019-06-11 08:51:45 +00:00
/// The topic of the room if set by a participant.
String topic;
/// The avatar of the room if set by a participant.
2019-06-12 10:04:52 +00:00
MxContent avatar = MxContent("");
2019-06-11 08:51:45 +00:00
/// The count of unread notifications.
2019-06-09 10:16:48 +00:00
int notificationCount;
2019-06-11 08:51:45 +00:00
/// The count of highlighted notifications.
2019-06-09 10:16:48 +00:00
int highlightCount;
2019-06-11 08:51:45 +00:00
String prev_batch;
String draft;
/// Time when the user has last read the chat.
ChatTime unread;
/// ID of the fully read marker event.
String fullyRead;
/// The address in the format: #roomname:homeserver.org.
String canonicalAlias;
/// If this room is a direct chat, this is the matrix ID of the user
String directChatMatrixID;
/// Must be one of [all, mention]
String notificationSettings;
/// Are guest users allowed?
String guestAccess;
/// Who can see the history of this room?
String historyVisibility;
/// Who is allowed to join this room?
String joinRules;
/// The needed power levels for all actions.
2019-06-11 11:44:25 +00:00
Map<String, int> powerLevels = {};
2019-06-11 08:51:45 +00:00
2019-06-21 10:18:54 +00:00
Event lastEvent;
2019-06-11 08:51:45 +00:00
/// Your current client instance.
final Client client;
@Deprecated("Rooms.roomID is deprecated! Use Rooms.id instead!")
2019-06-11 11:44:25 +00:00
String get roomID => this.id;
2019-06-11 08:51:45 +00:00
@Deprecated("Rooms.matrix is deprecated! Use Rooms.client instead!")
Client get matrix => this.client;
@Deprecated("Rooms.status is deprecated! Use Rooms.membership instead!")
String get status => this.membership.toString().split('.').last;
2019-06-11 08:51:45 +00:00
2019-06-09 10:16:48 +00:00
Room({
2019-06-11 08:51:45 +00:00
this.id,
this.membership,
2019-06-09 10:16:48 +00:00
this.name,
2019-07-23 13:07:31 +00:00
this.hasName = false,
2019-06-11 08:51:45 +00:00
this.topic,
2019-06-09 10:16:48 +00:00
this.avatar,
this.notificationCount,
this.highlightCount,
2019-06-28 09:42:57 +00:00
this.prev_batch = "",
2019-06-11 08:51:45 +00:00
this.draft,
this.unread,
this.fullyRead,
this.canonicalAlias,
this.directChatMatrixID,
this.notificationSettings,
this.guestAccess,
this.historyVisibility,
this.joinRules,
this.powerLevels,
2019-06-21 10:18:54 +00:00
this.lastEvent,
2019-06-11 08:51:45 +00:00
this.client,
2019-06-09 10:16:48 +00:00
});
2019-06-11 08:51:45 +00:00
/// The last message sent to this room.
String get lastMessage {
2019-06-21 10:18:54 +00:00
if (lastEvent != null)
return lastEvent.getBody();
2019-06-11 11:44:25 +00:00
else
return "";
2019-06-11 08:51:45 +00:00
}
/// When the last message received.
ChatTime get timeCreated {
2019-06-21 10:18:54 +00:00
if (lastEvent != null)
return lastEvent.time;
2019-06-11 11:44:25 +00:00
else
return ChatTime.now();
2019-06-09 10:16:48 +00:00
}
2019-06-11 08:51:45 +00:00
/// Call the Matrix API to change the name of this room.
2019-06-11 11:44:25 +00:00
Future<dynamic> setName(String newName) async {
2019-06-11 08:51:45 +00:00
dynamic res = await client.connection.jsonRequest(
type: HTTPType.PUT,
2019-07-26 11:32:18 +00:00
action: "/client/r0/rooms/${id}/state/m.room.name",
2019-06-09 10:16:48 +00:00
data: {"name": newName});
2019-06-11 08:51:45 +00:00
if (res is ErrorResponse) client.connection.onError.add(res);
2019-06-09 10:16:48 +00:00
return res;
}
2019-06-11 08:51:45 +00:00
/// Call the Matrix API to change the topic of this room.
2019-06-11 11:44:25 +00:00
Future<dynamic> setDescription(String newName) async {
2019-06-11 08:51:45 +00:00
dynamic res = await client.connection.jsonRequest(
type: HTTPType.PUT,
2019-07-26 11:32:18 +00:00
action: "/client/r0/rooms/${id}/state/m.room.topic",
2019-06-09 10:16:48 +00:00
data: {"topic": newName});
2019-06-11 08:51:45 +00:00
if (res is ErrorResponse) client.connection.onError.add(res);
2019-06-09 10:16:48 +00:00
return res;
}
2019-06-11 08:51:45 +00:00
/// Call the Matrix API to send a simple text message.
Future<dynamic> sendText(String message, {String txid = null}) async {
if (txid == null) txid = "txid${DateTime.now().millisecondsSinceEpoch}";
final dynamic res = await client.connection.jsonRequest(
type: HTTPType.PUT,
2019-06-12 09:46:57 +00:00
action: "/client/r0/rooms/${id}/send/m.room.message/$txid",
2019-06-09 10:16:48 +00:00
data: {"msgtype": "m.text", "body": message});
2019-06-26 14:36:34 +00:00
if (res is ErrorResponse) client.connection.onError.add(res);
return res;
}
2019-06-26 14:36:34 +00:00
Future<String> sendTextEvent(String message, {String txid = null}) async {
final String type = "m.room.message";
// Create new transaction id
2019-06-26 14:36:34 +00:00
String messageID;
final int now = DateTime.now().millisecondsSinceEpoch;
2019-06-26 14:36:34 +00:00
if (txid == null) {
messageID = "msg$now";
} else
messageID = txid;
2019-06-26 14:36:34 +00:00
// Display a *sending* event and store it.
2019-06-12 09:46:57 +00:00
EventUpdate eventUpdate =
2019-06-18 10:33:40 +00:00
EventUpdate(type: "timeline", roomID: id, eventType: type, content: {
2019-06-12 09:46:57 +00:00
"type": type,
2019-06-27 07:44:37 +00:00
"event_id": messageID,
2019-06-12 09:46:57 +00:00
"sender": client.userID,
"status": 0,
"origin_server_ts": now,
"content": {
"msgtype": "m.text",
"body": message,
}
2019-06-12 09:46:57 +00:00
});
client.connection.onEvent.add(eventUpdate);
2019-06-26 14:36:34 +00:00
await client.store?.transaction(() {
client.store.storeEventUpdate(eventUpdate);
return;
});
2019-06-26 14:36:34 +00:00
// Send the text and on success, store and display a *sent* event.
final dynamic res = await sendText(message, txid: messageID);
2019-06-26 14:36:34 +00:00
2019-06-27 07:44:37 +00:00
if (res is ErrorResponse || !(res["event_id"] is String)) {
2019-06-26 14:36:34 +00:00
// On error, set status to -1
eventUpdate.content["status"] = -1;
2019-07-23 09:09:13 +00:00
eventUpdate.content["unsigned"] = {"transaction_id": messageID};
2019-06-26 14:36:34 +00:00
client.connection.onEvent.add(eventUpdate);
await client.store?.transaction(() {
client.store.storeEventUpdate(eventUpdate);
return;
});
2019-06-12 09:46:57 +00:00
} else {
2019-06-26 14:36:34 +00:00
eventUpdate.content["status"] = 1;
2019-07-23 09:09:13 +00:00
eventUpdate.content["unsigned"] = {"transaction_id": messageID};
2019-06-27 07:44:37 +00:00
eventUpdate.content["event_id"] = res["event_id"];
2019-06-26 14:36:34 +00:00
client.connection.onEvent.add(eventUpdate);
await client.store?.transaction(() {
client.store.storeEventUpdate(eventUpdate);
return;
});
2019-06-27 07:44:37 +00:00
return res["event_id"];
}
return null;
2019-06-09 10:16:48 +00:00
}
2019-06-11 08:51:45 +00:00
/// Call the Matrix API to leave this room.
2019-06-09 10:16:48 +00:00
Future<dynamic> leave() async {
dynamic res = await client.connection.jsonRequest(
type: HTTPType.POST, action: "/client/r0/rooms/${id}/leave");
2019-06-11 08:51:45 +00:00
if (res is ErrorResponse) client.connection.onError.add(res);
2019-06-09 10:16:48 +00:00
return res;
}
2019-06-11 08:51:45 +00:00
/// Call the Matrix API to forget this room if you already left it.
2019-06-09 10:16:48 +00:00
Future<dynamic> forget() async {
2019-06-12 11:22:16 +00:00
client.store.forgetRoom(id);
dynamic res = await client.connection.jsonRequest(
type: HTTPType.POST, action: "/client/r0/rooms/${id}/forget");
2019-06-11 08:51:45 +00:00
if (res is ErrorResponse) client.connection.onError.add(res);
2019-06-09 10:16:48 +00:00
return res;
}
2019-06-11 08:51:45 +00:00
/// Call the Matrix API to kick a user from this room.
2019-06-09 10:16:48 +00:00
Future<dynamic> kick(String userID) async {
2019-06-11 08:51:45 +00:00
dynamic res = await client.connection.jsonRequest(
type: HTTPType.POST,
2019-06-11 11:44:25 +00:00
action: "/client/r0/rooms/${id}/kick",
2019-06-09 10:16:48 +00:00
data: {"user_id": userID});
2019-06-11 08:51:45 +00:00
if (res is ErrorResponse) client.connection.onError.add(res);
2019-06-09 10:16:48 +00:00
return res;
}
2019-06-11 08:51:45 +00:00
/// Call the Matrix API to ban a user from this room.
2019-06-09 10:16:48 +00:00
Future<dynamic> ban(String userID) async {
2019-06-11 08:51:45 +00:00
dynamic res = await client.connection.jsonRequest(
type: HTTPType.POST,
2019-06-11 11:44:25 +00:00
action: "/client/r0/rooms/${id}/ban",
2019-06-09 10:16:48 +00:00
data: {"user_id": userID});
2019-06-11 08:51:45 +00:00
if (res is ErrorResponse) client.connection.onError.add(res);
2019-06-09 10:16:48 +00:00
return res;
}
2019-06-11 08:51:45 +00:00
/// Call the Matrix API to unban a banned user from this room.
2019-06-09 10:16:48 +00:00
Future<dynamic> unban(String userID) async {
2019-06-11 08:51:45 +00:00
dynamic res = await client.connection.jsonRequest(
type: HTTPType.POST,
2019-06-11 11:44:25 +00:00
action: "/client/r0/rooms/${id}/unban",
2019-06-09 10:16:48 +00:00
data: {"user_id": userID});
2019-06-11 08:51:45 +00:00
if (res is ErrorResponse) client.connection.onError.add(res);
2019-06-09 10:16:48 +00:00
return res;
}
2019-06-11 11:32:14 +00:00
/// Call the Matrix API to unban a banned user from this room.
Future<dynamic> setPower(String userID, int power) async {
2019-06-11 11:44:25 +00:00
Map<String, int> powerMap = await client.store.getPowerLevels(id);
2019-06-11 11:32:14 +00:00
powerMap[userID] = power;
dynamic res = await client.connection.jsonRequest(
type: HTTPType.PUT,
2019-07-26 08:05:08 +00:00
action: "/client/r0/rooms/$id/state/m.room.power_levels",
2019-06-11 11:32:14 +00:00
data: {"users": powerMap});
if (res is ErrorResponse) client.connection.onError.add(res);
return res;
}
2019-06-11 08:51:45 +00:00
/// Call the Matrix API to invite a user to this room.
2019-06-09 10:16:48 +00:00
Future<dynamic> invite(String userID) async {
2019-06-11 08:51:45 +00:00
dynamic res = await client.connection.jsonRequest(
type: HTTPType.POST,
2019-06-11 11:44:25 +00:00
action: "/client/r0/rooms/${id}/invite",
2019-06-09 10:16:48 +00:00
data: {"user_id": userID});
2019-06-11 08:51:45 +00:00
if (res is ErrorResponse) client.connection.onError.add(res);
2019-06-09 10:16:48 +00:00
return res;
}
2019-06-11 11:44:25 +00:00
/// Request more previous events from the server.
Future<void> requestHistory({int historyCount = 100}) async {
final dynamic resp = await client.connection.jsonRequest(
type: HTTPType.GET,
2019-06-28 09:42:57 +00:00
action:
"/client/r0/rooms/$id/messages?from=${prev_batch}&dir=b&limit=$historyCount");
2019-06-11 11:44:25 +00:00
if (resp is ErrorResponse) return;
prev_batch = resp["end"];
client.store?.storeRoomPrevBatch(this);
2019-06-12 09:46:57 +00:00
if (!(resp["chunk"] is List<dynamic> &&
resp["chunk"].length > 0 &&
2019-06-11 11:44:25 +00:00
resp["end"] is String)) return;
List<dynamic> history = resp["chunk"];
client.store?.transaction(() {
2019-06-11 11:44:25 +00:00
for (int i = 0; i < history.length; i++) {
EventUpdate eventUpdate = EventUpdate(
type: "history",
2019-06-11 11:44:25 +00:00
roomID: id,
eventType: history[i]["type"],
2019-06-11 11:44:25 +00:00
content: history[i],
);
client.connection.onEvent.add(eventUpdate);
client.store.storeEventUpdate(eventUpdate);
client.store.txn.rawUpdate(
2019-06-12 09:46:57 +00:00
"UPDATE Rooms SET prev_batch=? WHERE id=?", [resp["end"], id]);
2019-06-11 11:44:25 +00:00
}
return;
2019-06-11 11:44:25 +00:00
});
if (client.store == null) {
for (int i = 0; i < history.length; i++) {
EventUpdate eventUpdate = EventUpdate(
type: "history",
roomID: id,
eventType: history[i]["type"],
content: history[i],
);
client.connection.onEvent.add(eventUpdate);
}
}
2019-06-11 11:44:25 +00:00
}
2019-06-09 10:16:48 +00:00
2019-06-26 14:39:52 +00:00
/// Sets this room as a direct chat for this user.
2019-06-12 09:46:57 +00:00
Future<dynamic> addToDirectChat(String userID) async {
Map<String, List<String>> directChats =
await client.store.getAccountDataDirectChats();
if (directChats.containsKey(userID)) if (!directChats[userID].contains(id))
directChats[userID].add(id);
else
return null; // Is already in direct chats
else
directChats[userID] = [id];
final resp = await client.connection.jsonRequest(
type: HTTPType.PUT,
2019-06-12 09:46:57 +00:00
action: "/client/r0/user/${client.userID}/account_data/m.direct",
data: directChats);
return resp;
}
2019-06-26 14:39:52 +00:00
/// Sends *m.fully_read* and *m.read* for the given event ID.
2019-06-11 12:13:30 +00:00
Future<dynamic> sendReadReceipt(String eventID) async {
final dynamic resp = client.connection.jsonRequest(
type: HTTPType.POST,
2019-06-11 12:13:30 +00:00
action: "/client/r0/rooms/$id/read_markers",
2019-06-12 09:46:57 +00:00
data: {
"m.fully_read": eventID,
"m.read": eventID,
});
2019-06-11 12:13:30 +00:00
return resp;
}
2019-06-11 11:44:25 +00:00
/// Returns a Room from a json String which comes normally from the store.
2019-06-12 09:46:57 +00:00
static Future<Room> getRoomFromTableRow(
Map<String, dynamic> row, Client matrix) async {
2019-07-23 13:03:16 +00:00
bool newHasName = false;
2019-06-11 10:21:45 +00:00
String name = row["topic"];
2019-07-23 09:09:13 +00:00
if (name == "" && !row["canonical_alias"].isEmpty)
name = row["canonical_alias"];
else if (name == "")
2019-06-11 11:44:25 +00:00
name = await matrix.store?.getChatNameFromMemberNames(row["id"]) ?? "";
2019-07-23 13:03:16 +00:00
else
newHasName = true;
2019-06-09 10:16:48 +00:00
2019-06-12 06:13:04 +00:00
String avatarUrl = row["avatar_url"];
if (avatarUrl == "")
avatarUrl = await matrix.store?.getAvatarFromSingleChat(row["id"]) ?? "";
2019-06-09 10:16:48 +00:00
return Room(
2019-06-11 08:51:45 +00:00
id: row["id"],
2019-06-09 10:16:48 +00:00
name: name,
2019-07-23 13:03:16 +00:00
hasName: newHasName,
membership: Membership.values
.firstWhere((e) => e.toString() == 'Membership.' + row["membership"]),
2019-06-11 10:21:45 +00:00
topic: row["description"],
2019-06-12 07:04:04 +00:00
avatar: MxContent(avatarUrl),
2019-06-09 10:16:48 +00:00
notificationCount: row["notification_count"],
highlightCount: row["highlight_count"],
2019-06-11 10:21:45 +00:00
unread: ChatTime(row["unread"]),
fullyRead: row["fully_read"],
notificationSettings: row["notification_settings"],
directChatMatrixID: row["direct_chat_matrix_id"],
draft: row["draft"],
prev_batch: row["prev_batch"],
guestAccess: row["guest_access"],
historyVisibility: row["history_visibility"],
joinRules: row["join_rules"],
2019-07-23 09:09:13 +00:00
canonicalAlias: row["canonical_alias"],
2019-06-11 10:21:45 +00:00
powerLevels: {
"power_events_default": row["power_events_default"],
"power_state_default": row["power_state_default"],
"power_redact": row["power_redact"],
"power_invite": row["power_invite"],
"power_ban": row["power_ban"],
"power_kick": row["power_kick"],
"power_user_default": row["power_user_default"],
"power_event_avatar": row["power_event_avatar"],
"power_event_history_visibility": row["power_event_history_visibility"],
"power_event_canonical_alias": row["power_event_canonical_alias"],
"power_event_aliases": row["power_event_aliases"],
"power_event_name": row["power_event_name"],
"power_event_power_levels": row["power_event_power_levels"],
},
2019-06-21 10:18:54 +00:00
lastEvent: Event.fromJson(row, null),
2019-06-11 08:51:45 +00:00
client: matrix,
2019-06-09 10:16:48 +00:00
);
}
2019-06-11 08:51:45 +00:00
@Deprecated("Use client.store.getRoomById(String id) instead!")
2019-06-09 10:16:48 +00:00
static Future<Room> getRoomById(String id, Client matrix) async {
2019-06-11 08:51:45 +00:00
Room room = await matrix.store.getRoomById(id);
return room;
2019-06-09 10:16:48 +00:00
}
2019-06-11 08:51:45 +00:00
/// Load a room from the store including all room events.
2019-06-09 10:16:48 +00:00
static Future<Room> loadRoomEvents(String id, Client matrix) async {
2019-06-11 11:44:25 +00:00
Room room = await matrix.store.getRoomById(id);
await room.loadEvents();
return room;
2019-06-09 10:16:48 +00:00
}
2019-06-26 14:39:52 +00:00
/// Creates a timeline from the store. Returns a [Timeline] object.
2019-06-25 10:06:26 +00:00
Future<Timeline> getTimeline(
{onTimelineUpdateCallback onUpdate,
onTimelineInsertCallback onInsert}) async {
2019-06-21 10:18:54 +00:00
List<Event> events = await loadEvents();
return Timeline(
room: this,
events: events,
onUpdate: onUpdate,
onInsert: onInsert,
);
}
2019-06-11 08:51:45 +00:00
/// Load all events for a given room from the store. This includes all
/// senders of those events, who will be added to the participants list.
Future<List<Event>> loadEvents() async {
2019-06-21 10:18:54 +00:00
return await client.store.getEventList(this);
2019-06-11 08:51:45 +00:00
}
/// Load all participants for a given room from the store.
Future<List<User>> loadParticipants() async {
2019-06-21 10:18:54 +00:00
return await client.store.loadParticipants(this);
2019-06-11 08:51:45 +00:00
}
/// Request the full list of participants from the server. The local list
/// from the store is not complete if the client uses lazy loading.
2019-06-18 10:06:55 +00:00
Future<List<User>> requestParticipants() async {
2019-06-09 10:16:48 +00:00
List<User> participants = [];
dynamic res = await client.connection.jsonRequest(
type: HTTPType.GET, action: "/client/r0/rooms/${id}/members");
2019-06-09 10:16:48 +00:00
if (res is ErrorResponse || !(res["chunk"] is List<dynamic>))
return participants;
for (num i = 0; i < res["chunk"].length; i++) {
User newUser = User(res["chunk"][i]["state_key"],
displayName: res["chunk"][i]["content"]["displayname"] ?? "",
membership: Membership.values.firstWhere((e) =>
e.toString() ==
'Membership.' + res["chunk"][i]["content"]["membership"] ??
""),
2019-06-11 11:44:25 +00:00
avatarUrl: MxContent(res["chunk"][i]["content"]["avatar_url"] ?? ""),
2019-06-11 09:13:14 +00:00
room: this);
if (newUser.membership != Membership.leave) participants.add(newUser);
2019-06-09 10:16:48 +00:00
}
2019-06-21 10:18:54 +00:00
return participants;
2019-06-09 10:16:48 +00:00
}
/// Searches for the event in the store. If it isn't found, try to request it
/// from the server. Returns null if not found.
Future<Event> getEventById(String eventID) async {
if (client.store != null) {
final Event storeEvent = await client.store.getEventById(eventID, this);
if (storeEvent != null) return storeEvent;
}
final dynamic resp = await client.connection.jsonRequest(
type: HTTPType.GET, action: "/client/r0/rooms/$id/event/$eventID");
if (resp is ErrorResponse) return null;
return Event.fromJson(resp, this,
senderUser:
(await client.store.getUser(matrixID: resp["sender"], room: this)));
}
2019-06-09 10:16:48 +00:00
}