comparison sat/plugins/plugin_xep_0085.py @ 4037:524856bd7b19

massive refactoring to switch from camelCase to snake_case: historically, Libervia (SàT before) was using camelCase as allowed by PEP8 when using a pre-PEP8 code, to use the same coding style as in Twisted. However, snake_case is more readable and it's better to follow PEP8 best practices, so it has been decided to move on full snake_case. Because Libervia has a huge codebase, this ended with a ugly mix of camelCase and snake_case. To fix that, this patch does a big refactoring by renaming every function and method (including bridge) that are not coming from Twisted or Wokkel, to use fully snake_case. This is a massive change, and may result in some bugs.
author Goffi <goffi@goffi.org>
date Sat, 08 Apr 2023 13:54:42 +0200
parents 6cf4bd6972c2
children c23cad65ae99
comparison
equal deleted inserted replaced
4036:c4464d7ae97b 4037:524856bd7b19
101 log.info(_("Chat State Notifications plugin initialization")) 101 log.info(_("Chat State Notifications plugin initialization"))
102 self.host = host 102 self.host = host
103 self.map = {} # FIXME: would be better to use client instead of mapping profile to data 103 self.map = {} # FIXME: would be better to use client instead of mapping profile to data
104 104
105 # parameter value is retrieved before each use 105 # parameter value is retrieved before each use
106 host.memory.updateParams(self.params) 106 host.memory.update_params(self.params)
107 107
108 # triggers from core 108 # triggers from core
109 host.trigger.add("messageReceived", self.messageReceivedTrigger) 109 host.trigger.add("messageReceived", self.message_received_trigger)
110 host.trigger.add("sendMessage", self.sendMessageTrigger) 110 host.trigger.add("sendMessage", self.send_message_trigger)
111 host.trigger.add("paramUpdateTrigger", self.paramUpdateTrigger) 111 host.trigger.add("param_update_trigger", self.param_update_trigger)
112 112
113 # args: to_s (jid as string), profile 113 # args: to_s (jid as string), profile
114 host.bridge.addMethod( 114 host.bridge.add_method(
115 "chatStateComposing", 115 "chat_state_composing",
116 ".plugin", 116 ".plugin",
117 in_sign="ss", 117 in_sign="ss",
118 out_sign="", 118 out_sign="",
119 method=self.chatStateComposing, 119 method=self.chat_state_composing,
120 ) 120 )
121 121
122 # args: from (jid as string), state in CHAT_STATES, profile 122 # args: from (jid as string), state in CHAT_STATES, profile
123 host.bridge.addSignal("chatStateReceived", ".plugin", signature="sss") 123 host.bridge.add_signal("chat_state_received", ".plugin", signature="sss")
124 124
125 def getHandler(self, client): 125 def get_handler(self, client):
126 return XEP_0085_handler(self, client.profile) 126 return XEP_0085_handler(self, client.profile)
127 127
128 def profileDisconnected(self, client): 128 def profile_disconnected(self, client):
129 """Eventually send a 'gone' state to all one2one contacts.""" 129 """Eventually send a 'gone' state to all one2one contacts."""
130 profile = client.profile 130 profile = client.profile
131 if profile not in self.map: 131 if profile not in self.map:
132 return 132 return
133 for to_jid in self.map[profile]: 133 for to_jid in self.map[profile]:
135 # before the chat state, so it will be ignored... find a way to 135 # before the chat state, so it will be ignored... find a way to
136 # actually defer the disconnection 136 # actually defer the disconnection
137 self.map[profile][to_jid]._onEvent("gone") 137 self.map[profile][to_jid]._onEvent("gone")
138 del self.map[profile] 138 del self.map[profile]
139 139
140 def updateCache(self, entity_jid, value, profile): 140 def update_cache(self, entity_jid, value, profile):
141 """Update the entity data of the given profile for one or all contacts. 141 """Update the entity data of the given profile for one or all contacts.
142 Reset the chat state(s) display if the notification has been disabled. 142 Reset the chat state(s) display if the notification has been disabled.
143 143
144 @param entity_jid: contact's JID, or C.ENTITY_ALL to update all contacts. 144 @param entity_jid: contact's JID, or C.ENTITY_ALL to update all contacts.
145 @param value: True, False or DELETE_VALUE to delete the entity data 145 @param value: True, False or DELETE_VALUE to delete the entity data
146 @param profile: current profile 146 @param profile: current profile
147 """ 147 """
148 client = self.host.getClient(profile) 148 client = self.host.get_client(profile)
149 if value == DELETE_VALUE: 149 if value == DELETE_VALUE:
150 self.host.memory.delEntityDatum(client, entity_jid, ENTITY_KEY) 150 self.host.memory.del_entity_datum(client, entity_jid, ENTITY_KEY)
151 else: 151 else:
152 self.host.memory.updateEntityData( 152 self.host.memory.update_entity_data(
153 client, entity_jid, ENTITY_KEY, value 153 client, entity_jid, ENTITY_KEY, value
154 ) 154 )
155 if not value or value == DELETE_VALUE: 155 if not value or value == DELETE_VALUE:
156 # reinit chat state UI for this or these contact(s) 156 # reinit chat state UI for this or these contact(s)
157 self.host.bridge.chatStateReceived(entity_jid.full(), "", profile) 157 self.host.bridge.chat_state_received(entity_jid.full(), "", profile)
158 158
159 def paramUpdateTrigger(self, name, value, category, type_, profile): 159 def param_update_trigger(self, name, value, category, type_, profile):
160 """Reset all the existing chat state entity data associated with this profile after a parameter modification. 160 """Reset all the existing chat state entity data associated with this profile after a parameter modification.
161 161
162 @param name: parameter name 162 @param name: parameter name
163 @param value: "true" to activate the notifications, or any other value to delete it 163 @param value: "true" to activate the notifications, or any other value to delete it
164 @param category: parameter category 164 @param category: parameter category
165 @param type_: parameter type 165 @param type_: parameter type
166 """ 166 """
167 if (category, name) == (PARAM_KEY, PARAM_NAME): 167 if (category, name) == (PARAM_KEY, PARAM_NAME):
168 self.updateCache( 168 self.update_cache(
169 C.ENTITY_ALL, True if C.bool(value) else DELETE_VALUE, profile=profile 169 C.ENTITY_ALL, True if C.bool(value) else DELETE_VALUE, profile=profile
170 ) 170 )
171 return False 171 return False
172 return True 172 return True
173 173
174 def messageReceivedTrigger(self, client, message, post_treat): 174 def message_received_trigger(self, client, message, post_treat):
175 """ 175 """
176 Update the entity cache when we receive a message with body. 176 Update the entity cache when we receive a message with body.
177 Check for a chat state in the message and signal frontends. 177 Check for a chat state in the message and signal frontends.
178 """ 178 """
179 profile = client.profile 179 profile = client.profile
180 if not self.host.memory.getParamA(PARAM_NAME, PARAM_KEY, profile_key=profile): 180 if not self.host.memory.param_get_a(PARAM_NAME, PARAM_KEY, profile_key=profile):
181 return True 181 return True
182 182
183 from_jid = JID(message.getAttribute("from")) 183 from_jid = JID(message.getAttribute("from"))
184 if self._isMUC(from_jid, profile): 184 if self._is_muc(from_jid, profile):
185 from_jid = from_jid.userhostJID() 185 from_jid = from_jid.userhostJID()
186 else: # update entity data for one2one chat 186 else: # update entity data for one2one chat
187 # assert from_jid.resource # FIXME: assert doesn't work on normal message from server (e.g. server announce), because there is no resource 187 # assert from_jid.resource # FIXME: assert doesn't work on normal message from server (e.g. server announce), because there is no resource
188 try: 188 try:
189 next(domish.generateElementsNamed(message.elements(), name="body")) 189 next(domish.generateElementsNamed(message.elements(), name="body"))
190 try: 190 try:
191 next(domish.generateElementsNamed(message.elements(), name="active")) 191 next(domish.generateElementsNamed(message.elements(), name="active"))
192 # contact enabled Chat State Notifications 192 # contact enabled Chat State Notifications
193 self.updateCache(from_jid, True, profile=profile) 193 self.update_cache(from_jid, True, profile=profile)
194 except StopIteration: 194 except StopIteration:
195 if message.getAttribute("type") == "chat": 195 if message.getAttribute("type") == "chat":
196 # contact didn't enable Chat State Notifications 196 # contact didn't enable Chat State Notifications
197 self.updateCache(from_jid, False, profile=profile) 197 self.update_cache(from_jid, False, profile=profile)
198 return True 198 return True
199 except StopIteration: 199 except StopIteration:
200 pass 200 pass
201 201
202 # send our next "composing" states to any MUC and to the contacts who enabled the feature 202 # send our next "composing" states to any MUC and to the contacts who enabled the feature
203 self._chatStateInit(from_jid, message.getAttribute("type"), profile) 203 self._chat_state_init(from_jid, message.getAttribute("type"), profile)
204 204
205 state_list = [ 205 state_list = [
206 child.name 206 child.name
207 for child in message.elements() 207 for child in message.elements()
208 if message.getAttribute("type") in MESSAGE_TYPES 208 if message.getAttribute("type") in MESSAGE_TYPES
210 and child.defaultUri == NS_CHAT_STATES 210 and child.defaultUri == NS_CHAT_STATES
211 ] 211 ]
212 for state in state_list: 212 for state in state_list:
213 # there must be only one state according to the XEP 213 # there must be only one state according to the XEP
214 if state != "gone" or message.getAttribute("type") != "groupchat": 214 if state != "gone" or message.getAttribute("type") != "groupchat":
215 self.host.bridge.chatStateReceived( 215 self.host.bridge.chat_state_received(
216 message.getAttribute("from"), state, profile 216 message.getAttribute("from"), state, profile
217 ) 217 )
218 break 218 break
219 return True 219 return True
220 220
221 def sendMessageTrigger( 221 def send_message_trigger(
222 self, client, mess_data, pre_xml_treatments, post_xml_treatments 222 self, client, mess_data, pre_xml_treatments, post_xml_treatments
223 ): 223 ):
224 """ 224 """
225 Eventually add the chat state to the message and initiate 225 Eventually add the chat state to the message and initiate
226 the state machine when sending an "active" state. 226 the state machine when sending an "active" state.
228 profile = client.profile 228 profile = client.profile
229 229
230 def treatment(mess_data): 230 def treatment(mess_data):
231 message = mess_data["xml"] 231 message = mess_data["xml"]
232 to_jid = JID(message.getAttribute("to")) 232 to_jid = JID(message.getAttribute("to"))
233 if not self._checkActivation(to_jid, forceEntityData=True, profile=profile): 233 if not self._check_activation(to_jid, forceEntityData=True, profile=profile):
234 return mess_data 234 return mess_data
235 try: 235 try:
236 # message with a body always mean active state 236 # message with a body always mean active state
237 next(domish.generateElementsNamed(message.elements(), name="body")) 237 next(domish.generateElementsNamed(message.elements(), name="body"))
238 message.addElement("active", NS_CHAT_STATES) 238 message.addElement("active", NS_CHAT_STATES)
239 # launch the chat state machine (init the timer) 239 # launch the chat state machine (init the timer)
240 if self._isMUC(to_jid, profile): 240 if self._is_muc(to_jid, profile):
241 to_jid = to_jid.userhostJID() 241 to_jid = to_jid.userhostJID()
242 self._chatStateActive(to_jid, mess_data["type"], profile) 242 self._chat_state_active(to_jid, mess_data["type"], profile)
243 except StopIteration: 243 except StopIteration:
244 if "chat_state" in mess_data["extra"]: 244 if "chat_state" in mess_data["extra"]:
245 state = mess_data["extra"].pop("chat_state") 245 state = mess_data["extra"].pop("chat_state")
246 assert state in CHAT_STATES 246 assert state in CHAT_STATES
247 message.addElement(state, NS_CHAT_STATES) 247 message.addElement(state, NS_CHAT_STATES)
248 return mess_data 248 return mess_data
249 249
250 post_xml_treatments.addCallback(treatment) 250 post_xml_treatments.addCallback(treatment)
251 return True 251 return True
252 252
253 def _isMUC(self, to_jid, profile): 253 def _is_muc(self, to_jid, profile):
254 """Tell if that JID is a MUC or not 254 """Tell if that JID is a MUC or not
255 255
256 @param to_jid (JID): full or bare JID to check 256 @param to_jid (JID): full or bare JID to check
257 @param profile (str): %(doc_profile)s 257 @param profile (str): %(doc_profile)s
258 @return: bool 258 @return: bool
259 """ 259 """
260 client = self.host.getClient(profile) 260 client = self.host.get_client(profile)
261 try: 261 try:
262 type_ = self.host.memory.getEntityDatum( 262 type_ = self.host.memory.get_entity_datum(
263 client, to_jid.userhostJID(), C.ENTITY_TYPE) 263 client, to_jid.userhostJID(), C.ENTITY_TYPE)
264 if type_ == C.ENTITY_TYPE_MUC: 264 if type_ == C.ENTITY_TYPE_MUC:
265 return True 265 return True
266 except (exceptions.UnknownEntityError, KeyError): 266 except (exceptions.UnknownEntityError, KeyError):
267 pass 267 pass
268 return False 268 return False
269 269
270 def _checkActivation(self, to_jid, forceEntityData, profile): 270 def _check_activation(self, to_jid, forceEntityData, profile):
271 """ 271 """
272 @param to_jid: the contact's full JID (or bare if you know it's a MUC) 272 @param to_jid: the contact's full JID (or bare if you know it's a MUC)
273 @param forceEntityData: if set to True, a non-existing 273 @param forceEntityData: if set to True, a non-existing
274 entity data will be considered to be True (and initialized) 274 entity data will be considered to be True (and initialized)
275 @param: current profile 275 @param: current profile
276 @return: True if the notifications should be sent to this JID. 276 @return: True if the notifications should be sent to this JID.
277 """ 277 """
278 client = self.host.getClient(profile) 278 client = self.host.get_client(profile)
279 # check if the parameter is active 279 # check if the parameter is active
280 if not self.host.memory.getParamA(PARAM_NAME, PARAM_KEY, profile_key=profile): 280 if not self.host.memory.param_get_a(PARAM_NAME, PARAM_KEY, profile_key=profile):
281 return False 281 return False
282 # check if notifications should be sent to this contact 282 # check if notifications should be sent to this contact
283 if self._isMUC(to_jid, profile): 283 if self._is_muc(to_jid, profile):
284 return True 284 return True
285 # FIXME: this assertion crash when we want to send a message to an online bare jid 285 # FIXME: this assertion crash when we want to send a message to an online bare jid
286 # assert to_jid.resource or not self.host.memory.isEntityAvailable(to_jid, profile) # must either have a resource, or talk to an offline contact 286 # assert to_jid.resource or not self.host.memory.is_entity_available(to_jid, profile) # must either have a resource, or talk to an offline contact
287 try: 287 try:
288 return self.host.memory.getEntityDatum(client, to_jid, ENTITY_KEY) 288 return self.host.memory.get_entity_datum(client, to_jid, ENTITY_KEY)
289 except (exceptions.UnknownEntityError, KeyError): 289 except (exceptions.UnknownEntityError, KeyError):
290 if forceEntityData: 290 if forceEntityData:
291 # enable it for the first time 291 # enable it for the first time
292 self.updateCache(to_jid, True, profile=profile) 292 self.update_cache(to_jid, True, profile=profile)
293 return True 293 return True
294 # wait for the first message before sending states 294 # wait for the first message before sending states
295 return False 295 return False
296 296
297 def _chatStateInit(self, to_jid, mess_type, profile): 297 def _chat_state_init(self, to_jid, mess_type, profile):
298 """ 298 """
299 Data initialization for the chat state machine. 299 Data initialization for the chat state machine.
300 300
301 @param to_jid (JID): full JID for one2one, bare JID for MUC 301 @param to_jid (JID): full JID for one2one, bare JID for MUC
302 @param mess_type (str): "one2one" or "groupchat" 302 @param mess_type (str): "one2one" or "groupchat"
307 profile_map = self.map.setdefault(profile, {}) 307 profile_map = self.map.setdefault(profile, {})
308 if to_jid not in profile_map: 308 if to_jid not in profile_map:
309 machine = ChatStateMachine(self.host, to_jid, mess_type, profile) 309 machine = ChatStateMachine(self.host, to_jid, mess_type, profile)
310 self.map[profile][to_jid] = machine 310 self.map[profile][to_jid] = machine
311 311
312 def _chatStateActive(self, to_jid, mess_type, profile_key): 312 def _chat_state_active(self, to_jid, mess_type, profile_key):
313 """ 313 """
314 Launch the chat state machine on "active" state. 314 Launch the chat state machine on "active" state.
315 315
316 @param to_jid (JID): full JID for one2one, bare JID for MUC 316 @param to_jid (JID): full JID for one2one, bare JID for MUC
317 @param mess_type (str): "one2one" or "groupchat" 317 @param mess_type (str): "one2one" or "groupchat"
318 @param profile (str): %(doc_profile)s 318 @param profile (str): %(doc_profile)s
319 """ 319 """
320 profile = self.host.memory.getProfileName(profile_key) 320 profile = self.host.memory.get_profile_name(profile_key)
321 if profile is None: 321 if profile is None:
322 raise exceptions.ProfileUnknownError 322 raise exceptions.ProfileUnknownError
323 self._chatStateInit(to_jid, mess_type, profile) 323 self._chat_state_init(to_jid, mess_type, profile)
324 self.map[profile][to_jid]._onEvent("active") 324 self.map[profile][to_jid]._onEvent("active")
325 325
326 def chatStateComposing(self, to_jid_s, profile_key): 326 def chat_state_composing(self, to_jid_s, profile_key):
327 """Move to the "composing" state when required. 327 """Move to the "composing" state when required.
328 328
329 Since this method is called from the front-end, it needs to check the 329 Since this method is called from the front-end, it needs to check the
330 values of the parameter "Send chat state notifications" and the entity 330 values of the parameter "Send chat state notifications" and the entity
331 data associated to the target JID. 331 data associated to the target JID.
332 332
333 @param to_jid_s (str): contact full JID as a string 333 @param to_jid_s (str): contact full JID as a string
334 @param profile_key (str): %(doc_profile_key)s 334 @param profile_key (str): %(doc_profile_key)s
335 """ 335 """
336 # TODO: try to optimize this method which is called often 336 # TODO: try to optimize this method which is called often
337 client = self.host.getClient(profile_key) 337 client = self.host.get_client(profile_key)
338 to_jid = JID(to_jid_s) 338 to_jid = JID(to_jid_s)
339 if self._isMUC(to_jid, client.profile): 339 if self._is_muc(to_jid, client.profile):
340 to_jid = to_jid.userhostJID() 340 to_jid = to_jid.userhostJID()
341 elif not to_jid.resource: 341 elif not to_jid.resource:
342 to_jid.resource = self.host.memory.getMainResource(client, to_jid) 342 to_jid.resource = self.host.memory.main_resource_get(client, to_jid)
343 if not self._checkActivation( 343 if not self._check_activation(
344 to_jid, forceEntityData=False, profile=client.profile 344 to_jid, forceEntityData=False, profile=client.profile
345 ): 345 ):
346 return 346 return
347 try: 347 try:
348 self.map[client.profile][to_jid]._onEvent("composing") 348 self.map[client.profile][to_jid]._onEvent("composing")
390 log.debug( 390 log.debug(
391 "sending state '{state}' to {jid}".format( 391 "sending state '{state}' to {jid}".format(
392 state=state, jid=self.to_jid.full() 392 state=state, jid=self.to_jid.full()
393 ) 393 )
394 ) 394 )
395 client = self.host.getClient(self.profile) 395 client = self.host.get_client(self.profile)
396 mess_data = { 396 mess_data = {
397 "from": client.jid, 397 "from": client.jid,
398 "to": self.to_jid, 398 "to": self.to_jid,
399 "uid": "", 399 "uid": "",
400 "message": {}, 400 "message": {},
401 "type": self.mess_type, 401 "type": self.mess_type,
402 "subject": {}, 402 "subject": {},
403 "extra": {}, 403 "extra": {},
404 } 404 }
405 client.generateMessageXML(mess_data) 405 client.generate_message_xml(mess_data)
406 mess_data["xml"].addElement(state, NS_CHAT_STATES) 406 mess_data["xml"].addElement(state, NS_CHAT_STATES)
407 client.send(mess_data["xml"]) 407 client.send(mess_data["xml"])
408 408
409 self.state = state 409 self.state = state
410 try: 410 try: