slidge 0.2.0a2__py3-none-any.whl → 0.2.0a4__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
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 ###