Mercurial > libervia-web
comparison 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 |
comparison
equal
deleted
inserted
replaced
590:1bffc4c244c3 | 679:a90cc8fc9605 |
---|---|
21 """ | 21 """ |
22 This file is adapted from sat.plugins.plugin.sec_otr. It offers browser-side OTR encryption using otr.js. | 22 This file is adapted from sat.plugins.plugin.sec_otr. It offers browser-side OTR encryption using otr.js. |
23 The text messages to display are mostly taken from the Pidgin OTR plugin (GPL 2.0, see http://otr.cypherpunks.ca). | 23 The text messages to display are mostly taken from the Pidgin OTR plugin (GPL 2.0, see http://otr.cypherpunks.ca). |
24 """ | 24 """ |
25 | 25 |
26 from sat.core.log import getLogger | |
27 log = getLogger(__name__) | |
28 | |
26 from sat.core.i18n import _, D_ | 29 from sat.core.i18n import _, D_ |
27 from sat.core.log import getLogger | |
28 from sat.core import exceptions | 30 from sat.core import exceptions |
29 log = getLogger(__name__) | 31 from sat.tools.misc import TriggerManager |
30 | 32 |
31 from constants import Const as C | 33 from constants import Const as C |
32 import jid | 34 from sat_frontends.tools import jid |
33 import otrjs_wrapper as otr | 35 import otrjs_wrapper as otr |
34 import dialog | 36 import dialog |
35 import panels | 37 import chat |
36 | 38 |
37 | 39 |
38 NS_OTR = "otr_plugin" | 40 NS_OTR = "otr_plugin" |
39 PRIVATE_KEY = "PRIVATE KEY" | 41 PRIVATE_KEY = "PRIVATE KEY" |
40 MAIN_MENU = D_('OTR encryption') | 42 MAIN_MENU = D_('OTR') # TODO: get this constant directly from backend's plugin |
41 DIALOG_EOL = "<br />" | 43 DIALOG_EOL = "<br />" |
42 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>") | |
43 | 44 |
44 AUTH_TRUSTED = D_("Verified") | 45 AUTH_TRUSTED = D_("Verified") |
45 AUTH_UNTRUSTED = D_("Unverified") | 46 AUTH_UNTRUSTED = D_("Unverified") |
46 AUTH_OTHER_TITLE = D_("Authentication of {jid}") | 47 AUTH_OTHER_TITLE = D_("Authentication of {jid}") |
47 AUTH_US_TITLE = D_("Authentication to {jid}") | 48 AUTH_US_TITLE = D_("Authentication to {jid}") |
70 QUERY_ENCRYPTED = D_('Attempting to refresh the OTR conversation with {jid}...') | 71 QUERY_ENCRYPTED = D_('Attempting to refresh the OTR conversation with {jid}...') |
71 QUERY_NOT_ENCRYPTED = D_('Attempting to start an OTR conversation with {jid}...') | 72 QUERY_NOT_ENCRYPTED = D_('Attempting to start an OTR conversation with {jid}...') |
72 AKE_ENCRYPTED = D_(" conversation with {jid} started. Your client is not logging this conversation.") | 73 AKE_ENCRYPTED = D_(" conversation with {jid} started. Your client is not logging this conversation.") |
73 AKE_NOT_ENCRYPTED = D_("ERROR: successfully ake'd with {jid} but the conversation is not encrypted!") | 74 AKE_NOT_ENCRYPTED = D_("ERROR: successfully ake'd with {jid} but the conversation is not encrypted!") |
74 END_ENCRYPTED = D_("ERROR: the OTR session ended but the context is still supposedly encrypted!") | 75 END_ENCRYPTED = D_("ERROR: the OTR session ended but the context is still supposedly encrypted!") |
75 END_PLAIN = D_("Your conversation with {jid} is no more or hasn't been encrypted.") | 76 END_PLAIN_NO_MORE = D_("Your conversation with {jid} is no more encrypted.") |
77 END_PLAIN_HAS_NOT = D_("Your conversation with {jid} hasn't been encrypted.") | |
76 END_FINISHED = D_("{jid} has ended his or her private conversation with you; you should do the same.") | 78 END_FINISHED = D_("{jid} has ended his or her private conversation with you; you should do the same.") |
77 | 79 |
78 KEY_TITLE = D_('Private key') | 80 KEY_TITLE = D_('Private key') |
79 KEY_NA_TITLE = D_("No private key") | 81 KEY_NA_TITLE = D_("No private key") |
80 KEY_NA_TXT = D_("You don't have any private key yet.") | 82 KEY_NA_TXT = D_("You don't have any private key yet.") |
90 QUERY_KEY = D_("You already have a private key, but to start the conversation will still require a couple of seconds.{eol}{eol}") | 92 QUERY_KEY = D_("You already have a private key, but to start the conversation will still require a couple of seconds.{eol}{eol}") |
91 QUERY_CONFIRM = D_("Press OK to start now the encryption.") | 93 QUERY_CONFIRM = D_("Press OK to start now the encryption.") |
92 | 94 |
93 ACTION_NA_TITLE = D_("Impossible action") | 95 ACTION_NA_TITLE = D_("Impossible action") |
94 ACTION_NA = D_("Your correspondent must be connected to start an OTR conversation with him.") | 96 ACTION_NA = D_("Your correspondent must be connected to start an OTR conversation with him.") |
95 RESOURCE_ISSUE_TITLE = D_("Security issue") | |
96 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}.") | |
97 | 97 |
98 DEFAULT_POLICY_FLAGS = { | 98 DEFAULT_POLICY_FLAGS = { |
99 'ALLOW_V2': True, | 99 'ALLOW_V2': True, |
100 'ALLOW_V3': True, | 100 'ALLOW_V3': True, |
101 'REQUIRE_ENCRYPTION': False, | 101 'REQUIRE_ENCRYPTION': False, |
102 'SEND_WHITESPACE_TAG': False, # FIXME: we need to complete sendMessageTrigger before turning this to True | 102 'SEND_WHITESPACE_TAG': False, # FIXME: we need to complete sendMessageTg before turning this to True |
103 'WHITESPACE_START_AKE': False, # FIXME: we need to complete messageReceivedTrigger before turning this to True | 103 'WHITESPACE_START_AKE': False, # FIXME: we need to complete newMessageTg before turning this to True |
104 } | 104 } |
105 | 105 |
106 # list a couple of texts or htmls (untrusted, trusted) for each state | 106 # list a couple of texts or htmls (untrusted, trusted) for each state |
107 OTR_MSG_STATES = { | 107 OTR_MSG_STATES = { |
108 otr.context.STATE_PLAINTEXT: [ | 108 otr.context.STATE_PLAINTEXT: [ |
118 '<img src="media/icons/silk/lock_break.png" /><img src="media/icons/silk/key.png" />' | 118 '<img src="media/icons/silk/lock_break.png" /><img src="media/icons/silk/key.png" />' |
119 ] | 119 ] |
120 } | 120 } |
121 | 121 |
122 | 122 |
123 unicode = str # FIXME: pyjamas workaround | |
124 | |
125 | |
126 class NotConnectedEntity(Exception): | |
127 pass | |
128 | |
129 | |
123 class Context(otr.context.Context): | 130 class Context(otr.context.Context): |
124 | 131 |
125 def __init__(self, host, account, other_jid): | 132 def __init__(self, host, account, other_jid): |
126 """ | 133 """ |
127 | 134 |
128 @param host (satWebFrontend) | 135 @param host (satWebFrontend) |
129 @param account (Account) | 136 @param account (Account) |
130 @param other_jid (JID): JID of the person your chat correspondent | 137 @param other_jid (jid.JID): JID of the person your chat correspondent |
131 """ | 138 """ |
132 super(Context, self).__init__(account, other_jid) | 139 super(Context, self).__init__(account, other_jid) |
133 self.host = host | 140 self.host = host |
134 | 141 |
135 def getPolicy(self, key): | 142 def getPolicy(self, key): |
136 """Get the value of the specified policy | 143 """Get the value of the specified policy |
137 | 144 |
138 @param key (str): a value in: | 145 @param key (unicode): a value in: |
139 - ALLOW_V1 (apriori removed from otr.js) | 146 - ALLOW_V1 (apriori removed from otr.js) |
140 - ALLOW_V2 | 147 - ALLOW_V2 |
141 - ALLOW_V3 | 148 - ALLOW_V3 |
142 - REQUIRE_ENCRYPTION | 149 - REQUIRE_ENCRYPTION |
143 - SEND_WHITESPACE_TAG | 150 - SEND_WHITESPACE_TAG |
144 - WHITESPACE_START_AKE | 151 - WHITESPACE_START_AKE |
145 - ERROR_START_AKE | 152 - ERROR_START_AKE |
146 @return: str | 153 @return: unicode |
147 """ | 154 """ |
148 if key in DEFAULT_POLICY_FLAGS: | 155 if key in DEFAULT_POLICY_FLAGS: |
149 return DEFAULT_POLICY_FLAGS[key] | 156 return DEFAULT_POLICY_FLAGS[key] |
150 else: | 157 else: |
151 return False | 158 return False |
155 if not encrypted: | 162 if not encrypted: |
156 log.warning("A plain-text message has been handled by otr.js") | 163 log.warning("A plain-text message has been handled by otr.js") |
157 log.debug("message received (was %s): %s" % ('encrypted' if encrypted else 'plain', msg)) | 164 log.debug("message received (was %s): %s" % ('encrypted' if encrypted else 'plain', msg)) |
158 if not encrypted: | 165 if not encrypted: |
159 if self.state == otr.context.STATE_ENCRYPTED: | 166 if self.state == otr.context.STATE_ENCRYPTED: |
160 log.warning(u"Received unencrypted message in an encrypted context (from %(jid)s)" % {'jid': self.peer.full()}) | 167 log.warning(u"Received unencrypted message in an encrypted context (from %(jid)s)" % {'jid': self.peer}) |
161 self.host.newMessageCb(self.peer, RECEIVE_PLAIN_IN_ENCRYPTED_CONTEXT, C.MESS_TYPE_INFO, self.host.whoami, {}) | 168 self.host.newMessageHandler(unicode(self.peer), RECEIVE_PLAIN_IN_ENCRYPTED_CONTEXT, C.MESS_TYPE_INFO, unicode(self.host.whoami), {}) |
162 self.host.newMessageCb(self.peer, msg, "chat", self.host.whoami, {}) | 169 self.host.newMessageHandler(unicode(self.peer), msg, C.MESS_TYPE_CHAT, unicode(self.host.whoami), {}) |
163 | 170 |
164 def sendMessageCb(self, msg, meta=None): | 171 def sendMessageCb(self, msg, meta=None): |
165 assert isinstance(self.peer, jid.JID) | 172 assert isinstance(self.peer, jid.JID) |
166 log.debug("message to send%s: %s" % ((' (attached meta data: %s)' % meta) if meta else '', msg)) | 173 log.debug("message to send%s: %s" % ((' (attached meta data: %s)' % meta) if meta else '', msg)) |
167 self.host.bridge.call('sendMessage', (None, self.host.sendError), self.peer.full(), msg, '', 'chat', {'send_only': 'true'}) | 174 self.host.bridge.call('sendMessage', (None, self.host.sendError), unicode(self.peer), msg, '', C.MESS_TYPE_CHAT, {'send_only': 'true'}) |
168 | 175 |
169 def messageErrorCb(self, error): | 176 def messageErrorCb(self, error): |
170 log.error('error occured: %s' % error) | 177 log.error('error occured: %s' % error) |
171 | 178 |
172 def setStateCb(self, msg_state, status): | 179 def setStateCb(self, msg_state, status): |
173 if status == otr.context.STATUS_AKE_INIT: | 180 if status == otr.context.STATUS_AKE_INIT: |
174 return | 181 return |
175 | 182 |
176 other_jid_s = self.peer.full() | 183 other_jid_s = self.peer |
177 feedback = _(u"Error: the state of the conversation with %s is unknown!") | 184 feedback = _(u"Error: the state of the conversation with %s is unknown!") |
178 trust = self.getCurrentTrust() | 185 trust = self.getCurrentTrust() |
179 | 186 |
180 if status == otr.context.STATUS_SEND_QUERY: | 187 if status == otr.context.STATUS_SEND_QUERY: |
181 feedback = QUERY_ENCRYPTED if msg_state == otr.context.STATE_ENCRYPTED else QUERY_NOT_ENCRYPTED | 188 feedback = QUERY_ENCRYPTED if msg_state == otr.context.STATE_ENCRYPTED else QUERY_NOT_ENCRYPTED |
184 trusted_str = AUTH_TRUSTED if trust else AUTH_UNTRUSTED | 191 trusted_str = AUTH_TRUSTED if trust else AUTH_UNTRUSTED |
185 feedback = (trusted_str + AKE_ENCRYPTED) if msg_state == otr.context.STATE_ENCRYPTED else AKE_NOT_ENCRYPTED | 192 feedback = (trusted_str + AKE_ENCRYPTED) if msg_state == otr.context.STATE_ENCRYPTED else AKE_NOT_ENCRYPTED |
186 | 193 |
187 elif status == otr.context.STATUS_END_OTR: | 194 elif status == otr.context.STATUS_END_OTR: |
188 if msg_state == otr.context.STATE_PLAINTEXT: | 195 if msg_state == otr.context.STATE_PLAINTEXT: |
189 feedback = END_PLAIN | 196 feedback = END_PLAIN_NO_MORE |
190 elif msg_state == otr.context.STATE_ENCRYPTED: | 197 elif msg_state == otr.context.STATE_ENCRYPTED: |
191 log.error(END_ENCRYPTED) | 198 log.error(END_ENCRYPTED) |
192 elif msg_state == otr.context.STATE_FINISHED: | 199 elif msg_state == otr.context.STATE_FINISHED: |
193 feedback = END_FINISHED | 200 feedback = END_FINISHED |
194 | 201 |
195 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)}) | 202 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)}) |
196 | 203 |
197 def setCurrentTrust(self, new_trust='', act='asked', type_='trust'): | 204 def setCurrentTrust(self, new_trust='', act='asked', type_='trust'): |
198 log.debug("setCurrentTrust: trust={trust}, act={act}, type={type}".format(type=type_, trust=new_trust, act=act)) | 205 log.debug("setCurrentTrust: trust={trust}, act={act}, type={type}".format(type=type_, trust=new_trust, act=act)) |
199 title = (AUTH_OTHER_TITLE if act == "asked" else AUTH_US_TITLE).format(jid=self.peer.full()) | 206 title = (AUTH_OTHER_TITLE if act == "asked" else AUTH_US_TITLE).format(jid=self.peer) |
200 old_trust = self.getCurrentTrust() | 207 old_trust = self.getCurrentTrust() |
201 if type_ == 'abort': | 208 if type_ == 'abort': |
202 msg = AUTH_ABORTED_TXT | 209 msg = AUTH_ABORTED_TXT |
203 elif new_trust: | 210 elif new_trust: |
204 if act == "asked": | 211 if act == "asked": |
213 if act != "asked": | 220 if act != "asked": |
214 return | 221 return |
215 otr.context.Context.setCurrentTrust(self, new_trust) | 222 otr.context.Context.setCurrentTrust(self, new_trust) |
216 if old_trust != new_trust: | 223 if old_trust != new_trust: |
217 feedback = AUTH_STATUS.format(state=(AUTH_TRUSTED if new_trust else AUTH_UNTRUSTED).lower()) | 224 feedback = AUTH_STATUS.format(state=(AUTH_TRUSTED if new_trust else AUTH_UNTRUSTED).lower()) |
218 self.host.newMessageCb(self.peer, feedback, C.MESS_TYPE_INFO, self.host.whoami, {'header_info': OTR.getInfoText(self.state, new_trust)}) | 225 self.host.newMessageHandler(unicode(self.peer), feedback, C.MESS_TYPE_INFO, unicode(self.host.whoami), {'header_info': OTR.getInfoText(self.state, new_trust)}) |
219 | 226 |
220 def fingerprintAuthCb(self): | 227 def fingerprintAuthCb(self): |
221 """OTR v2 authentication using manual fingerprint comparison""" | 228 """OTR v2 authentication using manual fingerprint comparison""" |
222 priv_key = self.user.privkey | 229 priv_key = self.user.privkey |
223 | 230 |
234 | 241 |
235 def setTrust(confirm): | 242 def setTrust(confirm): |
236 self.setCurrentTrust('fingerprint' if confirm else '') | 243 self.setCurrentTrust('fingerprint' if confirm else '') |
237 | 244 |
238 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) | 245 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) |
239 title = AUTH_OTHER_TITLE.format(jid=self.peer.full()) | 246 title = AUTH_OTHER_TITLE.format(jid=self.peer) |
240 dialog.ConfirmDialog(setTrust, text, title, AddStyleName="maxWidthLimit").show() | 247 dialog.ConfirmDialog(setTrust, text, title, AddStyleName="maxWidthLimit").show() |
241 | 248 |
242 def smpAuthCb(self, type_, data, act=None): | 249 def smpAuthCb(self, type_, data, act=None): |
243 """OTR v3 authentication using the socialist millionaire protocol. | 250 """OTR v3 authentication using the socialist millionaire protocol. |
244 | 251 |
245 @param type_ (str): a value in ('question', 'trust', 'abort') | 252 @param type_ (unicode): a value in ('question', 'trust', 'abort') |
246 @param data (str, bool): this could be: | 253 @param data (unicode, bool): this could be: |
247 - a string containing the question if type_ is 'question' | 254 - a string containing the question if type_ is 'question' |
248 - a boolean value telling if the authentication succeed when type_ is 'trust' | 255 - a boolean value telling if the authentication succeed when type_ is 'trust' |
249 @param act (str): a value in ('asked', 'answered') | 256 @param act (unicode): a value in ('asked', 'answered') |
250 """ | 257 """ |
251 log.debug("smpAuthCb: type={type}, data={data}, act={act}".format(type=type_, data=data, act=act)) | 258 log.debug("smpAuthCb: type={type}, data={data}, act={act}".format(type=type_, data=data, act=act)) |
252 if act is None: | 259 if act is None: |
253 if type_ == 'question': | 260 if type_ == 'question': |
254 act = 'answered' # OTR._authenticate calls this method with act="asked" | 261 act = 'answered' # OTR._authenticate calls this method with act="asked" |
259 # fingerprints, we will reach this code... that's wrong, this method is for SMP! | 266 # fingerprints, we will reach this code... that's wrong, this method is for SMP! |
260 # There's probably a bug to fix in otr.js. Do it together with the issue that | 267 # There's probably a bug to fix in otr.js. Do it together with the issue that |
261 # make us need the dirty self.smpAuthAbort. | 268 # make us need the dirty self.smpAuthAbort. |
262 else: | 269 else: |
263 log.error("FIXME: unmanaged ambiguous 'act' value in Context.smpAuthCb!") | 270 log.error("FIXME: unmanaged ambiguous 'act' value in Context.smpAuthCb!") |
264 title = (AUTH_OTHER_TITLE if act == "asked" else AUTH_US_TITLE).format(jid=self.peer.full()) | 271 title = (AUTH_OTHER_TITLE if act == "asked" else AUTH_US_TITLE).format(jid=self.peer) |
265 if type_ == 'question': | 272 if type_ == 'question': |
266 if act == 'asked': | 273 if act == 'asked': |
267 def cb(question, answer=None): | 274 def cb(result, question, answer=None): |
268 if question is False or not answer: # dialog cancelled or the answer is empty | 275 if not result or not answer: # dialog cancelled or the answer is empty |
269 return | 276 return |
270 self.smpAuthSecret(answer, question) | 277 self.smpAuthSecret(answer, question) |
271 text = (AUTH_INFO_TXT + "<i>" + AUTH_QUEST_DEFINE_TXT + "</i>" + AUTH_QUEST_DEFINE).format(eol=DIALOG_EOL) | 278 text = (AUTH_INFO_TXT + "<i>" + AUTH_QUEST_DEFINE_TXT + "</i>" + AUTH_QUEST_DEFINE).format(eol=DIALOG_EOL) |
272 dialog.PromptDialog(cb, [text, AUTH_SECRET_INPUT.format(eol=DIALOG_EOL)], title=title, AddStyleName="maxWidthLimit").show() | 279 dialog.PromptDialog(cb, [text, AUTH_SECRET_INPUT.format(eol=DIALOG_EOL)], title=title, AddStyleName="maxWidthLimit").show() |
273 else: | 280 else: |
274 def cb(answer): | 281 def cb(result, answer): |
275 if not answer: # dialog cancelled or the answer is empty | 282 if not result or not answer: # dialog cancelled or the answer is empty |
276 self.smpAuthAbort('answered') | 283 self.smpAuthAbort('answered') |
277 return | 284 return |
278 self.smpAuthSecret(answer) | 285 self.smpAuthSecret(answer) |
279 text = (AUTH_INFO_TXT + "<i>" + AUTH_QUEST_ANSWER_TXT + "</i>" + AUTH_QUEST_ANSWER).format(eol=DIALOG_EOL, question=data) | 286 text = (AUTH_INFO_TXT + "<i>" + AUTH_QUEST_ANSWER_TXT + "</i>" + AUTH_QUEST_ANSWER).format(eol=DIALOG_EOL, question=data) |
280 dialog.PromptDialog(cb, text + AUTH_SECRET_INPUT.format(eol=DIALOG_EOL), title=title, AddStyleName="maxWidthLimit").show() | 287 dialog.PromptDialog(cb, [text + AUTH_SECRET_INPUT.format(eol=DIALOG_EOL)], title=title, AddStyleName="maxWidthLimit").show() |
281 elif type_ == 'trust': | 288 elif type_ == 'trust': |
282 self.setCurrentTrust('smp' if data else '', act) | 289 self.setCurrentTrust('smp' if data else '', act) |
283 elif type_ == 'abort': | 290 elif type_ == 'abort': |
284 self.setCurrentTrust('', act, 'abort') | 291 self.setCurrentTrust('', act, 'abort') |
285 | 292 |
295 | 302 |
296 | 303 |
297 class Account(otr.context.Account): | 304 class Account(otr.context.Account): |
298 | 305 |
299 def __init__(self, host): | 306 def __init__(self, host): |
300 log.debug(u"new account: %s" % host.whoami.full()) | 307 log.debug(u"new account: %s" % host.whoami) |
301 if not host.whoami.resource: | 308 if not host.whoami.resource: |
302 log.warning("Account created without resource") | 309 log.warning("Account created without resource") |
303 super(Account, self).__init__(host.whoami) | 310 super(Account, self).__init__(host.whoami) |
304 self.host = host | 311 self.host = host |
305 | 312 |
325 self.host = host | 332 self.host = host |
326 self.account = Account(host) | 333 self.account = Account(host) |
327 self.contexts = {} | 334 self.contexts = {} |
328 | 335 |
329 def startContext(self, other_jid): | 336 def startContext(self, other_jid): |
330 assert isinstance(other_jid, jid.JID) | 337 assert isinstance(other_jid, jid.JID) # never start an OTR session with a bare JID |
331 # FIXME upstream: apparently pyjamas doesn't implement setdefault well, it ignores JID.__hash__ redefinition | 338 # FIXME upstream: apparently pyjamas doesn't implement setdefault well, it ignores JID.__hash__ redefinition |
332 #context = self.contexts.setdefault(other_jid, Context(self.host, self.account, other_jid)) | 339 #context = self.contexts.setdefault(other_jid, Context(self.host, self.account, other_jid)) |
333 if other_jid not in self.contexts: | 340 if other_jid not in self.contexts: |
334 self.contexts[other_jid] = Context(self.host, self.account, other_jid) | 341 self.contexts[other_jid] = Context(self.host, self.account, other_jid) |
335 return self.contexts[other_jid] | 342 return self.contexts[other_jid] |
336 | 343 |
337 def getContextForUser(self, other_jid, start=True): | 344 def getContextForUser(self, other_jid, start=True): |
338 """Get the context for the given JID | 345 """Get the context for the given JID |
339 | 346 |
340 @param other_jid (JID): your correspondent | 347 @param other_jid (jid.JID): your correspondent |
341 @param start (bool): start non-existing context if True | 348 @param start (bool): start non-existing context if True |
342 @return: Context | 349 @return: Context |
343 """ | 350 """ |
351 try: | |
352 other_jid = self.fixResource(other_jid) | |
353 except NotConnectedEntity: | |
354 log.debug(u"getContextForUser [%s]: not connected!" % other_jid) | |
355 return None | |
344 log.debug(u"getContextForUser [%s]" % other_jid) | 356 log.debug(u"getContextForUser [%s]" % other_jid) |
345 if not other_jid.resource: | |
346 log.error("getContextForUser called with a bare jid") | |
347 running_sessions = [jid.bareJID() for jid in self.contexts.keys() if self.contexts[jid].state == otr.context.STATE_ENCRYPTED] | |
348 if start or (other_jid in running_sessions): | |
349 users_ml = DIALOG_USERS_ML.format(subject=D_("OTR issue in Libervia: getContextForUser called with a bare jid in an encrypted context")) | |
350 text = RESOURCE_ISSUE.format(eol=DIALOG_EOL, jid=other_jid.full(), users_ml=users_ml) | |
351 dialog.InfoDialog(RESOURCE_ISSUE_TITLE, text, AddStyleName="maxWidthLimit").show() | |
352 return None # never start an OTR session with a bare JID | |
353 if start: | 357 if start: |
354 return self.startContext(other_jid) | 358 return self.startContext(other_jid) |
355 else: | 359 else: |
356 return self.contexts.get(other_jid, None) | 360 return self.contexts.get(other_jid, None) |
357 | 361 |
362 def getContextsForBareUser(self, bare_jid): | |
363 """Get all the contexts for the users sharing the given bare JID. | |
364 | |
365 @param bare_jid (jid.JID): bare JID | |
366 @return: list[Context] | |
367 """ | |
368 return [context for other_jid, context in self.contexts.iteritems() if other_jid.bare == bare_jid] | |
369 | |
370 def fixResource(self, other_jid): | |
371 """Return the full JID in case the resource of the given JID is missing. | |
372 | |
373 @param other_jid (jid.JID): JID to check | |
374 @return jid.JID | |
375 """ | |
376 if other_jid.resource: | |
377 return other_jid | |
378 clist = self.host.contact_list | |
379 if clist.getCache(other_jid.bare, C.PRESENCE_SHOW) is None: | |
380 raise NotConnectedEntity | |
381 return clist.getFullJid(other_jid) | |
382 | |
358 | 383 |
359 class OTR(object): | 384 class OTR(object): |
360 | 385 |
361 def __init__(self, host): | 386 def __init__(self, host): |
362 log.info(_(u"OTR plugin initialization")) | 387 log.info(_(u"OTR plugin initialization")) |
363 self.host = host | 388 self.host = host |
364 self.context_manager = None | 389 self.context_manager = None |
365 self.last_resources = {} | |
366 self.host.bridge._registerMethods(["skipOTR"]) | 390 self.host.bridge._registerMethods(["skipOTR"]) |
391 self.host.trigger.add("newMessageTrigger", self.newMessageTg, priority=TriggerManager.MAX_PRIORITY) | |
392 self.host.trigger.add("sendMessageTrigger", self.sendMessageTg, priority=TriggerManager.MAX_PRIORITY) | |
393 | |
394 # 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) | |
395 self._profilePluggedListener = self.profilePluggedListener | |
396 self._gotMenusListener = self.gotMenusListener | |
397 # FIXME: these listeners are never removed, can't be removed by themselves (it modifies the list while looping), maybe need a 'one_shot' argument | |
398 self.host.addListener('profilePlugged', self._profilePluggedListener) | |
399 self.host.addListener('gotMenus', self._gotMenusListener) | |
367 | 400 |
368 @classmethod | 401 @classmethod |
369 def getInfoText(self, state=otr.context.STATE_PLAINTEXT, trust=''): | 402 def getInfoText(self, state=otr.context.STATE_PLAINTEXT, trust=''): |
370 """Get the widget info text for a certain message state and trust. | 403 """Get the widget info text for a certain message state and trust. |
371 | 404 |
372 @param state (str): message state | 405 @param state (unicode): message state |
373 @param trust (str): trust | 406 @param trust (unicode): trust |
374 @return: str | 407 @return: unicode |
375 """ | 408 """ |
376 if not state: | 409 if not state: |
377 state = OTR_MSG_STATES.keys()[0] | 410 state = OTR_MSG_STATES.keys()[0] |
378 return OTR_MSG_STATES[state][1 if trust else 0] | 411 return OTR_MSG_STATES[state][1 if trust else 0] |
379 | 412 |
380 def infoTextCallback(self, other_jid, cb): | 413 def getInfoTextForUser(self, other_jid): |
381 """Get the current info text for a conversation and run a callback. | 414 """Get the current info text for a conversation. |
382 | 415 |
383 @param other_jid (JID): JID of the correspondant | 416 @param other_jid (jid.JID): JID of the correspondant |
384 @paam cb (callable): method to be called with the computed info text | 417 """ |
385 """ | 418 otrctx = self.context_manager.getContextForUser(other_jid, start=False) |
386 def gotResource(other_jid): | 419 if otrctx is None: |
387 otrctx = self.context_manager.getContextForUser(other_jid, start=False) | 420 return OTR.getInfoText() |
388 if otrctx is None: | 421 else: |
389 cb(OTR.getInfoText()) | 422 return OTR.getInfoText(otrctx.state, otrctx.getCurrentTrust()) |
390 else: | 423 |
391 cb(OTR.getInfoText(otrctx.state, otrctx.getCurrentTrust())) | 424 def gotMenusListener(self,): |
392 | 425 # TODO: get menus paths to hook directly from backend's OTR plugin |
393 self.fixResource(other_jid, gotResource) | 426 self.host.menus.addMenuHook(C.MENU_SINGLE, (MAIN_MENU, D_(u"Start/Refresh")), callback=self._startRefresh) |
394 | 427 self.host.menus.addMenuHook(C.MENU_SINGLE, (MAIN_MENU, D_(u"End session")), callback=self._endSession) |
395 def inhibitMenus(self): | 428 self.host.menus.addMenuHook(C.MENU_SINGLE, (MAIN_MENU, D_(u"Authenticate")), callback=self._authenticate) |
396 """Tell the caller which dynamic menus should be inhibited""" | 429 self.host.menus.addMenuHook(C.MENU_SINGLE, (MAIN_MENU, D_(u"Drop private key")), callback=self._dropPrivkey) |
397 return ["OTR"] # menu categories name to inhibit | 430 |
398 | 431 def profilePluggedListener(self, profile): |
399 def extraMenus(self): | 432 # 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) |
400 # FIXME: handle help strings too | 433 self._presenceListener = self.presenceListener |
401 return [(self._startRefresh, C.MENU_SINGLE, (MAIN_MENU, "Start / refresh"), (MAIN_MENU, D_("Start / refresh"))), | 434 self._disconnectListener = self.disconnectListener |
402 (self._endSession, C.MENU_SINGLE, (MAIN_MENU, "Stop encryption"), (MAIN_MENU, D_("Stop encryption"))), | 435 self.host.addListener('presence', self._presenceListener, [C.PROF_KEY_NONE]) |
403 (self._authenticate, C.MENU_SINGLE, (MAIN_MENU, "Authenticate correspondent"), (MAIN_MENU, D_("Authenticate correspondent"))), | 436 # FIXME: this listener is never removed, can't be removed by itself (it modifies the list while looping), maybe need a 'one_shot' argument |
404 (self._dropPrivkey, C.MENU_SINGLE, (MAIN_MENU, "Drop your private key"), (MAIN_MENU, D_("Drop your private key")))] | 437 self.host.addListener('disconnect', self._disconnectListener, [C.PROF_KEY_NONE]) |
405 | 438 |
406 def profileConnected(self): | |
407 self.host.bridge.call('skipOTR', None) | 439 self.host.bridge.call('skipOTR', None) |
408 self.context_manager = ContextManager(self.host) | 440 self.context_manager = ContextManager(self.host) |
409 # TODO: retrieve the encrypted private key from a HTML5 persistent storage, | 441 # TODO: retrieve the encrypted private key from a HTML5 persistent storage, |
410 # decrypt it, parse it with otr.crypt.PK.parsePrivateKey(privkey) and | 442 # decrypt it, parse it with otr.crypt.PK.parsePrivateKey(privkey) and |
411 # assign it to self.context_manager.account.privkey | 443 # assign it to self.context_manager.account.privkey |
412 | 444 |
413 def profileDisconnected(self): | 445 def disconnectListener(self, profile): |
446 """Things to do just before the profile disconnection""" | |
447 self.host.removeListener('presence', self._presenceListener) | |
448 | |
414 for context in self.context_manager.contexts.values(): | 449 for context in self.context_manager.contexts.values(): |
415 context.disconnect() | 450 context.disconnect() # FIXME: no time to send the message before the profile has been disconnected |
416 | 451 |
417 def fixResource(self, jid, cb): | 452 def presenceListener(self, entity, show, priority, statuses, profile): |
418 # FIXME: it's dirty, but libervia doesn't manage resources correctly now, refactoring is planed | 453 if show == C.PRESENCE_UNAVAILABLE: |
419 if jid.resource: | 454 self.endSession(entity, disconnect=False) |
420 self.last_resources[jid.bare] = jid.resource | 455 |
421 cb(jid) | 456 def newMessageTg(self, from_jid, msg, msg_type, to_jid, extra, profile): |
422 elif jid.bare in self.last_resources: | 457 if msg_type != C.MESS_TYPE_CHAT: |
423 jid.setResource(self.last_resources[jid.bare]) | |
424 cb(jid) | |
425 else: | |
426 def gotResource(resource): | |
427 if resource: | |
428 jid.setResource(resource) | |
429 self.last_resources[jid.bare] = jid.resource | |
430 cb(jid) | |
431 self.host.bridge.call('getLastResource', gotResource, jid.full()) | |
432 | |
433 def messageReceivedTrigger(self, from_jid, msg, msg_type, to_jid, extra): | |
434 if msg_type == C.MESS_TYPE_INFO: | |
435 return True | 458 return True |
436 | 459 |
437 tag = otr.proto.checkForOTR(msg) | 460 tag = otr.proto.checkForOTR(msg) |
438 if tag is None or (tag == otr.context.WHITESPACE_TAG and not DEFAULT_POLICY_FLAGS['WHITESPACE_START_AKE']): | 461 if tag is None or (tag == otr.context.WHITESPACE_TAG and not DEFAULT_POLICY_FLAGS['WHITESPACE_START_AKE']): |
439 return True | 462 return True |
440 | 463 |
441 def decrypt(context): | |
442 context.receiveMessage(msg) | |
443 | |
444 def cb(jid): | |
445 otrctx = self.context_manager.getContextForUser(jid, start=False) | |
446 if otrctx is None: | |
447 def confirm(confirm): | |
448 if confirm: | |
449 self.host.getOrCreateLiberviaWidget(panels.ChatPanel, {'item': jid}) | |
450 decrypt(self.context_manager.startContext(jid)) | |
451 else: | |
452 # FIXME: plain text messages with whitespaces would be lost here when WHITESPACE_START_AKE is True | |
453 pass | |
454 key = self.context_manager.account.privkey | |
455 msg = QUERY_RECEIVED + QUERY_SLOWDOWN + (QUERY_KEY if key else QUERY_NO_KEY) + QUERY_CONFIRM | |
456 dialog.ConfirmDialog(confirm, msg.format(jid=jid.full(), eol=DIALOG_EOL), QUERY_TITLE, AddStyleName="maxWidthLimit").show() | |
457 else: # do not ask if the context exist | |
458 decrypt(otrctx) | |
459 | |
460 other_jid = to_jid if from_jid.bare == self.host.whoami.bare else from_jid | 464 other_jid = to_jid if from_jid.bare == self.host.whoami.bare else from_jid |
461 self.fixResource(other_jid, cb) | 465 otrctx = self.context_manager.getContextForUser(other_jid, start=False) |
466 if otrctx is None: | |
467 def confirm(confirm): | |
468 if confirm: | |
469 self.host.displayWidget(chat.Chat, other_jid) | |
470 self.context_manager.startContext(other_jid).receiveMessage(msg) | |
471 else: | |
472 # FIXME: plain text messages with whitespaces would be lost here when WHITESPACE_START_AKE is True | |
473 pass | |
474 key = self.context_manager.account.privkey | |
475 question = QUERY_RECEIVED + QUERY_SLOWDOWN + (QUERY_KEY if key else QUERY_NO_KEY) + QUERY_CONFIRM | |
476 dialog.ConfirmDialog(confirm, question.format(jid=other_jid, eol=DIALOG_EOL), QUERY_TITLE, AddStyleName="maxWidthLimit").show() | |
477 else: # do not ask for user confirmation if the context exist | |
478 otrctx.receiveMessage(msg) | |
479 | |
462 return False # interrupt the main process | 480 return False # interrupt the main process |
463 | 481 |
464 def sendMessageTrigger(self, to_jid, msg, msg_type, extra): | 482 def sendMessageTg(self, to_jid, message, subject, mess_type, extra, callback, errback, profile_key): |
465 def cb(jid): | 483 if mess_type != C.MESS_TYPE_CHAT: |
466 otrctx = self.context_manager.getContextForUser(jid, start=False) | 484 return True |
467 if otrctx is not None and otrctx.state != otr.context.STATE_PLAINTEXT: | 485 |
468 if otrctx.state == otr.context.STATE_ENCRYPTED: | 486 otrctx = self.context_manager.getContextForUser(to_jid, start=False) |
469 log.debug(u"encrypting message") | 487 if otrctx is not None and otrctx.state != otr.context.STATE_PLAINTEXT: |
470 otrctx.sendMessage(msg) | 488 if otrctx.state == otr.context.STATE_ENCRYPTED: |
471 self.host.newMessageCb(self.host.whoami, msg, msg_type, jid, extra) | 489 log.debug(u"encrypting message") |
472 else: | 490 otrctx.sendMessage(message) |
473 feedback = SEND_PLAIN_IN_FINISHED_CONTEXT | 491 self.host.newMessageHandler(unicode(self.host.whoami), message, mess_type, unicode(to_jid), extra) |
474 dialog.InfoDialog(FINISHED_CONTEXT_TITLE.format(jid=to_jid.full()), feedback, AddStyleName="maxWidthLimit").show() | |
475 else: | 492 else: |
476 log.debug(u"sending message unencrypted") | 493 feedback = SEND_PLAIN_IN_FINISHED_CONTEXT |
477 self.host.bridge.call('sendMessage', (None, self.host.sendError), to_jid.full(), msg, '', msg_type, extra) | 494 dialog.InfoDialog(FINISHED_CONTEXT_TITLE.format(jid=to_jid), feedback, AddStyleName="maxWidthLimit").show() |
478 | 495 return False # interrupt the main process |
479 if msg_type == 'groupchat': | 496 |
480 return True | 497 log.debug(u"sending message unencrypted") |
481 self.fixResource(to_jid, cb) | |
482 return False # interrupt the main process | |
483 | |
484 def presenceReceivedTrigger(self, entity, show, priority, statuses): | |
485 if show == "unavailable": | |
486 self.endSession(entity, finish=True) | |
487 return True | 498 return True |
488 | 499 |
489 def endSession(self, other_jid, profile, finish=False): | 500 def endSession(self, other_jid, disconnect=True): |
490 """Finish or disconnect an OTR session | 501 """Finish or disconnect an OTR session |
491 | 502 |
492 @param other_jid (JID): str | 503 @param other_jid (jid.JID): other JID |
493 @param finish: if True, finish the session but do not disconnect it | 504 @param disconnect (bool): if False, finish the session but do not disconnect it |
494 @return: True if the session has been finished or disconnected, False if there was nothing to do | 505 """ |
495 """ | 506 # checking for private key existence is not needed, context checking is enough |
496 def cb(other_jid): | 507 if other_jid.resource: |
497 def not_available(): | 508 contexts = [self.context_manager.getContextForUser(other_jid, start=False)] |
498 if not finish: | 509 else: # contact disconnected itself so we need to terminate the OTR session but the Chat panel lost its resource |
499 self.host.newMessageCb(other_jid, END_PLAIN.format(jid=other_jid.full()), C.MESS_TYPE_INFO, self.host.whoami, {}) | 510 contexts = self.context_manager.getContextsForBareUser(other_jid) |
500 | 511 for otrctx in contexts: |
501 priv_key = self.context_manager.account.privkey | 512 if otrctx is None or otrctx.state == otr.context.STATE_PLAINTEXT: |
502 if priv_key is None: | 513 if disconnect: |
503 not_available() | 514 self.host.newMessageHandler(unicode(other_jid), END_PLAIN_HAS_NOT.format(jid=other_jid), C.MESS_TYPE_INFO, unicode(self.host.whoami), {}) |
504 return | 515 return |
505 | 516 if disconnect: |
506 otrctx = self.context_manager.getContextForUser(other_jid, start=False) | 517 otrctx.disconnect() |
507 if otrctx is None: | 518 else: |
508 not_available() | |
509 return | |
510 if finish: | |
511 otrctx.finish() | 519 otrctx.finish() |
512 else: | |
513 otrctx.disconnect() | |
514 | |
515 self.fixResource(other_jid, cb) | |
516 | 520 |
517 # Menu callbacks | 521 # Menu callbacks |
518 | 522 |
519 def _startRefresh(self, menu_data): | 523 def _startRefresh(self, caller, menu_data, profile): |
520 """Start or refresh an OTR session | 524 """Start or refresh an OTR session |
521 | 525 |
522 @param menu_data: %(menu_data)s | 526 @param menu_data: %(menu_data)s |
523 """ | 527 """ |
524 def query(other_jid): | 528 def query(other_jid): |
525 otrctx = self.context_manager.getContextForUser(other_jid) | 529 otrctx = self.context_manager.getContextForUser(other_jid) |
526 if otrctx: | 530 if otrctx: |
527 otrctx.sendQueryMessage() | 531 otrctx.sendQueryMessage() |
528 | 532 |
529 def cb(jid): | 533 other_jid = jid.JID(menu_data['jid']) |
530 key = self.context_manager.account.privkey | 534 clist = self.host.contact_list |
531 if key is None: | 535 if clist.getCache(other_jid.bare, C.PRESENCE_SHOW) is None: |
532 def confirm(confirm): | 536 dialog.InfoDialog(ACTION_NA_TITLE, ACTION_NA, AddStyleName="maxWidthLimit").show() |
533 if confirm: | 537 return |
534 query(jid) | 538 |
535 msg = QUERY_SEND + QUERY_SLOWDOWN + QUERY_NO_KEY + QUERY_CONFIRM | 539 key = self.context_manager.account.privkey |
536 dialog.ConfirmDialog(confirm, msg.format(jid=jid.full(), eol=DIALOG_EOL), QUERY_TITLE, AddStyleName="maxWidthLimit").show() | 540 if key is None: |
537 else: # on query reception we ask always, if we initiate we just ask the first time | 541 def confirm(confirm): |
538 query(jid) | 542 if confirm: |
539 | 543 query(other_jid) |
540 try: | 544 msg = QUERY_SEND + QUERY_SLOWDOWN + QUERY_NO_KEY + QUERY_CONFIRM |
541 other_jid = menu_data['jid'] | 545 dialog.ConfirmDialog(confirm, msg.format(jid=other_jid, eol=DIALOG_EOL), QUERY_TITLE, AddStyleName="maxWidthLimit").show() |
542 if other_jid.bare not in self.host.contact_panel.connected: | 546 else: # on query reception we ask always, if we initiate we just ask the first time |
543 dialog.InfoDialog(ACTION_NA_TITLE, ACTION_NA, AddStyleName="maxWidthLimit").show() | 547 query(other_jid) |
544 return | 548 |
545 self.fixResource(other_jid, cb) | 549 def _endSession(self, caller, menu_data, profile): |
546 except KeyError: | |
547 log.error(_("jid key is not present !")) | |
548 | |
549 def _endSession(self, menu_data): | |
550 """End an OTR session | 550 """End an OTR session |
551 | 551 |
552 @param menu_data: %(menu_data)s | 552 @param menu_data: %(menu_data)s |
553 """ | 553 """ |
554 try: | 554 self.endSession(jid.JID(menu_data['jid'])) |
555 other_jid = menu_data['jid'] | 555 |
556 except KeyError: | 556 def _authenticate(self, caller, menu_data, profile): |
557 log.error(_("jid key is not present !")) | |
558 return None | |
559 self.endSession(other_jid) | |
560 | |
561 def _authenticate(self, menu_data, profile): | |
562 """Authenticate other user and see our own fingerprint | 557 """Authenticate other user and see our own fingerprint |
563 | 558 |
564 @param menu_data: %(menu_data)s | 559 @param menu_data: %(menu_data)s |
565 @param profile: %(doc_profile)s | 560 @param profile: %(doc_profile)s |
566 """ | 561 """ |
567 def not_available(): | 562 def not_available(): |
568 dialog.InfoDialog(AUTH_TRUST_NA_TITLE, AUTH_TRUST_NA_TXT, AddStyleName="maxWidthLimit").show() | 563 dialog.InfoDialog(AUTH_TRUST_NA_TITLE, AUTH_TRUST_NA_TXT, AddStyleName="maxWidthLimit").show() |
569 | 564 |
570 priv_key = self.context_manager.account.privkey | 565 to_jid = jid.JID(menu_data['jid']) |
571 if priv_key is None: | 566 |
567 # checking for private key existence is not needed, context checking is enough | |
568 otrctx = self.context_manager.getContextForUser(to_jid, start=False) | |
569 if otrctx is None or otrctx.state != otr.context.STATE_ENCRYPTED: | |
572 not_available() | 570 not_available() |
573 return | 571 return |
574 | 572 otr_version = otrctx.getUsedVersion() |
575 def cb(to_jid): | 573 if otr_version == otr.context.OTR_VERSION_2: |
576 otrctx = self.context_manager.getContextForUser(to_jid, start=False) | 574 otrctx.fingerprintAuthCb() |
577 if otrctx is None: | 575 elif otr_version == otr.context.OTR_VERSION_3: |
578 not_available() | 576 otrctx.smpAuthCb('question', None, 'asked') |
579 return | 577 else: |
580 otr_version = otrctx.getUsedVersion() | 578 not_available() |
581 if otr_version == otr.context.OTR_VERSION_2: | 579 |
582 otrctx.fingerprintAuthCb() | 580 def _dropPrivkey(self, caller, menu_data, profile): |
583 elif otr_version == otr.context.OTR_VERSION_3: | |
584 otrctx.smpAuthCb('question', None, 'asked') | |
585 else: | |
586 not_available() | |
587 | |
588 try: | |
589 to_jid = menu_data['jid'] | |
590 self.fixResource(to_jid, cb) | |
591 except KeyError: | |
592 log.error(_("jid key is not present !")) | |
593 return None | |
594 | |
595 def _dropPrivkey(self, menu_data, profile): | |
596 """Drop our private Key | 581 """Drop our private Key |
597 | 582 |
598 @param menu_data: %(menu_data)s | 583 @param menu_data: %(menu_data)s |
599 @param profile: %(doc_profile)s | 584 @param profile: %(doc_profile)s |
600 """ | 585 """ |
602 if priv_key is None: | 587 if priv_key is None: |
603 # we have no private key yet | 588 # we have no private key yet |
604 dialog.InfoDialog(KEY_NA_TITLE, KEY_NA_TXT, AddStyleName="maxWidthLimit").show() | 589 dialog.InfoDialog(KEY_NA_TITLE, KEY_NA_TXT, AddStyleName="maxWidthLimit").show() |
605 return | 590 return |
606 | 591 |
607 def cb(to_jid): | 592 def dropKey(confirm): |
608 def dropKey(confirm): | 593 if confirm: |
609 if confirm: | 594 # we end all sessions |
610 # we end all sessions | 595 for context in self.context_manager.contexts.values(): |
611 for context in self.context_manager.contexts.values(): | 596 context.disconnect() |
612 context.disconnect() | 597 self.context_manager.contexts.clear() |
613 self.context_manager.contexts.clear() | 598 self.context_manager.account.privkey = None |
614 self.context_manager.account.privkey = None | 599 dialog.InfoDialog(KEY_TITLE, KEY_DROPPED_TXT, AddStyleName="maxWidthLimit").show() |
615 dialog.InfoDialog(KEY_TITLE, KEY_DROPPED_TXT, AddStyleName="maxWidthLimit").show() | 600 |
616 | 601 dialog.ConfirmDialog(dropKey, KEY_DROP_TXT.format(eol=DIALOG_EOL), KEY_DROP_TITLE, AddStyleName="maxWidthLimit").show() |
617 dialog.ConfirmDialog(dropKey, KEY_DROP_TXT.format(eol=DIALOG_EOL), KEY_DROP_TITLE, AddStyleName="maxWidthLimit").show() | |
618 | |
619 try: | |
620 to_jid = menu_data['jid'] | |
621 self.fixResource(to_jid, cb) | |
622 except KeyError: | |
623 log.error(_("jid key is not present !")) | |
624 return None |