slidge 0.2.0a5__tar.gz → 0.2.0a8__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.0a5 → slidge-0.2.0a8}/PKG-INFO +6 -2
  2. {slidge-0.2.0a5 → slidge-0.2.0a8}/pyproject.toml +11 -4
  3. {slidge-0.2.0a5 → slidge-0.2.0a8}/slidge/__version__.py +1 -1
  4. {slidge-0.2.0a5 → slidge-0.2.0a8}/slidge/command/chat_command.py +15 -1
  5. {slidge-0.2.0a5 → slidge-0.2.0a8}/slidge/command/user.py +2 -0
  6. {slidge-0.2.0a5 → slidge-0.2.0a8}/slidge/contact/contact.py +43 -11
  7. {slidge-0.2.0a5 → slidge-0.2.0a8}/slidge/contact/roster.py +0 -3
  8. {slidge-0.2.0a5 → slidge-0.2.0a8}/slidge/core/gateway/base.py +15 -7
  9. {slidge-0.2.0a5 → slidge-0.2.0a8}/slidge/core/gateway/session_dispatcher.py +16 -7
  10. {slidge-0.2.0a5 → slidge-0.2.0a8}/slidge/core/mixins/attachment.py +44 -44
  11. {slidge-0.2.0a5 → slidge-0.2.0a8}/slidge/core/mixins/avatar.py +11 -7
  12. {slidge-0.2.0a5 → slidge-0.2.0a8}/slidge/core/pubsub.py +55 -68
  13. {slidge-0.2.0a5 → slidge-0.2.0a8}/slidge/core/session.py +4 -4
  14. slidge-0.2.0a8/slidge/db/alembic/versions/3071e0fa69d4_add_contact_client_type.py +52 -0
  15. {slidge-0.2.0a5 → slidge-0.2.0a8}/slidge/db/alembic/versions/5bd48bfdffa2_lift_room_legacy_id_constraint.py +19 -13
  16. {slidge-0.2.0a5 → slidge-0.2.0a8}/slidge/db/alembic/versions/aa9d82a7f6ef_db_creation.py +5 -1
  17. slidge-0.2.0a8/slidge/db/alembic/versions/abba1ae0edb3_store_avatar_legacy_id_in_the_contact_.py +78 -0
  18. {slidge-0.2.0a5 → slidge-0.2.0a8}/slidge/db/avatar.py +14 -49
  19. {slidge-0.2.0a5 → slidge-0.2.0a8}/slidge/db/models.py +8 -5
  20. {slidge-0.2.0a5 → slidge-0.2.0a8}/slidge/db/store.py +30 -16
  21. {slidge-0.2.0a5 → slidge-0.2.0a8}/slidge/group/room.py +18 -4
  22. {slidge-0.2.0a5 → slidge-0.2.0a8}/slidge/main.py +8 -3
  23. {slidge-0.2.0a5 → slidge-0.2.0a8}/slidge/migration.py +15 -1
  24. {slidge-0.2.0a5 → slidge-0.2.0a8}/slidge/util/test.py +8 -1
  25. {slidge-0.2.0a5 → slidge-0.2.0a8}/slidge/util/types.py +4 -0
  26. {slidge-0.2.0a5 → slidge-0.2.0a8}/LICENSE +0 -0
  27. {slidge-0.2.0a5 → slidge-0.2.0a8}/README.md +0 -0
  28. {slidge-0.2.0a5 → slidge-0.2.0a8}/slidge/__init__.py +0 -0
  29. {slidge-0.2.0a5 → slidge-0.2.0a8}/slidge/__main__.py +0 -0
  30. {slidge-0.2.0a5 → slidge-0.2.0a8}/slidge/command/__init__.py +0 -0
  31. {slidge-0.2.0a5 → slidge-0.2.0a8}/slidge/command/adhoc.py +0 -0
  32. {slidge-0.2.0a5 → slidge-0.2.0a8}/slidge/command/admin.py +0 -0
  33. {slidge-0.2.0a5 → slidge-0.2.0a8}/slidge/command/base.py +0 -0
  34. {slidge-0.2.0a5 → slidge-0.2.0a8}/slidge/command/categories.py +0 -0
  35. {slidge-0.2.0a5 → slidge-0.2.0a8}/slidge/command/register.py +0 -0
  36. {slidge-0.2.0a5 → slidge-0.2.0a8}/slidge/contact/__init__.py +0 -0
  37. {slidge-0.2.0a5 → slidge-0.2.0a8}/slidge/core/__init__.py +0 -0
  38. {slidge-0.2.0a5 → slidge-0.2.0a8}/slidge/core/config.py +0 -0
  39. {slidge-0.2.0a5 → slidge-0.2.0a8}/slidge/core/gateway/__init__.py +0 -0
  40. {slidge-0.2.0a5 → slidge-0.2.0a8}/slidge/core/gateway/caps.py +0 -0
  41. {slidge-0.2.0a5 → slidge-0.2.0a8}/slidge/core/gateway/delivery_receipt.py +0 -0
  42. {slidge-0.2.0a5 → slidge-0.2.0a8}/slidge/core/gateway/disco.py +0 -0
  43. {slidge-0.2.0a5 → slidge-0.2.0a8}/slidge/core/gateway/mam.py +0 -0
  44. {slidge-0.2.0a5 → slidge-0.2.0a8}/slidge/core/gateway/muc_admin.py +0 -0
  45. {slidge-0.2.0a5 → slidge-0.2.0a8}/slidge/core/gateway/ping.py +0 -0
  46. {slidge-0.2.0a5 → slidge-0.2.0a8}/slidge/core/gateway/presence.py +0 -0
  47. {slidge-0.2.0a5 → slidge-0.2.0a8}/slidge/core/gateway/registration.py +0 -0
  48. {slidge-0.2.0a5 → slidge-0.2.0a8}/slidge/core/gateway/search.py +0 -0
  49. {slidge-0.2.0a5 → slidge-0.2.0a8}/slidge/core/gateway/vcard_temp.py +0 -0
  50. {slidge-0.2.0a5 → slidge-0.2.0a8}/slidge/core/mixins/__init__.py +0 -0
  51. {slidge-0.2.0a5 → slidge-0.2.0a8}/slidge/core/mixins/base.py +0 -0
  52. {slidge-0.2.0a5 → slidge-0.2.0a8}/slidge/core/mixins/db.py +0 -0
  53. {slidge-0.2.0a5 → slidge-0.2.0a8}/slidge/core/mixins/disco.py +0 -0
  54. {slidge-0.2.0a5 → slidge-0.2.0a8}/slidge/core/mixins/lock.py +0 -0
  55. {slidge-0.2.0a5 → slidge-0.2.0a8}/slidge/core/mixins/message.py +0 -0
  56. {slidge-0.2.0a5 → slidge-0.2.0a8}/slidge/core/mixins/message_maker.py +0 -0
  57. {slidge-0.2.0a5 → slidge-0.2.0a8}/slidge/core/mixins/presence.py +0 -0
  58. {slidge-0.2.0a5 → slidge-0.2.0a8}/slidge/core/mixins/recipient.py +0 -0
  59. {slidge-0.2.0a5 → slidge-0.2.0a8}/slidge/db/__init__.py +0 -0
  60. {slidge-0.2.0a5 → slidge-0.2.0a8}/slidge/db/alembic/__init__.py +0 -0
  61. {slidge-0.2.0a5 → slidge-0.2.0a8}/slidge/db/alembic/env.py +0 -0
  62. {slidge-0.2.0a5 → slidge-0.2.0a8}/slidge/db/alembic/old_user_store.py +0 -0
  63. {slidge-0.2.0a5 → slidge-0.2.0a8}/slidge/db/alembic/script.py.mako +0 -0
  64. {slidge-0.2.0a5 → slidge-0.2.0a8}/slidge/db/alembic/versions/09f27f098baa_add_missing_attributes_in_room.py +0 -0
  65. {slidge-0.2.0a5 → slidge-0.2.0a8}/slidge/db/alembic/versions/2461390c0af2_store_contacts_caps_verstring_in_db.py +0 -0
  66. {slidge-0.2.0a5 → slidge-0.2.0a8}/slidge/db/alembic/versions/29f5280c61aa_store_subject_setter_in_room.py +0 -0
  67. {slidge-0.2.0a5 → slidge-0.2.0a8}/slidge/db/alembic/versions/2b1f45ab7379_store_room_subject_setter_by_nickname.py +0 -0
  68. {slidge-0.2.0a5 → slidge-0.2.0a8}/slidge/db/alembic/versions/82a4af84b679_add_muc_history_filled.py +0 -0
  69. {slidge-0.2.0a5 → slidge-0.2.0a8}/slidge/db/alembic/versions/8b993243a536_add_vcard_content_to_contact_table.py +0 -0
  70. {slidge-0.2.0a5 → slidge-0.2.0a8}/slidge/db/alembic/versions/8d2ced764698_rely_on_db_to_store_contacts_rooms_and_.py +0 -0
  71. {slidge-0.2.0a5 → slidge-0.2.0a8}/slidge/db/alembic/versions/b33993e87db3_move_everything_to_persistent_db.py +0 -0
  72. {slidge-0.2.0a5 → slidge-0.2.0a8}/slidge/db/alembic/versions/b64b1a793483_add_source_and_legacy_id_for_archived_.py +0 -0
  73. {slidge-0.2.0a5 → slidge-0.2.0a8}/slidge/db/alembic/versions/c4a8ec35a0e8_per_room_user_nick.py +0 -0
  74. {slidge-0.2.0a5 → slidge-0.2.0a8}/slidge/db/alembic/versions/e91195719c2c_store_users_avatars_persistently.py +0 -0
  75. {slidge-0.2.0a5 → slidge-0.2.0a8}/slidge/db/meta.py +0 -0
  76. {slidge-0.2.0a5 → slidge-0.2.0a8}/slidge/group/__init__.py +0 -0
  77. {slidge-0.2.0a5 → slidge-0.2.0a8}/slidge/group/archive.py +0 -0
  78. {slidge-0.2.0a5 → slidge-0.2.0a8}/slidge/group/bookmarks.py +0 -0
  79. {slidge-0.2.0a5 → slidge-0.2.0a8}/slidge/group/participant.py +0 -0
  80. {slidge-0.2.0a5 → slidge-0.2.0a8}/slidge/py.typed +0 -0
  81. {slidge-0.2.0a5 → slidge-0.2.0a8}/slidge/slixfix/__init__.py +0 -0
  82. {slidge-0.2.0a5 → slidge-0.2.0a8}/slidge/slixfix/link_preview/__init__.py +0 -0
  83. {slidge-0.2.0a5 → slidge-0.2.0a8}/slidge/slixfix/link_preview/link_preview.py +0 -0
  84. {slidge-0.2.0a5 → slidge-0.2.0a8}/slidge/slixfix/link_preview/stanza.py +0 -0
  85. {slidge-0.2.0a5 → slidge-0.2.0a8}/slidge/slixfix/roster.py +0 -0
  86. {slidge-0.2.0a5 → slidge-0.2.0a8}/slidge/slixfix/xep_0077/__init__.py +0 -0
  87. {slidge-0.2.0a5 → slidge-0.2.0a8}/slidge/slixfix/xep_0077/register.py +0 -0
  88. {slidge-0.2.0a5 → slidge-0.2.0a8}/slidge/slixfix/xep_0077/stanza.py +0 -0
  89. {slidge-0.2.0a5 → slidge-0.2.0a8}/slidge/slixfix/xep_0100/__init__.py +0 -0
  90. {slidge-0.2.0a5 → slidge-0.2.0a8}/slidge/slixfix/xep_0100/gateway.py +0 -0
  91. {slidge-0.2.0a5 → slidge-0.2.0a8}/slidge/slixfix/xep_0100/stanza.py +0 -0
  92. {slidge-0.2.0a5 → slidge-0.2.0a8}/slidge/slixfix/xep_0153/__init__.py +0 -0
  93. {slidge-0.2.0a5 → slidge-0.2.0a8}/slidge/slixfix/xep_0153/stanza.py +0 -0
  94. {slidge-0.2.0a5 → slidge-0.2.0a8}/slidge/slixfix/xep_0153/vcard_avatar.py +0 -0
  95. {slidge-0.2.0a5 → slidge-0.2.0a8}/slidge/slixfix/xep_0264/__init__.py +0 -0
  96. {slidge-0.2.0a5 → slidge-0.2.0a8}/slidge/slixfix/xep_0264/stanza.py +0 -0
  97. {slidge-0.2.0a5 → slidge-0.2.0a8}/slidge/slixfix/xep_0264/thumbnail.py +0 -0
  98. {slidge-0.2.0a5 → slidge-0.2.0a8}/slidge/slixfix/xep_0292/__init__.py +0 -0
  99. {slidge-0.2.0a5 → slidge-0.2.0a8}/slidge/slixfix/xep_0292/vcard4.py +0 -0
  100. {slidge-0.2.0a5 → slidge-0.2.0a8}/slidge/slixfix/xep_0313/__init__.py +0 -0
  101. {slidge-0.2.0a5 → slidge-0.2.0a8}/slidge/slixfix/xep_0313/mam.py +0 -0
  102. {slidge-0.2.0a5 → slidge-0.2.0a8}/slidge/slixfix/xep_0313/stanza.py +0 -0
  103. {slidge-0.2.0a5 → slidge-0.2.0a8}/slidge/slixfix/xep_0317/__init__.py +0 -0
  104. {slidge-0.2.0a5 → slidge-0.2.0a8}/slidge/slixfix/xep_0317/hats.py +0 -0
  105. {slidge-0.2.0a5 → slidge-0.2.0a8}/slidge/slixfix/xep_0317/stanza.py +0 -0
  106. {slidge-0.2.0a5 → slidge-0.2.0a8}/slidge/slixfix/xep_0356_old/__init__.py +0 -0
  107. {slidge-0.2.0a5 → slidge-0.2.0a8}/slidge/slixfix/xep_0356_old/privilege.py +0 -0
  108. {slidge-0.2.0a5 → slidge-0.2.0a8}/slidge/slixfix/xep_0356_old/stanza.py +0 -0
  109. {slidge-0.2.0a5 → slidge-0.2.0a8}/slidge/slixfix/xep_0424/__init__.py +0 -0
  110. {slidge-0.2.0a5 → slidge-0.2.0a8}/slidge/slixfix/xep_0424/retraction.py +0 -0
  111. {slidge-0.2.0a5 → slidge-0.2.0a8}/slidge/slixfix/xep_0424/stanza.py +0 -0
  112. {slidge-0.2.0a5 → slidge-0.2.0a8}/slidge/slixfix/xep_0490/__init__.py +0 -0
  113. {slidge-0.2.0a5 → slidge-0.2.0a8}/slidge/slixfix/xep_0490/mds.py +0 -0
  114. {slidge-0.2.0a5 → slidge-0.2.0a8}/slidge/slixfix/xep_0490/stanza.py +0 -0
  115. {slidge-0.2.0a5 → slidge-0.2.0a8}/slidge/util/__init__.py +0 -0
  116. {slidge-0.2.0a5 → slidge-0.2.0a8}/slidge/util/archive_msg.py +0 -0
  117. {slidge-0.2.0a5 → slidge-0.2.0a8}/slidge/util/conf.py +0 -0
  118. {slidge-0.2.0a5 → slidge-0.2.0a8}/slidge/util/db.py +0 -0
  119. {slidge-0.2.0a5 → slidge-0.2.0a8}/slidge/util/util.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: slidge
3
- Version: 0.2.0a5
3
+ Version: 0.2.0a8
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.0alpha5"
3
+ version = "0.2.0alpha8"
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.0alpha5"
5
+ __version__ = "0.2.0alpha8"
@@ -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
 
@@ -329,14 +356,13 @@ class LegacyContact(
329
356
  return self.xmpp.store.contacts.get_avatar_legacy_id(self.contact_pk)
330
357
 
331
358
  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)
359
+ self.__ensure_pk()
360
+ assert self.contact_pk is not None
361
+ self.xmpp.store.contacts.set_avatar(
362
+ self.contact_pk,
363
+ self._avatar_pk,
364
+ None if self.avatar_id is None else str(self.avatar_id),
365
+ )
340
366
  for p in self.participants:
341
367
  self.log.debug("Propagating new avatar to %s", p.muc)
342
368
  p.send_last_presence(force=True, no_cache_online=True)
@@ -603,14 +629,20 @@ class LegacyContact(
603
629
  contact.contact_pk = stored.id
604
630
  contact._name = stored.nick
605
631
  contact._is_friend = stored.is_friend
606
- contact.added_to_roster = stored.added_to_roster
632
+ contact._added_to_roster = stored.added_to_roster
607
633
  if (data := stored.extra_attributes) is not None:
608
634
  contact.deserialize_extra_attributes(data)
609
635
  contact._caps_ver = stored.caps_ver
610
636
  contact._set_logger_name()
611
- contact._set_avatar_from_store(stored)
637
+ contact._AvatarMixin__avatar_unique_id = ( # type:ignore
638
+ None
639
+ if stored.avatar_legacy_id is None
640
+ else session.xmpp.AVATAR_ID_TYPE(stored.avatar_legacy_id)
641
+ )
642
+ contact._avatar_pk = stored.avatar_id
612
643
  contact._vcard = stored.vcard
613
644
  contact._vcard_fetched = stored.vcard_fetched
645
+ contact._client_type = stored.client_type
614
646
  return contact
615
647
 
616
648
 
@@ -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,6 +8,7 @@ import stat
7
8
  import tempfile
8
9
  import warnings
9
10
  from datetime import datetime
11
+ from itertools import chain
10
12
  from mimetypes import guess_type
11
13
  from pathlib import Path
12
14
  from typing import IO, AsyncIterator, Collection, Optional, Sequence, Union
@@ -14,15 +16,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,
@@ -223,37 +225,44 @@ class AttachmentMixin(MessageMaker):
223
225
  content_type: Optional[str] = None,
224
226
  caption: Optional[str] = None,
225
227
  file_name: Optional[str] = None,
226
- ):
228
+ ) -> Thumbnail | None:
227
229
  cache = self.__store.get_sims(uploaded_url)
228
230
  if cache:
229
- msg.append(Sims(xml=ET.fromstring(cache)))
230
- return
231
+ ref = self.xmpp["xep_0372"].stanza.Reference(xml=ET.fromstring(cache))
232
+ msg.append(ref)
233
+ if ref["sims"]["file"].get_plugin("thumbnail", check=True):
234
+ return ref["sims"]["file"]["thumbnail"]
235
+ else:
236
+ return None
231
237
 
232
238
  if not path:
233
- return
239
+ return None
234
240
 
235
- sims = self.xmpp["xep_0385"].get_sims(
241
+ ref = self.xmpp["xep_0385"].get_sims(
236
242
  path, [uploaded_url], content_type, caption
237
243
  )
238
244
  if file_name:
239
- sims["sims"]["file"]["name"] = file_name
245
+ ref["sims"]["file"]["name"] = file_name
246
+ thumbnail = None
240
247
  if content_type is not None and content_type.startswith("image"):
241
248
  try:
242
249
  h, x, y = await self.xmpp.loop.run_in_executor(
243
- avatar_cache._thread_pool, get_blurhash, path
250
+ avatar_cache._thread_pool, get_thumbhash, path
244
251
  )
245
252
  except Exception as e:
246
- log.debug("Could not generate a blurhash", exc_info=e)
253
+ log.debug("Could not generate a thumbhash", exc_info=e)
247
254
  else:
248
- thumbnail = sims["sims"]["file"]["thumbnail"]
255
+ thumbnail = ref["sims"]["file"]["thumbnail"]
249
256
  thumbnail["width"] = x
250
257
  thumbnail["height"] = y
251
- thumbnail["media-type"] = "image/blurhash"
252
- thumbnail["uri"] = "data:image/blurhash," + urlquote(h)
258
+ thumbnail["media-type"] = "image/thumbhash"
259
+ thumbnail["uri"] = "data:image/thumbhash," + urlquote(h)
260
+
261
+ self.__store.set_sims(uploaded_url, str(ref))
253
262
 
254
- self.__store.set_sims(uploaded_url, str(sims))
263
+ msg.append(ref)
255
264
 
256
- msg.append(sims)
265
+ return thumbnail
257
266
 
258
267
  def __set_sfs(
259
268
  self,
@@ -263,6 +272,7 @@ class AttachmentMixin(MessageMaker):
263
272
  content_type: Optional[str] = None,
264
273
  caption: Optional[str] = None,
265
274
  file_name: Optional[str] = None,
275
+ thumbnail: Optional[Thumbnail] = None,
266
276
  ):
267
277
  cache = self.__store.get_sfs(uploaded_url)
268
278
  if cache:
@@ -275,6 +285,8 @@ class AttachmentMixin(MessageMaker):
275
285
  sfs = self.xmpp["xep_0447"].get_sfs(path, [uploaded_url], content_type, caption)
276
286
  if file_name:
277
287
  sfs["file"]["name"] = file_name
288
+ if thumbnail is not None:
289
+ sfs["file"].append(thumbnail)
278
290
  self.__store.set_sfs(uploaded_url, str(sfs))
279
291
 
280
292
  msg.append(sfs)
@@ -371,10 +383,12 @@ class AttachmentMixin(MessageMaker):
371
383
  self._set_msg_id(msg, legacy_msg_id)
372
384
  return None, [self._send(msg, **kwargs)]
373
385
 
374
- await self.__set_sims(
386
+ thumbnail = await self.__set_sims(
375
387
  msg, new_url, local_path, content_type, caption, file_name
376
388
  )
377
- self.__set_sfs(msg, new_url, local_path, content_type, caption, file_name)
389
+ self.__set_sfs(
390
+ msg, new_url, local_path, content_type, caption, file_name, thumbnail
391
+ )
378
392
  if is_temp and isinstance(local_path, Path):
379
393
  local_path.unlink()
380
394
  local_path.parent.rmdir()
@@ -493,32 +507,18 @@ class AttachmentMixin(MessageMaker):
493
507
  )
494
508
 
495
509
 
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
510
+ def get_thumbhash(path: Path) -> tuple[str, int, int]:
511
+ with path.open("rb") as fp:
512
+ img = Image.open(fp)
513
+ width, height = img.size
514
+ img = img.convert("RGBA")
515
+ if width > 100 or height > 100:
516
+ img.thumbnail((100, 100))
517
+ img = ImageOps.exif_transpose(img)
518
+ rgba_2d = list(img.getdata())
519
+ rgba = list(chain(*rgba_2d))
520
+ ints = thumbhash.rgba_to_thumb_hash(img.width, img.height, rgba)
521
+ return base64.b64encode(bytes(ints)).decode(), width, height
522
522
 
523
523
 
524
524
  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