slidge 0.2.0a6__tar.gz → 0.2.0a9__tar.gz

Sign up to get free protection for your applications and to get access to all the features.
Files changed (119) hide show
  1. {slidge-0.2.0a6 → slidge-0.2.0a9}/PKG-INFO +6 -2
  2. {slidge-0.2.0a6 → slidge-0.2.0a9}/pyproject.toml +11 -4
  3. {slidge-0.2.0a6 → slidge-0.2.0a9}/slidge/__version__.py +1 -1
  4. {slidge-0.2.0a6 → slidge-0.2.0a9}/slidge/command/chat_command.py +15 -1
  5. {slidge-0.2.0a6 → slidge-0.2.0a9}/slidge/command/user.py +2 -0
  6. {slidge-0.2.0a6 → slidge-0.2.0a9}/slidge/contact/contact.py +46 -13
  7. {slidge-0.2.0a6 → slidge-0.2.0a9}/slidge/contact/roster.py +0 -3
  8. {slidge-0.2.0a6 → slidge-0.2.0a9}/slidge/core/gateway/base.py +15 -7
  9. {slidge-0.2.0a6 → slidge-0.2.0a9}/slidge/core/gateway/session_dispatcher.py +16 -7
  10. {slidge-0.2.0a6 → slidge-0.2.0a9}/slidge/core/mixins/attachment.py +51 -46
  11. {slidge-0.2.0a6 → slidge-0.2.0a9}/slidge/core/mixins/avatar.py +11 -7
  12. {slidge-0.2.0a6 → slidge-0.2.0a9}/slidge/core/pubsub.py +55 -68
  13. {slidge-0.2.0a6 → slidge-0.2.0a9}/slidge/core/session.py +4 -4
  14. slidge-0.2.0a9/slidge/db/alembic/versions/3071e0fa69d4_add_contact_client_type.py +52 -0
  15. {slidge-0.2.0a6 → slidge-0.2.0a9}/slidge/db/alembic/versions/5bd48bfdffa2_lift_room_legacy_id_constraint.py +19 -13
  16. {slidge-0.2.0a6 → slidge-0.2.0a9}/slidge/db/alembic/versions/aa9d82a7f6ef_db_creation.py +5 -1
  17. slidge-0.2.0a9/slidge/db/alembic/versions/abba1ae0edb3_store_avatar_legacy_id_in_the_contact_.py +78 -0
  18. {slidge-0.2.0a6 → slidge-0.2.0a9}/slidge/db/avatar.py +14 -49
  19. {slidge-0.2.0a6 → slidge-0.2.0a9}/slidge/db/models.py +8 -5
  20. {slidge-0.2.0a6 → slidge-0.2.0a9}/slidge/db/store.py +30 -16
  21. {slidge-0.2.0a6 → slidge-0.2.0a9}/slidge/group/room.py +17 -4
  22. {slidge-0.2.0a6 → slidge-0.2.0a9}/slidge/main.py +8 -3
  23. {slidge-0.2.0a6 → slidge-0.2.0a9}/slidge/migration.py +15 -1
  24. {slidge-0.2.0a6 → slidge-0.2.0a9}/slidge/util/test.py +8 -1
  25. {slidge-0.2.0a6 → slidge-0.2.0a9}/slidge/util/types.py +4 -0
  26. {slidge-0.2.0a6 → slidge-0.2.0a9}/LICENSE +0 -0
  27. {slidge-0.2.0a6 → slidge-0.2.0a9}/README.md +0 -0
  28. {slidge-0.2.0a6 → slidge-0.2.0a9}/slidge/__init__.py +0 -0
  29. {slidge-0.2.0a6 → slidge-0.2.0a9}/slidge/__main__.py +0 -0
  30. {slidge-0.2.0a6 → slidge-0.2.0a9}/slidge/command/__init__.py +0 -0
  31. {slidge-0.2.0a6 → slidge-0.2.0a9}/slidge/command/adhoc.py +0 -0
  32. {slidge-0.2.0a6 → slidge-0.2.0a9}/slidge/command/admin.py +0 -0
  33. {slidge-0.2.0a6 → slidge-0.2.0a9}/slidge/command/base.py +0 -0
  34. {slidge-0.2.0a6 → slidge-0.2.0a9}/slidge/command/categories.py +0 -0
  35. {slidge-0.2.0a6 → slidge-0.2.0a9}/slidge/command/register.py +0 -0
  36. {slidge-0.2.0a6 → slidge-0.2.0a9}/slidge/contact/__init__.py +0 -0
  37. {slidge-0.2.0a6 → slidge-0.2.0a9}/slidge/core/__init__.py +0 -0
  38. {slidge-0.2.0a6 → slidge-0.2.0a9}/slidge/core/config.py +0 -0
  39. {slidge-0.2.0a6 → slidge-0.2.0a9}/slidge/core/gateway/__init__.py +0 -0
  40. {slidge-0.2.0a6 → slidge-0.2.0a9}/slidge/core/gateway/caps.py +0 -0
  41. {slidge-0.2.0a6 → slidge-0.2.0a9}/slidge/core/gateway/delivery_receipt.py +0 -0
  42. {slidge-0.2.0a6 → slidge-0.2.0a9}/slidge/core/gateway/disco.py +0 -0
  43. {slidge-0.2.0a6 → slidge-0.2.0a9}/slidge/core/gateway/mam.py +0 -0
  44. {slidge-0.2.0a6 → slidge-0.2.0a9}/slidge/core/gateway/muc_admin.py +0 -0
  45. {slidge-0.2.0a6 → slidge-0.2.0a9}/slidge/core/gateway/ping.py +0 -0
  46. {slidge-0.2.0a6 → slidge-0.2.0a9}/slidge/core/gateway/presence.py +0 -0
  47. {slidge-0.2.0a6 → slidge-0.2.0a9}/slidge/core/gateway/registration.py +0 -0
  48. {slidge-0.2.0a6 → slidge-0.2.0a9}/slidge/core/gateway/search.py +0 -0
  49. {slidge-0.2.0a6 → slidge-0.2.0a9}/slidge/core/gateway/vcard_temp.py +0 -0
  50. {slidge-0.2.0a6 → slidge-0.2.0a9}/slidge/core/mixins/__init__.py +0 -0
  51. {slidge-0.2.0a6 → slidge-0.2.0a9}/slidge/core/mixins/base.py +0 -0
  52. {slidge-0.2.0a6 → slidge-0.2.0a9}/slidge/core/mixins/db.py +0 -0
  53. {slidge-0.2.0a6 → slidge-0.2.0a9}/slidge/core/mixins/disco.py +0 -0
  54. {slidge-0.2.0a6 → slidge-0.2.0a9}/slidge/core/mixins/lock.py +0 -0
  55. {slidge-0.2.0a6 → slidge-0.2.0a9}/slidge/core/mixins/message.py +0 -0
  56. {slidge-0.2.0a6 → slidge-0.2.0a9}/slidge/core/mixins/message_maker.py +0 -0
  57. {slidge-0.2.0a6 → slidge-0.2.0a9}/slidge/core/mixins/presence.py +0 -0
  58. {slidge-0.2.0a6 → slidge-0.2.0a9}/slidge/core/mixins/recipient.py +0 -0
  59. {slidge-0.2.0a6 → slidge-0.2.0a9}/slidge/db/__init__.py +0 -0
  60. {slidge-0.2.0a6 → slidge-0.2.0a9}/slidge/db/alembic/__init__.py +0 -0
  61. {slidge-0.2.0a6 → slidge-0.2.0a9}/slidge/db/alembic/env.py +0 -0
  62. {slidge-0.2.0a6 → slidge-0.2.0a9}/slidge/db/alembic/old_user_store.py +0 -0
  63. {slidge-0.2.0a6 → slidge-0.2.0a9}/slidge/db/alembic/script.py.mako +0 -0
  64. {slidge-0.2.0a6 → slidge-0.2.0a9}/slidge/db/alembic/versions/09f27f098baa_add_missing_attributes_in_room.py +0 -0
  65. {slidge-0.2.0a6 → slidge-0.2.0a9}/slidge/db/alembic/versions/2461390c0af2_store_contacts_caps_verstring_in_db.py +0 -0
  66. {slidge-0.2.0a6 → slidge-0.2.0a9}/slidge/db/alembic/versions/29f5280c61aa_store_subject_setter_in_room.py +0 -0
  67. {slidge-0.2.0a6 → slidge-0.2.0a9}/slidge/db/alembic/versions/2b1f45ab7379_store_room_subject_setter_by_nickname.py +0 -0
  68. {slidge-0.2.0a6 → slidge-0.2.0a9}/slidge/db/alembic/versions/82a4af84b679_add_muc_history_filled.py +0 -0
  69. {slidge-0.2.0a6 → slidge-0.2.0a9}/slidge/db/alembic/versions/8b993243a536_add_vcard_content_to_contact_table.py +0 -0
  70. {slidge-0.2.0a6 → slidge-0.2.0a9}/slidge/db/alembic/versions/8d2ced764698_rely_on_db_to_store_contacts_rooms_and_.py +0 -0
  71. {slidge-0.2.0a6 → slidge-0.2.0a9}/slidge/db/alembic/versions/b33993e87db3_move_everything_to_persistent_db.py +0 -0
  72. {slidge-0.2.0a6 → slidge-0.2.0a9}/slidge/db/alembic/versions/b64b1a793483_add_source_and_legacy_id_for_archived_.py +0 -0
  73. {slidge-0.2.0a6 → slidge-0.2.0a9}/slidge/db/alembic/versions/c4a8ec35a0e8_per_room_user_nick.py +0 -0
  74. {slidge-0.2.0a6 → slidge-0.2.0a9}/slidge/db/alembic/versions/e91195719c2c_store_users_avatars_persistently.py +0 -0
  75. {slidge-0.2.0a6 → slidge-0.2.0a9}/slidge/db/meta.py +0 -0
  76. {slidge-0.2.0a6 → slidge-0.2.0a9}/slidge/group/__init__.py +0 -0
  77. {slidge-0.2.0a6 → slidge-0.2.0a9}/slidge/group/archive.py +0 -0
  78. {slidge-0.2.0a6 → slidge-0.2.0a9}/slidge/group/bookmarks.py +0 -0
  79. {slidge-0.2.0a6 → slidge-0.2.0a9}/slidge/group/participant.py +0 -0
  80. {slidge-0.2.0a6 → slidge-0.2.0a9}/slidge/py.typed +0 -0
  81. {slidge-0.2.0a6 → slidge-0.2.0a9}/slidge/slixfix/__init__.py +0 -0
  82. {slidge-0.2.0a6 → slidge-0.2.0a9}/slidge/slixfix/link_preview/__init__.py +0 -0
  83. {slidge-0.2.0a6 → slidge-0.2.0a9}/slidge/slixfix/link_preview/link_preview.py +0 -0
  84. {slidge-0.2.0a6 → slidge-0.2.0a9}/slidge/slixfix/link_preview/stanza.py +0 -0
  85. {slidge-0.2.0a6 → slidge-0.2.0a9}/slidge/slixfix/roster.py +0 -0
  86. {slidge-0.2.0a6 → slidge-0.2.0a9}/slidge/slixfix/xep_0077/__init__.py +0 -0
  87. {slidge-0.2.0a6 → slidge-0.2.0a9}/slidge/slixfix/xep_0077/register.py +0 -0
  88. {slidge-0.2.0a6 → slidge-0.2.0a9}/slidge/slixfix/xep_0077/stanza.py +0 -0
  89. {slidge-0.2.0a6 → slidge-0.2.0a9}/slidge/slixfix/xep_0100/__init__.py +0 -0
  90. {slidge-0.2.0a6 → slidge-0.2.0a9}/slidge/slixfix/xep_0100/gateway.py +0 -0
  91. {slidge-0.2.0a6 → slidge-0.2.0a9}/slidge/slixfix/xep_0100/stanza.py +0 -0
  92. {slidge-0.2.0a6 → slidge-0.2.0a9}/slidge/slixfix/xep_0153/__init__.py +0 -0
  93. {slidge-0.2.0a6 → slidge-0.2.0a9}/slidge/slixfix/xep_0153/stanza.py +0 -0
  94. {slidge-0.2.0a6 → slidge-0.2.0a9}/slidge/slixfix/xep_0153/vcard_avatar.py +0 -0
  95. {slidge-0.2.0a6 → slidge-0.2.0a9}/slidge/slixfix/xep_0264/__init__.py +0 -0
  96. {slidge-0.2.0a6 → slidge-0.2.0a9}/slidge/slixfix/xep_0264/stanza.py +0 -0
  97. {slidge-0.2.0a6 → slidge-0.2.0a9}/slidge/slixfix/xep_0264/thumbnail.py +0 -0
  98. {slidge-0.2.0a6 → slidge-0.2.0a9}/slidge/slixfix/xep_0292/__init__.py +0 -0
  99. {slidge-0.2.0a6 → slidge-0.2.0a9}/slidge/slixfix/xep_0292/vcard4.py +0 -0
  100. {slidge-0.2.0a6 → slidge-0.2.0a9}/slidge/slixfix/xep_0313/__init__.py +0 -0
  101. {slidge-0.2.0a6 → slidge-0.2.0a9}/slidge/slixfix/xep_0313/mam.py +0 -0
  102. {slidge-0.2.0a6 → slidge-0.2.0a9}/slidge/slixfix/xep_0313/stanza.py +0 -0
  103. {slidge-0.2.0a6 → slidge-0.2.0a9}/slidge/slixfix/xep_0317/__init__.py +0 -0
  104. {slidge-0.2.0a6 → slidge-0.2.0a9}/slidge/slixfix/xep_0317/hats.py +0 -0
  105. {slidge-0.2.0a6 → slidge-0.2.0a9}/slidge/slixfix/xep_0317/stanza.py +0 -0
  106. {slidge-0.2.0a6 → slidge-0.2.0a9}/slidge/slixfix/xep_0356_old/__init__.py +0 -0
  107. {slidge-0.2.0a6 → slidge-0.2.0a9}/slidge/slixfix/xep_0356_old/privilege.py +0 -0
  108. {slidge-0.2.0a6 → slidge-0.2.0a9}/slidge/slixfix/xep_0356_old/stanza.py +0 -0
  109. {slidge-0.2.0a6 → slidge-0.2.0a9}/slidge/slixfix/xep_0424/__init__.py +0 -0
  110. {slidge-0.2.0a6 → slidge-0.2.0a9}/slidge/slixfix/xep_0424/retraction.py +0 -0
  111. {slidge-0.2.0a6 → slidge-0.2.0a9}/slidge/slixfix/xep_0424/stanza.py +0 -0
  112. {slidge-0.2.0a6 → slidge-0.2.0a9}/slidge/slixfix/xep_0490/__init__.py +0 -0
  113. {slidge-0.2.0a6 → slidge-0.2.0a9}/slidge/slixfix/xep_0490/mds.py +0 -0
  114. {slidge-0.2.0a6 → slidge-0.2.0a9}/slidge/slixfix/xep_0490/stanza.py +0 -0
  115. {slidge-0.2.0a6 → slidge-0.2.0a9}/slidge/util/__init__.py +0 -0
  116. {slidge-0.2.0a6 → slidge-0.2.0a9}/slidge/util/archive_msg.py +0 -0
  117. {slidge-0.2.0a6 → slidge-0.2.0a9}/slidge/util/conf.py +0 -0
  118. {slidge-0.2.0a6 → slidge-0.2.0a9}/slidge/util/db.py +0 -0
  119. {slidge-0.2.0a6 → slidge-0.2.0a9}/slidge/util/util.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: slidge
3
- Version: 0.2.0a6
3
+ Version: 0.2.0a9
4
4
  Summary: XMPP bridging framework
5
5
  Home-page: https://sr.ht/~nicoco/slidge/
6
6
  License: AGPL-3.0-or-later
@@ -10,16 +10,20 @@ Requires-Python: >=3.11,<4.0
10
10
  Classifier: License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)
11
11
  Classifier: Programming Language :: Python :: 3
12
12
  Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Topic :: Internet :: XMPP
14
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
13
15
  Requires-Dist: ConfigArgParse (>=1.5.3,<2.0.0)
14
16
  Requires-Dist: Pillow (>=10,<11)
15
17
  Requires-Dist: aiohttp[speedups] (>=3.8.3,<4.0.0)
16
18
  Requires-Dist: alembic (>=1.13.1,<2.0.0)
17
- Requires-Dist: blurhash-python (>=1.2.1,<2.0.0)
18
19
  Requires-Dist: pickle-secure (>=0.99.9,<0.100.0)
19
20
  Requires-Dist: python-magic (>=0.4.27,<0.5.0)
20
21
  Requires-Dist: qrcode (>=7.4.1,<8.0.0)
21
22
  Requires-Dist: slixmpp (>=1.8.5,<2.0.0)
22
23
  Requires-Dist: sqlalchemy (>=2.0.29,<3.0.0)
24
+ Requires-Dist: thumbhash (>=0.1.2,<0.2.0)
25
+ Project-URL: Bug tracker, https://todo.sr.ht/~nicoco/slidge
26
+ Project-URL: Chat room, https://conference.nicoco.fr:5281/muc_log/slidge/
23
27
  Project-URL: Documentation, https://slidge.im/
24
28
  Project-URL: Repository, https://git.sr.ht/~nicoco/slidge/
25
29
  Description-Content-Type: text/markdown
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "slidge"
3
- version = "0.2.0alpha6"
3
+ version = "0.2.0alpha9"
4
4
  description = "XMPP bridging framework"
5
5
  authors = ["Nicolas Cedilnik <nicoco@nicoco.fr>"]
6
6
  readme = "README.md"
@@ -8,7 +8,14 @@ license = "AGPL-3.0-or-later"
8
8
  homepage = "https://sr.ht/~nicoco/slidge/"
9
9
  repository = "https://git.sr.ht/~nicoco/slidge/"
10
10
  documentation = "https://slidge.im/"
11
- include = ["slidge/util/schema.sql"]
11
+ classifiers = [
12
+ "Topic :: Internet :: XMPP",
13
+ "Topic :: Software Development :: Libraries :: Python Modules",
14
+ ]
15
+
16
+ [tool.poetry.urls]
17
+ "Bug tracker" = "https://todo.sr.ht/~nicoco/slidge"
18
+ "Chat room" = "https://conference.nicoco.fr:5281/muc_log/slidge/"
12
19
 
13
20
  [tool.poetry.dependencies]
14
21
  python = "^3.11"
@@ -19,9 +26,9 @@ ConfigArgParse = "^1.5.3"
19
26
  pickle-secure = "^0.99.9"
20
27
  python-magic = "^0.4.27"
21
28
  slixmpp = "^1.8.5"
22
- blurhash-python = "^1.2.1"
23
29
  sqlalchemy = "^2.0.29"
24
30
  alembic = "^1.13.1"
31
+ thumbhash = "^0.1.2"
25
32
 
26
33
  [build-system]
27
34
  requires = ["poetry-core>=1.0.0"]
@@ -58,7 +65,7 @@ exclude = ["tests", "slidge.slixfix.*"]
58
65
 
59
66
  [[tool.mypy.overrides]]
60
67
  module = [
61
- "blurhash",
68
+ "thumbhash",
62
69
  "configargparse",
63
70
  "qrcode",
64
71
  ]
@@ -2,4 +2,4 @@ from slidge.util.util import get_version # noqa: F401
2
2
 
3
3
  # this is modified before publish, but if someone cloned from the repo,
4
4
  # it can help
5
- __version__ = "0.2.0alpha6"
5
+ __version__ = "0.2.0alpha9"
@@ -153,6 +153,9 @@ class ChatCommandProvider:
153
153
  self.xmpp.delivery_receipt.ack(msg)
154
154
  return await self._handle_result(result, msg, session)
155
155
 
156
+ def __make_uri(self, body: str) -> str:
157
+ return f"xmpp:{self.xmpp.boundjid.bare}?message;body={body}"
158
+
156
159
  async def _handle_result(self, result: CommandResponseType, msg: Message, session):
157
160
  if isinstance(result, str) or result is None:
158
161
  reply = msg.reply()
@@ -175,9 +178,14 @@ class ChatCommandProvider:
175
178
  ).send()
176
179
  if f.options:
177
180
  for o in f.options:
178
- msg.reply(f"{o['value']} -- {o['label']}").send()
181
+ msg.reply(
182
+ f"{o['label']}: {self.__make_uri(o['value'])}"
183
+ ).send()
179
184
  if f.value:
180
185
  msg.reply(f"Default: {f.value}").send()
186
+ if f.type == "boolean":
187
+ msg.reply("yes: " + self.__make_uri("yes")).send()
188
+ msg.reply("no: " + self.__make_uri("no")).send()
181
189
 
182
190
  ans = await self.xmpp.input(
183
191
  msg.get_from(), (f.label or f.var) + "? (or 'abort')"
@@ -186,6 +194,12 @@ class ChatCommandProvider:
186
194
  return await self._handle_result(
187
195
  "Command aborted", msg, session
188
196
  )
197
+ if f.type == "boolean":
198
+ if ans.lower() == "yes":
199
+ ans = "true"
200
+ else:
201
+ ans = "false"
202
+
189
203
  if f.type.endswith("multi"):
190
204
  form_values[f.var] = f.validate(ans.split(" "))
191
205
  else:
@@ -260,6 +260,8 @@ class Preferences(Command):
260
260
  self.xmpp.store.users.update(user)
261
261
  if form_values["sync_avatar"]:
262
262
  await self.xmpp.fetch_user_avatar(session)
263
+ else:
264
+ session.xmpp.store.users.set_avatar_hash(session.user_pk, None)
263
265
  return "Your preferences have been updated."
264
266
 
265
267
 
@@ -17,7 +17,7 @@ from ..core.mixins.disco import ContactAccountDiscoMixin
17
17
  from ..core.mixins.recipient import ReactionRecipientMixin, ThreadRecipientMixin
18
18
  from ..db.models import Contact
19
19
  from ..util import SubclassableOnce
20
- from ..util.types import LegacyUserIdType, MessageOrPresenceTypeVar
20
+ from ..util.types import ClientType, LegacyUserIdType, MessageOrPresenceTypeVar
21
21
 
22
22
  if TYPE_CHECKING:
23
23
  from ..core.session import BaseSession
@@ -128,6 +128,7 @@ class LegacyContact(
128
128
  self._caps_ver: str | None = None
129
129
  self._vcard_fetched = False
130
130
  self._vcard: str | None = None
131
+ self._client_type: ClientType = "pc"
131
132
 
132
133
  async def get_vcard(self, fetch=True) -> VCard4 | None:
133
134
  if fetch and not self._vcard_fetched:
@@ -187,6 +188,32 @@ class LegacyContact(
187
188
  def user_jid(self):
188
189
  return self.session.user_jid
189
190
 
191
+ @property # type:ignore
192
+ def DISCO_TYPE(self) -> ClientType:
193
+ return self._client_type
194
+
195
+ @DISCO_TYPE.setter
196
+ def DISCO_TYPE(self, value: ClientType) -> None:
197
+ self.client_type = value
198
+
199
+ @property
200
+ def client_type(self) -> ClientType:
201
+ """
202
+ The client type of this contact, cf https://xmpp.org/registrar/disco-categories.html#client
203
+
204
+ Default is "pc".
205
+ """
206
+ return self._client_type
207
+
208
+ @client_type.setter
209
+ def client_type(self, value: ClientType) -> None:
210
+ self._client_type = value
211
+ if self._updating_info:
212
+ return
213
+ self.__ensure_pk()
214
+ assert self.contact_pk is not None
215
+ self.xmpp.store.contacts.set_client_type(self.contact_pk, value)
216
+
190
217
  def _set_logger_name(self):
191
218
  self.log.name = f"{self.user_jid.bare}:contact:{self}"
192
219
 
@@ -255,8 +282,9 @@ class LegacyContact(
255
282
  self._privileged_send(stanza)
256
283
  return stanza # type:ignore
257
284
 
258
- if not self._updating_info and isinstance(stanza, Presence):
259
- self.__propagate_to_participants(stanza)
285
+ if isinstance(stanza, Presence):
286
+ if not self._updating_info:
287
+ self.__propagate_to_participants(stanza)
260
288
  if (
261
289
  not self.is_friend
262
290
  and stanza["type"] not in self._NON_FRIEND_PRESENCES_FILTER
@@ -329,14 +357,13 @@ class LegacyContact(
329
357
  return self.xmpp.store.contacts.get_avatar_legacy_id(self.contact_pk)
330
358
 
331
359
  def _post_avatar_update(self):
332
- if self._updating_info:
333
- return
334
- if self.contact_pk is None:
335
- # happens in LegacyRoster.fill(), the contact primary key is not
336
- # set yet, but this will eventually be called in LegacyRoster.__finish_init_contact
337
- self.log.debug("Not setting avatar PK")
338
- return
339
- self.xmpp.store.contacts.set_avatar(self.contact_pk, self._avatar_pk)
360
+ self.__ensure_pk()
361
+ assert self.contact_pk is not None
362
+ self.xmpp.store.contacts.set_avatar(
363
+ self.contact_pk,
364
+ self._avatar_pk,
365
+ None if self.avatar_id is None else str(self.avatar_id),
366
+ )
340
367
  for p in self.participants:
341
368
  self.log.debug("Propagating new avatar to %s", p.muc)
342
369
  p.send_last_presence(force=True, no_cache_online=True)
@@ -603,14 +630,20 @@ class LegacyContact(
603
630
  contact.contact_pk = stored.id
604
631
  contact._name = stored.nick
605
632
  contact._is_friend = stored.is_friend
606
- contact.added_to_roster = stored.added_to_roster
633
+ contact._added_to_roster = stored.added_to_roster
607
634
  if (data := stored.extra_attributes) is not None:
608
635
  contact.deserialize_extra_attributes(data)
609
636
  contact._caps_ver = stored.caps_ver
610
637
  contact._set_logger_name()
611
- contact._set_avatar_from_store(stored)
638
+ contact._AvatarMixin__avatar_unique_id = ( # type:ignore
639
+ None
640
+ if stored.avatar_legacy_id is None
641
+ else session.xmpp.AVATAR_ID_TYPE(stored.avatar_legacy_id)
642
+ )
643
+ contact._avatar_pk = stored.avatar_id
612
644
  contact._vcard = stored.vcard
613
645
  contact._vcard_fetched = stored.vcard_fetched
646
+ contact._client_type = stored.client_type
614
647
  return contact
615
648
 
616
649
 
@@ -150,10 +150,7 @@ class LegacyRoster(
150
150
  except Exception as e:
151
151
  raise XMPPError("internal-server-error", str(e))
152
152
  contact._caps_ver = await contact.get_caps_ver(contact.jid)
153
- need_avatar = contact.contact_pk is None
154
153
  contact.contact_pk = self.__store.update(contact, commit=not self.__filling)
155
- if need_avatar:
156
- contact._post_avatar_update()
157
154
  return contact
158
155
 
159
156
  async def by_stanza(self, s) -> LegacyContact:
@@ -270,6 +270,8 @@ class BaseGateway(
270
270
  Common example: ``int``.
271
271
  """
272
272
 
273
+ http: aiohttp.ClientSession
274
+
273
275
  def __init__(self):
274
276
  self.log = log
275
277
  self.datetime_started = datetime.now()
@@ -303,7 +305,7 @@ class BaseGateway(
303
305
  fix_error_ns=True,
304
306
  )
305
307
  self.loop.set_exception_handler(self.__exception_handler)
306
- self.http: aiohttp.ClientSession = aiohttp.ClientSession()
308
+ self.loop.create_task(self.__set_http())
307
309
  self.has_crashed: bool = False
308
310
  self.use_origin_id = False
309
311
 
@@ -370,6 +372,12 @@ class BaseGateway(
370
372
 
371
373
  MessageMixin.__init__(self) # ComponentXMPP does not call super().__init__()
372
374
 
375
+ async def __set_http(self):
376
+ self.http = aiohttp.ClientSession()
377
+ if getattr(self, "_test_mode", False):
378
+ return
379
+ avatar_cache.http = self.http
380
+
373
381
  async def __mam_cleanup(self):
374
382
  if not config.MAM_MAX_DAYS:
375
383
  return
@@ -455,10 +463,8 @@ class BaseGateway(
455
463
  await disco.del_feature(feature="urn:xmpp:http:upload:0", jid=self.boundjid)
456
464
  await self.plugin["xep_0115"].update_caps(jid=self.boundjid)
457
465
 
458
- if self.COMPONENT_AVATAR:
459
- cached_avatar = await avatar_cache.convert_or_get(
460
- self.COMPONENT_AVATAR, None
461
- )
466
+ if self.COMPONENT_AVATAR is not None:
467
+ cached_avatar = await avatar_cache.convert_or_get(self.COMPONENT_AVATAR)
462
468
  self.avatar_pk = cached_avatar.pk
463
469
  else:
464
470
  cached_avatar = None
@@ -580,6 +586,8 @@ class BaseGateway(
580
586
  session.send_gateway_status(status, show="chat")
581
587
  if session.user.preferences.get("sync_avatar", False):
582
588
  session.create_task(self.fetch_user_avatar(session))
589
+ else:
590
+ self.xmpp.store.users.set_avatar_hash(session.user_pk, None)
583
591
 
584
592
  async def fetch_user_avatar(self, session: BaseSession):
585
593
  try:
@@ -588,8 +596,8 @@ class BaseGateway(
588
596
  self.xmpp.plugin["xep_0084"].stanza.MetaData.namespace,
589
597
  ifrom=self.boundjid.bare,
590
598
  )
591
- except IqError as e:
592
- session.log.debug("Failed to retrieve avatar: %r", e)
599
+ except IqError:
600
+ self.xmpp.store.users.set_avatar_hash(session.user_pk, None)
593
601
  return
594
602
  await self.__dispatcher.on_avatar_metadata_info(
595
603
  session, iq["pubsub"]["items"]["item"]["avatar_metadata"]["info"]
@@ -31,7 +31,6 @@ class Ignore(BaseException):
31
31
  class SessionDispatcher:
32
32
  def __init__(self, xmpp: "BaseGateway"):
33
33
  self.xmpp = xmpp
34
- self.http = xmpp.http
35
34
 
36
35
  xmpp.register_handler(
37
36
  CoroutineCallback(
@@ -98,6 +97,10 @@ class SessionDispatcher:
98
97
  event, _exceptions_to_xmpp_errors(getattr(self, "on_" + event))
99
98
  )
100
99
 
100
+ @property
101
+ def http(self):
102
+ return self.xmpp.http
103
+
101
104
  async def __get_session(
102
105
  self, stanza: Union[Message, Presence, Iq], timeout: Optional[int] = 10
103
106
  ) -> BaseSession:
@@ -503,8 +506,19 @@ class SessionDispatcher:
503
506
  resources,
504
507
  merge_resources(resources),
505
508
  )
509
+ if p.get_type() == "available":
510
+ await self.xmpp.pubsub.on_presence_available(p, None)
506
511
  return
507
512
 
513
+ if p.get_type() == "available":
514
+ try:
515
+ contact = await session.contacts.by_jid(pto)
516
+ except XMPPError:
517
+ contact = None
518
+ if contact is not None:
519
+ await self.xmpp.pubsub.on_presence_available(p, contact)
520
+ return
521
+
508
522
  muc = session.bookmarks.by_jid_only_if_exists(JID(pto.bare))
509
523
 
510
524
  if muc is not None and p.get_type() == "unavailable":
@@ -580,12 +594,7 @@ class SessionDispatcher:
580
594
  if session.user.avatar_hash == hash_:
581
595
  session.log.debug("We already know this avatar hash")
582
596
  return
583
- with self.xmpp.store.session() as orm_session:
584
- user = self.xmpp.store.users.get(session.user_jid)
585
- assert user is not None
586
- user.avatar_hash = hash_
587
- orm_session.add(user)
588
- orm_session.commit()
597
+ self.xmpp.store.users.set_avatar_hash(session.user_pk, None)
589
598
 
590
599
  if hash_:
591
600
  try:
@@ -1,3 +1,4 @@
1
+ import base64
1
2
  import functools
2
3
  import logging
3
4
  import os
@@ -7,22 +8,23 @@ import stat
7
8
  import tempfile
8
9
  import warnings
9
10
  from datetime import datetime
10
- from mimetypes import guess_type
11
+ from itertools import chain
12
+ from mimetypes import guess_extension, guess_type
11
13
  from pathlib import Path
12
14
  from typing import IO, AsyncIterator, Collection, Optional, Sequence, Union
13
15
  from urllib.parse import quote as urlquote
14
16
  from uuid import uuid4
15
17
  from xml.etree import ElementTree as ET
16
18
 
17
- import blurhash
18
- from PIL import Image
19
+ import thumbhash
20
+ from PIL import Image, ImageOps
19
21
  from slixmpp import JID, Message
20
22
  from slixmpp.exceptions import IqError
21
23
  from slixmpp.plugins.xep_0363 import FileUploadError
22
- from slixmpp.plugins.xep_0385.stanza import Sims
23
24
  from slixmpp.plugins.xep_0447.stanza import StatelessFileSharing
24
25
 
25
26
  from ...db.avatar import avatar_cache
27
+ from ...slixfix.xep_0264.stanza import Thumbnail
26
28
  from ...util.types import (
27
29
  LegacyAttachment,
28
30
  LegacyMessageType,
@@ -169,7 +171,12 @@ class AttachmentMixin(MessageMaker):
169
171
  )
170
172
 
171
173
  if file_path is None:
172
- file_name = str(uuid4()) if file_name is None else file_name
174
+ if file_name is None:
175
+ file_name = str(uuid4())
176
+ if content_type is not None:
177
+ ext = guess_extension(content_type, strict=False) # type:ignore
178
+ if ext is not None:
179
+ file_name += ext
173
180
  temp_dir = Path(tempfile.mkdtemp())
174
181
  file_path = temp_dir / file_name
175
182
  if file_url:
@@ -223,37 +230,44 @@ class AttachmentMixin(MessageMaker):
223
230
  content_type: Optional[str] = None,
224
231
  caption: Optional[str] = None,
225
232
  file_name: Optional[str] = None,
226
- ):
233
+ ) -> Thumbnail | None:
227
234
  cache = self.__store.get_sims(uploaded_url)
228
235
  if cache:
229
- msg.append(Sims(xml=ET.fromstring(cache)))
230
- return
236
+ ref = self.xmpp["xep_0372"].stanza.Reference(xml=ET.fromstring(cache))
237
+ msg.append(ref)
238
+ if ref["sims"]["file"].get_plugin("thumbnail", check=True):
239
+ return ref["sims"]["file"]["thumbnail"]
240
+ else:
241
+ return None
231
242
 
232
243
  if not path:
233
- return
244
+ return None
234
245
 
235
- sims = self.xmpp["xep_0385"].get_sims(
246
+ ref = self.xmpp["xep_0385"].get_sims(
236
247
  path, [uploaded_url], content_type, caption
237
248
  )
238
249
  if file_name:
239
- sims["sims"]["file"]["name"] = file_name
250
+ ref["sims"]["file"]["name"] = file_name
251
+ thumbnail = None
240
252
  if content_type is not None and content_type.startswith("image"):
241
253
  try:
242
254
  h, x, y = await self.xmpp.loop.run_in_executor(
243
- avatar_cache._thread_pool, get_blurhash, path
255
+ avatar_cache._thread_pool, get_thumbhash, path
244
256
  )
245
257
  except Exception as e:
246
- log.debug("Could not generate a blurhash", exc_info=e)
258
+ log.debug("Could not generate a thumbhash", exc_info=e)
247
259
  else:
248
- thumbnail = sims["sims"]["file"]["thumbnail"]
260
+ thumbnail = ref["sims"]["file"]["thumbnail"]
249
261
  thumbnail["width"] = x
250
262
  thumbnail["height"] = y
251
- thumbnail["media-type"] = "image/blurhash"
252
- thumbnail["uri"] = "data:image/blurhash," + urlquote(h)
263
+ thumbnail["media-type"] = "image/thumbhash"
264
+ thumbnail["uri"] = "data:image/thumbhash," + urlquote(h)
265
+
266
+ self.__store.set_sims(uploaded_url, str(ref))
253
267
 
254
- self.__store.set_sims(uploaded_url, str(sims))
268
+ msg.append(ref)
255
269
 
256
- msg.append(sims)
270
+ return thumbnail
257
271
 
258
272
  def __set_sfs(
259
273
  self,
@@ -263,6 +277,7 @@ class AttachmentMixin(MessageMaker):
263
277
  content_type: Optional[str] = None,
264
278
  caption: Optional[str] = None,
265
279
  file_name: Optional[str] = None,
280
+ thumbnail: Optional[Thumbnail] = None,
266
281
  ):
267
282
  cache = self.__store.get_sfs(uploaded_url)
268
283
  if cache:
@@ -275,6 +290,8 @@ class AttachmentMixin(MessageMaker):
275
290
  sfs = self.xmpp["xep_0447"].get_sfs(path, [uploaded_url], content_type, caption)
276
291
  if file_name:
277
292
  sfs["file"]["name"] = file_name
293
+ if thumbnail is not None:
294
+ sfs["file"].append(thumbnail)
278
295
  self.__store.set_sfs(uploaded_url, str(sfs))
279
296
 
280
297
  msg.append(sfs)
@@ -371,10 +388,12 @@ class AttachmentMixin(MessageMaker):
371
388
  self._set_msg_id(msg, legacy_msg_id)
372
389
  return None, [self._send(msg, **kwargs)]
373
390
 
374
- await self.__set_sims(
391
+ thumbnail = await self.__set_sims(
375
392
  msg, new_url, local_path, content_type, caption, file_name
376
393
  )
377
- self.__set_sfs(msg, new_url, local_path, content_type, caption, file_name)
394
+ self.__set_sfs(
395
+ msg, new_url, local_path, content_type, caption, file_name, thumbnail
396
+ )
378
397
  if is_temp and isinstance(local_path, Path):
379
398
  local_path.unlink()
380
399
  local_path.parent.rmdir()
@@ -493,32 +512,18 @@ class AttachmentMixin(MessageMaker):
493
512
  )
494
513
 
495
514
 
496
- def get_blurhash(path: Path, n=9) -> tuple[str, int, int]:
497
- img = Image.open(path)
498
- width, height = img.size
499
- n = min(width, height, n)
500
- if width == height:
501
- x = y = n
502
- elif width > height:
503
- x = n
504
- y = round(n * height / width)
505
- else:
506
- x = round(n * width / height)
507
- y = n
508
- # There are 2 blurhash-python packages:
509
- # https://github.com/woltapp/blurhash-python
510
- # https://github.com/halcy/blurhash-python
511
- # With this hack we're compatible with both, which is useful for packaging
512
- # without using pyproject.toml, as most distro do
513
- try:
514
- hash_ = blurhash.encode(img, x, y)
515
- except TypeError:
516
- # We are using halcy's blurhash which expects
517
- # the 1st argument to be a 3-dimensional array
518
- import numpy # type:ignore
519
-
520
- hash_ = blurhash.encode(numpy.array(img.convert("RGB")), x, y)
521
- return hash_, width, height
515
+ def get_thumbhash(path: Path) -> tuple[str, int, int]:
516
+ with path.open("rb") as fp:
517
+ img = Image.open(fp)
518
+ width, height = img.size
519
+ img = img.convert("RGBA")
520
+ if width > 100 or height > 100:
521
+ img.thumbnail((100, 100))
522
+ img = ImageOps.exif_transpose(img)
523
+ rgba_2d = list(img.getdata())
524
+ rgba = list(chain(*rgba_2d))
525
+ ints = thumbhash.rgba_to_thumb_hash(img.width, img.height, rgba)
526
+ return base64.b64encode(bytes(ints)).decode(), width, height
522
527
 
523
528
 
524
529
  log = logging.getLogger(__name__)
@@ -73,6 +73,10 @@ class AvatarMixin:
73
73
  name=f"Set avatar of {self} from property",
74
74
  )
75
75
 
76
+ @property
77
+ def avatar_pk(self) -> int | None:
78
+ return self._avatar_pk
79
+
76
80
  @staticmethod
77
81
  def __get_uid(a: Optional[AvatarType]) -> Optional[AvatarIdType]:
78
82
  if isinstance(a, str):
@@ -95,10 +99,7 @@ class AvatarMixin:
95
99
  self._avatar_pk = None
96
100
  else:
97
101
  try:
98
- cached_avatar = await avatar_cache.convert_or_get(
99
- URL(a) if isinstance(a, URL) else a,
100
- None if isinstance(uid, URL) else uid,
101
- )
102
+ cached_avatar = await avatar_cache.convert_or_get(a)
102
103
  except Exception as e:
103
104
  self.session.log.error("Failed to set avatar %s", a, exc_info=e)
104
105
  self._avatar_pk = None
@@ -167,9 +168,9 @@ class AvatarMixin:
167
168
  await awaitable
168
169
 
169
170
  def get_cached_avatar(self) -> Optional["CachedAvatar"]:
170
- if not self.__avatar_unique_id:
171
+ if self._avatar_pk is None:
171
172
  return None
172
- return avatar_cache.get(self.__avatar_unique_id)
173
+ return avatar_cache.get_by_pk(self._avatar_pk)
173
174
 
174
175
  def get_avatar(self) -> Optional["PepAvatar"]:
175
176
  cached_avatar = self.get_cached_avatar()
@@ -211,7 +212,10 @@ class AvatarMixin:
211
212
  if self.__should_pubsub_broadcast():
212
213
  if new_id is None and cached_id is None:
213
214
  return
214
- cached_avatar = avatar_cache.get(cached_id)
215
+ if self._avatar_pk is not None:
216
+ cached_avatar = avatar_cache.get_by_pk(self._avatar_pk)
217
+ else:
218
+ cached_avatar = None
215
219
  self.__broadcast_task = self.session.xmpp.loop.create_task(
216
220
  self.session.xmpp.pubsub.broadcast_avatar(
217
221
  self.__avatar_jid, self.session.user_jid, cached_avatar