Coverage for slidge/core/mixins/message.py: 87%

106 statements  

« prev     ^ index     » next       coverage.py v7.15.2, created at 2026-08-18 04:30 +0000

1import logging 

2import uuid 

3import warnings 

4 

5from slixmpp import Iq, Message 

6from slixmpp.plugins.xep_0004.stanza.form import Form 

7 

8from ...util.types import AnyMUC, ChatState, Marker 

9from .attachment import AttachmentMixin 

10from .base import SessionBound 

11from .message_maker import MessageMaker 

12from .message_text import TextMessageMixin 

13 

14# this is for MDS 

15PUBLISH_OPTIONS = Form() 

16PUBLISH_OPTIONS["type"] = "submit" 

17PUBLISH_OPTIONS.add_field( 

18 "FORM_TYPE", "hidden", value="http://jabber.org/protocol/pubsub#publish-options" 

19) 

20PUBLISH_OPTIONS.add_field("pubsub#persist_items", value="true") 

21PUBLISH_OPTIONS.add_field("pubsub#max_items", value="max") 

22PUBLISH_OPTIONS.add_field("pubsub#send_last_published_item", value="never") 

23PUBLISH_OPTIONS.add_field("pubsub#access_model", value="whitelist") 

24 

25 

26class ChatStateMixin(MessageMaker): 

27 def __init__(self) -> None: 

28 super().__init__() 

29 self.__last_chat_state: ChatState | None = None 

30 

31 def _chat_state( 

32 self, state: ChatState, forced: bool = False, **kwargs: object 

33 ) -> None: 

34 carbon = kwargs.get("carbon", False) 

35 if carbon or (state == self.__last_chat_state and not forced): 

36 return 

37 self.__last_chat_state = state 

38 msg = self._make_message(state=state, hints={"no-store"}) 

39 self._send(msg, **kwargs) 

40 

41 def active(self, **kwargs: object) -> None: 

42 """ 

43 Send an "active" chat state (:xep:`0085`) from this 

44 :term:`XMPP Entity`. 

45 """ 

46 self._chat_state("active", forced=False, **kwargs) 

47 

48 def composing(self, **kwargs: object) -> None: 

49 """ 

50 Send a "composing" (ie "typing notification") chat state (:xep:`0085`) 

51 from this :term:`XMPP Entity`. 

52 """ 

53 self._chat_state("composing", forced=True, **kwargs) 

54 

55 def paused(self, **kwargs: object) -> None: 

56 """ 

57 Send a "paused" (ie "typing paused notification") chat state 

58 (:xep:`0085`) from this :term:`XMPP Entity`. 

59 """ 

60 self._chat_state("paused", forced=False, **kwargs) 

61 

62 def inactive(self, **kwargs: object) -> None: 

63 """ 

64 Send an "inactive" (ie "contact has not interacted with the chat session 

65 interface for an intermediate period of time") chat state (:xep:`0085`) 

66 from this :term:`XMPP Entity`. 

67 """ 

68 self._chat_state("inactive", forced=False, **kwargs) 

69 

70 def gone(self, **kwargs: object) -> None: 

71 """ 

72 Send a "gone" (ie "contact has not interacted with the chat session interface, 

73 system, or device for a relatively long period of time") chat state 

74 (:xep:`0085`) from this :term:`XMPP Entity`. 

75 """ 

76 self._chat_state("gone", forced=False, **kwargs) 

77 

78 

79class MarkerMixin(MessageMaker, SessionBound): 

80 def _make_marker( 

81 self, legacy_msg_id: str, marker: Marker, carbon: bool = False 

82 ) -> Message: 

83 msg = self._make_message(carbon=carbon) 

84 msg[marker]["id"] = self._legacy_to_xmpp(legacy_msg_id)[-1] 

85 return msg 

86 

87 def ack(self, legacy_msg_id: str, **kwargs: object) -> None: 

88 """ 

89 Send an "acknowledged" message marker (:xep:`0333`) from this :term:`XMPP Entity`. 

90 

91 :param legacy_msg_id: The message this marker refers to 

92 """ 

93 self._send( 

94 self._make_marker( 

95 legacy_msg_id, "acknowledged", carbon=bool(kwargs.get("carbon")) 

96 ), 

97 **kwargs, 

98 ) 

99 

100 def received(self, legacy_msg_id: str, **kwargs: object) -> None: 

101 """ 

102 Send a "received" message marker (:xep:`0333`) from this :term:`XMPP Entity`. 

103 If called on a :class:`LegacyContact`, also send a delivery receipt 

104 marker (:xep:`0184`). 

105 

106 :param legacy_msg_id: The message this marker refers to 

107 """ 

108 carbon = bool(kwargs.get("carbon")) 

109 if self.mtype == "chat": 

110 for msg_id in self._legacy_to_xmpp(legacy_msg_id): 

111 self._send( 

112 self.xmpp.delivery_receipt.make_ack( 

113 msg_id, 

114 mfrom=self.jid, 

115 mto=self.user_jid, 

116 ) 

117 ) 

118 self._send( 

119 self._make_marker(legacy_msg_id, "received", carbon=carbon), **kwargs 

120 ) 

121 

122 def displayed(self, legacy_msg_id: str, **kwargs: object) -> None: 

123 """ 

124 Send a "displayed" message marker (:xep:`0333`) from this :term:`XMPP Entity`. 

125 

126 :param legacy_msg_id: The message this marker refers to 

127 """ 

128 if ( 

129 self.xmpp.MARK_ALL_MESSAGES 

130 and (muc := getattr(self, "muc", None)) 

131 and getattr(self, "is_user", False) 

132 ): 

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

134 if self.xmpp.store.mam.is_displayed_by_user( 

135 orm, muc.jid.local, str(legacy_msg_id) 

136 ): 

137 self.session.log.debug( 

138 "Ignoring carbon marker for message already displayed by user." 

139 ) 

140 return 

141 else: 

142 muc.pop_unread_xmpp_ids_up_to( 

143 self._legacy_to_xmpp(legacy_msg_id)[-1] 

144 ) 

145 

146 self._send( 

147 self._make_marker( 

148 legacy_msg_id, "displayed", carbon=bool(kwargs.get("carbon")) 

149 ), 

150 **kwargs, 

151 ) 

152 if getattr(self, "is_user", False): 

153 self.session.create_task(self.__send_mds(legacy_msg_id)) 

154 

155 async def __send_mds(self, legacy_msg_id: str) -> None: 

156 # Send a MDS displayed marker on behalf of the user for a group chat 

157 if muc := getattr(self, "muc", None): 

158 muc_jid = muc.jid.bare 

159 else: 

160 # This is not implemented for 1:1 chat because it would rely on 

161 # storing the XMPP-server injected stanza-id, which we don't track 

162 # ATM. 

163 # In practice, MDS should mostly be useful for public group chats, 

164 # so it should not be an issue. 

165 # We'll see if we need to implement that later 

166 return 

167 xmpp_msg_id = self._legacy_to_xmpp(legacy_msg_id)[-1] 

168 iq = Iq(sto=self.user_jid.bare, sfrom=self.user_jid.bare, stype="set") 

169 iq["pubsub"]["publish"]["node"] = self.xmpp.plugin["xep_0490"].stanza.NS 

170 iq["pubsub"]["publish"]["item"]["id"] = muc_jid 

171 displayed = self.xmpp.plugin["xep_0490"].stanza.Displayed() 

172 displayed["stanza_id"]["id"] = xmpp_msg_id 

173 displayed["stanza_id"]["by"] = muc_jid 

174 iq["pubsub"]["publish"]["item"]["payload"] = displayed 

175 iq["pubsub"]["publish_options"] = PUBLISH_OPTIONS 

176 try: 

177 await self.xmpp.plugin["xep_0356"].send_privileged_iq(iq) 

178 except Exception as e: 

179 self.session.log.debug("Could not MDS mark", exc_info=e) 

180 

181 

182class ContentMessageMixin(AttachmentMixin, TextMessageMixin): 

183 pass 

184 

185 

186class CarbonMessageMixin(ContentMessageMixin, MarkerMixin): 

187 def _privileged_send(self, msg: Message) -> None: 

188 i = msg.get_id() 

189 if i: 

190 self.session.ignore_messages.add(i) 

191 else: 

192 i = "slidge-carbon-" + str(uuid.uuid4()) 

193 msg.set_id(i) 

194 msg.del_origin_id() 

195 try: 

196 self.xmpp.plugin["xep_0356"].send_privileged_message(msg) 

197 except PermissionError: 

198 warnings.warn( 

199 f"Slidge does not have the privilege (XEP-0356) to send messages on behalf of {self.user_jid}. " 

200 "If this is a local user, consider configuring your XMPP server for that." 

201 ) 

202 

203 

204class InviteMixin(MessageMaker): 

205 def invite_to( 

206 self, 

207 muc: AnyMUC, 

208 reason: str | None = None, 

209 password: str | None = None, 

210 **send_kwargs: object, 

211 ) -> None: 

212 """ 

213 Send an invitation to join a group (:xep:`0249`) from this :term:`XMPP Entity`. 

214 

215 :param muc: the muc the user is invited to 

216 :param reason: a text explaining why the user should join this muc 

217 :param password: maybe this will make sense later? not sure 

218 :param send_kwargs: additional kwargs to be passed to _send() 

219 (internal use by slidge) 

220 """ 

221 msg = self._make_message(mtype="normal") 

222 msg["groupchat_invite"]["jid"] = muc.jid 

223 if reason: 

224 msg["groupchat_invite"]["reason"] = reason 

225 if password: 

226 msg["groupchat_invite"]["password"] = password 

227 self._send(msg, **send_kwargs) 

228 

229 

230class MessageMixin(InviteMixin, ChatStateMixin, MarkerMixin, ContentMessageMixin): 

231 pass 

232 

233 

234class MessageCarbonMixin(InviteMixin, ChatStateMixin, CarbonMessageMixin): 

235 pass 

236 

237 

238log = logging.getLogger(__name__)