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 []