Coverage for slidge/core/mixins/disco.py: 96%
106 statements
« prev ^ index » next coverage.py v7.17.0, created at 2026-11-10 12:26 +0000
« prev ^ index » next coverage.py v7.17.0, created at 2026-11-10 12:26 +0000
1from collections.abc import Mapping
2from typing import TYPE_CHECKING, Any, ClassVar
4from slixmpp.plugins.xep_0004.stanza.form import Form
5from slixmpp.plugins.xep_0030.stanza.info import DiscoInfo
6from slixmpp.plugins.xep_0030.stanza.items import DiscoItems
7from slixmpp.types import OptJid
9from .base import Base
11if TYPE_CHECKING:
12 from slidge.command.base import ContactCommand, RoomCommand
15class BaseDiscoMixin(Base):
16 @property
17 def DISCO_TYPE(self) -> str:
18 raise NotImplementedError
20 DISCO_CATEGORY: str = NotImplemented
21 DISCO_NAME: str = NotImplemented
22 DISCO_LANG = None
24 _commands: ClassVar[Mapping[str, "type[ContactCommand[Any] | RoomCommand[Any]]"]]
26 def _get_disco_name(self) -> str | None:
27 if self.DISCO_NAME is NotImplemented:
28 return self.xmpp.COMPONENT_NAME
29 return self.DISCO_NAME or self.xmpp.COMPONENT_NAME
31 def features(self) -> list[str]:
32 return []
34 async def extended_features(self) -> list[Form] | None:
35 return None
37 async def get_disco_info(
38 self, jid: OptJid = None, node: str | None = None
39 ) -> DiscoInfo:
40 info = DiscoInfo()
41 if node == "http://jabber.org/protocol/commands":
42 info.add_identity(category="automation", itype="command-list")
43 elif node and node in self._commands:
44 info.add_identity(
45 category="automation",
46 itype="command-node",
47 name=self._commands[node].NAME,
48 )
49 info.add_feature("http://jabber.org/protocol/commands")
50 info.add_feature("jabber:x:data")
51 else:
52 for feature in self.features():
53 info.add_feature(feature)
54 info.add_identity(
55 category=self.DISCO_CATEGORY,
56 itype=self.DISCO_TYPE,
57 name=self._get_disco_name(),
58 lang=self.DISCO_LANG,
59 )
60 if forms := await self.extended_features():
61 for form in forms:
62 info.append(form)
63 return info
65 async def get_caps_ver(self, jid: OptJid = None, node: str | None = None) -> str:
66 info = await self.get_disco_info(jid, node)
67 caps = self.xmpp.plugin["xep_0115"]
68 ver = caps.generate_verstring(info, caps.hash)
69 return ver # type:ignore[no-any-return]
71 async def get_disco_items(self, node: str | None) -> DiscoItems:
72 items = DiscoItems()
73 if node == "http://jabber.org/protocol/commands":
74 for node_, command in self._commands.items():
75 items.add_item(jid=self.jid, node=node_, name=command.NAME)
77 return items
80class ChatterDiscoMixin(BaseDiscoMixin):
81 AVATAR = True
82 RECEIPTS = True
83 MARKS = True
84 CHAT_STATES = True
85 UPLOAD = True
86 CORRECTION = True
87 REACTION = True
88 RETRACTION = True
89 REPLIES = True
90 _INVITATION_RECIPIENT = False
92 @property
93 def DISCO_TYPE(self) -> str:
94 return "pc"
96 DISCO_CATEGORY = "client"
97 DISCO_NAME = ""
99 is_participant: bool
101 def features(self) -> list[str]:
102 features = []
103 if self.CHAT_STATES:
104 features.append("http://jabber.org/protocol/chatstates")
105 if self.RECEIPTS:
106 features.append("urn:xmpp:receipts")
107 if self.CORRECTION:
108 features.append("urn:xmpp:message-correct:0")
109 if self.MARKS:
110 features.append("urn:xmpp:chat-markers:0")
111 if self.UPLOAD:
112 features.append("jabber:x:oob")
113 if self.REACTION:
114 features.append("urn:xmpp:reactions:0")
115 if self.RETRACTION:
116 features.append("urn:xmpp:message-retract:1")
117 if self.REPLIES:
118 features.append("urn:xmpp:reply:0")
119 if self._INVITATION_RECIPIENT:
120 features.append("jabber:x:conference")
121 features.append("urn:ietf:params:xml:ns:vcard-4.0")
122 if not self.is_participant:
123 features.append("http://jabber.org/protocol/commands")
124 return features
126 async def extended_features(self) -> list[Form] | None:
127 f = getattr(self, "restricted_emoji_extended_feature", None)
128 if f is None:
129 return None
131 e = await f()
132 if not e:
133 return None
135 return [e]
138class ContactAccountDiscoMixin(BaseDiscoMixin):
139 async def get_disco_info(
140 self, jid: OptJid = None, node: str | None = None
141 ) -> DiscoInfo:
142 if jid and jid.resource:
143 return await super().get_disco_info(jid, node)
144 info = DiscoInfo()
145 info.add_feature("http://jabber.org/protocol/pubsub")
146 info.add_feature("http://jabber.org/protocol/pubsub#retrieve-items")
147 info.add_feature("http://jabber.org/protocol/pubsub#subscribe")
148 info.add_identity(
149 category="account",
150 itype="registered",
151 name=self._get_disco_name(),
152 lang=self.DISCO_LANG,
153 )
154 info.add_identity(
155 category="pubsub",
156 itype="pep",
157 name=self._get_disco_name(),
158 lang=self.DISCO_LANG,
159 )
160 return info