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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. slidge/__init__.py +3 -5
  2. slidge/__main__.py +2 -196
  3. slidge/__version__.py +5 -0
  4. slidge/command/adhoc.py +8 -1
  5. slidge/command/admin.py +5 -6
  6. slidge/command/base.py +1 -2
  7. slidge/command/register.py +32 -16
  8. slidge/command/user.py +85 -5
  9. slidge/contact/contact.py +93 -31
  10. slidge/contact/roster.py +54 -39
  11. slidge/core/config.py +13 -7
  12. slidge/core/gateway/base.py +139 -34
  13. slidge/core/gateway/disco.py +2 -4
  14. slidge/core/gateway/mam.py +1 -4
  15. slidge/core/gateway/ping.py +2 -3
  16. slidge/core/gateway/presence.py +1 -1
  17. slidge/core/gateway/registration.py +32 -21
  18. slidge/core/gateway/search.py +3 -5
  19. slidge/core/gateway/session_dispatcher.py +109 -51
  20. slidge/core/gateway/vcard_temp.py +6 -4
  21. slidge/core/mixins/__init__.py +11 -1
  22. slidge/core/mixins/attachment.py +15 -10
  23. slidge/core/mixins/avatar.py +66 -18
  24. slidge/core/mixins/base.py +8 -2
  25. slidge/core/mixins/message.py +11 -7
  26. slidge/core/mixins/message_maker.py +17 -9
  27. slidge/core/mixins/presence.py +14 -4
  28. slidge/core/pubsub.py +54 -212
  29. slidge/core/session.py +65 -33
  30. slidge/db/__init__.py +4 -0
  31. slidge/db/alembic/env.py +64 -0
  32. slidge/db/alembic/script.py.mako +26 -0
  33. slidge/db/alembic/versions/09f27f098baa_add_missing_attributes_in_room.py +36 -0
  34. slidge/db/alembic/versions/29f5280c61aa_store_subject_setter_in_room.py +37 -0
  35. slidge/db/alembic/versions/8d2ced764698_rely_on_db_to_store_contacts_rooms_and_.py +133 -0
  36. slidge/db/alembic/versions/aa9d82a7f6ef_db_creation.py +76 -0
  37. slidge/db/alembic/versions/b33993e87db3_move_everything_to_persistent_db.py +214 -0
  38. slidge/db/alembic/versions/e91195719c2c_store_users_avatars_persistently.py +26 -0
  39. slidge/db/avatar.py +224 -0
  40. slidge/db/meta.py +65 -0
  41. slidge/db/models.py +365 -0
  42. slidge/db/store.py +976 -0
  43. slidge/group/archive.py +13 -14
  44. slidge/group/bookmarks.py +59 -56
  45. slidge/group/participant.py +81 -29
  46. slidge/group/room.py +242 -142
  47. slidge/main.py +201 -0
  48. slidge/migration.py +30 -0
  49. slidge/slixfix/__init__.py +35 -2
  50. slidge/slixfix/roster.py +11 -4
  51. slidge/slixfix/xep_0292/vcard4.py +1 -0
  52. slidge/util/db.py +1 -47
  53. slidge/util/test.py +21 -4
  54. slidge/util/types.py +24 -4
  55. {slidge-0.1.2.dist-info → slidge-0.2.0a0.dist-info}/METADATA +3 -1
  56. slidge-0.2.0a0.dist-info/RECORD +108 -0
  57. slidge/core/cache.py +0 -183
  58. slidge/util/schema.sql +0 -126
  59. slidge/util/sql.py +0 -508
  60. slidge-0.1.2.dist-info/RECORD +0 -96
  61. {slidge-0.1.2.dist-info → slidge-0.2.0a0.dist-info}/LICENSE +0 -0
  62. {slidge-0.1.2.dist-info → slidge-0.2.0a0.dist-info}/WHEEL +0 -0
  63. {slidge-0.1.2.dist-info → slidge-0.2.0a0.dist-info}/entry_points.txt +0 -0
slidge/contact/contact.py CHANGED
@@ -2,7 +2,7 @@ import datetime
2
2
  import logging
3
3
  import warnings
4
4
  from datetime import date
5
- from typing import TYPE_CHECKING, Generic, Iterable, Optional, Union
5
+ from typing import TYPE_CHECKING, Generic, Iterable, Optional, Self, Union
6
6
 
7
7
  from slixmpp import JID, Message, Presence
8
8
  from slixmpp.exceptions import IqError
@@ -10,10 +10,10 @@ from slixmpp.plugins.xep_0292.stanza import VCard4
10
10
  from slixmpp.types import MessageTypes
11
11
 
12
12
  from ..core import config
13
- from ..core.mixins import FullCarbonMixin
14
- from ..core.mixins.avatar import AvatarMixin
13
+ from ..core.mixins import AvatarMixin, FullCarbonMixin, StoredAttributeMixin
15
14
  from ..core.mixins.disco import ContactAccountDiscoMixin
16
15
  from ..core.mixins.recipient import ReactionRecipientMixin, ThreadRecipientMixin
16
+ from ..db.models import Contact
17
17
  from ..util import SubclassableOnce
18
18
  from ..util.types import LegacyUserIdType, MessageOrPresenceTypeVar
19
19
 
@@ -24,6 +24,7 @@ if TYPE_CHECKING:
24
24
 
25
25
  class LegacyContact(
26
26
  Generic[LegacyUserIdType],
27
+ StoredAttributeMixin,
27
28
  AvatarMixin,
28
29
  ContactAccountDiscoMixin,
29
30
  FullCarbonMixin,
@@ -100,7 +101,6 @@ class LegacyContact(
100
101
  """
101
102
  super().__init__()
102
103
  self.session = session
103
- self.user = session.user
104
104
  self.legacy_id: LegacyUserIdType = legacy_id
105
105
  """
106
106
  The legacy identifier of the :term:`Legacy Contact`.
@@ -116,16 +116,40 @@ class LegacyContact(
116
116
 
117
117
  self._name: Optional[str] = None
118
118
 
119
- if self.xmpp.MARK_ALL_MESSAGES:
120
- self._sent_order = list[str]()
121
-
122
119
  self.xmpp = session.xmpp
123
120
  self.jid = JID(self.jid_username + "@" + self.xmpp.boundjid.bare)
124
121
  self.jid.resource = self.RESOURCE
125
- self.log = logging.getLogger(f"{self.user.bare_jid}:{self.jid.bare}")
126
- self.participants = set["LegacyParticipant"]()
127
- self.is_friend: bool = False
128
- self.__added_to_roster = False
122
+ self.log = logging.getLogger(f"{self.user_jid.bare}:{self.jid.bare}")
123
+ self._is_friend: bool = False
124
+ self.added_to_roster = False
125
+
126
+ @property
127
+ def is_friend(self):
128
+ return self._is_friend
129
+
130
+ @is_friend.setter
131
+ def is_friend(self, value: bool):
132
+ if value == self._is_friend:
133
+ return
134
+ self._is_friend = value
135
+ assert self.contact_pk is not None
136
+ self.xmpp.store.contacts.set_friend(self.contact_pk, value)
137
+
138
+ @property
139
+ def participants(self) -> list["LegacyParticipant"]:
140
+ assert self.contact_pk is not None
141
+ from ..group.participant import LegacyParticipant
142
+
143
+ return [
144
+ LegacyParticipant.get_self_or_unique_subclass().from_store(
145
+ self.session, stored, contact=self
146
+ )
147
+ for stored in self.xmpp.store.participants.get_for_contact(self.contact_pk)
148
+ ]
149
+
150
+ @property
151
+ def user_jid(self):
152
+ return self.session.user_jid
129
153
 
130
154
  def __repr__(self):
131
155
  return f"<Contact '{self.legacy_id}'/'{self.jid.bare}'>"
@@ -170,7 +194,7 @@ class LegacyContact(
170
194
  ) -> MessageOrPresenceTypeVar:
171
195
  if carbon and isinstance(stanza, Message):
172
196
  stanza["to"] = self.jid.bare
173
- stanza["from"] = self.user.jid
197
+ stanza["from"] = self.user_jid
174
198
  self._privileged_send(stanza)
175
199
  return stanza # type:ignore
176
200
 
@@ -186,12 +210,13 @@ class LegacyContact(
186
210
  n["nick"] = self.name
187
211
  stanza.append(n)
188
212
  if self.xmpp.MARK_ALL_MESSAGES and is_markable(stanza):
189
- self._sent_order.append(stanza["id"])
190
- stanza["to"] = self.user.jid
213
+ assert self.contact_pk is not None
214
+ self.xmpp.store.contacts.add_to_sent(self.contact_pk, stanza["id"])
215
+ stanza["to"] = self.user_jid
191
216
  stanza.send()
192
217
  return stanza
193
218
 
194
- def get_msg_xmpp_id_up_to(self, horizon_xmpp_id: str):
219
+ def get_msg_xmpp_id_up_to(self, horizon_xmpp_id: str) -> list[str]:
195
220
  """
196
221
  Return XMPP msg ids sent by this contact up to a given XMPP msg id.
197
222
 
@@ -205,15 +230,8 @@ class LegacyContact(
205
230
  :param horizon_xmpp_id: The latest message
206
231
  :return: A list of XMPP ids or None if horizon_xmpp_id was not found
207
232
  """
208
- for i, xmpp_id in enumerate(self._sent_order):
209
- if xmpp_id == horizon_xmpp_id:
210
- break
211
- else:
212
- return
213
- i += 1
214
- res = self._sent_order[:i]
215
- self._sent_order = self._sent_order[i:]
216
- return res
233
+ assert self.contact_pk is not None
234
+ return self.xmpp.store.contacts.pop_sent_up_to(self.contact_pk, horizon_xmpp_id)
217
235
 
218
236
  @property
219
237
  def name(self):
@@ -229,9 +247,19 @@ class LegacyContact(
229
247
  for p in self.participants:
230
248
  p.nickname = n
231
249
  self._name = n
232
- self.xmpp.pubsub.set_nick(user=self.user, jid=self.jid.bare, nick=n)
250
+ assert self.contact_pk is not None
251
+ self.xmpp.store.contacts.update_nick(self.contact_pk, n)
252
+ self.xmpp.pubsub.broadcast_nick(
253
+ user_jid=self.user_jid, jid=self.jid.bare, nick=n
254
+ )
255
+
256
+ def _get_cached_avatar_id(self):
257
+ assert self.contact_pk is not None
258
+ return self.xmpp.store.contacts.get_avatar_legacy_id(self.contact_pk)
233
259
 
234
260
  def _post_avatar_update(self):
261
+ assert self.contact_pk is not None
262
+ self.xmpp.store.contacts.set_avatar(self.contact_pk, self._avatar_pk)
235
263
  for p in self.participants:
236
264
  self.log.debug("Propagating new avatar to %s", p.muc)
237
265
  p.send_last_presence(force=True, no_cache_online=True)
@@ -283,7 +311,7 @@ class LegacyContact(
283
311
  elif country:
284
312
  vcard.add_address(country, locality)
285
313
 
286
- self.xmpp.vcard.set_vcard(self.jid.bare, vcard, {self.user.jid.bare})
314
+ self.xmpp.vcard.set_vcard(self.jid.bare, vcard, {self.user_jid.bare})
287
315
 
288
316
  async def add_to_roster(self, force=False):
289
317
  """
@@ -291,7 +319,7 @@ class LegacyContact(
291
319
 
292
320
  :param force: add even if the contact was already added successfully
293
321
  """
294
- if self.__added_to_roster and not force:
322
+ if self.added_to_roster and not force:
295
323
  return
296
324
  if config.NO_ROSTER_PUSH:
297
325
  log.debug("Roster push request by plugin ignored (--no-roster-push)")
@@ -303,7 +331,7 @@ class LegacyContact(
303
331
  if (n := self.name) is not None:
304
332
  item["name"] = n
305
333
  kw = dict(
306
- jid=self.user.jid,
334
+ jid=self.user_jid,
307
335
  roster_items={self.jid.bare: item},
308
336
  )
309
337
  try:
@@ -325,12 +353,26 @@ class LegacyContact(
325
353
  else:
326
354
  # we only broadcast pubsub events for contacts added to the roster
327
355
  # so if something was set before, we need to push it now
328
- self.__added_to_roster = True
356
+ self.added_to_roster = True
329
357
  self.session.create_task(self.__broadcast_pubsub_items())
330
358
  self.send_last_presence()
331
359
 
332
360
  async def __broadcast_pubsub_items(self):
333
- await self.xmpp.pubsub.broadcast_all(JID(self.jid.bare), self.user.jid)
361
+ cached_avatar = self.get_cached_avatar()
362
+ if cached_avatar is not None:
363
+ await self.xmpp.pubsub.broadcast_avatar(
364
+ self.jid.bare, self.session.user_jid, cached_avatar
365
+ )
366
+ nick = self.name
367
+ from ..core.pubsub import PepNick
368
+
369
+ if nick is not None:
370
+ pep_nick = PepNick(nick)
371
+ await self.xmpp.pubsub.broadcast(
372
+ pep_nick.nick,
373
+ self.jid.bare,
374
+ self.session.user_jid,
375
+ )
334
376
 
335
377
  async def _set_roster(self, **kw):
336
378
  try:
@@ -350,6 +392,8 @@ class LegacyContact(
350
392
  :param text: Optional message from the friend to the user
351
393
  """
352
394
  self.is_friend = True
395
+ assert self.contact_pk is not None
396
+ self.xmpp.store.contacts.set_friend(self.contact_pk, True)
353
397
  self.log.debug("Accepting friend request")
354
398
  presence = self._make_presence(ptype="subscribed", pstatus=text, bare=True)
355
399
  self._send(presence, nick=True)
@@ -415,7 +459,7 @@ class LegacyContact(
415
459
  their 'friends'".
416
460
  """
417
461
  for ptype in "unsubscribe", "unsubscribed", "unavailable":
418
- self.xmpp.send_presence(pfrom=self.jid, pto=self.user.jid.bare, ptype=ptype) # type: ignore
462
+ self.xmpp.send_presence(pfrom=self.jid, pto=self.user_jid.bare, ptype=ptype) # type: ignore
419
463
 
420
464
  async def update_info(self):
421
465
  """
@@ -442,6 +486,24 @@ class LegacyContact(
442
486
  """
443
487
  pass
444
488
 
489
+ @classmethod
490
+ def from_store(cls, session, stored: Contact, *args, **kwargs) -> Self:
491
+ contact = cls(
492
+ session,
493
+ cls.xmpp.LEGACY_CONTACT_ID_TYPE(stored.legacy_id),
494
+ stored.jid.username, # type: ignore
495
+ *args, # type: ignore
496
+ **kwargs, # type: ignore
497
+ )
498
+ contact.contact_pk = stored.id
499
+ contact._name = stored.nick
500
+ contact._is_friend = stored.is_friend
501
+ contact.added_to_roster = stored.added_to_roster
502
+ if (data := stored.extra_attributes) is not None:
503
+ contact.deserialize_extra_attributes(data)
504
+ contact._set_avatar_from_store(stored)
505
+ return contact
506
+
445
507
 
446
508
  def is_markable(stanza: Union[Message, Presence]):
447
509
  if isinstance(stanza, Presence):
slidge/contact/roster.py CHANGED
@@ -1,11 +1,12 @@
1
1
  import asyncio
2
2
  import logging
3
- from typing import TYPE_CHECKING, Generic, Optional, Type
3
+ from typing import TYPE_CHECKING, Generic, Iterator, Optional, Type
4
4
 
5
5
  from slixmpp import JID
6
6
  from slixmpp.jid import JID_UNESCAPE_TRANSFORMATIONS, _unescape_node
7
7
 
8
8
  from ..core.mixins.lock import NamedLockMixin
9
+ from ..db.store import ContactStore
9
10
  from ..util import SubclassableOnce
10
11
  from ..util.types import LegacyContactType, LegacyUserIdType
11
12
  from .contact import LegacyContact
@@ -43,38 +44,45 @@ class LegacyRoster(
43
44
  LegacyContact.get_self_or_unique_subclass()
44
45
  )
45
46
  self._contact_cls.xmpp = session.xmpp
47
+ self.__store: ContactStore = session.xmpp.store.contacts
46
48
 
47
49
  self.session = session
48
- self._contacts_by_bare_jid: dict[str, LegacyContactType] = {}
49
- self._contacts_by_legacy_id: dict[LegacyUserIdType, LegacyContactType] = {}
50
- self.log = logging.getLogger(f"{self.session.user.bare_jid}:roster")
50
+ self.log = logging.getLogger(f"{self.session.user_jid.bare}:roster")
51
51
  self.user_legacy_id: Optional[LegacyUserIdType] = None
52
52
  self.ready: asyncio.Future[bool] = self.session.xmpp.loop.create_future()
53
53
  super().__init__()
54
54
 
55
55
  def __repr__(self):
56
- return f"<Roster of {self.session.user}>"
56
+ return f"<Roster of {self.session.user_jid}>"
57
57
 
58
- def __iter__(self):
59
- return iter(self._contacts_by_legacy_id.values())
58
+ def __iter__(self) -> Iterator[LegacyContactType]:
59
+ with self.__store.session():
60
+ for stored in self.__store.get_all(user_pk=self.session.user_pk):
61
+ yield self._contact_cls.from_store(self.session, stored)
60
62
 
61
63
  async def __finish_init_contact(
62
64
  self, legacy_id: LegacyUserIdType, jid_username: str, *args, **kwargs
63
65
  ):
64
66
  c = self._contact_cls(self.session, legacy_id, jid_username, *args, **kwargs)
65
- async with self.lock(("finish", c)):
66
- if legacy_id in self._contacts_by_legacy_id:
67
- self.log.debug("Already updated %s", c)
68
- return c
69
- await c.avatar_wrap_update_info()
70
- self._contacts_by_legacy_id[legacy_id] = c
71
- self._contacts_by_bare_jid[c.jid.bare] = c
67
+ async with self.lock(("finish", c.legacy_id)):
68
+ with self.__store.session():
69
+ stored = self.__store.get_by_legacy_id(
70
+ self.session.user_pk, str(legacy_id)
71
+ )
72
+ if stored is not None and stored.updated:
73
+ self.log.debug("Already updated %s", c)
74
+ return self._contact_cls.from_store(self.session, stored)
75
+ c.contact_pk = self.__store.add(
76
+ self.session.user_pk, c.legacy_id, c.jid
77
+ )
78
+ await c.avatar_wrap_update_info()
79
+ self.__store.update(c)
72
80
  return c
73
81
 
74
82
  def known_contacts(self, only_friends=True) -> dict[str, LegacyContactType]:
75
83
  if only_friends:
76
- return {j: c for j, c in self._contacts_by_bare_jid.items() if c.is_friend}
77
- return self._contacts_by_bare_jid
84
+ return {j: c for j, c in self if c.is_friend} # type:ignore
85
+ return {c.jid.bare: c for c in self}
78
86
 
79
87
  async def by_jid(self, contact_jid: JID) -> LegacyContactType:
80
88
  # """
@@ -89,16 +97,17 @@ class LegacyRoster(
89
97
  # """
90
98
  username = contact_jid.node
91
99
  async with self.lock(("username", username)):
92
- bare = contact_jid.bare
93
- c = self._contacts_by_bare_jid.get(bare)
94
- if c is None:
95
- legacy_id = await self.jid_username_to_legacy_id(username)
96
- log.debug("Contact %s not found", contact_jid)
97
- if self.get_lock(("legacy_id", legacy_id)):
98
- log.debug("Already updating %s", contact_jid)
99
- return await self.by_legacy_id(legacy_id)
100
- c = await self.__finish_init_contact(legacy_id, username)
101
- return c
100
+ with self.__store.session():
101
+ stored = self.__store.get_by_jid(self.session.user_pk, contact_jid)
102
+ if stored is not None and stored.updated:
103
+ return self._contact_cls.from_store(self.session, stored)
104
+
105
+ legacy_id = await self.jid_username_to_legacy_id(username)
106
+ log.debug("Contact %s not found", contact_jid)
107
+ if self.get_lock(("legacy_id", legacy_id)):
108
+ log.debug("Already updating %s", contact_jid)
109
+ return await self.by_legacy_id(legacy_id)
110
+ return await self.__finish_init_contact(legacy_id, username)
102
111
 
103
112
  async def by_legacy_id(
104
113
  self, legacy_id: LegacyUserIdType, *args, **kwargs
@@ -121,20 +130,26 @@ class LegacyRoster(
121
130
  if legacy_id == self.user_legacy_id:
122
131
  raise ContactIsUser
123
132
  async with self.lock(("legacy_id", legacy_id)):
124
- c = self._contacts_by_legacy_id.get(legacy_id)
125
- if c is None:
126
- username = await self.legacy_id_to_jid_username(legacy_id)
127
- log.debug("Contact %s not found", legacy_id)
128
- if self.get_lock(("username", username)):
129
- log.debug("Already updating %s", username)
130
- jid = JID()
131
- jid.node = username
132
- jid.domain = self.session.xmpp.boundjid.bare
133
- return await self.by_jid(jid)
134
- c = await self.__finish_init_contact(
135
- legacy_id, username, *args, **kwargs
133
+ with self.__store.session():
134
+ stored = self.__store.get_by_legacy_id(
135
+ self.session.user_pk, str(legacy_id)
136
136
  )
137
- return c
137
+ if stored is not None and stored.updated:
138
+ return self._contact_cls.from_store(
139
+ self.session, stored, *args, **kwargs
140
+ )
141
+
142
+ username = await self.legacy_id_to_jid_username(legacy_id)
143
+ log.debug("Contact %s not found", legacy_id)
144
+ if self.get_lock(("username", username)):
145
+ log.debug("Already updating %s", username)
146
+ jid = JID()
147
+ jid.node = username
148
+ jid.domain = self.session.xmpp.boundjid.bare
149
+ return await self.by_jid(jid)
150
+ return await self.__finish_init_contact(
151
+ legacy_id, username, *args, **kwargs
152
+ )
138
153
 
139
154
  async def by_stanza(self, s) -> LegacyContact:
140
155
  # """
slidge/core/config.py CHANGED
@@ -43,11 +43,18 @@ PORT__SHORT = "p"
43
43
 
44
44
  HOME_DIR: Path
45
45
  HOME_DIR__DOC = (
46
- "Shelve file used to store persistent user data. "
46
+ "Directory where slidge will writes it persistent data and cache. "
47
47
  "Defaults to /var/lib/slidge/${SLIDGE_JID}. "
48
48
  )
49
49
  HOME_DIR__DYNAMIC_DEFAULT = True
50
50
 
51
+ DB_URL: str
52
+ DB_URL__DOC = (
53
+ "Database URL, see <https://docs.sqlalchemy.org/en/20/core/engines.html#database-urls>. "
54
+ "Defaults to sqlite:///${HOME_DIR}/slidge.sqlite"
55
+ )
56
+ DB_URL__DYNAMIC_DEFAULT = True
57
+
51
58
  USER_JID_VALIDATOR: str
52
59
  USER_JID_VALIDATOR__DOC = (
53
60
  "Regular expression to restrict users that can register to the gateway, by JID. "
@@ -70,7 +77,7 @@ UPLOAD_SERVICE__DOC = (
70
77
  )
71
78
 
72
79
  SECRET_KEY: Optional[str] = None
73
- SECRET_KEY__DOC = "Encryption for disk storage"
80
+ SECRET_KEY__DOC = "Encryption for disk storage. Deprecated."
74
81
 
75
82
  NO_ROSTER_PUSH = False
76
83
  NO_ROSTER_PUSH__DOC = "Do not fill users' rosters with legacy contacts automatically"
@@ -135,11 +142,13 @@ PARTIAL_REGISTRATION_TIMEOUT__DOC = (
135
142
  "a single step registration process is not enough."
136
143
  )
137
144
 
138
- LAST_SEEN_FALLBACK = True
145
+ LAST_SEEN_FALLBACK = False
139
146
  LAST_SEEN_FALLBACK__DOC = (
140
147
  "When using XEP-0319 (Last User Interaction in Presence), use the presence status"
141
148
  " to display the last seen information in the presence status. Useful for clients"
142
- " that do not implement XEP-0319."
149
+ " that do not implement XEP-0319. Because of implementation details, this can increase"
150
+ " RAM usage and might be deprecated in the future. Ask your client dev for XEP-0319"
151
+ " support ;o)."
143
152
  )
144
153
 
145
154
  QR_TIMEOUT = 60
@@ -211,6 +220,3 @@ DEV_MODE__DOC = (
211
220
  "Enables an interactive python shell via chat commands, for admins."
212
221
  "Not safe to use in prod, but great during dev."
213
222
  )
214
-
215
- SYNC_AVATAR = True
216
- SYNC_AVATAR__DOC = "Sync the user XMPP avatar to legacy network (if supported)."