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.
- slidge/__init__.py +54 -31
- slidge/__main__.py +51 -5
- slidge/command/__init__.py +28 -0
- slidge/command/adhoc.py +258 -0
- slidge/command/admin.py +193 -0
- slidge/command/base.py +441 -0
- slidge/command/categories.py +3 -0
- slidge/command/chat_command.py +288 -0
- slidge/command/register.py +179 -0
- slidge/command/user.py +250 -0
- slidge/contact/__init__.py +8 -0
- slidge/contact/contact.py +452 -0
- slidge/contact/roster.py +192 -0
- slidge/core/__init__.py +2 -0
- slidge/core/cache.py +121 -39
- slidge/core/config.py +116 -11
- slidge/core/gateway/__init__.py +3 -0
- slidge/core/gateway/base.py +895 -0
- slidge/core/gateway/caps.py +63 -0
- slidge/core/gateway/delivery_receipt.py +52 -0
- slidge/core/gateway/disco.py +80 -0
- slidge/core/gateway/mam.py +75 -0
- slidge/core/gateway/muc_admin.py +35 -0
- slidge/core/gateway/ping.py +66 -0
- slidge/core/gateway/presence.py +95 -0
- slidge/core/gateway/registration.py +53 -0
- slidge/core/gateway/search.py +102 -0
- slidge/core/gateway/session_dispatcher.py +795 -0
- slidge/core/gateway/vcard_temp.py +130 -0
- slidge/core/mixins/__init__.py +9 -1
- slidge/core/mixins/attachment.py +506 -0
- slidge/core/mixins/avatar.py +167 -0
- slidge/core/mixins/base.py +6 -19
- slidge/core/mixins/disco.py +66 -15
- slidge/core/mixins/lock.py +31 -0
- slidge/core/mixins/message.py +254 -252
- slidge/core/mixins/message_maker.py +154 -0
- slidge/core/mixins/presence.py +128 -31
- slidge/core/mixins/recipient.py +43 -0
- slidge/core/pubsub.py +275 -116
- slidge/core/session.py +586 -518
- slidge/group/__init__.py +10 -0
- slidge/group/archive.py +125 -0
- slidge/group/bookmarks.py +163 -0
- slidge/group/participant.py +458 -0
- slidge/group/room.py +1103 -0
- slidge/migration.py +18 -0
- slidge/slixfix/__init__.py +68 -0
- slidge/{util/xep_0050 → slixfix/link_preview}/__init__.py +4 -5
- slidge/slixfix/link_preview/link_preview.py +17 -0
- slidge/slixfix/link_preview/stanza.py +99 -0
- slidge/slixfix/roster.py +60 -0
- slidge/{util → slixfix}/xep_0077/register.py +1 -2
- slidge/slixfix/xep_0077/stanza.py +104 -0
- slidge/{util → slixfix}/xep_0100/gateway.py +17 -12
- slidge/slixfix/xep_0153/__init__.py +10 -0
- slidge/slixfix/xep_0153/stanza.py +25 -0
- slidge/slixfix/xep_0153/vcard_avatar.py +23 -0
- slidge/slixfix/xep_0264/__init__.py +5 -0
- slidge/slixfix/xep_0264/stanza.py +36 -0
- slidge/slixfix/xep_0264/thumbnail.py +23 -0
- slidge/slixfix/xep_0292/__init__.py +5 -0
- slidge/slixfix/xep_0292/vcard4.py +100 -0
- slidge/slixfix/xep_0313/__init__.py +12 -0
- slidge/slixfix/xep_0313/mam.py +262 -0
- slidge/slixfix/xep_0313/stanza.py +359 -0
- slidge/slixfix/xep_0317/__init__.py +5 -0
- slidge/slixfix/xep_0317/hats.py +17 -0
- slidge/slixfix/xep_0317/stanza.py +28 -0
- slidge/{util → slixfix}/xep_0356_old/privilege.py +9 -7
- slidge/slixfix/xep_0424/__init__.py +9 -0
- slidge/slixfix/xep_0424/retraction.py +77 -0
- slidge/slixfix/xep_0424/stanza.py +28 -0
- slidge/slixfix/xep_0490/__init__.py +8 -0
- slidge/slixfix/xep_0490/mds.py +47 -0
- slidge/slixfix/xep_0490/stanza.py +17 -0
- slidge/util/__init__.py +4 -6
- slidge/util/archive_msg.py +61 -0
- slidge/util/conf.py +25 -4
- slidge/util/db.py +23 -69
- slidge/util/schema.sql +126 -0
- slidge/util/sql.py +508 -0
- slidge/util/test.py +136 -86
- slidge/util/types.py +155 -14
- slidge/util/util.py +225 -51
- slidge-0.1.2.dist-info/METADATA +111 -0
- slidge-0.1.2.dist-info/RECORD +96 -0
- {slidge-0.1.0rc1.dist-info → slidge-0.1.2.dist-info}/WHEEL +1 -1
- slidge/core/adhoc.py +0 -492
- slidge/core/chat_command.py +0 -197
- slidge/core/contact.py +0 -441
- slidge/core/disco.py +0 -59
- slidge/core/gateway.py +0 -899
- slidge/core/muc/__init__.py +0 -3
- slidge/core/muc/bookmarks.py +0 -74
- slidge/core/muc/participant.py +0 -152
- slidge/core/muc/room.py +0 -348
- slidge/plugins/discord/__init__.py +0 -121
- slidge/plugins/discord/client.py +0 -121
- slidge/plugins/discord/session.py +0 -172
- slidge/plugins/dummy.py +0 -334
- slidge/plugins/facebook.py +0 -591
- slidge/plugins/hackernews.py +0 -209
- slidge/plugins/mattermost/__init__.py +0 -1
- slidge/plugins/mattermost/api.py +0 -288
- slidge/plugins/mattermost/gateway.py +0 -417
- slidge/plugins/mattermost/websocket.py +0 -248
- slidge/plugins/signal/__init__.py +0 -4
- slidge/plugins/signal/config.py +0 -4
- slidge/plugins/signal/contact.py +0 -104
- slidge/plugins/signal/gateway.py +0 -379
- slidge/plugins/signal/group.py +0 -76
- slidge/plugins/signal/session.py +0 -515
- slidge/plugins/signal/txt.py +0 -13
- slidge/plugins/signal/util.py +0 -32
- slidge/plugins/skype.py +0 -310
- slidge/plugins/steam.py +0 -400
- slidge/plugins/telegram/__init__.py +0 -6
- slidge/plugins/telegram/client.py +0 -325
- slidge/plugins/telegram/config.py +0 -21
- slidge/plugins/telegram/contact.py +0 -154
- slidge/plugins/telegram/gateway.py +0 -182
- slidge/plugins/telegram/group.py +0 -184
- slidge/plugins/telegram/session.py +0 -275
- slidge/plugins/telegram/util.py +0 -153
- slidge/plugins/whatsapp/__init__.py +0 -6
- slidge/plugins/whatsapp/config.py +0 -17
- slidge/plugins/whatsapp/contact.py +0 -33
- slidge/plugins/whatsapp/event.go +0 -455
- slidge/plugins/whatsapp/gateway.go +0 -156
- slidge/plugins/whatsapp/gateway.py +0 -69
- slidge/plugins/whatsapp/go.mod +0 -17
- slidge/plugins/whatsapp/go.sum +0 -22
- slidge/plugins/whatsapp/session.go +0 -371
- slidge/plugins/whatsapp/session.py +0 -370
- slidge/util/xep_0030/__init__.py +0 -13
- slidge/util/xep_0030/disco.py +0 -811
- slidge/util/xep_0030/stanza/__init__.py +0 -7
- slidge/util/xep_0030/stanza/info.py +0 -270
- slidge/util/xep_0030/stanza/items.py +0 -147
- slidge/util/xep_0030/static.py +0 -467
- slidge/util/xep_0050/adhoc.py +0 -631
- slidge/util/xep_0050/stanza.py +0 -180
- slidge/util/xep_0077/stanza.py +0 -71
- slidge/util/xep_0292/__init__.py +0 -1
- slidge/util/xep_0292/stanza.py +0 -167
- slidge/util/xep_0292/vcard4.py +0 -74
- slidge/util/xep_0356/__init__.py +0 -7
- slidge/util/xep_0356/permissions.py +0 -35
- slidge/util/xep_0356/privilege.py +0 -160
- slidge/util/xep_0356/stanza.py +0 -44
- slidge/util/xep_0461/__init__.py +0 -6
- slidge/util/xep_0461/reply.py +0 -48
- slidge/util/xep_0461/stanza.py +0 -80
- slidge-0.1.0rc1.dist-info/METADATA +0 -171
- slidge-0.1.0rc1.dist-info/RECORD +0 -99
- /slidge/{plugins/__init__.py → py.typed} +0 -0
- /slidge/{util → slixfix}/xep_0077/__init__.py +0 -0
- /slidge/{util → slixfix}/xep_0100/__init__.py +0 -0
- /slidge/{util → slixfix}/xep_0100/stanza.py +0 -0
- /slidge/{util → slixfix}/xep_0356_old/__init__.py +0 -0
- /slidge/{util → slixfix}/xep_0356_old/stanza.py +0 -0
- {slidge-0.1.0rc1.dist-info → slidge-0.1.2.dist-info}/LICENSE +0 -0
- {slidge-0.1.0rc1.dist-info → slidge-0.1.2.dist-info}/entry_points.txt +0 -0
slidge/plugins/telegram/group.py
DELETED
@@ -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__)
|
slidge/plugins/telegram/util.py
DELETED
@@ -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,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
|