slidge 0.1.2__py3-none-any.whl → 0.2.0__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
Files changed (102) hide show
  1. slidge/__init__.py +3 -5
  2. slidge/__main__.py +2 -197
  3. slidge/__version__.py +5 -0
  4. slidge/command/adhoc.py +40 -17
  5. slidge/command/admin.py +24 -12
  6. slidge/command/base.py +10 -8
  7. slidge/command/categories.py +13 -3
  8. slidge/command/chat_command.py +29 -2
  9. slidge/command/register.py +32 -16
  10. slidge/command/user.py +106 -13
  11. slidge/contact/contact.py +254 -50
  12. slidge/contact/roster.py +124 -53
  13. slidge/core/config.py +19 -13
  14. slidge/core/dispatcher/__init__.py +3 -0
  15. slidge/core/{gateway → dispatcher}/caps.py +12 -8
  16. slidge/core/{gateway → dispatcher}/disco.py +10 -18
  17. slidge/core/dispatcher/message/__init__.py +10 -0
  18. slidge/core/dispatcher/message/chat_state.py +40 -0
  19. slidge/core/dispatcher/message/marker.py +62 -0
  20. slidge/core/dispatcher/message/message.py +397 -0
  21. slidge/core/dispatcher/muc/__init__.py +12 -0
  22. slidge/core/dispatcher/muc/admin.py +98 -0
  23. slidge/core/{gateway → dispatcher/muc}/mam.py +25 -17
  24. slidge/core/dispatcher/muc/misc.py +121 -0
  25. slidge/core/dispatcher/muc/owner.py +96 -0
  26. slidge/core/{gateway → dispatcher/muc}/ping.py +11 -17
  27. slidge/core/dispatcher/presence.py +176 -0
  28. slidge/core/dispatcher/registration.py +85 -0
  29. slidge/core/{gateway → dispatcher}/search.py +9 -16
  30. slidge/core/dispatcher/session_dispatcher.py +84 -0
  31. slidge/core/dispatcher/util.py +174 -0
  32. slidge/core/{gateway/vcard_temp.py → dispatcher/vcard.py} +35 -19
  33. slidge/core/{gateway/base.py → gateway.py} +176 -153
  34. slidge/core/mixins/__init__.py +11 -1
  35. slidge/core/mixins/attachment.py +106 -67
  36. slidge/core/mixins/avatar.py +94 -25
  37. slidge/core/mixins/base.py +10 -4
  38. slidge/core/mixins/db.py +18 -0
  39. slidge/core/mixins/disco.py +0 -10
  40. slidge/core/mixins/lock.py +10 -8
  41. slidge/core/mixins/message.py +11 -195
  42. slidge/core/mixins/message_maker.py +17 -9
  43. slidge/core/mixins/message_text.py +211 -0
  44. slidge/core/mixins/presence.py +17 -4
  45. slidge/core/pubsub.py +114 -288
  46. slidge/core/session.py +101 -40
  47. slidge/db/__init__.py +4 -0
  48. slidge/db/alembic/__init__.py +0 -0
  49. slidge/db/alembic/env.py +64 -0
  50. slidge/db/alembic/old_user_store.py +183 -0
  51. slidge/db/alembic/script.py.mako +26 -0
  52. slidge/db/alembic/versions/09f27f098baa_add_missing_attributes_in_room.py +36 -0
  53. slidge/db/alembic/versions/15b0bd83407a_remove_bogus_unique_constraints_on_room_.py +85 -0
  54. slidge/db/alembic/versions/2461390c0af2_store_contacts_caps_verstring_in_db.py +36 -0
  55. slidge/db/alembic/versions/29f5280c61aa_store_subject_setter_in_room.py +37 -0
  56. slidge/db/alembic/versions/2b1f45ab7379_store_room_subject_setter_by_nickname.py +41 -0
  57. slidge/db/alembic/versions/3071e0fa69d4_add_contact_client_type.py +52 -0
  58. slidge/db/alembic/versions/45c24cc73c91_add_bob.py +42 -0
  59. slidge/db/alembic/versions/5bd48bfdffa2_lift_room_legacy_id_constraint.py +61 -0
  60. slidge/db/alembic/versions/82a4af84b679_add_muc_history_filled.py +48 -0
  61. slidge/db/alembic/versions/8b993243a536_add_vcard_content_to_contact_table.py +43 -0
  62. slidge/db/alembic/versions/8d2ced764698_rely_on_db_to_store_contacts_rooms_and_.py +139 -0
  63. slidge/db/alembic/versions/aa9d82a7f6ef_db_creation.py +101 -0
  64. slidge/db/alembic/versions/abba1ae0edb3_store_avatar_legacy_id_in_the_contact_.py +79 -0
  65. slidge/db/alembic/versions/b33993e87db3_move_everything_to_persistent_db.py +214 -0
  66. slidge/db/alembic/versions/b64b1a793483_add_source_and_legacy_id_for_archived_.py +52 -0
  67. slidge/db/alembic/versions/c4a8ec35a0e8_per_room_user_nick.py +34 -0
  68. slidge/db/alembic/versions/e91195719c2c_store_users_avatars_persistently.py +26 -0
  69. slidge/db/avatar.py +205 -0
  70. slidge/db/meta.py +72 -0
  71. slidge/db/models.py +405 -0
  72. slidge/db/store.py +1257 -0
  73. slidge/group/archive.py +58 -14
  74. slidge/group/bookmarks.py +89 -65
  75. slidge/group/participant.py +111 -44
  76. slidge/group/room.py +402 -213
  77. slidge/main.py +202 -0
  78. slidge/migration.py +45 -1
  79. slidge/slixfix/__init__.py +31 -1
  80. slidge/{core/gateway → slixfix}/delivery_receipt.py +1 -1
  81. slidge/slixfix/roster.py +13 -4
  82. slidge/slixfix/xep_0292/vcard4.py +1 -87
  83. slidge/util/archive_msg.py +2 -1
  84. slidge/util/db.py +4 -228
  85. slidge/util/test.py +91 -4
  86. slidge/util/types.py +39 -4
  87. slidge/util/util.py +45 -2
  88. {slidge-0.1.2.dist-info → slidge-0.2.0.dist-info}/METADATA +10 -5
  89. slidge-0.2.0.dist-info/RECORD +131 -0
  90. slidge-0.2.0.dist-info/entry_points.txt +3 -0
  91. slidge/core/cache.py +0 -183
  92. slidge/core/gateway/__init__.py +0 -3
  93. slidge/core/gateway/muc_admin.py +0 -35
  94. slidge/core/gateway/presence.py +0 -95
  95. slidge/core/gateway/registration.py +0 -53
  96. slidge/core/gateway/session_dispatcher.py +0 -795
  97. slidge/util/schema.sql +0 -126
  98. slidge/util/sql.py +0 -508
  99. slidge-0.1.2.dist-info/RECORD +0 -96
  100. slidge-0.1.2.dist-info/entry_points.txt +0 -3
  101. {slidge-0.1.2.dist-info → slidge-0.2.0.dist-info}/LICENSE +0 -0
  102. {slidge-0.1.2.dist-info → slidge-0.2.0.dist-info}/WHEEL +0 -0
@@ -6,7 +6,6 @@ step for a JID to become a slidge :term:`User`.
6
6
  import asyncio
7
7
  import functools
8
8
  import tempfile
9
- from datetime import datetime
10
9
  from enum import IntEnum
11
10
  from typing import Any
12
11
 
@@ -15,8 +14,10 @@ from slixmpp import JID, Iq
15
14
  from slixmpp.exceptions import XMPPError
16
15
 
17
16
  from ..core import config
18
- from ..util.db import GatewayUser
17
+ from ..db import GatewayUser
18
+ from ..util.types import UserPreferences
19
19
  from .base import Command, CommandAccess, Form, FormField, FormValues
20
+ from .user import Preferences
20
21
 
21
22
 
22
23
  class RegistrationType(IntEnum):
@@ -66,9 +67,12 @@ class Register(Command):
66
67
 
67
68
  SUCCESS_MESSAGE = "Success, welcome!"
68
69
 
69
- def _finalize(self, user: GatewayUser):
70
- user.commit()
71
- self.xmpp.event("user_register", Iq(sfrom=user.jid))
70
+ def _finalize(
71
+ self, form_values: UserPreferences, _session, ifrom: JID, user: GatewayUser, *_
72
+ ) -> str:
73
+ user.preferences = form_values # type: ignore
74
+ self.xmpp.store.users.update(user)
75
+ self.xmpp.event("user_register", Iq(sfrom=ifrom.bare))
72
76
  return self.SUCCESS_MESSAGE
73
77
 
74
78
  async def run(self, _session, _ifrom, *_):
@@ -82,26 +86,26 @@ class Register(Command):
82
86
  async def register(self, form_values: dict[str, Any], _session, ifrom: JID):
83
87
  two_fa_needed = True
84
88
  try:
85
- await self.xmpp.user_prevalidate(ifrom, form_values)
89
+ data = await self.xmpp.user_prevalidate(ifrom, form_values)
86
90
  except ValueError as e:
87
91
  raise XMPPError("bad-request", str(e))
88
92
  except TwoFactorNotRequired:
93
+ data = None
89
94
  if self.xmpp.REGISTRATION_TYPE == RegistrationType.TWO_FACTOR_CODE:
90
95
  two_fa_needed = False
91
96
  else:
92
97
  raise
93
98
 
94
99
  user = GatewayUser(
95
- bare_jid=ifrom.bare,
96
- registration_form=form_values,
97
- registration_date=datetime.now(),
100
+ jid=ifrom.bare,
101
+ legacy_module_data=form_values if data is None else data,
98
102
  )
99
103
 
100
104
  if self.xmpp.REGISTRATION_TYPE == RegistrationType.SINGLE_STEP_FORM or (
101
105
  self.xmpp.REGISTRATION_TYPE == RegistrationType.TWO_FACTOR_CODE
102
106
  and not two_fa_needed
103
107
  ):
104
- return self._finalize(user)
108
+ return await self.preferences(user)
105
109
 
106
110
  if self.xmpp.REGISTRATION_TYPE == RegistrationType.TWO_FACTOR_CODE:
107
111
  return Form(
@@ -113,7 +117,7 @@ class Register(Command):
113
117
 
114
118
  elif self.xmpp.REGISTRATION_TYPE == RegistrationType.QRCODE:
115
119
  self.xmpp.qr_pending_registrations[ # type:ignore
116
- user.bare_jid
120
+ user.jid.bare
117
121
  ] = (
118
122
  self.xmpp.loop.create_future()
119
123
  )
@@ -159,13 +163,15 @@ class Register(Command):
159
163
  self, form_values: FormValues, _session, _ifrom, user: GatewayUser
160
164
  ):
161
165
  assert isinstance(form_values["code"], str)
162
- await self.xmpp.validate_two_factor_code(user, form_values["code"])
163
- return self._finalize(user)
166
+ data = await self.xmpp.validate_two_factor_code(user, form_values["code"])
167
+ if data is not None:
168
+ user.legacy_module_data.update(data)
169
+ return await self.preferences(user)
164
170
 
165
171
  async def qr(self, _form_values: FormValues, _session, _ifrom, user: GatewayUser):
166
172
  try:
167
- await asyncio.wait_for(
168
- self.xmpp.qr_pending_registrations[user.bare_jid], # type:ignore
173
+ data = await asyncio.wait_for(
174
+ self.xmpp.qr_pending_registrations[user.jid.bare], # type:ignore
169
175
  config.QR_TIMEOUT,
170
176
  )
171
177
  except asyncio.TimeoutError:
@@ -176,4 +182,14 @@ class Register(Command):
176
182
  "or you took too much time"
177
183
  ),
178
184
  )
179
- return self._finalize(user)
185
+ if data is not None:
186
+ user.legacy_module_data.update(data)
187
+ return await self.preferences(user)
188
+
189
+ async def preferences(self, user: GatewayUser) -> Form:
190
+ return Form(
191
+ title="Preferences",
192
+ instructions=Preferences.HELP,
193
+ fields=self.xmpp.PREFERENCES,
194
+ handler=functools.partial(self._finalize, user=user), # type:ignore
195
+ )
slidge/command/user.py CHANGED
@@ -1,10 +1,12 @@
1
1
  # Commands available to users
2
+ from copy import deepcopy
2
3
  from typing import TYPE_CHECKING, Any, Optional, Union, cast
3
4
 
4
5
  from slixmpp import JID # type:ignore[attr-defined]
5
6
  from slixmpp.exceptions import XMPPError
6
7
 
7
- from ..util.types import AnyBaseSession, LegacyGroupIdType
8
+ from ..group.room import LegacyMUC
9
+ from ..util.types import AnyBaseSession, LegacyGroupIdType, UserPreferences
8
10
  from .base import (
9
11
  Command,
10
12
  CommandAccess,
@@ -24,8 +26,8 @@ if TYPE_CHECKING:
24
26
  class Search(Command):
25
27
  NAME = "🔎 Search for contacts"
26
28
  HELP = "Search for contacts via this gateway"
27
- NODE = "search"
28
29
  CHAT_COMMAND = "find"
30
+ NODE = CONTACTS.node + "/" + CHAT_COMMAND
29
31
  ACCESS = CommandAccess.USER_LOGGED
30
32
  CATEGORY = CONTACTS
31
33
 
@@ -62,7 +64,8 @@ class SyncContacts(Command):
62
64
  "Synchronize your XMPP roster with your legacy contacts. "
63
65
  "Slidge will only add/remove/modify contacts in its dedicated roster group"
64
66
  )
65
- NODE = CHAT_COMMAND = "sync-contacts"
67
+ CHAT_COMMAND = "sync-contacts"
68
+ NODE = CONTACTS.node + "/" + CHAT_COMMAND
66
69
  ACCESS = CommandAccess.USER_LOGGED
67
70
  CATEGORY = CONTACTS
68
71
 
@@ -76,7 +79,7 @@ class SyncContacts(Command):
76
79
  async def sync(self, session: Optional[AnyBaseSession], _ifrom: JID) -> str:
77
80
  if session is None:
78
81
  raise RuntimeError
79
- roster_iq = await self.xmpp["xep_0356"].get_roster(session.user.bare_jid)
82
+ roster_iq = await self.xmpp["xep_0356"].get_roster(session.user_jid.bare)
80
83
 
81
84
  contacts = session.contacts.known_contacts()
82
85
 
@@ -90,13 +93,13 @@ class SyncContacts(Command):
90
93
  if contact is None:
91
94
  if len(groups) == 1:
92
95
  await self.xmpp["xep_0356"].set_roster(
93
- session.user.jid, {item["jid"]: {"subscription": "remove"}}
96
+ session.user_jid, {item["jid"]: {"subscription": "remove"}}
94
97
  )
95
98
  removed += 1
96
99
  else:
97
100
  groups.remove(self.xmpp.ROSTER_GROUP)
98
101
  await self.xmpp["xep_0356"].set_roster(
99
- session.user.jid,
102
+ session.user_jid,
100
103
  {
101
104
  item["jid"]: {
102
105
  "subscription": item["subscription"],
@@ -121,7 +124,8 @@ class SyncContacts(Command):
121
124
 
122
125
  class ListContacts(Command):
123
126
  NAME = HELP = "👤 List your legacy contacts"
124
- NODE = CHAT_COMMAND = "contacts"
127
+ CHAT_COMMAND = "contacts"
128
+ NODE = CONTACTS.node + "/" + CHAT_COMMAND
125
129
  ACCESS = CommandAccess.USER_LOGGED
126
130
  CATEGORY = CONTACTS
127
131
 
@@ -129,7 +133,6 @@ class ListContacts(Command):
129
133
  self, session: Optional[AnyBaseSession], _ifrom: JID, *_
130
134
  ) -> TableResult:
131
135
  assert session is not None
132
- await session.contacts.fill()
133
136
  contacts = sorted(
134
137
  session.contacts, key=lambda c: c.name.casefold() if c.name else ""
135
138
  )
@@ -142,7 +145,8 @@ class ListContacts(Command):
142
145
 
143
146
  class ListGroups(Command):
144
147
  NAME = HELP = "👥 List your legacy groups"
145
- NODE = CHAT_COMMAND = "groups"
148
+ CHAT_COMMAND = "groups"
149
+ NODE = GROUPS.node + "/" + CHAT_COMMAND
146
150
  ACCESS = CommandAccess.USER_LOGGED
147
151
  CATEGORY = GROUPS
148
152
 
@@ -161,7 +165,8 @@ class ListGroups(Command):
161
165
  class Login(Command):
162
166
  NAME = "🔐 Re-login to the legacy network"
163
167
  HELP = "Login to the legacy service"
164
- NODE = CHAT_COMMAND = "re-login"
168
+ CHAT_COMMAND = "re-login"
169
+ NODE = "https://slidge.im/command/core/" + CHAT_COMMAND
165
170
 
166
171
  ACCESS = CommandAccess.USER_NON_LOGGED
167
172
 
@@ -183,7 +188,8 @@ class Login(Command):
183
188
  class CreateGroup(Command):
184
189
  NAME = "🆕 New legacy group"
185
190
  HELP = "Create a group on the legacy service"
186
- NODE = CHAT_COMMAND = "create-group"
191
+ CHAT_COMMAND = "create-group"
192
+ NODE = GROUPS.node + "/" + CHAT_COMMAND
187
193
  CATEGORY = GROUPS
188
194
 
189
195
  ACCESS = CommandAccess.USER_LOGGED
@@ -229,10 +235,47 @@ class CreateGroup(Command):
229
235
  )
230
236
 
231
237
 
238
+ class Preferences(Command):
239
+ NAME = "⚙️ Preferences"
240
+ HELP = "Customize the gateway behaviour to your liking"
241
+ CHAT_COMMAND = "preferences"
242
+ NODE = "https://slidge.im/command/core/preferences"
243
+ ACCESS = CommandAccess.USER
244
+
245
+ async def run(
246
+ self, session: Optional[AnyBaseSession], _ifrom: JID, *_: Any
247
+ ) -> Form:
248
+ fields = deepcopy(self.xmpp.PREFERENCES)
249
+ assert session is not None
250
+ current = session.user.preferences
251
+ for field in fields:
252
+ field.value = current.get(field.var) # type:ignore
253
+ return Form(
254
+ title="Preferences",
255
+ instructions=self.HELP,
256
+ fields=fields,
257
+ handler=self.finish, # type:ignore
258
+ )
259
+
260
+ async def finish(
261
+ self, form_values: UserPreferences, session: Optional[AnyBaseSession], *_
262
+ ) -> str:
263
+ assert session is not None
264
+ user = session.user
265
+ user.preferences.update(form_values) # type:ignore
266
+ self.xmpp.store.users.update(user)
267
+ if form_values["sync_avatar"]:
268
+ await self.xmpp.fetch_user_avatar(session)
269
+ else:
270
+ session.xmpp.store.users.set_avatar_hash(session.user_pk, None)
271
+ return "Your preferences have been updated."
272
+
273
+
232
274
  class Unregister(Command):
233
275
  NAME = "❌ Unregister from the gateway"
234
276
  HELP = "Unregister from the gateway"
235
- NODE = CHAT_COMMAND = "unregister"
277
+ CHAT_COMMAND = "unregister"
278
+ NODE = "https://slidge.im/command/core/unregister"
236
279
  ACCESS = CommandAccess.USER
237
280
 
238
281
  async def run(
@@ -246,5 +289,55 @@ class Unregister(Command):
246
289
 
247
290
  async def unregister(self, session: Optional[AnyBaseSession], _ifrom: JID) -> str:
248
291
  assert session is not None
249
- await self.xmpp.unregister_user(session.user)
292
+ user = self.xmpp.store.users.get(session.user_jid)
293
+ assert user is not None
294
+ await self.xmpp.unregister_user(user)
250
295
  return "OK"
296
+
297
+
298
+ class LeaveGroup(Command):
299
+ NAME = HELP = "❌ Leave a legacy group"
300
+ CHAT_COMMAND = "leave-group"
301
+ NODE = GROUPS.node + "/" + CHAT_COMMAND
302
+ ACCESS = CommandAccess.USER_LOGGED
303
+ CATEGORY = GROUPS
304
+
305
+ async def run(self, session, _ifrom, *_):
306
+ assert session is not None
307
+ await session.bookmarks.fill()
308
+ groups = sorted(session.bookmarks, key=lambda g: g.DISCO_NAME.casefold())
309
+ return Form(
310
+ title="Leave a group",
311
+ instructions="Select the group you want to leave",
312
+ fields=[
313
+ FormField(
314
+ "group",
315
+ "Group name",
316
+ type="list-single",
317
+ options=[
318
+ {"label": g.name, "value": str(i)} for i, g in enumerate(groups)
319
+ ],
320
+ )
321
+ ],
322
+ handler=self.confirm, # type:ignore
323
+ handler_args=(groups,),
324
+ )
325
+
326
+ async def confirm(
327
+ self,
328
+ form_values: FormValues,
329
+ _session: AnyBaseSession,
330
+ _ifrom,
331
+ groups: list[LegacyMUC],
332
+ ):
333
+ group = groups[int(form_values["group"])] # type:ignore
334
+ return Confirmation(
335
+ prompt=f"Are you sure you want to leave the group '{group.name}'?",
336
+ handler=self.finish, # type:ignore
337
+ handler_args=(group,),
338
+ )
339
+
340
+ @staticmethod
341
+ async def finish(session: AnyBaseSession, _ifrom, group: LegacyMUC):
342
+ await session.on_leave_group(group.legacy_id)
343
+ await session.bookmarks.remove(group, reason="You left this group via slidge.")