Coverage for slidge/core/dispatcher/pubsub.py: 95%
154 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-22 03:37 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-22 03:37 +0000
1from slixmpp import CoroutineCallback, Iq, StanzaPath
2from slixmpp.exceptions import XMPPError
3from slixmpp.plugins.xep_0060.stanza import Affiliation, Subscription
4from slixmpp.plugins.xep_0084.stanza import Data as AvatarData
5from slixmpp.plugins.xep_0084.stanza import MetaData as AvatarMetadata
6from slixmpp.xmlstream import StanzaBase
8from slidge.db.models import Space
10from ...util.types import AnyGateway, AnySession
11from .util import DispatcherMixin, exceptions_to_xmpp_errors
14class PubSubMixin(DispatcherMixin):
15 __slots__: list[str] = []
16 xmpp: AnyGateway
18 def __init__(self, xmpp: AnyGateway) -> None:
19 super().__init__(xmpp)
21 for path, func in [
22 ("get/pubsub/items", self.__get_items),
23 ("set/pubsub/subscribe", self.__set_subscribe),
24 ("set/pubsub/unsubscribe", self.__set_unsubscribe),
25 ("get/pubsub/affiliations", self.__get_affiliations),
26 ("get/pubsub_owner/affiliations", self.__owner_get_affiliations),
27 ("get/pubsub/subscriptions", self.__get_subscriptions),
28 ("get/pubsub_owner/subscriptions", self.__owner_get_subscriptions),
29 ]:
30 self.xmpp.register_handler(
31 CoroutineCallback(
32 func.__name__,
33 StanzaPath(f"iq@to={self.xmpp.boundjid.bare}@type={path}"),
34 func, # type:ignore[arg-type] # ty:ignore[invalid-argument-type]
35 )
36 )
38 self.xmpp.register_handler(
39 CoroutineCallback(
40 "get_items_any",
41 StanzaPath("iq@type=get/pubsub/items"),
42 self.__get_items_any,
43 )
44 )
46 async def __get_items_any(self, iq: StanzaBase) -> None:
47 ito = iq.get_to()
48 if not ito.local:
49 return # handled elsewhere (all over the place actually, refactor needed!)
50 node = iq["pubsub"]["items"]["node"]
51 if node not in _NODES_HANDLED_ELSEWHERE:
52 raise XMPPError("item-not-found")
54 async def __get_items(self, iq: Iq) -> None:
55 node = iq["pubsub"]["items"]["node"]
57 if node in _NODES_HANDLED_ELSEWHERE:
58 # handled in slidge/core/pubsub.py
59 # TODO: have a single entrypoint for all get/pubsub/items
60 return
62 session = await self._get_session(iq, logged=True)
63 legacy_id = await self.__get_legacy_id(session, node)
64 item_ids: list[str] = [item["id"] for item in iq["pubsub"]["items"]["items"]]
66 room_item_ids = list(filter(_is_not_space_image, item_ids))
67 invalid = list(filter(self.__is_not_local_jid, room_item_ids))
69 if invalid:
70 raise XMPPError(
71 "item-not-found",
72 f"These items are not part of this space: {invalid}.",
73 )
75 room_legacy_ids = (
76 [self.__item_to_room_legacy_id(item_id) for item_id in room_item_ids]
77 if room_item_ids
78 else None
79 )
81 space = await session.bookmarks.get_updated_space(
82 legacy_id, room_legacy_id_filter=room_legacy_ids
83 )
84 if space is None:
85 raise XMPPError("item-not-found", f"No space '{legacy_id}'")
87 with self.xmpp.store.session() as orm:
88 orm.add(space)
89 if room_item_ids and len(space.rooms) != len(room_item_ids):
90 raise XMPPError(
91 "item-not-found",
92 f"Could not find rooms: {set(room_item_ids) - {str(r.jid) for r in space.rooms}}",
93 )
95 reply = iq.reply()
96 reply["pubsub"]["items"]["node"] = node
97 self.xmpp.pubsub.set_space_items(space, reply["pubsub"]["items"], item_ids)
99 if len(reply["pubsub"]["items"]) == 0:
100 if len(item_ids) == 1:
101 raise XMPPError(
102 "item-not-found", f"{item_ids[0]!r} could not be found."
103 )
104 else:
105 raise XMPPError(
106 "item-not-found",
107 f"Some of these items could not be found: {item_ids}.",
108 )
110 reply.send()
112 def __item_to_room_legacy_id(self, item_id: str) -> str:
113 _type, legacy_id = self.xmpp.localpart_to_legacy_id(
114 item_id.removesuffix(f"@{self.xmpp.boundjid.bare}")
115 )
116 return legacy_id
118 def __is_not_local_jid(self, item_id: str) -> bool:
119 return not item_id.endswith(f"@{self.xmpp.boundjid.bare}")
121 async def __get_legacy_id(self, session: AnySession, node: str) -> str:
122 try:
123 legacy_id = session.bookmarks.space_node_to_legacy_id(node)
124 except Exception as e: # noqa: BLE001
125 raise XMPPError("item-not-found", str(e))
126 with self.xmpp.store.session() as orm:
127 if not self.xmpp.store.spaces.exists(orm, session.user_pk, legacy_id):
128 raise XMPPError(
129 "item-not-found", f"This is not a known space: '{legacy_id}'"
130 )
131 return legacy_id
133 async def __set_subscribe(self, iq: Iq) -> None:
134 node = iq["pubsub"]["subscribe"]["node"]
136 session = await self._get_session(iq, logged=True)
137 await self.__get_legacy_id(session, node)
139 reply = iq.reply(clear=True)
140 sub = reply["pubsub"]["subscription"]
141 sub["node"] = node
142 sub["jid"] = session.user_jid
143 sub["subscription"] = "subscribed"
144 reply.send()
146 @exceptions_to_xmpp_errors
147 async def __set_unsubscribe(self, iq: Iq) -> None:
148 session = await self._get_session(iq, logged=True)
150 jid = iq["pubsub"]["unsubscribe"]["jid"]
151 if jid != session.user_jid.bare:
152 raise XMPPError("bad-request", f"Cannot unsubscribe JID {jid}")
154 node = iq["pubsub"]["unsubscribe"]["node"]
155 legacy_id = session.bookmarks.space_node_to_legacy_id(node)
157 await session.on_leave_space(legacy_id)
158 reply = iq.reply(clear=True)
160 sub = reply["pubsub"]["subscription"]
161 sub["node"] = node
162 sub["jid"] = session.user_jid
163 sub["subscription"] = "none"
164 reply.send()
166 async def __get_affiliations(self, iq: Iq) -> None:
167 session = await self._get_session(iq, logged=True)
169 reply = iq.reply()
170 node = iq["pubsub"]["affiliations"]["node"]
171 if node:
172 legacy_id = await self.__get_legacy_id(session, node)
173 with self.xmpp.store.session() as orm:
174 if not self.xmpp.store.spaces.exists(
175 orm, session.user_pk, str(legacy_id)
176 ):
177 raise XMPPError("item-not-found", f"Space '{legacy_id}' not found")
178 affiliation = reply["pubsub"]["affiliations"]["affiliation"]
179 affiliation["node"] = node
180 affiliation["affiliation"] = "subscriber"
181 else:
182 with self.xmpp.store.session() as orm:
183 spaces = list(self.xmpp.store.spaces.get_all(orm, session.user_pk))
184 for space in spaces:
185 reply["pubsub"]["affiliations"].append(
186 await self.__make_affiliation(session, space)
187 )
189 reply.send()
191 @staticmethod
192 async def __make_affiliation(session: AnySession, space: Space) -> Affiliation:
193 affiliation = Affiliation()
194 node = session.bookmarks.space_legacy_id_to_node(space.legacy_id)
195 affiliation["node"] = node
196 affiliation["affiliation"] = "subscriber"
197 return affiliation
199 async def __owner_get_affiliations(self, iq: Iq) -> None:
200 raise XMPPError(
201 "forbidden", "Slidge does not implement managing space affiliations."
202 )
204 async def __owner_get_subscriptions(self, iq: Iq) -> None:
205 node = iq["pubsub_owner"]["subscriptions"]["node"]
206 if not node:
207 raise XMPPError("bad-request", "No node was specified")
209 session = await self._get_session(iq, logged=True)
210 await self.__get_legacy_id(session, node)
212 reply = iq.reply(clear=False)
213 sub = reply["pubsub_owner"]["subscriptions"]["subscription"]
214 sub["jid"] = session.user_jid
215 sub["subscription"] = "subscribed"
216 reply.send()
218 async def __get_subscriptions(self, iq: Iq) -> None:
219 _node = iq["pubsub"]["subscriptions"]["node"]
221 session = await self._get_session(iq, logged=True)
223 reply = iq.reply(clear=False)
224 subscriptions = reply["pubsub"]["subscriptions"]
225 if node := iq["pubsub"]["subscriptions"]["node"]:
226 await self.__get_legacy_id(session, node)
227 subscription = subscriptions["subscription"]
228 subscription["node"] = node
229 subscription["jid"] = session.user_jid
230 subscription["subscription"] = "subscribed"
231 else:
232 with self.xmpp.store.session() as orm:
233 spaces = list(self.xmpp.store.spaces.get_all(orm, session.user_pk))
234 for space in spaces:
235 subscriptions.append(await self.__make_subscription(session, space))
237 reply.send()
239 @staticmethod
240 async def __make_subscription(session: AnySession, space: Space) -> Subscription:
241 subscription = Subscription()
242 node = session.bookmarks.space_legacy_id_to_node(space.legacy_id)
243 subscription["node"] = node
244 subscription["jid"] = session.user_jid
245 subscription["subscription"] = "subscribed"
246 return subscription
249def _is_not_space_image(item_id: str) -> bool:
250 return item_id not in _SPACE_IMAGES_NAMESPACES
253_SPACE_IMAGES_NAMESPACES = (
254 "urn:xmpp:spaces:avatar:metadata:0",
255 "urn:xmpp:spaces:banner:metadata:0",
256)
258_NODES_HANDLED_ELSEWHERE = (
259 AvatarData.namespace,
260 AvatarMetadata.namespace,
261 "urn:xmpp:vcard4",
262 *_SPACE_IMAGES_NAMESPACES,
263)