Coverage for slidge/core/mixins/message_text.py: 89%

90 statements  

« prev     ^ index     » next       coverage.py v7.17.0, created at 2026-11-10 12:26 +0000

1import logging 

2from collections.abc import Iterable 

3from datetime import datetime 

4 

5from slixmpp import Message 

6 

7from ...util.types import ( 

8 LinkPreview, 

9 MessageReference, 

10 ProcessingHint, 

11) 

12from ...util.util import ( 

13 add_quote_prefix, 

14 remove_emoji_variation_selector_16, 

15 uniquify_replace, 

16) 

17from .message_maker import MessageMaker 

18 

19 

20class TextMessageMixin(MessageMaker): 

21 def __default_hints( 

22 self, hints: Iterable[ProcessingHint] | None = None 

23 ) -> Iterable[ProcessingHint]: 

24 if hints is not None: 

25 return hints 

26 elif self._mtype == "chat": 

27 return {"markable", "store"} 

28 elif self._mtype == "groupchat": 

29 return {"markable"} 

30 else: 

31 raise RuntimeError("Never") 

32 

33 def _replace_id(self, legacy_msg_id: str) -> str: 

34 if ( 

35 self._mtype == "groupchat" 

36 and (recipient_pk := self._recipient_pk()) is not None 

37 ): 

38 with self.xmpp.store.session() as orm: 

39 ids = self.xmpp.store.id_map.get_origin( 

40 orm, recipient_pk, str(legacy_msg_id) 

41 ) 

42 if ids: 

43 if len(ids) > 1: 

44 log.warning( 

45 "More than 1 origin msg ID for '%s': '%s'", 

46 legacy_msg_id, 

47 ids, 

48 ) 

49 return ids[0] 

50 return legacy_msg_id 

51 else: 

52 return self._legacy_to_xmpp(legacy_msg_id)[0] 

53 

54 def send_text( 

55 self, 

56 body: str, 

57 legacy_msg_id: str | None = None, 

58 *, 

59 when: datetime | None = None, 

60 reply_to: MessageReference | None = None, 

61 thread: str | None = None, 

62 hints: Iterable[ProcessingHint] | None = None, 

63 carbon: bool = False, 

64 correction: bool = False, 

65 correction_event_id: str | None = None, 

66 link_previews: list[LinkPreview] | None = None, 

67 **send_kwargs: object, 

68 ) -> Message | None: 

69 """ 

70 Send a text message from this :term:`XMPP Entity`. 

71 

72 :param body: Content of the message 

73 :param legacy_msg_id: If you want to be able to transport read markers from the gateway 

74 user to the legacy network, specify this 

75 :param when: when the message was sent, for a "delay" tag (:xep:`0203`) 

76 :param reply_to: Quote another message (:xep:`0461`) 

77 :param hints: 

78 :param thread: 

79 :param carbon: (only used if called on a :class:`LegacyContact`) 

80 Set this to ``True`` if this is actually a message sent **to** the 

81 :class:`LegacyContact` by the :term:`User`. 

82 Use this to synchronize outgoing history for legacy official apps. 

83 :param correction: whether this message is a correction or not 

84 :param correction_event_id: in the case where an ID is associated with the legacy 

85 'correction event', specify it here to use it on the XMPP side. If not specified, 

86 a random ID will be used. 

87 :param link_previews: A little of sender (or server, or gateway)-generated 

88 previews of URLs linked in the body. 

89 """ 

90 if ( 

91 carbon 

92 and not self.is_participant 

93 and (recipient_pk := self._recipient_pk()) is not None 

94 ): 

95 with self.xmpp.store.session() as orm: 

96 if not correction and self.xmpp.store.id_map.was_sent_by_user( 

97 orm, recipient_pk, str(legacy_msg_id) 

98 ): 

99 log.warning( 

100 "Carbon message for a message an XMPP has sent? This is a bug! %s", 

101 legacy_msg_id, 

102 ) 

103 return None 

104 hints = self.__default_hints(hints) 

105 

106 if correction_event_id == legacy_msg_id: 

107 correction_event_id = None 

108 

109 if correction: 

110 assert legacy_msg_id 

111 replace_id = legacy_msg_id 

112 event_id: str | None = correction_event_id or uniquify_replace( 

113 legacy_msg_id 

114 ) 

115 else: 

116 if correction_event_id: 

117 raise TypeError( 

118 "'correction_event_id' was passed but 'correction=False'" 

119 ) 

120 event_id = legacy_msg_id 

121 

122 msg = self._make_message( 

123 mbody=body, 

124 legacy_msg_id=event_id, 

125 when=when, 

126 reply_to=reply_to, 

127 hints=hints or (), 

128 carbon=carbon, 

129 thread=thread, 

130 link_previews=link_previews, 

131 ) 

132 if correction: 

133 msg["replace"]["id"] = self._replace_id(replace_id) 

134 

135 if event_id is not None: 

136 self._store_sent_msg(event_id, when) 

137 

138 return self._send( 

139 msg, 

140 carbon=carbon, 

141 legacy_msg_id=event_id, 

142 **send_kwargs, 

143 ) 

144 

145 def _store_sent_msg(self, legacy_id: str, when: datetime | None) -> None: 

146 pass 

147 

148 def correct( 

149 self, 

150 legacy_msg_id: str, 

151 new_text: str, 

152 *, 

153 when: datetime | None = None, 

154 reply_to: MessageReference | None = None, 

155 thread: str | None = None, 

156 hints: Iterable[ProcessingHint] | None = None, 

157 carbon: bool = False, 

158 correction_event_id: str | None = None, 

159 link_previews: list[LinkPreview] | None = None, 

160 **send_kwargs: object, 

161 ) -> None: 

162 """ 

163 Modify a message that was previously sent by this :term:`XMPP Entity`. 

164 

165 Uses last message correction (:xep:`0308`) 

166 

167 :param new_text: New content of the message 

168 :param legacy_msg_id: The legacy message ID of the message to correct 

169 :param when: when the message was sent, for a "delay" tag (:xep:`0203`) 

170 :param reply_to: Quote another message (:xep:`0461`) 

171 :param hints: 

172 :param thread: 

173 :param carbon: (only in 1:1) Reflect a message sent to this ``Contact`` by the user. 

174 Use this to synchronize outgoing history for legacy official apps. 

175 :param correction_event_id: in the case where an ID is associated with the legacy 

176 'correction event', specify it here to use it on the XMPP side. If not specified, 

177 a random ID will be used. 

178 :param link_previews: A little of sender (or server, or gateway)-generated 

179 previews of URLs linked in the body. 

180 """ 

181 self.send_text( 

182 new_text, 

183 legacy_msg_id, 

184 when=when, 

185 reply_to=reply_to, 

186 hints=hints, 

187 carbon=carbon, 

188 thread=thread, 

189 correction=True, 

190 correction_event_id=correction_event_id, 

191 link_previews=link_previews, 

192 **send_kwargs, 

193 ) 

194 

195 def react( 

196 self, 

197 legacy_msg_id: str, 

198 emojis: Iterable[str] = (), 

199 thread: str | None = None, 

200 **kwargs: object, 

201 ) -> None: 

202 """ 

203 Send a reaction (:xep:`0444`) from this :term:`XMPP Entity`. 

204 

205 :param legacy_msg_id: The message which the reaction refers to. 

206 :param emojis: An iterable of emojis used as reactions 

207 :param thread: 

208 """ 

209 xmpp_id = kwargs.pop("xmpp_id", None) 

210 emojis = {remove_emoji_variation_selector_16(e) for e in emojis} 

211 if xmpp_id: 

212 assert isinstance(xmpp_id, str) 

213 xmpp_ids = [xmpp_id] 

214 else: 

215 xmpp_ids = self._legacy_to_xmpp(legacy_msg_id) 

216 for xmpp_id in xmpp_ids: 

217 msg = self._make_message( 

218 hints={"store"}, carbon=bool(kwargs.get("carbon")), thread=thread 

219 ) 

220 self.xmpp.plugin["xep_0444"].set_reactions( 

221 msg, to_id=xmpp_id, reactions=emojis 

222 ) 

223 self.__add_reaction_fallback(msg, legacy_msg_id, emojis) 

224 self._send(msg, **kwargs) 

225 

226 def __add_reaction_fallback( 

227 self, 

228 msg: Message, 

229 legacy_msg_id: str, 

230 emojis: Iterable[str] = (), 

231 ) -> None: 

232 session = self._session 

233 if session is None or not session.preferences.get("reaction_fallback", False): 

234 return 

235 msg["fallback"]["for"] = self.xmpp.plugin["xep_0444"].namespace 

236 msg["fallback"].enable("body") 

237 msg["body"] = " ".join(emojis) 

238 if not self.is_participant: 

239 return 

240 with self.xmpp.store.session() as orm: 

241 body = self.xmpp.store.mam.get_body( 

242 orm, self._room._stored.id, str(legacy_msg_id) 

243 ) 

244 if body is None: 

245 return 

246 msg["body"] = add_quote_prefix(body) + "\n" + msg["body"] 

247 

248 def retract( 

249 self, 

250 legacy_msg_id: str, 

251 thread: str | None = None, 

252 **kwargs: object, 

253 ) -> None: 

254 """ 

255 Send a message retraction (:XEP:`0424`) from this :term:`XMPP Entity`. 

256 

257 :param legacy_msg_id: Legacy ID of the message to delete 

258 :param thread: 

259 """ 

260 xmpp_ids = self._legacy_to_xmpp(legacy_msg_id) 

261 replace_id = self._replace_id(legacy_msg_id) 

262 if replace_id not in xmpp_ids: 

263 xmpp_ids.append(replace_id) 

264 for xmpp_id in xmpp_ids: 

265 msg = self._make_message( 

266 state=None, 

267 hints={"store"}, 

268 mbody=f"/me retracted the message {legacy_msg_id}", 

269 carbon=bool(kwargs.get("carbon")), 

270 thread=thread, 

271 ) 

272 msg.enable("fallback") 

273 # namespace version mismatch between slidge and slixmpp, update me later 

274 msg["fallback"]["for"] = self.xmpp.plugin["xep_0424"].namespace[:-1] + "1" 

275 msg["retract"]["id"] = msg["replace"]["id"] = xmpp_id 

276 self._send(msg, **kwargs) 

277 

278 

279log = logging.getLogger(__name__)