slidge 0.1.0rc1__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 (164) hide show
  1. slidge/__init__.py +54 -31
  2. slidge/__main__.py +51 -5
  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 +121 -39
  16. slidge/core/config.py +116 -11
  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 +9 -1
  31. slidge/core/mixins/attachment.py +506 -0
  32. slidge/core/mixins/avatar.py +167 -0
  33. slidge/core/mixins/base.py +6 -19
  34. slidge/core/mixins/disco.py +66 -15
  35. slidge/core/mixins/lock.py +31 -0
  36. slidge/core/mixins/message.py +254 -252
  37. slidge/core/mixins/message_maker.py +154 -0
  38. slidge/core/mixins/presence.py +128 -31
  39. slidge/core/mixins/recipient.py +43 -0
  40. slidge/core/pubsub.py +275 -116
  41. slidge/core/session.py +586 -518
  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_0050 → slixfix/link_preview}/__init__.py +4 -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 +1 -2
  54. slidge/slixfix/xep_0077/stanza.py +104 -0
  55. slidge/{util → slixfix}/xep_0100/gateway.py +17 -12
  56. slidge/slixfix/xep_0153/__init__.py +10 -0
  57. slidge/slixfix/xep_0153/stanza.py +25 -0
  58. slidge/slixfix/xep_0153/vcard_avatar.py +23 -0
  59. slidge/slixfix/xep_0264/__init__.py +5 -0
  60. slidge/slixfix/xep_0264/stanza.py +36 -0
  61. slidge/slixfix/xep_0264/thumbnail.py +23 -0
  62. slidge/slixfix/xep_0292/__init__.py +5 -0
  63. slidge/slixfix/xep_0292/vcard4.py +100 -0
  64. slidge/slixfix/xep_0313/__init__.py +12 -0
  65. slidge/slixfix/xep_0313/mam.py +262 -0
  66. slidge/slixfix/xep_0313/stanza.py +359 -0
  67. slidge/slixfix/xep_0317/__init__.py +5 -0
  68. slidge/slixfix/xep_0317/hats.py +17 -0
  69. slidge/slixfix/xep_0317/stanza.py +28 -0
  70. slidge/{util → slixfix}/xep_0356_old/privilege.py +9 -7
  71. slidge/slixfix/xep_0424/__init__.py +9 -0
  72. slidge/slixfix/xep_0424/retraction.py +77 -0
  73. slidge/slixfix/xep_0424/stanza.py +28 -0
  74. slidge/slixfix/xep_0490/__init__.py +8 -0
  75. slidge/slixfix/xep_0490/mds.py +47 -0
  76. slidge/slixfix/xep_0490/stanza.py +17 -0
  77. slidge/util/__init__.py +4 -6
  78. slidge/util/archive_msg.py +61 -0
  79. slidge/util/conf.py +25 -4
  80. slidge/util/db.py +23 -69
  81. slidge/util/schema.sql +126 -0
  82. slidge/util/sql.py +508 -0
  83. slidge/util/test.py +136 -86
  84. slidge/util/types.py +155 -14
  85. slidge/util/util.py +225 -51
  86. slidge-0.1.1.dist-info/METADATA +110 -0
  87. slidge-0.1.1.dist-info/RECORD +96 -0
  88. {slidge-0.1.0rc1.dist-info → slidge-0.1.1.dist-info}/WHEEL +1 -1
  89. slidge/core/adhoc.py +0 -492
  90. slidge/core/chat_command.py +0 -197
  91. slidge/core/contact.py +0 -441
  92. slidge/core/disco.py +0 -59
  93. slidge/core/gateway.py +0 -899
  94. slidge/core/muc/__init__.py +0 -3
  95. slidge/core/muc/bookmarks.py +0 -74
  96. slidge/core/muc/participant.py +0 -152
  97. slidge/core/muc/room.py +0 -348
  98. slidge/plugins/discord/__init__.py +0 -121
  99. slidge/plugins/discord/client.py +0 -121
  100. slidge/plugins/discord/session.py +0 -172
  101. slidge/plugins/dummy.py +0 -334
  102. slidge/plugins/facebook.py +0 -591
  103. slidge/plugins/hackernews.py +0 -209
  104. slidge/plugins/mattermost/__init__.py +0 -1
  105. slidge/plugins/mattermost/api.py +0 -288
  106. slidge/plugins/mattermost/gateway.py +0 -417
  107. slidge/plugins/mattermost/websocket.py +0 -248
  108. slidge/plugins/signal/__init__.py +0 -4
  109. slidge/plugins/signal/config.py +0 -4
  110. slidge/plugins/signal/contact.py +0 -104
  111. slidge/plugins/signal/gateway.py +0 -379
  112. slidge/plugins/signal/group.py +0 -76
  113. slidge/plugins/signal/session.py +0 -515
  114. slidge/plugins/signal/txt.py +0 -13
  115. slidge/plugins/signal/util.py +0 -32
  116. slidge/plugins/skype.py +0 -310
  117. slidge/plugins/steam.py +0 -400
  118. slidge/plugins/telegram/__init__.py +0 -6
  119. slidge/plugins/telegram/client.py +0 -325
  120. slidge/plugins/telegram/config.py +0 -21
  121. slidge/plugins/telegram/contact.py +0 -154
  122. slidge/plugins/telegram/gateway.py +0 -182
  123. slidge/plugins/telegram/group.py +0 -184
  124. slidge/plugins/telegram/session.py +0 -275
  125. slidge/plugins/telegram/util.py +0 -153
  126. slidge/plugins/whatsapp/__init__.py +0 -6
  127. slidge/plugins/whatsapp/config.py +0 -17
  128. slidge/plugins/whatsapp/contact.py +0 -33
  129. slidge/plugins/whatsapp/event.go +0 -455
  130. slidge/plugins/whatsapp/gateway.go +0 -156
  131. slidge/plugins/whatsapp/gateway.py +0 -69
  132. slidge/plugins/whatsapp/go.mod +0 -17
  133. slidge/plugins/whatsapp/go.sum +0 -22
  134. slidge/plugins/whatsapp/session.go +0 -371
  135. slidge/plugins/whatsapp/session.py +0 -370
  136. slidge/util/xep_0030/__init__.py +0 -13
  137. slidge/util/xep_0030/disco.py +0 -811
  138. slidge/util/xep_0030/stanza/__init__.py +0 -7
  139. slidge/util/xep_0030/stanza/info.py +0 -270
  140. slidge/util/xep_0030/stanza/items.py +0 -147
  141. slidge/util/xep_0030/static.py +0 -467
  142. slidge/util/xep_0050/adhoc.py +0 -631
  143. slidge/util/xep_0050/stanza.py +0 -180
  144. slidge/util/xep_0077/stanza.py +0 -71
  145. slidge/util/xep_0292/__init__.py +0 -1
  146. slidge/util/xep_0292/stanza.py +0 -167
  147. slidge/util/xep_0292/vcard4.py +0 -74
  148. slidge/util/xep_0356/__init__.py +0 -7
  149. slidge/util/xep_0356/permissions.py +0 -35
  150. slidge/util/xep_0356/privilege.py +0 -160
  151. slidge/util/xep_0356/stanza.py +0 -44
  152. slidge/util/xep_0461/__init__.py +0 -6
  153. slidge/util/xep_0461/reply.py +0 -48
  154. slidge/util/xep_0461/stanza.py +0 -80
  155. slidge-0.1.0rc1.dist-info/METADATA +0 -171
  156. slidge-0.1.0rc1.dist-info/RECORD +0 -99
  157. /slidge/{plugins/__init__.py → py.typed} +0 -0
  158. /slidge/{util → slixfix}/xep_0077/__init__.py +0 -0
  159. /slidge/{util → slixfix}/xep_0100/__init__.py +0 -0
  160. /slidge/{util → slixfix}/xep_0100/stanza.py +0 -0
  161. /slidge/{util → slixfix}/xep_0356_old/__init__.py +0 -0
  162. /slidge/{util → slixfix}/xep_0356_old/stanza.py +0 -0
  163. {slidge-0.1.0rc1.dist-info → slidge-0.1.1.dist-info}/LICENSE +0 -0
  164. {slidge-0.1.0rc1.dist-info → slidge-0.1.1.dist-info}/entry_points.txt +0 -0
@@ -1,325 +0,0 @@
1
- import asyncio
2
- import functools
3
- from datetime import datetime
4
- from typing import TYPE_CHECKING, Union
5
-
6
- import aiotdlib
7
- from aiotdlib import api as tgapi
8
-
9
- from . import config
10
- from .util import get_best_file
11
-
12
- if TYPE_CHECKING:
13
- from .contact import Contact
14
- from .group import MUC, Participant
15
- from .session import Session
16
-
17
-
18
- def get_base_kwargs(user_reg_form: dict):
19
- return dict(
20
- phone_number=user_reg_form["phone"],
21
- api_id=user_reg_form.get("api_id") or config.API_ID,
22
- api_hash=user_reg_form.get("api_hash") or config.API_HASH,
23
- database_encryption_key=config.TDLIB_KEY,
24
- files_directory=config.TDLIB_PATH,
25
- )
26
-
27
-
28
- class CredentialsValidation(aiotdlib.Client):
29
- def __init__(self, registration_form: dict):
30
- super().__init__(**get_base_kwargs(registration_form))
31
- self.code_future: asyncio.Future[
32
- str
33
- ] = asyncio.get_running_loop().create_future()
34
- self._auth_get_code = self._get_code
35
- self._auth_get_password = self._get_code
36
-
37
- async def _get_code(self):
38
- return await self.code_future
39
-
40
-
41
- class TelegramClient(aiotdlib.Client):
42
- def __init__(self, session: "Session"):
43
- super().__init__(
44
- parse_mode=aiotdlib.ClientParseMode.MARKDOWN,
45
- **get_base_kwargs(session.user.registration_form),
46
- )
47
- self.session = session
48
- self.contacts = session.contacts
49
- self.bookmarks = session.bookmarks
50
- self.log = self.session.log
51
-
52
- async def input_(prompt):
53
- self.session.send_gateway_status(f"Action required: {prompt}")
54
- return await session.input(prompt)
55
-
56
- self.input = input_
57
- self._auth_get_code = functools.partial(input_, "Enter code")
58
- self._auth_get_password = functools.partial(input_, "Enter 2FA password:")
59
- self._auth_get_first_name = functools.partial(input_, "Enter first name:")
60
- self._auth_get_last_name = functools.partial(input_, "Enter last name:")
61
-
62
- self.add_event_handler(self.dispatch_update, tgapi.API.Types.ANY)
63
-
64
- async def dispatch_update(self, _self, update: tgapi.Update):
65
- try:
66
- handler = getattr(self, "handle_" + update.ID[6:])
67
- except AttributeError:
68
- self.session.log.debug("No handler for %s, ignoring", update.ID)
69
- except IndexError:
70
- self.session.log.debug("Ignoring weird event: %s", update.ID)
71
- else:
72
- await handler(update)
73
-
74
- async def handle_NewMessage(self, update: tgapi.UpdateNewMessage):
75
- if (msg := update.message).is_channel_post:
76
- self.log.debug("Ignoring channel post")
77
- return
78
-
79
- if not await self.is_private_chat(msg.chat_id):
80
- return await self.handle_group_message(msg)
81
-
82
- session = self.session
83
- if msg.is_outgoing:
84
- # This means slidge is responsible for this message, so no carbon is needed;
85
- # but maybe this does not handle all possible cases gracefully?
86
- if msg.sending_state is not None or msg.id in session.sent:
87
- return
88
- content = msg.content
89
- contact = await session.contacts.by_legacy_id(msg.chat_id)
90
- if isinstance(content, tgapi.MessageText):
91
- contact.send_text(
92
- content.text.text,
93
- legacy_msg_id=msg.id,
94
- when=datetime.fromtimestamp(msg.date),
95
- carbon=True,
96
- )
97
- elif best_file := get_best_file(content):
98
- file = await self.api.download_file(
99
- file_id=best_file.id,
100
- synchronous=True,
101
- priority=1,
102
- offset=0,
103
- limit=0,
104
- )
105
- has_caption = (caption := content.caption) and (text := caption.text)
106
- await contact.send_file(
107
- filename=file.local.path,
108
- legacy_msg_id=None if has_caption else msg.id,
109
- carbon=True,
110
- )
111
- if has_caption:
112
- contact.send_text(text, legacy_msg_id=msg.id, carbon=True)
113
- return
114
-
115
- sender = msg.sender_id
116
- if not isinstance(sender, tgapi.MessageSenderUser):
117
- # Does this happen?
118
- self.log.warning("Ignoring chat sender in direct message: %s", msg)
119
- return
120
-
121
- await (await session.contacts.by_legacy_id(sender.user_id)).send_tg_message(msg)
122
-
123
- async def handle_group_message(self, msg: tgapi.Message):
124
- self.log.debug("MUC message: %s", msg)
125
- if msg.is_outgoing:
126
- if msg.sending_state is not None or msg.id in self.session.sent:
127
- return
128
-
129
- muc = await self.bookmarks.by_legacy_id(msg.chat_id)
130
- sender = msg.sender_id
131
- if isinstance(sender, tgapi.MessageSenderUser):
132
- participant = await muc.participant_by_tg_user(
133
- await self.api.get_user(sender.user_id)
134
- )
135
- else:
136
- participant = await muc.participant_system()
137
- await participant.send_tg_message(msg)
138
-
139
- async def handle_UserStatus(self, update: tgapi.UpdateUserStatus):
140
- if update.user_id == await self.get_my_id():
141
- return
142
- contact = await self.contacts.by_legacy_id(update.user_id)
143
- if not contact.added_to_roster:
144
- self.log.debug("Ignoring presence of contact not in the roster")
145
- return
146
- contact.update_status(update.status)
147
-
148
- async def handle_ChatReadOutbox(self, update: tgapi.UpdateChatReadOutbox):
149
- if await self.is_private_chat(update.chat_id):
150
- contact = await self.contacts.by_legacy_id(update.chat_id)
151
- contact.displayed(update.last_read_outbox_message_id)
152
- else:
153
- muc = await self.bookmarks.by_legacy_id(update.chat_id)
154
- async for p in muc.get_participants():
155
- p.displayed(update.last_read_outbox_message_id)
156
-
157
- async def handle_ChatAction(self, action: tgapi.UpdateChatAction):
158
- sender = action.sender_id
159
- if not isinstance(sender, tgapi.MessageSenderUser):
160
- self.log.debug("Ignoring action: %s", action)
161
- return
162
-
163
- chat_id = action.chat_id
164
- user_id = sender.user_id
165
- if chat_id == user_id:
166
- composer: Union[
167
- "Contact", "Participant"
168
- ] = await self.contacts.by_legacy_id(chat_id)
169
- else:
170
- muc: MUC = await self.bookmarks.by_legacy_id(chat_id)
171
- composer = await muc.participant_by_tg_user(
172
- await self.api.get_user(user_id)
173
- )
174
-
175
- composer.composing()
176
-
177
- async def handle_ChatReadInbox(self, action: tgapi.UpdateChatReadInbox):
178
- if not await self.is_private_chat(action.chat_id):
179
- return
180
-
181
- session = self.session
182
- msg_id = action.last_read_inbox_message_id
183
- self.log.debug(
184
- "Self read mark for %s and we sent %s", msg_id, session.sent_read_marks
185
- )
186
- try:
187
- session.sent_read_marks.remove(msg_id)
188
- except KeyError:
189
- # slidge didn't send this read mark, so it comes from the official tg client
190
- contact = await session.contacts.by_legacy_id(action.chat_id)
191
- contact.displayed(msg_id, carbon=True)
192
-
193
- async def handle_MessageContent(self, action: tgapi.UpdateMessageContent):
194
- new = action.new_content
195
- if isinstance(new, tgapi.MessagePhoto):
196
- # Happens when the user send a picture, looks safe to ignore
197
- self.log.debug("Ignoring message photo update: %s", new)
198
- return
199
- if not isinstance(new, tgapi.MessageText):
200
- self.log.warning("Ignoring message update: %s", new)
201
- return
202
- session = self.session
203
- corrected_msg_id = action.message_id
204
- chat_id = action.chat_id
205
- try:
206
- fut = session.user_correction_futures.pop(action.message_id)
207
- except KeyError:
208
- if await self.is_private_chat(chat_id):
209
- contact = await session.contacts.by_legacy_id(chat_id)
210
- if action.message_id in self.session.sent:
211
- contact.correct(corrected_msg_id, new.text.text, carbon=True)
212
- else:
213
- contact.correct(corrected_msg_id, new.text.text)
214
- else:
215
- if action.message_id not in self.session.muc_sent_msg_ids:
216
- muc = await session.bookmarks.by_legacy_id(chat_id)
217
- msg = await self.api.get_message(chat_id, corrected_msg_id)
218
- participant = await muc.participant_by_tg_user(
219
- await self.api.get_user(msg.sender_id.user_id)
220
- )
221
- participant.correct(action.message_id, new.text.text)
222
- else:
223
- self.log.debug("User correction confirmation received")
224
- fut.set_result(None)
225
-
226
- async def handle_User(self, action: tgapi.UpdateUser):
227
- u = action.user
228
- if u.id == await self.get_my_id():
229
- return
230
- contact = await self.session.contacts.by_legacy_id(u.id)
231
- await contact.update_info_from_user(u)
232
- if u.is_contact:
233
- await contact.add_to_roster()
234
-
235
- async def handle_MessageInteractionInfo(
236
- self, update: tgapi.UpdateMessageInteractionInfo
237
- ):
238
- if not await self.is_private_chat(update.chat_id):
239
- return
240
-
241
- contact = await self.session.contacts.by_legacy_id(update.chat_id)
242
- me = await self.get_my_id()
243
- if update.interaction_info is None:
244
- contact.react(update.message_id, [])
245
- contact.react(update.message_id, [], carbon=True)
246
- else:
247
- user_reactions = list[str]()
248
- contact_reactions = list[str]()
249
- # these sanity checks might not be necessary, but in doubt…
250
- for reaction in update.interaction_info.reactions:
251
- if reaction.total_count == 1:
252
- if len(reaction.recent_sender_ids) != 1:
253
- self.log.warning(
254
- "Weird reactions (wrong count): %s",
255
- update.interaction_info.reactions,
256
- )
257
- continue
258
- sender = reaction.recent_sender_ids[0]
259
- if isinstance(sender, tgapi.MessageSenderUser):
260
- if sender.user_id == me:
261
- user_reactions.append(reaction.reaction)
262
- elif sender.user_id == contact.legacy_id:
263
- contact_reactions.append(reaction.reaction)
264
- else:
265
- self.log.warning(
266
- "Weird reactions (neither me nor them): %s",
267
- update.interaction_info.reactions,
268
- )
269
- elif reaction.total_count == 2:
270
- user_reactions.append(reaction.reaction)
271
- contact_reactions.append(reaction.reaction)
272
- else:
273
- self.log.warning(
274
- "Weird reactions (empty): %s", update.interaction_info.reactions
275
- )
276
-
277
- contact.react(update.message_id, contact_reactions)
278
- contact.react(update.message_id, user_reactions, carbon=True)
279
-
280
- async def handle_DeleteMessages(self, update: tgapi.UpdateDeleteMessages):
281
- if not update.is_permanent: # tdlib send 'delete from cache' updates apparently
282
- self.log.debug("Ignoring non permanent delete")
283
- return
284
- for legacy_msg_id in update.message_ids:
285
- try:
286
- future = self.session.delete_futures.pop(legacy_msg_id)
287
- except KeyError:
288
- if await self.is_private_chat(update.chat_id):
289
- contact = await self.session.contacts.by_legacy_id(update.chat_id)
290
- if legacy_msg_id in self.session.sent:
291
- contact.retract(legacy_msg_id, carbon=True)
292
- else:
293
- contact.retract(legacy_msg_id)
294
- else:
295
- return
296
- # FIXME: does not work because we need to fetch the participant,
297
- # the DeleteMessage payload has not author info,
298
- # and we cannot get_message() anymore
299
- # We should probably use MUC moderation tools here
300
- # muc = await self.session.bookmarks.by_legacy_id(update.chat_id)
301
- # msg = await self.api.get_message(update.chat_id, legacy_msg_id)
302
- # participant = await muc.participant_by_tg_user_id(
303
- # msg.sender_id.user_id
304
- # )
305
- # participant.retract(legacy_msg_id)
306
- else:
307
- future.set_result(update)
308
-
309
- async def handle_MessageSendSucceeded(
310
- self, update: tgapi.UpdateMessageSendSucceeded
311
- ):
312
- self.session.sent_read_marks.add(update.message.id)
313
- for _ in range(10):
314
- try:
315
- future = self.session.ack_futures.pop(update.message.id)
316
- except KeyError:
317
- await asyncio.sleep(0.5)
318
- else:
319
- future.set_result(update.message.id)
320
- return
321
- self.log.warning("Ignoring Send success for %s", update.message.id)
322
-
323
- async def is_private_chat(self, chat_id: int):
324
- chat = await self.get_chat(chat_id)
325
- return isinstance(chat.type_, tgapi.ChatTypePrivate)
@@ -1,21 +0,0 @@
1
- from pathlib import Path
2
- from typing import Optional
3
-
4
- TDLIB_PATH: Path
5
- TDLIB_PATH__DOC = "Defaults to ${SLIDGE_HOME_DIR}/tdlib"
6
- TDLIB_PATH__DYNAMIC_DEFAULT = True
7
-
8
- TDLIB_KEY: str = "NOT_SECURE"
9
- TDLIB_KEY__DOC = "Key used to encrypt tdlib persistent DB"
10
-
11
- API_ID: Optional[int] = None
12
- API_ID__DOC = "Telegram app api_id, obtained at https://my.telegram.org/apps"
13
-
14
- API_HASH: Optional[str] = None
15
- API_HASH__DOC = "Telegram app api_hash, obtained at https://my.telegram.org/apps"
16
-
17
- REGISTRATION_AUTH_CODE_TIMEOUT: int = 60
18
- REGISTRATION_AUTH_CODE_TIMEOUT__DOC = (
19
- "On registration, users will be prompted for a 2FA code they receive "
20
- "on other telegram clients."
21
- )
@@ -1,154 +0,0 @@
1
- import asyncio
2
- import logging
3
- import time
4
- from datetime import datetime, timedelta
5
- from typing import TYPE_CHECKING, Optional, Union
6
-
7
- import aiotdlib.api as tgapi
8
- from slixmpp.exceptions import XMPPError
9
-
10
- from slidge import *
11
-
12
- from .util import AvailableEmojisMixin, TelegramToXMPPMixin
13
-
14
- if TYPE_CHECKING:
15
- from .session import Session
16
-
17
-
18
- async def noop():
19
- return
20
-
21
-
22
- class Contact(AvailableEmojisMixin, LegacyContact["Session", int], TelegramToXMPPMixin):
23
- CLIENT_TYPE = "phone"
24
- session: "Session" # type:ignore
25
-
26
- def __init__(self, *a, **k):
27
- super().__init__(*a, **k)
28
- self.chat_id = self.legacy_id
29
- self._online_expire_task = self.xmpp.loop.create_task(noop())
30
-
31
- async def _expire_online(self, timestamp: Union[int, float]):
32
- now = time.time()
33
- how_long = timestamp - now
34
- log.debug("Online status expires in %s seconds", how_long)
35
- await asyncio.sleep(how_long)
36
- self.away(last_seen=datetime.fromtimestamp(timestamp))
37
-
38
- def update_status(self, status: tgapi.UserStatus):
39
- if isinstance(status, tgapi.UserStatusEmpty):
40
- self.inactive()
41
- self.offline()
42
- elif isinstance(status, tgapi.UserStatusLastMonth):
43
- self.inactive()
44
- self.extended_away(
45
- "Offline since last month"
46
- if global_config.LAST_SEEN_FALLBACK
47
- else None,
48
- last_seen=datetime.now() - timedelta(days=31),
49
- )
50
- elif isinstance(status, tgapi.UserStatusLastWeek):
51
- self.inactive()
52
- self.extended_away(
53
- "Offline since last week" if global_config.LAST_SEEN_FALLBACK else None,
54
- last_seen=datetime.now() - timedelta(days=7),
55
- )
56
- elif isinstance(status, tgapi.UserStatusOffline):
57
- self.inactive()
58
- if self._online_expire_task.done():
59
- # we've never seen the contact online, so we use the was_online timestamp
60
- self.away(last_seen=datetime.fromtimestamp(status.was_online))
61
- elif isinstance(status, tgapi.UserStatusOnline):
62
- self.online()
63
- self.active()
64
- self._online_expire_task.cancel()
65
- self._online_expire_task = self.xmpp.loop.create_task(
66
- self._expire_online(status.expires)
67
- )
68
- elif isinstance(status, tgapi.UserStatusRecently):
69
- self.inactive()
70
- self.away(
71
- "Last seen recently" if global_config.LAST_SEEN_FALLBACK else None,
72
- last_seen=datetime.now(),
73
- )
74
-
75
- async def update_info_from_user(self, user: Optional[tgapi.User] = None):
76
- if user is None:
77
- user = await self.session.tg.api.get_user(self.legacy_id)
78
- if username := user.username:
79
- name = username
80
- else:
81
- name = user.first_name
82
- if last := user.last_name:
83
- name += " " + last
84
- self.name = name
85
-
86
- if photo := user.profile_photo:
87
- if (local := photo.small.local) and (path := local.path):
88
- with open(path, "rb") as f:
89
- self.avatar = f.read()
90
- else:
91
- response = await self.session.tg.api.download_file(
92
- file_id=photo.small.id,
93
- synchronous=True,
94
- priority=1,
95
- offset=0,
96
- limit=0,
97
- )
98
- with open(response.local.path, "rb") as f:
99
- self.avatar = f.read()
100
-
101
- if isinstance(user.type_, tgapi.UserTypeBot) or user.id == 777000:
102
- # 777000 is not marked as bot, it's the "Telegram" contact, which gives
103
- # confirmation codes and announces telegram-related stuff
104
- self.CLIENT_TYPE = "bot"
105
-
106
- else:
107
- if user.is_contact:
108
- self._subscribe_to = True
109
- self._subscribe_from = user.is_mutual_contact
110
- else:
111
- self._subscribe_to = self._subscribe_from = False
112
-
113
- self.update_status(user.status)
114
-
115
- if p := user.phone_number:
116
- phone = "+" + p
117
- else:
118
- phone = None
119
- self.set_vcard(
120
- given=user.first_name, surname=user.last_name, phone=phone, full_name=name
121
- )
122
-
123
- async def update_info_from_chat(self, chat: tgapi.Chat):
124
- self.name = chat.title
125
- if isinstance(chat.photo, tgapi.ChatPhotoInfo):
126
- if (local := chat.photo.small.local) and (path := local.path):
127
- with open(path, "rb") as f:
128
- self.avatar = f.read()
129
- else:
130
- response = await self.session.tg.api.download_file(
131
- file_id=chat.photo.small.id,
132
- synchronous=True,
133
- priority=1,
134
- offset=0,
135
- limit=0,
136
- )
137
- with open(response.local.path, "rb") as f:
138
- self.avatar = f.read()
139
-
140
-
141
- class Roster(LegacyRoster["Session", "Contact", int]):
142
- async def jid_username_to_legacy_id(self, jid_username: str) -> int:
143
- try:
144
- tg_id = int(jid_username)
145
- except ValueError:
146
- raise XMPPError("bad-request", "This is not a telegram user ID")
147
- else:
148
- if tg_id > 0:
149
- return tg_id
150
- else:
151
- raise XMPPError("bad-request", "This looks like a telegram group ID")
152
-
153
-
154
- log = logging.getLogger(__name__)
@@ -1,182 +0,0 @@
1
- import asyncio
2
- import logging
3
- import typing
4
- from datetime import datetime
5
-
6
- import aiotdlib.api as tgapi
7
- from slixmpp import JID, Iq
8
- from slixmpp.exceptions import XMPPError
9
-
10
- from slidge import *
11
- from slidge.core.adhoc import RegistrationType
12
-
13
- from ...util import is_valid_phone_number
14
- from . import config
15
- from .client import CredentialsValidation
16
-
17
- if typing.TYPE_CHECKING:
18
- from .session import Session
19
-
20
- REGISTRATION_INSTRUCTIONS = (
21
- "You need to create a telegram account in an official telegram client.\n\n"
22
- "Then you can enter your phone number here, and you will receive a confirmation code "
23
- "in the official telegram client. "
24
- "You can uninstall the telegram client after this if you want."
25
- )
26
-
27
-
28
- class Gateway(BaseGateway["Session"]):
29
- REGISTRATION_INSTRUCTIONS = REGISTRATION_INSTRUCTIONS
30
- REGISTRATION_FIELDS = [FormField(var="phone", label="Phone number", required=True)]
31
- REGISTRATION_TYPE = RegistrationType.TWO_FACTOR_CODE
32
- ROSTER_GROUP = "Telegram"
33
- COMPONENT_NAME = "Telegram (slidge)"
34
- COMPONENT_TYPE = "telegram"
35
- COMPONENT_AVATAR = "https://web.telegram.org/img/logo_share.png"
36
-
37
- SEARCH_FIELDS = [
38
- FormField(var="phone", label="Phone number", required=True),
39
- FormField(var="first", label="First name", required=True),
40
- FormField(var="last", label="Last name", required=False),
41
- ]
42
-
43
- GROUPS = True
44
-
45
- def __init__(self):
46
- super().__init__()
47
- if config.TDLIB_PATH is None:
48
- config.TDLIB_PATH = global_config.HOME_DIR / "tdlib"
49
- self._pending_registrations = dict[
50
- str, tuple[asyncio.Task[CredentialsValidation], CredentialsValidation]
51
- ]()
52
- if not config.API_ID:
53
- self.REGISTRATION_FIELDS.extend(
54
- [
55
- FormField(
56
- var="info",
57
- type="fixed",
58
- label="Get API id and hash on https://my.telegram.org/apps",
59
- ),
60
- FormField(var="api_id", label="API ID", required=True),
61
- FormField(var="api_hash", label="API Hash", required=True),
62
- ]
63
- )
64
- log.debug("CONFIG %s", vars(config))
65
-
66
- async def validate(
67
- self, user_jid: JID, registration_form: dict[str, typing.Optional[str]]
68
- ):
69
- phone = registration_form.get("phone")
70
- if not is_valid_phone_number(phone):
71
- raise ValueError("Not a valid phone number")
72
- tg_client = CredentialsValidation(registration_form) # type: ignore
73
- auth_task = self.loop.create_task(tg_client.start())
74
- self._pending_registrations[user_jid.bare] = auth_task, tg_client
75
-
76
- async def validate_two_factor_code(self, user: GatewayUser, code: str):
77
- auth_task, tg_client = self._pending_registrations.pop(user.bare_jid)
78
- tg_client.code_future.set_result(code)
79
- try:
80
- await asyncio.wait_for(auth_task, config.REGISTRATION_AUTH_CODE_TIMEOUT)
81
- except asyncio.TimeoutError:
82
- raise XMPPError(
83
- "not-authorized",
84
- text="Something went wrong when trying to authenticate you on the "
85
- "telegram network. Please retry and/or contact your slidge admin.",
86
- )
87
- await tg_client.stop()
88
-
89
- async def unregister(self, user: GatewayUser):
90
- session = self.session_cls.from_user(user)
91
- # FIXME: this effectively removes user data from disk, but crashes slidge.
92
- await session.tg.start()
93
- await session.tg.api.log_out()
94
-
95
- def add_adhoc_commands(self):
96
- self.adhoc.add_command(
97
- node="get_sessions",
98
- name="List active sessions",
99
- handler=self.adhoc_active_sessions1,
100
- only_users=True,
101
- )
102
-
103
- async def adhoc_active_sessions1(
104
- self, iq: Iq, adhoc_session: dict[str, typing.Any]
105
- ):
106
- user = user_store.get_by_stanza(iq)
107
- if user is None:
108
- raise XMPPError("subscription-required")
109
- session = self.session_cls.from_stanza(iq)
110
-
111
- form = self["xep_0004"].make_form("form", "Active telegram sessions")
112
- tg_sessions = (await session.tg.api.get_active_sessions()).sessions
113
- form.add_field(
114
- "tg_session_id",
115
- ftype="list-single",
116
- label="Sessions",
117
- options=[{"label": f"{s.country}", "value": s.id} for s in tg_sessions],
118
- )
119
-
120
- adhoc_session["payload"] = form
121
- adhoc_session["next"] = self.adhoc_active_sessions2
122
- adhoc_session["has_next"] = True
123
- adhoc_session["tg_sessions"] = {s.id: s for s in tg_sessions}
124
- adhoc_session["slidge_session"] = session
125
-
126
- return adhoc_session
127
-
128
- async def adhoc_active_sessions2(self, form, adhoc_session: dict[str, typing.Any]):
129
- tg_session_id = int(form.get_values()["tg_session_id"])
130
-
131
- form = self["xep_0004"].make_form("form", "Telegram session info")
132
- tg_session: tgapi.Session = adhoc_session["tg_sessions"][str(tg_session_id)]
133
- for x in fmt_tg_session(tg_session):
134
- form.add_field(
135
- ftype="fixed",
136
- value=x,
137
- )
138
- if tg_session.is_current:
139
- adhoc_session["has_next"] = False
140
- else:
141
- form.add_field(
142
- "terminate", ftype="boolean", label="Terminate session", value="0"
143
- )
144
- form.add_field("tg_session_id", ftype="hidden", value=tg_session.id)
145
- adhoc_session["has_next"] = True
146
- adhoc_session["next"] = self.adhoc_active_sessions3
147
-
148
- adhoc_session["payload"] = form
149
-
150
- return adhoc_session
151
-
152
- async def adhoc_active_sessions3(self, form, adhoc_session: dict[str, typing.Any]):
153
- form_values = form.get_values()
154
- terminate = bool(int(form_values["terminate"]))
155
-
156
- if terminate:
157
- session: Session = adhoc_session["slidge_session"]
158
- await session.tg.api.terminate_session(int(form_values["tg_session_id"]))
159
- info = "Session terminated."
160
- else:
161
- info = "Session not terminated."
162
-
163
- adhoc_session["notes"] = [("info", info)]
164
- adhoc_session["has_next"] = False
165
-
166
- return adhoc_session
167
-
168
-
169
- def fmt_tg_session(s: tgapi.Session):
170
- return [
171
- f"Country: {s.country}",
172
- f"Region: {s.region}",
173
- f"Ip: {s.ip}",
174
- f"App: {s.application_name}",
175
- f"Device: {s.device_model}",
176
- f"Platform: {s.platform}",
177
- f"Since: {datetime.fromtimestamp(s.log_in_date).isoformat()}",
178
- f"Last seen: {datetime.fromtimestamp(s.last_active_date).isoformat()}",
179
- ]
180
-
181
-
182
- log = logging.getLogger(__name__)