Mercurial > libervia-backend
view src/plugins/plugin_xep_0085.py @ 637:3b02554d4c8b
primitivus: chat state implementation
for now chat states are displayed in primitivus surrended text (after the contact's name)
author | souliane <souliane@mailoo.org> |
---|---|
date | Sun, 08 Sep 2013 19:13:02 +0200 |
parents | 7ea6d5a86e58 |
children | 262d9d9ad27a |
line wrap: on
line source
#!/usr/bin/python # -*- coding: utf-8 -*- # SAT plugin for Chat State Notifications Protocol (xep-0085) # Copyright (C) 2009, 2010, 2011, 2012, 2013 Adrien Cossa (souliane@mailoo.org) # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero General Public License for more details. # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. from sat.core import exceptions from logging import debug, info, error from wokkel import disco, iwokkel from zope.interface import implements from twisted.words.protocols.jabber.jid import JID try: from twisted.words.protocols.xmlstream import XMPPHandler except ImportError: from wokkel.subprotocols import XMPPHandler from threading import Timer from twisted.words.xish.domish import generateElementsNamed NS_XMPP_CLIENT = "jabber:client" NS_CHAT_STATES = "http://jabber.org/protocol/chatstates" CHAT_STATES = ["active", "inactive", "gone", "composing", "paused"] MESSAGE_TYPES = ["chat", "groupchat"] PARAM_KEY = "Chat State Notifications" PARAM_NAME = "Enabled" PLUGIN_INFO = { "name": "Chat State Notifications Protocol Plugin", "import_name": "XEP-0085", "type": "XEP", "protocols": ["XEP-0085"], "dependencies": [], "main": "XEP_0085", "handler": "yes", "description": _("""Implementation of Chat State Notifications Protocol""") } # Describe the internal transitions that are triggered # by a timer. Beside that, external transitions can be # runned to target the states "active" or "composing". # Delay is specified here in seconds. TRANSITIONS = { "active": {"next_state": "inactive", "delay": 120}, "inactive": {"next_state": "gone", "delay": 480}, "gone": {"next_state": "", "delay": 0}, "composing": {"next_state": "paused", "delay": 30}, "paused": {"next_state": "inactive", "delay": 450} } class UnknownChatStateException(Exception): """ This error is raised when an unknown chat state is used. """ pass class XEP_0085(object): """ Implementation for XEP 0085 """ params = """ <params> <individual> <category name="%(category_name)s" label="%(category_label)s"> <param name="%(param_name)s" label="%(param_label)s" value="true" type="bool" security="0"/> </category> </individual> </params> """ % { 'category_name': PARAM_KEY, 'category_label': _(PARAM_KEY), 'param_name': PARAM_NAME, 'param_label': _('Enable chat state notifications') } def __init__(self, host): info(_("CSN plugin initialization")) self.host = host # parameter value is retrieved before each use host.memory.importParams(self.params) # triggers from core host.trigger.add("MessageReceived", self.messageReceivedTrigger) host.trigger.add("sendMessageXml", self.sendMessageXmlTrigger) host.trigger.add("paramUpdateTrigger", self.paramUpdateTrigger) #TODO: handle profile disconnexion (free memory in entity data) # args: to_s (jid as string), profile host.bridge.addMethod("chatStateComposing", ".plugin", in_sign='ss', out_sign='', method=self.chatStateComposing) # args: from (jid as string), state in CHAT_STATES, profile host.bridge.addSignal("chatStateReceived", ".plugin", signature='sss') def getHandler(self, profile): return XEP_0085_handler(self, profile) def updateEntityData(self, entity_jid, value, profile): """ Update the entity data and reset the chat state display if the notification has been disabled. Parameter "entity_jid" could be @ALL@ to update all entities. """ self.host.memory.updateEntityData(entity_jid, PARAM_KEY, value, profile) if not value or value == "@NONE@": # disable chat state for this or these contact(s) self.host.bridge.chatStateReceived(unicode(entity_jid), "", profile) def paramUpdateTrigger(self, name, value, category, type, profile): """ Reset all the existing chat state entity data associated with this profile after a parameter modification (value different then "true" would delete the entity data). """ if (category, name) == (PARAM_KEY, PARAM_NAME): self.updateEntityData("@ALL@", True if value == "true" else "@NONE@", profile) def messageReceivedTrigger(self, message, profile): """ Update the entity cache when we receive a message with body. Check for a check state in the incoming message and broadcast signal. """ if not self.host.memory.getParamA(PARAM_NAME, PARAM_KEY, profile_key=profile): return True try: generateElementsNamed(message.children, name="body").next() from_jid = JID(message.getAttribute("from")) try: generateElementsNamed(message.children, name="active").next() # contact enabled Chat State Notifications self.updateEntityData(from_jid, True, profile) # init to send following "composing" state self.__chatStateInit(from_jid, message.getAttribute("type"), profile) except StopIteration: # contact didn't enable Chat State Notifications self.updateEntityData(from_jid, False, profile) except StopIteration: pass state_list = [child.name for child in message.children if message.getAttribute("type") in MESSAGE_TYPES and child.name in CHAT_STATES and child.defaultUri == NS_CHAT_STATES] for state in state_list: # there must be only one state according to the XEP self.host.bridge.chatStateReceived(message.getAttribute("from"), state, profile) break return True def sendMessageXmlTrigger(self, message, mess_data, profile): """ Eventually add the chat state to the message and initiate the state machine when sending an "active" state. """ if not self.host.memory.getParamA(PARAM_NAME, PARAM_KEY, profile_key=profile): return True # check if notifications should be sent to this contact contact_enabled = True to_jid = JID(message.getAttribute("to")) try: contact_enabled = self.host.memory.getEntityData( to_jid, [PARAM_KEY], profile)[PARAM_KEY] except (exceptions.UnknownEntityError, KeyError): # enable it for the first time self.updateEntityData(to_jid, True, profile) if not contact_enabled: return True try: # message with a body always mean active state generateElementsNamed(message.children, name="body").next() message.addElement('active', NS_CHAT_STATES) # launch the chat state machine (init the timer) self.__chatStateActive(to_jid, mess_data["type"], profile) except StopIteration: if "chat_state" in mess_data["options"]: state = mess_data["options"]["chat_state"] assert(state in CHAT_STATES) message.addElement(state, NS_CHAT_STATES) return True def __chatStateInit(self, to_jid, mess_type, profile): """ Data initialization for the chat state machine. """ # TODO: use also the resource in map key to_jid = to_jid.userhostJID() if mess_type is None: return if not hasattr(self, "map"): self.map = {} profile_map = self.map.setdefault(profile, {}) if not to_jid in profile_map: machine = ChatStateMachine(self.host, to_jid, mess_type, profile) self.map[profile][to_jid] = machine def __chatStateActive(self, to_jid, mess_type, profile_key): """ Launch the chat state machine on "active" state. """ # TODO: use also the resource in map key to_jid = to_jid.userhostJID() profile = self.host.memory.getProfileName(profile_key) if profile is None: raise exceptions.ProfileUnknownError self.__chatStateInit(to_jid, mess_type, profile) self.map[profile][to_jid]._onEvent("active") def chatStateComposing(self, to_jid_s, profile_key): """ Move to the "composing" state. """ # TODO: use also the resource in map key to_jid = JID(to_jid_s).userhostJID() profile = self.host.memory.getProfileName(profile_key) if profile is None: raise exceptions.ProfileUnknownError try: self.map[profile][to_jid]._onEvent("composing") except: return class ChatStateMachine: """ This class represents a chat state, between one profile and one target contact. A timer is used to move from one state to the other. The initialization is done through the "active" state which is internally set when a message is sent. The state "composing" can be set externally (through the bridge by a frontend). Other states are automatically set with the timer. """ def __init__(self, host, to_jid, mess_type, profile): """ Initialization need to store the target, message type and a profile for sending later messages. """ self.host = host self.to_jid = to_jid self.mess_type = mess_type self.profile = profile self.state = None self.timer = None def _onEvent(self, state): """ Move to the specified state, eventually send the notification to the contact (the "active" state is automatically sent with each message) and set the timer. """ if state != self.state and state != "active": # send a new message without body self.host.sendMessage(self.to_jid, '', '', self.mess_type, options={"chat_state": state}, profile_key=self.profile) self.state = state if not self.timer is None: self.timer.cancel() if not state in TRANSITIONS: return if not "next_state" in TRANSITIONS[state]: return if not "delay" in TRANSITIONS[state]: return next_state = TRANSITIONS[state]["next_state"] delay = TRANSITIONS[state]["delay"] if next_state == "" or delay < 0: return self.timer = Timer(delay, self._onEvent, [next_state]) self.timer.start() class XEP_0085_handler(XMPPHandler): implements(iwokkel.IDisco) def __init__(self, plugin_parent, profile): self.plugin_parent = plugin_parent self.host = plugin_parent.host self.profile = profile def getDiscoInfo(self, requestor, target, nodeIdentifier=''): return [disco.DiscoFeature(NS_CHAT_STATES)] def getDiscoItems(self, requestor, target, nodeIdentifier=''): return []