slidge 0.2.0a2__py3-none-any.whl → 0.2.0a4__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.
slidge/__version__.py CHANGED
@@ -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.0alpha2"
5
+ __version__ = "0.2.0alpha4"
slidge/command/admin.py CHANGED
@@ -1,5 +1,6 @@
1
1
  # Commands only accessible for slidge admins
2
2
  import functools
3
+ import importlib
3
4
  import logging
4
5
  from datetime import datetime
5
6
  from typing import Any, Optional
@@ -7,6 +8,7 @@ from typing import Any, Optional
7
8
  from slixmpp import JID
8
9
  from slixmpp.exceptions import XMPPError
9
10
 
11
+ from ..core import config
10
12
  from ..util.types import AnyBaseSession
11
13
  from .base import (
12
14
  Command,
@@ -90,8 +92,12 @@ class SlidgeInfo(AdminCommand):
90
92
  [a for a in (days_ago, hours_ago, minutes_ago, seconds_ago) if a]
91
93
  )
92
94
 
95
+ legacy_module = importlib.import_module(config.LEGACY_MODULE)
96
+ version = getattr(legacy_module, "__version__", "No version")
97
+
93
98
  return (
94
- f"{self.xmpp.COMPONENT_NAME} version {__version__}\n"
99
+ f"{self.xmpp.COMPONENT_NAME} (slidge core {__version__},"
100
+ f" {config.LEGACY_MODULE} {version})\n"
95
101
  f"Up since {start:%Y-%m-%d %H:%M} ({ago} ago)"
96
102
  )
97
103
 
slidge/contact/contact.py CHANGED
@@ -3,6 +3,7 @@ import logging
3
3
  import warnings
4
4
  from datetime import date
5
5
  from typing import TYPE_CHECKING, Generic, Iterable, Optional, Self, Union
6
+ from xml.etree import ElementTree as ET
6
7
 
7
8
  from slixmpp import JID, Message, Presence
8
9
  from slixmpp.exceptions import IqError
@@ -125,6 +126,16 @@ class LegacyContact(
125
126
  self._is_friend: bool = False
126
127
  self._added_to_roster = False
127
128
  self._caps_ver: str | None = None
129
+ self._vcard_fetched = False
130
+ self._vcard: str | None = None
131
+
132
+ async def get_vcard(self, fetch=True) -> VCard4 | None:
133
+ if fetch and not self._vcard_fetched:
134
+ await self.fetch_vcard()
135
+ if self._vcard is None:
136
+ return None
137
+
138
+ return VCard4(xml=ET.fromstring(self._vcard))
128
139
 
129
140
  @property
130
141
  def is_friend(self):
@@ -373,7 +384,17 @@ class LegacyContact(
373
384
  elif country:
374
385
  vcard.add_address(country, locality)
375
386
 
376
- self.xmpp.vcard.set_vcard(self.jid.bare, vcard, {self.user_jid.bare})
387
+ self._vcard = str(vcard)
388
+ self._vcard_fetched = True
389
+ self.session.create_task(
390
+ self.xmpp.pubsub.broadcast_vcard_event(self.jid, self.user_jid, vcard)
391
+ )
392
+
393
+ if self._updating_info:
394
+ return
395
+
396
+ assert self.contact_pk is not None
397
+ self.xmpp.store.contacts.set_vcard(self.contact_pk, self._vcard)
377
398
 
378
399
  def get_roster_item(self):
379
400
  item = {
@@ -584,6 +605,8 @@ class LegacyContact(
584
605
  contact._caps_ver = stored.caps_ver
585
606
  contact._set_logger_name()
586
607
  contact._set_avatar_from_store(stored)
608
+ contact._vcard = stored.vcard
609
+ contact._vcard_fetched = stored.vcard_fetched
587
610
  return contact
588
611
 
589
612
 
slidge/contact/roster.py CHANGED
@@ -8,6 +8,7 @@ from slixmpp.exceptions import IqError, XMPPError
8
8
  from slixmpp.jid import JID_UNESCAPE_TRANSFORMATIONS, _unescape_node
9
9
 
10
10
  from ..core.mixins.lock import NamedLockMixin
11
+ from ..db.models import Contact
11
12
  from ..db.store import ContactStore
12
13
  from ..util import SubclassableOnce
13
14
  from ..util.types import LegacyContactType, LegacyUserIdType
@@ -63,37 +64,6 @@ class LegacyRoster(
63
64
  for stored in self.__store.get_all(user_pk=self.session.user_pk):
64
65
  yield self._contact_cls.from_store(self.session, stored)
65
66
 
66
- async def __finish_init_contact(
67
- self, legacy_id: LegacyUserIdType, jid_username: str, *args, **kwargs
68
- ):
69
- async with self.lock(("finish", legacy_id)):
70
- with self.__store.session():
71
- stored = self.__store.get_by_legacy_id(
72
- self.session.user_pk, str(legacy_id)
73
- )
74
- if stored is not None:
75
- if stored.updated:
76
- return self._contact_cls.from_store(self.session, stored)
77
- c: LegacyContact = self._contact_cls(
78
- self.session, legacy_id, jid_username, *args, **kwargs
79
- )
80
- c.contact_pk = stored.id
81
- else:
82
- c = self._contact_cls(
83
- self.session, legacy_id, jid_username, *args, **kwargs
84
- )
85
- try:
86
- with c.updating_info():
87
- await c.avatar_wrap_update_info()
88
- except Exception as e:
89
- raise XMPPError("internal-server-error", str(e))
90
- c._caps_ver = await c.get_caps_ver(c.jid)
91
- need_avatar = c.contact_pk is None
92
- c.contact_pk = self.__store.update(c, commit=not self.__filling)
93
- if need_avatar:
94
- c._post_avatar_update()
95
- return c
96
-
97
67
  def known_contacts(self, only_friends=True) -> dict[str, LegacyContactType]:
98
68
  if only_friends:
99
69
  return {c.jid.bare: c for c in self if c.is_friend}
@@ -112,17 +82,15 @@ class LegacyRoster(
112
82
  # """
113
83
  username = contact_jid.node
114
84
  async with self.lock(("username", username)):
115
- with self.__store.session():
116
- stored = self.__store.get_by_jid(self.session.user_pk, contact_jid)
117
- if stored is not None and stored.updated:
118
- return self._contact_cls.from_store(self.session, stored)
119
-
120
85
  legacy_id = await self.jid_username_to_legacy_id(username)
121
86
  log.debug("Contact %s not found", contact_jid)
122
87
  if self.get_lock(("legacy_id", legacy_id)):
123
88
  log.debug("Already updating %s", contact_jid)
124
89
  return await self.by_legacy_id(legacy_id)
125
- return await self.__finish_init_contact(legacy_id, username)
90
+
91
+ with self.__store.session():
92
+ stored = self.__store.get_by_jid(self.session.user_pk, contact_jid)
93
+ return await self.__update_contact(stored, legacy_id, username)
126
94
 
127
95
  async def by_legacy_id(
128
96
  self, legacy_id: LegacyUserIdType, *args, **kwargs
@@ -145,26 +113,48 @@ class LegacyRoster(
145
113
  if legacy_id == self.user_legacy_id:
146
114
  raise ContactIsUser
147
115
  async with self.lock(("legacy_id", legacy_id)):
148
- with self.__store.session():
149
- stored = self.__store.get_by_legacy_id(
150
- self.session.user_pk, str(legacy_id)
151
- )
152
- if stored is not None and stored.updated:
153
- return self._contact_cls.from_store(
154
- self.session, stored, *args, **kwargs
155
- )
156
-
157
116
  username = await self.legacy_id_to_jid_username(legacy_id)
158
- log.debug("Contact %s not found", legacy_id)
159
117
  if self.get_lock(("username", username)):
160
118
  log.debug("Already updating %s", username)
161
119
  jid = JID()
162
120
  jid.node = username
163
121
  jid.domain = self.session.xmpp.boundjid.bare
164
122
  return await self.by_jid(jid)
165
- return await self.__finish_init_contact(
166
- legacy_id, username, *args, **kwargs
167
- )
123
+
124
+ with self.__store.session():
125
+ stored = self.__store.get_by_legacy_id(
126
+ self.session.user_pk, str(legacy_id)
127
+ )
128
+ return await self.__update_contact(
129
+ stored, legacy_id, username, *args, **kwargs
130
+ )
131
+
132
+ async def __update_contact(
133
+ self,
134
+ stored: Contact | None,
135
+ legacy_id: LegacyUserIdType,
136
+ username: str,
137
+ *a,
138
+ **kw,
139
+ ) -> LegacyContactType:
140
+ if stored is None:
141
+ contact = self._contact_cls(self.session, legacy_id, username, *a, **kw)
142
+ else:
143
+ contact = self._contact_cls.from_store(self.session, stored, *a, **kw)
144
+ if stored.updated:
145
+ return contact
146
+
147
+ try:
148
+ with contact.updating_info():
149
+ await contact.avatar_wrap_update_info()
150
+ except Exception as e:
151
+ raise XMPPError("internal-server-error", str(e))
152
+ contact._caps_ver = await contact.get_caps_ver(contact.jid)
153
+ need_avatar = contact.contact_pk is None
154
+ contact.contact_pk = self.__store.update(contact, commit=not self.__filling)
155
+ if need_avatar:
156
+ contact._post_avatar_update()
157
+ return contact
168
158
 
169
159
  async def by_stanza(self, s) -> LegacyContact:
170
160
  # """
@@ -225,7 +215,7 @@ class LegacyRoster(
225
215
  item = contact.get_roster_item()
226
216
  old = user_roster.get(contact.jid.bare)
227
217
  if old is not None and all(
228
- old[k] == item[contact.jid.bare][k]
218
+ old[k] == item[contact.jid.bare].get(k)
229
219
  for k in ("subscription", "groups", "name")
230
220
  ):
231
221
  self.log.debug("No need to update roster")
@@ -36,7 +36,6 @@ from ...command.register import RegistrationType
36
36
  from ...db import GatewayUser, SlidgeStore
37
37
  from ...db.avatar import avatar_cache
38
38
  from ...slixfix.roster import RosterBackend
39
- from ...slixfix.xep_0292.vcard4 import VCard4Provider
40
39
  from ...util import ABCSubclassableOnceAtMost
41
40
  from ...util.types import AvatarType, MessageOrPresenceTypeVar
42
41
  from ...util.util import timeit
@@ -316,7 +315,7 @@ class BaseGateway(
316
315
 
317
316
  from ...group.room import LegacyMUC
318
317
 
319
- LegacyMUC.get_unique_subclass().xmpp = self
318
+ LegacyMUC.get_self_or_unique_subclass().xmpp = self
320
319
 
321
320
  self.get_session_from_stanza: Callable[
322
321
  [Union[Message, Presence, Iq]], BaseSession
@@ -331,7 +330,6 @@ class BaseGateway(
331
330
 
332
331
  self.register_plugin("pubsub", {"component_name": self.COMPONENT_NAME})
333
332
  self.pubsub: PubSubComponent = self["pubsub"]
334
- self.vcard: VCard4Provider = self["xep_0292_provider"]
335
333
  self.delivery_receipt: DeliveryReceipt = DeliveryReceipt(self)
336
334
 
337
335
  # with this we receive user avatar updates
@@ -68,6 +68,13 @@ class SessionDispatcher:
68
68
  _exceptions_to_xmpp_errors(self.on_ibr_remove), # type: ignore
69
69
  )
70
70
  )
71
+ self.xmpp.register_handler(
72
+ CoroutineCallback(
73
+ "get_vcard",
74
+ StanzaPath("iq@type=get/vcard"),
75
+ _exceptions_to_xmpp_errors(self.on_get_vcard), # type:ignore
76
+ )
77
+ )
71
78
 
72
79
  for event in (
73
80
  "legacy_message",
@@ -778,6 +785,18 @@ class SessionDispatcher:
778
785
 
779
786
  raise XMPPError("feature-not-implemented")
780
787
 
788
+ async def on_get_vcard(self, iq: Iq):
789
+ session = await self.__get_session(iq)
790
+ session.raise_if_not_logged()
791
+ contact = await session.contacts.by_jid(iq.get_to())
792
+ vcard = await contact.get_vcard()
793
+ reply = iq.reply()
794
+ if vcard:
795
+ reply.append(vcard)
796
+ else:
797
+ reply.enable("vcard")
798
+ reply.send()
799
+
781
800
  def _xmpp_msg_id_to_legacy(self, session: "BaseSession", xmpp_id: str):
782
801
  sent = self.xmpp.store.sent.get_legacy_id(session.user_pk, xmpp_id)
783
802
  if sent is not None:
@@ -79,14 +79,14 @@ class VCardTemp:
79
79
  elif not (contact := entity.contact):
80
80
  raise XMPPError("item-not-found", "This participant has no contact")
81
81
  else:
82
- vcard = await self.xmpp.vcard.get_vcard(contact.jid, iq.get_from())
82
+ vcard = await contact.get_vcard()
83
83
  avatar = contact.get_avatar()
84
84
  type_ = "image/png"
85
85
  else:
86
86
  avatar = entity.get_avatar()
87
87
  type_ = "image/png"
88
88
  if isinstance(entity, LegacyContact):
89
- vcard = await self.xmpp.vcard.get_vcard(entity.jid, iq.get_from())
89
+ vcard = await entity.get_vcard(fetch=False)
90
90
  else:
91
91
  vcard = None
92
92
  v = self.xmpp.plugin["xep_0054"].make_vcard()
slidge/core/pubsub.py CHANGED
@@ -22,6 +22,7 @@ from slixmpp.types import JidStr, OptJidStr
22
22
 
23
23
  from ..db.avatar import CachedAvatar, avatar_cache
24
24
  from ..db.store import ContactStore, SlidgeStore
25
+ from ..util.types import URL
25
26
  from .mixins.lock import NamedLockMixin
26
27
 
27
28
  if TYPE_CHECKING:
@@ -135,6 +136,7 @@ class PubSubComponent(NamedLockMixin, BasePlugin):
135
136
 
136
137
  to = p.get_to()
137
138
 
139
+ contact = None
138
140
  # we don't want to push anything for contacts that are not in the user's roster
139
141
  if to != self.xmpp.boundjid.bare:
140
142
  session = self.xmpp.get_session_from_stanza(p)
@@ -170,14 +172,13 @@ class PubSubComponent(NamedLockMixin, BasePlugin):
170
172
  except XMPPError:
171
173
  pass
172
174
  else:
173
- if pep_avatar.metadata is None:
174
- raise XMPPError("internal-server-error", "Avatar but no metadata?")
175
- await self.__broadcast(
176
- data=pep_avatar.metadata,
177
- from_=p.get_to(),
178
- to=from_,
179
- id=pep_avatar.metadata["info"]["id"],
180
- )
175
+ if pep_avatar.metadata is not None:
176
+ await self.__broadcast(
177
+ data=pep_avatar.metadata,
178
+ from_=p.get_to(),
179
+ to=from_,
180
+ id=pep_avatar.metadata["info"]["id"],
181
+ )
181
182
  if UserNick.namespace + "+notify" in features:
182
183
  try:
183
184
  pep_nick = await self._get_authorized_nick(p)
@@ -186,17 +187,19 @@ class PubSubComponent(NamedLockMixin, BasePlugin):
186
187
  else:
187
188
  await self.__broadcast(data=pep_nick.nick, from_=p.get_to(), to=from_)
188
189
 
189
- if VCARD4_NAMESPACE + "+notify" in features:
190
- await self.broadcast_vcard_event(p.get_to(), to=from_)
190
+ if contact is not None and VCARD4_NAMESPACE + "+notify" in features:
191
+ await self.broadcast_vcard_event(
192
+ p.get_to(), from_, await contact.get_vcard()
193
+ )
191
194
 
192
- async def broadcast_vcard_event(self, from_, to):
195
+ async def broadcast_vcard_event(self, from_: JID, to: JID, vcard: VCard4 | None):
193
196
  item = Item()
194
197
  item.namespace = VCARD4_NAMESPACE
195
198
  item["id"] = "current"
196
- vcard: VCard4 = await self.xmpp["xep_0292_provider"].get_vcard(from_, to)
199
+ # vcard: VCard4 = await self.xmpp["xep_0292_provider"].get_vcard(from_, to)
197
200
  # The vcard content should NOT be in this event according to the spec:
198
201
  # https://xmpp.org/extensions/xep-0292.html#sect-idm45669698174224
199
- # but movim expects it to be here, and I guess
202
+ # but movim expects it to be here, and I guess it does not hurt
200
203
 
201
204
  log.debug("Broadcast vcard4 event: %s", vcard)
202
205
  await self.__broadcast(
@@ -219,7 +222,9 @@ class PubSubComponent(NamedLockMixin, BasePlugin):
219
222
  item = PepAvatar()
220
223
  avatar_id = entity.avatar_id
221
224
  if avatar_id is not None:
222
- stored = avatar_cache.get(str(avatar_id))
225
+ stored = avatar_cache.get(
226
+ avatar_id if isinstance(avatar_id, URL) else str(avatar_id)
227
+ )
223
228
  assert stored is not None
224
229
  item.set_avatar_from_cache(stored)
225
230
  return item
@@ -268,10 +273,9 @@ class PubSubComponent(NamedLockMixin, BasePlugin):
268
273
  # this is not the proper way that clients should retrieve VCards, but
269
274
  # gajim does it this way.
270
275
  # https://xmpp.org/extensions/xep-0292.html#sect-idm45669698174224
271
- vcard: VCard4 = await self.xmpp["xep_0292_provider"].get_vcard(
272
- iq.get_to().bare, iq.get_from().bare
273
- )
274
- log.debug("VCARD: %s -- %s -- %s", iq.get_to().bare, iq.get_from().bare, vcard)
276
+ session = self.xmpp.get_session_from_stanza(iq)
277
+ contact = await session.contacts.by_jid(iq.get_to())
278
+ vcard = await contact.get_vcard()
275
279
  if vcard is None:
276
280
  raise XMPPError("item-not-found")
277
281
  self._reply_with_payload(iq, vcard, "current", VCARD4_NAMESPACE)
File without changes
@@ -0,0 +1,183 @@
1
+ """
2
+ This module covers a backend for storing user data persistently and managing a
3
+ pseudo-roster for the gateway component.
4
+ """
5
+
6
+ import dataclasses
7
+ import datetime
8
+ import logging
9
+ import os.path
10
+ import shelve
11
+ from io import BytesIO
12
+ from os import PathLike
13
+ from typing import Iterable, Optional, Union
14
+
15
+ from pickle_secure import Pickler, Unpickler
16
+ from slixmpp import JID, Iq, Message, Presence
17
+
18
+
19
+ # noinspection PyUnresolvedReferences
20
+ class EncryptedShelf(shelve.DbfilenameShelf):
21
+ cache: dict
22
+ dict: dict
23
+ writeback: bool
24
+ keyencoding: str
25
+ _protocol: int
26
+
27
+ def __init__(
28
+ self, filename: PathLike, key: str, flag="c", protocol=None, writeback=False
29
+ ):
30
+ super().__init__(str(filename), flag, protocol, writeback)
31
+ self.secret_key = key
32
+
33
+ def __getitem__(self, key):
34
+ try:
35
+ value = self.cache[key]
36
+ except KeyError:
37
+ f = BytesIO(self.dict[key.encode(self.keyencoding)])
38
+ value = Unpickler(f, key=self.secret_key).load() # type:ignore
39
+ if self.writeback:
40
+ self.cache[key] = value
41
+ return value
42
+
43
+ def __setitem__(self, key, value):
44
+ if self.writeback:
45
+ self.cache[key] = value
46
+ f = BytesIO()
47
+ p = Pickler(f, self._protocol, key=self.secret_key) # type:ignore
48
+ p.dump(value)
49
+ self.dict[key.encode(self.keyencoding)] = f.getvalue()
50
+
51
+
52
+ @dataclasses.dataclass
53
+ class GatewayUser:
54
+ """
55
+ A gateway user
56
+ """
57
+
58
+ bare_jid: str
59
+ """Bare JID of the user"""
60
+ registration_form: dict[str, Optional[str]]
61
+ """Content of the registration form, as a dict"""
62
+ plugin_data: Optional[dict] = None
63
+ registration_date: Optional[datetime.datetime] = None
64
+
65
+ def __hash__(self):
66
+ return hash(self.bare_jid)
67
+
68
+ def __repr__(self):
69
+ return f"<User {self.bare_jid}>"
70
+
71
+ def __post_init__(self):
72
+ if self.registration_date is None:
73
+ self.registration_date = datetime.datetime.now()
74
+
75
+ @property
76
+ def jid(self) -> JID:
77
+ """
78
+ The user's (bare) JID
79
+
80
+ :return:
81
+ """
82
+ return JID(self.bare_jid)
83
+
84
+ def get(self, field: str, default: str = "") -> Optional[str]:
85
+ # """
86
+ # Get fields from the registration form (required to comply with slixmpp backend protocol)
87
+ #
88
+ # :param field: Name of the field
89
+ # :param default: Default value to return if the field is not present
90
+ #
91
+ # :return: Value of the field
92
+ # """
93
+ return self.registration_form.get(field, default)
94
+
95
+
96
+ class UserStore:
97
+ """
98
+ Basic user store implementation using shelve from the python standard library
99
+
100
+ Set_file must be called before it is usable
101
+ """
102
+
103
+ def __init__(self):
104
+ self._users: shelve.Shelf[GatewayUser] = None # type: ignore
105
+
106
+ def set_file(self, filename: PathLike, secret_key: Optional[str] = None):
107
+ """
108
+ Set the file to use to store user data
109
+
110
+ :param filename: Path to the shelf file
111
+ :param secret_key: Secret key to store files encrypted on disk
112
+ """
113
+ if self._users is not None:
114
+ raise RuntimeError("Shelf file already set!")
115
+ if os.path.exists(filename):
116
+ log.info("Using existing slidge DB: %s", filename)
117
+ else:
118
+ log.info("Creating a new slidge DB: %s", filename)
119
+ if secret_key:
120
+ self._users = EncryptedShelf(filename, key=secret_key)
121
+ else:
122
+ self._users = shelve.open(str(filename))
123
+ log.info("Registered users in the DB: %s", list(self._users.keys()))
124
+
125
+ def get_all(self) -> Iterable[GatewayUser]:
126
+ """
127
+ Get all users in the store
128
+
129
+ :return: An iterable of GatewayUsers
130
+ """
131
+ return self._users.values()
132
+
133
+ def commit(self, user: GatewayUser):
134
+ self._users[user.jid.bare] = user
135
+ self._users.sync()
136
+
137
+ def get(self, _gateway_jid, _node, ifrom: JID, iq) -> Optional[GatewayUser]:
138
+ """
139
+ Get a user from the store
140
+
141
+ NB: there is no reason to call this, it is used by SliXMPP internal API
142
+
143
+ :param _gateway_jid:
144
+ :param _node:
145
+ :param ifrom:
146
+ :param iq:
147
+ :return:
148
+ """
149
+ if ifrom is None: # bug in SliXMPP's XEP_0100 plugin
150
+ ifrom = iq["from"]
151
+ log.debug("Getting user %s", ifrom.bare)
152
+ return self._users.get(ifrom.bare)
153
+
154
+ def get_by_jid(self, jid: JID) -> Optional[GatewayUser]:
155
+ """
156
+ Convenience function to get a user from their JID.
157
+
158
+ :param jid: JID of the gateway user
159
+ :return:
160
+ """
161
+ return self._users.get(jid.bare)
162
+
163
+ def get_by_stanza(self, s: Union[Presence, Message, Iq]) -> Optional[GatewayUser]:
164
+ """
165
+ Convenience function to get a user from a stanza they sent.
166
+
167
+ :param s: A stanza sent by the gateway user
168
+ :return:
169
+ """
170
+ return self.get_by_jid(s.get_from())
171
+
172
+ def close(self):
173
+ self._users.sync()
174
+ self._users.close()
175
+
176
+
177
+ user_store = UserStore()
178
+ """
179
+ A persistent store for slidge users. Not public, but I didn't find how to hide
180
+ it from the docs!
181
+ """
182
+
183
+ log = logging.getLogger(__name__)
@@ -0,0 +1,44 @@
1
+ """Lift room legacy ID constraint
2
+
3
+ Revision ID: 5bd48bfdffa2
4
+ Revises: b64b1a793483
5
+ Create Date: 2024-07-24 10:29:23.467851
6
+
7
+ """
8
+
9
+ from typing import Sequence, Union
10
+
11
+ from alembic import op
12
+
13
+ from slidge.db.models import Room
14
+
15
+ # revision identifiers, used by Alembic.
16
+ revision: str = "5bd48bfdffa2"
17
+ down_revision: Union[str, None] = "b64b1a793483"
18
+ branch_labels: Union[str, Sequence[str], None] = None
19
+ depends_on: Union[str, Sequence[str], None] = None
20
+
21
+
22
+ def upgrade() -> None:
23
+ with op.batch_alter_table(
24
+ "room",
25
+ schema=None,
26
+ # without copy_from, the newly created table keeps the constraints
27
+ # we actually want to ditch.
28
+ copy_from=Room.__table__, # type:ignore
29
+ ) as batch_op:
30
+ batch_op.create_unique_constraint(
31
+ "uq_room_user_account_id_jid", ["user_account_id", "jid"]
32
+ )
33
+ batch_op.create_unique_constraint(
34
+ "uq_room_user_account_id_legacy_id", ["user_account_id", "legacy_id"]
35
+ )
36
+
37
+
38
+ def downgrade() -> None:
39
+ # ### commands auto generated by Alembic - please adjust! ###
40
+ with op.batch_alter_table("room", schema=None) as batch_op:
41
+ batch_op.drop_constraint("uq_room_user_account_id_legacy_id", type_="unique")
42
+ batch_op.drop_constraint("uq_room_user_account_id_jid", type_="unique")
43
+
44
+ # ### end Alembic commands ###
@@ -0,0 +1,43 @@
1
+ """Add vcard content to contact table
2
+
3
+ Revision ID: 8b993243a536
4
+ Revises: 5bd48bfdffa2
5
+ Create Date: 2024-07-24 07:02:47.770894
6
+
7
+ """
8
+
9
+ from typing import Sequence, Union
10
+
11
+ import sqlalchemy as sa
12
+ from alembic import op
13
+
14
+ # revision identifiers, used by Alembic.
15
+ revision: str = "8b993243a536"
16
+ down_revision: Union[str, None] = "5bd48bfdffa2"
17
+ branch_labels: Union[str, Sequence[str], None] = None
18
+ depends_on: Union[str, Sequence[str], None] = None
19
+
20
+
21
+ def upgrade() -> None:
22
+ # ### commands auto generated by Alembic - please adjust! ###
23
+ with op.batch_alter_table("contact", schema=None) as batch_op:
24
+ batch_op.add_column(sa.Column("vcard", sa.String(), nullable=True))
25
+ batch_op.add_column(
26
+ sa.Column(
27
+ "vcard_fetched",
28
+ sa.Boolean(),
29
+ nullable=False,
30
+ server_default=sa.sql.true(),
31
+ )
32
+ )
33
+
34
+ # ### end Alembic commands ###
35
+
36
+
37
+ def downgrade() -> None:
38
+ # ### commands auto generated by Alembic - please adjust! ###
39
+ with op.batch_alter_table("contact", schema=None) as batch_op:
40
+ batch_op.drop_column("vcard_fetched")
41
+ batch_op.drop_column("vcard")
42
+
43
+ # ### end Alembic commands ###