slidge 0.1.0rc1__py3-none-any.whl → 0.1.2__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
Files changed (164) hide show
  1. slidge/__init__.py +54 -31
  2. slidge/__main__.py +51 -5
  3. slidge/command/__init__.py +28 -0
  4. slidge/command/adhoc.py +258 -0
  5. slidge/command/admin.py +193 -0
  6. slidge/command/base.py +441 -0
  7. slidge/command/categories.py +3 -0
  8. slidge/command/chat_command.py +288 -0
  9. slidge/command/register.py +179 -0
  10. slidge/command/user.py +250 -0
  11. slidge/contact/__init__.py +8 -0
  12. slidge/contact/contact.py +452 -0
  13. slidge/contact/roster.py +192 -0
  14. slidge/core/__init__.py +2 -0
  15. slidge/core/cache.py +121 -39
  16. slidge/core/config.py +116 -11
  17. slidge/core/gateway/__init__.py +3 -0
  18. slidge/core/gateway/base.py +895 -0
  19. slidge/core/gateway/caps.py +63 -0
  20. slidge/core/gateway/delivery_receipt.py +52 -0
  21. slidge/core/gateway/disco.py +80 -0
  22. slidge/core/gateway/mam.py +75 -0
  23. slidge/core/gateway/muc_admin.py +35 -0
  24. slidge/core/gateway/ping.py +66 -0
  25. slidge/core/gateway/presence.py +95 -0
  26. slidge/core/gateway/registration.py +53 -0
  27. slidge/core/gateway/search.py +102 -0
  28. slidge/core/gateway/session_dispatcher.py +795 -0
  29. slidge/core/gateway/vcard_temp.py +130 -0
  30. slidge/core/mixins/__init__.py +9 -1
  31. slidge/core/mixins/attachment.py +506 -0
  32. slidge/core/mixins/avatar.py +167 -0
  33. slidge/core/mixins/base.py +6 -19
  34. slidge/core/mixins/disco.py +66 -15
  35. slidge/core/mixins/lock.py +31 -0
  36. slidge/core/mixins/message.py +254 -252
  37. slidge/core/mixins/message_maker.py +154 -0
  38. slidge/core/mixins/presence.py +128 -31
  39. slidge/core/mixins/recipient.py +43 -0
  40. slidge/core/pubsub.py +275 -116
  41. slidge/core/session.py +586 -518
  42. slidge/group/__init__.py +10 -0
  43. slidge/group/archive.py +125 -0
  44. slidge/group/bookmarks.py +163 -0
  45. slidge/group/participant.py +458 -0
  46. slidge/group/room.py +1103 -0
  47. slidge/migration.py +18 -0
  48. slidge/slixfix/__init__.py +68 -0
  49. slidge/{util/xep_0050 → slixfix/link_preview}/__init__.py +4 -5
  50. slidge/slixfix/link_preview/link_preview.py +17 -0
  51. slidge/slixfix/link_preview/stanza.py +99 -0
  52. slidge/slixfix/roster.py +60 -0
  53. slidge/{util → slixfix}/xep_0077/register.py +1 -2
  54. slidge/slixfix/xep_0077/stanza.py +104 -0
  55. slidge/{util → slixfix}/xep_0100/gateway.py +17 -12
  56. slidge/slixfix/xep_0153/__init__.py +10 -0
  57. slidge/slixfix/xep_0153/stanza.py +25 -0
  58. slidge/slixfix/xep_0153/vcard_avatar.py +23 -0
  59. slidge/slixfix/xep_0264/__init__.py +5 -0
  60. slidge/slixfix/xep_0264/stanza.py +36 -0
  61. slidge/slixfix/xep_0264/thumbnail.py +23 -0
  62. slidge/slixfix/xep_0292/__init__.py +5 -0
  63. slidge/slixfix/xep_0292/vcard4.py +100 -0
  64. slidge/slixfix/xep_0313/__init__.py +12 -0
  65. slidge/slixfix/xep_0313/mam.py +262 -0
  66. slidge/slixfix/xep_0313/stanza.py +359 -0
  67. slidge/slixfix/xep_0317/__init__.py +5 -0
  68. slidge/slixfix/xep_0317/hats.py +17 -0
  69. slidge/slixfix/xep_0317/stanza.py +28 -0
  70. slidge/{util → slixfix}/xep_0356_old/privilege.py +9 -7
  71. slidge/slixfix/xep_0424/__init__.py +9 -0
  72. slidge/slixfix/xep_0424/retraction.py +77 -0
  73. slidge/slixfix/xep_0424/stanza.py +28 -0
  74. slidge/slixfix/xep_0490/__init__.py +8 -0
  75. slidge/slixfix/xep_0490/mds.py +47 -0
  76. slidge/slixfix/xep_0490/stanza.py +17 -0
  77. slidge/util/__init__.py +4 -6
  78. slidge/util/archive_msg.py +61 -0
  79. slidge/util/conf.py +25 -4
  80. slidge/util/db.py +23 -69
  81. slidge/util/schema.sql +126 -0
  82. slidge/util/sql.py +508 -0
  83. slidge/util/test.py +136 -86
  84. slidge/util/types.py +155 -14
  85. slidge/util/util.py +225 -51
  86. slidge-0.1.2.dist-info/METADATA +111 -0
  87. slidge-0.1.2.dist-info/RECORD +96 -0
  88. {slidge-0.1.0rc1.dist-info → slidge-0.1.2.dist-info}/WHEEL +1 -1
  89. slidge/core/adhoc.py +0 -492
  90. slidge/core/chat_command.py +0 -197
  91. slidge/core/contact.py +0 -441
  92. slidge/core/disco.py +0 -59
  93. slidge/core/gateway.py +0 -899
  94. slidge/core/muc/__init__.py +0 -3
  95. slidge/core/muc/bookmarks.py +0 -74
  96. slidge/core/muc/participant.py +0 -152
  97. slidge/core/muc/room.py +0 -348
  98. slidge/plugins/discord/__init__.py +0 -121
  99. slidge/plugins/discord/client.py +0 -121
  100. slidge/plugins/discord/session.py +0 -172
  101. slidge/plugins/dummy.py +0 -334
  102. slidge/plugins/facebook.py +0 -591
  103. slidge/plugins/hackernews.py +0 -209
  104. slidge/plugins/mattermost/__init__.py +0 -1
  105. slidge/plugins/mattermost/api.py +0 -288
  106. slidge/plugins/mattermost/gateway.py +0 -417
  107. slidge/plugins/mattermost/websocket.py +0 -248
  108. slidge/plugins/signal/__init__.py +0 -4
  109. slidge/plugins/signal/config.py +0 -4
  110. slidge/plugins/signal/contact.py +0 -104
  111. slidge/plugins/signal/gateway.py +0 -379
  112. slidge/plugins/signal/group.py +0 -76
  113. slidge/plugins/signal/session.py +0 -515
  114. slidge/plugins/signal/txt.py +0 -13
  115. slidge/plugins/signal/util.py +0 -32
  116. slidge/plugins/skype.py +0 -310
  117. slidge/plugins/steam.py +0 -400
  118. slidge/plugins/telegram/__init__.py +0 -6
  119. slidge/plugins/telegram/client.py +0 -325
  120. slidge/plugins/telegram/config.py +0 -21
  121. slidge/plugins/telegram/contact.py +0 -154
  122. slidge/plugins/telegram/gateway.py +0 -182
  123. slidge/plugins/telegram/group.py +0 -184
  124. slidge/plugins/telegram/session.py +0 -275
  125. slidge/plugins/telegram/util.py +0 -153
  126. slidge/plugins/whatsapp/__init__.py +0 -6
  127. slidge/plugins/whatsapp/config.py +0 -17
  128. slidge/plugins/whatsapp/contact.py +0 -33
  129. slidge/plugins/whatsapp/event.go +0 -455
  130. slidge/plugins/whatsapp/gateway.go +0 -156
  131. slidge/plugins/whatsapp/gateway.py +0 -69
  132. slidge/plugins/whatsapp/go.mod +0 -17
  133. slidge/plugins/whatsapp/go.sum +0 -22
  134. slidge/plugins/whatsapp/session.go +0 -371
  135. slidge/plugins/whatsapp/session.py +0 -370
  136. slidge/util/xep_0030/__init__.py +0 -13
  137. slidge/util/xep_0030/disco.py +0 -811
  138. slidge/util/xep_0030/stanza/__init__.py +0 -7
  139. slidge/util/xep_0030/stanza/info.py +0 -270
  140. slidge/util/xep_0030/stanza/items.py +0 -147
  141. slidge/util/xep_0030/static.py +0 -467
  142. slidge/util/xep_0050/adhoc.py +0 -631
  143. slidge/util/xep_0050/stanza.py +0 -180
  144. slidge/util/xep_0077/stanza.py +0 -71
  145. slidge/util/xep_0292/__init__.py +0 -1
  146. slidge/util/xep_0292/stanza.py +0 -167
  147. slidge/util/xep_0292/vcard4.py +0 -74
  148. slidge/util/xep_0356/__init__.py +0 -7
  149. slidge/util/xep_0356/permissions.py +0 -35
  150. slidge/util/xep_0356/privilege.py +0 -160
  151. slidge/util/xep_0356/stanza.py +0 -44
  152. slidge/util/xep_0461/__init__.py +0 -6
  153. slidge/util/xep_0461/reply.py +0 -48
  154. slidge/util/xep_0461/stanza.py +0 -80
  155. slidge-0.1.0rc1.dist-info/METADATA +0 -171
  156. slidge-0.1.0rc1.dist-info/RECORD +0 -99
  157. /slidge/{plugins/__init__.py → py.typed} +0 -0
  158. /slidge/{util → slixfix}/xep_0077/__init__.py +0 -0
  159. /slidge/{util → slixfix}/xep_0100/__init__.py +0 -0
  160. /slidge/{util → slixfix}/xep_0100/stanza.py +0 -0
  161. /slidge/{util → slixfix}/xep_0356_old/__init__.py +0 -0
  162. /slidge/{util → slixfix}/xep_0356_old/stanza.py +0 -0
  163. {slidge-0.1.0rc1.dist-info → slidge-0.1.2.dist-info}/LICENSE +0 -0
  164. {slidge-0.1.0rc1.dist-info → slidge-0.1.2.dist-info}/entry_points.txt +0 -0
slidge/core/pubsub.py CHANGED
@@ -1,116 +1,185 @@
1
- import asyncio
2
1
  import hashlib
3
2
  import io
4
3
  import logging
5
4
  from copy import copy
6
5
  from pathlib import Path
7
- from typing import Optional, Union
6
+ from typing import TYPE_CHECKING, Optional, Type, Union
8
7
 
9
8
  from PIL import Image, UnidentifiedImageError
10
- from slixmpp import JID, ComponentXMPP, CoroutineCallback, Iq, Presence, StanzaPath
9
+ from PIL.Image import Image as PILImage
10
+ from slixmpp import (
11
+ JID,
12
+ CoroutineCallback,
13
+ Iq,
14
+ Presence,
15
+ StanzaPath,
16
+ register_stanza_plugin,
17
+ )
11
18
  from slixmpp.exceptions import XMPPError
12
19
  from slixmpp.plugins.base import BasePlugin, register_plugin
13
20
  from slixmpp.plugins.xep_0060.stanza import Event, EventItem, EventItems, Item
14
21
  from slixmpp.plugins.xep_0084 import Data as AvatarData
15
22
  from slixmpp.plugins.xep_0084 import MetaData as AvatarMetadata
16
23
  from slixmpp.plugins.xep_0172 import UserNick
24
+ from slixmpp.plugins.xep_0292.stanza import VCard4
17
25
  from slixmpp.types import JidStr, OptJidStr
18
26
 
19
- from ..util.db import user_store
20
- from ..util.types import AvatarType, PepItemType
21
- from ..util.xep_0292.stanza import VCard4
22
- from . import config
23
- from .cache import avatar_cache
27
+ from ..contact.contact import LegacyContact
28
+ from ..contact.roster import ContactIsUser
29
+ from ..util.db import GatewayUser, user_store
30
+ from ..util.sql import db
31
+ from ..util.types import AvatarType, LegacyFileIdType, PepItemType
32
+ from .cache import CachedAvatar, avatar_cache
33
+ from .mixins.lock import NamedLockMixin
34
+
35
+ if TYPE_CHECKING:
36
+ from slidge import BaseGateway
24
37
 
25
38
  VCARD4_NAMESPACE = "urn:xmpp:vcard4"
26
39
 
27
40
 
28
41
  class PepItem:
29
- def __init__(self, authorized_jid: Optional[JidStr] = None):
30
- self.authorized_jid = authorized_jid
42
+ @staticmethod
43
+ def from_db(jid: JID, user: Optional[GatewayUser] = None) -> Optional["PepItem"]:
44
+ raise NotImplementedError
45
+
46
+ def to_db(self, jid: JID, user: Optional[GatewayUser] = None):
47
+ raise NotImplementedError
31
48
 
32
49
 
33
50
  class PepAvatar(PepItem):
34
- def __init__(self, authorized_jid: Optional[JidStr] = None):
35
- super().__init__(authorized_jid)
51
+ def __init__(self, jid: JID):
36
52
  self.metadata: Optional[AvatarMetadata] = None
37
- self.data: Optional[AvatarData] = None
38
53
  self.id: Optional[str] = None
54
+ self.jid = jid
55
+ self._avatar_data_path: Optional[Path] = None
56
+
57
+ @property
58
+ def data(self) -> Optional[AvatarData]:
59
+ if self._avatar_data_path is None:
60
+ return None
61
+ data = AvatarData()
62
+ data.set_value(self._avatar_data_path.read_bytes())
63
+ return data
64
+
65
+ @staticmethod
66
+ def _sha(b: bytes):
67
+ return hashlib.sha1(b).hexdigest()
68
+
69
+ def _get_weak_unique_id(self, avatar: AvatarType):
70
+ if isinstance(avatar, str):
71
+ return avatar # url
72
+ elif isinstance(avatar, bytes):
73
+ return self._sha(avatar)
74
+ elif isinstance(avatar, Path):
75
+ return self._sha(avatar.read_bytes())
39
76
 
40
- async def set_avatar(self, avatar: AvatarType):
77
+ @staticmethod
78
+ async def _get_image(avatar: AvatarType) -> PILImage:
41
79
  if isinstance(avatar, str):
42
- return await self.set_avatar_from_url(avatar)
80
+ # async with aiohttp.ClientSession() as session:
81
+ async with avatar_cache.http.get(avatar) as response:
82
+ return Image.open(io.BytesIO(await response.read()))
43
83
  elif isinstance(avatar, bytes):
44
- img = Image.open(io.BytesIO(avatar))
84
+ return Image.open(io.BytesIO(avatar))
45
85
  elif isinstance(avatar, Path):
46
- img = Image.open(avatar)
86
+ return Image.open(avatar)
47
87
  else:
48
88
  raise TypeError("Avatar must be bytes, a Path or a str (URL)", avatar)
49
89
 
50
- metadata = AvatarMetadata()
90
+ async def set_avatar(
91
+ self, avatar: AvatarType, unique_id: Optional[LegacyFileIdType] = None
92
+ ):
93
+ if unique_id is None:
94
+ if isinstance(avatar, str):
95
+ await self._set_avatar_from_url_alone(avatar)
96
+ return
97
+ unique_id = self._get_weak_unique_id(avatar)
51
98
 
52
- resampled = False
53
- if (size := config.AVATAR_SIZE) and any(x > size for x in img.size):
54
- img.thumbnail((size, size))
55
- log.debug("Resampled image to %s", img.size)
56
- resampled = True
57
-
58
- if not resampled and img.format == "PNG" and isinstance(avatar, bytes):
59
- avatar_bytes = avatar
60
- elif not resampled and img.format == "PNG" and isinstance(avatar, Path):
61
- with avatar.open("rb") as f:
62
- avatar_bytes = f.read()
99
+ await self._set_avatar_from_unique_id(avatar, unique_id)
100
+
101
+ async def _set_avatar_from_unique_id(
102
+ self, avatar: AvatarType, unique_id: LegacyFileIdType
103
+ ):
104
+ cached_avatar = avatar_cache.get(unique_id)
105
+ if cached_avatar:
106
+ # this shouldn't be necessary but here to re-use avatars downloaded
107
+ # before the change introducing the JID to unique ID mapping
108
+ avatar_cache.store_jid(self.jid, unique_id)
63
109
  else:
64
- with io.BytesIO() as f:
65
- img.save(f, format="PNG")
66
- avatar_bytes = f.getvalue()
110
+ img = await self._get_image(avatar)
111
+ cached_avatar = await avatar_cache.convert_and_store(
112
+ img, unique_id, self.jid
113
+ )
67
114
 
68
- hash_ = hashlib.sha1(avatar_bytes).hexdigest()
69
- self.id = hash_
70
- metadata.add_info(
71
- id=hash_,
72
- itype="image/png",
73
- ibytes=len(avatar_bytes),
74
- height=str(img.height),
75
- width=str(img.width),
76
- )
77
- self.metadata = metadata
115
+ self._set_avatar_from_cache(cached_avatar)
78
116
 
79
- data = AvatarData()
80
- data.set_value(avatar_bytes)
81
- self.data = data
117
+ async def _set_avatar_from_url_alone(self, url: str):
118
+ cached_avatar = await avatar_cache.get_avatar_from_url_alone(url, self.jid)
119
+ self._set_avatar_from_cache(cached_avatar)
82
120
 
83
- async def set_avatar_from_url(self, url: str):
84
- avatar = await avatar_cache.get_avatar(url)
121
+ def _set_avatar_from_cache(self, cached_avatar: CachedAvatar):
85
122
  metadata = AvatarMetadata()
86
- self.id = avatar.hash
123
+ self.id = cached_avatar.hash
87
124
  metadata.add_info(
88
- id=avatar.hash,
125
+ id=cached_avatar.hash,
89
126
  itype="image/png",
90
- ibytes=len(avatar.data),
91
- height=str(avatar.height),
92
- width=str(avatar.width),
127
+ ibytes=cached_avatar.path.stat().st_size,
128
+ height=str(cached_avatar.height),
129
+ width=str(cached_avatar.width),
93
130
  )
94
131
  self.metadata = metadata
132
+ self._avatar_data_path = cached_avatar.path
95
133
 
96
- data = AvatarData()
97
- data.set_value(avatar.data)
98
- self.data = data
134
+ @staticmethod
135
+ def from_db(jid: JID, user: Optional[GatewayUser] = None) -> Optional["PepAvatar"]:
136
+ cached_id = db.avatar_get(jid)
137
+ if cached_id is None:
138
+ return None
139
+ item = PepAvatar(jid)
140
+ cached_avatar = avatar_cache.get(cached_id)
141
+ if cached_avatar is None:
142
+ raise XMPPError("internal-server-error")
143
+ item._set_avatar_from_cache(cached_avatar)
144
+ return item
145
+
146
+ def to_db(self, jid: JID, user=None):
147
+ cached_id = avatar_cache.get_cached_id_for(jid)
148
+ if cached_id is None:
149
+ log.warning("Could not store avatar for %s", jid)
150
+ return
151
+ db.avatar_store(jid, cached_id)
152
+
153
+ @staticmethod
154
+ def remove_from_db(jid: JID):
155
+ db.avatar_delete(jid)
99
156
 
100
157
 
101
158
  class PepNick(PepItem):
102
- def __init__(
103
- self, authorized_jid: Optional[JidStr] = None, nick: Optional[str] = None
104
- ):
105
- super().__init__(authorized_jid)
159
+ def __init__(self, nick: Optional[str] = None):
106
160
  nickname = UserNick()
107
161
  if nick is not None:
108
162
  nickname["nick"] = nick
109
163
  self.nick = nickname
164
+ self.__nick_str = nick
110
165
 
166
+ @staticmethod
167
+ def from_db(jid: JID, user: Optional[GatewayUser] = None) -> Optional["PepNick"]:
168
+ if user is None:
169
+ raise XMPPError("not-allowed")
170
+ nick = db.nick_get(jid, user)
171
+ if nick is None:
172
+ return None
173
+ return PepNick(nick)
111
174
 
112
- class PubSubComponent(BasePlugin):
113
- xmpp: ComponentXMPP
175
+ def to_db(self, jid: JID, user: Optional[GatewayUser] = None):
176
+ if user is None:
177
+ raise XMPPError("not-allowed")
178
+ db.nick_store(jid, str(self.__nick_str), user)
179
+
180
+
181
+ class PubSubComponent(NamedLockMixin, BasePlugin):
182
+ xmpp: "BaseGateway"
114
183
 
115
184
  name = "pubsub"
116
185
  description = "Pubsub component"
@@ -126,8 +195,7 @@ class PubSubComponent(BasePlugin):
126
195
 
127
196
  def __init__(self, *a, **kw):
128
197
  super(PubSubComponent, self).__init__(*a, **kw)
129
- self._avatars = dict[JID, PepAvatar]()
130
- self._nicks = dict[JID, PepNick]()
198
+ register_stanza_plugin(EventItem, UserNick)
131
199
 
132
200
  def plugin_init(self):
133
201
  self.xmpp.register_handler(
@@ -151,7 +219,7 @@ class PubSubComponent(BasePlugin):
151
219
  self._get_vcard, # type:ignore
152
220
  )
153
221
  )
154
- self.xmpp.add_event_handler("got_online", self._on_got_online)
222
+ self.xmpp.add_event_handler("presence_available", self._on_presence_available)
155
223
 
156
224
  disco = self.xmpp.plugin["xep_0030"]
157
225
  disco.add_identity("pubsub", "pep", self.component_name)
@@ -160,23 +228,55 @@ class PubSubComponent(BasePlugin):
160
228
  disco.add_feature("http://jabber.org/protocol/pubsub#retrieve-items")
161
229
  disco.add_feature("http://jabber.org/protocol/pubsub#persistent-items")
162
230
 
163
- async def _on_got_online(self, p: Presence):
231
+ async def _on_presence_available(self, p: Presence):
232
+ if p.get_plugin("muc_join", check=True) is not None:
233
+ log.debug("Ignoring MUC presence here")
234
+ return
235
+
164
236
  from_ = p.get_from()
165
237
  ver_string = p["caps"]["ver"]
166
238
  info = None
239
+
240
+ to = p.get_to()
241
+
242
+ # we don't want to push anything for contacts that are not in the user's roster
243
+ if to != self.xmpp.boundjid.bare:
244
+ session = self.xmpp.get_session_from_stanza(p)
245
+
246
+ if session is None:
247
+ return
248
+
249
+ await session.contacts.ready
250
+ try:
251
+ contact = await session.contacts.by_jid(to)
252
+ except XMPPError as e:
253
+ log.debug(
254
+ "Could not determine if %s was added to the roster: %s", to, e
255
+ )
256
+ return
257
+ except Exception as e:
258
+ log.warning("Could not determine if %s was added to the roster.", to)
259
+ log.exception(e)
260
+ return
261
+ if not contact.is_friend:
262
+ return
263
+
167
264
  if ver_string:
168
- await asyncio.sleep(5)
169
265
  info = await self.xmpp.plugin["xep_0115"].get_caps(from_)
170
266
  if info is None:
171
- info = await self.xmpp.plugin["xep_0030"].get_info(from_)
267
+ async with self.lock(from_):
268
+ iq = await self.xmpp.plugin["xep_0030"].get_info(from_)
269
+ info = iq["disco_info"]
172
270
  features = info["features"]
173
271
  if AvatarMetadata.namespace + "+notify" in features:
174
272
  try:
175
- pep_avatar = self._get_authorized_avatar(p)
273
+ pep_avatar = await self._get_authorized_avatar(p)
176
274
  except XMPPError:
177
275
  pass
178
276
  else:
179
- self._broadcast(
277
+ if pep_avatar.metadata is None:
278
+ raise XMPPError("internal-server-error", "Avatar but no metadata?")
279
+ await self._broadcast(
180
280
  data=pep_avatar.metadata,
181
281
  from_=p.get_to(),
182
282
  to=from_,
@@ -184,26 +284,26 @@ class PubSubComponent(BasePlugin):
184
284
  )
185
285
  if UserNick.namespace + "+notify" in features:
186
286
  try:
187
- pep_nick = self._get_authorized_nick(p)
287
+ pep_nick = await self._get_authorized_nick(p)
188
288
  except XMPPError:
189
289
  pass
190
290
  else:
191
- self._broadcast(data=pep_nick.nick, from_=p.get_to(), to=from_)
291
+ await self._broadcast(data=pep_nick.nick, from_=p.get_to(), to=from_)
192
292
 
193
293
  if VCARD4_NAMESPACE + "+notify" in features:
194
- self.broadcast_vcard_event(p.get_to(), to=from_)
294
+ await self.broadcast_vcard_event(p.get_to(), to=from_)
195
295
 
196
- def broadcast_vcard_event(self, from_, to):
296
+ async def broadcast_vcard_event(self, from_, to):
197
297
  item = Item()
198
298
  item.namespace = VCARD4_NAMESPACE
199
299
  item["id"] = "current"
200
- vcard: VCard4 = self.xmpp["xep_0292_provider"].get_vcard(from_, to)
300
+ vcard: VCard4 = await self.xmpp["xep_0292_provider"].get_vcard(from_, to)
201
301
  # The vcard content should NOT be in this event according to the spec:
202
302
  # https://xmpp.org/extensions/xep-0292.html#sect-idm45669698174224
203
303
  # but movim expects it to be here, and I guess
204
304
 
205
305
  log.debug("Broadcast vcard4 event: %s", vcard)
206
- self._broadcast(
306
+ await self._broadcast(
207
307
  data=vcard,
208
308
  from_=JID(from_).bare,
209
309
  to=to,
@@ -211,28 +311,29 @@ class PubSubComponent(BasePlugin):
211
311
  node=VCARD4_NAMESPACE,
212
312
  )
213
313
 
214
- @staticmethod
215
- def _get_authorized_item(
216
- store: dict[JID, PepItemType], stanza: Union[Iq, Presence]
314
+ async def _get_authorized_item(
315
+ self, cls: Type[PepItemType], stanza: Union[Iq, Presence]
217
316
  ) -> PepItemType:
218
- item = store.get(stanza.get_to())
317
+ sto = stanza.get_to()
318
+ user = user_store.get_by_jid(stanza.get_from())
319
+ item = cls.from_db(sto, user)
219
320
  if item is None:
220
321
  raise XMPPError("item-not-found")
221
322
 
222
- if item.authorized_jid is not None:
223
- if stanza.get_from().bare != item.authorized_jid:
224
- raise XMPPError("item-not-found")
323
+ if sto != self.xmpp.boundjid.bare:
324
+ session = self.xmpp.get_session_from_stanza(stanza)
325
+ await session.contacts.by_jid(sto)
225
326
 
226
- return item
327
+ return item # type:ignore
227
328
 
228
- def _get_authorized_avatar(self, stanza: Union[Iq, Presence]):
229
- return self._get_authorized_item(self._avatars, stanza)
329
+ async def _get_authorized_avatar(self, stanza: Union[Iq, Presence]) -> PepAvatar:
330
+ return await self._get_authorized_item(PepAvatar, stanza)
230
331
 
231
- def _get_authorized_nick(self, stanza: Union[Iq, Presence]):
232
- return self._get_authorized_item(self._nicks, stanza)
332
+ async def _get_authorized_nick(self, stanza: Union[Iq, Presence]) -> PepNick:
333
+ return await self._get_authorized_item(PepNick, stanza)
233
334
 
234
335
  async def _get_avatar_data(self, iq: Iq):
235
- pep_avatar = self._get_authorized_avatar(iq)
336
+ pep_avatar = await self._get_authorized_avatar(iq)
236
337
 
237
338
  requested_items = iq["pubsub"]["items"]
238
339
  if len(requested_items) == 0:
@@ -246,7 +347,7 @@ class PubSubComponent(BasePlugin):
246
347
  raise XMPPError("item-not-found")
247
348
 
248
349
  async def _get_avatar_metadata(self, iq: Iq):
249
- pep_avatar = self._get_authorized_avatar(iq)
350
+ pep_avatar = await self._get_authorized_avatar(iq)
250
351
 
251
352
  requested_items = iq["pubsub"]["items"]
252
353
  if len(requested_items) == 0:
@@ -263,7 +364,7 @@ class PubSubComponent(BasePlugin):
263
364
  # this is not the proper way that clients should retrieve VCards, but
264
365
  # gajim does it this way.
265
366
  # https://xmpp.org/extensions/xep-0292.html#sect-idm45669698174224
266
- vcard: VCard4 = self.xmpp["xep_0292_provider"].get_vcard(
367
+ vcard: VCard4 = await self.xmpp["xep_0292_provider"].get_vcard(
267
368
  iq.get_to().bare, iq.get_from().bare
268
369
  )
269
370
  log.debug("VCARD: %s -- %s -- %s", iq.get_to().bare, iq.get_from().bare, vcard)
@@ -271,25 +372,43 @@ class PubSubComponent(BasePlugin):
271
372
  raise XMPPError("item-not-found")
272
373
  self._reply_with_payload(iq, vcard, "current", VCARD4_NAMESPACE)
273
374
 
375
+ @staticmethod
376
+ def get_avatar(jid: JID):
377
+ return PepAvatar.from_db(jid)
378
+
274
379
  @staticmethod
275
380
  def _reply_with_payload(
276
381
  iq: Iq,
277
- payload: Union[AvatarMetadata, AvatarData, VCard4],
278
- id_: str,
382
+ payload: Optional[Union[AvatarMetadata, AvatarData, VCard4]],
383
+ id_: Optional[str],
279
384
  namespace: Optional[str] = None,
280
385
  ):
281
386
  result = iq.reply()
282
387
  item = Item()
283
388
  if payload:
284
389
  item.set_payload(payload.xml)
285
- item["id"] = id_
286
- result["pubsub"]["items"]["node"] = (
287
- namespace if namespace else payload.namespace
288
- )
390
+ item["id"] = id_
391
+ result["pubsub"]["items"]["node"] = (
392
+ namespace if namespace else payload.namespace
393
+ )
289
394
  result["pubsub"]["items"].append(item)
290
395
  result.send()
291
396
 
292
- def _broadcast(self, data, from_: JidStr, to: OptJidStr = None, **kwargs):
397
+ async def _broadcast(self, data, from_: JidStr, to: OptJidStr = None, **kwargs):
398
+ from_ = JID(from_)
399
+ if from_ != self.xmpp.boundjid.bare and to is not None:
400
+ to = JID(to)
401
+ session = self.xmpp.get_session_from_jid(to)
402
+ if session is None:
403
+ return
404
+ await session.ready
405
+ try:
406
+ entity = await session.get_contact_or_group_or_participant(from_)
407
+ except ContactIsUser:
408
+ return
409
+ if isinstance(entity, LegacyContact) and not entity.is_friend:
410
+ return
411
+
293
412
  item = EventItem()
294
413
  if data:
295
414
  item.set_payload(data.xml)
@@ -321,45 +440,85 @@ class PubSubComponent(BasePlugin):
321
440
  self,
322
441
  jid: JidStr,
323
442
  avatar: Optional[AvatarType] = None,
324
- restrict_to: OptJidStr = None,
443
+ broadcast_to: OptJidStr = None,
444
+ unique_id=None,
445
+ broadcast=True,
325
446
  ):
326
447
  jid = JID(jid)
327
448
  if avatar is None:
328
- try:
329
- del self._avatars[jid]
330
- except KeyError:
331
- pass
332
- self._broadcast(AvatarMetadata(), jid, restrict_to)
449
+ PepAvatar.remove_from_db(jid)
450
+ await self._broadcast(AvatarMetadata(), jid, broadcast_to)
451
+ avatar_cache.delete_jid(jid)
333
452
  else:
334
- pep_avatar = PepAvatar()
453
+ pep_avatar = PepAvatar(jid)
335
454
  try:
336
- await pep_avatar.set_avatar(avatar)
337
- except UnidentifiedImageError as e:
455
+ await pep_avatar.set_avatar(avatar, unique_id)
456
+ except (UnidentifiedImageError, FileNotFoundError) as e:
338
457
  log.warning("Failed to set avatar for %s: %r", self, e)
339
458
  return
340
-
341
- pep_avatar.authorized_jid = restrict_to
342
- self._avatars[jid] = pep_avatar
459
+ pep_avatar.to_db(jid)
343
460
  if pep_avatar.metadata is None:
344
461
  raise RuntimeError
345
- self._broadcast(
462
+ if not broadcast:
463
+ return
464
+ await self._broadcast(
346
465
  pep_avatar.metadata,
347
466
  jid,
348
- restrict_to,
467
+ broadcast_to,
349
468
  id=pep_avatar.metadata["info"]["id"],
350
469
  )
351
470
 
471
+ async def set_avatar_from_cache(
472
+ self, jid: JID, send_empty: bool, broadcast_to: OptJidStr = None, broadcast=True
473
+ ):
474
+ uid = avatar_cache.get_cached_id_for(jid)
475
+ if uid is None:
476
+ if not send_empty:
477
+ return
478
+ self.xmpp.loop.create_task(
479
+ self.set_avatar(jid, None, broadcast_to, uid, broadcast)
480
+ )
481
+ return
482
+ cached_avatar = avatar_cache.get(str(uid))
483
+ if cached_avatar is None:
484
+ # should not happen but well…
485
+ log.warning(
486
+ "Something is wrong with the avatar, %s won't have an "
487
+ "avatar because avatar not found in cache",
488
+ jid,
489
+ )
490
+ return
491
+ self.xmpp.loop.create_task(
492
+ self.set_avatar(jid, cached_avatar.path, broadcast_to, uid, broadcast)
493
+ )
494
+
352
495
  def set_nick(
353
496
  self,
497
+ user: GatewayUser,
354
498
  jid: JidStr,
355
499
  nick: Optional[str] = None,
356
- restrict_to: OptJidStr = None,
357
500
  ):
358
501
  jid = JID(jid)
359
- nickname = PepNick(restrict_to, nick)
360
- self._nicks[jid] = nickname
361
- log.debug("NICK: %s", nickname.nick)
362
- self._broadcast(nickname.nick, jid, restrict_to)
502
+ nickname = PepNick(nick)
503
+ nickname.to_db(jid, user)
504
+ log.debug("New nickname: %s", nickname.nick)
505
+ self.xmpp.loop.create_task(self._broadcast(nickname.nick, jid, user.bare_jid))
506
+
507
+ async def broadcast_all(self, from_: JID, to: JID):
508
+ """
509
+ Force push avatar and nick for a stored JID.
510
+ """
511
+ a = PepAvatar.from_db(from_)
512
+ if a:
513
+ if a.metadata:
514
+ await self._broadcast(
515
+ a.metadata, from_, to, id=a.metadata["info"]["id"]
516
+ )
517
+ else:
518
+ log.warning("No metadata associated to this cached avatar?!")
519
+ n = PepNick.from_db(from_, user_store.get_by_jid(to))
520
+ if n:
521
+ await self._broadcast(n.nick, from_, to)
363
522
 
364
523
 
365
524
  log = logging.getLogger(__name__)