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

83 statements  

« prev     ^ index     » next       coverage.py v7.15.2, created at 2026-09-29 05:05 +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 add_quote_prefix, remove_emoji_variation_selector_16 

13from .message_maker import MessageMaker 

14 

15 

16class TextMessageMixin(MessageMaker): 

17 def __default_hints( 

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

19 ) -> Iterable[ProcessingHint]: 

20 if hints is not None: 

21 return hints 

22 elif self.mtype == "chat": 

23 return {"markable", "store"} 

24 elif self.mtype == "groupchat": 

25 return {"markable"} 

26 else: 

27 raise RuntimeError("Never") 

28 

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

30 if ( 

31 self.mtype == "groupchat" 

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

33 ): 

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

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

36 orm, recipient_pk, str(legacy_msg_id) 

37 ) 

38 if ids: 

39 if len(ids) > 1: 

40 log.warning( 

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

42 legacy_msg_id, 

43 ids, 

44 ) 

45 return ids[0] 

46 return legacy_msg_id 

47 else: 

48 return self._legacy_to_xmpp(legacy_msg_id)[0] 

49 

50 def send_text( 

51 self, 

52 body: str, 

53 legacy_msg_id: str | None = None, 

54 *, 

55 when: datetime | None = None, 

56 reply_to: MessageReference | None = None, 

57 thread: str | None = None, 

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

59 carbon: bool = False, 

60 archive_only: bool = False, 

61 correction: bool = False, 

62 correction_event_id: str | None = None, 

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

64 **send_kwargs: object, 

65 ) -> Message | None: 

66 """ 

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

68 

69 :param body: Content of the message 

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

71 user to the legacy network, specify this 

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

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

74 :param hints: 

75 :param thread: 

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

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

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

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

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

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

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

83 a random ID will be used. 

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

85 previews of URLs linked in the body. 

86 :param archive_only: (only in groups) Do not send this message to user, 

87 but store it in the archive. Meant to be used during ``MUC.backfill()`` 

88 """ 

89 if ( 

90 carbon 

91 and not self.is_participant 

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

93 ): 

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

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

96 orm, recipient_pk, str(legacy_msg_id) 

97 ): 

98 log.warning( 

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

100 legacy_msg_id, 

101 ) 

102 return None 

103 hints = self.__default_hints(hints) 

104 msg = self._make_message( 

105 mbody=body, 

106 legacy_msg_id=correction_event_id if correction else legacy_msg_id, 

107 when=when, 

108 reply_to=reply_to, 

109 hints=hints or (), 

110 carbon=carbon, 

111 thread=thread, 

112 link_previews=link_previews, 

113 ) 

114 if correction: 

115 if not legacy_msg_id: 

116 raise TypeError 

117 msg["replace"]["id"] = self._replace_id(legacy_msg_id) 

118 if legacy_msg_id is not None: 

119 self._store_sent_msg(legacy_msg_id, when) 

120 return self._send( 

121 msg, 

122 archive_only=archive_only, 

123 carbon=carbon, 

124 legacy_msg_id=legacy_msg_id, 

125 **send_kwargs, 

126 ) 

127 

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

129 pass 

130 

131 def correct( 

132 self, 

133 legacy_msg_id: str, 

134 new_text: str, 

135 *, 

136 when: datetime | None = None, 

137 reply_to: MessageReference | None = None, 

138 thread: str | None = None, 

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

140 carbon: bool = False, 

141 archive_only: bool = False, 

142 correction_event_id: str | None = None, 

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

144 **send_kwargs: object, 

145 ) -> None: 

146 """ 

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

148 

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

150 

151 :param new_text: New content of the message 

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

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

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

155 :param hints: 

156 :param thread: 

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

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

159 :param archive_only: (only in groups) Do not send this message to user, 

160 but store it in the archive. Meant to be used during ``MUC.backfill()`` 

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

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

163 a random ID will be used. 

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

165 previews of URLs linked in the body. 

166 """ 

167 self.send_text( 

168 new_text, 

169 legacy_msg_id, 

170 when=when, 

171 reply_to=reply_to, 

172 hints=hints, 

173 carbon=carbon, 

174 thread=thread, 

175 correction=True, 

176 archive_only=archive_only, 

177 correction_event_id=correction_event_id, 

178 link_previews=link_previews, 

179 **send_kwargs, 

180 ) 

181 

182 def react( 

183 self, 

184 legacy_msg_id: str, 

185 emojis: Iterable[str] = (), 

186 thread: str | None = None, 

187 **kwargs: object, 

188 ) -> None: 

189 """ 

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

191 

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

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

194 :param thread: 

195 """ 

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

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

198 if xmpp_id: 

199 assert isinstance(xmpp_id, str) 

200 xmpp_ids = [xmpp_id] 

201 else: 

202 xmpp_ids = self._legacy_to_xmpp(legacy_msg_id) 

203 for xmpp_id in xmpp_ids: 

204 msg = self._make_message( 

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

206 ) 

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

208 msg, to_id=xmpp_id, reactions=emojis 

209 ) 

210 self.__add_reaction_fallback(msg, legacy_msg_id, emojis) 

211 self._send(msg, **kwargs) 

212 

213 def __add_reaction_fallback( 

214 self, 

215 msg: Message, 

216 legacy_msg_id: str, 

217 emojis: Iterable[str] = (), 

218 ) -> None: 

219 session = self.session 

220 if session is None or not session.user.preferences.get( 

221 "reaction_fallback", False 

222 ): 

223 return 

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

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

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

227 if not self.is_participant: 

228 return 

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

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

231 orm, self.muc.stored.id, str(legacy_msg_id) 

232 ) 

233 if body is None: 

234 return 

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

236 

237 def retract( 

238 self, 

239 legacy_msg_id: str, 

240 thread: str | None = None, 

241 **kwargs: object, 

242 ) -> None: 

243 """ 

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

245 

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

247 :param thread: 

248 """ 

249 xmpp_ids = self._legacy_to_xmpp(legacy_msg_id) 

250 replace_id = self._replace_id(legacy_msg_id) 

251 if replace_id not in xmpp_ids: 

252 xmpp_ids.append(replace_id) 

253 for xmpp_id in xmpp_ids: 

254 msg = self._make_message( 

255 state=None, 

256 hints={"store"}, 

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

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

259 thread=thread, 

260 ) 

261 msg.enable("fallback") 

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

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

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

265 self._send(msg, **kwargs) 

266 

267 

268log = logging.getLogger(__name__)