slidge 0.2.5__py3-none-any.whl → 0.2.7__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/__version__.py +1 -1
- slidge/command/adhoc.py +14 -3
- slidge/command/user.py +5 -0
- slidge/contact/contact.py +1 -7
- slidge/core/config.py +0 -4
- slidge/core/gateway.py +4 -2
- slidge/core/mixins/message.py +6 -9
- slidge/core/session.py +11 -3
- slidge/db/alembic/versions/04cf35e3cf85_add_participant_nickname_no_illegal.py +33 -0
- slidge/db/alembic/versions/aa9d82a7f6ef_db_creation.py +0 -51
- slidge/db/models.py +1 -0
- slidge/db/store.py +3 -0
- slidge/group/participant.py +4 -1
- slidge/group/room.py +115 -61
- slidge/slixfix/__init__.py +0 -2
- slidge/slixfix/xep_0100/gateway.py +1 -4
- slidge/slixfix/xep_0492/stanza.py +7 -2
- slidge/util/util.py +10 -6
- {slidge-0.2.5.dist-info → slidge-0.2.7.dist-info}/METADATA +9 -10
- {slidge-0.2.5.dist-info → slidge-0.2.7.dist-info}/RECORD +23 -27
- {slidge-0.2.5.dist-info → slidge-0.2.7.dist-info}/WHEEL +1 -1
- slidge/db/alembic/old_user_store.py +0 -183
- slidge/slixfix/xep_0356_old/__init__.py +0 -7
- slidge/slixfix/xep_0356_old/privilege.py +0 -167
- slidge/slixfix/xep_0356_old/stanza.py +0 -44
- slidge/util/db.py +0 -5
- {slidge-0.2.5.dist-info → slidge-0.2.7.dist-info}/entry_points.txt +0 -0
- {slidge-0.2.5.dist-info → slidge-0.2.7.dist-info}/top_level.txt +0 -0
slidge/__version__.py
CHANGED
slidge/command/adhoc.py
CHANGED
@@ -235,18 +235,29 @@ class AdhocProvider:
|
|
235
235
|
"""
|
236
236
|
Get items for a disco query
|
237
237
|
|
238
|
-
:param jid:
|
238
|
+
:param jid: the entity that should return its items
|
239
239
|
:param node: which command node is requested
|
240
240
|
:param iq: the disco query IQ
|
241
241
|
:return: commands accessible to the given JID will be listed
|
242
242
|
"""
|
243
|
+
ifrom = iq.get_from()
|
244
|
+
ifrom_str = str(ifrom)
|
245
|
+
if (
|
246
|
+
not self.xmpp.jid_validator.match(ifrom_str)
|
247
|
+
and ifrom_str not in config.ADMINS
|
248
|
+
):
|
249
|
+
raise XMPPError(
|
250
|
+
"forbidden",
|
251
|
+
"You are not authorized to execute adhoc commands on this gateway. "
|
252
|
+
"If this is unexpected, ask your administrator to verify that "
|
253
|
+
"'user-jid-validator' is correctly set in slidge's configuration.",
|
254
|
+
)
|
255
|
+
|
243
256
|
all_items = self.xmpp.plugin["xep_0030"].static.get_items(jid, node, None, None)
|
244
257
|
log.debug("Static items: %r", all_items)
|
245
258
|
if not all_items:
|
246
259
|
return DiscoItems()
|
247
260
|
|
248
|
-
ifrom = iq.get_from()
|
249
|
-
|
250
261
|
filtered_items = DiscoItems()
|
251
262
|
filtered_items["node"] = self.xmpp.plugin["xep_0050"].stanza.Command.namespace
|
252
263
|
for item in all_items:
|
slidge/command/user.py
CHANGED
@@ -172,6 +172,9 @@ class Login(Command):
|
|
172
172
|
|
173
173
|
async def run(self, session: Optional[AnyBaseSession], _ifrom, *_):
|
174
174
|
assert session is not None
|
175
|
+
if session.is_logging_in:
|
176
|
+
raise XMPPError("bad-request", "You are already logging in.")
|
177
|
+
session.is_logging_in = True
|
175
178
|
try:
|
176
179
|
msg = await session.login()
|
177
180
|
except Exception as e:
|
@@ -179,6 +182,8 @@ class Login(Command):
|
|
179
182
|
raise XMPPError(
|
180
183
|
"internal-server-error", etype="wait", text=f"Could not login: {e}"
|
181
184
|
)
|
185
|
+
finally:
|
186
|
+
session.is_logging_in = False
|
182
187
|
session.logged = True
|
183
188
|
session.send_gateway_status(msg or "Re-connected", show="chat")
|
184
189
|
session.send_gateway_message(msg or "Re-connected")
|
slidge/contact/contact.py
CHANGED
@@ -448,7 +448,7 @@ class LegacyContact(
|
|
448
448
|
log.debug("Roster push request by plugin ignored (--no-roster-push)")
|
449
449
|
return
|
450
450
|
try:
|
451
|
-
await self.
|
451
|
+
await self.xmpp["xep_0356"].set_roster(
|
452
452
|
jid=self.user_jid, roster_items=self.get_roster_item()
|
453
453
|
)
|
454
454
|
except PermissionError:
|
@@ -490,12 +490,6 @@ class LegacyContact(
|
|
490
490
|
nick,
|
491
491
|
)
|
492
492
|
|
493
|
-
async def _set_roster(self, **kw):
|
494
|
-
try:
|
495
|
-
await self.xmpp["xep_0356"].set_roster(**kw)
|
496
|
-
except PermissionError:
|
497
|
-
await self.xmpp["xep_0356_old"].set_roster(**kw)
|
498
|
-
|
499
493
|
def send_friend_request(self, text: Optional[str] = None):
|
500
494
|
presence = self._make_presence(ptype="subscribe", pstatus=text, bare=True)
|
501
495
|
self._send(presence, nick=True)
|
slidge/core/config.py
CHANGED
@@ -68,7 +68,6 @@ USER_JID_VALIDATOR__DYNAMIC_DEFAULT = True
|
|
68
68
|
ADMINS: tuple[JIDType, ...] = ()
|
69
69
|
ADMINS__DOC = "JIDs of the gateway admins"
|
70
70
|
|
71
|
-
|
72
71
|
UPLOAD_SERVICE: Optional[str] = None
|
73
72
|
UPLOAD_SERVICE__DOC = (
|
74
73
|
"JID of an HTTP upload service the gateway can use. "
|
@@ -76,9 +75,6 @@ UPLOAD_SERVICE__DOC = (
|
|
76
75
|
"discovery."
|
77
76
|
)
|
78
77
|
|
79
|
-
SECRET_KEY: Optional[str] = None
|
80
|
-
SECRET_KEY__DOC = "Encryption for disk storage. Deprecated."
|
81
|
-
|
82
78
|
NO_ROSTER_PUSH = False
|
83
79
|
NO_ROSTER_PUSH__DOC = "Do not fill users' rosters with legacy contacts automatically"
|
84
80
|
|
slidge/core/gateway.py
CHANGED
@@ -515,6 +515,7 @@ class BaseGateway(
|
|
515
515
|
@timeit
|
516
516
|
async def login_wrap(self, session: "BaseSession"):
|
517
517
|
session.send_gateway_status("Logging in…", show="dnd")
|
518
|
+
session.is_logging_in = True
|
518
519
|
try:
|
519
520
|
status = await session.login()
|
520
521
|
except Exception as e:
|
@@ -808,7 +809,9 @@ class BaseGateway(
|
|
808
809
|
|
809
810
|
async def unregister_user(self, user: GatewayUser):
|
810
811
|
self.send_presence(
|
811
|
-
pshow="
|
812
|
+
pshow="dnd",
|
813
|
+
pstatus="You unregistered from this gateway.",
|
814
|
+
pto=user.jid,
|
812
815
|
)
|
813
816
|
await self.xmpp.plugin["xep_0077"].api["user_remove"](None, None, user.jid)
|
814
817
|
await self.xmpp.session_cls.kill_by_jid(user.jid)
|
@@ -913,7 +916,6 @@ SLIXMPP_PLUGINS = [
|
|
913
916
|
"xep_0333", # Chat markers
|
914
917
|
"xep_0334", # Message Processing Hints
|
915
918
|
"xep_0356", # Privileged Entity
|
916
|
-
"xep_0356_old", # Privileged Entity (old namespace)
|
917
919
|
"xep_0363", # HTTP file upload
|
918
920
|
"xep_0385", # Stateless in-line media sharing
|
919
921
|
"xep_0402", # PEP Native Bookmarks
|
slidge/core/mixins/message.py
CHANGED
@@ -177,15 +177,12 @@ class CarbonMessageMixin(ContentMessageMixin, MarkerMixin):
|
|
177
177
|
try:
|
178
178
|
self.xmpp["xep_0356"].send_privileged_message(msg)
|
179
179
|
except PermissionError:
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
" https://slidge.im/docs/slidge/main/admin/privilege.html"
|
187
|
-
" for more info."
|
188
|
-
)
|
180
|
+
warnings.warn(
|
181
|
+
"Slidge does not have privileges to send message on behalf of"
|
182
|
+
" user.Refer to"
|
183
|
+
" https://slidge.im/docs/slidge/main/admin/privilege.html"
|
184
|
+
" for more info."
|
185
|
+
)
|
189
186
|
|
190
187
|
|
191
188
|
class InviteMixin(MessageMaker):
|
slidge/core/session.py
CHANGED
@@ -33,7 +33,7 @@ from ..util.types import (
|
|
33
33
|
ResourceDict,
|
34
34
|
Sticker,
|
35
35
|
)
|
36
|
-
from ..util.util import deprecated
|
36
|
+
from ..util.util import deprecated, noop_coro
|
37
37
|
|
38
38
|
if TYPE_CHECKING:
|
39
39
|
from ..group.participant import LegacyParticipant
|
@@ -98,6 +98,7 @@ class BaseSession(
|
|
98
98
|
self.ignore_messages = set[str]()
|
99
99
|
|
100
100
|
self.contacts: LegacyRoster = LegacyRoster.get_self_or_unique_subclass()(self)
|
101
|
+
self.is_logging_in = False
|
101
102
|
self._logged = False
|
102
103
|
self.__reset_ready()
|
103
104
|
|
@@ -527,11 +528,13 @@ class BaseSession(
|
|
527
528
|
|
528
529
|
@logged.setter
|
529
530
|
def logged(self, v: bool):
|
531
|
+
self.is_logging_in = False
|
530
532
|
self._logged = v
|
531
533
|
if self.ready.done():
|
532
534
|
if v:
|
533
535
|
return
|
534
536
|
self.__reset_ready()
|
537
|
+
self.shutdown(logout=False)
|
535
538
|
else:
|
536
539
|
if v:
|
537
540
|
self.ready.set_result(True)
|
@@ -539,12 +542,15 @@ class BaseSession(
|
|
539
542
|
def __repr__(self):
|
540
543
|
return f"<Session of {self.user_jid}>"
|
541
544
|
|
542
|
-
def shutdown(self) -> asyncio.Task:
|
545
|
+
def shutdown(self, logout=True) -> asyncio.Task:
|
543
546
|
for c in self.contacts:
|
544
547
|
c.offline()
|
545
548
|
for m in self.bookmarks:
|
546
549
|
m.shutdown()
|
547
|
-
|
550
|
+
if logout:
|
551
|
+
return self.xmpp.loop.create_task(self.logout())
|
552
|
+
else:
|
553
|
+
return self.xmpp.loop.create_task(noop_coro())
|
548
554
|
|
549
555
|
@staticmethod
|
550
556
|
def legacy_to_xmpp_msg_id(legacy_msg_id: LegacyMessageType) -> str:
|
@@ -657,6 +663,8 @@ class BaseSession(
|
|
657
663
|
return
|
658
664
|
for c in session.contacts:
|
659
665
|
c.unsubscribe()
|
666
|
+
for m in session.bookmarks:
|
667
|
+
m.shutdown()
|
660
668
|
user = cls.xmpp.store.users.get(jid)
|
661
669
|
if user is None:
|
662
670
|
log.warning("User not found during unregistration")
|
@@ -0,0 +1,33 @@
|
|
1
|
+
"""Add Participant.nickname_no_illegal
|
2
|
+
|
3
|
+
Revision ID: 04cf35e3cf85
|
4
|
+
Revises: 15b0bd83407a
|
5
|
+
Create Date: 2025-02-22 06:57:45.491326
|
6
|
+
|
7
|
+
"""
|
8
|
+
|
9
|
+
from typing import Sequence, Union
|
10
|
+
|
11
|
+
import sqlalchemy as sa
|
12
|
+
from alembic import op
|
13
|
+
|
14
|
+
# revision identifiers, used by Alembic.
|
15
|
+
revision: str = "04cf35e3cf85"
|
16
|
+
down_revision: Union[str, None] = "15b0bd83407a"
|
17
|
+
branch_labels: Union[str, Sequence[str], None] = None
|
18
|
+
depends_on: Union[str, Sequence[str], None] = None
|
19
|
+
|
20
|
+
|
21
|
+
def upgrade() -> None:
|
22
|
+
# ### commands auto generated by Alembic - please adjust! ###
|
23
|
+
with op.batch_alter_table("participant", schema=None) as batch_op:
|
24
|
+
batch_op.add_column(
|
25
|
+
sa.Column("nickname_no_illegal", sa.String(), nullable=True)
|
26
|
+
)
|
27
|
+
|
28
|
+
# ### end Alembic commands ###
|
29
|
+
|
30
|
+
|
31
|
+
def downgrade() -> None:
|
32
|
+
with op.batch_alter_table("participant", schema=None) as batch_op:
|
33
|
+
batch_op.drop_column("nickname_no_illegal")
|
@@ -8,8 +8,6 @@ Create Date: 2024-04-17 20:57:01.357041
|
|
8
8
|
|
9
9
|
"""
|
10
10
|
|
11
|
-
import logging
|
12
|
-
from datetime import datetime
|
13
11
|
from typing import Sequence, Union
|
14
12
|
|
15
13
|
import sqlalchemy as sa
|
@@ -44,58 +42,9 @@ def upgrade() -> None:
|
|
44
42
|
sa.UniqueConstraint("jid"),
|
45
43
|
)
|
46
44
|
# ### end Alembic commands ###
|
47
|
-
try:
|
48
|
-
migrate_from_shelf(accounts)
|
49
|
-
except Exception:
|
50
|
-
downgrade()
|
51
|
-
raise
|
52
45
|
|
53
46
|
|
54
47
|
def downgrade() -> None:
|
55
48
|
# ### commands auto generated by Alembic - please adjust! ###
|
56
49
|
op.drop_table("user_account")
|
57
50
|
# ### end Alembic commands ###
|
58
|
-
|
59
|
-
|
60
|
-
def migrate_from_shelf(accounts: sa.Table) -> None:
|
61
|
-
from slidge import global_config
|
62
|
-
|
63
|
-
home = getattr(global_config, "HOME_DIR", None)
|
64
|
-
if home is None:
|
65
|
-
return
|
66
|
-
|
67
|
-
db_file = home / "slidge.db"
|
68
|
-
if not db_file.exists():
|
69
|
-
return
|
70
|
-
|
71
|
-
try:
|
72
|
-
from slidge.db.alembic.old_user_store import user_store
|
73
|
-
except ImportError:
|
74
|
-
return
|
75
|
-
|
76
|
-
user_store.set_file(db_file, global_config.SECRET_KEY)
|
77
|
-
|
78
|
-
try:
|
79
|
-
users = list(user_store.get_all())
|
80
|
-
except AttributeError:
|
81
|
-
return
|
82
|
-
logging.info("Migrating %s users from the deprecated user_store shelf", len(users))
|
83
|
-
op.bulk_insert(
|
84
|
-
accounts,
|
85
|
-
[
|
86
|
-
{
|
87
|
-
"jid": user.jid,
|
88
|
-
"registration_date": (
|
89
|
-
user.registration_date
|
90
|
-
if user.registration_date is not None
|
91
|
-
else datetime.now()
|
92
|
-
),
|
93
|
-
"legacy_module_data": user.registration_form,
|
94
|
-
"preferences": {},
|
95
|
-
}
|
96
|
-
for user in users
|
97
|
-
],
|
98
|
-
)
|
99
|
-
|
100
|
-
user_store.close()
|
101
|
-
db_file.unlink()
|
slidge/db/models.py
CHANGED
@@ -388,6 +388,7 @@ class Participant(Base):
|
|
388
388
|
|
389
389
|
resource: Mapped[Optional[str]] = mapped_column(default=None)
|
390
390
|
nickname: Mapped[str] = mapped_column(nullable=True, default=None)
|
391
|
+
nickname_no_illegal: Mapped[str] = mapped_column(nullable=True, default=None)
|
391
392
|
|
392
393
|
hats: Mapped[list["Hat"]] = relationship(
|
393
394
|
secondary=participant_hats, back_populates="participants"
|
slidge/db/store.py
CHANGED
@@ -698,6 +698,8 @@ class MultiStore(EngineMixin):
|
|
698
698
|
).scalar()
|
699
699
|
if multi is None:
|
700
700
|
return []
|
701
|
+
if multi.legacy_ids_multi is None:
|
702
|
+
return []
|
701
703
|
return [m.xmpp_id for m in multi.legacy_ids_multi.xmpp_ids]
|
702
704
|
|
703
705
|
def set_xmpp_ids(
|
@@ -1072,6 +1074,7 @@ class ParticipantStore(EngineMixin):
|
|
1072
1074
|
.where(Participant.id == participant.pk)
|
1073
1075
|
.values(
|
1074
1076
|
resource=participant.jid.resource,
|
1077
|
+
nickname_no_illegal=participant._nickname_no_illegal,
|
1075
1078
|
affiliation=participant.affiliation,
|
1076
1079
|
role=participant.role,
|
1077
1080
|
presence_sent=participant._presence_sent, # type:ignore
|
slidge/group/participant.py
CHANGED
@@ -71,6 +71,7 @@ class LegacyParticipant(
|
|
71
71
|
role: MucRole = "participant",
|
72
72
|
affiliation: MucAffiliation = "member",
|
73
73
|
resource: str | None = None,
|
74
|
+
nickname_no_illegal: str | None = None,
|
74
75
|
):
|
75
76
|
self.session = session = muc.session
|
76
77
|
self.xmpp = session.xmpp
|
@@ -87,7 +88,8 @@ class LegacyParticipant(
|
|
87
88
|
if resource is None:
|
88
89
|
self.__update_jid(nickname)
|
89
90
|
else:
|
90
|
-
|
91
|
+
assert nickname_no_illegal is not None
|
92
|
+
self._nickname_no_illegal = nickname_no_illegal
|
91
93
|
self.jid = JID(self.muc.jid)
|
92
94
|
self.jid.resource = resource
|
93
95
|
|
@@ -518,6 +520,7 @@ class LegacyParticipant(
|
|
518
520
|
role=stored.role,
|
519
521
|
affiliation=stored.affiliation,
|
520
522
|
resource=stored.resource,
|
523
|
+
nickname_no_illegal=stored.nickname_no_illegal,
|
521
524
|
)
|
522
525
|
part.pk = stored.id
|
523
526
|
if contact is not None:
|
slidge/group/room.py
CHANGED
@@ -14,6 +14,7 @@ from slixmpp.jid import _unescape_node
|
|
14
14
|
from slixmpp.plugins.xep_0004 import Form
|
15
15
|
from slixmpp.plugins.xep_0060.stanza import Item
|
16
16
|
from slixmpp.plugins.xep_0082 import parse as str_to_datetime
|
17
|
+
from slixmpp.plugins.xep_0469.stanza import NS as PINNING_NS
|
17
18
|
from slixmpp.xmlstream import ET
|
18
19
|
|
19
20
|
from ..contact.contact import LegacyContact
|
@@ -26,6 +27,7 @@ from ..core.mixins.disco import ChatterDiscoMixin
|
|
26
27
|
from ..core.mixins.lock import NamedLockMixin
|
27
28
|
from ..core.mixins.recipient import ReactionRecipientMixin, ThreadRecipientMixin
|
28
29
|
from ..db.models import Room
|
30
|
+
from ..slixfix.xep_0492.stanza import NS as NOTIFY_NS
|
29
31
|
from ..slixfix.xep_0492.stanza import WhenLiteral
|
30
32
|
from ..util import ABCSubclassableOnceAtMost
|
31
33
|
from ..util.types import (
|
@@ -231,7 +233,7 @@ class LegacyMUC(
|
|
231
233
|
self._participants_filled = True
|
232
234
|
async for p in self.fill_participants():
|
233
235
|
self.__participants_store.update(p)
|
234
|
-
|
236
|
+
self.__store.set_participants_filled(self.pk)
|
235
237
|
|
236
238
|
async def get_participants(self) -> AsyncIterator[LegacyParticipant]:
|
237
239
|
assert self.pk is not None
|
@@ -248,19 +250,21 @@ class LegacyMUC(
|
|
248
250
|
async with self.lock("fill participants"):
|
249
251
|
self._participants_filled = True
|
250
252
|
# We only fill the participants list if/when the MUC is first
|
251
|
-
# joined by an XMPP client. But we may have instantiated
|
253
|
+
# joined by an XMPP client. But we may have instantiated some before.
|
252
254
|
resources = set[str]()
|
255
|
+
async for participant in self.fill_participants():
|
256
|
+
# TODO: batch SQL update at the end of this function for perf?
|
257
|
+
self.__store_participant(participant)
|
258
|
+
yield participant
|
259
|
+
resources.add(participant.jid.resource)
|
253
260
|
for db_participant in self.xmpp.store.participants.get_all(
|
254
261
|
self.pk, user_included=True
|
255
262
|
):
|
256
263
|
participant = self.Participant.from_store(
|
257
264
|
self.session, db_participant, muc=self
|
258
265
|
)
|
259
|
-
|
260
|
-
|
261
|
-
async for p in self.fill_participants():
|
262
|
-
if p.jid.resource not in resources:
|
263
|
-
yield p
|
266
|
+
if participant.jid.resource not in resources:
|
267
|
+
yield participant
|
264
268
|
self.__store.set_participants_filled(self.pk)
|
265
269
|
return
|
266
270
|
|
@@ -423,7 +427,7 @@ class LegacyMUC(
|
|
423
427
|
|
424
428
|
if subject_setter == self._subject_setter:
|
425
429
|
return
|
426
|
-
assert isinstance(subject_setter, str)
|
430
|
+
assert isinstance(subject_setter, str | None)
|
427
431
|
self._subject_setter = subject_setter
|
428
432
|
if self._updating_info:
|
429
433
|
return
|
@@ -687,6 +691,8 @@ class LegacyMUC(
|
|
687
691
|
assert self.pk is not None
|
688
692
|
p.pk = self.__participants_store.add(self.pk, p.nickname)
|
689
693
|
self.__participants_store.update(p)
|
694
|
+
if p._hats:
|
695
|
+
self.__participants_store.set_hats(p.pk, p._hats)
|
690
696
|
|
691
697
|
async def get_participant(
|
692
698
|
self,
|
@@ -995,6 +1001,32 @@ class LegacyMUC(
|
|
995
1001
|
p["muc"]["status_codes"] = {110, 333}
|
996
1002
|
p.send()
|
997
1003
|
|
1004
|
+
async def __get_bookmark(self) -> Item | None:
|
1005
|
+
item = Item()
|
1006
|
+
item["id"] = self.jid
|
1007
|
+
|
1008
|
+
iq = Iq(stype="get", sfrom=self.user_jid, sto=self.user_jid)
|
1009
|
+
iq["pubsub"]["items"]["node"] = self.xmpp["xep_0402"].stanza.NS
|
1010
|
+
iq["pubsub"]["items"].append(item)
|
1011
|
+
|
1012
|
+
try:
|
1013
|
+
ans = await self.xmpp["xep_0356"].send_privileged_iq(iq)
|
1014
|
+
if len(ans["pubsub"]["items"]) != 1:
|
1015
|
+
return None
|
1016
|
+
# this below creates the item if it wasn't here already
|
1017
|
+
# (slixmpp annoying magic)
|
1018
|
+
item = ans["pubsub"]["items"]["item"]
|
1019
|
+
item["id"] = self.jid
|
1020
|
+
return item
|
1021
|
+
except (IqError, IqTimeout) as exc:
|
1022
|
+
warnings.warn(f"Cannot fetch bookmark: {exc}")
|
1023
|
+
return None
|
1024
|
+
except PermissionError:
|
1025
|
+
warnings.warn(
|
1026
|
+
"IQ privileges (XEP0356) are not set, we cannot fetch the user bookmarks"
|
1027
|
+
)
|
1028
|
+
return None
|
1029
|
+
|
998
1030
|
async def add_to_bookmarks(
|
999
1031
|
self,
|
1000
1032
|
auto_join=True,
|
@@ -1026,68 +1058,90 @@ class LegacyMUC(
|
|
1026
1058
|
If set to ``None`` (default), the setting will be untouched. Only the "global"
|
1027
1059
|
notification setting is supported (ie, per client type is not possible).
|
1028
1060
|
"""
|
1029
|
-
|
1030
|
-
item["id"] = self.jid
|
1061
|
+
existing = await self.__get_bookmark() if preserve else None
|
1031
1062
|
|
1032
|
-
|
1033
|
-
|
1034
|
-
|
1063
|
+
new = Item()
|
1064
|
+
new["id"] = self.jid
|
1065
|
+
new["conference"]["nick"] = self.user_nick
|
1035
1066
|
|
1036
|
-
|
1037
|
-
|
1038
|
-
|
1039
|
-
ans = await self.xmpp["xep_0356"].send_privileged_iq(iq)
|
1040
|
-
is_update = len(ans["pubsub"]["items"]) == 1
|
1041
|
-
# this below creates the item if it wasn't here already
|
1042
|
-
# (slixmpp annoying magic)
|
1043
|
-
item = ans["pubsub"]["items"]["item"]
|
1044
|
-
item["id"] = self.jid
|
1045
|
-
except (IqError, IqTimeout):
|
1046
|
-
item["conference"]["autojoin"] = auto_join
|
1047
|
-
except PermissionError:
|
1048
|
-
warnings.warn(
|
1049
|
-
"IQ privileges (XEP0356) are not set, we cannot fetch the user bookmarks"
|
1050
|
-
)
|
1051
|
-
else:
|
1052
|
-
# if the bookmark is already present, we preserve it as much as
|
1053
|
-
# possible, especially custom <extensions>
|
1054
|
-
self.log.debug("Existing: %s", item)
|
1055
|
-
# if it's an update, we do not touch the auto join flag
|
1056
|
-
if not is_update:
|
1057
|
-
item["conference"]["autojoin"] = auto_join
|
1067
|
+
if existing is None:
|
1068
|
+
change = True
|
1069
|
+
new["conference"]["autojoin"] = auto_join
|
1058
1070
|
else:
|
1059
|
-
|
1060
|
-
|
1061
|
-
|
1071
|
+
change = False
|
1072
|
+
new["conference"]["autojoin"] = existing["conference"]["autojoin"]
|
1073
|
+
|
1074
|
+
existing_extensions = existing is not None and existing[
|
1075
|
+
"conference"
|
1076
|
+
].get_plugin("extensions", check=True)
|
1077
|
+
|
1078
|
+
# preserving extensions we don't know about is a MUST
|
1079
|
+
if existing_extensions:
|
1080
|
+
assert existing is not None
|
1081
|
+
for el in existing["conference"]["extensions"].xml:
|
1082
|
+
if el.tag.startswith(f"{{{NOTIFY_NS}}}"):
|
1083
|
+
if notify is not None:
|
1084
|
+
continue
|
1085
|
+
if el.tag.startswith(f"{{{PINNING_NS}}}"):
|
1086
|
+
if pin is not None:
|
1087
|
+
continue
|
1088
|
+
new["conference"]["extensions"].append(el)
|
1062
1089
|
|
1063
1090
|
if pin is not None:
|
1064
|
-
|
1091
|
+
if existing_extensions:
|
1092
|
+
assert existing is not None
|
1093
|
+
existing_pin = (
|
1094
|
+
existing["conference"]["extensions"].get_plugin(
|
1095
|
+
"pinned", check=True
|
1096
|
+
)
|
1097
|
+
is not None
|
1098
|
+
)
|
1099
|
+
if existing_pin != pin:
|
1100
|
+
change = True
|
1101
|
+
new["conference"]["extensions"]["pinned"] = pin
|
1065
1102
|
|
1066
1103
|
if notify is not None:
|
1067
|
-
|
1068
|
-
|
1069
|
-
|
1070
|
-
|
1071
|
-
|
1072
|
-
|
1073
|
-
|
1104
|
+
new["conference"]["extensions"].enable("notify")
|
1105
|
+
if existing_extensions:
|
1106
|
+
assert existing is not None
|
1107
|
+
existing_notify = existing["conference"]["extensions"].get_plugin(
|
1108
|
+
"notify", check=True
|
1109
|
+
)
|
1110
|
+
if existing_notify is None:
|
1111
|
+
change = True
|
1112
|
+
else:
|
1113
|
+
if existing_notify.get_config() != notify:
|
1114
|
+
change = True
|
1115
|
+
for el in existing_notify:
|
1116
|
+
new["conference"]["extensions"]["notify"].append(el)
|
1117
|
+
new["conference"]["extensions"]["notify"].configure(notify)
|
1118
|
+
|
1119
|
+
if change:
|
1120
|
+
iq = Iq(stype="set", sfrom=self.user_jid, sto=self.user_jid)
|
1121
|
+
iq["pubsub"]["publish"]["node"] = self.xmpp["xep_0402"].stanza.NS
|
1122
|
+
iq["pubsub"]["publish"].append(new)
|
1123
|
+
|
1124
|
+
iq["pubsub"]["publish_options"] = _BOOKMARKS_OPTIONS
|
1074
1125
|
|
1075
|
-
|
1076
|
-
|
1077
|
-
|
1078
|
-
|
1079
|
-
|
1080
|
-
|
1081
|
-
|
1082
|
-
|
1083
|
-
|
1084
|
-
|
1085
|
-
|
1086
|
-
|
1087
|
-
|
1088
|
-
|
1126
|
+
try:
|
1127
|
+
await self.xmpp["xep_0356"].send_privileged_iq(iq)
|
1128
|
+
except PermissionError:
|
1129
|
+
warnings.warn(
|
1130
|
+
"IQ privileges (XEP0356) are not set, we cannot add bookmarks for the user"
|
1131
|
+
)
|
1132
|
+
# fallback by forcing invitation
|
1133
|
+
invite = True
|
1134
|
+
except IqError as e:
|
1135
|
+
warnings.warn(
|
1136
|
+
f"Something went wrong while trying to set the bookmarks: {e}"
|
1137
|
+
)
|
1138
|
+
# fallback by forcing invitation
|
1139
|
+
invite = True
|
1140
|
+
else:
|
1141
|
+
self.log.debug("Bookmark does not need updating.")
|
1142
|
+
return
|
1089
1143
|
|
1090
|
-
if invite or (config.ALWAYS_INVITE_WHEN_ADDING_BOOKMARKS and
|
1144
|
+
if invite or (config.ALWAYS_INVITE_WHEN_ADDING_BOOKMARKS and existing is None):
|
1091
1145
|
self.session.send_gateway_invite(
|
1092
1146
|
self, reason="This group could not be added automatically for you"
|
1093
1147
|
)
|
slidge/slixfix/__init__.py
CHANGED
@@ -20,7 +20,6 @@ from . import (
|
|
20
20
|
xep_0100,
|
21
21
|
xep_0153,
|
22
22
|
xep_0292,
|
23
|
-
xep_0356_old,
|
24
23
|
xep_0492,
|
25
24
|
)
|
26
25
|
|
@@ -148,7 +147,6 @@ slixmpp.plugins.PLUGINS.extend(
|
|
148
147
|
[
|
149
148
|
"link_preview",
|
150
149
|
"xep_0292_provider",
|
151
|
-
"xep_0356_old",
|
152
150
|
"xep_0492",
|
153
151
|
]
|
154
152
|
)
|
@@ -93,10 +93,7 @@ class XEP_0100(BasePlugin):
|
|
93
93
|
self.xmpp.send_presence(ptype="subscribe", pto=jid.bare)
|
94
94
|
|
95
95
|
async def _set_roster(self, jid, items):
|
96
|
-
|
97
|
-
await self.xmpp["xep_0356"].set_roster(jid=jid.bare, roster_items=items)
|
98
|
-
except PermissionError:
|
99
|
-
await self.xmpp["xep_0356_old"].set_roster(jid=jid.bare, roster_items=items)
|
96
|
+
await self.xmpp["xep_0356"].set_roster(jid=jid.bare, roster_items=items)
|
100
97
|
|
101
98
|
def on_presence_unsubscribe(self, p: Presence):
|
102
99
|
if p.get_to() == self.xmpp.boundjid.bare:
|
@@ -51,7 +51,7 @@ class Notify(ElementBase):
|
|
51
51
|
self.append(element)
|
52
52
|
|
53
53
|
def get_config(
|
54
|
-
|
54
|
+
self, client_type: Optional[ClientType] = None
|
55
55
|
) -> Optional[WhenLiteral]:
|
56
56
|
"""
|
57
57
|
Get the chat notification settings for this bookmark.
|
@@ -67,7 +67,6 @@ class Notify(ElementBase):
|
|
67
67
|
return cast(WhenLiteral, child.name)
|
68
68
|
return None
|
69
69
|
|
70
|
-
|
71
70
|
class _Base(ElementBase):
|
72
71
|
namespace = NS
|
73
72
|
interfaces = {"client-type"}
|
@@ -75,14 +74,17 @@ class _Base(ElementBase):
|
|
75
74
|
|
76
75
|
class Never(_Base):
|
77
76
|
name = "never"
|
77
|
+
plugin_attrib = name
|
78
78
|
|
79
79
|
|
80
80
|
class Always(_Base):
|
81
81
|
name = "always"
|
82
|
+
plugin_attrib = name
|
82
83
|
|
83
84
|
|
84
85
|
class OnMention(_Base):
|
85
86
|
name = "on-mention"
|
87
|
+
plugin_attrib = name
|
86
88
|
|
87
89
|
|
88
90
|
class Advanced(ElementBase):
|
@@ -100,3 +102,6 @@ _CLASS_MAP = {
|
|
100
102
|
def register_plugin():
|
101
103
|
register_stanza_plugin(Extensions, Notify)
|
102
104
|
register_stanza_plugin(Notify, Advanced)
|
105
|
+
register_stanza_plugin(Notify, Never, iterable=True)
|
106
|
+
register_stanza_plugin(Notify, Always, iterable=True)
|
107
|
+
register_stanza_plugin(Notify, OnMention, iterable=True)
|
slidge/util/util.py
CHANGED
@@ -218,18 +218,18 @@ class SlidgeLogger(logging.Logger):
|
|
218
218
|
log = logging.getLogger(__name__)
|
219
219
|
|
220
220
|
|
221
|
-
def get_version():
|
221
|
+
def get_version() -> str:
|
222
222
|
try:
|
223
223
|
git = subprocess.check_output(
|
224
|
-
["git", "rev-parse", "HEAD"],
|
224
|
+
["git", "rev-parse", "HEAD"],
|
225
|
+
stderr=subprocess.DEVNULL,
|
226
|
+
cwd=Path(__file__).parent,
|
225
227
|
).decode()
|
226
228
|
except (FileNotFoundError, subprocess.CalledProcessError):
|
227
|
-
|
229
|
+
return "NO_VERSION"
|
228
230
|
else:
|
229
231
|
return "git-" + git[:10]
|
230
232
|
|
231
|
-
return "NO_VERSION"
|
232
|
-
|
233
233
|
|
234
234
|
def merge_resources(resources: dict[str, ResourceDict]) -> Optional[ResourceDict]:
|
235
235
|
if len(resources) == 0:
|
@@ -321,7 +321,7 @@ def timeit(func):
|
|
321
321
|
async def wrapped(self, *args, **kwargs):
|
322
322
|
start = time()
|
323
323
|
r = await func(self, *args, **kwargs)
|
324
|
-
self.log.
|
324
|
+
self.log.debug("%s took %s ms", func.__name__, round((time() - start) * 1000))
|
325
325
|
return r
|
326
326
|
|
327
327
|
return wrapped
|
@@ -336,3 +336,7 @@ def strip_leading_emoji(text: str) -> str:
|
|
336
336
|
if len(words) > 1 and emoji.purely_emoji(words[0]):
|
337
337
|
return " ".join(words[1:])
|
338
338
|
return text
|
339
|
+
|
340
|
+
|
341
|
+
async def noop_coro():
|
342
|
+
pass
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.2
|
2
2
|
Name: slidge
|
3
|
-
Version: 0.2.
|
3
|
+
Version: 0.2.7
|
4
4
|
Summary: XMPP bridging framework
|
5
5
|
Author-email: Nicolas Cedilnik <nicoco@nicoco.fr>
|
6
6
|
License: GNU AFFERO GENERAL PUBLIC LICENSE
|
@@ -676,16 +676,15 @@ Classifier: Topic :: Internet :: XMPP
|
|
676
676
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
677
677
|
Requires-Python: >=3.11
|
678
678
|
Description-Content-Type: text/markdown
|
679
|
-
Requires-Dist: aiohttp[speedups]
|
680
|
-
Requires-Dist: alembic
|
681
|
-
Requires-Dist: configargparse
|
679
|
+
Requires-Dist: aiohttp[speedups]<4,>=3.11.11
|
680
|
+
Requires-Dist: alembic<2,>=1.14.0
|
681
|
+
Requires-Dist: configargparse<2,>=1.7
|
682
682
|
Requires-Dist: defusedxml>=0.7.1
|
683
|
-
Requires-Dist:
|
684
|
-
Requires-Dist:
|
685
|
-
Requires-Dist:
|
686
|
-
Requires-Dist:
|
687
|
-
Requires-Dist:
|
688
|
-
Requires-Dist: sqlalchemy>=2.0.36
|
683
|
+
Requires-Dist: pillow<12,>=11.0.0
|
684
|
+
Requires-Dist: python-magic<0.5,>=0.4.27
|
685
|
+
Requires-Dist: qrcode<9,>=8.0
|
686
|
+
Requires-Dist: slixmpp<2,>=1.8.6
|
687
|
+
Requires-Dist: sqlalchemy<3,>=2
|
689
688
|
Requires-Dist: thumbhash>=0.1.2
|
690
689
|
|
691
690
|

|
@@ -1,25 +1,25 @@
|
|
1
1
|
slidge/__init__.py,sha256=S0tUjqpZlzsr8G4Y_1Xt-KCYB07qaknTB0OwHU8k29U,1587
|
2
2
|
slidge/__main__.py,sha256=ydjUklOoavS4YlGfjRX_8BQN2DaSbaXPMi47RkOgcFI,37
|
3
|
-
slidge/__version__.py,sha256=
|
3
|
+
slidge/__version__.py,sha256=4G6eK4xrrezi8g1TxDqa6IID1uzOe-SonV8sT5-MrUs,165
|
4
4
|
slidge/main.py,sha256=vMJzhvUxbeuIXuHxXXs6lm_ShBjXiS9B5Li5Ge4vWPo,6238
|
5
5
|
slidge/migration.py,sha256=4BJmPIRB56_WIhRTqBFIIBXuvnhhBjjOMl4CE7jY6oc,1541
|
6
6
|
slidge/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
7
7
|
slidge/command/__init__.py,sha256=UYf1mjCYbZ5G7PIgaFTWSQRAzEJkQ6dTH8Fu_e_XnO0,613
|
8
|
-
slidge/command/adhoc.py,sha256
|
8
|
+
slidge/command/adhoc.py,sha256=bYLXcYJjbqPE9nBhrEQevqYFh9AS7Ei3m1vTZBIggrc,10557
|
9
9
|
slidge/command/admin.py,sha256=TYrzgCIhjcTIwl1IUaFlUd3D98SPyao10gB20zo8b3Q,6187
|
10
10
|
slidge/command/base.py,sha256=EDcEl5dJcooSmLarXI2fmBq6QtU7h-7MOM3DDsxXmTU,13447
|
11
11
|
slidge/command/categories.py,sha256=vF0KGDV9sEn8TNkcMoDRw-u3gEyNHSXghOU2JRHQtKs,351
|
12
12
|
slidge/command/chat_command.py,sha256=z-4qp03rK7kCh3_kEozDViwkDg_hVjHvRCiYYJxedBQ,11153
|
13
13
|
slidge/command/register.py,sha256=BduDI31Kx8CbWWEdjybimTA5Wcfhn-Jkt8sSPsySCpo,6724
|
14
|
-
slidge/command/user.py,sha256=
|
14
|
+
slidge/command/user.py,sha256=fLh5d7XTSXicj6g0I80F5n6BFaA20PaYoXFkfDOR4zA,12303
|
15
15
|
slidge/contact/__init__.py,sha256=WMMaHk7UW7YT9EH2LtPdkU0bHQaOp4ikBhbBQskmoc8,191
|
16
|
-
slidge/contact/contact.py,sha256=
|
16
|
+
slidge/contact/contact.py,sha256=ITcjW_3VMdVEHQfqxscpIULnofZBJ841wvnYwOvk1Tc,22934
|
17
17
|
slidge/contact/roster.py,sha256=x3speGdHbZ-VTLoQLQW4s53rBeBvW87W8ZibCCZSLDA,10300
|
18
18
|
slidge/core/__init__.py,sha256=RG7Jj5JCJERjhqJ31lOLYV-7bH_oblClQD1KF9LsTXo,68
|
19
|
-
slidge/core/config.py,sha256=
|
20
|
-
slidge/core/gateway.py,sha256=
|
19
|
+
slidge/core/config.py,sha256=OjJfpXJaDhMxRB-vYA0cqkSf0fwMt-HMThM8GS1htCg,7964
|
20
|
+
slidge/core/gateway.py,sha256=qdvLGh5GF823mACVw5j1WLYMzsSmeGpmQFuWvz5Z9AU,37028
|
21
21
|
slidge/core/pubsub.py,sha256=BoeYE__ptmRAn4x55Hn_6JWRA4nM-XJgDemG5Cy5kN4,11959
|
22
|
-
slidge/core/session.py,sha256=
|
22
|
+
slidge/core/session.py,sha256=Y6psOKm_lv4q7yXARiLuijvSebuS64NjqSNF1WARtHM,28439
|
23
23
|
slidge/core/dispatcher/__init__.py,sha256=1EXcjXietUKlxEqdrCWCV3xZ3q_DSsjHoqWrPMbtYao,84
|
24
24
|
slidge/core/dispatcher/caps.py,sha256=vzCAXo_bhALuLEpJWtyJTzVfWx96g1AsWD8_wkoDl0Y,2028
|
25
25
|
slidge/core/dispatcher/disco.py,sha256=j56VY9NIFzwPEWFKQQZ7YIqS9GdD-ZaF_K8a2L-JvRk,2006
|
@@ -46,7 +46,7 @@ slidge/core/mixins/base.py,sha256=MOd-pas38_52VawQVlxWtBtmTKC6My9G0ZaCeQxOJbs,74
|
|
46
46
|
slidge/core/mixins/db.py,sha256=5Qpegd7D8e5TLXLLINYcf_DuVdN-7wNmsfztUuFYPcU,442
|
47
47
|
slidge/core/mixins/disco.py,sha256=jk3Z1B6zTuisHv8VKNRJodIo0ee5btYHh2ZrlflPj_Q,3670
|
48
48
|
slidge/core/mixins/lock.py,sha256=Vf1rrkbyNbSprr38WGfZiMgTB7AdbqH8ppFHY8N2yXE,975
|
49
|
-
slidge/core/mixins/message.py,sha256=
|
49
|
+
slidge/core/mixins/message.py,sha256=X8Ka8j0nOnBcecYE_YuK8_J7MeO5-R0TIZw4X8c7R3Y,7846
|
50
50
|
slidge/core/mixins/message_maker.py,sha256=TcCutHi0sIwL6beJNkN7XyR0aDIbA0xZyxd2Gc9ulG4,6022
|
51
51
|
slidge/core/mixins/message_text.py,sha256=pCY4tezEuwB2ZuUyUi72i4v9AJkxp_SWF1jrFsn94Ns,8096
|
52
52
|
slidge/core/mixins/presence.py,sha256=yywo6KAw8C7GaZSMrSMuioNfhW08MrnobHt8XbHd0q8,7891
|
@@ -54,12 +54,12 @@ slidge/core/mixins/recipient.py,sha256=b0uFnpym-hOFgYxGjXT1xQcZ4YRbDSBftPcNWLzSw
|
|
54
54
|
slidge/db/__init__.py,sha256=EBDH1JSEhgqYcli2Bw11CRC749wJk8AOucgBzmhDSvU,105
|
55
55
|
slidge/db/avatar.py,sha256=z5e72STv8PdN6zkNyKlLqF7NFxHwCa6IjwgFpzu5ghE,8033
|
56
56
|
slidge/db/meta.py,sha256=v1Jf-npZ28QwdGpsLQWLBHEbEP3-jnPrygRg05tJ_Iw,1831
|
57
|
-
slidge/db/models.py,sha256=
|
58
|
-
slidge/db/store.py,sha256=
|
57
|
+
slidge/db/models.py,sha256=MSVNW04x05qfxahvjCYRDFjfFP-XXp-lOHnK5IqFCXw,14046
|
58
|
+
slidge/db/store.py,sha256=ETk6oUUEz8-YUFGCh7nspSJH7dFOj8qeIWLQhX4nOH8,47051
|
59
59
|
slidge/db/alembic/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
60
60
|
slidge/db/alembic/env.py,sha256=hsBlRNs0zF5diSHGRSa8Fi3qRVQDA2rJdR41AEIdvxc,1642
|
61
|
-
slidge/db/alembic/old_user_store.py,sha256=zFOv0JEWQQK0_TMRlU4Z0G5Mc9pxvEErLyOzXmRAe5Q,5209
|
62
61
|
slidge/db/alembic/script.py.mako,sha256=MEqL-2qATlST9TAOeYgscMn1uy6HUS9NFvDgl93dMj8,635
|
62
|
+
slidge/db/alembic/versions/04cf35e3cf85_add_participant_nickname_no_illegal.py,sha256=Dwz_azOXr7Tsw7Wnj0L8mknITIPXO9ewEsRn169EUNA,904
|
63
63
|
slidge/db/alembic/versions/09f27f098baa_add_missing_attributes_in_room.py,sha256=mUL-0Io6ZPd_QbnKfwGYyjdMcM2uxQ0Wg72H23-2t_E,1033
|
64
64
|
slidge/db/alembic/versions/15b0bd83407a_remove_bogus_unique_constraints_on_room_.py,sha256=kzHuHGhzey5CY0p_OsKf5a-3zSk2649wqg2ToLiSD1I,2927
|
65
65
|
slidge/db/alembic/versions/2461390c0af2_store_contacts_caps_verstring_in_db.py,sha256=CLB-kOP9Rc0FJIKDLef912L5sYkjpTIPC8fhrIdrC7k,1084
|
@@ -71,7 +71,7 @@ slidge/db/alembic/versions/5bd48bfdffa2_lift_room_legacy_id_constraint.py,sha256
|
|
71
71
|
slidge/db/alembic/versions/82a4af84b679_add_muc_history_filled.py,sha256=g37po0ydp8ZmzJrE5oFV7GscnploxjCtPDpw28SqVGk,1429
|
72
72
|
slidge/db/alembic/versions/8b993243a536_add_vcard_content_to_contact_table.py,sha256=18tG8B03Kq8Qz_-mMd28Beed6jow8XNTtrz7gT5QY3g,1210
|
73
73
|
slidge/db/alembic/versions/8d2ced764698_rely_on_db_to_store_contacts_rooms_and_.py,sha256=ikoAlRV3_BJcDcFRANF-9HTB--0xpY0C5XdGuMuW9c0,4866
|
74
|
-
slidge/db/alembic/versions/aa9d82a7f6ef_db_creation.py,sha256=
|
74
|
+
slidge/db/alembic/versions/aa9d82a7f6ef_db_creation.py,sha256=eFlfn-LiDph05yyCc8gmtJwVKzgrSHwyWJ6nuVfEpQA,1391
|
75
75
|
slidge/db/alembic/versions/abba1ae0edb3_store_avatar_legacy_id_in_the_contact_.py,sha256=VprqEVHipYuM-ea-CIM4_ubOD5zJ9inLTbhXc869n3A,2779
|
76
76
|
slidge/db/alembic/versions/b33993e87db3_move_everything_to_persistent_db.py,sha256=2tiRxoC9PYOQn6XQrwK0JTEsb45Pzp2PsKoZSS4rcIA,7564
|
77
77
|
slidge/db/alembic/versions/b64b1a793483_add_source_and_legacy_id_for_archived_.py,sha256=r2sOgR5HcfueJyc3cWNDRmlZzdHOSX6nl2gef54wDbk,1559
|
@@ -80,9 +80,9 @@ slidge/db/alembic/versions/e91195719c2c_store_users_avatars_persistently.py,sha2
|
|
80
80
|
slidge/group/__init__.py,sha256=yFt7cHqeaKIMN6f9ZyhhspOcJJvBtLedGv-iICG7lto,258
|
81
81
|
slidge/group/archive.py,sha256=IPqklzo0UN3lPHckfsKW9c4nl3m_9XGY4u0eehrhe8k,5281
|
82
82
|
slidge/group/bookmarks.py,sha256=AvFL34bEX6n3OP1Np309T5hrLK9GnjkjdyLJ3uiLZyc,6616
|
83
|
-
slidge/group/participant.py,sha256=
|
84
|
-
slidge/group/room.py,sha256=
|
85
|
-
slidge/slixfix/__init__.py,sha256=
|
83
|
+
slidge/group/participant.py,sha256=cUuyJRGq8AIHrwtubje5cyb5hHY2hGLtGboBju4SI0c,17781
|
84
|
+
slidge/group/room.py,sha256=IilOT_Z5P-gh0lo5KLFc054z6sOrxJQ6MvjPmLKXiRs,49011
|
85
|
+
slidge/slixfix/__init__.py,sha256=Og6_EAuWst6paWmDiGqeqQH6Iof6V8Vkr5pyYisgjsw,4282
|
86
86
|
slidge/slixfix/delivery_receipt.py,sha256=3bWdZH3-X3CZJXmnI_TpjkTUUK-EY4Ktm78lW0-40fc,1366
|
87
87
|
slidge/slixfix/roster.py,sha256=KvDjh9q7pqaZf69H93okfib13cc95uVZUJ6rzpqmDaU,1704
|
88
88
|
slidge/slixfix/link_preview/__init__.py,sha256=TDPTSEH5FQxgGpQpQIde-D72AHg-6YVWG-tOj4KpKmU,290
|
@@ -92,27 +92,23 @@ slidge/slixfix/xep_0077/__init__.py,sha256=0lY1YXdgAsfrfxI_Woxaf1etHCJXe35Xtntq_
|
|
92
92
|
slidge/slixfix/xep_0077/register.py,sha256=6nwTfHNL7Z9-1wUhpAF743TNbjQLCMP7Rflkdad8d60,10431
|
93
93
|
slidge/slixfix/xep_0077/stanza.py,sha256=Lngly7F1ChCkNKn7yl1QmN838fO-KqkAhkazxzDsz80,2410
|
94
94
|
slidge/slixfix/xep_0100/__init__.py,sha256=AtEXDQOrEWodkN3fgKR0W3Ezsz_Zza6cgO5ZaZS-JOo,107
|
95
|
-
slidge/slixfix/xep_0100/gateway.py,sha256=
|
95
|
+
slidge/slixfix/xep_0100/gateway.py,sha256=aSR18605MWVqgucNd_zd1SdYlXry7k5uoqDzsvVqQyM,4363
|
96
96
|
slidge/slixfix/xep_0100/stanza.py,sha256=7vCzej9VFQupsTpGGl0cJWuGNH4I6oVcckBu_-fE55c,232
|
97
97
|
slidge/slixfix/xep_0153/__init__.py,sha256=hsEldnLuzvcp0NqSscxPV7FJl-6GFP372vlDg1G3S3I,283
|
98
98
|
slidge/slixfix/xep_0153/vcard_avatar.py,sha256=py-qzj1jmmzsM4GCTKLRW7cAdAmSVjodp6q0r5B0RqQ,458
|
99
99
|
slidge/slixfix/xep_0292/__init__.py,sha256=_MvS9wGra6ig3P_dPAVlCPDJkiOFvUWGjaRsHj1woUg,98
|
100
100
|
slidge/slixfix/xep_0292/vcard4.py,sha256=jL-TOW3eG2QXLduSLNq03L8HoUNmvy8kTZI5ojvo6GE,358
|
101
|
-
slidge/slixfix/xep_0356_old/__init__.py,sha256=3jGWJX2m5gWgDCxcVqCsCCVPRTcfmU96yenwvAJtOKE,180
|
102
|
-
slidge/slixfix/xep_0356_old/privilege.py,sha256=kcJzFbzhOHtQMtzOJpvvwm1pghSpealWnqhC0zc8dGo,5338
|
103
|
-
slidge/slixfix/xep_0356_old/stanza.py,sha256=i7aqcaTg6PBhVwbHToLtlrwxBj7uO-M7VrYSyElyEKI,1229
|
104
101
|
slidge/slixfix/xep_0492/__init__.py,sha256=kjWVeX3SG_2ohHx0fuMh1gmM2G57Bl6SRo7Mfv6sglA,161
|
105
102
|
slidge/slixfix/xep_0492/notify.py,sha256=8EPSdU3rTzWkHNm8oFr0tK2PmMJ6hBAIr88GoOmHTuQ,340
|
106
|
-
slidge/slixfix/xep_0492/stanza.py,sha256=
|
103
|
+
slidge/slixfix/xep_0492/stanza.py,sha256=TlwyAHozA6zu32QoBb6M12xqWR-ytT0F9XVFqZkd4d4,2895
|
107
104
|
slidge/util/__init__.py,sha256=BELovoTMPcPPGz3D48esBr8A4BRRHXTvavfgnArBgEc,301
|
108
105
|
slidge/util/archive_msg.py,sha256=xXAR0BI5r3d6KKWjae9594izCOv6iI03z2WLuTecNw8,1724
|
109
106
|
slidge/util/conf.py,sha256=1j2OnOsCBar1tOObErhXR5RC3Vl3faliOZ1U8J3My58,6613
|
110
|
-
slidge/util/db.py,sha256=4LxZj8oBYgiSnyBUnF_ALjr0TblkfNQq_p28sCfkHMY,242
|
111
107
|
slidge/util/test.py,sha256=l1VHBsw5Uzk2t7wtkfb9kWvtehcYhw1t_d567JAJFKA,14135
|
112
108
|
slidge/util/types.py,sha256=R_xfS5mRL0XUJIoDpnaAkZlTOoLPerduXBFftaVwIAI,5489
|
113
|
-
slidge/util/util.py,sha256=
|
114
|
-
slidge-0.2.
|
115
|
-
slidge-0.2.
|
116
|
-
slidge-0.2.
|
117
|
-
slidge-0.2.
|
118
|
-
slidge-0.2.
|
109
|
+
slidge/util/util.py,sha256=IfyYLkujW6Pk0kvHpfkpQwejFDHSe5oR4_bczEUnhjs,9678
|
110
|
+
slidge-0.2.7.dist-info/METADATA,sha256=u5B3zAw9h1inyIcHrGvr23-GNtI7Ba4gHNXLRmTtktc,44841
|
111
|
+
slidge-0.2.7.dist-info/WHEEL,sha256=nn6H5-ilmfVryoAQl3ZQ2l8SH5imPWFpm1A5FgEuFV4,91
|
112
|
+
slidge-0.2.7.dist-info/entry_points.txt,sha256=py3_x834fFJ2TEzPd18Wt2DnysdAfuVqJ5zzBrXbAZs,44
|
113
|
+
slidge-0.2.7.dist-info/top_level.txt,sha256=2LRjDYHaGZ5ieCMF8xy58JIiabRMzX-MGMbCZwfE17c,7
|
114
|
+
slidge-0.2.7.dist-info/RECORD,,
|
@@ -1,183 +0,0 @@
|
|
1
|
-
"""
|
2
|
-
This module covers a backend for storing user data persistently and managing a
|
3
|
-
pseudo-roster for the gateway component.
|
4
|
-
"""
|
5
|
-
|
6
|
-
import dataclasses
|
7
|
-
import datetime
|
8
|
-
import logging
|
9
|
-
import os.path
|
10
|
-
import shelve
|
11
|
-
from io import BytesIO
|
12
|
-
from os import PathLike
|
13
|
-
from typing import Iterable, Optional, Union
|
14
|
-
|
15
|
-
from pickle_secure import Pickler, Unpickler
|
16
|
-
from slixmpp import JID, Iq, Message, Presence
|
17
|
-
|
18
|
-
|
19
|
-
# noinspection PyUnresolvedReferences
|
20
|
-
class EncryptedShelf(shelve.DbfilenameShelf):
|
21
|
-
cache: dict
|
22
|
-
dict: dict
|
23
|
-
writeback: bool
|
24
|
-
keyencoding: str
|
25
|
-
_protocol: int
|
26
|
-
|
27
|
-
def __init__(
|
28
|
-
self, filename: PathLike, key: str, flag="c", protocol=None, writeback=False
|
29
|
-
):
|
30
|
-
super().__init__(str(filename), flag, protocol, writeback)
|
31
|
-
self.secret_key = key
|
32
|
-
|
33
|
-
def __getitem__(self, key):
|
34
|
-
try:
|
35
|
-
value = self.cache[key]
|
36
|
-
except KeyError:
|
37
|
-
f = BytesIO(self.dict[key.encode(self.keyencoding)])
|
38
|
-
value = Unpickler(f, key=self.secret_key).load() # type:ignore
|
39
|
-
if self.writeback:
|
40
|
-
self.cache[key] = value
|
41
|
-
return value
|
42
|
-
|
43
|
-
def __setitem__(self, key, value):
|
44
|
-
if self.writeback:
|
45
|
-
self.cache[key] = value
|
46
|
-
f = BytesIO()
|
47
|
-
p = Pickler(f, self._protocol, key=self.secret_key) # type:ignore
|
48
|
-
p.dump(value)
|
49
|
-
self.dict[key.encode(self.keyencoding)] = f.getvalue()
|
50
|
-
|
51
|
-
|
52
|
-
@dataclasses.dataclass
|
53
|
-
class GatewayUser:
|
54
|
-
"""
|
55
|
-
A gateway user
|
56
|
-
"""
|
57
|
-
|
58
|
-
bare_jid: str
|
59
|
-
"""Bare JID of the user"""
|
60
|
-
registration_form: dict[str, Optional[str]]
|
61
|
-
"""Content of the registration form, as a dict"""
|
62
|
-
plugin_data: Optional[dict] = None
|
63
|
-
registration_date: Optional[datetime.datetime] = None
|
64
|
-
|
65
|
-
def __hash__(self):
|
66
|
-
return hash(self.bare_jid)
|
67
|
-
|
68
|
-
def __repr__(self):
|
69
|
-
return f"<User {self.bare_jid}>"
|
70
|
-
|
71
|
-
def __post_init__(self):
|
72
|
-
if self.registration_date is None:
|
73
|
-
self.registration_date = datetime.datetime.now()
|
74
|
-
|
75
|
-
@property
|
76
|
-
def jid(self) -> JID:
|
77
|
-
"""
|
78
|
-
The user's (bare) JID
|
79
|
-
|
80
|
-
:return:
|
81
|
-
"""
|
82
|
-
return JID(self.bare_jid)
|
83
|
-
|
84
|
-
def get(self, field: str, default: str = "") -> Optional[str]:
|
85
|
-
# """
|
86
|
-
# Get fields from the registration form (required to comply with slixmpp backend protocol)
|
87
|
-
#
|
88
|
-
# :param field: Name of the field
|
89
|
-
# :param default: Default value to return if the field is not present
|
90
|
-
#
|
91
|
-
# :return: Value of the field
|
92
|
-
# """
|
93
|
-
return self.registration_form.get(field, default)
|
94
|
-
|
95
|
-
|
96
|
-
class UserStore:
|
97
|
-
"""
|
98
|
-
Basic user store implementation using shelve from the python standard library
|
99
|
-
|
100
|
-
Set_file must be called before it is usable
|
101
|
-
"""
|
102
|
-
|
103
|
-
def __init__(self):
|
104
|
-
self._users: shelve.Shelf[GatewayUser] = None # type: ignore
|
105
|
-
|
106
|
-
def set_file(self, filename: PathLike, secret_key: Optional[str] = None):
|
107
|
-
"""
|
108
|
-
Set the file to use to store user data
|
109
|
-
|
110
|
-
:param filename: Path to the shelf file
|
111
|
-
:param secret_key: Secret key to store files encrypted on disk
|
112
|
-
"""
|
113
|
-
if self._users is not None:
|
114
|
-
raise RuntimeError("Shelf file already set!")
|
115
|
-
if os.path.exists(filename):
|
116
|
-
log.info("Using existing slidge DB: %s", filename)
|
117
|
-
else:
|
118
|
-
log.info("Creating a new slidge DB: %s", filename)
|
119
|
-
if secret_key:
|
120
|
-
self._users = EncryptedShelf(filename, key=secret_key)
|
121
|
-
else:
|
122
|
-
self._users = shelve.open(str(filename))
|
123
|
-
log.info("Registered users in the DB: %s", list(self._users.keys()))
|
124
|
-
|
125
|
-
def get_all(self) -> Iterable[GatewayUser]:
|
126
|
-
"""
|
127
|
-
Get all users in the store
|
128
|
-
|
129
|
-
:return: An iterable of GatewayUsers
|
130
|
-
"""
|
131
|
-
return self._users.values()
|
132
|
-
|
133
|
-
def commit(self, user: GatewayUser):
|
134
|
-
self._users[user.jid.bare] = user
|
135
|
-
self._users.sync()
|
136
|
-
|
137
|
-
def get(self, _gateway_jid, _node, ifrom: JID, iq) -> Optional[GatewayUser]:
|
138
|
-
"""
|
139
|
-
Get a user from the store
|
140
|
-
|
141
|
-
NB: there is no reason to call this, it is used by SliXMPP internal API
|
142
|
-
|
143
|
-
:param _gateway_jid:
|
144
|
-
:param _node:
|
145
|
-
:param ifrom:
|
146
|
-
:param iq:
|
147
|
-
:return:
|
148
|
-
"""
|
149
|
-
if ifrom is None: # bug in SliXMPP's XEP_0100 plugin
|
150
|
-
ifrom = iq["from"]
|
151
|
-
log.debug("Getting user %s", ifrom.bare)
|
152
|
-
return self._users.get(ifrom.bare)
|
153
|
-
|
154
|
-
def get_by_jid(self, jid: JID) -> Optional[GatewayUser]:
|
155
|
-
"""
|
156
|
-
Convenience function to get a user from their JID.
|
157
|
-
|
158
|
-
:param jid: JID of the gateway user
|
159
|
-
:return:
|
160
|
-
"""
|
161
|
-
return self._users.get(jid.bare)
|
162
|
-
|
163
|
-
def get_by_stanza(self, s: Union[Presence, Message, Iq]) -> Optional[GatewayUser]:
|
164
|
-
"""
|
165
|
-
Convenience function to get a user from a stanza they sent.
|
166
|
-
|
167
|
-
:param s: A stanza sent by the gateway user
|
168
|
-
:return:
|
169
|
-
"""
|
170
|
-
return self.get_by_jid(s.get_from())
|
171
|
-
|
172
|
-
def close(self):
|
173
|
-
self._users.sync()
|
174
|
-
self._users.close()
|
175
|
-
|
176
|
-
|
177
|
-
user_store = UserStore()
|
178
|
-
"""
|
179
|
-
A persistent store for slidge users. Not public, but I didn't find how to hide
|
180
|
-
it from the docs!
|
181
|
-
"""
|
182
|
-
|
183
|
-
log = logging.getLogger(__name__)
|
@@ -1,167 +0,0 @@
|
|
1
|
-
import logging
|
2
|
-
import typing
|
3
|
-
from collections import defaultdict
|
4
|
-
|
5
|
-
from slixmpp import JID, Iq, Message
|
6
|
-
from slixmpp.plugins.base import BasePlugin
|
7
|
-
from slixmpp.plugins.xep_0356.permissions import (
|
8
|
-
MessagePermission,
|
9
|
-
Permissions,
|
10
|
-
RosterAccess,
|
11
|
-
)
|
12
|
-
from slixmpp.types import JidStr
|
13
|
-
from slixmpp.xmlstream import StanzaBase
|
14
|
-
from slixmpp.xmlstream.handler import Callback
|
15
|
-
from slixmpp.xmlstream.matcher import StanzaPath
|
16
|
-
|
17
|
-
from . import stanza
|
18
|
-
|
19
|
-
log = logging.getLogger(__name__)
|
20
|
-
|
21
|
-
|
22
|
-
# noinspection PyPep8Naming
|
23
|
-
class XEP_0356_OLD(BasePlugin):
|
24
|
-
"""
|
25
|
-
XEP-0356: Privileged Entity
|
26
|
-
|
27
|
-
Events:
|
28
|
-
|
29
|
-
::
|
30
|
-
|
31
|
-
privileges_advertised_old -- Received message/privilege from the server
|
32
|
-
"""
|
33
|
-
|
34
|
-
name = "xep_0356_old"
|
35
|
-
description = "XEP-0356: Privileged Entity (slidge - old namespace)"
|
36
|
-
dependencies = {"xep_0297"}
|
37
|
-
stanza = stanza
|
38
|
-
|
39
|
-
granted_privileges: defaultdict[JidStr, Permissions] = defaultdict(Permissions)
|
40
|
-
|
41
|
-
def plugin_init(self):
|
42
|
-
if not self.xmpp.is_component:
|
43
|
-
log.error("XEP 0356 is only available for components")
|
44
|
-
return
|
45
|
-
|
46
|
-
stanza.register()
|
47
|
-
|
48
|
-
self.xmpp.register_handler(
|
49
|
-
Callback(
|
50
|
-
"Privileges_old",
|
51
|
-
StanzaPath("message/privilege_old"),
|
52
|
-
self._handle_privilege,
|
53
|
-
)
|
54
|
-
)
|
55
|
-
|
56
|
-
def plugin_end(self):
|
57
|
-
self.xmpp.remove_handler("Privileges_old")
|
58
|
-
|
59
|
-
def _handle_privilege(self, msg: StanzaBase):
|
60
|
-
"""
|
61
|
-
Called when the XMPP server advertise the component's privileges.
|
62
|
-
|
63
|
-
Stores the privileges in this instance's granted_privileges attribute (a dict)
|
64
|
-
and raises the privileges_advertised event
|
65
|
-
"""
|
66
|
-
for perm in msg["privilege_old"]["perms"]:
|
67
|
-
setattr(
|
68
|
-
self.granted_privileges[msg.get_from()], perm["access"], perm["type"]
|
69
|
-
)
|
70
|
-
log.debug(f"Privileges (old): {self.granted_privileges}")
|
71
|
-
self.xmpp.event("privileges_advertised_old")
|
72
|
-
|
73
|
-
def send_privileged_message(self, msg: Message):
|
74
|
-
if (
|
75
|
-
self.granted_privileges[msg.get_from().domain].message
|
76
|
-
!= MessagePermission.OUTGOING
|
77
|
-
):
|
78
|
-
raise PermissionError(
|
79
|
-
"The server hasn't authorized us to send messages on behalf of other users"
|
80
|
-
)
|
81
|
-
else:
|
82
|
-
self._make_privileged_message(msg).send()
|
83
|
-
|
84
|
-
def _make_privileged_message(self, msg: Message):
|
85
|
-
server = msg.get_from().domain
|
86
|
-
wrapped = self.xmpp.make_message(mto=server, mfrom=self.xmpp.boundjid.bare)
|
87
|
-
wrapped["privilege_old"]["forwarded"].append(msg)
|
88
|
-
return wrapped
|
89
|
-
|
90
|
-
def _make_get_roster(self, jid: typing.Union[JID, str], **iq_kwargs):
|
91
|
-
return self.xmpp.make_iq_get(
|
92
|
-
queryxmlns="jabber:iq:roster",
|
93
|
-
ifrom=self.xmpp.boundjid.bare,
|
94
|
-
ito=jid,
|
95
|
-
**iq_kwargs,
|
96
|
-
)
|
97
|
-
|
98
|
-
def _make_set_roster(
|
99
|
-
self,
|
100
|
-
jid: typing.Union[JID, str],
|
101
|
-
roster_items: dict,
|
102
|
-
**iq_kwargs,
|
103
|
-
):
|
104
|
-
iq = self.xmpp.make_iq_set(
|
105
|
-
ifrom=self.xmpp.boundjid.bare,
|
106
|
-
ito=jid,
|
107
|
-
**iq_kwargs,
|
108
|
-
)
|
109
|
-
iq["roster"]["items"] = roster_items
|
110
|
-
return iq
|
111
|
-
|
112
|
-
async def get_roster(self, jid: typing.Union[JID, str], **send_kwargs) -> Iq:
|
113
|
-
"""
|
114
|
-
Return the roster of user on the server the component has privileged access to.
|
115
|
-
|
116
|
-
Raises ValueError if the server did not advertise the corresponding privileges
|
117
|
-
|
118
|
-
:param jid: user we want to fetch the roster from
|
119
|
-
"""
|
120
|
-
if isinstance(jid, str):
|
121
|
-
jid = JID(jid)
|
122
|
-
if self.granted_privileges[jid.domain].roster not in (
|
123
|
-
RosterAccess.GET,
|
124
|
-
RosterAccess.BOTH,
|
125
|
-
):
|
126
|
-
raise PermissionError(
|
127
|
-
"The server did not grant us privileges to get rosters"
|
128
|
-
)
|
129
|
-
else:
|
130
|
-
return await self._make_get_roster(jid).send(**send_kwargs)
|
131
|
-
|
132
|
-
async def set_roster(
|
133
|
-
self, jid: typing.Union[JID, str], roster_items: dict, **send_kwargs
|
134
|
-
) -> Iq:
|
135
|
-
"""
|
136
|
-
Return the roster of user on the server the component has privileged access to.
|
137
|
-
|
138
|
-
Raises ValueError if the server did not advertise the corresponding privileges
|
139
|
-
|
140
|
-
:param jid: user we want to add or modify roster items
|
141
|
-
:param roster_items: a dict containing the roster items' JIDs as keys and
|
142
|
-
nested dicts containing names, subscriptions and groups.
|
143
|
-
Example:
|
144
|
-
{
|
145
|
-
"friend1@example.com": {
|
146
|
-
"name": "Friend 1",
|
147
|
-
"subscription": "both",
|
148
|
-
"groups": ["group1", "group2"],
|
149
|
-
},
|
150
|
-
"friend2@example.com": {
|
151
|
-
"name": "Friend 2",
|
152
|
-
"subscription": "from",
|
153
|
-
"groups": ["group3"],
|
154
|
-
},
|
155
|
-
}
|
156
|
-
"""
|
157
|
-
if isinstance(jid, str):
|
158
|
-
jid = JID(jid)
|
159
|
-
if self.granted_privileges[jid.domain].roster not in (
|
160
|
-
RosterAccess.GET,
|
161
|
-
RosterAccess.BOTH,
|
162
|
-
):
|
163
|
-
raise PermissionError(
|
164
|
-
"The server did not grant us privileges to set rosters"
|
165
|
-
)
|
166
|
-
else:
|
167
|
-
return await self._make_set_roster(jid, roster_items).send(**send_kwargs)
|
@@ -1,44 +0,0 @@
|
|
1
|
-
from slixmpp.plugins.xep_0297 import Forwarded
|
2
|
-
from slixmpp.stanza import Message
|
3
|
-
from slixmpp.xmlstream import ElementBase, register_stanza_plugin
|
4
|
-
|
5
|
-
|
6
|
-
class PrivilegeOld(ElementBase):
|
7
|
-
namespace = "urn:xmpp:privilege:1"
|
8
|
-
name = "privilege"
|
9
|
-
plugin_attrib = "privilege_old"
|
10
|
-
|
11
|
-
def permission(self, access):
|
12
|
-
for perm in self["perms"]:
|
13
|
-
if perm["access"] == access:
|
14
|
-
return perm["type"]
|
15
|
-
|
16
|
-
def roster(self):
|
17
|
-
return self.permission("roster")
|
18
|
-
|
19
|
-
def message(self):
|
20
|
-
return self.permission("message")
|
21
|
-
|
22
|
-
def presence(self):
|
23
|
-
return self.permission("presence")
|
24
|
-
|
25
|
-
def add_perm(self, access, type):
|
26
|
-
# This should only be needed for servers, so maybe out of scope for slixmpp
|
27
|
-
perm = PermOld()
|
28
|
-
perm["type"] = type
|
29
|
-
perm["access"] = access
|
30
|
-
self.append(perm)
|
31
|
-
|
32
|
-
|
33
|
-
class PermOld(ElementBase):
|
34
|
-
namespace = "urn:xmpp:privilege:1"
|
35
|
-
name = "perm"
|
36
|
-
plugin_attrib = "perm"
|
37
|
-
plugin_multi_attrib = "perms"
|
38
|
-
interfaces = {"type", "access"}
|
39
|
-
|
40
|
-
|
41
|
-
def register():
|
42
|
-
register_stanza_plugin(Message, PrivilegeOld)
|
43
|
-
register_stanza_plugin(PrivilegeOld, Forwarded)
|
44
|
-
register_stanza_plugin(PrivilegeOld, PermOld, iterable=True)
|
slidge/util/db.py
DELETED
File without changes
|
File without changes
|