Coverage for slidge / util / types.py: 94%

120 statements  

« prev     ^ index     » next       coverage.py v7.13.0, created at 2026-02-15 09:02 +0000

1""" 

2Typing stuff 

3""" 

4 

5from collections.abc import AsyncIterator, Hashable 

6from dataclasses import dataclass, fields 

7from datetime import datetime 

8from enum import IntEnum 

9from pathlib import Path 

10from typing import ( 

11 IO, 

12 TYPE_CHECKING, 

13 Any, 

14 Generic, 

15 Literal, 

16 NamedTuple, 

17 TypedDict, 

18 TypeVar, 

19 Union, 

20) 

21 

22from slixmpp import Message, Presence 

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

24 

25if TYPE_CHECKING: 

26 from ..contact import LegacyContact 

27 from ..core.session import BaseSession 

28 from ..group import LegacyMUC 

29 from ..group.participant import LegacyParticipant 

30 

31 AnyBaseSession = BaseSession[Any, Any] 

32else: 

33 # Hack to work around circular import. 

34 AnyBaseSession = int 

35 

36 

37LegacyGroupIdType = TypeVar("LegacyGroupIdType", bound=Hashable) 

38""" 

39Type of the unique identifier for groups, usually a str or an int, 

40but anything hashable should work. 

41""" 

42LegacyMessageType = TypeVar("LegacyMessageType", bound=Hashable) 

43LegacyThreadType = TypeVar("LegacyThreadType", bound=Hashable) 

44LegacyUserIdType = TypeVar("LegacyUserIdType", bound=Hashable) 

45 

46LegacyContactType = TypeVar("LegacyContactType", bound="LegacyContact[Any]") 

47LegacyMUCType = TypeVar("LegacyMUCType", bound="LegacyMUC[Any, Any, Any, Any]") 

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

49 

50Recipient = Union["LegacyMUC[Any, Any, Any, Any]", "LegacyContact[Any]"] 

51RecipientType = TypeVar("RecipientType", bound=Recipient) 

52Sender = Union["LegacyContact[Any]", "LegacyParticipant"] 

53LegacyFileIdType = int | str 

54 

55ChatState = Literal["active", "composing", "gone", "inactive", "paused"] 

56ProcessingHint = Literal["no-store", "markable", "store"] 

57Marker = Literal["acknowledged", "received", "displayed"] 

58FieldType = Literal[ 

59 "boolean", 

60 "fixed", 

61 "text-single", 

62 "jid-single", 

63 "jid-multi", 

64 "list-single", 

65 "list-multi", 

66 "text-private", 

67] 

68MucAffiliation = Literal["owner", "admin", "member", "outcast", "none"] 

69MucRole = Literal["visitor", "participant", "moderator", "none"] 

70# https://xmpp.org/registrar/disco-categories.html#client 

71ClientType = Literal[ 

72 "bot", "console", "game", "handheld", "pc", "phone", "sms", "tablet", "web" 

73] 

74AttachmentDisposition = Literal["attachment", "inline"] 

75 

76 

77@dataclass 

78class MessageReference(Generic[LegacyMessageType]): 

79 """ 

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

81 

82 At the very minimum, the legacy message ID attribute must be set, but to 

83 ensure that the quote is displayed in all XMPP clients, the author must also 

84 be set (use the string "user" if the slidge user is the author of the referenced 

85 message). 

86 The body is used as a fallback for XMPP clients that do not support :xep:`0461` 

87 of that failed to find the referenced message. 

88 """ 

89 

90 legacy_id: LegacyMessageType 

91 author: Union[Literal["user"], "LegacyParticipant", "LegacyContact"] | None = None 

92 body: str | None = None 

93 

94 

95@dataclass 

96class LegacyAttachment: 

97 """ 

98 A file attachment to a message 

99 

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

101 has to be set 

102 

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

104 :meth:`.LegacyParticipant.send_files` 

105 """ 

106 

107 path: Path | str | None = None 

108 name: str | None = None 

109 stream: IO[bytes] | None = None 

110 aio_stream: AsyncIterator[bytes] | None = None 

111 data: bytes | None = None 

112 content_type: str | None = None 

113 legacy_file_id: str | int | None = None 

114 url: str | None = None 

115 caption: str | None = None 

116 disposition: AttachmentDisposition | None = None 

117 """ 

118 A caption for this specific image. For a global caption for a list of attachments, 

119 use the ``body`` parameter of :meth:`.AttachmentMixin.send_files` 

120 """ 

121 

122 def __post_init__(self) -> None: 

123 if all( 

124 x is None 

125 for x in (self.path, self.stream, self.data, self.url, self.aio_stream) 

126 ): 

127 raise TypeError("There is not data in this attachment", self) 

128 if isinstance(self.path, str): 

129 self.path = Path(self.path) 

130 

131 def format_for_user(self) -> str: 

132 if self.name: 

133 name = self.name 

134 elif self.path: 

135 name = self.path.name # type:ignore[union-attr] 

136 elif self.url: 

137 name = self.url 

138 else: 

139 name = "" 

140 

141 if self.caption: 

142 if name: 

143 name = f"{name}: {self.caption}" 

144 else: 

145 name = self.caption 

146 

147 return name 

148 

149 def __str__(self): 

150 attrs = ", ".join( 

151 f"{f.name}={getattr(self, f.name)!r}" 

152 for f in fields(self) 

153 if getattr(self, f.name) is not None and f.name != "data" 

154 ) 

155 if self.data is not None: 

156 data_str = f"data=<{len(self.data)} bytes>" 

157 to_join = (attrs, data_str) if attrs else (data_str,) 

158 attrs = ", ".join(to_join) 

159 return f"Attachment({attrs})" 

160 

161 

162class MucType(IntEnum): 

163 """ 

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

165 """ 

166 

167 GROUP = 0 

168 """ 

169 A private group, members-only and non-anonymous, eg a family group. 

170 """ 

171 CHANNEL = 1 

172 """ 

173 A public group, aka an anonymous channel. 

174 """ 

175 CHANNEL_NON_ANONYMOUS = 2 

176 """ 

177 A public group where participants' legacy IDs are visible to everybody. 

178 """ 

179 

180 

181PseudoPresenceShow = PresenceShows | Literal[""] 

182 

183 

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

185 

186 

187class LinkPreview(NamedTuple): 

188 about: str 

189 title: str | None 

190 description: str | None 

191 url: str | None 

192 image: str | None 

193 type: str | None 

194 site_name: str | None 

195 

196 

197class Mention(NamedTuple): 

198 contact: "LegacyContact[Any]" 

199 start: int 

200 end: int 

201 

202 

203class Hat(NamedTuple): 

204 uri: str 

205 title: str 

206 hue: float | None = None 

207 

208 

209class UserPreferences(TypedDict): 

210 sync_avatar: bool 

211 sync_presence: bool 

212 

213 

214class MamMetadata(NamedTuple): 

215 id: str 

216 sent_on: datetime 

217 

218 

219class HoleBound(NamedTuple): 

220 id: int | str 

221 timestamp: datetime 

222 

223 

224class CachedPresence(NamedTuple): 

225 last_seen: datetime | None = None 

226 ptype: PresenceTypes | None = None 

227 pstatus: str | None = None 

228 pshow: PresenceShows | None = None 

229 

230 

231class Sticker(NamedTuple): 

232 path: Path 

233 content_type: str | None 

234 hashes: dict[str, str] 

235 

236 

237class Avatar(NamedTuple): 

238 path: Path | None = None 

239 unique_id: str | int | None = None 

240 url: str | None = None 

241 data: bytes | None = None