slidge 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- slidge/__init__.py +61 -0
- slidge/__main__.py +192 -0
- 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 +3 -0
- slidge/core/cache.py +183 -0
- slidge/core/config.py +209 -0
- slidge/core/gateway/__init__.py +3 -0
- slidge/core/gateway/base.py +892 -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 +757 -0
- slidge/core/gateway/vcard_temp.py +130 -0
- slidge/core/mixins/__init__.py +19 -0
- slidge/core/mixins/attachment.py +506 -0
- slidge/core/mixins/avatar.py +167 -0
- slidge/core/mixins/base.py +31 -0
- slidge/core/mixins/disco.py +130 -0
- slidge/core/mixins/lock.py +31 -0
- slidge/core/mixins/message.py +398 -0
- slidge/core/mixins/message_maker.py +154 -0
- slidge/core/mixins/presence.py +217 -0
- slidge/core/mixins/recipient.py +43 -0
- slidge/core/pubsub.py +525 -0
- slidge/core/session.py +752 -0
- slidge/group/__init__.py +10 -0
- slidge/group/archive.py +125 -0
- slidge/group/bookmarks.py +163 -0
- slidge/group/participant.py +440 -0
- slidge/group/room.py +1095 -0
- slidge/migration.py +18 -0
- slidge/py.typed +0 -0
- slidge/slixfix/__init__.py +68 -0
- slidge/slixfix/link_preview/__init__.py +10 -0
- slidge/slixfix/link_preview/link_preview.py +17 -0
- slidge/slixfix/link_preview/stanza.py +99 -0
- slidge/slixfix/roster.py +60 -0
- slidge/slixfix/xep_0077/__init__.py +10 -0
- slidge/slixfix/xep_0077/register.py +289 -0
- slidge/slixfix/xep_0077/stanza.py +104 -0
- slidge/slixfix/xep_0100/__init__.py +5 -0
- slidge/slixfix/xep_0100/gateway.py +121 -0
- slidge/slixfix/xep_0100/stanza.py +9 -0
- 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/slixfix/xep_0356_old/__init__.py +7 -0
- slidge/slixfix/xep_0356_old/privilege.py +167 -0
- slidge/slixfix/xep_0356_old/stanza.py +44 -0
- 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 +15 -0
- slidge/util/archive_msg.py +61 -0
- slidge/util/conf.py +206 -0
- slidge/util/db.py +229 -0
- slidge/util/schema.sql +126 -0
- slidge/util/sql.py +508 -0
- slidge/util/test.py +295 -0
- slidge/util/types.py +180 -0
- slidge/util/util.py +295 -0
- slidge-0.1.0.dist-info/LICENSE +661 -0
- slidge-0.1.0.dist-info/METADATA +109 -0
- slidge-0.1.0.dist-info/RECORD +96 -0
- slidge-0.1.0.dist-info/WHEEL +4 -0
- slidge-0.1.0.dist-info/entry_points.txt +3 -0
slidge/command/user.py
ADDED
@@ -0,0 +1,250 @@
|
|
1
|
+
# Commands available to users
|
2
|
+
from typing import TYPE_CHECKING, Any, Optional, Union, cast
|
3
|
+
|
4
|
+
from slixmpp import JID # type:ignore[attr-defined]
|
5
|
+
from slixmpp.exceptions import XMPPError
|
6
|
+
|
7
|
+
from ..util.types import AnyBaseSession, LegacyGroupIdType
|
8
|
+
from .base import (
|
9
|
+
Command,
|
10
|
+
CommandAccess,
|
11
|
+
Confirmation,
|
12
|
+
Form,
|
13
|
+
FormField,
|
14
|
+
FormValues,
|
15
|
+
SearchResult,
|
16
|
+
TableResult,
|
17
|
+
)
|
18
|
+
from .categories import CONTACTS, GROUPS
|
19
|
+
|
20
|
+
if TYPE_CHECKING:
|
21
|
+
pass
|
22
|
+
|
23
|
+
|
24
|
+
class Search(Command):
|
25
|
+
NAME = "🔎 Search for contacts"
|
26
|
+
HELP = "Search for contacts via this gateway"
|
27
|
+
NODE = "search"
|
28
|
+
CHAT_COMMAND = "find"
|
29
|
+
ACCESS = CommandAccess.USER_LOGGED
|
30
|
+
CATEGORY = CONTACTS
|
31
|
+
|
32
|
+
async def run(
|
33
|
+
self, session: Optional[AnyBaseSession], _ifrom: JID, *args: str
|
34
|
+
) -> Union[Form, SearchResult, None]:
|
35
|
+
if args:
|
36
|
+
assert session is not None
|
37
|
+
return await session.on_search(
|
38
|
+
{self.xmpp.SEARCH_FIELDS[0].var: " ".join(args)}
|
39
|
+
)
|
40
|
+
return Form(
|
41
|
+
title=self.xmpp.SEARCH_TITLE,
|
42
|
+
instructions=self.xmpp.SEARCH_INSTRUCTIONS,
|
43
|
+
fields=self.xmpp.SEARCH_FIELDS,
|
44
|
+
handler=self.search,
|
45
|
+
)
|
46
|
+
|
47
|
+
@staticmethod
|
48
|
+
async def search(
|
49
|
+
form_values: FormValues, session: Optional[AnyBaseSession], _ifrom: JID
|
50
|
+
) -> SearchResult:
|
51
|
+
assert session is not None
|
52
|
+
results = await session.on_search(form_values) # type: ignore
|
53
|
+
if results is None:
|
54
|
+
raise XMPPError("item-not-found", "No contact was found")
|
55
|
+
|
56
|
+
return results
|
57
|
+
|
58
|
+
|
59
|
+
class SyncContacts(Command):
|
60
|
+
NAME = "🔄 Sync XMPP roster"
|
61
|
+
HELP = (
|
62
|
+
"Synchronize your XMPP roster with your legacy contacts. "
|
63
|
+
"Slidge will only add/remove/modify contacts in its dedicated roster group"
|
64
|
+
)
|
65
|
+
NODE = CHAT_COMMAND = "sync-contacts"
|
66
|
+
ACCESS = CommandAccess.USER_LOGGED
|
67
|
+
CATEGORY = CONTACTS
|
68
|
+
|
69
|
+
async def run(self, session: Optional[AnyBaseSession], _ifrom, *_) -> Confirmation:
|
70
|
+
return Confirmation(
|
71
|
+
prompt="Are you sure you want to sync your roster?",
|
72
|
+
success=None,
|
73
|
+
handler=self.sync,
|
74
|
+
)
|
75
|
+
|
76
|
+
async def sync(self, session: Optional[AnyBaseSession], _ifrom: JID) -> str:
|
77
|
+
if session is None:
|
78
|
+
raise RuntimeError
|
79
|
+
roster_iq = await self.xmpp["xep_0356"].get_roster(session.user.bare_jid)
|
80
|
+
|
81
|
+
contacts = session.contacts.known_contacts()
|
82
|
+
|
83
|
+
added = 0
|
84
|
+
removed = 0
|
85
|
+
updated = 0
|
86
|
+
for item in roster_iq["roster"]:
|
87
|
+
groups = set(item["groups"])
|
88
|
+
if self.xmpp.ROSTER_GROUP in groups:
|
89
|
+
contact = contacts.pop(item["jid"], None)
|
90
|
+
if contact is None:
|
91
|
+
if len(groups) == 1:
|
92
|
+
await self.xmpp["xep_0356"].set_roster(
|
93
|
+
session.user.jid, {item["jid"]: {"subscription": "remove"}}
|
94
|
+
)
|
95
|
+
removed += 1
|
96
|
+
else:
|
97
|
+
groups.remove(self.xmpp.ROSTER_GROUP)
|
98
|
+
await self.xmpp["xep_0356"].set_roster(
|
99
|
+
session.user.jid,
|
100
|
+
{
|
101
|
+
item["jid"]: {
|
102
|
+
"subscription": item["subscription"],
|
103
|
+
"name": item["name"],
|
104
|
+
"groups": groups,
|
105
|
+
}
|
106
|
+
},
|
107
|
+
)
|
108
|
+
updated += 1
|
109
|
+
else:
|
110
|
+
if contact.name != item["name"]:
|
111
|
+
await contact.add_to_roster(force=True)
|
112
|
+
updated += 1
|
113
|
+
|
114
|
+
# we popped before so this only acts on slidge contacts not in the xmpp roster
|
115
|
+
for contact in contacts.values():
|
116
|
+
added += 1
|
117
|
+
await contact.add_to_roster()
|
118
|
+
|
119
|
+
return f"{added} added, {removed} removed, {updated} updated"
|
120
|
+
|
121
|
+
|
122
|
+
class ListContacts(Command):
|
123
|
+
NAME = HELP = "👤 List your legacy contacts"
|
124
|
+
NODE = CHAT_COMMAND = "contacts"
|
125
|
+
ACCESS = CommandAccess.USER_LOGGED
|
126
|
+
CATEGORY = CONTACTS
|
127
|
+
|
128
|
+
async def run(
|
129
|
+
self, session: Optional[AnyBaseSession], _ifrom: JID, *_
|
130
|
+
) -> TableResult:
|
131
|
+
assert session is not None
|
132
|
+
await session.contacts.fill()
|
133
|
+
contacts = sorted(
|
134
|
+
session.contacts, key=lambda c: c.name.casefold() if c.name else ""
|
135
|
+
)
|
136
|
+
return TableResult(
|
137
|
+
description="Your buddies",
|
138
|
+
fields=[FormField("name"), FormField("jid", type="jid-single")],
|
139
|
+
items=[{"name": c.name, "jid": c.jid.bare} for c in contacts],
|
140
|
+
)
|
141
|
+
|
142
|
+
|
143
|
+
class ListGroups(Command):
|
144
|
+
NAME = HELP = "👥 List your legacy groups"
|
145
|
+
NODE = CHAT_COMMAND = "groups"
|
146
|
+
ACCESS = CommandAccess.USER_LOGGED
|
147
|
+
CATEGORY = GROUPS
|
148
|
+
|
149
|
+
async def run(self, session, _ifrom, *_):
|
150
|
+
assert session is not None
|
151
|
+
await session.bookmarks.fill()
|
152
|
+
groups = sorted(session.bookmarks, key=lambda g: g.DISCO_NAME.casefold())
|
153
|
+
return TableResult(
|
154
|
+
description="Your groups",
|
155
|
+
fields=[FormField("name"), FormField("jid", type="jid-single")],
|
156
|
+
items=[{"name": g.name, "jid": g.jid.bare} for g in groups],
|
157
|
+
jids_are_mucs=True,
|
158
|
+
)
|
159
|
+
|
160
|
+
|
161
|
+
class Login(Command):
|
162
|
+
NAME = "🔐 Re-login to the legacy network"
|
163
|
+
HELP = "Login to the legacy service"
|
164
|
+
NODE = CHAT_COMMAND = "re-login"
|
165
|
+
|
166
|
+
ACCESS = CommandAccess.USER_NON_LOGGED
|
167
|
+
|
168
|
+
async def run(self, session: Optional[AnyBaseSession], _ifrom, *_):
|
169
|
+
assert session is not None
|
170
|
+
try:
|
171
|
+
msg = await session.login()
|
172
|
+
except Exception as e:
|
173
|
+
session.send_gateway_status(f"Re-login failed: {e}", show="dnd")
|
174
|
+
raise XMPPError(
|
175
|
+
"internal-server-error", etype="wait", text=f"Could not login: {e}"
|
176
|
+
)
|
177
|
+
session.logged = True
|
178
|
+
session.send_gateway_status(msg or "Re-connected", show="chat")
|
179
|
+
session.send_gateway_message(msg or "Re-connected")
|
180
|
+
return msg
|
181
|
+
|
182
|
+
|
183
|
+
class CreateGroup(Command):
|
184
|
+
NAME = "🆕 New legacy group"
|
185
|
+
HELP = "Create a group on the legacy service"
|
186
|
+
NODE = CHAT_COMMAND = "create-group"
|
187
|
+
CATEGORY = GROUPS
|
188
|
+
|
189
|
+
ACCESS = CommandAccess.USER_LOGGED
|
190
|
+
|
191
|
+
async def run(self, session: Optional[AnyBaseSession], _ifrom, *_):
|
192
|
+
assert session is not None
|
193
|
+
contacts = session.contacts.known_contacts(only_friends=True)
|
194
|
+
return Form(
|
195
|
+
title="Create a new group",
|
196
|
+
instructions="Pick contacts that should be part of this new group",
|
197
|
+
fields=[
|
198
|
+
FormField(var="group_name", label="Name of the group", required=True),
|
199
|
+
FormField(
|
200
|
+
var="contacts",
|
201
|
+
label="Contacts to add to the new group",
|
202
|
+
type="list-multi",
|
203
|
+
options=[
|
204
|
+
{"value": str(contact.jid), "label": contact.name}
|
205
|
+
for contact in sorted(contacts.values(), key=lambda c: c.name)
|
206
|
+
],
|
207
|
+
required=False,
|
208
|
+
),
|
209
|
+
],
|
210
|
+
handler=self.finish,
|
211
|
+
)
|
212
|
+
|
213
|
+
@staticmethod
|
214
|
+
async def finish(form_values: FormValues, session: Optional[AnyBaseSession], *_):
|
215
|
+
assert session is not None
|
216
|
+
legacy_id: LegacyGroupIdType = await session.on_create_group( # type:ignore
|
217
|
+
cast(str, form_values["group_name"]),
|
218
|
+
[
|
219
|
+
await session.contacts.by_jid(JID(j))
|
220
|
+
for j in form_values.get("contacts", []) # type:ignore
|
221
|
+
],
|
222
|
+
)
|
223
|
+
muc = await session.bookmarks.by_legacy_id(legacy_id)
|
224
|
+
return TableResult(
|
225
|
+
description=f"Your new group: xmpp:{muc.jid}?join",
|
226
|
+
fields=[FormField("name"), FormField("jid", type="jid-single")],
|
227
|
+
items=[{"name": muc.name, "jid": muc.jid}],
|
228
|
+
jids_are_mucs=True,
|
229
|
+
)
|
230
|
+
|
231
|
+
|
232
|
+
class Unregister(Command):
|
233
|
+
NAME = "❌ Unregister from the gateway"
|
234
|
+
HELP = "Unregister from the gateway"
|
235
|
+
NODE = CHAT_COMMAND = "unregister"
|
236
|
+
ACCESS = CommandAccess.USER
|
237
|
+
|
238
|
+
async def run(
|
239
|
+
self, session: Optional[AnyBaseSession], _ifrom: JID, *_: Any
|
240
|
+
) -> Confirmation:
|
241
|
+
return Confirmation(
|
242
|
+
prompt=f"Are you sure you want to unregister from '{self.xmpp.boundjid}'?",
|
243
|
+
success=f"You are not registered to '{self.xmpp.boundjid}' anymore.",
|
244
|
+
handler=self.unregister,
|
245
|
+
)
|
246
|
+
|
247
|
+
async def unregister(self, session: Optional[AnyBaseSession], _ifrom: JID) -> str:
|
248
|
+
assert session is not None
|
249
|
+
await self.xmpp.unregister_user(session.user)
|
250
|
+
return "OK"
|