comparison src/plugins/plugin_xep_0085.py @ 659:b6c22d9f593a

plugin xep-0085: bug fix + improvement
author souliane <souliane@mailoo.org>
date Mon, 07 Oct 2013 13:09:57 +0200
parents 5c5cf5bca240
children 69a8bfd266a5
comparison
equal deleted inserted replaced
658:e26134122ed7 659:b6c22d9f593a
16 16
17 # You should have received a copy of the GNU Affero General Public License 17 # You should have received a copy of the GNU Affero General Public License
18 # along with this program. If not, see <http://www.gnu.org/licenses/>. 18 # along with this program. If not, see <http://www.gnu.org/licenses/>.
19 19
20 from sat.core import exceptions 20 from sat.core import exceptions
21 from logging import debug, info, error 21 from logging import info
22 from wokkel import disco, iwokkel 22 from wokkel import disco, iwokkel
23 from zope.interface import implements 23 from zope.interface import implements
24 from twisted.words.protocols.jabber.jid import JID 24 from twisted.words.protocols.jabber.jid import JID
25 try: 25 try:
26 from twisted.words.protocols.xmlstream import XMPPHandler 26 from twisted.words.protocols.xmlstream import XMPPHandler
87 'param_name': PARAM_NAME, 87 'param_name': PARAM_NAME,
88 'param_label': _('Enable chat state notifications') 88 'param_label': _('Enable chat state notifications')
89 } 89 }
90 90
91 def __init__(self, host): 91 def __init__(self, host):
92 info(_("CSN plugin initialization")) 92 info(_("Chat State Notifications plugin initialization"))
93 self.host = host 93 self.host = host
94 94
95 # parameter value is retrieved before each use 95 # parameter value is retrieved before each use
96 host.memory.importParams(self.params) 96 host.memory.importParams(self.params)
97 97
98 # triggers from core 98 # triggers from core
99 host.trigger.add("MessageReceived", self.messageReceivedTrigger) 99 host.trigger.add("MessageReceived", self.messageReceivedTrigger)
100 host.trigger.add("sendMessageXml", self.sendMessageXmlTrigger) 100 host.trigger.add("sendMessageXml", self.sendMessageXmlTrigger)
101 host.trigger.add("paramUpdateTrigger", self.paramUpdateTrigger) 101 host.trigger.add("paramUpdateTrigger", self.paramUpdateTrigger)
102 # TODO: handle profile disconnexion (free memory in entity data) 102 # TODO: handle profile disconnection (free memory in entity data)
103 103
104 # args: to_s (jid as string), profile 104 # args: to_s (jid as string), profile
105 host.bridge.addMethod("chatStateComposing", ".plugin", in_sign='ss', 105 host.bridge.addMethod("chatStateComposing", ".plugin", in_sign='ss',
106 out_sign='', method=self.chatStateComposing) 106 out_sign='', method=self.chatStateComposing)
107 107
111 def getHandler(self, profile): 111 def getHandler(self, profile):
112 return XEP_0085_handler(self, profile) 112 return XEP_0085_handler(self, profile)
113 113
114 def updateEntityData(self, entity_jid, value, profile): 114 def updateEntityData(self, entity_jid, value, profile):
115 """ 115 """
116 Update the entity data and reset the chat state display 116 Update the entity data of the given profile for one or all contacts.
117 if the notification has been disabled. Parameter "entity_jid" 117 Reset the chat state(s) display if the notification has been disabled.
118 could be @ALL@ to update all entities. 118 @param entity_jid: contact's JID, or '@ALL@' to update all contacts.
119 @param value: True, False or '@NONE@' to delete the entity data
120 @param profile: current profile
119 """ 121 """
120 self.host.memory.updateEntityData(entity_jid, ENTITY_KEY, value, profile) 122 self.host.memory.updateEntityData(entity_jid, ENTITY_KEY, value, profile)
121 if not value or value == "@NONE@": 123 if not value or value == "@NONE@":
122 # disable chat state for this or these contact(s) 124 # disable chat state for this or these contact(s)
123 self.host.bridge.chatStateReceived(unicode(entity_jid), "", profile) 125 self.host.bridge.chatStateReceived(unicode(entity_jid), "", profile)
124 126
125 def paramUpdateTrigger(self, name, value, category, type, profile): 127 def paramUpdateTrigger(self, name, value, category, type, profile):
126 """ 128 """
127 Reset all the existing chat state entity data associated 129 Reset all the existing chat state entity data associated with this profile after a parameter modification.
128 with this profile after a parameter modification (value 130 @param name: parameter name
129 different then "true" would delete the entity data). 131 @param value: "true" to activate the notifications, or any other value to delete it
132 @param category: parameter category
130 """ 133 """
131 if (category, name) == (PARAM_KEY, PARAM_NAME): 134 if (category, name) == (PARAM_KEY, PARAM_NAME):
132 self.updateEntityData("@ALL@", True if value == "true" else "@NONE@", profile) 135 self.updateEntityData("@ALL@", True if value == "true" else "@NONE@", profile)
133 136
134 def messageReceivedTrigger(self, message, profile): 137 def messageReceivedTrigger(self, message, profile):
149 # init to send following "composing" state 152 # init to send following "composing" state
150 self.__chatStateInit(from_jid, message.getAttribute("type"), profile) 153 self.__chatStateInit(from_jid, message.getAttribute("type"), profile)
151 except StopIteration: 154 except StopIteration:
152 # contact didn't enable Chat State Notifications 155 # contact didn't enable Chat State Notifications
153 self.updateEntityData(from_jid, False, profile) 156 self.updateEntityData(from_jid, False, profile)
157 return True
154 except StopIteration: 158 except StopIteration:
155 pass 159 pass
156 160
157 state_list = [child.name for child in message.children if 161 state_list = [child.name for child in message.children if
158 message.getAttribute("type") in MESSAGE_TYPES 162 message.getAttribute("type") in MESSAGE_TYPES
159 and child.name in CHAT_STATES 163 and child.name in CHAT_STATES
160 and child.defaultUri == NS_CHAT_STATES] 164 and child.defaultUri == NS_CHAT_STATES]
161 for state in state_list: 165 for state in state_list:
162 # there must be only one state according to the XEP 166 # there must be only one state according to the XEP
163 self.host.bridge.chatStateReceived(message.getAttribute("from"), 167 self.host.bridge.chatStateReceived(message.getAttribute("from"), state, profile)
164 state, profile)
165 break 168 break
166 return True 169 return True
167 170
168 def sendMessageXmlTrigger(self, message, mess_data, profile): 171 def sendMessageXmlTrigger(self, message, mess_data, profile):
169 """ 172 """
170 Eventually add the chat state to the message and initiate 173 Eventually add the chat state to the message and initiate
171 the state machine when sending an "active" state. 174 the state machine when sending an "active" state.
172 """ 175 """
173 if not self.host.memory.getParamA(PARAM_NAME, PARAM_KEY, profile_key=profile):
174 return True
175
176 # check if notifications should be sent to this contact
177 contact_enabled = True
178 to_jid = JID(message.getAttribute("to")) 176 to_jid = JID(message.getAttribute("to"))
179 try: 177 if not self.__checkActivation(to_jid, forceEntityData=True, profile=profile):
180 contact_enabled = self.host.memory.getEntityData(
181 to_jid, [ENTITY_KEY], profile)[ENTITY_KEY]
182 except (exceptions.UnknownEntityError, KeyError):
183 # enable it for the first time
184 self.updateEntityData(to_jid, True, profile)
185 if not contact_enabled:
186 return True 178 return True
187 try: 179 try:
188 # message with a body always mean active state 180 # message with a body always mean active state
189 generateElementsNamed(message.children, name="body").next() 181 generateElementsNamed(message.children, name="body").next()
190 message.addElement('active', NS_CHAT_STATES) 182 message.addElement('active', NS_CHAT_STATES)
195 state = mess_data["options"]["chat_state"] 187 state = mess_data["options"]["chat_state"]
196 assert(state in CHAT_STATES) 188 assert(state in CHAT_STATES)
197 message.addElement(state, NS_CHAT_STATES) 189 message.addElement(state, NS_CHAT_STATES)
198 return True 190 return True
199 191
192 def __checkActivation(self, to_jid, forceEntityData, profile):
193 """
194 @param to_joid: the contact's JID
195 @param forceEntityData: if set to True, a non-existing
196 entity data will be considered to be True (and initialized)
197 @param: current profile
198 @return: True if the notifications should be sent to this JID.
199 """
200 # check if the parameter is active
201 if not self.host.memory.getParamA(PARAM_NAME, PARAM_KEY, profile_key=profile):
202 return False
203 # check if notifications should be sent to this contact
204 try:
205 return self.host.memory.getEntityData(to_jid, [ENTITY_KEY], profile)[ENTITY_KEY]
206 except (exceptions.UnknownEntityError, KeyError):
207 if forceEntityData:
208 # enable it for the first time
209 self.updateEntityData(to_jid, True, profile)
210 return True
211 # wait for the first message before sending states
212 return False
213
200 def __chatStateInit(self, to_jid, mess_type, profile): 214 def __chatStateInit(self, to_jid, mess_type, profile):
201 """ 215 """
202 Data initialization for the chat state machine. 216 Data initialization for the chat state machine.
203 """ 217 """
204 # TODO: use also the resource in map key 218 # TODO: use also the resource in map key
220 # TODO: use also the JID resource in the map key 234 # TODO: use also the JID resource in the map key
221 to_jid = to_jid.userhostJID() 235 to_jid = to_jid.userhostJID()
222 profile = self.host.memory.getProfileName(profile_key) 236 profile = self.host.memory.getProfileName(profile_key)
223 if profile is None: 237 if profile is None:
224 raise exceptions.ProfileUnknownError 238 raise exceptions.ProfileUnknownError
239 return
225 self.__chatStateInit(to_jid, mess_type, profile) 240 self.__chatStateInit(to_jid, mess_type, profile)
226 self.map[profile][to_jid]._onEvent("active") 241 self.map[profile][to_jid]._onEvent("active")
227 242
228 def chatStateComposing(self, to_jid_s, profile_key): 243 def chatStateComposing(self, to_jid_s, profile_key):
229 """ 244 """
231 from the front-end, it needs to check the values of the 246 from the front-end, it needs to check the values of the
232 parameter "Send chat state notifications" and the entity 247 parameter "Send chat state notifications" and the entity
233 data associated to the target JID. 248 data associated to the target JID.
234 TODO: try to optimize this method which is called often 249 TODO: try to optimize this method which is called often
235 """ 250 """
236 # check if the parameter is active
237 if not self.host.memory.getParamA(PARAM_NAME, PARAM_KEY, profile_key=profile_key):
238 return
239 # TODO: use also the JID resource in the map key 251 # TODO: use also the JID resource in the map key
240 to_jid = JID(to_jid_s).userhostJID() 252 to_jid = JID(to_jid_s).userhostJID()
241 profile = self.host.memory.getProfileName(profile_key) 253 profile = self.host.memory.getProfileName(profile_key)
242 if profile is None: 254 if profile is None:
243 raise exceptions.ProfileUnknownError 255 raise exceptions.ProfileUnknownError
244 # check if notifications should be sent to this contact 256 return
245 contact_enabled = True 257 if not self.__checkActivation(to_jid, forceEntityData=False, profile=profile):
258 return
246 try: 259 try:
247 contact_enabled = self.host.memory.getEntityData( 260 self.map[profile][to_jid]._onEvent("composing")
248 to_jid, [ENTITY_KEY], profile)[ENTITY_KEY] 261 except AttributeError:
249 except (exceptions.UnknownEntityError, KeyError): 262 # no message has been sent/received since the notifications
250 # wait for the first message before sending states 263 # have been enabled, it's better to wait for a first one
251 pass 264 pass
252 if not contact_enabled:
253 return True
254 # now we are sure that the state should be sent
255 self.map[profile][to_jid]._onEvent("composing")
256 265
257 266
258 class ChatStateMachine: 267 class ChatStateMachine:
259 """ 268 """
260 This class represents a chat state, between one profile and 269 This class represents a chat state, between one profile and