Mercurial > moin > 1.9
changeset 2077:636e646f909b
Make jabber bot aware of contacts' presence.
It now doesn't disturb contacts that are dnd, and queues messages for later
delivery. This is a checkpoint commit.
author | Karol 'grzywacz' Nowak <grzywacz@sul.uni.lodz.pl> |
---|---|
date | Sat, 02 Jun 2007 16:54:13 +0200 |
parents | 0128bbaf0172 |
children | 804513b0e689 |
files | MoinMoin/events/__init__.py MoinMoin/jabber/xmppbot.py |
diffstat | 2 files changed, 187 insertions(+), 21 deletions(-) [+] |
line wrap: on
line diff
--- a/MoinMoin/events/__init__.py Fri Jun 01 21:18:01 2007 +0200 +++ b/MoinMoin/events/__init__.py Sat Jun 02 16:54:13 2007 +0200 @@ -73,7 +73,6 @@ handler = wikiutil.importPlugin(cfg, "events", name, "handle") except PluginAttributeError: handler = None - pass if handler is not None: event_handlers.append(handler)
--- a/MoinMoin/jabber/xmppbot.py Fri Jun 01 21:18:01 2007 +0200 +++ b/MoinMoin/jabber/xmppbot.py Sat Jun 02 16:54:13 2007 +0200 @@ -22,9 +22,85 @@ from xmlrpcbot import NotificationCommand +class Contact: + """Abstraction of a roster item / contact + + This class handles some logic related to keeping track of + contact availability, status, etc.""" + + def __init__(self, jid, resource, priority, show): + self.jid = jid + self.resources = [{'resource': resource, 'show': show, 'priority': priority}] + + # Queued messages, waiting for contact to change its "show" + # status to something different than "dnd". The messages should + # also be sent when contact becomes "unavailable" directly from + # "dnd", as we can't guarantee, that the bot will be up and running + # the next time she becomes "available". + self.messages = [] + + def add_resource(self, resource, show, priority): + self.resources.append( {'resource': resource, 'show': show, 'priority': priority} ) + + def remove_resource(self, resource): + for i in xrange(len(self.resources)): + if self.resources[i]['resource'] == resource: + del self.resources[i] + return + else: + raise ValueError("No such resource!") + + def is_dnd(self): + """Checks if contact is DoNotDisturb + + The contact is DND if its resource with the highest priority is DND + """ + + max_prio = self.resources[0]['priority'] + max_prio_show = self.resources[0]['show'] + + for res in self.resources: + # TODO: check RFC for behaviour when 2 resources have identical priority + if res['priority'] > max_prio: + max_prio = res['priority'] + max_prio_show = res['show'] + + if max_prio_show == u'dnd': + print "DND!!!!" + return True + else: + return False + + def set_show(self, resource, show): + """Sets show property for a given resource + + @param resource: resource to alter + @param show: new value of the show property + @raise ValueError: no resource with given name has been found + """ + for res in self.resources: + if res['resource'] == resource: + res['show'] = show + else: + raise ValueError("There's no such resource") + + def uses_resource(self, resource): + """Checks if contact uses a given resource""" + + for res in self.resources: + if res['resource'] == resource: return True + else: + return False + + def __str__(self): + retval = "%s (%s) has %d queued messages" + resources = ", ".join(r['resource'] + " is " + r['show'] for r in self.resources) + return retval % (self.jid.as_utf8(), resources, len(self.messages)) + + class XMPPBot(Client, Thread): """A simple XMPP bot""" - + def __init__(self, config, from_commands, to_commands): """A constructor. @@ -36,11 +112,15 @@ self.from_commands = from_commands self.to_commands = to_commands - jid = "%s@%s/%s" % (config.xmpp_node, config.xmpp_server, config.xmpp_resource) + jid = u"%s@%s/%s" % (config.xmpp_node, config.xmpp_server, config.xmpp_resource) self.config = config self.jid = JID(node_or_jid = jid, domain = config.xmpp_server, resource = config.xmpp_resource) self.tlsconfig = TLSSettings(require = True, verify_peer = False) + + # A dictionary of contact objects, ordered by bare JID + self.contacts = { } + Client.__init__(self, self.jid, self.config.xmpp_password, self.config.xmpp_server, tls_settings = self.tlsconfig) def run(self): @@ -56,32 +136,54 @@ """Main event loop - stream and command handling""" while 1: - stream=self.get_stream() + stream = self.get_stream() if not stream: break - act=stream.loop_iter(timeout) + + act = stream.loop_iter(timeout) if not act: - self.poll_commands() + # Process all available commands + while self.poll_commands(): pass self.idle() def poll_commands(self): - """Checks for new commands in the input queue and executes them""" + """Checks for new commands in the input queue and executes them + + @return: True if any command has been executed, False otherwise.""" try: command = self.to_commands.get_nowait() self.handle_command(command) + return True except Queue.Empty: - pass + return False def handle_command(self, command): """Excecutes commands from other components""" if isinstance(command, NotificationCommand): jid = JID(node_or_jid=command.jid) + jid_text = jid.bare().as_utf8() text = command.text + + # Check if contact is DoNotDisturb. If so, queue the message for delayed delivery + try: + contact = self.contacts[jid_text] + if contact.is_dnd(): + contact.messages.append(command) + return + except KeyError: + pass + self.send_message(jid, text) def send_message(self, to, text, type=u"chat"): + """Sends a message + + @param to: JID to send the message to + @param text: message's body + @param type: message type, as defined in RFC""" + message = Message(to_jid = to, body = text, stanza_type=type) self.get_stream().send(message) @@ -104,9 +206,81 @@ if not response == u"": self.send_message(sender, response) - def handle_presence(self): - pass + def handle_presence(self, stanza): + """Handles <presence /> stanzas""" + + if self.config.verbose: + self.log("Handling presence.") + + p = Presence(stanza) + show = p.get_show() + priority = p.get_priority() + type = p.get_stanza_type() + jid = p.get_from_jid() + bare_jid = jid.bare().as_utf8() + if type == u"unavailable": + # If we get presence, this contact should already be known + if bare_jid in self.contacts: + contact = self.contacts[bare_jid] + + if self.config.verbose: + self.log(contact + ", going OFFLINE.") + + try: + self.contacts.remove_resource(jid.resource) + if len(contact.resources) == 0: + # Send queued messages now, as we can't guarantee to be alive + # the next time this contact becomes available + self.send_queued_messages(contact) + del self.contacts[bare_jid] + else: + # The highest-priority resource, which used to be DnD might + # have gone offline. If so, try to deliver messages now. + if not contact.is_dnd(): + self.send_queued_messages(contact) + + except ValueError: + self.log("Weirdness. Unknown contact (resource) going offline...") + + else: + self.log("Unavailable presence from unknown contact? Weirdness.") + + elif type == u"available" or type is None: + if bare_jid in self.contacts: + contact = self.contacts[bare_jid] + + if self.config.verbose: + self.log(contact) + + # The resource is already known, so update it + if contact.uses_resource(bare_jid): + contact.set_show(bare_jid, show) + + # Unknown resource, add it to the list + else: + contact.add_resource(resource, show, priority) + + # Either way check, if we can deliver queued messages now + if not contact.is_dnd(): + self.send_queued_messages(contact) + + else: + self.contacts[bare_jid] = Contact(jid, jid.resource, priority, show) + + if self.config.verbose: + self.log(self.contacts[bare_jid]) + else: + # TODO: subscriptions and errors + print type + + return True + + def send_queued_messages(self, contact): + """Sends messages queued for the contact""" + for command in contact.messages: + self.handle_command(command) + def reply_help(self): """Constructs a generic help message @@ -114,16 +288,6 @@ return u"""Hello there! I'm a MoinMoin Notification Bot. Too bad I can't say anything more (yet!).""" - def check_availability(self, jid, type): - """Checks if a given contacts has its availability set to "type". - - Possible values of the "type" argument are: away, chat, dnd, xa""" - - if self.roster is None or jid not in self.roster: - return False - - # XXX: finish this! - def log(self, message): """Logs a message and its timestamp""" @@ -144,7 +308,8 @@ stream = self.get_stream() stream.set_message_handler("normal", self.handle_message) - stream.set_presence_handler(None, self.handle_presence, "jabber:client", 0) + stream.set_presence_handler("available", self.handle_presence) + stream.set_presence_handler("unavailable", self.handle_presence) self.request_session() # self.request_roster() @@ -170,6 +335,8 @@ if self.config.verbose: self.log("Updating roster.") + print "Groups:", self.roster.get_groups() + print "Contacts:", " ".join( [str(c) for c in self.roster.get_items()] ) # def session_started(self): # """Called when session has been successfully started"""