comparison src/core/sat_main.py @ 1052:e88bff4c8b77

core (XMPP): sendMessage refactoring: - better separation of message sending actions - use of more generic exceptions to hook the behaviour (SkipHistory and CancelError) - use of raise instead of return - use of failure.trap
author Goffi <goffi@goffi.org>
date Sat, 07 Jun 2014 16:35:29 +0200
parents 066308706dc6
children b2b9c184033f
comparison
equal deleted inserted replaced
1051:854880a31717 1052:e88bff4c8b77
50 50
51 def sat_next_id(): 51 def sat_next_id():
52 global sat_id 52 global sat_id
53 sat_id += 1 53 sat_id += 1
54 return "sat_id_" + str(sat_id) 54 return "sat_id_" + str(sat_id)
55
56
57 class MessageSentAndStored(Exception):
58 """ Exception to raise if the message has been already sent and stored in the
59 history by the trigger, so the rest of the process should be stopped. This
60 should normally be raised by the trigger with the minimal priority """
61 def __init__(self, reason, mess_data):
62 Exception.__init__(self, reason)
63 self.mess_data = mess_data # added for testing purpose
64
65
66 class AbortSendMessage(Exception):
67 """ Exception to raise if sending the message should be aborted. This can be
68 raised by any trigger but a side action should be planned by the trigger
69 to inform the user about what happened """
70 pass
71 55
72 56
73 class SAT(service.Service): 57 class SAT(service.Service):
74 58
75 @property 59 @property
431 if profile not in self.profiles: 415 if profile not in self.profiles:
432 return False 416 return False
433 return self.profiles[profile].isConnected() 417 return self.profiles[profile].isConnected()
434 418
435 419
436 ## jabber methods ## 420 ## XMPP methods ##
437 421
438 def getWaitingConf(self, profile_key=None): 422 def getWaitingConf(self, profile_key=None):
439 assert(profile_key) 423 assert(profile_key)
440 client = self.getClient(profile_key) 424 client = self.getClient(profile_key)
441 ret = [] 425 ret = []
442 for conf_id in client._waiting_conf: 426 for conf_id in client._waiting_conf:
443 conf_type, data = client._waiting_conf[conf_id][:2] 427 conf_type, data = client._waiting_conf[conf_id][:2]
444 ret.append((conf_id, conf_type, data)) 428 ret.append((conf_id, conf_type, data))
445 return ret 429 return ret
446 430
431 def generateMessageXML(self, mess_data):
432 mess_data['xml'] = domish.Element((None, 'message'))
433 mess_data['xml']["to"] = mess_data["to"].full()
434 mess_data['xml']["from"] = mess_data['from'].full()
435 mess_data['xml']["type"] = mess_data["type"]
436 mess_data['xml']['id'] = str(uuid4())
437 if mess_data["subject"]:
438 mess_data['xml'].addElement("subject", None, mess_data['subject'])
439 if mess_data["message"]: # message without body are used to send chat states
440 mess_data['xml'].addElement("body", None, mess_data["message"])
441 return mess_data
442
447 def _sendMessage(self, to_s, msg, subject=None, mess_type='auto', extra={}, profile_key=C.PROF_KEY_NONE): 443 def _sendMessage(self, to_s, msg, subject=None, mess_type='auto', extra={}, profile_key=C.PROF_KEY_NONE):
448 to_jid = jid.JID(to_s) 444 to_jid = jid.JID(to_s)
449 #XXX: we need to use the dictionary comprehension because D-Bus return its own types, and pickle can't manage them. TODO: Need to find a better way 445 #XXX: we need to use the dictionary comprehension because D-Bus return its own types, and pickle can't manage them. TODO: Need to find a better way
450 return self.sendMessage(to_jid, msg, subject, mess_type, {unicode(key): unicode(value) for key, value in extra.items()}, profile_key=profile_key) 446 return self.sendMessage(to_jid, msg, subject, mess_type, {unicode(key): unicode(value) for key, value in extra.items()}, profile_key=profile_key)
451 447
452 def sendMessage(self, to_jid, msg, subject=None, mess_type='auto', extra={}, no_trigger=False, profile_key=C.PROF_KEY_NONE): 448 def sendMessage(self, to_jid, msg, subject=None, mess_type='auto', extra={}, no_trigger=False, profile_key=C.PROF_KEY_NONE):
453 #FIXME: check validity of recipient 449 #FIXME: check validity of recipient
454 profile = self.memory.getProfileName(profile_key) 450 profile = self.memory.getProfileName(profile_key)
455 assert(profile) 451 assert(profile)
456 client = self.profiles[profile] 452 client = self.profiles[profile]
457 current_jid = client.jid
458 if extra is None: 453 if extra is None:
459 extra = {} 454 extra = {}
460 mess_data = { # we put data in a dict, so trigger methods can change them 455 mess_data = { # we put data in a dict, so trigger methods can change them
461 "to": to_jid, 456 "to": to_jid,
457 "from": client.jid,
462 "message": msg, 458 "message": msg,
463 "subject": subject, 459 "subject": subject,
464 "type": mess_type, 460 "type": mess_type,
465 "extra": extra, 461 "extra": extra,
466 } 462 }
491 if not self.trigger.point("sendMessage", mess_data, pre_xml_treatments, post_xml_treatments, profile): 487 if not self.trigger.point("sendMessage", mess_data, pre_xml_treatments, post_xml_treatments, profile):
492 return defer.succeed(None) 488 return defer.succeed(None)
493 489
494 log.debug(_("Sending jabber message of type [%(type)s] to %(to)s...") % {"type": mess_data["type"], "to": to_jid.full()}) 490 log.debug(_("Sending jabber message of type [%(type)s] to %(to)s...") % {"type": mess_data["type"], "to": to_jid.full()})
495 491
496 def generateXML(mess_data): 492 def cancelErrorTrap(failure):
497 mess_data['xml'] = domish.Element((None, 'message')) 493 """A message sending can be cancelled by a plugin treatment"""
498 mess_data['xml']["to"] = mess_data["to"].full() 494 failure.trap(exceptions.CancelError)
499 mess_data['xml']["from"] = current_jid.full() 495
500 mess_data['xml']["type"] = mess_data["type"] 496 pre_xml_treatments.addCallback(lambda dummy: self.generateMessageXML(mess_data))
501 if mess_data["subject"]:
502 mess_data['xml'].addElement("subject", None, subject)
503 # message without body are used to send chat states
504 if mess_data["message"]:
505 mess_data['xml'].addElement("body", None, mess_data["message"])
506 return mess_data
507
508 def sendErrback(e):
509 text = '%s: %s' % (e.value.__class__.__name__, e.getErrorMessage())
510 if e.check(MessageSentAndStored):
511 log.debug(text)
512 elif e.check(AbortSendMessage):
513 log.warning(text)
514 return e
515 else:
516 log.error("Unmanaged exception: %s" % text)
517 return e
518 pre_xml_treatments.addCallback(generateXML)
519 pre_xml_treatments.chainDeferred(post_xml_treatments) 497 pre_xml_treatments.chainDeferred(post_xml_treatments)
520 post_xml_treatments.addCallback(self.sendAndStoreMessage, False, profile) 498 post_xml_treatments.addCallback(self._sendMessageToStream, client)
521 post_xml_treatments.addErrback(sendErrback) 499 post_xml_treatments.addCallback(self._storeMessage, client)
500 post_xml_treatments.addCallback(self.sendMessageToBridge, client)
501 post_xml_treatments.addErrback(cancelErrorTrap)
522 pre_xml_treatments.callback(mess_data) 502 pre_xml_treatments.callback(mess_data)
523 return pre_xml_treatments 503 return pre_xml_treatments
524 504
525 def sendAndStoreMessage(self, mess_data, skip_send=False, profile=None): 505 def _sendMessageToStream(self, mess_data, client):
526 """Actually send and store the message to history, after all the treatments have been done. 506 """Actualy send the message to the server
527 This has been moved outside the main sendMessage method because it is used by XEP-0033 to complete a server-side feature not yet 507
528 implemented by the prosody plugin. 508 @param mess_data: message data dictionnary
529 @param mess_data: message data dictionary 509 @param client: profile's client
530 @param skip_send: set to True to skip sending the message to only store it 510 """
531 @param profile: profile 511 client.xmlstream.send(mess_data['xml'])
532 """ 512 return mess_data
533 try: 513
534 client = self.profiles[profile] 514 def _storeMessage(self, mess_data, client):
535 except KeyError: 515 """Store message into database (for local history)
536 log.error(_("Trying to send a message with no profile")) 516
537 return 517 @param mess_data: message data dictionnary
538 current_jid = client.jid 518 @param client: profile's client
539 if not skip_send: 519 """
540 client.xmlstream.send(mess_data['xml'])
541 if mess_data["type"] != "groupchat": 520 if mess_data["type"] != "groupchat":
542 # we don't add groupchat message to history, as we get them back 521 # we don't add groupchat message to history, as we get them back
543 # and they will be added then 522 # and they will be added then
544 if mess_data['message']: # we need a message to save something 523 if mess_data['message']: # we need a message to save something
545 self.memory.addToHistory(current_jid, mess_data['to'], 524 self.memory.addToHistory(client.jid, mess_data['to'],
546 unicode(mess_data["message"]), 525 unicode(mess_data["message"]),
547 unicode(mess_data["type"]), 526 unicode(mess_data["type"]),
548 mess_data['extra'], 527 mess_data['extra'],
549 profile=profile) 528 profile=client.profile)
529 else:
530 log.warning(_("No message found")) # empty body should be managed by plugins before this point
531 return mess_data
532
533 def sendMessageToBridge(self, mess_data, client):
534 """Send message to bridge, so frontends can display it
535
536 @param mess_data: message data dictionnary
537 @param client: profile's client
538 """
539 if mess_data["type"] != "groupchat":
540 # we don't send groupchat message back to bridge, as we get them back
541 # and they will be added the
542 if mess_data['message']: # we need a message to save something
550 # We send back the message, so all clients are aware of it 543 # We send back the message, so all clients are aware of it
551 self.bridge.newMessage(mess_data['xml']['from'], 544 self.bridge.newMessage(mess_data['from'].full(),
552 unicode(mess_data["message"]), 545 unicode(mess_data["message"]),
553 mess_type=mess_data["type"], 546 mess_type=mess_data["type"],
554 to_jid=mess_data['xml']['to'], 547 to_jid=mess_data['to'].full(),
555 extra=mess_data['extra'], 548 extra=mess_data['extra'],
556 profile=profile) 549 profile=client.profile)
550 else:
551 log.warning(_("No message found"))
552 return mess_data
557 553
558 def _setPresence(self, to="", show="", statuses=None, profile_key=C.PROF_KEY_NONE): 554 def _setPresence(self, to="", show="", statuses=None, profile_key=C.PROF_KEY_NONE):
559 return self.setPresence(jid.JID(to) if to else None, show, statuses, profile_key) 555 return self.setPresence(jid.JID(to) if to else None, show, statuses, profile_key)
560 556
561 def setPresence(self, to_jid=None, show="", statuses=None, profile_key=C.PROF_KEY_NONE): 557 def setPresence(self, to_jid=None, show="", statuses=None, profile_key=C.PROF_KEY_NONE):