Mercurial > libervia-backend
comparison sat/plugins/plugin_xep_0085.py @ 2624:56f94936df1e
code style reformatting using black
author | Goffi <goffi@goffi.org> |
---|---|
date | Wed, 27 Jun 2018 20:14:46 +0200 |
parents | 26edcf3a30eb |
children | 94708a7d3ecf |
comparison
equal
deleted
inserted
replaced
2623:49533de4540b | 2624:56f94936df1e |
---|---|
19 | 19 |
20 from sat.core.i18n import _ | 20 from sat.core.i18n import _ |
21 from sat.core.constants import Const as C | 21 from sat.core.constants import Const as C |
22 from sat.core import exceptions | 22 from sat.core import exceptions |
23 from sat.core.log import getLogger | 23 from sat.core.log import getLogger |
24 | |
24 log = getLogger(__name__) | 25 log = getLogger(__name__) |
25 from wokkel import disco, iwokkel | 26 from wokkel import disco, iwokkel |
26 from zope.interface import implements | 27 from zope.interface import implements |
27 from twisted.words.protocols.jabber.jid import JID | 28 from twisted.words.protocols.jabber.jid import JID |
29 | |
28 try: | 30 try: |
29 from twisted.words.protocols.xmlstream import XMPPHandler | 31 from twisted.words.protocols.xmlstream import XMPPHandler |
30 except ImportError: | 32 except ImportError: |
31 from wokkel.subprotocols import XMPPHandler | 33 from wokkel.subprotocols import XMPPHandler |
32 from twisted.words.xish import domish | 34 from twisted.words.xish import domish |
48 C.PI_TYPE: "XEP", | 50 C.PI_TYPE: "XEP", |
49 C.PI_PROTOCOLS: ["XEP-0085"], | 51 C.PI_PROTOCOLS: ["XEP-0085"], |
50 C.PI_DEPENDENCIES: [], | 52 C.PI_DEPENDENCIES: [], |
51 C.PI_MAIN: "XEP_0085", | 53 C.PI_MAIN: "XEP_0085", |
52 C.PI_HANDLER: "yes", | 54 C.PI_HANDLER: "yes", |
53 C.PI_DESCRIPTION: _("""Implementation of Chat State Notifications Protocol""") | 55 C.PI_DESCRIPTION: _("""Implementation of Chat State Notifications Protocol"""), |
54 } | 56 } |
55 | 57 |
56 | 58 |
57 # Describe the internal transitions that are triggered | 59 # Describe the internal transitions that are triggered |
58 # by a timer. Beside that, external transitions can be | 60 # by a timer. Beside that, external transitions can be |
61 TRANSITIONS = { | 63 TRANSITIONS = { |
62 "active": {"next_state": "inactive", "delay": 120}, | 64 "active": {"next_state": "inactive", "delay": 120}, |
63 "inactive": {"next_state": "gone", "delay": 480}, | 65 "inactive": {"next_state": "gone", "delay": 480}, |
64 "gone": {"next_state": "", "delay": 0}, | 66 "gone": {"next_state": "", "delay": 0}, |
65 "composing": {"next_state": "paused", "delay": 30}, | 67 "composing": {"next_state": "paused", "delay": 30}, |
66 "paused": {"next_state": "inactive", "delay": 450} | 68 "paused": {"next_state": "inactive", "delay": 450}, |
67 } | 69 } |
68 | 70 |
69 | 71 |
70 class UnknownChatStateException(Exception): | 72 class UnknownChatStateException(Exception): |
71 """ | 73 """ |
72 This error is raised when an unknown chat state is used. | 74 This error is raised when an unknown chat state is used. |
73 """ | 75 """ |
76 | |
74 pass | 77 pass |
75 | 78 |
76 | 79 |
77 class XEP_0085(object): | 80 class XEP_0085(object): |
78 """ | 81 """ |
79 Implementation for XEP 0085 | 82 Implementation for XEP 0085 |
80 """ | 83 """ |
84 | |
81 params = """ | 85 params = """ |
82 <params> | 86 <params> |
83 <individual> | 87 <individual> |
84 <category name="%(category_name)s" label="%(category_label)s"> | 88 <category name="%(category_name)s" label="%(category_label)s"> |
85 <param name="%(param_name)s" label="%(param_label)s" value="true" type="bool" security="0"/> | 89 <param name="%(param_name)s" label="%(param_label)s" value="true" type="bool" security="0"/> |
86 </category> | 90 </category> |
87 </individual> | 91 </individual> |
88 </params> | 92 </params> |
89 """ % { | 93 """ % { |
90 'category_name': PARAM_KEY, | 94 "category_name": PARAM_KEY, |
91 'category_label': _(PARAM_KEY), | 95 "category_label": _(PARAM_KEY), |
92 'param_name': PARAM_NAME, | 96 "param_name": PARAM_NAME, |
93 'param_label': _('Enable chat state notifications') | 97 "param_label": _("Enable chat state notifications"), |
94 } | 98 } |
95 | 99 |
96 def __init__(self, host): | 100 def __init__(self, host): |
97 log.info(_("Chat State Notifications plugin initialization")) | 101 log.info(_("Chat State Notifications plugin initialization")) |
98 self.host = host | 102 self.host = host |
105 host.trigger.add("MessageReceived", self.messageReceivedTrigger) | 109 host.trigger.add("MessageReceived", self.messageReceivedTrigger) |
106 host.trigger.add("sendMessage", self.sendMessageTrigger) | 110 host.trigger.add("sendMessage", self.sendMessageTrigger) |
107 host.trigger.add("paramUpdateTrigger", self.paramUpdateTrigger) | 111 host.trigger.add("paramUpdateTrigger", self.paramUpdateTrigger) |
108 | 112 |
109 # args: to_s (jid as string), profile | 113 # args: to_s (jid as string), profile |
110 host.bridge.addMethod("chatStateComposing", ".plugin", in_sign='ss', | 114 host.bridge.addMethod( |
111 out_sign='', method=self.chatStateComposing) | 115 "chatStateComposing", |
116 ".plugin", | |
117 in_sign="ss", | |
118 out_sign="", | |
119 method=self.chatStateComposing, | |
120 ) | |
112 | 121 |
113 # args: from (jid as string), state in CHAT_STATES, profile | 122 # args: from (jid as string), state in CHAT_STATES, profile |
114 host.bridge.addSignal("chatStateReceived", ".plugin", signature='sss') | 123 host.bridge.addSignal("chatStateReceived", ".plugin", signature="sss") |
115 | 124 |
116 def getHandler(self, client): | 125 def getHandler(self, client): |
117 return XEP_0085_handler(self, client.profile) | 126 return XEP_0085_handler(self, client.profile) |
118 | 127 |
119 def profileDisconnected(self, client): | 128 def profileDisconnected(self, client): |
123 return | 132 return |
124 for to_jid in self.map[profile]: | 133 for to_jid in self.map[profile]: |
125 # FIXME: the "unavailable" presence stanza is received by to_jid | 134 # FIXME: the "unavailable" presence stanza is received by to_jid |
126 # 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 |
127 # actually defer the disconnection | 136 # actually defer the disconnection |
128 self.map[profile][to_jid]._onEvent('gone') | 137 self.map[profile][to_jid]._onEvent("gone") |
129 del self.map[profile] | 138 del self.map[profile] |
130 | 139 |
131 def updateCache(self, entity_jid, value, profile): | 140 def updateCache(self, entity_jid, value, profile): |
132 """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. |
133 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. |
137 @param profile: current profile | 146 @param profile: current profile |
138 """ | 147 """ |
139 if value == DELETE_VALUE: | 148 if value == DELETE_VALUE: |
140 self.host.memory.delEntityDatum(entity_jid, ENTITY_KEY, profile) | 149 self.host.memory.delEntityDatum(entity_jid, ENTITY_KEY, profile) |
141 else: | 150 else: |
142 self.host.memory.updateEntityData(entity_jid, ENTITY_KEY, value, profile_key=profile) | 151 self.host.memory.updateEntityData( |
152 entity_jid, ENTITY_KEY, value, profile_key=profile | |
153 ) | |
143 if not value or value == DELETE_VALUE: | 154 if not value or value == DELETE_VALUE: |
144 # reinit chat state UI for this or these contact(s) | 155 # reinit chat state UI for this or these contact(s) |
145 self.host.bridge.chatStateReceived(entity_jid.full(), "", profile) | 156 self.host.bridge.chatStateReceived(entity_jid.full(), "", profile) |
146 | 157 |
147 def paramUpdateTrigger(self, name, value, category, type_, profile): | 158 def paramUpdateTrigger(self, name, value, category, type_, profile): |
151 @param value: "true" to activate the notifications, or any other value to delete it | 162 @param value: "true" to activate the notifications, or any other value to delete it |
152 @param category: parameter category | 163 @param category: parameter category |
153 @param type_: parameter type | 164 @param type_: parameter type |
154 """ | 165 """ |
155 if (category, name) == (PARAM_KEY, PARAM_NAME): | 166 if (category, name) == (PARAM_KEY, PARAM_NAME): |
156 self.updateCache(C.ENTITY_ALL, True if C.bool(value) else DELETE_VALUE, profile=profile) | 167 self.updateCache( |
168 C.ENTITY_ALL, True if C.bool(value) else DELETE_VALUE, profile=profile | |
169 ) | |
157 return False | 170 return False |
158 return True | 171 return True |
159 | 172 |
160 def messageReceivedTrigger(self, client, message, post_treat): | 173 def messageReceivedTrigger(self, client, message, post_treat): |
161 """ | 174 """ |
176 try: | 189 try: |
177 domish.generateElementsNamed(message.elements(), name="active").next() | 190 domish.generateElementsNamed(message.elements(), name="active").next() |
178 # contact enabled Chat State Notifications | 191 # contact enabled Chat State Notifications |
179 self.updateCache(from_jid, True, profile=profile) | 192 self.updateCache(from_jid, True, profile=profile) |
180 except StopIteration: | 193 except StopIteration: |
181 if message.getAttribute('type') == 'chat': | 194 if message.getAttribute("type") == "chat": |
182 # contact didn't enable Chat State Notifications | 195 # contact didn't enable Chat State Notifications |
183 self.updateCache(from_jid, False, profile=profile) | 196 self.updateCache(from_jid, False, profile=profile) |
184 return True | 197 return True |
185 except StopIteration: | 198 except StopIteration: |
186 pass | 199 pass |
187 | 200 |
188 # send our next "composing" states to any MUC and to the contacts who enabled the feature | 201 # send our next "composing" states to any MUC and to the contacts who enabled the feature |
189 self._chatStateInit(from_jid, message.getAttribute("type"), profile) | 202 self._chatStateInit(from_jid, message.getAttribute("type"), profile) |
190 | 203 |
191 state_list = [child.name for child in message.elements() if | 204 state_list = [ |
192 message.getAttribute("type") in MESSAGE_TYPES | 205 child.name |
193 and child.name in CHAT_STATES | 206 for child in message.elements() |
194 and child.defaultUri == NS_CHAT_STATES] | 207 if message.getAttribute("type") in MESSAGE_TYPES |
208 and child.name in CHAT_STATES | |
209 and child.defaultUri == NS_CHAT_STATES | |
210 ] | |
195 for state in state_list: | 211 for state in state_list: |
196 # there must be only one state according to the XEP | 212 # there must be only one state according to the XEP |
197 if state != 'gone' or message.getAttribute('type') != 'groupchat': | 213 if state != "gone" or message.getAttribute("type") != "groupchat": |
198 self.host.bridge.chatStateReceived(message.getAttribute("from"), state, profile) | 214 self.host.bridge.chatStateReceived( |
215 message.getAttribute("from"), state, profile | |
216 ) | |
199 break | 217 break |
200 return True | 218 return True |
201 | 219 |
202 def sendMessageTrigger(self, client, mess_data, pre_xml_treatments, post_xml_treatments): | 220 def sendMessageTrigger( |
221 self, client, mess_data, pre_xml_treatments, post_xml_treatments | |
222 ): | |
203 """ | 223 """ |
204 Eventually add the chat state to the message and initiate | 224 Eventually add the chat state to the message and initiate |
205 the state machine when sending an "active" state. | 225 the state machine when sending an "active" state. |
206 """ | 226 """ |
207 profile = client.profile | 227 profile = client.profile |
228 | |
208 def treatment(mess_data): | 229 def treatment(mess_data): |
209 message = mess_data['xml'] | 230 message = mess_data["xml"] |
210 to_jid = JID(message.getAttribute("to")) | 231 to_jid = JID(message.getAttribute("to")) |
211 if not self._checkActivation(to_jid, forceEntityData=True, profile=profile): | 232 if not self._checkActivation(to_jid, forceEntityData=True, profile=profile): |
212 return mess_data | 233 return mess_data |
213 try: | 234 try: |
214 # message with a body always mean active state | 235 # message with a body always mean active state |
215 domish.generateElementsNamed(message.elements(), name="body").next() | 236 domish.generateElementsNamed(message.elements(), name="body").next() |
216 message.addElement('active', NS_CHAT_STATES) | 237 message.addElement("active", NS_CHAT_STATES) |
217 # launch the chat state machine (init the timer) | 238 # launch the chat state machine (init the timer) |
218 if self._isMUC(to_jid, profile): | 239 if self._isMUC(to_jid, profile): |
219 to_jid = to_jid.userhostJID() | 240 to_jid = to_jid.userhostJID() |
220 self._chatStateActive(to_jid, mess_data["type"], profile) | 241 self._chatStateActive(to_jid, mess_data["type"], profile) |
221 except StopIteration: | 242 except StopIteration: |
234 @param to_jid (JID): full or bare JID to check | 255 @param to_jid (JID): full or bare JID to check |
235 @param profile (str): %(doc_profile)s | 256 @param profile (str): %(doc_profile)s |
236 @return: bool | 257 @return: bool |
237 """ | 258 """ |
238 try: | 259 try: |
239 type_ = self.host.memory.getEntityDatum(to_jid.userhostJID(), 'type', profile) | 260 type_ = self.host.memory.getEntityDatum(to_jid.userhostJID(), "type", profile) |
240 if type_ == 'chatroom': # FIXME: should not use disco instead ? | 261 if type_ == "chatroom": # FIXME: should not use disco instead ? |
241 return True | 262 return True |
242 except (exceptions.UnknownEntityError, KeyError): | 263 except (exceptions.UnknownEntityError, KeyError): |
243 pass | 264 pass |
244 return False | 265 return False |
245 | 266 |
279 """ | 300 """ |
280 if mess_type is None: | 301 if mess_type is None: |
281 return | 302 return |
282 profile_map = self.map.setdefault(profile, {}) | 303 profile_map = self.map.setdefault(profile, {}) |
283 if to_jid not in profile_map: | 304 if to_jid not in profile_map: |
284 machine = ChatStateMachine(self.host, to_jid, | 305 machine = ChatStateMachine(self.host, to_jid, mess_type, profile) |
285 mess_type, profile) | |
286 self.map[profile][to_jid] = machine | 306 self.map[profile][to_jid] = machine |
287 | 307 |
288 def _chatStateActive(self, to_jid, mess_type, profile_key): | 308 def _chatStateActive(self, to_jid, mess_type, profile_key): |
289 """ | 309 """ |
290 Launch the chat state machine on "active" state. | 310 Launch the chat state machine on "active" state. |
314 to_jid = JID(to_jid_s) | 334 to_jid = JID(to_jid_s) |
315 if self._isMUC(to_jid, client.profile): | 335 if self._isMUC(to_jid, client.profile): |
316 to_jid = to_jid.userhostJID() | 336 to_jid = to_jid.userhostJID() |
317 elif not to_jid.resource: | 337 elif not to_jid.resource: |
318 to_jid.resource = self.host.memory.getMainResource(client, to_jid) | 338 to_jid.resource = self.host.memory.getMainResource(client, to_jid) |
319 if not self._checkActivation(to_jid, forceEntityData=False, profile=client.profile): | 339 if not self._checkActivation( |
340 to_jid, forceEntityData=False, profile=client.profile | |
341 ): | |
320 return | 342 return |
321 try: | 343 try: |
322 self.map[client.profile][to_jid]._onEvent("composing") | 344 self.map[client.profile][to_jid]._onEvent("composing") |
323 except (KeyError, AttributeError): | 345 except (KeyError, AttributeError): |
324 # no message has been sent/received since the notifications | 346 # no message has been sent/received since the notifications |
357 assert state in TRANSITIONS | 379 assert state in TRANSITIONS |
358 transition = TRANSITIONS[state] | 380 transition = TRANSITIONS[state] |
359 assert "next_state" in transition and "delay" in transition | 381 assert "next_state" in transition and "delay" in transition |
360 | 382 |
361 if state != self.state and state != "active": | 383 if state != self.state and state != "active": |
362 if state != 'gone' or self.mess_type != 'groupchat': | 384 if state != "gone" or self.mess_type != "groupchat": |
363 # send a new message without body | 385 # send a new message without body |
364 log.debug(u"sending state '{state}' to {jid}".format(state=state, jid=self.to_jid.full())) | 386 log.debug( |
387 u"sending state '{state}' to {jid}".format( | |
388 state=state, jid=self.to_jid.full() | |
389 ) | |
390 ) | |
365 client = self.host.getClient(self.profile) | 391 client = self.host.getClient(self.profile) |
366 mess_data = { | 392 mess_data = { |
367 'from': client.jid, | 393 "from": client.jid, |
368 'to': self.to_jid, | 394 "to": self.to_jid, |
369 'uid': '', | 395 "uid": "", |
370 'message': {}, | 396 "message": {}, |
371 'type': self.mess_type, | 397 "type": self.mess_type, |
372 'subject': {}, | 398 "subject": {}, |
373 'extra': {}, | 399 "extra": {}, |
374 } | 400 } |
375 client.generateMessageXML(mess_data) | 401 client.generateMessageXML(mess_data) |
376 mess_data['xml'].addElement(state, NS_CHAT_STATES) | 402 mess_data["xml"].addElement(state, NS_CHAT_STATES) |
377 client.send(mess_data['xml']) | 403 client.send(mess_data["xml"]) |
378 | 404 |
379 self.state = state | 405 self.state = state |
380 try: | 406 try: |
381 self.timer.cancel() | 407 self.timer.cancel() |
382 except (internet_error.AlreadyCalled, AttributeError): | 408 except (internet_error.AlreadyCalled, AttributeError): |
383 pass | 409 pass |
384 | 410 |
385 if transition["next_state"] and transition["delay"] > 0: | 411 if transition["next_state"] and transition["delay"] > 0: |
386 self.timer = reactor.callLater(transition["delay"], self._onEvent, transition["next_state"]) | 412 self.timer = reactor.callLater( |
413 transition["delay"], self._onEvent, transition["next_state"] | |
414 ) | |
387 | 415 |
388 | 416 |
389 class XEP_0085_handler(XMPPHandler): | 417 class XEP_0085_handler(XMPPHandler): |
390 implements(iwokkel.IDisco) | 418 implements(iwokkel.IDisco) |
391 | 419 |
392 def __init__(self, plugin_parent, profile): | 420 def __init__(self, plugin_parent, profile): |
393 self.plugin_parent = plugin_parent | 421 self.plugin_parent = plugin_parent |
394 self.host = plugin_parent.host | 422 self.host = plugin_parent.host |
395 self.profile = profile | 423 self.profile = profile |
396 | 424 |
397 def getDiscoInfo(self, requestor, target, nodeIdentifier=''): | 425 def getDiscoInfo(self, requestor, target, nodeIdentifier=""): |
398 return [disco.DiscoFeature(NS_CHAT_STATES)] | 426 return [disco.DiscoFeature(NS_CHAT_STATES)] |
399 | 427 |
400 def getDiscoItems(self, requestor, target, nodeIdentifier=''): | 428 def getDiscoItems(self, requestor, target, nodeIdentifier=""): |
401 return [] | 429 return [] |