Coverage for slidge / util / types.py: 95%
143 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-05-20 19:56 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-05-20 19:56 +0000
1"""
2Typing stuff
3"""
5import warnings
6from collections.abc import AsyncIterator, Hashable, Iterable
7from dataclasses import dataclass, fields
8from datetime import datetime
9from enum import IntEnum
10from pathlib import Path
11from typing import (
12 IO,
13 TYPE_CHECKING,
14 Any,
15 Generic,
16 Literal,
17 NamedTuple,
18 TypedDict,
19 TypeVar,
20 Union,
21)
23from slixmpp import Message, Presence
24from slixmpp.types import PresenceShows, PresenceTypes, ResourceDict # noqa: F401
26if TYPE_CHECKING:
27 from ..contact import LegacyContact, LegacyRoster
28 from ..core.gateway import BaseGateway
29 from ..core.session import BaseSession
30 from ..group import LegacyBookmarks, LegacyMUC
31 from ..group.participant import LegacyParticipant
33 AnySession = BaseSession[Any, Any]
34 AnyGateway = BaseGateway[AnySession]
35 AnyContact = LegacyContact[Any]
36 AnyMUC = LegacyMUC[Any, Any, Any, Any]
37 AnyBookmarks = LegacyBookmarks[Any, Any, Any]
38 AnyRoster = LegacyRoster[Any, Any]
39else:
40 # Hack to work around circular import.
41 AnySession = AnyGateway = AnyContact = AnyMUC = AnyBookmarks = AnyRoster = int
44LegacyGroupIdType = TypeVar("LegacyGroupIdType", bound=Hashable)
45"""
46Type of the unique identifier for groups, usually a str or an int,
47but anything hashable should work.
48"""
49LegacyMessageType = TypeVar("LegacyMessageType", bound=Hashable)
50LegacyThreadType = TypeVar("LegacyThreadType", bound=Hashable)
51LegacyUserIdType = TypeVar("LegacyUserIdType", bound=Hashable)
53LegacyContactType = TypeVar("LegacyContactType", bound=AnyContact)
54LegacyMUCType = TypeVar("LegacyMUCType", bound=AnyMUC)
55LegacyParticipantType = TypeVar("LegacyParticipantType", bound="LegacyParticipant")
57SessionType = TypeVar("SessionType", bound=AnySession)
58AnyRecipient = Union[AnyContact, AnyMUC] # noqa:UP007 because it messes up mypy to use | here
59RecipientType = TypeVar("RecipientType", bound=AnyRecipient)
60Sender = Union[AnyContact, "LegacyParticipant"]
61LegacyFileIdType = int | str
63ChatState = Literal["active", "composing", "gone", "inactive", "paused"]
64ProcessingHint = Literal["no-store", "markable", "store"]
65Marker = Literal["acknowledged", "received", "displayed"]
66FieldType = Literal[
67 "boolean",
68 "fixed",
69 "text-single",
70 "text-multi",
71 "jid-single",
72 "jid-multi",
73 "list-single",
74 "list-multi",
75 "text-private",
76]
77MucAffiliation = Literal["owner", "admin", "member", "outcast", "none"]
78MucRole = Literal["visitor", "participant", "moderator", "none"]
79# https://xmpp.org/registrar/disco-categories.html#client
80ClientType = Literal[
81 "bot", "console", "game", "handheld", "pc", "phone", "sms", "tablet", "web"
82]
83AttachmentDisposition = Literal["attachment", "inline"]
86@dataclass
87class MessageReference(Generic[LegacyMessageType]):
88 """
89 A "message reply", ie a "quoted message" (:xep:`0461`)
91 At the very minimum, the legacy message ID attribute must be set, but to
92 ensure that the quote is displayed in all XMPP clients, the author must also
93 be set (use the string "user" if the slidge user is the author of the referenced
94 message).
95 The body is used as a fallback for XMPP clients that do not support :xep:`0461`
96 of that failed to find the referenced message.
97 """
99 legacy_id: LegacyMessageType
100 author: Union[Literal["user"], "LegacyParticipant", AnyContact] | None = None
101 body: str | None = None
104AnyMessageReference = MessageReference[Any]
107@dataclass
108class LegacyAttachment:
109 """
110 A file attachment to a message
112 At the minimum, one of the ``path``, ``steam``, ``data`` or ``url`` attribute
113 has to be set
115 To be used with :meth:`.LegacyContact.send_files` or
116 :meth:`.LegacyParticipant.send_files`
117 """
119 path: Path | str | None = None
120 name: str | None = None
121 stream: IO[bytes] | None = None
122 aio_stream: AsyncIterator[bytes] | None = None
123 data: bytes | None = None
124 content_type: str | None = None
125 legacy_file_id: str | int | None = None
126 url: str | None = None
127 caption: str | None = None
128 disposition: AttachmentDisposition | None = None
129 is_sticker: bool = False
130 """
131 A caption for this specific image. For a global caption for a list of attachments,
132 use the ``body`` parameter of :meth:`.AttachmentMixin.send_files`
133 """
135 def __post_init__(self) -> None:
136 if all(
137 x is None
138 for x in (self.path, self.stream, self.data, self.url, self.aio_stream)
139 ):
140 raise TypeError("There is not data in this attachment", self)
141 if isinstance(self.path, str):
142 self.path = Path(self.path)
143 if self.is_sticker:
144 if self.disposition == "attachment":
145 warnings.warn(
146 "Sticker declared as 'attachment' disposition, changing it to 'inline'"
147 )
148 self.disposition = "inline"
150 def format_for_user(self) -> str:
151 if self.name:
152 name = self.name
153 elif self.path:
154 name = self.path.name # type:ignore[union-attr]
155 elif self.url:
156 name = self.url
157 else:
158 name = ""
160 if self.caption:
161 name = f"{name}: {self.caption}" if name else self.caption
163 return name
165 def __str__(self) -> str:
166 attrs = ", ".join(
167 f"{f.name}={getattr(self, f.name)!r}"
168 for f in fields(self)
169 if getattr(self, f.name) is not None and f.name != "data"
170 )
171 if self.data is not None:
172 data_str = f"data=<{len(self.data)} bytes>"
173 to_join = (attrs, data_str) if attrs else (data_str,)
174 attrs = ", ".join(to_join)
175 return f"Attachment({attrs})"
178class MucType(IntEnum):
179 """
180 The type of group, private, public, anonymous or not.
181 """
183 GROUP = 0
184 """
185 A private group, members-only and non-anonymous, eg a family group.
186 """
187 CHANNEL = 1
188 """
189 A public group, aka an anonymous channel.
190 """
191 CHANNEL_NON_ANONYMOUS = 2
192 """
193 A public group where participants' legacy IDs are visible to everybody.
194 """
197PseudoPresenceShow = PresenceShows | Literal[""]
200MessageOrPresenceTypeVar = TypeVar("MessageOrPresenceTypeVar", bound=Message | Presence)
203class LinkPreview(NamedTuple):
204 """
205 Embedded metadata from :xep:`0511`.
207 See <https://ogp.me/>_.
208 """
210 about: str
211 """
212 URL of the link.
213 """
214 title: str | None
215 """
216 Title of the linked page.
217 """
218 description: str | None
219 """
220 A description of the page.
221 """
222 url: str | None
223 """
224 The canonical URL of the link.
225 """
226 image: str | Path | bytes | None
227 """
228 An image representing the link. If it is a string, it should represent a URL to an image.
229 """
230 type: str | None
231 """
232 Type of the link destination.
233 """
234 site_name: str | None
235 """
236 Name of the web site.
237 """
239 @property
240 def is_empty(self) -> bool:
241 return not any(x for x in self)
244class Mention(NamedTuple):
245 contact: "LegacyContact[Any]"
246 start: int
247 end: int
250class Hat(NamedTuple):
251 uri: str
252 title: str
253 hue: float | None = None
256class UserPreferences(TypedDict):
257 sync_avatar: bool
258 sync_presence: bool
261class MamMetadata(NamedTuple):
262 id: str
263 sent_on: datetime
266class HoleBound(NamedTuple):
267 id: int | str
268 timestamp: datetime
271class CachedPresence(NamedTuple):
272 last_seen: datetime | None = None
273 ptype: PresenceTypes | None = None
274 pstatus: str | None = None
275 pshow: PresenceShows | None = None
278class Sticker(NamedTuple):
279 path: Path
280 content_type: str | None
281 hashes: dict[str, str]
284class Avatar(NamedTuple):
285 path: Path | None = None
286 unique_id: str | int | None = None
287 url: str | None = None
288 data: bytes | None = None
291class SpaceMetadata(NamedTuple, Generic[LegacyUserIdType]):
292 creator_legacy_id: LegacyUserIdType | None = None
293 name: str | None = None
294 owner_legacy_ids: Iterable[LegacyUserIdType] = []
295 description: str | None = None
296 member_count: int | None = None
299AnySpaceMetadata = SpaceMetadata[Any]