slidge 0.1.0rc1__py3-none-any.whl → 0.1.2__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 +795 -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.2.dist-info/METADATA +111 -0
  87. slidge-0.1.2.dist-info/RECORD +96 -0
  88. {slidge-0.1.0rc1.dist-info → slidge-0.1.2.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.2.dist-info}/LICENSE +0 -0
  164. {slidge-0.1.0rc1.dist-info → slidge-0.1.2.dist-info}/entry_points.txt +0 -0
@@ -1,184 +0,0 @@
1
- from datetime import datetime, timezone
2
- from typing import TYPE_CHECKING, Optional
3
-
4
- import aiotdlib.api as tgapi
5
- from slixmpp import JID
6
- from slixmpp.exceptions import XMPPError
7
-
8
- from slidge import *
9
-
10
- from .util import AvailableEmojisMixin, TelegramToXMPPMixin
11
-
12
- if TYPE_CHECKING:
13
- from .contact import Contact
14
- from .session import Session
15
-
16
-
17
- class Bookmarks(LegacyBookmarks):
18
- @staticmethod
19
- async def legacy_id_to_jid_local_part(legacy_id: int):
20
- return "group" + str(legacy_id)
21
-
22
- @staticmethod
23
- async def jid_local_part_to_legacy_id(local_part: str):
24
- return int(local_part.replace("group", ""))
25
-
26
-
27
- class MUC(LegacyMUC["Session", int, "Participant", int], AvailableEmojisMixin):
28
- MAX_SUPER_GROUP_PARTICIPANTS = 200
29
- session: "Session"
30
- name = "unnamed"
31
-
32
- async def join(self, join_presence):
33
- self.user_nick = await self.session.my_name
34
- await self.update_subject_from_msg()
35
- await super().join(join_presence)
36
-
37
- async def update_subject_from_msg(self, msg: Optional[tgapi.Message] = None):
38
- if msg is None:
39
- try:
40
- msg = await self.session.tg.api.get_chat_pinned_message(self.legacy_id)
41
- self.log.debug("Pinned message: %s", type(msg.content))
42
- except tgapi.NotFound:
43
- self.log.debug("Pinned message not found?")
44
- return
45
- content = msg.content
46
- if not isinstance(content, (tgapi.MessagePhoto, tgapi.MessageText)):
47
- return
48
-
49
- sender_id = msg.sender_id
50
- self.subject_date = datetime.fromtimestamp(msg.date, tz=timezone.utc)
51
- if isinstance(sender_id, tgapi.MessageSenderUser):
52
- if sender_id.user_id == await self.session.tg.get_my_id():
53
- self.subject_setter = self.user_nick
54
- else:
55
- contact = await self.session.contacts.by_legacy_id(sender_id.user_id)
56
- self.subject_setter = contact.name
57
- else:
58
- self.subject_setter = self.name
59
-
60
- if isinstance(content, tgapi.MessagePhoto):
61
- self.subject = content.caption.text
62
- if isinstance(content, tgapi.MessageText):
63
- self.subject = content.text.text
64
-
65
- async def get_participants(self):
66
- self.log.debug("Getting participants")
67
- chat = await self.session.tg.get_chat(chat_id=self.legacy_id)
68
- if not isinstance(
69
- chat.type_, (tgapi.ChatTypeBasicGroup, tgapi.ChatTypeSupergroup)
70
- ):
71
- raise XMPPError("item-not-found", text="This is not a valid group ID")
72
-
73
- info = await self.session.tg.get_chat_info(chat, full=True)
74
- if isinstance(info, tgapi.BasicGroupFullInfo):
75
- members = info.members
76
- elif isinstance(info, tgapi.SupergroupFullInfo):
77
- if info.can_get_members:
78
- members = (
79
- await self.session.tg.api.get_supergroup_members(
80
- supergroup_id=chat.type_.supergroup_id,
81
- filter_=None,
82
- offset=0,
83
- limit=self.MAX_SUPER_GROUP_PARTICIPANTS,
84
- skip_validation=True,
85
- )
86
- ).members
87
- else:
88
- members = []
89
- else:
90
- raise RuntimeError
91
- self.log.debug("%s participants", len(members))
92
- for member in members:
93
- sender = member.member_id
94
- if not isinstance(sender, tgapi.MessageSenderUser):
95
- self.log.debug("Ignoring non-user sender") # Does this happen?
96
- continue
97
- if sender.user_id == await self.session.tg.get_my_id():
98
- continue
99
- yield await self.participant_by_tg_user(
100
- await self.session.tg.get_user(sender.user_id)
101
- )
102
-
103
- async def send_text(self, text: str) -> int:
104
- result = await self.session.tg.send_text(self.legacy_id, text)
105
- self.log.debug("MUC SEND RESULT: %s", result)
106
- msg_id = await self.session.wait_for_tdlib_success(result.id)
107
- self.log.debug("MUC SEND MSG: %s", msg_id)
108
- return msg_id
109
-
110
- async def participant_by_tg_user(self, user: tgapi.User) -> "Participant":
111
- return await Participant.by_tg_user(self, user)
112
-
113
- async def participant_system(self) -> "Participant":
114
- return await self.get_participant("")
115
-
116
- async def participant_by_tg_user_id(self, user_id: int) -> "Participant":
117
- return await Participant.by_tg_user(
118
- self, await self.session.tg.api.get_user(user_id)
119
- )
120
-
121
- async def get_tg_chat(self):
122
- return await self.session.tg.get_chat(self.legacy_id)
123
-
124
- async def fill_history(
125
- self,
126
- full_jid: JID,
127
- maxchars: Optional[int] = None,
128
- maxstanzas: Optional[int] = None,
129
- seconds: Optional[int] = None,
130
- since: Optional[int] = None,
131
- ):
132
- for m in await self.fetch_history(50):
133
- part = await self.participant_by_tg_user(
134
- await self.session.tg.get_user(m.sender_id.user_id)
135
- )
136
- await part.send_tg_message(m, full_jid=full_jid)
137
-
138
- async def fetch_history(self, n: int):
139
- tg = self.session.tg
140
- chat = await self.get_tg_chat()
141
- m = chat.last_message
142
- if m is None:
143
- return []
144
-
145
- messages = [chat.last_message]
146
- i = 0
147
- last_message_id = m.id
148
- while True:
149
- fetched = (
150
- await tg.api.get_chat_history(
151
- chat_id=self.legacy_id,
152
- from_message_id=last_message_id,
153
- offset=0,
154
- limit=10,
155
- only_local=False,
156
- )
157
- ).messages
158
- if len(fetched) == 0:
159
- break
160
- messages.extend(fetched)
161
- i += len(fetched)
162
- if i > n:
163
- break
164
-
165
- last_message_id = fetched[-1].id
166
-
167
- return reversed(messages)
168
-
169
-
170
- class Participant(LegacyParticipant[MUC], TelegramToXMPPMixin):
171
- contact: "Contact"
172
- session: "Session" # type:ignore
173
-
174
- def __init__(self, *a, **k):
175
- super().__init__(*a, **k)
176
- self.chat_id = self.muc.legacy_id
177
- self.session.log.debug("PARTICIPANT-N: %s", self.muc.n_participants)
178
-
179
- @staticmethod
180
- async def by_tg_user(muc: MUC, user: tgapi.User):
181
- nick = " ".join((user.first_name, user.last_name)).strip()
182
- p = Participant(muc, nick)
183
- p.contact = await muc.session.contacts.by_legacy_id(user.id)
184
- return p
@@ -1,275 +0,0 @@
1
- import asyncio
2
- import functools
3
- import logging
4
- import re
5
- import tempfile
6
- from mimetypes import guess_type
7
- from typing import Union
8
-
9
- import aiohttp
10
- import aiotdlib.api as tgapi
11
- from aiotdlib.api.errors import BadRequest
12
- from slixmpp.exceptions import XMPPError
13
-
14
- from slidge import *
15
-
16
- from ...util.types import Chat
17
- from . import config
18
- from .client import TelegramClient
19
- from .contact import Contact, Roster
20
- from .gateway import Gateway
21
- from .group import MUC
22
-
23
-
24
- def catch_chat_not_found(coroutine):
25
- @functools.wraps(coroutine)
26
- async def wrapped(*a, **k):
27
- try:
28
- return await coroutine(*a, **k)
29
- except tgapi.BadRequest as e:
30
- if e.code == 400:
31
- raise XMPPError(condition="item-not-found", text="Recipient not found")
32
- else:
33
- raise
34
-
35
- return wrapped
36
-
37
-
38
- class Session(
39
- BaseSession[Gateway, int, Roster, Contact, LegacyBookmarks, MUC, LegacyParticipant]
40
- ):
41
- def __init__(self, user):
42
- super().__init__(user)
43
- self.sent_read_marks = set[int]()
44
- self.ack_futures = dict[int, asyncio.Future]()
45
- self.user_correction_futures = dict[int, asyncio.Future]()
46
- self.delete_futures = dict[int, asyncio.Future]()
47
-
48
- self.my_name: asyncio.Future[str] = self.xmpp.loop.create_future()
49
-
50
- self.tg = TelegramClient(self)
51
-
52
- @staticmethod
53
- def xmpp_msg_id_to_legacy_msg_id(i: str) -> int:
54
- return int(i)
55
-
56
- async def login(self):
57
- await self.tg.start()
58
- await self.add_contacts_to_roster()
59
- await self.add_groups()
60
- me = await self.tg.get_user(await self.tg.get_my_id())
61
- my_name = (me.first_name + " " + me.last_name).strip()
62
- self.my_name.set_result(my_name)
63
- return f"Connected as {my_name}"
64
-
65
- async def logout(self):
66
- await self.tg.stop()
67
-
68
- async def wait_for_tdlib_success(self, result_id: int):
69
- fut = self.xmpp.loop.create_future()
70
- self.ack_futures[result_id] = fut
71
- return await fut
72
-
73
- @catch_chat_not_found
74
- async def send_text(
75
- self,
76
- text: str,
77
- chat: Union[Contact, MUC],
78
- *,
79
- reply_to_msg_id=None,
80
- reply_to_fallback_text=None,
81
- reply_to=None,
82
- **kwargs,
83
- ) -> int:
84
- text = escape(text)
85
- result = await self.tg.send_text(
86
- chat_id=chat.legacy_id, text=text, reply_to_message_id=reply_to_msg_id
87
- )
88
- new_message_id = await self.wait_for_tdlib_success(result.id)
89
- self.log.debug("Result: %s / %s", result, new_message_id)
90
- return new_message_id
91
-
92
- @catch_chat_not_found
93
- async def send_file(
94
- self,
95
- url: str,
96
- chat: Chat,
97
- reply_to_msg_id=None,
98
- **kwargs,
99
- ) -> int:
100
- type_, _ = guess_type(url)
101
- if type_ is not None:
102
- type_, subtype = type_.split("/")
103
-
104
- async with aiohttp.ClientSession() as session:
105
- async with session.get(url) as response:
106
- response.raise_for_status()
107
- kwargs = dict(
108
- chat_id=chat.legacy_id, reply_to_message_id=reply_to_msg_id
109
- )
110
- with tempfile.NamedTemporaryFile() as file:
111
- bytes_ = await response.read()
112
- file.write(bytes_)
113
- if type_ == "image":
114
- result = await self.tg.send_photo(photo=file.name, **kwargs)
115
- elif type_ == "video":
116
- result = await self.tg.send_video(video=file.name, **kwargs)
117
- elif type_ == "audio":
118
- result = await self.tg.send_audio(audio=file.name, **kwargs)
119
- else:
120
- result = await self.tg.send_document(
121
- document=file.name, **kwargs
122
- )
123
-
124
- return result.id
125
-
126
- @catch_chat_not_found
127
- async def active(self, c: "Contact"):
128
- res = await self.tg.api.open_chat(chat_id=c.legacy_id)
129
- self.log.debug("Open chat res: %s", res)
130
-
131
- @catch_chat_not_found
132
- async def inactive(self, c: "Contact"):
133
- res = await self.tg.api.close_chat(chat_id=c.legacy_id)
134
- self.log.debug("Close chat res: %s", res)
135
-
136
- @catch_chat_not_found
137
- async def composing(self, c: "Contact"):
138
- res = await self.tg.api.send_chat_action(
139
- chat_id=c.legacy_id,
140
- action=tgapi.ChatActionTyping(),
141
- message_thread_id=0, # TODO: check what telegram's threads really are
142
- )
143
- self.log.debug("Send composing res: %s", res)
144
-
145
- async def paused(self, c: "Contact"):
146
- pass
147
-
148
- @catch_chat_not_found
149
- async def displayed(self, tg_id: int, c: "Contact"):
150
- res = await self.tg.api.view_messages(
151
- chat_id=c.legacy_id,
152
- message_thread_id=0,
153
- message_ids=[tg_id],
154
- force_read=True,
155
- )
156
- self.log.debug("Send chat action res: %s", res)
157
-
158
- @catch_chat_not_found
159
- async def add_contacts_to_roster(self):
160
- users = await self.tg.api.get_contacts()
161
- for id_ in users.user_ids:
162
- contact = await self.contacts.by_legacy_id(id_)
163
- await contact.add_to_roster()
164
- await contact.update_info_from_user()
165
-
166
- async def add_groups(self):
167
- for chat in await self.tg.get_main_list_chats_all():
168
- if isinstance(chat.type_, tgapi.ChatTypeBasicGroup):
169
- muc = await self.bookmarks.by_legacy_id(chat.id)
170
- group = await self.tg.get_basic_group(chat.type_.basic_group_id)
171
- muc.type = MucType.GROUP
172
- elif isinstance(chat.type_, tgapi.ChatTypeSupergroup):
173
- muc = await self.bookmarks.by_legacy_id(chat.id)
174
- group = await self.tg.get_supergroup(chat.type_.supergroup_id)
175
- muc.type = MucType.CHANNEL
176
- else:
177
- continue
178
-
179
- muc.n_participants = group.member_count
180
- muc.DISCO_NAME = chat.title
181
-
182
- @catch_chat_not_found
183
- async def correct(self, text: str, legacy_msg_id: int, c: "Contact"):
184
- f = self.user_correction_futures[legacy_msg_id] = self.xmpp.loop.create_future()
185
- await self.tg.api.edit_message_text(
186
- chat_id=c.legacy_id,
187
- message_id=legacy_msg_id,
188
- reply_markup=None,
189
- input_message_content=tgapi.InputMessageText.construct(
190
- text=tgapi.FormattedText.construct(text=text)
191
- ),
192
- skip_validation=True,
193
- )
194
- await f
195
-
196
- @catch_chat_not_found
197
- async def search(self, form_values: dict[str, str]):
198
- phone = form_values["phone"]
199
- first = form_values.get("first", phone)
200
- last = form_values.get("last", "")
201
- response = await self.tg.api.import_contacts(
202
- contacts=[
203
- tgapi.Contact(
204
- phone_number=phone,
205
- user_id=0,
206
- first_name=first,
207
- vcard="",
208
- last_name=last,
209
- )
210
- ]
211
- )
212
- user_id = response.user_ids[0]
213
- if user_id == 0:
214
- return
215
-
216
- await self.add_contacts_to_roster()
217
- contact = await self.contacts.by_legacy_id(user_id)
218
- await contact.update_info_from_user()
219
- await contact.add_to_roster()
220
-
221
- return SearchResult(
222
- fields=[FormField("phone"), FormField("jid", type="jid-single")],
223
- items=[{"phone": form_values["phone"], "jid": contact.jid.bare}],
224
- )
225
-
226
- @catch_chat_not_found
227
- async def remove_reactions(self, legacy_msg_id, c: "Contact"):
228
- try:
229
- r = await self.tg.api.set_message_reaction(
230
- chat_id=c.legacy_id,
231
- message_id=legacy_msg_id,
232
- reaction="",
233
- is_big=False,
234
- )
235
- except BadRequest as e:
236
- self.log.debug("Remove reaction error: %s", e)
237
- else:
238
- self.log.debug("Remove reaction response: %s", r)
239
-
240
- @catch_chat_not_found
241
- async def react(self, legacy_msg_id: int, emojis: list[str], c: "Contact"):
242
- if len(emojis) == 0:
243
- await self.remove_reactions(legacy_msg_id, c)
244
- return
245
-
246
- # we never have more than 1 emoji, slidge core makes sure of that
247
- try:
248
- r = await self.tg.api.set_message_reaction(
249
- chat_id=c.legacy_id,
250
- message_id=legacy_msg_id,
251
- reaction=emojis[0],
252
- is_big=False,
253
- )
254
- except BadRequest as e:
255
- raise XMPPError("bad-request", text=e.message)
256
- else:
257
- self.log.debug("Message reaction response: %s", r)
258
-
259
- @catch_chat_not_found
260
- async def retract(self, legacy_msg_id, c):
261
- f = self.delete_futures[legacy_msg_id] = self.xmpp.loop.create_future()
262
- r = await self.tg.api.delete_messages(c.legacy_id, [legacy_msg_id], revoke=True)
263
- self.log.debug("Delete message response: %s", r)
264
- confirmation = await f
265
- self.log.debug("Message delete confirmation: %s", confirmation)
266
-
267
-
268
- def escape(t: str):
269
- return re.sub(ESCAPE_PATTERN, r"\\\1", t)
270
-
271
-
272
- RESERVED_CHARS = "_*[]()~`>#+-=|{}.!"
273
- ESCAPE_PATTERN = re.compile(f"([{re.escape(RESERVED_CHARS)}])")
274
-
275
- log = logging.getLogger(__name__)
@@ -1,153 +0,0 @@
1
- from datetime import datetime
2
- from typing import TYPE_CHECKING
3
-
4
- import aiotdlib.api as tgapi
5
-
6
- if TYPE_CHECKING:
7
- from .session import Session
8
-
9
-
10
- def get_best_file(content: tgapi.MessageContent):
11
- if isinstance(content, tgapi.MessagePhoto):
12
- photo = content.photo
13
- return max(photo.sizes, key=lambda x: x.width).photo
14
- elif isinstance(content, tgapi.MessageVideo):
15
- return content.video.video
16
- elif isinstance(content, tgapi.MessageAnimation):
17
- return content.animation.animation
18
- elif isinstance(content, tgapi.MessageAudio):
19
- return content.audio.audio
20
- elif isinstance(content, tgapi.MessageDocument):
21
- return content.document.document
22
-
23
-
24
- class AvailableEmojisMixin:
25
- session: "Session"
26
- chat_id: int
27
-
28
- async def available_emojis(self, legacy_msg_id):
29
- available = await self.session.tg.api.get_message_available_reactions(
30
- chat_id=self.chat_id, message_id=legacy_msg_id
31
- )
32
- return {a.reaction for a in available.reactions}
33
-
34
-
35
- class TelegramToXMPPMixin:
36
- session: "Session" # type:ignore
37
- chat_id: int
38
- is_group: bool = NotImplemented
39
-
40
- def send_text(self, *a, **k):
41
- raise NotImplemented
42
-
43
- def send_file(self, *a, **k):
44
- raise NotImplemented
45
-
46
- async def send_tg_message(self, msg: tgapi.Message, **kwargs):
47
- content = msg.content
48
- reply_to = msg.reply_to_message_id
49
- if reply_to:
50
- try:
51
- reply_to_msg = await self.session.tg.api.get_message(
52
- self.chat_id, reply_to
53
- )
54
- except tgapi.NotFound:
55
- # apparently in telegram it is possible to "reply-to" messages that have been deleted
56
- # TODO: mention in the body that this is reply to a deleted message
57
- reply_to = None
58
- reply_to_fallback = None
59
- reply_to_author = None
60
- reply_self = False
61
- else:
62
- reply_to_content = reply_to_msg.content
63
- reply_to_sender = reply_to_msg.sender_id
64
- if isinstance(reply_to_sender, tgapi.MessageSenderUser):
65
- sender_user_id = reply_to_sender.user_id
66
- reply_self = (
67
- isinstance(msg.sender_id, tgapi.MessageSenderUser)
68
- and sender_user_id == msg.sender_id.user_id
69
- )
70
- elif isinstance(reply_to_sender, tgapi.MessageSenderChat):
71
- reply_self = isinstance(msg.sender_id, tgapi.MessageSenderChat)
72
- sender_user_id = None
73
- else:
74
- raise RuntimeError("This should not happen")
75
-
76
- if self.is_group and not reply_self:
77
- muc = await self.session.bookmarks.by_legacy_id(msg.chat_id)
78
- if sender_user_id is None:
79
- reply_to_author = await muc.participant_system()
80
- else:
81
- reply_to_author = await muc.participant_by_tg_user_id(
82
- sender_user_id
83
- )
84
- else:
85
- reply_to_author = None
86
-
87
- if isinstance(reply_to_content, tgapi.MessageText):
88
- reply_to_fallback = reply_to_content.text.text
89
- elif isinstance(reply_to_content, tgapi.MessageAnimatedEmoji):
90
- reply_to_fallback = reply_to_content.animated_emoji.sticker.emoji
91
- elif isinstance(reply_to_content, tgapi.MessageSticker):
92
- reply_to_fallback = reply_to_content.sticker.emoji
93
- elif best_file := get_best_file(reply_to_content):
94
- reply_to_fallback = f"Attachment {best_file.id}"
95
- else:
96
- reply_to_fallback = "[unsupported by slidge]"
97
- else:
98
- # if reply_to = 0, telegram really means "None"
99
- reply_to = None
100
- reply_to_fallback = None
101
- reply_to_author = None
102
- reply_self = False
103
-
104
- kwargs.update(
105
- dict(
106
- legacy_msg_id=msg.id,
107
- reply_to_msg_id=reply_to,
108
- reply_to_fallback_text=reply_to_fallback,
109
- reply_to_author=reply_to_author,
110
- reply_self=reply_self,
111
- when=datetime.fromtimestamp(msg.date),
112
- )
113
- )
114
- self.session.log.debug("kwargs %s", kwargs)
115
- if isinstance(content, tgapi.MessageText):
116
- # TODO: parse formatted text to markdown
117
- formatted_text = content.text
118
- self.send_text(body=formatted_text.text, **kwargs)
119
- elif isinstance(content, tgapi.MessageAnimatedEmoji):
120
- emoji = content.animated_emoji.sticker.emoji
121
- self.send_text(body=emoji, **kwargs)
122
- elif isinstance(content, tgapi.MessageSticker):
123
- emoji = content.sticker.emoji
124
- self.send_text(body="[Sticker] " + emoji, **kwargs)
125
- elif best_file := get_best_file(content):
126
- await self.send_tg_file(best_file, content.caption.text, **kwargs)
127
- elif isinstance(content, tgapi.MessageBasicGroupChatCreate):
128
- # TODO: work out how to map this to group invitation
129
- pass
130
- elif isinstance(content, tgapi.MessageChatAddMembers):
131
- muc = await self.session.bookmarks.by_legacy_id(msg.chat_id)
132
- for user_id in content.member_user_ids:
133
- participant = await muc.participant_by_tg_user_id(user_id)
134
- participant.online()
135
- elif isinstance(content, tgapi.MessagePinMessage):
136
- if await self.session.tg.is_private_chat(msg.chat_id):
137
- return
138
- muc = await self.session.bookmarks.by_legacy_id(msg.chat_id)
139
- await muc.update_subject_from_msg()
140
- else:
141
- self.send_text(
142
- "/me tried to send an unsupported content. "
143
- "Please report this: https://todo.sr.ht/~nicoco/slidge",
144
- **kwargs,
145
- )
146
- self.session.log.warning("Ignoring content: %s", type(content))
147
-
148
- async def send_tg_file(self, best_file: tgapi.File, caption: str, **kwargs):
149
- query = tgapi.DownloadFile.construct(
150
- file_id=best_file.id, synchronous=True, priority=1
151
- )
152
- best_file_downloaded: tgapi.File = await self.session.tg.request(query)
153
- await self.send_file(best_file_downloaded.local.path, caption=caption, **kwargs)
@@ -1,6 +0,0 @@
1
- """
2
- WhatsApp gateway using the multi-device API.
3
- """
4
-
5
- from .gateway import Gateway
6
- from .session import Session
@@ -1,17 +0,0 @@
1
- """
2
- Config contains plugin-specific configuration for WhatsApp, and is loaded automatically by the
3
- core configuration framework.
4
- """
5
-
6
- from slidge import global_config
7
-
8
- DB_PATH = global_config.HOME_DIR / "whatsapp" / "whatsapp.db"
9
- DB_PATH__DOC = "The path to the database used for the WhatsApp plugin."
10
-
11
- ALWAYS_SYNC_ROSTER = False
12
- ALWAYS_SYNC_ROSTER__DOC = (
13
- "Whether or not to perform a full sync of the WhatsApp roster on startup."
14
- )
15
-
16
- SKIP_VERIFY_TLS = False
17
- SKIP_VERIFY_TLS__DOC = "Whether or not HTTPS connections made by this plugin should verify TLS certificates."
@@ -1,33 +0,0 @@
1
- from datetime import datetime
2
- from typing import TYPE_CHECKING
3
-
4
- from slidge import LegacyContact, LegacyRoster
5
- from slidge.plugins.whatsapp.generated import whatsapp
6
-
7
- if TYPE_CHECKING:
8
- from .session import Session
9
-
10
-
11
- class Contact(LegacyContact["Session", str]):
12
- # WhatsApp only allows message editing in Beta versions of their app, and support is uncertain.
13
- CORRECTION = False
14
- REACTIONS_SINGLE_EMOJI = True
15
-
16
- def update_presence(self, away: bool, last_seen_timestamp: int):
17
- last_seen = (
18
- datetime.fromtimestamp(last_seen_timestamp)
19
- if last_seen_timestamp > 0
20
- else None
21
- )
22
- if away:
23
- self.away(last_seen=last_seen)
24
- else:
25
- self.online(last_seen=last_seen)
26
-
27
-
28
- class Roster(LegacyRoster["Session", Contact, str]):
29
- async def legacy_id_to_jid_username(self, legacy_id: str) -> str:
30
- return "+" + legacy_id[: legacy_id.find("@")]
31
-
32
- async def jid_username_to_legacy_id(self, jid_username: str) -> str:
33
- return jid_username.removeprefix("+") + "@" + whatsapp.DefaultUserServer