slidge 0.1.0b2__py3-none-any.whl → 0.1.1__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
Files changed (155) hide show
  1. slidge/__init__.py +55 -31
  2. slidge/__main__.py +118 -116
  3. slidge/command/__init__.py +28 -0
  4. slidge/command/adhoc.py +258 -0
  5. slidge/command/admin.py +193 -0
  6. slidge/command/base.py +441 -0
  7. slidge/command/categories.py +3 -0
  8. slidge/command/chat_command.py +288 -0
  9. slidge/command/register.py +179 -0
  10. slidge/command/user.py +250 -0
  11. slidge/contact/__init__.py +8 -0
  12. slidge/contact/contact.py +452 -0
  13. slidge/contact/roster.py +192 -0
  14. slidge/core/__init__.py +2 -0
  15. slidge/core/cache.py +183 -0
  16. slidge/core/config.py +216 -0
  17. slidge/core/gateway/__init__.py +3 -0
  18. slidge/core/gateway/base.py +895 -0
  19. slidge/core/gateway/caps.py +63 -0
  20. slidge/core/gateway/delivery_receipt.py +52 -0
  21. slidge/core/gateway/disco.py +80 -0
  22. slidge/core/gateway/mam.py +75 -0
  23. slidge/core/gateway/muc_admin.py +35 -0
  24. slidge/core/gateway/ping.py +66 -0
  25. slidge/core/gateway/presence.py +95 -0
  26. slidge/core/gateway/registration.py +53 -0
  27. slidge/core/gateway/search.py +102 -0
  28. slidge/core/gateway/session_dispatcher.py +789 -0
  29. slidge/core/gateway/vcard_temp.py +130 -0
  30. slidge/core/mixins/__init__.py +19 -0
  31. slidge/core/mixins/attachment.py +506 -0
  32. slidge/core/mixins/avatar.py +167 -0
  33. slidge/core/mixins/base.py +31 -0
  34. slidge/core/mixins/disco.py +130 -0
  35. slidge/core/mixins/lock.py +31 -0
  36. slidge/core/mixins/message.py +398 -0
  37. slidge/core/mixins/message_maker.py +154 -0
  38. slidge/core/mixins/presence.py +217 -0
  39. slidge/core/mixins/recipient.py +43 -0
  40. slidge/core/pubsub.py +282 -116
  41. slidge/core/session.py +595 -372
  42. slidge/group/__init__.py +10 -0
  43. slidge/group/archive.py +125 -0
  44. slidge/group/bookmarks.py +163 -0
  45. slidge/group/participant.py +458 -0
  46. slidge/group/room.py +1103 -0
  47. slidge/migration.py +18 -0
  48. slidge/slixfix/__init__.py +68 -0
  49. slidge/{util/xep_0084 → slixfix/link_preview}/__init__.py +3 -5
  50. slidge/slixfix/link_preview/link_preview.py +17 -0
  51. slidge/slixfix/link_preview/stanza.py +99 -0
  52. slidge/slixfix/roster.py +60 -0
  53. slidge/{util → slixfix}/xep_0077/register.py +14 -2
  54. slidge/slixfix/xep_0077/stanza.py +104 -0
  55. slidge/{util → slixfix}/xep_0100/gateway.py +25 -15
  56. slidge/slixfix/xep_0100/stanza.py +9 -0
  57. slidge/slixfix/xep_0153/__init__.py +10 -0
  58. slidge/slixfix/xep_0153/stanza.py +25 -0
  59. slidge/slixfix/xep_0153/vcard_avatar.py +23 -0
  60. slidge/slixfix/xep_0264/__init__.py +5 -0
  61. slidge/slixfix/xep_0264/stanza.py +36 -0
  62. slidge/slixfix/xep_0264/thumbnail.py +23 -0
  63. slidge/slixfix/xep_0292/__init__.py +5 -0
  64. slidge/slixfix/xep_0292/vcard4.py +100 -0
  65. slidge/slixfix/xep_0313/__init__.py +12 -0
  66. slidge/slixfix/xep_0313/mam.py +262 -0
  67. slidge/slixfix/xep_0313/stanza.py +359 -0
  68. slidge/slixfix/xep_0317/__init__.py +5 -0
  69. slidge/slixfix/xep_0317/hats.py +17 -0
  70. slidge/slixfix/xep_0317/stanza.py +28 -0
  71. slidge/{util → slixfix}/xep_0356_old/privilege.py +9 -7
  72. slidge/slixfix/xep_0424/__init__.py +9 -0
  73. slidge/slixfix/xep_0424/retraction.py +77 -0
  74. slidge/slixfix/xep_0424/stanza.py +28 -0
  75. slidge/slixfix/xep_0490/__init__.py +8 -0
  76. slidge/slixfix/xep_0490/mds.py +47 -0
  77. slidge/slixfix/xep_0490/stanza.py +17 -0
  78. slidge/util/__init__.py +4 -6
  79. slidge/util/archive_msg.py +61 -0
  80. slidge/util/conf.py +206 -0
  81. slidge/util/db.py +57 -76
  82. slidge/util/schema.sql +126 -0
  83. slidge/util/sql.py +508 -0
  84. slidge/util/test.py +215 -25
  85. slidge/util/types.py +177 -4
  86. slidge/util/util.py +225 -59
  87. slidge-0.1.1.dist-info/METADATA +110 -0
  88. slidge-0.1.1.dist-info/RECORD +96 -0
  89. {slidge-0.1.0b2.dist-info → slidge-0.1.1.dist-info}/WHEEL +1 -1
  90. slidge/core/contact.py +0 -891
  91. slidge/core/gateway.py +0 -916
  92. slidge/plugins/discord/__init__.py +0 -90
  93. slidge/plugins/discord/client.py +0 -108
  94. slidge/plugins/discord/session.py +0 -162
  95. slidge/plugins/dummy.py +0 -203
  96. slidge/plugins/facebook.py +0 -493
  97. slidge/plugins/hackernews.py +0 -213
  98. slidge/plugins/mattermost/__init__.py +0 -1
  99. slidge/plugins/mattermost/api.py +0 -280
  100. slidge/plugins/mattermost/gateway.py +0 -365
  101. slidge/plugins/mattermost/websocket.py +0 -252
  102. slidge/plugins/signal/__init__.py +0 -3
  103. slidge/plugins/signal/contact.py +0 -106
  104. slidge/plugins/signal/gateway.py +0 -282
  105. slidge/plugins/signal/session.py +0 -448
  106. slidge/plugins/signal/txt.py +0 -53
  107. slidge/plugins/skype.py +0 -325
  108. slidge/plugins/steam.py +0 -310
  109. slidge/plugins/telegram/__init__.py +0 -5
  110. slidge/plugins/telegram/client.py +0 -228
  111. slidge/plugins/telegram/config.py +0 -12
  112. slidge/plugins/telegram/contact.py +0 -176
  113. slidge/plugins/telegram/gateway.py +0 -150
  114. slidge/plugins/telegram/session.py +0 -256
  115. slidge/util/xep_0030/__init__.py +0 -13
  116. slidge/util/xep_0030/disco.py +0 -811
  117. slidge/util/xep_0030/stanza/__init__.py +0 -7
  118. slidge/util/xep_0030/stanza/info.py +0 -270
  119. slidge/util/xep_0030/stanza/items.py +0 -147
  120. slidge/util/xep_0030/static.py +0 -467
  121. slidge/util/xep_0055/__init__.py +0 -5
  122. slidge/util/xep_0055/search.py +0 -75
  123. slidge/util/xep_0055/stanza.py +0 -10
  124. slidge/util/xep_0077/stanza.py +0 -71
  125. slidge/util/xep_0084/avatar.py +0 -137
  126. slidge/util/xep_0084/stanza.py +0 -104
  127. slidge/util/xep_0115/__init__.py +0 -12
  128. slidge/util/xep_0115/caps.py +0 -379
  129. slidge/util/xep_0115/stanza.py +0 -16
  130. slidge/util/xep_0115/static.py +0 -137
  131. slidge/util/xep_0292/__init__.py +0 -1
  132. slidge/util/xep_0292/stanza.py +0 -167
  133. slidge/util/xep_0292/vcard4.py +0 -75
  134. slidge/util/xep_0333/__init__.py +0 -10
  135. slidge/util/xep_0333/markers.py +0 -96
  136. slidge/util/xep_0333/stanza.py +0 -34
  137. slidge/util/xep_0356/__init__.py +0 -7
  138. slidge/util/xep_0356/permissions.py +0 -35
  139. slidge/util/xep_0356/privilege.py +0 -160
  140. slidge/util/xep_0356/stanza.py +0 -44
  141. slidge/util/xep_0363/__init__.py +0 -16
  142. slidge/util/xep_0363/http_upload.py +0 -215
  143. slidge/util/xep_0363/stanza.py +0 -46
  144. slidge/util/xep_0461/__init__.py +0 -6
  145. slidge/util/xep_0461/reply.py +0 -48
  146. slidge/util/xep_0461/stanza.py +0 -47
  147. slidge-0.1.0b2.dist-info/METADATA +0 -171
  148. slidge-0.1.0b2.dist-info/RECORD +0 -81
  149. /slidge/{plugins/__init__.py → py.typed} +0 -0
  150. /slidge/{util → slixfix}/xep_0077/__init__.py +0 -0
  151. /slidge/{util → slixfix}/xep_0100/__init__.py +0 -0
  152. /slidge/{util → slixfix}/xep_0356_old/__init__.py +0 -0
  153. /slidge/{util → slixfix}/xep_0356_old/stanza.py +0 -0
  154. {slidge-0.1.0b2.dist-info → slidge-0.1.1.dist-info}/LICENSE +0 -0
  155. {slidge-0.1.0b2.dist-info → slidge-0.1.1.dist-info}/entry_points.txt +0 -0
@@ -1 +0,0 @@
1
- from .gateway import Gateway, Session
@@ -1,280 +0,0 @@
1
- import asyncio
2
- import io
3
- import logging
4
-
5
- import aiohttp
6
- import emoji
7
- from mattermost_api_reference_client.api.channels import (
8
- create_direct_channel,
9
- get_channel_members,
10
- get_channels_for_team_for_user,
11
- view_channel,
12
- )
13
- from mattermost_api_reference_client.api.files import get_file, upload_file
14
- from mattermost_api_reference_client.api.posts import (
15
- create_post,
16
- delete_post,
17
- get_posts_for_channel,
18
- update_post,
19
- )
20
- from mattermost_api_reference_client.api.reactions import (
21
- delete_reaction,
22
- get_reactions,
23
- save_reaction,
24
- )
25
- from mattermost_api_reference_client.api.status import get_users_statuses_by_ids
26
- from mattermost_api_reference_client.api.teams import get_teams_for_user
27
- from mattermost_api_reference_client.api.users import (
28
- get_profile_image,
29
- get_user,
30
- get_user_by_username,
31
- get_users_by_ids,
32
- )
33
- from mattermost_api_reference_client.client import AuthenticatedClient
34
- from mattermost_api_reference_client.models import (
35
- Reaction,
36
- Status,
37
- User,
38
- ViewChannelJsonBody,
39
- )
40
- from mattermost_api_reference_client.models.create_post_json_body import (
41
- CreatePostJsonBody,
42
- )
43
- from mattermost_api_reference_client.models.update_post_json_body import (
44
- UpdatePostJsonBody,
45
- )
46
- from mattermost_api_reference_client.models.upload_file_multipart_data import (
47
- UploadFileMultipartData,
48
- )
49
- from mattermost_api_reference_client.types import File, Unset
50
-
51
-
52
- class MattermostClient:
53
- # TODO: this should be autogenerated using a template in mattermost_api_reference_client
54
-
55
- def __init__(self, *args, **kwargs):
56
- self.http = AuthenticatedClient(*args, **kwargs)
57
- self.mm_id: asyncio.Future[str] = asyncio.get_running_loop().create_future()
58
- self.me: asyncio.Future[User] = asyncio.get_running_loop().create_future()
59
-
60
- async def login(self):
61
- log.debug("Login")
62
- me = await get_user.asyncio("me", client=self.http)
63
- if me is None:
64
- raise RuntimeError("Could not login")
65
- self.me.set_result(me)
66
- my_id = me.id
67
- if isinstance(my_id, Unset):
68
- raise RuntimeError("Could not login")
69
- self.mm_id.set_result(my_id)
70
- log.debug("Me: %s", me)
71
-
72
- async def get_contacts(self) -> list[str]:
73
- mm = self.http
74
- my_id = await self.mm_id
75
-
76
- contact_mm_ids: list[str] = []
77
-
78
- teams = await get_teams_for_user.asyncio("me", client=mm)
79
-
80
- if teams is None:
81
- raise RuntimeError
82
-
83
- for team in teams:
84
- if isinstance(team.id, Unset):
85
- log.warning("Team without ID")
86
- continue
87
- channels = await get_channels_for_team_for_user.asyncio(
88
- "me", team.id, client=mm
89
- )
90
-
91
- if channels is None:
92
- log.warning("Team without channels")
93
- continue
94
-
95
- for channel in channels:
96
- if isinstance(channel.id, Unset):
97
- log.warning("Channel without ID")
98
- continue
99
- members = await self.get_channel_members(channel.id, per_page=4)
100
- if len(members) == 2:
101
- user_ids = {m.user_id for m in members}
102
- try:
103
- user_ids.remove(my_id)
104
- except KeyError:
105
- log.warning("Weird 2 person channel: %s", members)
106
- else:
107
- contact_id = user_ids.pop()
108
- if not isinstance(contact_id, str):
109
- log.warning("Weird contact: %s", members)
110
- continue
111
- contact_mm_ids.append(contact_id)
112
-
113
- return contact_mm_ids
114
-
115
- async def get_channel_members(
116
- self, channel_id: str, *, page: int = 0, per_page: int = 10
117
- ):
118
- members = await get_channel_members.asyncio(
119
- channel_id, client=self.http, per_page=per_page, page=page
120
- )
121
- if members is None:
122
- raise RuntimeError
123
- return members
124
-
125
- async def get_users_by_ids(self, user_ids: list[str]) -> list[User]:
126
- r = await get_users_by_ids.asyncio(json_body=user_ids, client=self.http)
127
- if r is None:
128
- raise RuntimeError
129
- return r
130
-
131
- async def get_user(self, user_id: str):
132
- r = await get_user.asyncio(user_id, client=self.http)
133
- if r is None:
134
- raise RuntimeError
135
- if isinstance(r.username, Unset):
136
- raise RuntimeError
137
- return r
138
-
139
- async def get_users_statuses_by_ids(self, user_ids: list[str]) -> list[Status]:
140
- r = await get_users_statuses_by_ids.asyncio(
141
- json_body=user_ids, client=self.http
142
- )
143
- if r is None:
144
- raise RuntimeError
145
- return r
146
-
147
- async def send_message_to_user(self, user_id: str, text: str) -> str:
148
- await self.mm_id
149
- mm = self.http
150
-
151
- other = await self.get_user_by_username(user_id)
152
-
153
- direct_channel = await self.get_direct_channel(other.id)
154
-
155
- msg = await create_post.asyncio(
156
- json_body=CreatePostJsonBody(channel_id=direct_channel.id, message=text),
157
- client=mm,
158
- )
159
- if msg is None:
160
- raise RuntimeError
161
-
162
- if isinstance(msg.id, Unset):
163
- raise RuntimeError
164
-
165
- return msg.id
166
-
167
- async def send_message_with_file(self, channel_id: str, file_id: str):
168
- r = await create_post.asyncio(
169
- json_body=CreatePostJsonBody(
170
- channel_id=channel_id, file_ids=[file_id], message=""
171
- ),
172
- client=self.http,
173
- )
174
- if r is None or isinstance(r.id, Unset):
175
- raise RuntimeError(r)
176
- return r.id
177
-
178
- async def get_user_by_username(self, username: str) -> User:
179
- user = await get_user_by_username.asyncio(username, client=self.http)
180
- if user is None or isinstance(user.id, Unset):
181
- raise RuntimeError("Contact not found")
182
- return user
183
-
184
- async def get_direct_channel(self, user_id):
185
- direct_channel = await create_direct_channel.asyncio(
186
- json_body=[await self.mm_id, user_id], client=self.http
187
- )
188
- if direct_channel is None or isinstance(direct_channel.id, Unset):
189
- raise RuntimeError("Could not create direct channel")
190
- return direct_channel
191
-
192
- async def get_profile_image(self, user_id: str) -> bytes:
193
- resp = await get_profile_image.asyncio_detailed(user_id, client=self.http)
194
- return resp.content
195
-
196
- async def get_file(self, file_id: str):
197
- resp = await get_file.asyncio_detailed(file_id, client=self.http)
198
- return resp.content
199
-
200
- async def delete_post(self, post_id: str):
201
- r = await delete_post.asyncio(post_id, client=self.http)
202
- if r is not None and not isinstance(r, Unset) and r.status != "ok":
203
- raise RuntimeError("Could not delete post %s", post_id)
204
-
205
- async def update_post(self, post_id: str, body: str):
206
- r = await update_post.asyncio(
207
- post_id,
208
- client=self.http,
209
- json_body=UpdatePostJsonBody(id=post_id, message=body),
210
- )
211
- if r is None or isinstance(r, Unset):
212
- raise RuntimeError(r)
213
- return r.id
214
-
215
- async def get_posts_for_channel(self, channel_id: str):
216
- r = await get_posts_for_channel.asyncio(
217
- channel_id, client=self.http, per_page=2
218
- )
219
- if r is None or isinstance(r, Unset):
220
- raise RuntimeError(r)
221
- return r
222
-
223
- async def upload_file(self, channel_id: str, url: str):
224
- async with aiohttp.ClientSession() as s:
225
- async with s.get(url) as get_response:
226
- data = await get_response.read()
227
- req = UploadFileMultipartData(
228
- files=File(file_name=url.split("/")[-1], payload=io.BytesIO(data)),
229
- channel_id=channel_id,
230
- )
231
- r = await upload_file.asyncio(client=self.http, multipart_data=req)
232
- if (
233
- r is None
234
- or isinstance(r, Unset)
235
- or r.file_infos is None
236
- or isinstance(r.file_infos, Unset)
237
- or len(r.file_infos) != 1
238
- ):
239
- raise RuntimeError(r)
240
- return r.file_infos[0].id
241
-
242
- async def react(self, post_id: str, emoji_char: str):
243
- return await save_reaction.asyncio(
244
- client=self.http,
245
- json_body=Reaction(
246
- user_id=await self.mm_id,
247
- post_id=post_id,
248
- emoji_name=demojize(emoji_char),
249
- ),
250
- )
251
-
252
- async def get_reactions(self, post_id: str):
253
- try:
254
- return await get_reactions.asyncio(post_id, client=self.http)
255
- except TypeError:
256
- return []
257
-
258
- async def delete_reaction(self, post_id: str, emoji_name: str):
259
- await delete_reaction.asyncio(
260
- await self.mm_id, post_id, emoji_name=emoji_name, client=self.http
261
- )
262
-
263
- async def view_channel(self, channel_id: str):
264
- await view_channel.asyncio(
265
- await self.mm_id,
266
- client=self.http,
267
- json_body=ViewChannelJsonBody(channel_id=channel_id),
268
- )
269
-
270
-
271
- def demojize(emoji_char: str):
272
- # TODO: find a better when than these non standard emoji aliases replace
273
- return (
274
- emoji.demojize(emoji_char, delimiters=("", ""), language="alias")
275
- .replace("_three_", "_3_")
276
- .replace("thumbsup", "+1")
277
- )
278
-
279
-
280
- log = logging.getLogger(__name__)
@@ -1,365 +0,0 @@
1
- import asyncio
2
- import io
3
- import pprint
4
- import re
5
- from datetime import datetime
6
- from typing import Any, Optional
7
-
8
- import emoji
9
- from mattermost_api_reference_client.models import Status
10
- from mattermost_api_reference_client.types import Unset
11
-
12
- from slidge import *
13
-
14
- from .api import MattermostClient
15
- from .websocket import EventType, MattermostEvent, Websocket
16
-
17
-
18
- class Gateway(BaseGateway):
19
- REGISTRATION_INSTRUCTIONS = (
20
- "Enter mattermost credentials. "
21
- "Get your MMAUTH_TOKEN on the web interface, using the dev tools of your browser (it's a cookie)."
22
- )
23
- REGISTRATION_FIELDS = [
24
- FormField(var="url", label="Mattermost server URL", required=True),
25
- FormField(var="token", label="MMAUTH_TOKEN", required=True),
26
- FormField(var="basepath", label="Base path", value="/api/v4", required=True),
27
- FormField(
28
- var="basepath_ws",
29
- label="Websocket base path",
30
- value="/websocket",
31
- required=True,
32
- ),
33
- FormField(
34
- var="strict_ssl",
35
- label="Strict SSL verification",
36
- value="1",
37
- required=False,
38
- type="boolean",
39
- ),
40
- ]
41
-
42
- ROSTER_GROUP = "Mattermost"
43
-
44
- COMPONENT_NAME = "Mattermost (slidge)"
45
- COMPONENT_TYPE = "mattermost"
46
-
47
- COMPONENT_AVATAR = "https://play-lh.googleusercontent.com/aX7JaAPkmnkeThK4kgb_HHlBnswXF0sPyNI8I8LNmEMMo1vDvMx32tCzgPMsyEXXzZRc"
48
-
49
-
50
- class Contact(LegacyContact["Session"]):
51
- legacy_id: str
52
-
53
- MARKS = False
54
-
55
- def __init__(self, *a, **kw):
56
- super().__init__(*a, **kw)
57
- self._direct_channel_id: Optional[str] = None
58
- self._mm_id: Optional[str] = None
59
-
60
- def update_status(self, status: Optional[str]):
61
- if status is None: # custom status
62
- self.session.log.debug("Status is None: %s", status)
63
- self.online()
64
- elif status == "online":
65
- self.online()
66
- elif status == "offline":
67
- self.offline()
68
- elif status == "away":
69
- self.away()
70
- elif status == "dnd":
71
- self.busy()
72
- else:
73
- self.session.log.warning(
74
- "Unknown status for '%s':",
75
- status,
76
- )
77
-
78
- async def direct_channel_id(self):
79
- if self._direct_channel_id is None:
80
- self._direct_channel_id = (
81
- await self.session.mm_client.get_direct_channel(await self.mm_id())
82
- ).id
83
- self.session.contacts.direct_channel_id_to_username[
84
- self._direct_channel_id
85
- ] = self.legacy_id
86
- return self._direct_channel_id
87
-
88
- async def mm_id(self):
89
- if self._mm_id is None:
90
- self._mm_id = (
91
- await self.session.mm_client.get_user_by_username(self.legacy_id)
92
- ).id
93
- self.session.contacts.user_id_to_username[self._mm_id] = self.legacy_id
94
- return self._mm_id
95
-
96
- async def update_reactions(self, legacy_msg_id):
97
- self.react(
98
- legacy_msg_id,
99
- [
100
- # TODO: find a better when than these non standard emoji aliases replace
101
- emoji.emojize(f":{x.replace('_3_', '_three_')}:", language="alias")
102
- for x in await self.session.get_mm_reactions(
103
- legacy_msg_id, await self.mm_id()
104
- )
105
- ],
106
- )
107
-
108
-
109
- class Roster(LegacyRoster[Contact, "Session"]):
110
- user_id_to_username: dict[str, str]
111
- direct_channel_id_to_username: dict[str, str]
112
-
113
- def __init__(self, *a, **kw):
114
- super().__init__(*a, **kw)
115
- self.user_id_to_username = {}
116
- self.direct_channel_id_to_username = {}
117
-
118
- async def by_mm_user_id(self, user_id: str):
119
- try:
120
- legacy_id = self.user_id_to_username[user_id]
121
- except KeyError:
122
- user = await self.session.mm_client.get_user(user_id)
123
- if isinstance(user.username, Unset):
124
- raise RuntimeError
125
- legacy_id = self.user_id_to_username[user_id] = user.username
126
- return self.by_legacy_id(legacy_id)
127
-
128
- async def by_direct_channel_id(self, channel_id: str):
129
- if (username := self.direct_channel_id_to_username.get(channel_id)) is None:
130
- for c in self:
131
- if (await c.direct_channel_id()) == channel_id:
132
- return c
133
- else:
134
- return self.by_legacy_id(username)
135
-
136
-
137
- class Session(BaseSession[Contact, Roster, Gateway]):
138
- mm_client: MattermostClient
139
- ws: Websocket
140
- messages_waiting_for_echo: set[str]
141
- send_lock: asyncio.Lock
142
- view_futures: dict[str, asyncio.Future[None]]
143
-
144
- def post_init(self):
145
- self.messages_waiting_for_echo = set()
146
- self.send_lock = asyncio.Lock()
147
- f = self.user.registration_form
148
- url = f["url"] + f["basepath"]
149
- self.mm_client = MattermostClient(
150
- url,
151
- verify_ssl=f["strict_ssl"],
152
- timeout=5,
153
- token=f["token"],
154
- )
155
- self.ws = Websocket(
156
- re.sub("^http", "ws", f["url"]) + f["basepath"] + f["basepath_ws"],
157
- f["token"],
158
- )
159
- self.view_futures = {}
160
-
161
- async def login(self):
162
- await self.mm_client.login()
163
- await self.add_contacts()
164
- self.xmpp.loop.create_task(self.ws.connect(self.on_mm_event))
165
- if self.mm_client.me is None:
166
- raise RuntimeError
167
-
168
- return f"Connected as '{(await self.mm_client.me).username}'"
169
-
170
- async def add_contacts(self):
171
- user_ids = await self.mm_client.get_contacts()
172
- contact_mm_users = await self.mm_client.get_users_by_ids(user_ids)
173
- contact_mm_statuses = await self.mm_client.get_users_statuses_by_ids(user_ids)
174
-
175
- statuses = {s.user_id: s for s in contact_mm_statuses}
176
-
177
- for user in contact_mm_users:
178
- status: Status = statuses[user.id]
179
- contact = self.contacts.by_legacy_id(user.username)
180
- self.contacts.user_id_to_username[user.id] = user.username
181
- if user.nickname:
182
- contact.name = user.nickname
183
- elif user.first_name and user.last_name:
184
- contact.name = user.first_name + " " + user.last_name
185
- elif user.first_name:
186
- contact.name = user.first_name
187
- elif user.last_name:
188
- contact.name = user.last_name
189
-
190
- contact.avatar = await self.mm_client.get_profile_image(user.id)
191
-
192
- await contact.add_to_roster()
193
- contact.update_status(status.status)
194
-
195
- async def on_mm_event(self, event: MattermostEvent):
196
- self.log.debug("Event: %s", event)
197
- if event.type == EventType.Hello:
198
- self.log.debug("Received hello event: %s", event.data)
199
- elif event.type == EventType.Posted:
200
- post = event.data["post"]
201
- self.log.debug("Post: %s", pprint.pformat(post))
202
-
203
- message = post["message"]
204
-
205
- channel_id = post["channel_id"]
206
- post_id = post["id"]
207
- user_id = post["user_id"]
208
-
209
- if event.data["channel_type"] == "D": # Direct messages?
210
- if user_id == await self.mm_client.mm_id:
211
- try:
212
- async with self.send_lock:
213
- self.messages_waiting_for_echo.remove(post_id)
214
- except KeyError:
215
- members = await self.mm_client.get_channel_members(channel_id)
216
- if len(members) > 2:
217
- raise RuntimeError("Not a direct message after all")
218
- for m in members:
219
- if m.user_id != await self.mm_client.mm_id:
220
- contact = await self.contacts.by_mm_user_id(m.user_id)
221
- break
222
- else:
223
- raise RuntimeError("What?")
224
-
225
- contact.carbon(
226
- message,
227
- post_id,
228
- datetime.fromtimestamp(post["update_at"] / 1000),
229
- )
230
- else:
231
- contact = await self.contacts.by_mm_user_id(user_id)
232
- if event.data.get("set_online"):
233
- contact.online()
234
- contact.send_text(message, legacy_msg_id=post_id)
235
- for file_meta in post.get("metadata", {}).get("files", []):
236
- await contact.send_file(
237
- filename=file_meta["name"],
238
- input_file=io.BytesIO(
239
- await self.mm_client.get_file(file_meta["id"])
240
- ),
241
- )
242
- elif event.data["channel_type"] == "P":
243
- # private channel
244
- pass
245
- elif event.type == EventType.ChannelViewed:
246
- channel_id = event.data["channel_id"]
247
- try:
248
- f = self.view_futures.pop(channel_id)
249
- except KeyError:
250
- pass
251
- else:
252
- f.set_result(None)
253
- return
254
- posts = await self.mm_client.get_posts_for_channel(channel_id)
255
- last_msg_id = posts.posts.additional_keys[-1]
256
- if (c := await self.contacts.by_direct_channel_id(channel_id)) is None:
257
- self.log.debug("Ignoring unknown channel")
258
- else:
259
- c.carbon_read(last_msg_id)
260
- elif event.type == EventType.StatusChange:
261
- user_id = event.data["user_id"]
262
- if user_id == await self.mm_client.mm_id:
263
- self.log.debug("Own status change")
264
- else:
265
-
266
- contact = await self.contacts.by_mm_user_id(user_id)
267
- contact.update_status(event.data["status"])
268
- elif event.type == EventType.Typing:
269
- contact = await self.contacts.by_mm_user_id(event.data["user_id"])
270
- contact.composing()
271
- elif event.type == EventType.PostEdited:
272
- post = event.data["post"]
273
- contact = await self.contacts.by_mm_user_id(post["user_id"])
274
- if post["channel_id"] == await contact.direct_channel_id():
275
- contact.correct(post["id"], post["message"])
276
- elif event.type == EventType.PostDeleted:
277
- post = event.data["post"]
278
- contact = await self.contacts.by_mm_user_id(post["user_id"])
279
- if post["channel_id"] == await contact.direct_channel_id():
280
- contact.retract(post["id"])
281
- elif event.type in (EventType.ReactionAdded, EventType.ReactionRemoved):
282
- reaction = event.data["reaction"]
283
- legacy_msg_id = reaction["post_id"]
284
- if (who := reaction["user_id"]) == await self.mm_client.mm_id:
285
- user_reactions_name = {
286
- f":{x}:" for x in await self.get_mm_reactions(legacy_msg_id, who)
287
- }
288
- user_reactions_char = {
289
- # TODO: find a better when than these non standard emoji aliases replace
290
- emoji.emojize(x.replace("_3_", "_three_"), language="alias")
291
- for x in user_reactions_name
292
- }
293
- self.log.debug(
294
- "carbon: %s vs %s", user_reactions_name, user_reactions_char
295
- )
296
- contact = await self.contacts.by_direct_channel_id(
297
- event.broadcast["channel_id"]
298
- )
299
- contact.carbon_react(legacy_msg_id, user_reactions_char)
300
- else:
301
- await (await self.contacts.by_mm_user_id(who)).update_reactions(
302
- reaction["post_id"]
303
- )
304
-
305
- async def logout(self):
306
- pass
307
-
308
- async def send_text(self, t: str, c: Contact, *, reply_to_msg_id=None):
309
- async with self.send_lock:
310
- msg_id = await self.mm_client.send_message_to_user(c.legacy_id, t)
311
- self.messages_waiting_for_echo.add(msg_id)
312
- return msg_id
313
-
314
- async def send_file(self, u: str, c: Contact, *, reply_to_msg_id=None):
315
- channel_id = await c.direct_channel_id()
316
- file_id = await self.mm_client.upload_file(channel_id, u)
317
- return await self.mm_client.send_message_with_file(channel_id, file_id)
318
-
319
- async def active(self, c: Contact):
320
- pass
321
-
322
- async def inactive(self, c: Contact):
323
- pass
324
-
325
- async def composing(self, c: Contact):
326
- await self.ws.user_typing(await c.direct_channel_id())
327
-
328
- async def paused(self, c: Contact):
329
- # no equivalent in MM, seems to have an automatic timeout in clients
330
- pass
331
-
332
- async def displayed(self, legacy_msg_id: Any, c: Contact):
333
- channel = await c.direct_channel_id()
334
- f = self.view_futures[channel] = self.xmpp.loop.create_future()
335
- await self.mm_client.view_channel(channel)
336
- await f
337
-
338
- async def correct(self, text: str, legacy_msg_id: Any, c: Contact):
339
- await self.mm_client.update_post(legacy_msg_id, text)
340
-
341
- async def search(self, form_values: dict[str, str]):
342
- pass
343
-
344
- async def retract(self, legacy_msg_id: Any, c: Contact):
345
- await self.mm_client.delete_post(legacy_msg_id)
346
-
347
- async def react(self, legacy_msg_id: Any, emojis: list[str], c: Contact):
348
- mm_reactions = await self.get_mm_reactions(
349
- legacy_msg_id, await self.mm_client.mm_id
350
- )
351
- xmpp_reactions = {
352
- emoji.demojize(x, language="alias", delimiters=("", "")) for x in emojis
353
- }
354
- self.log.debug("%s vs %s", mm_reactions, xmpp_reactions)
355
- for e in xmpp_reactions - mm_reactions:
356
- await self.mm_client.react(legacy_msg_id, e)
357
- for e in mm_reactions - xmpp_reactions:
358
- await self.mm_client.delete_reaction(legacy_msg_id, e)
359
-
360
- async def get_mm_reactions(self, legacy_msg_id: str, user_id: Optional[str]):
361
- return {
362
- x.emoji_name
363
- for x in await self.mm_client.get_reactions(legacy_msg_id)
364
- if x.user_id == user_id
365
- }