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

119 statements  

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

1""" 

2Typing stuff 

3""" 

4 

5from dataclasses import dataclass, fields 

6from datetime import datetime 

7from enum import IntEnum 

8from pathlib import Path 

9from typing import ( 

10 IO, 

11 TYPE_CHECKING, 

12 Any, 

13 AsyncIterator, 

14 Generic, 

15 Hashable, 

16 Literal, 

17 NamedTuple, 

18 Optional, 

19 TypedDict, 

20 TypeVar, 

21 Union, 

22) 

23 

24from slixmpp import Message, Presence 

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

26 

27if TYPE_CHECKING: 

28 from ..contact import LegacyContact 

29 from ..core.session import BaseSession 

30 from ..group import LegacyMUC 

31 from ..group.participant import LegacyParticipant 

32 

33 AnyBaseSession = BaseSession[Any, Any] 

34else: 

35 AnyBaseSession = None 

36 

37 

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

39""" 

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

41but anything hashable should work. 

42""" 

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

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

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

46 

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

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

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

50 

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

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

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

54LegacyFileIdType = Union[int, str] 

55 

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

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

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

59FieldType = Literal[ 

60 "boolean", 

61 "fixed", 

62 "text-single", 

63 "jid-single", 

64 "jid-multi", 

65 "list-single", 

66 "list-multi", 

67 "text-private", 

68] 

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

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

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

72ClientType = Literal[ 

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

74] 

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

76 

77 

78@dataclass 

79class MessageReference(Generic[LegacyMessageType]): 

80 """ 

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

82 

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

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

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

86 message). 

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

88 of that failed to find the referenced message. 

89 """ 

90 

91 legacy_id: LegacyMessageType 

92 author: Optional[Union[Literal["user"], "LegacyParticipant", "LegacyContact"]] = ( 

93 None 

94 ) 

95 body: Optional[str] = None 

96 

97 

98@dataclass 

99class LegacyAttachment: 

100 """ 

101 A file attachment to a message 

102 

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

104 has to be set 

105 

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

107 :meth:`.LegacyParticipant.send_files` 

108 """ 

109 

110 path: Optional[Union[Path, str]] = None 

111 name: Optional[Union[str]] = None 

112 stream: Optional[IO[bytes]] = None 

113 aio_stream: Optional[AsyncIterator[bytes]] = None 

114 data: Optional[bytes] = None 

115 content_type: Optional[str] = None 

116 legacy_file_id: Optional[Union[str, int]] = None 

117 url: Optional[str] = None 

118 caption: Optional[str] = None 

119 disposition: Optional[AttachmentDisposition] = None 

120 """ 

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

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

123 """ 

124 

125 def __post_init__(self) -> None: 

126 if all( 

127 x is None 

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

129 ): 

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

131 if isinstance(self.path, str): 

132 self.path = Path(self.path) 

133 

134 def format_for_user(self) -> str: 

135 if self.name: 

136 name = self.name 

137 elif self.path: 

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

139 elif self.url: 

140 name = self.url 

141 else: 

142 name = "" 

143 

144 if self.caption: 

145 if name: 

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

147 else: 

148 name = self.caption 

149 

150 return name 

151 

152 def __str__(self): 

153 attrs = ", ".join( 

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

155 for f in fields(self) 

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

157 ) 

158 if self.data is not None: 

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

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

161 attrs = ", ".join(to_join) 

162 return f"Attachment({attrs})" 

163 

164 

165class MucType(IntEnum): 

166 """ 

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

168 """ 

169 

170 GROUP = 0 

171 """ 

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

173 """ 

174 CHANNEL = 1 

175 """ 

176 A public group, aka an anonymous channel. 

177 """ 

178 CHANNEL_NON_ANONYMOUS = 2 

179 """ 

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

181 """ 

182 

183 

184PseudoPresenceShow = Union[PresenceShows, Literal[""]] 

185 

186 

187MessageOrPresenceTypeVar = TypeVar( 

188 "MessageOrPresenceTypeVar", bound=Union[Message, Presence] 

189) 

190 

191 

192class LinkPreview(NamedTuple): 

193 about: str 

194 title: Optional[str] 

195 description: Optional[str] 

196 url: Optional[str] 

197 image: Optional[str] 

198 type: Optional[str] 

199 site_name: Optional[str] 

200 

201 

202class Mention(NamedTuple): 

203 contact: "LegacyContact[Any]" 

204 start: int 

205 end: int 

206 

207 

208class Hat(NamedTuple): 

209 uri: str 

210 title: str 

211 hue: float | None = None 

212 

213 

214class UserPreferences(TypedDict): 

215 sync_avatar: bool 

216 sync_presence: bool 

217 

218 

219class MamMetadata(NamedTuple): 

220 id: str 

221 sent_on: datetime 

222 

223 

224class HoleBound(NamedTuple): 

225 id: int | str 

226 timestamp: datetime 

227 

228 

229class CachedPresence(NamedTuple): 

230 last_seen: Optional[datetime] = None 

231 ptype: Optional[PresenceTypes] = None 

232 pstatus: Optional[str] = None 

233 pshow: Optional[PresenceShows] = None 

234 

235 

236class Sticker(NamedTuple): 

237 path: Path 

238 content_type: Optional[str] 

239 hashes: dict[str, str] 

240 

241 

242class Avatar(NamedTuple): 

243 path: Optional[Path] = None 

244 unique_id: Optional[str | int] = None 

245 url: Optional[str] = None 

246 data: Optional[bytes] = None