comparison sat/plugins/plugin_sec_otr.py @ 2562:26edcf3a30eb

core, setup: huge cleaning: - moved directories from src and frontends/src to sat and sat_frontends, which is the recommanded naming convention - move twisted directory to root - removed all hacks from setup.py, and added missing dependencies, it is now clean - use https URL for website in setup.py - removed "Environment :: X11 Applications :: GTK", as wix is deprecated and removed - renamed sat.sh to sat and fixed its installation - added python_requires to specify Python version needed - replaced glib2reactor which use deprecated code by gtk3reactor sat can now be installed directly from virtualenv without using --system-site-packages anymore \o/
author Goffi <goffi@goffi.org>
date Mon, 02 Apr 2018 19:44:50 +0200
parents src/plugins/plugin_sec_otr.py@0046283a285d
children 56f94936df1e
comparison
equal deleted inserted replaced
2561:bd30dc3ffe5a 2562:26edcf3a30eb
1 #!/usr/bin/env python2
2 # -*- coding: utf-8 -*-
3
4 # SAT plugin for OTR encryption
5 # Copyright (C) 2009-2018 Jérôme Poisson (goffi@goffi.org)
6
7 # This program is free software: you can redistribute it and/or modify
8 # it under the terms of the GNU Affero General Public License as published by
9 # the Free Software Foundation, either version 3 of the License, or
10 # (at your option) any later version.
11
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU Affero General Public License for more details.
16
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/>.
19
20 # XXX: thanks to Darrik L Mazey for his documentation (https://blog.darmasoft.net/2013/06/30/using-pure-python-otr.html)
21 # this implentation is based on it
22
23 from sat.core.i18n import _, D_
24 from sat.core.constants import Const as C
25 from sat.core.log import getLogger
26 from sat.core import exceptions
27 log = getLogger(__name__)
28 from sat.tools import xml_tools
29 from twisted.words.protocols.jabber import jid
30 from twisted.python import failure
31 from twisted.internet import defer
32 from sat.memory import persistent
33 import potr
34 import copy
35 import time
36 import uuid
37
38
39 PLUGIN_INFO = {
40 C.PI_NAME: u"OTR",
41 C.PI_IMPORT_NAME: u"OTR",
42 C.PI_TYPE: u"SEC",
43 C.PI_PROTOCOLS: [u"XEP-0364"],
44 C.PI_DEPENDENCIES: [u"XEP-0280", u"XEP-0334"],
45 C.PI_MAIN: u"OTR",
46 C.PI_HANDLER: u"no",
47 C.PI_DESCRIPTION: _(u"""Implementation of OTR""")
48 }
49
50 NS_OTR = "otr_plugin"
51 PRIVATE_KEY = "PRIVATE KEY"
52 OTR_MENU = D_(u'OTR')
53 AUTH_TXT = D_(u"To authenticate your correspondent, you need to give your below fingerprint *BY AN EXTERNAL CANAL* (i.e. not in this chat), and check that the one he gives you is the same as below. If there is a mismatch, there can be a spy between you!")
54 DROP_TXT = D_(u"You private key is used to encrypt messages for your correspondent, nobody except you must know it, if you are in doubt, you should drop it!\n\nAre you sure you want to drop your private key?")
55 # NO_LOG_AND = D_(u"/!\\Your history is not logged anymore, and") # FIXME: not used at the moment
56 NO_ADV_FEATURES = D_(u"Some of advanced features are disabled !")
57
58 DEFAULT_POLICY_FLAGS = {
59 'ALLOW_V1':False,
60 'ALLOW_V2':True,
61 'REQUIRE_ENCRYPTION':True,
62 }
63
64 OTR_STATE_TRUSTED = 'trusted'
65 OTR_STATE_UNTRUSTED = 'untrusted'
66 OTR_STATE_UNENCRYPTED = 'unencrypted'
67 OTR_STATE_ENCRYPTED = 'encrypted'
68
69
70 class Context(potr.context.Context):
71 def __init__(self, host, account, other_jid):
72 super(Context, self).__init__(account, other_jid)
73 self.host = host
74
75 def getPolicy(self, key):
76 if key in DEFAULT_POLICY_FLAGS:
77 return DEFAULT_POLICY_FLAGS[key]
78 else:
79 return False
80
81 def inject(self, msg_str, appdata=None):
82 """Inject encrypted data in the stream
83
84 if appdata is not None, we are sending a message in sendMessageDataTrigger
85 stanza will be injected directly if appdata is None, else we just update the element
86 and follow normal workflow
87 @param msg_str(str): encrypted message body
88 @param appdata(None, dict): None for signal message,
89 message data when an encrypted message is going to be sent
90 """
91 assert isinstance(self.peer, jid.JID)
92 msg = msg_str.decode('utf-8')
93 client = self.user.client
94 log.debug(u'injecting encrypted message to {to}'.format(to=self.peer))
95 if appdata is None:
96 mess_data = {
97 'from': client.jid,
98 'to': self.peer,
99 'uid': unicode(uuid.uuid4()),
100 'message': {'': msg},
101 'subject': {},
102 'type': 'chat',
103 'extra': {},
104 'timestamp': time.time(),
105 }
106 client.generateMessageXML(mess_data)
107 client.send(mess_data['xml'])
108 else:
109 message_elt = appdata[u'xml']
110 assert message_elt.name == u'message'
111 message_elt.addElement("body", content=msg)
112
113 def setState(self, state):
114 client = self.user.client
115 old_state = self.state
116 super(Context, self).setState(state)
117 log.debug(u"setState: %s (old_state=%s)" % (state, old_state))
118
119 if state == potr.context.STATE_PLAINTEXT:
120 feedback = _(u"/!\\ conversation with %(other_jid)s is now UNENCRYPTED") % {'other_jid': self.peer.full()}
121 self.host.bridge.otrState(OTR_STATE_UNENCRYPTED, self.peer.full(), client.profile)
122 elif state == potr.context.STATE_ENCRYPTED:
123 try:
124 trusted = self.getCurrentTrust()
125 except TypeError:
126 trusted = False
127 trusted_str = _(u"trusted") if trusted else _(u"untrusted")
128
129 if old_state == potr.context.STATE_ENCRYPTED:
130 feedback = D_(u"{trusted} OTR conversation with {other_jid} REFRESHED").format(
131 trusted = trusted_str,
132 other_jid = self.peer.full())
133 else:
134 feedback = D_(u"{trusted} encrypted OTR conversation started with {other_jid}\n{extra_info}").format(
135 trusted = trusted_str,
136 other_jid = self.peer.full(),
137 extra_info = NO_ADV_FEATURES)
138 self.host.bridge.otrState(OTR_STATE_ENCRYPTED, self.peer.full(), client.profile)
139 elif state == potr.context.STATE_FINISHED:
140 feedback = D_(u"OTR conversation with {other_jid} is FINISHED").format(other_jid = self.peer.full())
141 self.host.bridge.otrState(OTR_STATE_UNENCRYPTED, self.peer.full(), client.profile)
142 else:
143 log.error(D_(u"Unknown OTR state"))
144 return
145
146 client.feedback(self.peer, feedback)
147
148 def disconnect(self):
149 """Disconnect the session."""
150 if self.state != potr.context.STATE_PLAINTEXT:
151 super(Context, self).disconnect()
152
153 def finish(self):
154 """Finish the session - avoid to send any message but the user still has to end the session himself."""
155 if self.state == potr.context.STATE_ENCRYPTED:
156 self.processTLVs([potr.proto.DisconnectTLV()])
157
158
159 class Account(potr.context.Account):
160 # TODO: manage trusted keys: if a fingerprint is not used anymore, we have no way to remove it from database yet (same thing for a correspondent jid)
161 # TODO: manage explicit message encryption
162
163 def __init__(self, host, client):
164 log.debug(u"new account: %s" % client.jid)
165 if not client.jid.resource:
166 log.warning("Account created without resource")
167 super(Account, self).__init__(unicode(client.jid), "xmpp", 1024)
168 self.host = host
169 self.client = client
170
171 def loadPrivkey(self):
172 log.debug(u"loadPrivkey")
173 return self.privkey
174
175 def savePrivkey(self):
176 log.debug(u"savePrivkey")
177 if self.privkey is None:
178 raise exceptions.InternalError(_(u"Save is called but privkey is None !"))
179 priv_key = self.privkey.serializePrivateKey().encode('hex')
180 d = self.host.memory.encryptValue(priv_key, self.client.profile)
181 def save_encrypted_key(encrypted_priv_key):
182 self.client._otr_data[PRIVATE_KEY] = encrypted_priv_key
183 d.addCallback(save_encrypted_key)
184
185 def loadTrusts(self):
186 trust_data = self.client._otr_data.get('trust', {})
187 for jid_, jid_data in trust_data.iteritems():
188 for fingerprint, trust_level in jid_data.iteritems():
189 log.debug(u'setting trust for {jid}: [{fingerprint}] = "{trust_level}"'.format(jid=jid_, fingerprint=fingerprint, trust_level=trust_level))
190 self.trusts.setdefault(jid.JID(jid_), {})[fingerprint] = trust_level
191
192 def saveTrusts(self):
193 log.debug(u"saving trusts for {profile}".format(profile=self.client.profile))
194 log.debug(u"trusts = {}".format(self.client._otr_data['trust']))
195 self.client._otr_data.force('trust')
196
197 def setTrust(self, other_jid, fingerprint, trustLevel):
198 try:
199 trust_data = self.client._otr_data['trust']
200 except KeyError:
201 trust_data = {}
202 self.client._otr_data['trust'] = trust_data
203 jid_data = trust_data.setdefault(other_jid.full(), {})
204 jid_data[fingerprint] = trustLevel
205 super(Account, self).setTrust(other_jid, fingerprint, trustLevel)
206
207
208 class ContextManager(object):
209
210 def __init__(self, host, client):
211 self.host = host
212 self.account = Account(host, client)
213 self.contexts = {}
214
215 def startContext(self, other_jid):
216 assert isinstance(other_jid, jid.JID)
217 context = self.contexts.setdefault(other_jid, Context(self.host, self.account, other_jid))
218 return context
219
220 def getContextForUser(self, other):
221 log.debug(u"getContextForUser [%s]" % other)
222 if not other.resource:
223 log.warning(u"getContextForUser called with a bare jid: %s" % other.full())
224 return self.startContext(other)
225
226
227 class OTR(object):
228
229 def __init__(self, host):
230 log.info(_(u"OTR plugin initialization"))
231 self.host = host
232 self.context_managers = {}
233 self.skipped_profiles = set() # FIXME: OTR should not be skipped per profile, this need to be refactored
234 self._p_hints = host.plugins[u'XEP-0334']
235 self._p_carbons = host.plugins[u'XEP-0280']
236 host.trigger.add("MessageReceived", self.MessageReceivedTrigger, priority=100000)
237 host.trigger.add("sendMessage", self.sendMessageTrigger, priority=100000)
238 host.trigger.add("sendMessageData", self._sendMessageDataTrigger)
239 host.bridge.addMethod("skipOTR", ".plugin", in_sign='s', out_sign='', method=self._skipOTR) # FIXME: must be removed, must be done on per-message basis
240 host.bridge.addSignal("otrState", ".plugin", signature='sss') # args: state, destinee_jid, profile
241 host.importMenu((OTR_MENU, D_(u"Start/Refresh")), self._otrStartRefresh, security_limit=0, help_string=D_(u"Start or refresh an OTR session"), type_=C.MENU_SINGLE)
242 host.importMenu((OTR_MENU, D_(u"End session")), self._otrSessionEnd, security_limit=0, help_string=D_(u"Finish an OTR session"), type_=C.MENU_SINGLE)
243 host.importMenu((OTR_MENU, D_(u"Authenticate")), self._otrAuthenticate, security_limit=0, help_string=D_(u"Authenticate user/see your fingerprint"), type_=C.MENU_SINGLE)
244 host.importMenu((OTR_MENU, D_(u"Drop private key")), self._dropPrivKey, security_limit=0, type_=C.MENU_SINGLE)
245 host.trigger.add("presenceReceived", self._presenceReceivedTrigger)
246
247 def _skipOTR(self, profile):
248 """Tell the backend to not handle OTR for this profile.
249
250 @param profile (str): %(doc_profile)s
251 """
252 # FIXME: should not be done per profile but per message, using extra data
253 # for message received, profile wide hook may be need, but client
254 # should be used anyway instead of a class attribute
255 self.skipped_profiles.add(profile)
256
257 @defer.inlineCallbacks
258 def profileConnected(self, client):
259 if client.profile in self.skipped_profiles:
260 return
261 ctxMng = client._otr_context_manager = ContextManager(self.host, client)
262 client._otr_data = persistent.PersistentBinaryDict(NS_OTR, client.profile)
263 yield client._otr_data.load()
264 encrypted_priv_key = client._otr_data.get(PRIVATE_KEY, None)
265 if encrypted_priv_key is not None:
266 priv_key = yield self.host.memory.decryptValue(encrypted_priv_key, client.profile)
267 ctxMng.account.privkey = potr.crypt.PK.parsePrivateKey(priv_key.decode('hex'))[0]
268 else:
269 ctxMng.account.privkey = None
270 ctxMng.account.loadTrusts()
271
272 def profileDisconnected(self, client):
273 if client.profile in self.skipped_profiles:
274 self.skipped_profiles.remove(client.profile)
275 return
276 for context in client._otr_context_manager.contexts.values():
277 context.disconnect()
278 del client._otr_context_manager
279
280 def _otrStartRefresh(self, menu_data, profile):
281 """Start or refresh an OTR session
282
283 @param menu_data: %(menu_data)s
284 @param profile: %(doc_profile)s
285 """
286 client = self.host.getClient(profile)
287 try:
288 to_jid = jid.JID(menu_data['jid'])
289 except KeyError:
290 log.error(_(u"jid key is not present !"))
291 return defer.fail(exceptions.DataError)
292 self.startRefresh(client, to_jid)
293 return {}
294
295 def startRefresh(self, client, to_jid):
296 """Start or refresh an OTR session
297
298 @param to_jid(jid.JID): jid to start encrypted session with
299 """
300 if not to_jid.resource:
301 to_jid.resource = self.host.memory.getMainResource(client, to_jid) # FIXME: temporary and unsecure, must be changed when frontends are refactored
302 otrctx = client._otr_context_manager.getContextForUser(to_jid)
303 query = otrctx.sendMessage(0, '?OTRv?')
304 otrctx.inject(query)
305
306 def _otrSessionEnd(self, menu_data, profile):
307 """End an OTR session
308
309 @param menu_data: %(menu_data)s
310 @param profile: %(doc_profile)s
311 """
312 client = self.host.getClient(profile)
313 try:
314 to_jid = jid.JID(menu_data['jid'])
315 except KeyError:
316 log.error(_(u"jid key is not present !"))
317 return defer.fail(exceptions.DataError)
318 self.endSession(client, to_jid)
319 return {}
320
321 def endSession(self, client, to_jid):
322 """End an OTR session"""
323 if not to_jid.resource:
324 to_jid.resource = self.host.memory.getMainResource(client, to_jid) # FIXME: temporary and unsecure, must be changed when frontends are refactored
325 otrctx = client._otr_context_manager.getContextForUser(to_jid)
326 otrctx.disconnect()
327 return {}
328
329 def _otrAuthenticate(self, menu_data, profile):
330 """End an OTR session
331
332 @param menu_data: %(menu_data)s
333 @param profile: %(doc_profile)s
334 """
335 client = self.host.getClient(profile)
336 try:
337 to_jid = jid.JID(menu_data['jid'])
338 except KeyError:
339 log.error(_(u"jid key is not present !"))
340 return defer.fail(exceptions.DataError)
341 return self.authenticate(client, to_jid)
342
343 def authenticate(self, client, to_jid):
344 """Authenticate other user and see our own fingerprint"""
345 if not to_jid.resource:
346 to_jid.resource = self.host.memory.getMainResource(client, to_jid) # FIXME: temporary and unsecure, must be changed when frontends are refactored
347 ctxMng = client._otr_context_manager
348 otrctx = ctxMng.getContextForUser(to_jid)
349 priv_key = ctxMng.account.privkey
350
351 if priv_key is None:
352 # we have no private key yet
353 dialog = xml_tools.XMLUI(C.XMLUI_DIALOG,
354 dialog_opt = {C.XMLUI_DATA_TYPE: C.XMLUI_DIALOG_MESSAGE,
355 C.XMLUI_DATA_MESS: _(u"You have no private key yet, start an OTR conversation to have one"),
356 C.XMLUI_DATA_LVL: C.XMLUI_DATA_LVL_WARNING
357 },
358 title = _(u"No private key"),
359 )
360 return {'xmlui': dialog.toXml()}
361
362 other_fingerprint = otrctx.getCurrentKey()
363
364 if other_fingerprint is None:
365 # we have a private key, but not the fingerprint of our correspondent
366 dialog = xml_tools.XMLUI(C.XMLUI_DIALOG,
367 dialog_opt = {C.XMLUI_DATA_TYPE: C.XMLUI_DIALOG_MESSAGE,
368 C.XMLUI_DATA_MESS: _(u"Your fingerprint is:\n{fingerprint}\n\nStart an OTR conversation to have your correspondent one.").format(fingerprint=priv_key),
369 C.XMLUI_DATA_LVL: C.XMLUI_DATA_LVL_INFO
370 },
371 title = _(u"Fingerprint"),
372 )
373 return {'xmlui': dialog.toXml()}
374
375 def setTrust(raw_data, profile):
376 # This method is called when authentication form is submited
377 data = xml_tools.XMLUIResult2DataFormResult(raw_data)
378 if data['match'] == 'yes':
379 otrctx.setCurrentTrust(OTR_STATE_TRUSTED)
380 note_msg = _(u"Your correspondent {correspondent} is now TRUSTED")
381 self.host.bridge.otrState(OTR_STATE_TRUSTED, to_jid.full(), client.profile)
382 else:
383 otrctx.setCurrentTrust('')
384 note_msg = _(u"Your correspondent {correspondent} is now UNTRUSTED")
385 self.host.bridge.otrState(OTR_STATE_UNTRUSTED, to_jid.full(), client.profile)
386 note = xml_tools.XMLUI(C.XMLUI_DIALOG, dialog_opt = {
387 C.XMLUI_DATA_TYPE: C.XMLUI_DIALOG_NOTE,
388 C.XMLUI_DATA_MESS: note_msg.format(correspondent=otrctx.peer)}
389 )
390 return {'xmlui': note.toXml()}
391
392 submit_id = self.host.registerCallback(setTrust, with_data=True, one_shot=True)
393 trusted = bool(otrctx.getCurrentTrust())
394
395 xmlui = xml_tools.XMLUI(C.XMLUI_FORM, title=_('Authentication (%s)') % to_jid.full(), submit_id=submit_id)
396 xmlui.addText(_(AUTH_TXT))
397 xmlui.addDivider()
398 xmlui.addText(D_(u"Your own fingerprint is:\n{fingerprint}").format(fingerprint=priv_key))
399 xmlui.addText(D_(u"Your correspondent fingerprint should be:\n{fingerprint}").format(fingerprint=other_fingerprint))
400 xmlui.addDivider('blank')
401 xmlui.changeContainer('pairs')
402 xmlui.addLabel(D_(u'Is your correspondent fingerprint the same as here ?'))
403 xmlui.addList("match", [('yes', _('yes')),('no', _('no'))], ['yes' if trusted else 'no'])
404 return {'xmlui': xmlui.toXml()}
405
406 def _dropPrivKey(self, menu_data, profile):
407 """Drop our private Key
408
409 @param menu_data: %(menu_data)s
410 @param profile: %(doc_profile)s
411 """
412 client = self.host.getClient(profile)
413 try:
414 to_jid = jid.JID(menu_data['jid'])
415 if not to_jid.resource:
416 to_jid.resource = self.host.memory.getMainResource(client, to_jid) # FIXME: temporary and unsecure, must be changed when frontends are refactored
417 except KeyError:
418 log.error(_(u"jid key is not present !"))
419 return defer.fail(exceptions.DataError)
420
421 ctxMng = client._otr_context_manager
422 if ctxMng.account.privkey is None:
423 return {'xmlui': xml_tools.note(_(u"You don't have a private key yet !")).toXml()}
424
425 def dropKey(data, profile):
426 if C.bool(data['answer']):
427 # we end all sessions
428 for context in ctxMng.contexts.values():
429 context.disconnect()
430 ctxMng.account.privkey = None
431 ctxMng.account.getPrivkey() # as account.privkey is None, getPrivkey will generate a new key, and save it
432 return {'xmlui': xml_tools.note(D_(u"Your private key has been dropped")).toXml()}
433 return {}
434
435 submit_id = self.host.registerCallback(dropKey, with_data=True, one_shot=True)
436
437 confirm = xml_tools.XMLUI(C.XMLUI_DIALOG, title=_(u'Confirm private key drop'), dialog_opt = {'type': C.XMLUI_DIALOG_CONFIRM, 'message': _(DROP_TXT)}, submit_id = submit_id)
438 return {'xmlui': confirm.toXml()}
439
440 def _receivedTreatment(self, data, client):
441 from_jid = data['from']
442 log.debug(u"_receivedTreatment [from_jid = %s]" % from_jid)
443 otrctx = client._otr_context_manager.getContextForUser(from_jid)
444
445 try:
446 message = data['message'].itervalues().next() # FIXME: Q&D fix for message refactoring, message is now a dict
447 res = otrctx.receiveMessage(message.encode('utf-8'))
448 except potr.context.UnencryptedMessage:
449 encrypted = False
450 if otrctx.state == potr.context.STATE_ENCRYPTED:
451 log.warning(u"Received unencrypted message in an encrypted context (from {jid})".format(
452 jid = from_jid.full()))
453
454 feedback=D_(u"WARNING: received unencrypted data in a supposedly encrypted context"),
455 client.feedback(from_jid, feedback)
456 except StopIteration:
457 return data
458 else:
459 encrypted = True
460
461 if encrypted:
462 if res[0] != None:
463 # decrypted messages handling.
464 # receiveMessage() will return a tuple, the first part of which will be the decrypted message
465 data['message'] = {'':res[0].decode('utf-8')} # FIXME: Q&D fix for message refactoring, message is now a dict
466 try:
467 # we want to keep message in history, even if no store is requested in message hints
468 del data[u'history']
469 except KeyError:
470 pass
471 # TODO: add skip history as an option, but by default we don't skip it
472 # data[u'history'] = C.HISTORY_SKIP # we send the decrypted message to frontends, but we don't want it in history
473 else:
474 log.warning(u"An encrypted message was expected, but got {}".format(data['message']))
475 raise failure.Failure(exceptions.CancelError('Cancelled by OTR')) # no message at all (no history, no signal)
476 return data
477
478 def _receivedTreatmentForSkippedProfiles(self, data):
479 """This profile must be skipped because the frontend manages OTR itself,
480
481 but we still need to check if the message must be stored in history or not
482 """
483 # XXX: FIXME: this should not be done on a per-profile basis, but per-message
484 try:
485 message = data['message'].itervalues().next().encode('utf-8') # FIXME: Q&D fix for message refactoring, message is now a dict
486 except StopIteration:
487 return data
488 if message.startswith(potr.proto.OTRTAG):
489 # FIXME: it may be better to cancel the message and send it direclty to bridge
490 # this is used by Libervia, but this may send garbage message to other frontends
491 # if they are used at the same time as Libervia.
492 # Hard to avoid with decryption on Libervia though.
493 data[u'history'] = C.HISTORY_SKIP
494 return data
495
496 def MessageReceivedTrigger(self, client, message_elt, post_treat):
497 if message_elt.getAttribute('type') == C.MESS_TYPE_GROUPCHAT:
498 # OTR is not possible in group chats
499 return True
500 if client.profile in self.skipped_profiles:
501 post_treat.addCallback(self._receivedTreatmentForSkippedProfiles)
502 else:
503 post_treat.addCallback(self._receivedTreatment, client)
504 return True
505
506 def _sendMessageDataTrigger(self, client, mess_data):
507 if not 'OTR' in mess_data:
508 return
509 otrctx = mess_data['OTR']
510 message_elt = mess_data['xml']
511 to_jid = mess_data['to']
512 if otrctx.state == potr.context.STATE_ENCRYPTED:
513 log.debug(u"encrypting message")
514 body = None
515 for child in list(message_elt.children):
516 if child.name == 'body':
517 # we remove all unencrypted body,
518 # and will only encrypt the first one
519 if body is None:
520 body = child
521 message_elt.children.remove(child)
522 elif child.name == 'html':
523 # we don't want any XHTML-IM element
524 message_elt.children.remove(child)
525 if body is None:
526 log.warning(u"No message found")
527 else:
528 self._p_carbons.setPrivate(message_elt)
529 otrctx.sendMessage(0, unicode(body).encode('utf-8'), appdata=mess_data)
530 else:
531 feedback = D_(u"Your message was not sent because your correspondent closed the encrypted conversation on his/her side. "
532 u"Either close your own side, or refresh the session.")
533 log.warning(_(u"Message discarded because closed encryption channel"))
534 client.feedback(to_jid, feedback)
535 raise failure.Failure(exceptions.CancelError(u'Cancelled by OTR plugin'))
536
537 def sendMessageTrigger(self, client, mess_data, pre_xml_treatments, post_xml_treatments):
538 if mess_data['type'] == 'groupchat':
539 return True
540 if client.profile in self.skipped_profiles: # FIXME: should not be done on a per-profile basis
541 return True
542 to_jid = copy.copy(mess_data['to'])
543 if not to_jid.resource:
544 to_jid.resource = self.host.memory.getMainResource(client, to_jid) # FIXME: full jid may not be known
545 otrctx = client._otr_context_manager.getContextForUser(to_jid)
546 if otrctx.state != potr.context.STATE_PLAINTEXT:
547 self._p_hints.addHint(mess_data, self._p_hints.HINT_NO_COPY)
548 self._p_hints.addHint(mess_data, self._p_hints.HINT_NO_PERMANENT_STORE)
549 mess_data['OTR'] = otrctx # this indicate that encryption is needed in sendMessageData trigger
550 if not mess_data['to'].resource: # if not resource was given, we force it here
551 mess_data['to'] = to_jid
552 return True
553
554 def _presenceReceivedTrigger(self, entity, show, priority, statuses, profile):
555 if show != C.PRESENCE_UNAVAILABLE:
556 return True
557 client = self.host.getClient(profile)
558 if not entity.resource:
559 try:
560 entity.resource = self.host.memory.getMainResource(client, entity) # FIXME: temporary and unsecure, must be changed when frontends are refactored
561 except exceptions.UnknownEntityError:
562 return True # entity was not connected
563 if entity in client._otr_context_manager.contexts:
564 otrctx = client._otr_context_manager.getContextForUser(entity)
565 otrctx.disconnect()
566 return True