Mercurial > libervia-web
diff src/browser/sat_browser/plugin_sec_otr.py @ 679:a90cc8fc9605
merged branch frontends_multi_profiles
author | Goffi <goffi@goffi.org> |
---|---|
date | Wed, 18 Mar 2015 16:15:18 +0100 |
parents | 2e087e093e7f |
children | 3b185ccb70b4 |
line wrap: on
line diff
--- a/src/browser/sat_browser/plugin_sec_otr.py Thu Feb 05 12:05:32 2015 +0100 +++ b/src/browser/sat_browser/plugin_sec_otr.py Wed Mar 18 16:15:18 2015 +0100 @@ -23,23 +23,24 @@ The text messages to display are mostly taken from the Pidgin OTR plugin (GPL 2.0, see http://otr.cypherpunks.ca). """ -from sat.core.i18n import _, D_ from sat.core.log import getLogger -from sat.core import exceptions log = getLogger(__name__) +from sat.core.i18n import _, D_ +from sat.core import exceptions +from sat.tools.misc import TriggerManager + from constants import Const as C -import jid +from sat_frontends.tools import jid import otrjs_wrapper as otr import dialog -import panels +import chat NS_OTR = "otr_plugin" PRIVATE_KEY = "PRIVATE KEY" -MAIN_MENU = D_('OTR encryption') +MAIN_MENU = D_('OTR') # TODO: get this constant directly from backend's plugin DIALOG_EOL = "<br />" -DIALOG_USERS_ML = D_("<a href='mailto:users@salut-a-toi.org?subject={subject}&body=Please give us some hints about how to reproduce the bug (your browser name and version, what you did and what happened)'>users@salut-a-toi.org</a>") AUTH_TRUSTED = D_("Verified") AUTH_UNTRUSTED = D_("Unverified") @@ -72,7 +73,8 @@ AKE_ENCRYPTED = D_(" conversation with {jid} started. Your client is not logging this conversation.") AKE_NOT_ENCRYPTED = D_("ERROR: successfully ake'd with {jid} but the conversation is not encrypted!") END_ENCRYPTED = D_("ERROR: the OTR session ended but the context is still supposedly encrypted!") -END_PLAIN = D_("Your conversation with {jid} is no more or hasn't been encrypted.") +END_PLAIN_NO_MORE = D_("Your conversation with {jid} is no more encrypted.") +END_PLAIN_HAS_NOT = D_("Your conversation with {jid} hasn't been encrypted.") END_FINISHED = D_("{jid} has ended his or her private conversation with you; you should do the same.") KEY_TITLE = D_('Private key') @@ -92,15 +94,13 @@ ACTION_NA_TITLE = D_("Impossible action") ACTION_NA = D_("Your correspondent must be connected to start an OTR conversation with him.") -RESOURCE_ISSUE_TITLE = D_("Security issue") -RESOURCE_ISSUE = D_("Your correspondent's resource is unknown!{eol}{eol}You should stop any OTR conversation with {jid} to avoid sending him unencrypted messages in an encrypted context.{eol}{eol}Please report the bug to the users mailing list: {users_ml}.") DEFAULT_POLICY_FLAGS = { 'ALLOW_V2': True, 'ALLOW_V3': True, 'REQUIRE_ENCRYPTION': False, - 'SEND_WHITESPACE_TAG': False, # FIXME: we need to complete sendMessageTrigger before turning this to True - 'WHITESPACE_START_AKE': False, # FIXME: we need to complete messageReceivedTrigger before turning this to True + 'SEND_WHITESPACE_TAG': False, # FIXME: we need to complete sendMessageTg before turning this to True + 'WHITESPACE_START_AKE': False, # FIXME: we need to complete newMessageTg before turning this to True } # list a couple of texts or htmls (untrusted, trusted) for each state @@ -120,6 +120,13 @@ } +unicode = str # FIXME: pyjamas workaround + + +class NotConnectedEntity(Exception): + pass + + class Context(otr.context.Context): def __init__(self, host, account, other_jid): @@ -127,7 +134,7 @@ @param host (satWebFrontend) @param account (Account) - @param other_jid (JID): JID of the person your chat correspondent + @param other_jid (jid.JID): JID of the person your chat correspondent """ super(Context, self).__init__(account, other_jid) self.host = host @@ -135,7 +142,7 @@ def getPolicy(self, key): """Get the value of the specified policy - @param key (str): a value in: + @param key (unicode): a value in: - ALLOW_V1 (apriori removed from otr.js) - ALLOW_V2 - ALLOW_V3 @@ -143,7 +150,7 @@ - SEND_WHITESPACE_TAG - WHITESPACE_START_AKE - ERROR_START_AKE - @return: str + @return: unicode """ if key in DEFAULT_POLICY_FLAGS: return DEFAULT_POLICY_FLAGS[key] @@ -157,14 +164,14 @@ log.debug("message received (was %s): %s" % ('encrypted' if encrypted else 'plain', msg)) if not encrypted: if self.state == otr.context.STATE_ENCRYPTED: - log.warning(u"Received unencrypted message in an encrypted context (from %(jid)s)" % {'jid': self.peer.full()}) - self.host.newMessageCb(self.peer, RECEIVE_PLAIN_IN_ENCRYPTED_CONTEXT, C.MESS_TYPE_INFO, self.host.whoami, {}) - self.host.newMessageCb(self.peer, msg, "chat", self.host.whoami, {}) + log.warning(u"Received unencrypted message in an encrypted context (from %(jid)s)" % {'jid': self.peer}) + self.host.newMessageHandler(unicode(self.peer), RECEIVE_PLAIN_IN_ENCRYPTED_CONTEXT, C.MESS_TYPE_INFO, unicode(self.host.whoami), {}) + self.host.newMessageHandler(unicode(self.peer), msg, C.MESS_TYPE_CHAT, unicode(self.host.whoami), {}) def sendMessageCb(self, msg, meta=None): assert isinstance(self.peer, jid.JID) log.debug("message to send%s: %s" % ((' (attached meta data: %s)' % meta) if meta else '', msg)) - self.host.bridge.call('sendMessage', (None, self.host.sendError), self.peer.full(), msg, '', 'chat', {'send_only': 'true'}) + self.host.bridge.call('sendMessage', (None, self.host.sendError), unicode(self.peer), msg, '', C.MESS_TYPE_CHAT, {'send_only': 'true'}) def messageErrorCb(self, error): log.error('error occured: %s' % error) @@ -173,7 +180,7 @@ if status == otr.context.STATUS_AKE_INIT: return - other_jid_s = self.peer.full() + other_jid_s = self.peer feedback = _(u"Error: the state of the conversation with %s is unknown!") trust = self.getCurrentTrust() @@ -186,17 +193,17 @@ elif status == otr.context.STATUS_END_OTR: if msg_state == otr.context.STATE_PLAINTEXT: - feedback = END_PLAIN + feedback = END_PLAIN_NO_MORE elif msg_state == otr.context.STATE_ENCRYPTED: log.error(END_ENCRYPTED) elif msg_state == otr.context.STATE_FINISHED: feedback = END_FINISHED - self.host.newMessageCb(self.peer, feedback.format(jid=other_jid_s), C.MESS_TYPE_INFO, self.host.whoami, {'header_info': OTR.getInfoText(msg_state, trust)}) + self.host.newMessageHandler(unicode(self.peer), feedback.format(jid=other_jid_s), C.MESS_TYPE_INFO, unicode(self.host.whoami), {'header_info': OTR.getInfoText(msg_state, trust)}) def setCurrentTrust(self, new_trust='', act='asked', type_='trust'): log.debug("setCurrentTrust: trust={trust}, act={act}, type={type}".format(type=type_, trust=new_trust, act=act)) - title = (AUTH_OTHER_TITLE if act == "asked" else AUTH_US_TITLE).format(jid=self.peer.full()) + title = (AUTH_OTHER_TITLE if act == "asked" else AUTH_US_TITLE).format(jid=self.peer) old_trust = self.getCurrentTrust() if type_ == 'abort': msg = AUTH_ABORTED_TXT @@ -215,7 +222,7 @@ otr.context.Context.setCurrentTrust(self, new_trust) if old_trust != new_trust: feedback = AUTH_STATUS.format(state=(AUTH_TRUSTED if new_trust else AUTH_UNTRUSTED).lower()) - self.host.newMessageCb(self.peer, feedback, C.MESS_TYPE_INFO, self.host.whoami, {'header_info': OTR.getInfoText(self.state, new_trust)}) + self.host.newMessageHandler(unicode(self.peer), feedback, C.MESS_TYPE_INFO, unicode(self.host.whoami), {'header_info': OTR.getInfoText(self.state, new_trust)}) def fingerprintAuthCb(self): """OTR v2 authentication using manual fingerprint comparison""" @@ -236,17 +243,17 @@ self.setCurrentTrust('fingerprint' if confirm else '') text = (AUTH_INFO_TXT + "<i>" + AUTH_FINGERPRINT_TXT + "</i>" + AUTH_FINGERPRINT_VERIFY).format(you=self.host.whoami, your_fp=priv_key.fingerprint(), other=self.peer, other_fp=other_key.fingerprint(), eol=DIALOG_EOL) - title = AUTH_OTHER_TITLE.format(jid=self.peer.full()) + title = AUTH_OTHER_TITLE.format(jid=self.peer) dialog.ConfirmDialog(setTrust, text, title, AddStyleName="maxWidthLimit").show() def smpAuthCb(self, type_, data, act=None): """OTR v3 authentication using the socialist millionaire protocol. - @param type_ (str): a value in ('question', 'trust', 'abort') - @param data (str, bool): this could be: + @param type_ (unicode): a value in ('question', 'trust', 'abort') + @param data (unicode, bool): this could be: - a string containing the question if type_ is 'question' - a boolean value telling if the authentication succeed when type_ is 'trust' - @param act (str): a value in ('asked', 'answered') + @param act (unicode): a value in ('asked', 'answered') """ log.debug("smpAuthCb: type={type}, data={data}, act={act}".format(type=type_, data=data, act=act)) if act is None: @@ -261,23 +268,23 @@ # make us need the dirty self.smpAuthAbort. else: log.error("FIXME: unmanaged ambiguous 'act' value in Context.smpAuthCb!") - title = (AUTH_OTHER_TITLE if act == "asked" else AUTH_US_TITLE).format(jid=self.peer.full()) + title = (AUTH_OTHER_TITLE if act == "asked" else AUTH_US_TITLE).format(jid=self.peer) if type_ == 'question': if act == 'asked': - def cb(question, answer=None): - if question is False or not answer: # dialog cancelled or the answer is empty + def cb(result, question, answer=None): + if not result or not answer: # dialog cancelled or the answer is empty return self.smpAuthSecret(answer, question) text = (AUTH_INFO_TXT + "<i>" + AUTH_QUEST_DEFINE_TXT + "</i>" + AUTH_QUEST_DEFINE).format(eol=DIALOG_EOL) dialog.PromptDialog(cb, [text, AUTH_SECRET_INPUT.format(eol=DIALOG_EOL)], title=title, AddStyleName="maxWidthLimit").show() else: - def cb(answer): - if not answer: # dialog cancelled or the answer is empty + def cb(result, answer): + if not result or not answer: # dialog cancelled or the answer is empty self.smpAuthAbort('answered') return self.smpAuthSecret(answer) text = (AUTH_INFO_TXT + "<i>" + AUTH_QUEST_ANSWER_TXT + "</i>" + AUTH_QUEST_ANSWER).format(eol=DIALOG_EOL, question=data) - dialog.PromptDialog(cb, text + AUTH_SECRET_INPUT.format(eol=DIALOG_EOL), title=title, AddStyleName="maxWidthLimit").show() + dialog.PromptDialog(cb, [text + AUTH_SECRET_INPUT.format(eol=DIALOG_EOL)], title=title, AddStyleName="maxWidthLimit").show() elif type_ == 'trust': self.setCurrentTrust('smp' if data else '', act) elif type_ == 'abort': @@ -297,7 +304,7 @@ class Account(otr.context.Account): def __init__(self, host): - log.debug(u"new account: %s" % host.whoami.full()) + log.debug(u"new account: %s" % host.whoami) if not host.whoami.resource: log.warning("Account created without resource") super(Account, self).__init__(host.whoami) @@ -327,7 +334,7 @@ self.contexts = {} def startContext(self, other_jid): - assert isinstance(other_jid, jid.JID) + assert isinstance(other_jid, jid.JID) # never start an OTR session with a bare JID # FIXME upstream: apparently pyjamas doesn't implement setdefault well, it ignores JID.__hash__ redefinition #context = self.contexts.setdefault(other_jid, Context(self.host, self.account, other_jid)) if other_jid not in self.contexts: @@ -337,24 +344,42 @@ def getContextForUser(self, other_jid, start=True): """Get the context for the given JID - @param other_jid (JID): your correspondent + @param other_jid (jid.JID): your correspondent @param start (bool): start non-existing context if True @return: Context """ + try: + other_jid = self.fixResource(other_jid) + except NotConnectedEntity: + log.debug(u"getContextForUser [%s]: not connected!" % other_jid) + return None log.debug(u"getContextForUser [%s]" % other_jid) - if not other_jid.resource: - log.error("getContextForUser called with a bare jid") - running_sessions = [jid.bareJID() for jid in self.contexts.keys() if self.contexts[jid].state == otr.context.STATE_ENCRYPTED] - if start or (other_jid in running_sessions): - users_ml = DIALOG_USERS_ML.format(subject=D_("OTR issue in Libervia: getContextForUser called with a bare jid in an encrypted context")) - text = RESOURCE_ISSUE.format(eol=DIALOG_EOL, jid=other_jid.full(), users_ml=users_ml) - dialog.InfoDialog(RESOURCE_ISSUE_TITLE, text, AddStyleName="maxWidthLimit").show() - return None # never start an OTR session with a bare JID if start: return self.startContext(other_jid) else: return self.contexts.get(other_jid, None) + def getContextsForBareUser(self, bare_jid): + """Get all the contexts for the users sharing the given bare JID. + + @param bare_jid (jid.JID): bare JID + @return: list[Context] + """ + return [context for other_jid, context in self.contexts.iteritems() if other_jid.bare == bare_jid] + + def fixResource(self, other_jid): + """Return the full JID in case the resource of the given JID is missing. + + @param other_jid (jid.JID): JID to check + @return jid.JID + """ + if other_jid.resource: + return other_jid + clist = self.host.contact_list + if clist.getCache(other_jid.bare, C.PRESENCE_SHOW) is None: + raise NotConnectedEntity + return clist.getFullJid(other_jid) + class OTR(object): @@ -362,161 +387,140 @@ log.info(_(u"OTR plugin initialization")) self.host = host self.context_manager = None - self.last_resources = {} self.host.bridge._registerMethods(["skipOTR"]) + self.host.trigger.add("newMessageTrigger", self.newMessageTg, priority=TriggerManager.MAX_PRIORITY) + self.host.trigger.add("sendMessageTrigger", self.sendMessageTg, priority=TriggerManager.MAX_PRIORITY) + + # FIXME: workaround for a pyjamas issue: calling hash on a class method always return a different value if that method is defined directly within the class (with the "def" keyword) + self._profilePluggedListener = self.profilePluggedListener + self._gotMenusListener = self.gotMenusListener + # FIXME: these listeners are never removed, can't be removed by themselves (it modifies the list while looping), maybe need a 'one_shot' argument + self.host.addListener('profilePlugged', self._profilePluggedListener) + self.host.addListener('gotMenus', self._gotMenusListener) @classmethod def getInfoText(self, state=otr.context.STATE_PLAINTEXT, trust=''): """Get the widget info text for a certain message state and trust. - @param state (str): message state - @param trust (str): trust - @return: str + @param state (unicode): message state + @param trust (unicode): trust + @return: unicode """ if not state: state = OTR_MSG_STATES.keys()[0] return OTR_MSG_STATES[state][1 if trust else 0] - def infoTextCallback(self, other_jid, cb): - """Get the current info text for a conversation and run a callback. + def getInfoTextForUser(self, other_jid): + """Get the current info text for a conversation. - @param other_jid (JID): JID of the correspondant - @paam cb (callable): method to be called with the computed info text + @param other_jid (jid.JID): JID of the correspondant """ - def gotResource(other_jid): - otrctx = self.context_manager.getContextForUser(other_jid, start=False) - if otrctx is None: - cb(OTR.getInfoText()) - else: - cb(OTR.getInfoText(otrctx.state, otrctx.getCurrentTrust())) + otrctx = self.context_manager.getContextForUser(other_jid, start=False) + if otrctx is None: + return OTR.getInfoText() + else: + return OTR.getInfoText(otrctx.state, otrctx.getCurrentTrust()) - self.fixResource(other_jid, gotResource) - - def inhibitMenus(self): - """Tell the caller which dynamic menus should be inhibited""" - return ["OTR"] # menu categories name to inhibit + def gotMenusListener(self,): + # TODO: get menus paths to hook directly from backend's OTR plugin + self.host.menus.addMenuHook(C.MENU_SINGLE, (MAIN_MENU, D_(u"Start/Refresh")), callback=self._startRefresh) + self.host.menus.addMenuHook(C.MENU_SINGLE, (MAIN_MENU, D_(u"End session")), callback=self._endSession) + self.host.menus.addMenuHook(C.MENU_SINGLE, (MAIN_MENU, D_(u"Authenticate")), callback=self._authenticate) + self.host.menus.addMenuHook(C.MENU_SINGLE, (MAIN_MENU, D_(u"Drop private key")), callback=self._dropPrivkey) - def extraMenus(self): - # FIXME: handle help strings too - return [(self._startRefresh, C.MENU_SINGLE, (MAIN_MENU, "Start / refresh"), (MAIN_MENU, D_("Start / refresh"))), - (self._endSession, C.MENU_SINGLE, (MAIN_MENU, "Stop encryption"), (MAIN_MENU, D_("Stop encryption"))), - (self._authenticate, C.MENU_SINGLE, (MAIN_MENU, "Authenticate correspondent"), (MAIN_MENU, D_("Authenticate correspondent"))), - (self._dropPrivkey, C.MENU_SINGLE, (MAIN_MENU, "Drop your private key"), (MAIN_MENU, D_("Drop your private key")))] + def profilePluggedListener(self, profile): + # FIXME: workaround for a pyjamas issue: calling hash on a class method always return a different value if that method is defined directly within the class (with the "def" keyword) + self._presenceListener = self.presenceListener + self._disconnectListener = self.disconnectListener + self.host.addListener('presence', self._presenceListener, [C.PROF_KEY_NONE]) + # FIXME: this listener is never removed, can't be removed by itself (it modifies the list while looping), maybe need a 'one_shot' argument + self.host.addListener('disconnect', self._disconnectListener, [C.PROF_KEY_NONE]) - def profileConnected(self): self.host.bridge.call('skipOTR', None) self.context_manager = ContextManager(self.host) # TODO: retrieve the encrypted private key from a HTML5 persistent storage, # decrypt it, parse it with otr.crypt.PK.parsePrivateKey(privkey) and # assign it to self.context_manager.account.privkey - def profileDisconnected(self): + def disconnectListener(self, profile): + """Things to do just before the profile disconnection""" + self.host.removeListener('presence', self._presenceListener) + for context in self.context_manager.contexts.values(): - context.disconnect() + context.disconnect() # FIXME: no time to send the message before the profile has been disconnected - def fixResource(self, jid, cb): - # FIXME: it's dirty, but libervia doesn't manage resources correctly now, refactoring is planed - if jid.resource: - self.last_resources[jid.bare] = jid.resource - cb(jid) - elif jid.bare in self.last_resources: - jid.setResource(self.last_resources[jid.bare]) - cb(jid) - else: - def gotResource(resource): - if resource: - jid.setResource(resource) - self.last_resources[jid.bare] = jid.resource - cb(jid) - self.host.bridge.call('getLastResource', gotResource, jid.full()) + def presenceListener(self, entity, show, priority, statuses, profile): + if show == C.PRESENCE_UNAVAILABLE: + self.endSession(entity, disconnect=False) - def messageReceivedTrigger(self, from_jid, msg, msg_type, to_jid, extra): - if msg_type == C.MESS_TYPE_INFO: + def newMessageTg(self, from_jid, msg, msg_type, to_jid, extra, profile): + if msg_type != C.MESS_TYPE_CHAT: return True tag = otr.proto.checkForOTR(msg) if tag is None or (tag == otr.context.WHITESPACE_TAG and not DEFAULT_POLICY_FLAGS['WHITESPACE_START_AKE']): return True - def decrypt(context): - context.receiveMessage(msg) + other_jid = to_jid if from_jid.bare == self.host.whoami.bare else from_jid + otrctx = self.context_manager.getContextForUser(other_jid, start=False) + if otrctx is None: + def confirm(confirm): + if confirm: + self.host.displayWidget(chat.Chat, other_jid) + self.context_manager.startContext(other_jid).receiveMessage(msg) + else: + # FIXME: plain text messages with whitespaces would be lost here when WHITESPACE_START_AKE is True + pass + key = self.context_manager.account.privkey + question = QUERY_RECEIVED + QUERY_SLOWDOWN + (QUERY_KEY if key else QUERY_NO_KEY) + QUERY_CONFIRM + dialog.ConfirmDialog(confirm, question.format(jid=other_jid, eol=DIALOG_EOL), QUERY_TITLE, AddStyleName="maxWidthLimit").show() + else: # do not ask for user confirmation if the context exist + otrctx.receiveMessage(msg) - def cb(jid): - otrctx = self.context_manager.getContextForUser(jid, start=False) - if otrctx is None: - def confirm(confirm): - if confirm: - self.host.getOrCreateLiberviaWidget(panels.ChatPanel, {'item': jid}) - decrypt(self.context_manager.startContext(jid)) - else: - # FIXME: plain text messages with whitespaces would be lost here when WHITESPACE_START_AKE is True - pass - key = self.context_manager.account.privkey - msg = QUERY_RECEIVED + QUERY_SLOWDOWN + (QUERY_KEY if key else QUERY_NO_KEY) + QUERY_CONFIRM - dialog.ConfirmDialog(confirm, msg.format(jid=jid.full(), eol=DIALOG_EOL), QUERY_TITLE, AddStyleName="maxWidthLimit").show() - else: # do not ask if the context exist - decrypt(otrctx) - - other_jid = to_jid if from_jid.bare == self.host.whoami.bare else from_jid - self.fixResource(other_jid, cb) return False # interrupt the main process - def sendMessageTrigger(self, to_jid, msg, msg_type, extra): - def cb(jid): - otrctx = self.context_manager.getContextForUser(jid, start=False) - if otrctx is not None and otrctx.state != otr.context.STATE_PLAINTEXT: - if otrctx.state == otr.context.STATE_ENCRYPTED: - log.debug(u"encrypting message") - otrctx.sendMessage(msg) - self.host.newMessageCb(self.host.whoami, msg, msg_type, jid, extra) - else: - feedback = SEND_PLAIN_IN_FINISHED_CONTEXT - dialog.InfoDialog(FINISHED_CONTEXT_TITLE.format(jid=to_jid.full()), feedback, AddStyleName="maxWidthLimit").show() + def sendMessageTg(self, to_jid, message, subject, mess_type, extra, callback, errback, profile_key): + if mess_type != C.MESS_TYPE_CHAT: + return True + + otrctx = self.context_manager.getContextForUser(to_jid, start=False) + if otrctx is not None and otrctx.state != otr.context.STATE_PLAINTEXT: + if otrctx.state == otr.context.STATE_ENCRYPTED: + log.debug(u"encrypting message") + otrctx.sendMessage(message) + self.host.newMessageHandler(unicode(self.host.whoami), message, mess_type, unicode(to_jid), extra) else: - log.debug(u"sending message unencrypted") - self.host.bridge.call('sendMessage', (None, self.host.sendError), to_jid.full(), msg, '', msg_type, extra) + feedback = SEND_PLAIN_IN_FINISHED_CONTEXT + dialog.InfoDialog(FINISHED_CONTEXT_TITLE.format(jid=to_jid), feedback, AddStyleName="maxWidthLimit").show() + return False # interrupt the main process - if msg_type == 'groupchat': - return True - self.fixResource(to_jid, cb) - return False # interrupt the main process - - def presenceReceivedTrigger(self, entity, show, priority, statuses): - if show == "unavailable": - self.endSession(entity, finish=True) + log.debug(u"sending message unencrypted") return True - def endSession(self, other_jid, profile, finish=False): + def endSession(self, other_jid, disconnect=True): """Finish or disconnect an OTR session - @param other_jid (JID): str - @param finish: if True, finish the session but do not disconnect it - @return: True if the session has been finished or disconnected, False if there was nothing to do + @param other_jid (jid.JID): other JID + @param disconnect (bool): if False, finish the session but do not disconnect it """ - def cb(other_jid): - def not_available(): - if not finish: - self.host.newMessageCb(other_jid, END_PLAIN.format(jid=other_jid.full()), C.MESS_TYPE_INFO, self.host.whoami, {}) - - priv_key = self.context_manager.account.privkey - if priv_key is None: - not_available() + # checking for private key existence is not needed, context checking is enough + if other_jid.resource: + contexts = [self.context_manager.getContextForUser(other_jid, start=False)] + else: # contact disconnected itself so we need to terminate the OTR session but the Chat panel lost its resource + contexts = self.context_manager.getContextsForBareUser(other_jid) + for otrctx in contexts: + if otrctx is None or otrctx.state == otr.context.STATE_PLAINTEXT: + if disconnect: + self.host.newMessageHandler(unicode(other_jid), END_PLAIN_HAS_NOT.format(jid=other_jid), C.MESS_TYPE_INFO, unicode(self.host.whoami), {}) return - - otrctx = self.context_manager.getContextForUser(other_jid, start=False) - if otrctx is None: - not_available() - return - if finish: + if disconnect: + otrctx.disconnect() + else: otrctx.finish() - else: - otrctx.disconnect() - - self.fixResource(other_jid, cb) # Menu callbacks - def _startRefresh(self, menu_data): + def _startRefresh(self, caller, menu_data, profile): """Start or refresh an OTR session @param menu_data: %(menu_data)s @@ -526,39 +530,30 @@ if otrctx: otrctx.sendQueryMessage() - def cb(jid): - key = self.context_manager.account.privkey - if key is None: - def confirm(confirm): - if confirm: - query(jid) - msg = QUERY_SEND + QUERY_SLOWDOWN + QUERY_NO_KEY + QUERY_CONFIRM - dialog.ConfirmDialog(confirm, msg.format(jid=jid.full(), eol=DIALOG_EOL), QUERY_TITLE, AddStyleName="maxWidthLimit").show() - else: # on query reception we ask always, if we initiate we just ask the first time - query(jid) + other_jid = jid.JID(menu_data['jid']) + clist = self.host.contact_list + if clist.getCache(other_jid.bare, C.PRESENCE_SHOW) is None: + dialog.InfoDialog(ACTION_NA_TITLE, ACTION_NA, AddStyleName="maxWidthLimit").show() + return - try: - other_jid = menu_data['jid'] - if other_jid.bare not in self.host.contact_panel.connected: - dialog.InfoDialog(ACTION_NA_TITLE, ACTION_NA, AddStyleName="maxWidthLimit").show() - return - self.fixResource(other_jid, cb) - except KeyError: - log.error(_("jid key is not present !")) + key = self.context_manager.account.privkey + if key is None: + def confirm(confirm): + if confirm: + query(other_jid) + msg = QUERY_SEND + QUERY_SLOWDOWN + QUERY_NO_KEY + QUERY_CONFIRM + dialog.ConfirmDialog(confirm, msg.format(jid=other_jid, eol=DIALOG_EOL), QUERY_TITLE, AddStyleName="maxWidthLimit").show() + else: # on query reception we ask always, if we initiate we just ask the first time + query(other_jid) - def _endSession(self, menu_data): + def _endSession(self, caller, menu_data, profile): """End an OTR session @param menu_data: %(menu_data)s """ - try: - other_jid = menu_data['jid'] - except KeyError: - log.error(_("jid key is not present !")) - return None - self.endSession(other_jid) + self.endSession(jid.JID(menu_data['jid'])) - def _authenticate(self, menu_data, profile): + def _authenticate(self, caller, menu_data, profile): """Authenticate other user and see our own fingerprint @param menu_data: %(menu_data)s @@ -567,32 +562,22 @@ def not_available(): dialog.InfoDialog(AUTH_TRUST_NA_TITLE, AUTH_TRUST_NA_TXT, AddStyleName="maxWidthLimit").show() - priv_key = self.context_manager.account.privkey - if priv_key is None: + to_jid = jid.JID(menu_data['jid']) + + # checking for private key existence is not needed, context checking is enough + otrctx = self.context_manager.getContextForUser(to_jid, start=False) + if otrctx is None or otrctx.state != otr.context.STATE_ENCRYPTED: not_available() return + otr_version = otrctx.getUsedVersion() + if otr_version == otr.context.OTR_VERSION_2: + otrctx.fingerprintAuthCb() + elif otr_version == otr.context.OTR_VERSION_3: + otrctx.smpAuthCb('question', None, 'asked') + else: + not_available() - def cb(to_jid): - otrctx = self.context_manager.getContextForUser(to_jid, start=False) - if otrctx is None: - not_available() - return - otr_version = otrctx.getUsedVersion() - if otr_version == otr.context.OTR_VERSION_2: - otrctx.fingerprintAuthCb() - elif otr_version == otr.context.OTR_VERSION_3: - otrctx.smpAuthCb('question', None, 'asked') - else: - not_available() - - try: - to_jid = menu_data['jid'] - self.fixResource(to_jid, cb) - except KeyError: - log.error(_("jid key is not present !")) - return None - - def _dropPrivkey(self, menu_data, profile): + def _dropPrivkey(self, caller, menu_data, profile): """Drop our private Key @param menu_data: %(menu_data)s @@ -604,21 +589,13 @@ dialog.InfoDialog(KEY_NA_TITLE, KEY_NA_TXT, AddStyleName="maxWidthLimit").show() return - def cb(to_jid): - def dropKey(confirm): - if confirm: - # we end all sessions - for context in self.context_manager.contexts.values(): - context.disconnect() - self.context_manager.contexts.clear() - self.context_manager.account.privkey = None - dialog.InfoDialog(KEY_TITLE, KEY_DROPPED_TXT, AddStyleName="maxWidthLimit").show() + def dropKey(confirm): + if confirm: + # we end all sessions + for context in self.context_manager.contexts.values(): + context.disconnect() + self.context_manager.contexts.clear() + self.context_manager.account.privkey = None + dialog.InfoDialog(KEY_TITLE, KEY_DROPPED_TXT, AddStyleName="maxWidthLimit").show() - dialog.ConfirmDialog(dropKey, KEY_DROP_TXT.format(eol=DIALOG_EOL), KEY_DROP_TITLE, AddStyleName="maxWidthLimit").show() - - try: - to_jid = menu_data['jid'] - self.fixResource(to_jid, cb) - except KeyError: - log.error(_("jid key is not present !")) - return None + dialog.ConfirmDialog(dropKey, KEY_DROP_TXT.format(eol=DIALOG_EOL), KEY_DROP_TITLE, AddStyleName="maxWidthLimit").show()