comparison src/core/sat_main.py @ 742:03744d9ebc13

plugin XEP-0033: implementation of the addressing feature: - frontends pass the recipients in the extra parameter of sendMessage - backend checks if the target server supports the feature (this is not done yet by prosody plugin) - features and identities are cached per profile and server - messages are duplicated in history for now (TODO: redesign the database) - echos signals are also duplicated to the sender (FIXME)
author souliane <souliane@mailoo.org>
date Wed, 11 Dec 2013 17:16:53 +0100
parents e07afabc4a25
children 5aff0beddb28
comparison
equal deleted inserted replaced
741:00318e60a06a 742:03744d9ebc13
58 58
59 def sat_next_id(): 59 def sat_next_id():
60 global sat_id 60 global sat_id
61 sat_id += 1 61 sat_id += 1
62 return "sat_id_" + str(sat_id) 62 return "sat_id_" + str(sat_id)
63
64
65 class MessageSentAndStored(Exception):
66 """ Exception to raise if the message has been already sent and stored in the
67 history by the trigger, so the rest of the process should be stopped. This
68 should normally be raised by the trigger with the minimal priority """
69 pass
70
71
72 class AbortSendMessage(Exception):
73 """ Exception to raise if sending the message should be aborted. This can be
74 raised by any trigger but a side action should be planned by the trigger
75 to inform the user about what happened """
76 pass
63 77
64 78
65 class SAT(service.Service): 79 class SAT(service.Service):
66 80
67 def get_next_id(self): 81 def get_next_id(self):
352 return self.profiles.values() 366 return self.profiles.values()
353 if profile.count('@') > 1: 367 if profile.count('@') > 1:
354 raise exceptions.ProfileKeyUnknownError 368 raise exceptions.ProfileKeyUnknownError
355 return [self.profiles[profile]] 369 return [self.profiles[profile]]
356 370
371 def getClientHostJid(self, profile_key):
372 """Convenient method to get the client host from profile key
373 @return: host jid or None if it doesn't exist"""
374 profile = self.memory.getProfileName(profile_key)
375 if not profile:
376 return None
377 return self.profiles[profile].getHostJid()
378
357 def registerNewAccount(self, login, password, email, server, port=5222, id=None, profile_key='@DEFAULT@'): 379 def registerNewAccount(self, login, password, email, server, port=5222, id=None, profile_key='@DEFAULT@'):
358 """Connect to a server and create a new account using in-band registration""" 380 """Connect to a server and create a new account using in-band registration"""
359 profile = self.memory.getProfileName(profile_key) 381 profile = self.memory.getProfileName(profile_key)
360 assert(profile) 382 assert(profile)
361 383
539 mess_data['xml'].addElement("subject", None, subject) 561 mess_data['xml'].addElement("subject", None, subject)
540 # message without body are used to send chat states 562 # message without body are used to send chat states
541 if mess_data["message"]: 563 if mess_data["message"]:
542 mess_data['xml'].addElement("body", None, mess_data["message"]) 564 mess_data['xml'].addElement("body", None, mess_data["message"])
543 565
544 def sendAndStore(mess_data): 566 def sendErrback(e):
567 text = '%s: %s' % (e.value.__class__.__name__, e.getErrorMessage())
568 if e.check(MessageSentAndStored):
569 debug(text)
570 elif e.check(AbortSendMessage):
571 warning(text)
572 else:
573 error("Unmanaged exception: %s" % text)
574 return e
575
576 treatments.addCallbacks(self.sendAndStoreMessage, sendErrback, [False, profile])
577 treatments.callback(mess_data)
578
579 def sendAndStoreMessage(self, mess_data, skip_send=False, profile=None):
580 """Actually send and store the message to history, after all the treatments
581 have been done. This has been moved outside the main sendMessage method
582 because it is used by XEP-0033 to complete a server-side feature not yet
583 implemented by the prosody plugin.
584 @param mess_data: message data dictionary
585 @param skip_send: set to True to skip sending the message to only store it
586 @param profile: profile
587 """
588 try:
589 client = self.profiles[profile]
590 except KeyError:
591 error(_("Trying to send a message with no profile"))
592 return
593 current_jid = client.jid
594 if not skip_send:
545 client.xmlstream.send(mess_data['xml']) 595 client.xmlstream.send(mess_data['xml'])
546 if mess_data["type"] != "groupchat": 596 if mess_data["type"] != "groupchat":
547 # we don't add groupchat message to history, as we get them back 597 # we don't add groupchat message to history, as we get them back
548 # and they will be added then 598 # and they will be added then
549 if mess_data['message']: # we need a message to save something 599 if mess_data['message']: # we need a message to save something
550 self.memory.addToHistory(current_jid, mess_data['to'], 600 self.memory.addToHistory(current_jid, mess_data['to'],
551 unicode(mess_data["message"]), 601 unicode(mess_data["message"]),
552 unicode(mess_data["type"]), 602 unicode(mess_data["type"]),
553 mess_data['extra'], 603 mess_data['extra'],
554 profile=profile) 604 profile=profile)
555 # We send back the message, so all clients are aware of it 605 # We send back the message, so all clients are aware of it
556 if mess_data["message"]: 606 self.bridge.newMessage(mess_data['xml']['from'],
557 self.bridge.newMessage(mess_data['xml']['from'], 607 unicode(mess_data["message"]),
558 unicode(mess_data["message"]), 608 mess_type=mess_data["type"],
559 mess_type=mess_data["type"], 609 to_jid=mess_data['xml']['to'],
560 to_jid=mess_data['xml']['to'], extra=mess_data['extra'], 610 extra=mess_data['extra'],
561 profile=profile) 611 profile=profile)
562
563 treatments.addCallback(sendAndStore)
564 treatments.callback(mess_data)
565 612
566 def setPresence(self, to="", show="", priority=0, statuses=None, profile_key='@NONE@'): 613 def setPresence(self, to="", show="", priority=0, statuses=None, profile_key='@NONE@'):
567 """Send our presence information""" 614 """Send our presence information"""
568 if statuses is None: 615 if statuses is None:
569 statuses = {} 616 statuses = {}
621 assert(profile) 668 assert(profile)
622 to_jid = jid.JID(to) 669 to_jid = jid.JID(to)
623 self.profiles[profile].roster.removeItem(to_jid) 670 self.profiles[profile].roster.removeItem(to_jid)
624 self.profiles[profile].presence.unsubscribe(to_jid) 671 self.profiles[profile].presence.unsubscribe(to_jid)
625 672
673 def requestServerDisco(self, feature, jid_=None, cache_only=False, profile_key="@NONE"):
674 """Discover if a server or its items offer a given feature
675 @param feature: the feature to check
676 @param jid_: the jid of the server
677 @param cache_only: expect the result to be in cache and don't actually
678 make any request to avoid returning a Deferred. This can be used anytime
679 for requesting the local server because the data are cached for sure.
680 @result: the Deferred entity jid offering the feature, or None
681 """
682 profile = self.memory.getProfileName(profile_key)
683
684 if not profile:
685 return defer.succeed(None)
686 if jid_ is None:
687 jid_ = self.getClientHostJid(profile)
688 cache_only = True
689 hasServerFeature = lambda entity: entity if self.memory.hasServerFeature(feature, entity, profile) else None
690
691 def haveItemsFeature(dummy=None):
692 if jid_ in self.memory.server_identities[profile]:
693 for entity in self.memory.server_identities[profile][jid_].values():
694 if hasServerFeature(entity):
695 return entity
696 return None
697
698 entity = hasServerFeature(jid_) or haveItemsFeature()
699 if entity:
700 return defer.succeed(entity)
701 elif entity is False or cache_only:
702 return defer.succeed(None)
703
704 # data for this server are not in cache
705 disco = self.profiles[profile].disco
706
707 def errback(failure, method, jid_, profile):
708 # the target server is not reachable
709 logging.error("disco.%s on %s failed! [%s]" % (method.func_name, jid_, profile))
710 logging.error("reason: %s" % failure.getErrorMessage())
711 if method == disco.requestInfo:
712 features = self.memory.server_features.setdefault(profile, {})
713 features.setdefault(jid_, [])
714 elif method == disco.requestItems:
715 identities = self.memory.server_identities.setdefault(profile, {})
716 identities.setdefault(jid_, {})
717 return failure
718
719 def callback(d):
720 if hasServerFeature(jid_):
721 return jid_
722 else:
723 d2 = disco.requestItems(jid_).addCallback(self.serverDiscoItems, disco, jid_, profile)
724 d2.addErrback(errback, disco.requestItems, jid_, profile)
725 return d2.addCallback(haveItemsFeature)
726
727 d = disco.requestInfo(jid_).addCallback(self.serverDisco, jid_, profile)
728 d.addCallbacks(callback, errback, [], [disco.requestInfo, jid_, profile])
729 return d
730
626 ## callbacks ## 731 ## callbacks ##
627 732
628 def serverDisco(self, disco, profile): 733 def serverDisco(self, disco, jid_=None, profile=None):
629 """xep-0030 Discovery Protocol.""" 734 """xep-0030 Discovery Protocol.
735 @param disco: result of the disco info query
736 @param jid_: the jid of the target server
737 @param profile: profile of the user
738 """
739 if jid_ is None:
740 jid_ = self.getClientHostJid(profile)
741 debug(_("Requested disco info on %s") % jid_)
630 for feature in disco.features: 742 for feature in disco.features:
631 debug(_("Feature found: %s"), feature) 743 debug(_("Feature found: %s") % feature)
632 self.memory.addServerFeature(feature, profile) 744 self.memory.addServerFeature(feature, jid_, profile)
633 for cat, type in disco.identities: 745 for cat, type_ in disco.identities:
634 debug(_("Identity found: [%(category)s/%(type)s] %(identity)s") % {'category': cat, 'type': type, 'identity': disco.identities[(cat, type)]}) 746 debug(_("Identity found: [%(category)s/%(type)s] %(identity)s")
635 747 % {'category': cat, 'type': type_, 'identity': disco.identities[(cat, type_)]})
636 def serverDiscoItems(self, disco_result, disco_client, profile, initialized): 748
749 def serverDiscoItems(self, disco_result, disco_client, jid_, profile, initialized=None):
637 """xep-0030 Discovery Protocol. 750 """xep-0030 Discovery Protocol.
638 @param disco_result: result of the disco item querry 751 @param disco_result: result of the disco item querry
639 @param disco_client: SatDiscoProtocol instance 752 @param disco_client: SatDiscoProtocol instance
753 @param jid_: the jid of the target server
640 @param profile: profile of the user 754 @param profile: profile of the user
641 @param initialized: deferred which must be chained when everything is done""" 755 @param initialized: deferred which must be chained when everything is done"""
642 756
643 def _check_entity_cb(result, entity, profile): 757 def _check_entity_cb(result, entity, jid_, profile):
644 for category, type in result.identities: 758 debug(_("Requested disco info on %s") % entity)
645 debug(_('Identity added: (%(category)s,%(type)s) ==> %(entity)s [%(profile)s]') % { 759 for category, type_ in result.identities:
646 'category': category, 'type': type, 'entity': entity, 'profile': profile}) 760 debug(_('Identity added: (%(category)s,%(type)s) ==> %(entity)s [%(profile)s]')
647 self.memory.addServerIdentity(category, type, entity, profile) 761 % {'category': category, 'type': type_, 'entity': entity, 'profile': profile})
648 762 self.memory.addServerIdentity(category, type_, entity, jid_, profile)
649 def _errback(result, entity, profile): 763 for feature in result.features:
764 self.memory.addServerFeature(feature, entity, profile)
765
766 def _errback(result, entity, jid_, profile):
650 warning(_("Can't get information on identity [%(entity)s] for profile [%(profile)s]") % {'entity': entity, 'profile': profile}) 767 warning(_("Can't get information on identity [%(entity)s] for profile [%(profile)s]") % {'entity': entity, 'profile': profile})
651 768
652 defer_list = [] 769 defer_list = []
653 for item in disco_result._items: 770 for item in disco_result._items:
654 if item.entity.full().count('.') == 1: # XXX: workaround for a bug on jabberfr, tmp 771 if item.entity.full().count('.') == 1: # XXX: workaround for a bug on jabberfr, tmp
655 warning(_('Using jabberfr workaround, be sure your domain has at least two levels (e.g. "example.tld", not "example" alone)')) 772 warning(_('Using jabberfr workaround, be sure your domain has at least two levels (e.g. "example.tld", not "example" alone)'))
656 continue 773 continue
657 args = [item.entity, profile] 774 args = [item.entity, jid_, profile]
658 defer_list.append(disco_client.requestInfo(item.entity).addCallbacks(_check_entity_cb, _errback, args, None, args)) 775 defer_list.append(disco_client.requestInfo(item.entity).addCallbacks(_check_entity_cb, _errback, args, None, args))
659 defer.DeferredList(defer_list).chainDeferred(initialized) 776 if initialized:
777 defer.DeferredList(defer_list).chainDeferred(initialized)
778
660 ## Generic HMI ## 779 ## Generic HMI ##
661 780
662 def actionResult(self, action_id, action_type, data, profile): 781 def actionResult(self, action_id, action_type, data, profile):
663 """Send the result of an action 782 """Send the result of an action
664 @param action_id: same action_id used with action 783 @param action_id: same action_id used with action