Mercurial > libervia-desktop-kivy
comparison cagou/plugins/plugin_wid_chat.py @ 233:ba8f3a4a5ac7
plugin chat: e2e encryption improvments:
OTR buttons has been replaced with a more generic one, which use new encryption mechanisms
to retrieve all current encryption algorithms (+ a button for plain text).
"refresh" and "authentify" buttons for OTR are temporarily removed.
Encryption state is checked on chat widget startup, and current encryption method is
selected (it will appear with a different background in encryption menu).
author | Goffi <goffi@goffi.org> |
---|---|
date | Sat, 11 Aug 2018 18:34:16 +0200 |
parents | a3162b29cda1 |
children | c2503168fab7 |
comparison
equal
deleted
inserted
replaced
232:557946bf9545 | 233:ba8f3a4a5ac7 |
---|---|
32 from sat_frontends.tools import jid | 32 from sat_frontends.tools import jid |
33 from cagou.core import cagou_widget | 33 from cagou.core import cagou_widget |
34 from cagou.core.image import Image | 34 from cagou.core.image import Image |
35 from cagou.core.common import SymbolButton, JidButton | 35 from cagou.core.common import SymbolButton, JidButton |
36 from kivy.uix.dropdown import DropDown | 36 from kivy.uix.dropdown import DropDown |
37 from kivy.uix.button import Button | |
37 from kivy.core.window import Window | 38 from kivy.core.window import Window |
38 from cagou import G | 39 from cagou import G |
40 from functools import partial | |
39 import mimetypes | 41 import mimetypes |
40 | 42 |
41 | 43 |
42 PLUGIN_INFO = { | 44 PLUGIN_INFO = { |
43 "name": _(u"chat"), | 45 "name": _(u"chat"), |
51 OTR_STATE_TRUSTED = 'trusted' | 53 OTR_STATE_TRUSTED = 'trusted' |
52 OTR_STATE_TRUST = (OTR_STATE_UNTRUSTED, OTR_STATE_TRUSTED) | 54 OTR_STATE_TRUST = (OTR_STATE_UNTRUSTED, OTR_STATE_TRUSTED) |
53 OTR_STATE_UNENCRYPTED = 'unencrypted' | 55 OTR_STATE_UNENCRYPTED = 'unencrypted' |
54 OTR_STATE_ENCRYPTED = 'encrypted' | 56 OTR_STATE_ENCRYPTED = 'encrypted' |
55 OTR_STATE_ENCRYPTION = (OTR_STATE_UNENCRYPTED, OTR_STATE_ENCRYPTED) | 57 OTR_STATE_ENCRYPTION = (OTR_STATE_UNENCRYPTED, OTR_STATE_ENCRYPTED) |
58 | |
59 SYMBOL_UNENCRYPTED = 'lock-open' | |
60 SYMBOL_ENCRYPTED = 'lock' | |
61 SYMBOL_ENCRYPTED_TRUSTED = 'lock-filled' | |
62 COLOR_UNENCRYPTED = (0.4, 0.4, 0.4, 1) | |
63 COLOR_ENCRYPTED = (0.4, 0.4, 0.4, 1) | |
64 COLOR_ENCRYPTED_TRUSTED = (0.29,0.87,0.0,1) | |
56 | 65 |
57 | 66 |
58 class MessAvatar(Image): | 67 class MessAvatar(Image): |
59 pass | 68 pass |
60 | 69 |
129 | 138 |
130 class MessagesWidget(GridLayout): | 139 class MessagesWidget(GridLayout): |
131 pass | 140 pass |
132 | 141 |
133 | 142 |
134 class EncryptionButton(SymbolButton): | 143 class EncryptionMainButton(SymbolButton): |
135 | 144 |
136 def __init__(self, chat, **kwargs): | 145 def __init__(self, chat, **kwargs): |
137 """ | 146 """ |
138 @param chat(Chat): Chat instance | 147 @param chat(Chat): Chat instance |
139 """ | 148 """ |
140 self.chat = chat | 149 self.chat = chat |
141 # for now we do a simple ContextMenu as we have only OTR | 150 # for now we do a simple ContextMenu as we have only OTR |
142 self.otr_menu = OtrMenu(chat) | 151 self.encryption_menu = EncryptionMenu(chat) |
143 super(EncryptionButton, self).__init__(**kwargs) | 152 super(EncryptionMainButton, self).__init__(**kwargs) |
144 self.bind(on_release=self.otr_menu.open) | 153 self.bind(on_release=self.encryption_menu.open) |
154 | |
155 def selectAlgo(self, name): | |
156 """Mark an encryption algorithm as selected. | |
157 | |
158 This will also deselect all other button | |
159 @param name(unicode, None): encryption plugin name | |
160 None for plain text | |
161 """ | |
162 buttons = self.encryption_menu.container.children | |
163 buttons[-1].selected = name is None | |
164 for button in buttons[:-1]: | |
165 button.selected = button.text == name | |
145 | 166 |
146 def getColor(self): | 167 def getColor(self): |
147 if self.chat.otr_state_encryption == OTR_STATE_UNENCRYPTED: | 168 if self.chat.otr_state_encryption == OTR_STATE_UNENCRYPTED: |
148 return (0.4, 0.4, 0.4, 1) | 169 return (0.4, 0.4, 0.4, 1) |
149 elif self.chat.otr_state_trust == OTR_STATE_TRUSTED: | 170 elif self.chat.otr_state_trust == OTR_STATE_TRUSTED: |
158 return 'lock-filled' | 179 return 'lock-filled' |
159 else: | 180 else: |
160 return 'lock' | 181 return 'lock' |
161 | 182 |
162 | 183 |
163 class OtrMenu(DropDown): | 184 class EncryptionButton(Button): |
185 selected = properties.BooleanProperty(False) | |
186 | |
187 | |
188 class EncryptionMenu(DropDown): | |
164 | 189 |
165 def __init__(self, chat, **kwargs): | 190 def __init__(self, chat, **kwargs): |
166 """ | 191 """ |
167 @param chat(Chat): Chat instance | 192 @param chat(Chat): Chat instance |
168 """ | 193 """ |
169 self.chat = chat | 194 self.chat = chat |
170 super(OtrMenu, self).__init__(**kwargs) | 195 super(EncryptionMenu, self).__init__(**kwargs) |
196 btn = EncryptionButton( | |
197 text=_(u"unencrypted (plain text)"), | |
198 on_release=self.unencrypted, | |
199 selected=True, | |
200 bold=False, | |
201 ) | |
202 self.add_widget(btn) | |
203 for plugin in G.host.encryption_plugins: | |
204 btn = EncryptionButton( | |
205 text=plugin[u'name'], | |
206 on_release=partial(self.startEncryption, plugin=plugin), | |
207 ) | |
208 self.add_widget(btn) | |
209 log.info("added encryption: {}".format(plugin['name'])) | |
210 | |
211 def messageEncryptionStopCb(self): | |
212 log.info(_(u"Session with {destinee} is now in plain text").format( | |
213 destinee = self.chat.target)) | |
214 | |
215 def messageEncryptionStopEb(self, failure_): | |
216 msg = _(u"Error while stopping encryption with {destinee}: {reason}").format( | |
217 destinee = self.chat.target, | |
218 reason = failure_) | |
219 log.warning(msg) | |
220 G.host.addNote(_(u"encryption problem"), msg, C.XMLUI_DATA_LVL_ERROR) | |
221 | |
222 def unencrypted(self, button): | |
223 self.dismiss() | |
224 G.host.bridge.messageEncryptionStop( | |
225 unicode(self.chat.target), | |
226 self.chat.profile, | |
227 callback=partial(self.messageEncryptionStopCb), | |
228 errback=partial(self.messageEncryptionStopEb)) | |
229 | |
230 def messageEncryptionStartCb(self, plugin): | |
231 log.info(_(u"Session with {destinee} is now encrypted with {encr_name}").format( | |
232 destinee = self.chat.target, | |
233 encr_name = plugin['name'])) | |
234 | |
235 def messageEncryptionStartEb(self, failure_): | |
236 msg = _(u"Session can't be encrypted with {destinee}: {reason}").format( | |
237 destinee = self.chat.target, | |
238 reason = failure_) | |
239 log.warning(msg) | |
240 G.host.addNote(_(u"encryption problem"), msg, C.XMLUI_DATA_LVL_ERROR) | |
241 | |
242 def startEncryption(self, button, plugin): | |
243 """Request encryption with given plugin for this session | |
244 | |
245 @param button(EncryptionButton): button which has been pressed | |
246 @param plugin(dict): plugin data | |
247 """ | |
248 self.dismiss() | |
249 G.host.bridge.messageEncryptionStart( | |
250 unicode(self.chat.target), | |
251 plugin['namespace'], | |
252 True, | |
253 self.chat.profile, | |
254 callback=partial(self.messageEncryptionStartCb, plugin=plugin), | |
255 errback=partial(self.messageEncryptionStartEb)) | |
171 | 256 |
172 def otr_start(self): | 257 def otr_start(self): |
173 self.dismiss() | 258 self.dismiss() |
174 G.host.launchMenu( | 259 G.host.launchMenu( |
175 C.MENU_SINGLE, | 260 C.MENU_SINGLE, |
205 | 290 |
206 class Chat(quick_chat.QuickChat, cagou_widget.CagouWidget): | 291 class Chat(quick_chat.QuickChat, cagou_widget.CagouWidget): |
207 message_input = properties.ObjectProperty() | 292 message_input = properties.ObjectProperty() |
208 messages_widget = properties.ObjectProperty() | 293 messages_widget = properties.ObjectProperty() |
209 | 294 |
210 def __init__(self, host, target, type_=C.CHAT_ONE2ONE, nick=None, occupants=None, subject=None, profiles=None): | 295 def __init__(self, host, target, type_=C.CHAT_ONE2ONE, nick=None, occupants=None, |
211 quick_chat.QuickChat.__init__(self, host, target, type_, nick, occupants, subject, profiles=profiles) | 296 subject=None, profiles=None): |
297 quick_chat.QuickChat.__init__( | |
298 self, host, target, type_, nick, occupants, subject, profiles=profiles) | |
212 self.otr_state_encryption = OTR_STATE_UNENCRYPTED | 299 self.otr_state_encryption = OTR_STATE_UNENCRYPTED |
213 self.otr_state_trust = OTR_STATE_UNTRUSTED | 300 self.otr_state_trust = OTR_STATE_UNTRUSTED |
214 # completion attributes | 301 # completion attributes |
215 self._hi_comp_data = None | 302 self._hi_comp_data = None |
216 self._hi_comp_last = None | 303 self._hi_comp_last = None |
217 self._hi_comp_dropdown = DropDown() | 304 self._hi_comp_dropdown = DropDown() |
218 self._hi_comp_allowed = True | 305 self._hi_comp_allowed = True |
219 cagou_widget.CagouWidget.__init__(self) | 306 cagou_widget.CagouWidget.__init__(self) |
220 if type_ == C.CHAT_ONE2ONE: | 307 if type_ == C.CHAT_ONE2ONE: |
221 self.encryption_btn = EncryptionButton(self) | 308 self.encryption_btn = EncryptionMainButton(self) |
222 self.headerInputAddExtra(self.encryption_btn) | 309 self.headerInputAddExtra(self.encryption_btn) |
223 self.header_input.hint_text = u"{}".format(target) | 310 self.header_input.hint_text = u"{}".format(target) |
224 self.host.addListener('progressError', self.onProgressError, profiles) | 311 self.host.addListener('progressError', self.onProgressError, profiles) |
225 self.host.addListener('progressFinished', self.onProgressFinished, profiles) | 312 self.host.addListener('progressFinished', self.onProgressFinished, profiles) |
226 self._waiting_pids = {} # waiting progress ids | 313 self._waiting_pids = {} # waiting progress ids |
240 profiles = list(profiles) | 327 profiles = list(profiles) |
241 if len(profiles) > 1: | 328 if len(profiles) > 1: |
242 raise NotImplementedError(u"Multi-profiles is not available yet for chat") | 329 raise NotImplementedError(u"Multi-profiles is not available yet for chat") |
243 if target is None: | 330 if target is None: |
244 target = G.host.profiles[profiles[0]].whoami | 331 target = G.host.profiles[profiles[0]].whoami |
245 return G.host.widgets.getOrCreateWidget(cls, target, on_new_widget=None, on_existing_widget=C.WIDGET_RECREATE, profiles=profiles) | 332 return G.host.widgets.getOrCreateWidget(cls, target, on_new_widget=None, |
333 on_existing_widget=C.WIDGET_RECREATE, | |
334 profiles=profiles) | |
246 | 335 |
247 ## header ## | 336 ## header ## |
248 | 337 |
249 def changeWidget(self, jid_): | 338 def changeWidget(self, jid_): |
250 """change current widget for a new one with given jid | 339 """change current widget for a new one with given jid |
267 return | 356 return |
268 | 357 |
269 def discoCb(disco): | 358 def discoCb(disco): |
270 # TODO: check if plugin XEP-0045 is activated | 359 # TODO: check if plugin XEP-0045 is activated |
271 if "conference" in [i[0] for i in disco[1]]: | 360 if "conference" in [i[0] for i in disco[1]]: |
272 G.host.bridge.mucJoin(unicode(jid_), "", "", self.profile, callback=self._mucJoinCb, errback=self._mucJoinEb) | 361 G.host.bridge.mucJoin(unicode(jid_), "", "", self.profile, |
362 callback=self._mucJoinCb, errback=self._mucJoinEb) | |
273 else: | 363 else: |
274 self.changeWidget(jid_) | 364 self.changeWidget(jid_) |
275 | 365 |
276 def discoEb(failure): | 366 def discoEb(failure): |
277 log.warning(u"Disco failure, ignore this text: {}".format(failure)) | 367 log.warning(u"Disco failure, ignore this text: {}".format(failure)) |
278 | 368 |
279 G.host.bridge.discoInfos(jid_.domain, self.profile, callback=discoCb, errback=discoEb) | 369 G.host.bridge.discoInfos(jid_.domain, self.profile, callback=discoCb, |
370 errback=discoEb) | |
280 | 371 |
281 def onHeaderInputCompleted(self, input_wid, completed_text): | 372 def onHeaderInputCompleted(self, input_wid, completed_text): |
282 self._hi_comp_allowed = False | 373 self._hi_comp_allowed = False |
283 input_wid.text = completed_text | 374 input_wid.text = completed_text |
284 self._hi_comp_allowed = True | 375 self._hi_comp_allowed = True |
367 or when one2one chat is not visible. A note is also there when widget | 458 or when one2one chat is not visible. A note is also there when widget |
368 is not visible. | 459 is not visible. |
369 For group chat, note will be added on mention, with a desktop notification if | 460 For group chat, note will be added on mention, with a desktop notification if |
370 window has not focus. | 461 window has not focus. |
371 """ | 462 """ |
372 visible_clones = [w for w in G.host.getVisibleList(self.__class__) if w.target == self.target] | 463 visible_clones = [w for w in G.host.getVisibleList(self.__class__) |
464 if w.target == self.target] | |
373 if len(visible_clones) > 1 and visible_clones.index(self) > 0: | 465 if len(visible_clones) > 1 and visible_clones.index(self) > 0: |
374 # to avoid multiple notifications in case of multiple cloned widgets | 466 # to avoid multiple notifications in case of multiple cloned widgets |
375 # we only handle first clone | 467 # we only handle first clone |
376 return | 468 return |
377 is_visible = bool(visible_clones) | 469 is_visible = bool(visible_clones) |
402 | 494 |
403 def onSend(self, input_widget): | 495 def onSend(self, input_widget): |
404 G.host.messageSend( | 496 G.host.messageSend( |
405 self.target, | 497 self.target, |
406 {'': input_widget.text}, # TODO: handle language | 498 {'': input_widget.text}, # TODO: handle language |
407 mess_type = C.MESS_TYPE_GROUPCHAT if self.type == C.CHAT_GROUP else C.MESS_TYPE_CHAT, # TODO: put this in QuickChat | 499 mess_type = (C.MESS_TYPE_GROUPCHAT |
500 if self.type == C.CHAT_GROUP else C.MESS_TYPE_CHAT), # TODO: put this in QuickChat | |
408 profile_key=self.profile | 501 profile_key=self.profile |
409 ) | 502 ) |
410 input_widget.text = '' | 503 input_widget.text = '' |
411 | 504 |
412 def onProgressFinished(self, progress_id, metadata, profile): | 505 def onProgressFinished(self, progress_id, metadata, profile): |
442 extra['xhtml'] = u"<img src='{url}' />".format(**metadata) | 535 extra['xhtml'] = u"<img src='{url}' />".format(**metadata) |
443 | 536 |
444 G.host.messageSend( | 537 G.host.messageSend( |
445 self.target, | 538 self.target, |
446 {'': metadata['url']}, | 539 {'': metadata['url']}, |
447 mess_type = C.MESS_TYPE_GROUPCHAT if self.type == C.CHAT_GROUP else C.MESS_TYPE_CHAT, | 540 mess_type = (C.MESS_TYPE_GROUPCHAT |
541 if self.type == C.CHAT_GROUP else C.MESS_TYPE_CHAT), | |
448 extra = extra, | 542 extra = extra, |
449 profile_key=profile | 543 profile_key=profile |
450 ) | 544 ) |
451 | 545 |
452 def fileTransferCb(self, progress_data, cleaning_cb): | 546 def fileTransferCb(self, progress_data, cleaning_cb): |
464 file_path, | 558 file_path, |
465 "", | 559 "", |
466 "", | 560 "", |
467 {"ignore_tls_errors": C.BOOL_TRUE}, # FIXME: should not be the default | 561 {"ignore_tls_errors": C.BOOL_TRUE}, # FIXME: should not be the default |
468 self.profile, | 562 self.profile, |
469 callback = lambda progress_data: self.fileTransferCb(progress_data, cleaning_cb) | 563 callback = lambda progress_data: self.fileTransferCb( |
564 progress_data, cleaning_cb) | |
470 ) | 565 ) |
471 elif transfer_type == C.TRANSFER_SEND: | 566 elif transfer_type == C.TRANSFER_SEND: |
472 if self.type == C.CHAT_GROUP: | 567 if self.type == C.CHAT_GROUP: |
473 log.warning(u"P2P transfer is not possible for group chat") | 568 log.warning(u"P2P transfer is not possible for group chat") |
474 # TODO: show an error dialog to user, or better hide the send button for MUC | 569 # TODO: show an error dialog to user, or better hide the send button for |
570 # MUC | |
475 else: | 571 else: |
476 jid_ = self.target | 572 jid_ = self.target |
477 if not jid_.resource: | 573 if not jid_.resource: |
478 jid_ = G.host.contact_lists[self.profile].getFullJid(jid_) | 574 jid_ = G.host.contact_lists[self.profile].getFullJid(jid_) |
479 G.host.bridge.fileSend(unicode(jid_), file_path, "", "", profile=self.profile) | 575 G.host.bridge.fileSend(unicode(jid_), file_path, "", "", |
576 profile=self.profile) | |
480 # TODO: notification of sending/failing | 577 # TODO: notification of sending/failing |
481 else: | 578 else: |
482 raise log.error(u"transfer of type {} are not handled".format(transfer_type)) | 579 raise log.error(u"transfer of type {} are not handled".format(transfer_type)) |
483 | 580 |
581 def messageEncryptionStarted(self, plugin_data): | |
582 quick_chat.QuickChat.messageEncryptionStarted(self, plugin_data) | |
583 self.encryption_btn.symbol = SYMBOL_ENCRYPTED | |
584 self.encryption_btn.color = COLOR_ENCRYPTED | |
585 self.encryption_btn.selectAlgo(plugin_data[u'name']) | |
586 | |
587 def messageEncryptionStopped(self, plugin_data): | |
588 quick_chat.QuickChat.messageEncryptionStopped(self, plugin_data) | |
589 self.encryption_btn.symbol = SYMBOL_UNENCRYPTED | |
590 self.encryption_btn.color = COLOR_UNENCRYPTED | |
591 self.encryption_btn.selectAlgo(None) | |
484 | 592 |
485 def _mucJoinCb(self, joined_data): | 593 def _mucJoinCb(self, joined_data): |
486 joined, room_jid_s, occupants, user_nick, subject, profile = joined_data | 594 joined, room_jid_s, occupants, user_nick, subject, profile = joined_data |
487 self.host.mucRoomJoinedHandler(*joined_data[1:]) | 595 self.host.mucRoomJoinedHandler(*joined_data[1:]) |
488 jid_ = jid.JID(room_jid_s) | 596 jid_ = jid.JID(room_jid_s) |
509 self.encryption_btn.color = self.encryption_btn.getColor() | 617 self.encryption_btn.color = self.encryption_btn.getColor() |
510 | 618 |
511 def onDelete(self, force=False): | 619 def onDelete(self, force=False): |
512 if force==True: | 620 if force==True: |
513 return self._onDelete() | 621 return self._onDelete() |
514 if len(list(G.host.widgets.getWidgets(self.__class__, self.target, profiles=self.profiles))) > 1: | 622 if len(list(G.host.widgets.getWidgets( |
623 self.__class__, self.target, profiles=self.profiles))) > 1: | |
515 # we don't keep duplicate widgets | 624 # we don't keep duplicate widgets |
516 return self._onDelete() | 625 return self._onDelete() |
517 return False | 626 return False |
518 | 627 |
519 | 628 |