errbot-backend-matrix 0.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- err_backend_matrix/__init__.py +4 -0
- err_backend_matrix/err_backend_matrix.plug +9 -0
- err_backend_matrix/matrix.py +344 -0
- err_backend_matrix/person.py +81 -0
- err_backend_matrix/room.py +147 -0
- errbot_backend_matrix-0.0.1.dist-info/METADATA +126 -0
- errbot_backend_matrix-0.0.1.dist-info/RECORD +11 -0
- errbot_backend_matrix-0.0.1.dist-info/WHEEL +5 -0
- errbot_backend_matrix-0.0.1.dist-info/entry_points.txt +2 -0
- errbot_backend_matrix-0.0.1.dist-info/licenses/LICENSE +674 -0
- errbot_backend_matrix-0.0.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import logging
|
|
3
|
+
from typing import Sequence
|
|
4
|
+
|
|
5
|
+
from errbot.backends.base import AWAY, DND, OFFLINE, ONLINE, Identifier, Message, Presence
|
|
6
|
+
from errbot.core import ErrBot
|
|
7
|
+
from errbot.rendering import text, xhtml
|
|
8
|
+
from nio import (
|
|
9
|
+
AsyncClient,
|
|
10
|
+
InviteMemberEvent,
|
|
11
|
+
LoginError,
|
|
12
|
+
MegolmEvent,
|
|
13
|
+
RoomCreateResponse,
|
|
14
|
+
RoomEncryptionEvent,
|
|
15
|
+
RoomMemberEvent,
|
|
16
|
+
RoomMessageText,
|
|
17
|
+
RoomPreset,
|
|
18
|
+
RoomResolveAliasResponse,
|
|
19
|
+
RoomSendResponse,
|
|
20
|
+
RoomTopicEvent,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
from err_backend_matrix.person import MatrixPerson, MatrixRoomOccupant
|
|
24
|
+
from err_backend_matrix.room import MatrixRoom
|
|
25
|
+
|
|
26
|
+
log = logging.getLogger(__name__)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class MatrixBackend(ErrBot):
|
|
30
|
+
"""A fully functional Errbot backend for Matrix using matrix-nio."""
|
|
31
|
+
|
|
32
|
+
def __init__(self, config):
|
|
33
|
+
super().__init__(config)
|
|
34
|
+
self.client = None
|
|
35
|
+
self.loop = None
|
|
36
|
+
self._dm_rooms = {}
|
|
37
|
+
|
|
38
|
+
# Markdown renderers
|
|
39
|
+
self.md_text = text()
|
|
40
|
+
self.md_xhtml = xhtml()
|
|
41
|
+
|
|
42
|
+
# Bot identity configuration
|
|
43
|
+
self.homeserver = self.bot_config.BOT_IDENTITY.get("homeserver", "https://matrix.org")
|
|
44
|
+
self.mxid = self.bot_config.BOT_IDENTITY.get("username")
|
|
45
|
+
self.password = self.bot_config.BOT_IDENTITY.get("password")
|
|
46
|
+
self.token = self.bot_config.BOT_IDENTITY.get("token")
|
|
47
|
+
self.device_id = self.bot_config.BOT_IDENTITY.get("device_id")
|
|
48
|
+
self.store_path = self.bot_config.BOT_IDENTITY.get("store_path")
|
|
49
|
+
self.ssl = self.bot_config.BOT_IDENTITY.get("ssl", True)
|
|
50
|
+
self.proxy = self.bot_config.BOT_IDENTITY.get("proxy")
|
|
51
|
+
|
|
52
|
+
def run_async_coroutine(self, coro):
|
|
53
|
+
"""Helper to run a coroutine in the backend event loop."""
|
|
54
|
+
if self.loop and self.loop.is_running():
|
|
55
|
+
return asyncio.run_coroutine_threadsafe(coro, self.loop)
|
|
56
|
+
else:
|
|
57
|
+
new_loop = asyncio.new_event_loop()
|
|
58
|
+
try:
|
|
59
|
+
return new_loop.run_until_complete(coro)
|
|
60
|
+
finally:
|
|
61
|
+
new_loop.close()
|
|
62
|
+
|
|
63
|
+
def run_async_coroutine_sync(self, coro):
|
|
64
|
+
"""Helper to run a coroutine in the backend loop and block for the result."""
|
|
65
|
+
if self.loop and self.loop.is_running():
|
|
66
|
+
future = asyncio.run_coroutine_threadsafe(coro, self.loop)
|
|
67
|
+
return future.result()
|
|
68
|
+
else:
|
|
69
|
+
new_loop = asyncio.new_event_loop()
|
|
70
|
+
try:
|
|
71
|
+
return new_loop.run_until_complete(coro)
|
|
72
|
+
finally:
|
|
73
|
+
new_loop.close()
|
|
74
|
+
|
|
75
|
+
def serve_once(self) -> bool:
|
|
76
|
+
log.info("Initializing Matrix backend connection...")
|
|
77
|
+
self.loop = asyncio.new_event_loop()
|
|
78
|
+
asyncio.set_event_loop(self.loop)
|
|
79
|
+
|
|
80
|
+
self.client = AsyncClient(
|
|
81
|
+
homeserver=self.homeserver,
|
|
82
|
+
user=self.mxid,
|
|
83
|
+
device_id=self.device_id,
|
|
84
|
+
store_path=self.store_path,
|
|
85
|
+
ssl=self.ssl,
|
|
86
|
+
proxy=self.proxy,
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
try:
|
|
90
|
+
self.loop.run_until_complete(self.async_run())
|
|
91
|
+
except Exception:
|
|
92
|
+
log.exception("Matrix connection crashed:")
|
|
93
|
+
return False
|
|
94
|
+
finally:
|
|
95
|
+
log.info("Closing Matrix client and loop...")
|
|
96
|
+
self.loop.run_until_complete(self.client.close())
|
|
97
|
+
self.loop.close()
|
|
98
|
+
self.loop = None
|
|
99
|
+
return True
|
|
100
|
+
|
|
101
|
+
async def async_run(self):
|
|
102
|
+
log.info("Logging into Matrix...")
|
|
103
|
+
if self.token:
|
|
104
|
+
self.client.access_token = self.token
|
|
105
|
+
self.client.user_id = self.mxid
|
|
106
|
+
# Verify token
|
|
107
|
+
await self.client.whoami()
|
|
108
|
+
elif self.password:
|
|
109
|
+
res = await self.client.login(self.password)
|
|
110
|
+
if isinstance(res, LoginError):
|
|
111
|
+
raise Exception(f"Failed to log in to Matrix: {res.message}")
|
|
112
|
+
else:
|
|
113
|
+
raise Exception("No Matrix password or token configured in BOT_IDENTITY.")
|
|
114
|
+
|
|
115
|
+
log.info("Matrix login successful.")
|
|
116
|
+
self.bot_identifier = MatrixPerson(self.client.user_id)
|
|
117
|
+
|
|
118
|
+
# Register event callbacks
|
|
119
|
+
self.client.add_event_callback(self.on_room_message, RoomMessageText)
|
|
120
|
+
self.client.add_event_callback(self.on_room_member, RoomMemberEvent)
|
|
121
|
+
self.client.add_event_callback(self.on_room_topic, RoomTopicEvent)
|
|
122
|
+
self.client.add_event_callback(self.on_invite, InviteMemberEvent)
|
|
123
|
+
self.client.add_event_callback(self.on_megolm_event, MegolmEvent)
|
|
124
|
+
self.client.add_event_callback(self.on_room_encryption, RoomEncryptionEvent)
|
|
125
|
+
|
|
126
|
+
# Trigger Errbot connect callback
|
|
127
|
+
self.connect_callback()
|
|
128
|
+
|
|
129
|
+
# Initial sync to build rooms lists and fetch state
|
|
130
|
+
log.info("Syncing with homeserver...")
|
|
131
|
+
await self.client.sync(timeout=30000)
|
|
132
|
+
log.info("Initial sync complete. Ready for events.")
|
|
133
|
+
|
|
134
|
+
# Start listening for incoming events forever
|
|
135
|
+
await self.client.sync_forever(timeout=30000)
|
|
136
|
+
|
|
137
|
+
def on_room_message(self, room, event: RoomMessageText):
|
|
138
|
+
# Skip self-sent messages
|
|
139
|
+
if event.sender == self.client.user_id:
|
|
140
|
+
return
|
|
141
|
+
|
|
142
|
+
msg = Message(body=event.body)
|
|
143
|
+
msg.extras["event_id"] = event.event_id
|
|
144
|
+
|
|
145
|
+
# Look up display name from room user cache
|
|
146
|
+
sender_user = room.users.get(event.sender)
|
|
147
|
+
display_name = sender_user.name if sender_user else None
|
|
148
|
+
|
|
149
|
+
# Check if room is DM or MUC
|
|
150
|
+
is_dm = len(room.users) <= 2 and not room.canonical_alias
|
|
151
|
+
|
|
152
|
+
if is_dm:
|
|
153
|
+
msg.frm = MatrixPerson(event.sender, displayname=display_name)
|
|
154
|
+
msg.to = self.bot_identifier
|
|
155
|
+
else:
|
|
156
|
+
room_obj = MatrixRoom(room.room_id, self)
|
|
157
|
+
msg.frm = MatrixRoomOccupant(event.sender, room_obj, displayname=display_name)
|
|
158
|
+
msg.to = room_obj
|
|
159
|
+
|
|
160
|
+
# Extract mentions
|
|
161
|
+
mentioned = []
|
|
162
|
+
for word in event.body.split():
|
|
163
|
+
if word.startswith("@") and ":" in word:
|
|
164
|
+
clean_word = word.rstrip(".,:;!?")
|
|
165
|
+
try:
|
|
166
|
+
mentioned.append(self.build_identifier(clean_word))
|
|
167
|
+
except Exception:
|
|
168
|
+
log.debug(f"Failed to build identifier for clean_word: {clean_word}", exc_info=True)
|
|
169
|
+
|
|
170
|
+
# Trigger message callback
|
|
171
|
+
self.callback_message(msg)
|
|
172
|
+
if mentioned:
|
|
173
|
+
self.callback_mention(msg, mentioned)
|
|
174
|
+
|
|
175
|
+
def on_room_member(self, room, event: RoomMemberEvent):
|
|
176
|
+
if event.state_key == self.client.user_id:
|
|
177
|
+
# Self membership change
|
|
178
|
+
room_obj = MatrixRoom(room.room_id, self)
|
|
179
|
+
if event.membership == "join":
|
|
180
|
+
self.callback_room_joined(room_obj)
|
|
181
|
+
elif event.membership in ("leave", "ban"):
|
|
182
|
+
self.callback_room_left(room_obj)
|
|
183
|
+
else:
|
|
184
|
+
# Other membership changes mapping to presence
|
|
185
|
+
status = ONLINE if event.membership == "join" else OFFLINE
|
|
186
|
+
room_obj = MatrixRoom(room.room_id, self)
|
|
187
|
+
user_obj = MatrixRoomOccupant(event.state_key, room_obj)
|
|
188
|
+
presence = Presence(identifier=user_obj, status=status)
|
|
189
|
+
self.callback_presence(presence)
|
|
190
|
+
|
|
191
|
+
def on_room_topic(self, room, event: RoomTopicEvent):
|
|
192
|
+
room_obj = MatrixRoom(room.room_id, self)
|
|
193
|
+
self.callback_room_topic(room_obj)
|
|
194
|
+
|
|
195
|
+
def on_invite(self, room, event: InviteMemberEvent):
|
|
196
|
+
auto_join = self.bot_config.BOT_IDENTITY.get("auto_join_on_invite", True)
|
|
197
|
+
if auto_join:
|
|
198
|
+
log.info(f"Automatically joining room {room.room_id} after invite from {event.sender}")
|
|
199
|
+
self.run_async_coroutine(self.client.join(room.room_id))
|
|
200
|
+
|
|
201
|
+
def on_megolm_event(self, room, event: MegolmEvent):
|
|
202
|
+
log.warning(
|
|
203
|
+
f"Received encrypted MegolmEvent in room {room.room_id} that could not be decrypted. Session ID: {event.session_id}"
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
def on_room_encryption(self, room, event: RoomEncryptionEvent):
|
|
207
|
+
log.info(f"Room {room.room_id} is now encrypted using {event.algorithm}")
|
|
208
|
+
|
|
209
|
+
def send_message(self, msg: Message) -> None:
|
|
210
|
+
super().send_message(msg)
|
|
211
|
+
self.run_async_coroutine(self._async_send_message(msg))
|
|
212
|
+
|
|
213
|
+
async def _async_send_message(self, msg: Message):
|
|
214
|
+
if not self.client:
|
|
215
|
+
log.error("Matrix client not initialized; cannot send message.")
|
|
216
|
+
return
|
|
217
|
+
|
|
218
|
+
to_id = None
|
|
219
|
+
if msg.is_group:
|
|
220
|
+
room_str = str(msg.to)
|
|
221
|
+
if room_str.startswith("#"):
|
|
222
|
+
try:
|
|
223
|
+
res = await self.client.room_resolve_alias(room_str)
|
|
224
|
+
if isinstance(res, RoomResolveAliasResponse):
|
|
225
|
+
to_id = res.room_id
|
|
226
|
+
else:
|
|
227
|
+
to_id = room_str
|
|
228
|
+
except Exception:
|
|
229
|
+
to_id = room_str
|
|
230
|
+
else:
|
|
231
|
+
to_id = room_str
|
|
232
|
+
else:
|
|
233
|
+
# Resolving DM room
|
|
234
|
+
user_id = msg.to.userid
|
|
235
|
+
to_id = self._dm_rooms.get(user_id)
|
|
236
|
+
if not to_id:
|
|
237
|
+
# Scan rooms list
|
|
238
|
+
for r_id, r in self.client.rooms.items():
|
|
239
|
+
if len(r.users) <= 2 and user_id in r.users:
|
|
240
|
+
to_id = r_id
|
|
241
|
+
self._dm_rooms[user_id] = to_id
|
|
242
|
+
break
|
|
243
|
+
|
|
244
|
+
if not to_id:
|
|
245
|
+
# Create DM
|
|
246
|
+
log.info(f"Creating DM room with {user_id}")
|
|
247
|
+
res = await self.client.room_create(
|
|
248
|
+
is_direct=True, invite=[user_id], preset=RoomPreset.private_chat
|
|
249
|
+
)
|
|
250
|
+
if isinstance(res, RoomCreateResponse):
|
|
251
|
+
to_id = res.room_id
|
|
252
|
+
self._dm_rooms[user_id] = to_id
|
|
253
|
+
log.info(f"Created DM room {to_id} with {user_id}")
|
|
254
|
+
else:
|
|
255
|
+
log.error(f"Failed to create DM room with {user_id}: {res}")
|
|
256
|
+
return
|
|
257
|
+
|
|
258
|
+
if not to_id:
|
|
259
|
+
log.error("Could not resolve recipient ID.")
|
|
260
|
+
return
|
|
261
|
+
|
|
262
|
+
# Markdown body conversion
|
|
263
|
+
body_text = self.md_text.convert(msg.body)
|
|
264
|
+
body_html = self.md_xhtml.convert(msg.body)
|
|
265
|
+
|
|
266
|
+
content = {
|
|
267
|
+
"msgtype": "m.text",
|
|
268
|
+
"body": body_text,
|
|
269
|
+
"format": "org.matrix.custom.html",
|
|
270
|
+
"formatted_body": body_html,
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
# Rich thread/replies relations
|
|
274
|
+
if msg.parent and "event_id" in msg.parent.extras:
|
|
275
|
+
content["m.relates_to"] = {"m.in_reply_to": {"event_id": msg.parent.extras["event_id"]}}
|
|
276
|
+
|
|
277
|
+
try:
|
|
278
|
+
res = await self.client.room_send(room_id=to_id, message_type="m.room.message", content=content)
|
|
279
|
+
if isinstance(res, RoomSendResponse):
|
|
280
|
+
msg.extras["event_id"] = res.event_id
|
|
281
|
+
except Exception:
|
|
282
|
+
log.exception(f"Exception sending message to {to_id}:")
|
|
283
|
+
|
|
284
|
+
def change_presence(self, status: str = ONLINE, message: str = "") -> None:
|
|
285
|
+
if not self.client:
|
|
286
|
+
return
|
|
287
|
+
|
|
288
|
+
matrix_presence = "online"
|
|
289
|
+
if status == OFFLINE:
|
|
290
|
+
matrix_presence = "offline"
|
|
291
|
+
elif status in (AWAY, DND):
|
|
292
|
+
matrix_presence = "unavailable"
|
|
293
|
+
|
|
294
|
+
self.run_async_coroutine(self.client.set_presence(matrix_presence))
|
|
295
|
+
|
|
296
|
+
def build_identifier(self, text_representation: str) -> Identifier:
|
|
297
|
+
if text_representation.startswith("!") or text_representation.startswith("#"):
|
|
298
|
+
return MatrixRoom(text_representation, self)
|
|
299
|
+
else:
|
|
300
|
+
return MatrixPerson(text_representation)
|
|
301
|
+
|
|
302
|
+
def is_from_self(self, msg: Message) -> bool:
|
|
303
|
+
return msg.frm.person == self.client.user_id
|
|
304
|
+
|
|
305
|
+
def build_reply(self, msg: Message, text=None, private: bool = False, threaded: bool = False) -> Message:
|
|
306
|
+
reply = self.build_message(text or "")
|
|
307
|
+
|
|
308
|
+
if private:
|
|
309
|
+
if isinstance(msg.frm, MatrixRoomOccupant):
|
|
310
|
+
reply.to = MatrixPerson(msg.frm.userid)
|
|
311
|
+
else:
|
|
312
|
+
reply.to = msg.frm
|
|
313
|
+
else:
|
|
314
|
+
if msg.is_group:
|
|
315
|
+
reply.to = msg.to
|
|
316
|
+
else:
|
|
317
|
+
reply.to = msg.frm
|
|
318
|
+
|
|
319
|
+
if threaded:
|
|
320
|
+
reply.parent = msg
|
|
321
|
+
|
|
322
|
+
return reply
|
|
323
|
+
|
|
324
|
+
def prefix_groupchat_reply(self, message: Message, identifier: Identifier):
|
|
325
|
+
message.body = f"@{identifier.nick}: {message.body}"
|
|
326
|
+
|
|
327
|
+
def shutdown(self) -> None:
|
|
328
|
+
log.info("Shutting down Matrix backend...")
|
|
329
|
+
if self.client:
|
|
330
|
+
self.client.stop_sync_forever()
|
|
331
|
+
super().shutdown()
|
|
332
|
+
|
|
333
|
+
@property
|
|
334
|
+
def mode(self) -> str:
|
|
335
|
+
return "matrix"
|
|
336
|
+
|
|
337
|
+
@property
|
|
338
|
+
def rooms(self) -> Sequence[MatrixRoom]:
|
|
339
|
+
if not self.client:
|
|
340
|
+
return []
|
|
341
|
+
return [MatrixRoom(room_id, self) for room_id in self.client.rooms]
|
|
342
|
+
|
|
343
|
+
def query_room(self, room: str) -> MatrixRoom:
|
|
344
|
+
return MatrixRoom(room, self)
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
from errbot.backends.base import Person, RoomOccupant
|
|
4
|
+
|
|
5
|
+
log = logging.getLogger(__name__)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class MatrixPerson(Person):
|
|
9
|
+
"""This class describes a person on the Matrix network."""
|
|
10
|
+
|
|
11
|
+
def __init__(self, userid: str, displayname: str = None):
|
|
12
|
+
if not userid.startswith("@") or ":" not in userid:
|
|
13
|
+
log.warning(f"Matrix user ID format might be invalid: {userid}")
|
|
14
|
+
|
|
15
|
+
self._userid = userid
|
|
16
|
+
self._displayname = displayname
|
|
17
|
+
|
|
18
|
+
@property
|
|
19
|
+
def userid(self) -> str:
|
|
20
|
+
return self._userid
|
|
21
|
+
|
|
22
|
+
@property
|
|
23
|
+
def username(self) -> str:
|
|
24
|
+
if self._displayname:
|
|
25
|
+
return self._displayname
|
|
26
|
+
if self._userid.startswith("@") and ":" in self._userid:
|
|
27
|
+
return self._userid[1:].split(":")[0]
|
|
28
|
+
return self._userid
|
|
29
|
+
|
|
30
|
+
@property
|
|
31
|
+
def fullname(self) -> str:
|
|
32
|
+
return self._displayname or self.username
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
def aclattr(self) -> str:
|
|
36
|
+
return self._userid
|
|
37
|
+
|
|
38
|
+
# Errbot Person expectations
|
|
39
|
+
person = aclattr
|
|
40
|
+
nick = username
|
|
41
|
+
client = userid
|
|
42
|
+
|
|
43
|
+
def __str__(self) -> str:
|
|
44
|
+
return self._userid
|
|
45
|
+
|
|
46
|
+
def __unicode__(self) -> str:
|
|
47
|
+
return self._userid
|
|
48
|
+
|
|
49
|
+
def __eq__(self, other) -> bool:
|
|
50
|
+
if not isinstance(other, MatrixPerson):
|
|
51
|
+
return False
|
|
52
|
+
return self._userid == other._userid
|
|
53
|
+
|
|
54
|
+
def __hash__(self) -> int:
|
|
55
|
+
return hash(self._userid)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class MatrixRoomOccupant(MatrixPerson, RoomOccupant):
|
|
59
|
+
"""This class represents a person inside a Matrix Room."""
|
|
60
|
+
|
|
61
|
+
def __init__(self, userid: str, room, displayname: str = None):
|
|
62
|
+
super().__init__(userid, displayname=displayname)
|
|
63
|
+
self._room = room
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
def room(self):
|
|
67
|
+
return self._room
|
|
68
|
+
|
|
69
|
+
def __str__(self) -> str:
|
|
70
|
+
return f"{self._room}/{self.username}"
|
|
71
|
+
|
|
72
|
+
def __unicode__(self) -> str:
|
|
73
|
+
return f"{self._room}/{self.username}"
|
|
74
|
+
|
|
75
|
+
def __eq__(self, other) -> bool:
|
|
76
|
+
if not isinstance(other, MatrixRoomOccupant):
|
|
77
|
+
return False
|
|
78
|
+
return self.room == other.room and self.userid == other.userid
|
|
79
|
+
|
|
80
|
+
def __hash__(self) -> int:
|
|
81
|
+
return hash((self.userid, self.room))
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
from errbot.backends.base import Room, RoomError
|
|
4
|
+
|
|
5
|
+
log = logging.getLogger(__name__)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class MatrixRoom(Room):
|
|
9
|
+
"""This class represents a Multi-User Chatroom on Matrix."""
|
|
10
|
+
|
|
11
|
+
def __init__(self, room_id_or_alias: str, backend=None):
|
|
12
|
+
self._id_or_alias = room_id_or_alias
|
|
13
|
+
self._backend = backend
|
|
14
|
+
|
|
15
|
+
def __str__(self) -> str:
|
|
16
|
+
return self._id_or_alias
|
|
17
|
+
|
|
18
|
+
@property
|
|
19
|
+
def id(self) -> str:
|
|
20
|
+
return self._id_or_alias
|
|
21
|
+
|
|
22
|
+
@property
|
|
23
|
+
def aclattr(self) -> str:
|
|
24
|
+
return self._id_or_alias
|
|
25
|
+
|
|
26
|
+
@property
|
|
27
|
+
def name(self) -> str:
|
|
28
|
+
if self._backend is not None and self._backend.client is not None:
|
|
29
|
+
resolved_id = self.resolve_room_id()
|
|
30
|
+
room = self._backend.client.rooms.get(resolved_id)
|
|
31
|
+
if room and room.name:
|
|
32
|
+
return room.name
|
|
33
|
+
return self._id_or_alias
|
|
34
|
+
|
|
35
|
+
@property
|
|
36
|
+
def exists(self) -> bool:
|
|
37
|
+
if self._backend is not None and self._backend.client is not None:
|
|
38
|
+
resolved_id = self.resolve_room_id()
|
|
39
|
+
return resolved_id in self._backend.client.rooms
|
|
40
|
+
return False
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
def joined(self) -> bool:
|
|
44
|
+
if self._backend is not None and self._backend.client is not None:
|
|
45
|
+
resolved_id = self.resolve_room_id()
|
|
46
|
+
return resolved_id in self._backend.client.rooms
|
|
47
|
+
return False
|
|
48
|
+
|
|
49
|
+
def join(self, username: str = None, password: str = None) -> None:
|
|
50
|
+
if self._backend is None:
|
|
51
|
+
raise RoomError("No backend associated with room")
|
|
52
|
+
self._backend.run_async_coroutine_sync(self._backend.client.join(self._id_or_alias))
|
|
53
|
+
|
|
54
|
+
def leave(self, reason: str = None) -> None:
|
|
55
|
+
if self._backend is None:
|
|
56
|
+
raise RoomError("No backend associated with room")
|
|
57
|
+
room_id = self.resolve_room_id()
|
|
58
|
+
if not room_id:
|
|
59
|
+
raise RoomError("Could not resolve room ID to leave")
|
|
60
|
+
self._backend.run_async_coroutine_sync(self._backend.client.room_leave(room_id))
|
|
61
|
+
|
|
62
|
+
def create(self) -> None:
|
|
63
|
+
if self._backend is None:
|
|
64
|
+
raise RoomError("No backend associated with room")
|
|
65
|
+
alias_localpart = None
|
|
66
|
+
if self._id_or_alias.startswith("#") and ":" in self._id_or_alias:
|
|
67
|
+
alias_localpart = self._id_or_alias[1:].split(":")[0]
|
|
68
|
+
|
|
69
|
+
self._backend.run_async_coroutine_sync(
|
|
70
|
+
self._backend.client.room_create(alias_localpart=alias_localpart, name=alias_localpart)
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
def destroy(self) -> None:
|
|
74
|
+
room_id = self.resolve_room_id()
|
|
75
|
+
if room_id:
|
|
76
|
+
self._backend.run_async_coroutine_sync(self._backend.client.room_leave(room_id))
|
|
77
|
+
self._backend.run_async_coroutine_sync(self._backend.client.room_forget(room_id))
|
|
78
|
+
|
|
79
|
+
@property
|
|
80
|
+
def topic(self) -> str:
|
|
81
|
+
if self._backend is not None and self._backend.client is not None:
|
|
82
|
+
room = self._backend.client.rooms.get(self.resolve_room_id())
|
|
83
|
+
if room:
|
|
84
|
+
return room.topic or ""
|
|
85
|
+
return ""
|
|
86
|
+
|
|
87
|
+
@topic.setter
|
|
88
|
+
def topic(self, topic: str) -> None:
|
|
89
|
+
if self._backend is None:
|
|
90
|
+
raise RoomError("No backend associated with room")
|
|
91
|
+
room_id = self.resolve_room_id()
|
|
92
|
+
if not room_id:
|
|
93
|
+
raise RoomError("Could not resolve room ID to set topic")
|
|
94
|
+
self._backend.run_async_coroutine_sync(
|
|
95
|
+
self._backend.client.room_put_state(room_id=room_id, event_type="m.room.topic", content={"topic": topic})
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
@property
|
|
99
|
+
def occupants(self) -> list:
|
|
100
|
+
if self._backend is None or self._backend.client is None:
|
|
101
|
+
return []
|
|
102
|
+
room_id = self.resolve_room_id()
|
|
103
|
+
if not room_id:
|
|
104
|
+
return []
|
|
105
|
+
room = self._backend.client.rooms.get(room_id)
|
|
106
|
+
if not room:
|
|
107
|
+
return []
|
|
108
|
+
|
|
109
|
+
from .person import MatrixRoomOccupant
|
|
110
|
+
|
|
111
|
+
return [MatrixRoomOccupant(user_id, self, displayname=user.name) for user_id, user in room.users.items()]
|
|
112
|
+
|
|
113
|
+
def invite(self, *args) -> None:
|
|
114
|
+
if self._backend is None:
|
|
115
|
+
raise RoomError("No backend associated with room")
|
|
116
|
+
room_id = self.resolve_room_id()
|
|
117
|
+
if not room_id:
|
|
118
|
+
raise RoomError("Could not resolve room ID to invite users")
|
|
119
|
+
for user in args:
|
|
120
|
+
user_id = str(user)
|
|
121
|
+
self._backend.run_async_coroutine_sync(self._backend.client.room_invite(room_id, user_id))
|
|
122
|
+
|
|
123
|
+
def resolve_room_id(self) -> str:
|
|
124
|
+
if self._id_or_alias.startswith("!"):
|
|
125
|
+
return self._id_or_alias
|
|
126
|
+
if self._backend is not None and self._backend.client is not None:
|
|
127
|
+
# Check standard room list
|
|
128
|
+
for room_id, room in self._backend.client.rooms.items():
|
|
129
|
+
if room.canonical_alias == self._id_or_alias:
|
|
130
|
+
return room_id
|
|
131
|
+
# Fetch resolution via API
|
|
132
|
+
try:
|
|
133
|
+
res = self._backend.run_async_coroutine_sync(self._backend.client.room_resolve_alias(self._id_or_alias))
|
|
134
|
+
# In matrix-nio, RoomResolveAliasResponse has room_id
|
|
135
|
+
if res and hasattr(res, "room_id"):
|
|
136
|
+
return res.room_id
|
|
137
|
+
except Exception:
|
|
138
|
+
log.debug(f"Failed to resolve room alias: {self._id_or_alias}", exc_info=True)
|
|
139
|
+
return self._id_or_alias
|
|
140
|
+
|
|
141
|
+
def __eq__(self, other) -> bool:
|
|
142
|
+
if not isinstance(other, MatrixRoom):
|
|
143
|
+
return False
|
|
144
|
+
return self.resolve_room_id() == other.resolve_room_id()
|
|
145
|
+
|
|
146
|
+
def __hash__(self) -> int:
|
|
147
|
+
return hash(self.resolve_room_id())
|