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,228 +0,0 @@
1
- import asyncio
2
- import functools
3
- from datetime import datetime
4
- from typing import TYPE_CHECKING
5
-
6
- import aiotdlib
7
- from aiotdlib import api as tgapi
8
-
9
- if TYPE_CHECKING:
10
- from .session import Session
11
-
12
-
13
- class TelegramClient(aiotdlib.Client):
14
- def __init__(self, session: "Session", **kw):
15
- super().__init__(parse_mode=aiotdlib.ClientParseMode.MARKDOWN, **kw)
16
- self.session = session
17
- self.contacts = session.contacts
18
- self.log = self.session.log
19
-
20
- async def input_(prompt):
21
- self.session.send_gateway_status(f"Action required: {prompt}")
22
- return await session.input(prompt)
23
-
24
- self.input = input_
25
- self._auth_get_code = functools.partial(input_, "Enter code")
26
- self._auth_get_password = functools.partial(input_, "Enter 2FA password:")
27
- self._auth_get_first_name = functools.partial(input_, "Enter first name:")
28
- self._auth_get_last_name = functools.partial(input_, "Enter last name:")
29
-
30
- self.add_event_handler(self.dispatch_update, tgapi.API.Types.ANY)
31
-
32
- async def dispatch_update(self, _self, update: tgapi.Update):
33
- try:
34
- handler = getattr(self, "handle_" + update.ID[6:])
35
- except AttributeError:
36
- self.session.log.debug("No handler for %s, ignoring", update.ID)
37
- except IndexError:
38
- self.session.log.debug("Ignoring weird event: %s", update.ID)
39
- else:
40
- await handler(update)
41
-
42
- async def handle_NewMessage(self, update: tgapi.UpdateNewMessage):
43
- if (msg := update.message).is_channel_post:
44
- self.log.debug("Ignoring channel post")
45
- return
46
-
47
- if not await self.is_private_chat(msg.chat_id):
48
- self.log.debug("Ignoring group message")
49
- return
50
-
51
- session = self.session
52
- if msg.is_outgoing:
53
- # This means slidge is responsible for this message, so no carbon is needed;
54
- # but maybe this does not handle all possible cases gracefully?
55
- if msg.sending_state is not None or msg.id in session.sent:
56
- return
57
- content = msg.content
58
- if isinstance(content, tgapi.MessageText):
59
- session.contacts.by_legacy_id(msg.chat_id).carbon(
60
- content.text.text, msg.id, datetime.fromtimestamp(msg.date)
61
- )
62
- # TODO: implement carbons for other contents
63
- return
64
-
65
- sender = msg.sender_id
66
- if not isinstance(sender, tgapi.MessageSenderUser):
67
- self.log.debug("Ignoring non-user sender") # Does this happen?
68
- return
69
-
70
- await session.contacts.by_legacy_id(sender.user_id).send_tg_message(msg)
71
-
72
- async def handle_UserStatus(self, update: tgapi.UpdateUserStatus):
73
- if update.user_id == await self.get_my_id():
74
- return
75
- contact = self.contacts.by_legacy_id(update.user_id)
76
- if not contact.added_to_roster:
77
- self.log.debug("Ignoring presence of contact not in the roster")
78
- return
79
- contact.update_status(update.status)
80
-
81
- async def handle_ChatReadOutbox(self, update: tgapi.UpdateChatReadOutbox):
82
- if not await self.is_private_chat(update.chat_id):
83
- return
84
- self.contacts.by_legacy_id(update.chat_id).displayed(
85
- update.last_read_outbox_message_id
86
- )
87
-
88
- async def handle_ChatAction(self, action: tgapi.UpdateChatAction):
89
- if not await self.is_private_chat(action.chat_id):
90
- return
91
-
92
- sender = action.sender_id
93
- if not isinstance(sender, tgapi.MessageSenderUser):
94
- self.log.debug("Ignoring action: %s", action)
95
- return
96
-
97
- if (chat_id := action.chat_id) != sender.user_id:
98
- self.log.debug("Ignoring group (?) action: %s", action)
99
- return
100
-
101
- self.contacts.by_legacy_id(chat_id).composing()
102
-
103
- async def handle_ChatReadInbox(self, action: tgapi.UpdateChatReadInbox):
104
- if not await self.is_private_chat(action.chat_id):
105
- return
106
-
107
- session = self.session
108
- msg_id = action.last_read_inbox_message_id
109
- self.log.debug(
110
- "Self read mark for %s and we sent %s", msg_id, session.sent_read_marks
111
- )
112
- try:
113
- session.sent_read_marks.remove(msg_id)
114
- except KeyError:
115
- # slidge didn't send this read mark, so it comes from the official tg client
116
- contact = session.contacts.by_legacy_id(action.chat_id)
117
- contact.carbon_read(msg_id)
118
-
119
- async def handle_MessageContent(self, action: tgapi.UpdateMessageContent):
120
- if not await self.is_private_chat(action.chat_id):
121
- return
122
-
123
- new = action.new_content
124
- if not isinstance(new, tgapi.MessageText):
125
- raise NotImplementedError(new)
126
- session = self.session
127
- try:
128
- fut = session.user_correction_futures.pop(action.message_id)
129
- except KeyError:
130
- contact = session.contacts.by_legacy_id(action.chat_id)
131
- if action.message_id in self.session.sent:
132
- contact.carbon_correct(action.message_id, new.text.text)
133
- else:
134
- contact.correct(action.message_id, new.text.text)
135
- else:
136
- self.log.debug("User correction confirmation received")
137
- fut.set_result(None)
138
-
139
- async def handle_User(self, action: tgapi.UpdateUser):
140
- u = action.user
141
- if u.id == await self.get_my_id():
142
- return
143
- contact = self.session.contacts.by_legacy_id(u.id)
144
- await contact.update_info_from_user(u)
145
- await contact.add_to_roster()
146
-
147
- async def handle_MessageInteractionInfo(
148
- self, update: tgapi.UpdateMessageInteractionInfo
149
- ):
150
- if not await self.is_private_chat(update.chat_id):
151
- return
152
-
153
- contact = self.session.contacts.by_legacy_id(update.chat_id)
154
- me = await self.get_my_id()
155
- if update.interaction_info is None:
156
- contact.react(update.message_id, [])
157
- contact.carbon_react(update.message_id, [])
158
- else:
159
- user_reactions = list[str]()
160
- contact_reactions = list[str]()
161
- # these sanity checks might not be necessary, but in doubt…
162
- for reaction in update.interaction_info.reactions:
163
- if reaction.total_count == 1:
164
- if len(reaction.recent_sender_ids) != 1:
165
- self.log.warning(
166
- "Weird reactions (wrong count): %s",
167
- update.interaction_info.reactions,
168
- )
169
- continue
170
- sender = reaction.recent_sender_ids[0]
171
- if isinstance(sender, tgapi.MessageSenderUser):
172
- if sender.user_id == me:
173
- user_reactions.append(reaction.reaction)
174
- elif sender.user_id == contact.legacy_id:
175
- contact_reactions.append(reaction.reaction)
176
- else:
177
- self.log.warning(
178
- "Weird reactions (neither me nor them): %s",
179
- update.interaction_info.reactions,
180
- )
181
- elif reaction.total_count == 2:
182
- user_reactions.append(reaction.reaction)
183
- contact_reactions.append(reaction.reaction)
184
- else:
185
- self.log.warning(
186
- "Weird reactions (empty): %s", update.interaction_info.reactions
187
- )
188
-
189
- contact.react(update.message_id, contact_reactions)
190
- contact.carbon_react(update.message_id, user_reactions)
191
-
192
- async def handle_DeleteMessages(self, update: tgapi.UpdateDeleteMessages):
193
- if not await self.is_private_chat(update.chat_id):
194
- return
195
-
196
- if not update.is_permanent: # tdlib send 'delete from cache' updates apparently
197
- self.log.debug("Ignoring non permanent delete")
198
- return
199
- for legacy_msg_id in update.message_ids:
200
- try:
201
- future = self.session.delete_futures.pop(legacy_msg_id)
202
- except KeyError:
203
- # FIXME: where do we filter out group chat messages here ?!
204
- contact = self.session.contacts.by_legacy_id(update.chat_id)
205
- if legacy_msg_id in self.session.sent:
206
- contact.carbon_retract(legacy_msg_id)
207
- else:
208
- contact.retract(legacy_msg_id)
209
- else:
210
- future.set_result(update)
211
-
212
- async def handle_MessageSendSucceeded(
213
- self, update: tgapi.UpdateMessageSendSucceeded
214
- ):
215
- self.session.sent_read_marks.add(update.message.id)
216
- for _ in range(10):
217
- try:
218
- future = self.session.ack_futures.pop(update.message.id)
219
- except KeyError:
220
- await asyncio.sleep(0.5)
221
- else:
222
- future.set_result(update.message.id)
223
- return
224
- self.log.warning("Ignoring Send success for %s", update.message.id)
225
-
226
- async def is_private_chat(self, chat_id: int):
227
- chat = await self.get_chat(chat_id)
228
- return isinstance(chat.type_, tgapi.ChatTypePrivate)
@@ -1,12 +0,0 @@
1
- from argparse import ArgumentParser
2
-
3
-
4
- def get_parser():
5
- parser = ArgumentParser()
6
- parser.add_argument("--tdlib-path", help="Defaults to ${SLIDGE_HOME_DIR}/tdlib")
7
- parser.add_argument(
8
- "--tdlib-key",
9
- default="NOT_SECURE",
10
- help="Key used to encrypt tdlib persistent DB",
11
- )
12
- return parser
@@ -1,176 +0,0 @@
1
- import asyncio
2
- import logging
3
- import time
4
- from datetime import datetime
5
- from typing import TYPE_CHECKING, Optional, Union
6
-
7
- import aiotdlib.api as tgapi
8
-
9
- from slidge import *
10
-
11
- if TYPE_CHECKING:
12
- from .session import Session
13
-
14
-
15
- async def noop():
16
- return
17
-
18
-
19
- class Contact(LegacyContact["Session"]):
20
- legacy_id: int
21
- # Telegram official clients have no XMPP presence equivalent, but a 'last seen' indication.
22
- CLIENT_TYPE = "phone"
23
-
24
- def __init__(self, *a, **k):
25
- super(Contact, self).__init__(*a, **k)
26
- self._online_expire_task = self.xmpp.loop.create_task(noop())
27
-
28
- @staticmethod
29
- def _format_last_seen(timestamp: Union[int, float]):
30
- return f"Last seen {datetime.fromtimestamp(timestamp):%A %H:%M GMT}"
31
-
32
- async def _expire_online(self, timestamp: Union[int, float]):
33
- now = time.time()
34
- how_long = timestamp - now
35
- log.debug("Online status expires in %s seconds", how_long)
36
- await asyncio.sleep(how_long)
37
- self.away(self._format_last_seen(now))
38
-
39
- def update_status(self, status: tgapi.UserStatus):
40
- if isinstance(status, tgapi.UserStatusEmpty):
41
- self.inactive()
42
- self.offline()
43
- elif isinstance(status, tgapi.UserStatusLastMonth):
44
- self.inactive()
45
- self.extended_away("Offline since last month")
46
- elif isinstance(status, tgapi.UserStatusLastWeek):
47
- self.inactive()
48
- self.extended_away("Offline since last week")
49
- elif isinstance(status, tgapi.UserStatusOffline):
50
- self.inactive()
51
- if self._online_expire_task.done():
52
- # we've never seen the contact online, so we use the was_online timestamp
53
- self.away(self._format_last_seen(status.was_online))
54
- elif isinstance(status, tgapi.UserStatusOnline):
55
- self.online()
56
- self.active()
57
- self._online_expire_task.cancel()
58
- self._online_expire_task = self.xmpp.loop.create_task(
59
- self._expire_online(status.expires)
60
- )
61
- elif isinstance(status, tgapi.UserStatusRecently):
62
- self.inactive()
63
- self.away("Last seen recently")
64
-
65
- async def send_tg_message(self, msg: tgapi.Message):
66
- content = msg.content
67
- if isinstance(content, tgapi.MessageText):
68
- # TODO: parse formatted text to markdown
69
- formatted_text = content.text
70
- self.send_text(
71
- body=formatted_text.text,
72
- legacy_msg_id=msg.id,
73
- reply_to_msg_id=msg.reply_to_message_id,
74
- )
75
- elif isinstance(content, tgapi.MessageAnimatedEmoji):
76
- emoji = content.animated_emoji.sticker.emoji
77
- self.send_text(
78
- body=emoji,
79
- legacy_msg_id=msg.id,
80
- reply_to_msg_id=msg.reply_to_message_id,
81
- )
82
- elif isinstance(content, tgapi.MessagePhoto):
83
- photo = content.photo
84
- best_file = max(photo.sizes, key=lambda x: x.width).photo
85
- await self.send_tg_file(best_file, content.caption, msg.id)
86
- elif isinstance(content, tgapi.MessageVideo):
87
- best_file = content.video.video
88
- await self.send_tg_file(best_file, content.caption, msg.id)
89
- elif isinstance(content, tgapi.MessageAnimation):
90
- best_file = content.animation.animation
91
- await self.send_tg_file(best_file, content.caption, msg.id)
92
- else:
93
- self.session.log.debug("Ignoring content: %s", type(content))
94
-
95
- async def send_tg_file(self, best_file, caption, msg_id):
96
- query = tgapi.DownloadFile.construct(
97
- file_id=best_file.id, synchronous=True, priority=1
98
- )
99
- best_file_downloaded: tgapi.File = await self.session.tg.request(query)
100
- await self.send_file(best_file_downloaded.local.path)
101
- if caption.text:
102
- self.send_text(caption.text, legacy_msg_id=msg_id)
103
-
104
- async def update_info_from_user(self, user: Optional[tgapi.User] = None):
105
- if user is None:
106
- user = await self.session.tg.api.get_user(self.legacy_id)
107
- if username := user.username:
108
- name = username
109
- else:
110
- name = user.first_name
111
- if last := user.last_name:
112
- name += " " + last
113
- self.name = name
114
-
115
- if photo := user.profile_photo:
116
- if (local := photo.small.local) and (path := local.path):
117
- with open(path, "rb") as f:
118
- self.avatar = f.read()
119
- else:
120
- response = await self.session.tg.api.download_file(
121
- file_id=photo.small.id,
122
- synchronous=True,
123
- priority=1,
124
- offset=0,
125
- limit=0,
126
- )
127
- with open(response.local.path, "rb") as f:
128
- self.avatar = f.read()
129
-
130
- if isinstance(user.type_, tgapi.UserTypeBot) or user.id == 777000:
131
- # 777000 is not marked as bot, it's the "Telegram" contact, which gives
132
- # confirmation codes and announces telegram-related stuff
133
- self.CLIENT_TYPE = "bot"
134
-
135
- else:
136
- if user.is_contact:
137
- self._subscribe_to = True
138
- self._subscribe_from = user.is_mutual_contact
139
- else:
140
- self._subscribe_to = self._subscribe_from = False
141
-
142
- self.update_status(user.status)
143
-
144
- if p := user.phone_number:
145
- phone = "+" + p
146
- else:
147
- phone = None
148
- self.set_vcard(
149
- given=user.first_name, surname=user.last_name, phone=phone, full_name=name
150
- )
151
-
152
- async def update_info_from_chat(self, chat: tgapi.Chat):
153
- self.name = chat.title
154
- if isinstance(chat.photo, tgapi.ChatPhotoInfo):
155
- if (local := chat.photo.small.local) and (path := local.path):
156
- with open(path, "rb") as f:
157
- self.avatar = f.read()
158
- else:
159
- response = await self.session.tg.api.download_file(
160
- file_id=chat.photo.small.id,
161
- synchronous=True,
162
- priority=1,
163
- offset=0,
164
- limit=0,
165
- )
166
- with open(response.local.path, "rb") as f:
167
- self.avatar = f.read()
168
-
169
-
170
- class Roster(LegacyRoster["Contact", "Session"]):
171
- @staticmethod
172
- def jid_username_to_legacy_id(jid_username: str) -> int:
173
- return int(jid_username)
174
-
175
-
176
- log = logging.getLogger(__name__)
@@ -1,150 +0,0 @@
1
- import logging
2
- import typing
3
- from argparse import Namespace
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
-
12
- from ...util import is_valid_phone_number
13
- from .config import get_parser
14
-
15
- if typing.TYPE_CHECKING:
16
- from .session import Session
17
-
18
- REGISTRATION_INSTRUCTIONS = """You can visit https://my.telegram.org/apps to get an API ID and an API HASH
19
-
20
- This is the only tested login method, but other methods (password, bot token, 2FA...)
21
- should work too, in theory at least.
22
- """
23
-
24
-
25
- class Gateway(BaseGateway["Session"]):
26
- REGISTRATION_INSTRUCTIONS = REGISTRATION_INSTRUCTIONS
27
- REGISTRATION_FIELDS = [
28
- FormField(var="phone", label="Phone number", required=True),
29
- FormField(var="api_id", label="API ID", required=False),
30
- FormField(var="api_hash", label="API hash", required=False),
31
- FormField(var="", value="The fields below have not been tested", type="fixed"),
32
- FormField(var="bot_token", label="Bot token", required=False),
33
- FormField(var="first", label="First name", required=False),
34
- FormField(var="last", label="Last name", required=False),
35
- ]
36
- ROSTER_GROUP = "Telegram"
37
- COMPONENT_NAME = "Telegram (slidge)"
38
- COMPONENT_TYPE = "telegram"
39
- COMPONENT_AVATAR = "https://web.telegram.org/img/logo_share.png"
40
-
41
- SEARCH_FIELDS = [
42
- FormField(var="phone", label="Phone number", required=True),
43
- FormField(var="first", label="First name", required=True),
44
- FormField(var="last", label="Last name", required=False),
45
- ]
46
-
47
- args: Namespace
48
-
49
- def config(self, argv: list[str]):
50
- Gateway.args = args = get_parser().parse_args(argv)
51
- if args.tdlib_path is None:
52
- args.tdlib_path = self.home_dir / "tdlib"
53
-
54
- async def validate(
55
- self, user_jid: JID, registration_form: dict[str, typing.Optional[str]]
56
- ):
57
- phone = registration_form.get("phone")
58
- if not is_valid_phone_number(phone):
59
- raise ValueError("Not a valid phone number")
60
-
61
- async def unregister(self, user):
62
- pass
63
-
64
- def add_adhoc_commands(self):
65
- self["xep_0050"].add_command(
66
- node="get_sessions",
67
- name="List active sessions",
68
- handler=self.adhoc_active_sessions1,
69
- )
70
-
71
- async def adhoc_active_sessions1(
72
- self, iq: Iq, adhoc_session: dict[str, typing.Any]
73
- ):
74
- user = user_store.get_by_stanza(iq)
75
- if user is None:
76
- raise XMPPError("subscription-required")
77
- session = self._session_cls.from_stanza(iq)
78
-
79
- form = self["xep_0004"].make_form("form", "Active telegram sessions")
80
- tg_sessions = (await session.tg.api.get_active_sessions()).sessions
81
- form.add_field(
82
- "tg_session_id",
83
- ftype="list-single",
84
- label="Sessions",
85
- options=[{"label": f"{s.country}", "value": s.id} for s in tg_sessions],
86
- )
87
-
88
- adhoc_session["payload"] = form
89
- adhoc_session["next"] = self.adhoc_active_sessions2
90
- adhoc_session["has_next"] = True
91
- adhoc_session["tg_sessions"] = {s.id: s for s in tg_sessions}
92
- adhoc_session["slidge_session"] = session
93
-
94
- return adhoc_session
95
-
96
- async def adhoc_active_sessions2(self, form, adhoc_session: dict[str, typing.Any]):
97
- tg_session_id = int(form.get_values()["tg_session_id"])
98
-
99
- form = self["xep_0004"].make_form("form", "Telegram session info")
100
- tg_session: tgapi.Session = adhoc_session["tg_sessions"][str(tg_session_id)]
101
- for x in fmt_tg_session(tg_session):
102
- form.add_field(
103
- ftype="fixed",
104
- value=x,
105
- )
106
- if tg_session.is_current:
107
- adhoc_session["has_next"] = False
108
- else:
109
- form.add_field(
110
- "terminate", ftype="boolean", label="Terminate session", value="0"
111
- )
112
- form.add_field("tg_session_id", ftype="hidden", value=tg_session.id)
113
- adhoc_session["has_next"] = True
114
- adhoc_session["next"] = self.adhoc_active_sessions3
115
-
116
- adhoc_session["payload"] = form
117
-
118
- return adhoc_session
119
-
120
- async def adhoc_active_sessions3(self, form, adhoc_session: dict[str, typing.Any]):
121
- form_values = form.get_values()
122
- terminate = bool(int(form_values["terminate"]))
123
-
124
- if terminate:
125
- session: Session = adhoc_session["slidge_session"]
126
- await session.tg.api.terminate_session(int(form_values["tg_session_id"]))
127
- info = "Session terminated."
128
- else:
129
- info = "Session not terminated."
130
-
131
- adhoc_session["notes"] = [("info", info)]
132
- adhoc_session["has_next"] = False
133
-
134
- return adhoc_session
135
-
136
-
137
- def fmt_tg_session(s: tgapi.Session):
138
- return [
139
- f"Country: {s.country}",
140
- f"Region: {s.region}",
141
- f"Ip: {s.ip}",
142
- f"App: {s.application_name}",
143
- f"Device: {s.device_model}",
144
- f"Platform: {s.platform}",
145
- f"Since: {datetime.fromtimestamp(s.log_in_date).isoformat()}",
146
- f"Last seen: {datetime.fromtimestamp(s.last_active_date).isoformat()}",
147
- ]
148
-
149
-
150
- log = logging.getLogger(__name__)