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

1""" 

2Typing stuff 

3""" 

4 

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) 

22 

23from slixmpp import Message, Presence 

24from slixmpp.types import PresenceShows, PresenceTypes, ResourceDict # noqa: F401 

25 

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 

32 

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 

42 

43 

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) 

52 

53LegacyContactType = TypeVar("LegacyContactType", bound=AnyContact) 

54LegacyMUCType = TypeVar("LegacyMUCType", bound=AnyMUC) 

55LegacyParticipantType = TypeVar("LegacyParticipantType", bound="LegacyParticipant") 

56 

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 

62 

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"] 

84 

85 

86@dataclass 

87class MessageReference(Generic[LegacyMessageType]): 

88 """ 

89 A "message reply", ie a "quoted message" (:xep:`0461`) 

90 

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 """ 

98 

99 legacy_id: LegacyMessageType 

100 author: Union[Literal["user"], "LegacyParticipant", AnyContact] | None = None 

101 body: str | None = None 

102 

103 

104AnyMessageReference = MessageReference[Any] 

105 

106 

107@dataclass 

108class LegacyAttachment: 

109 """ 

110 A file attachment to a message 

111 

112 At the minimum, one of the ``path``, ``steam``, ``data`` or ``url`` attribute 

113 has to be set 

114 

115 To be used with :meth:`.LegacyContact.send_files` or 

116 :meth:`.LegacyParticipant.send_files` 

117 """ 

118 

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 """ 

134 

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" 

149 

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 = "" 

159 

160 if self.caption: 

161 name = f"{name}: {self.caption}" if name else self.caption 

162 

163 return name 

164 

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})" 

176 

177 

178class MucType(IntEnum): 

179 """ 

180 The type of group, private, public, anonymous or not. 

181 """ 

182 

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 """ 

195 

196 

197PseudoPresenceShow = PresenceShows | Literal[""] 

198 

199 

200MessageOrPresenceTypeVar = TypeVar("MessageOrPresenceTypeVar", bound=Message | Presence) 

201 

202 

203class LinkPreview(NamedTuple): 

204 """ 

205 Embedded metadata from :xep:`0511`. 

206 

207 See <https://ogp.me/>_. 

208 """ 

209 

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 """ 

238 

239 @property 

240 def is_empty(self) -> bool: 

241 return not any(x for x in self) 

242 

243 

244class Mention(NamedTuple): 

245 contact: "LegacyContact[Any]" 

246 start: int 

247 end: int 

248 

249 

250class Hat(NamedTuple): 

251 uri: str 

252 title: str 

253 hue: float | None = None 

254 

255 

256class UserPreferences(TypedDict): 

257 sync_avatar: bool 

258 sync_presence: bool 

259 

260 

261class MamMetadata(NamedTuple): 

262 id: str 

263 sent_on: datetime 

264 

265 

266class HoleBound(NamedTuple): 

267 id: int | str 

268 timestamp: datetime 

269 

270 

271class CachedPresence(NamedTuple): 

272 last_seen: datetime | None = None 

273 ptype: PresenceTypes | None = None 

274 pstatus: str | None = None 

275 pshow: PresenceShows | None = None 

276 

277 

278class Sticker(NamedTuple): 

279 path: Path 

280 content_type: str | None 

281 hashes: dict[str, str] 

282 

283 

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 

289 

290 

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 

297 

298 

299AnySpaceMetadata = SpaceMetadata[Any]