changeset 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 557946bf9545
children 61ba5d193cfe
files cagou/plugins/plugin_wid_chat.kv cagou/plugins/plugin_wid_chat.py
diffstat 2 files changed, 152 insertions(+), 37 deletions(-) [+]
line wrap: on
line diff
--- a/cagou/plugins/plugin_wid_chat.kv	Sat Aug 11 19:48:58 2018 +0200
+++ b/cagou/plugins/plugin_wid_chat.kv	Sat Aug 11 18:34:16 2018 +0200
@@ -128,20 +128,26 @@
             width: dp(30)
             on_release: TransferMenu(callback=root.onTransferOK).show(self)
 
-<EncryptionButton>:
+<EncryptionMainButton>:
     size_hint: None, 1
     width: dp(30)
     color: self.getColor()
     symbol: self.getSymbol()
 
-<OtrButton@Button>:
+<EncryptionButton>:
+    group: 'encryption'
     size_hint: None, None
-    size: self.texture_size
+    width: max(self.texture_size[0], self.parent.minimum_width if self.parent else 0)
+    height: self.texture_size[1]
     padding: dp(5), dp(10)
+    color: 0, 0, 0, 1
+    bold: True
+    background_normal: app.expand('{media}/misc/borders/border_filled_black.png')
+    background_color: app.c_sec if self.selected else app.c_prim_dark
 
-<OtrMenu>:
+<EncryptionMenu>:
     size_hint_x: None
-    width: start_btn.width
+    width: self.container.minimum_width
     auto_width: False
     canvas.before:
         Color:
@@ -149,16 +155,16 @@
         Rectangle:
             pos: self.pos
             size: self.size
-    OtrButton:
-        size_hint: 1, None
-        id: start_btn
-        text: _(u"Start/Refresh encrypted session")
-        on_release: root.otr_start()
-    OtrButton:
-        size_hint: 1, None
-        text: _(u"Finish encrypted session")
-        on_release: root.otr_end()
-    OtrButton:
-        size_hint: 1, None
-        text: _(u"Authenticate destinee")
-        on_release: root.otr_authenticate()
+    # EncryptionButton:
+    #     size_hint: 1, None
+    #     id: start_btn
+    #     text: _(u"Start/Refresh encrypted session")
+    #     on_release: root.otr_start()
+    # EncryptionButton:
+    #     size_hint: 1, None
+    #     text: _(u"Finish encrypted session")
+    #     on_release: root.otr_end()
+    # EncryptionButton:
+    #     size_hint: 1, None
+    #     text: _(u"Authenticate destinee")
+    #     on_release: root.otr_authenticate()
--- a/cagou/plugins/plugin_wid_chat.py	Sat Aug 11 19:48:58 2018 +0200
+++ b/cagou/plugins/plugin_wid_chat.py	Sat Aug 11 18:34:16 2018 +0200
@@ -34,8 +34,10 @@
 from cagou.core.image import Image
 from cagou.core.common import SymbolButton, JidButton
 from kivy.uix.dropdown import DropDown
+from kivy.uix.button import Button
 from kivy.core.window import Window
 from cagou import G
+from functools import partial
 import mimetypes
 
 
@@ -54,6 +56,13 @@
 OTR_STATE_ENCRYPTED = 'encrypted'
 OTR_STATE_ENCRYPTION = (OTR_STATE_UNENCRYPTED, OTR_STATE_ENCRYPTED)
 
+SYMBOL_UNENCRYPTED = 'lock-open'
+SYMBOL_ENCRYPTED = 'lock'
+SYMBOL_ENCRYPTED_TRUSTED = 'lock-filled'
+COLOR_UNENCRYPTED = (0.4, 0.4, 0.4, 1)
+COLOR_ENCRYPTED = (0.4, 0.4, 0.4, 1)
+COLOR_ENCRYPTED_TRUSTED = (0.29,0.87,0.0,1)
+
 
 class MessAvatar(Image):
     pass
@@ -131,7 +140,7 @@
     pass
 
 
-class EncryptionButton(SymbolButton):
+class EncryptionMainButton(SymbolButton):
 
     def __init__(self, chat, **kwargs):
         """
@@ -139,9 +148,21 @@
         """
         self.chat = chat
         # for now we do a simple ContextMenu as we have only OTR
-        self.otr_menu = OtrMenu(chat)
-        super(EncryptionButton, self).__init__(**kwargs)
-        self.bind(on_release=self.otr_menu.open)
+        self.encryption_menu = EncryptionMenu(chat)
+        super(EncryptionMainButton, self).__init__(**kwargs)
+        self.bind(on_release=self.encryption_menu.open)
+
+    def selectAlgo(self, name):
+        """Mark an encryption algorithm as selected.
+
+        This will also deselect all other button
+        @param name(unicode, None): encryption plugin name
+            None for plain text
+        """
+        buttons = self.encryption_menu.container.children
+        buttons[-1].selected = name is None
+        for button in buttons[:-1]:
+            button.selected = button.text == name
 
     def getColor(self):
         if self.chat.otr_state_encryption == OTR_STATE_UNENCRYPTED:
@@ -160,14 +181,78 @@
             return 'lock'
 
 
-class OtrMenu(DropDown):
+class EncryptionButton(Button):
+    selected = properties.BooleanProperty(False)
+
+
+class EncryptionMenu(DropDown):
 
     def __init__(self, chat, **kwargs):
         """
         @param chat(Chat): Chat instance
         """
         self.chat = chat
-        super(OtrMenu, self).__init__(**kwargs)
+        super(EncryptionMenu, self).__init__(**kwargs)
+        btn = EncryptionButton(
+            text=_(u"unencrypted (plain text)"),
+            on_release=self.unencrypted,
+            selected=True,
+            bold=False,
+            )
+        self.add_widget(btn)
+        for plugin in G.host.encryption_plugins:
+            btn = EncryptionButton(
+                text=plugin[u'name'],
+                on_release=partial(self.startEncryption, plugin=plugin),
+                )
+            self.add_widget(btn)
+            log.info("added encryption: {}".format(plugin['name']))
+
+    def messageEncryptionStopCb(self):
+        log.info(_(u"Session with {destinee} is now in plain text").format(
+            destinee = self.chat.target))
+
+    def messageEncryptionStopEb(self, failure_):
+        msg = _(u"Error while stopping encryption with {destinee}: {reason}").format(
+            destinee = self.chat.target,
+            reason = failure_)
+        log.warning(msg)
+        G.host.addNote(_(u"encryption problem"), msg, C.XMLUI_DATA_LVL_ERROR)
+
+    def unencrypted(self, button):
+        self.dismiss()
+        G.host.bridge.messageEncryptionStop(
+            unicode(self.chat.target),
+            self.chat.profile,
+            callback=partial(self.messageEncryptionStopCb),
+            errback=partial(self.messageEncryptionStopEb))
+
+    def messageEncryptionStartCb(self, plugin):
+        log.info(_(u"Session with {destinee} is now encrypted with {encr_name}").format(
+            destinee = self.chat.target,
+            encr_name = plugin['name']))
+
+    def messageEncryptionStartEb(self, failure_):
+        msg = _(u"Session can't be encrypted with {destinee}: {reason}").format(
+            destinee = self.chat.target,
+            reason = failure_)
+        log.warning(msg)
+        G.host.addNote(_(u"encryption problem"), msg, C.XMLUI_DATA_LVL_ERROR)
+
+    def startEncryption(self, button, plugin):
+        """Request encryption with given plugin for this session
+
+        @param button(EncryptionButton): button which has been pressed
+        @param plugin(dict): plugin data
+        """
+        self.dismiss()
+        G.host.bridge.messageEncryptionStart(
+            unicode(self.chat.target),
+            plugin['namespace'],
+            True,
+            self.chat.profile,
+            callback=partial(self.messageEncryptionStartCb, plugin=plugin),
+            errback=partial(self.messageEncryptionStartEb))
 
     def otr_start(self):
         self.dismiss()
@@ -207,8 +292,10 @@
     message_input = properties.ObjectProperty()
     messages_widget = properties.ObjectProperty()
 
-    def __init__(self, host, target, type_=C.CHAT_ONE2ONE, nick=None, occupants=None, subject=None, profiles=None):
-        quick_chat.QuickChat.__init__(self, host, target, type_, nick, occupants, subject, profiles=profiles)
+    def __init__(self, host, target, type_=C.CHAT_ONE2ONE, nick=None, occupants=None,
+                 subject=None, profiles=None):
+        quick_chat.QuickChat.__init__(
+            self, host, target, type_, nick, occupants, subject, profiles=profiles)
         self.otr_state_encryption = OTR_STATE_UNENCRYPTED
         self.otr_state_trust = OTR_STATE_UNTRUSTED
         # completion attributes
@@ -218,7 +305,7 @@
         self._hi_comp_allowed = True
         cagou_widget.CagouWidget.__init__(self)
         if type_ == C.CHAT_ONE2ONE:
-            self.encryption_btn = EncryptionButton(self)
+            self.encryption_btn = EncryptionMainButton(self)
             self.headerInputAddExtra(self.encryption_btn)
         self.header_input.hint_text = u"{}".format(target)
         self.host.addListener('progressError', self.onProgressError, profiles)
@@ -242,7 +329,9 @@
             raise NotImplementedError(u"Multi-profiles is not available yet for chat")
         if target is None:
             target = G.host.profiles[profiles[0]].whoami
-        return G.host.widgets.getOrCreateWidget(cls, target, on_new_widget=None, on_existing_widget=C.WIDGET_RECREATE, profiles=profiles)
+        return G.host.widgets.getOrCreateWidget(cls, target, on_new_widget=None,
+                                                on_existing_widget=C.WIDGET_RECREATE,
+                                                profiles=profiles)
 
     ## header ##
 
@@ -269,14 +358,16 @@
         def discoCb(disco):
             # TODO: check if plugin XEP-0045 is activated
             if "conference" in [i[0] for i in disco[1]]:
-                G.host.bridge.mucJoin(unicode(jid_), "", "", self.profile, callback=self._mucJoinCb, errback=self._mucJoinEb)
+                G.host.bridge.mucJoin(unicode(jid_), "", "", self.profile,
+                                      callback=self._mucJoinCb, errback=self._mucJoinEb)
             else:
                 self.changeWidget(jid_)
 
         def discoEb(failure):
             log.warning(u"Disco failure, ignore this text: {}".format(failure))
 
-        G.host.bridge.discoInfos(jid_.domain, self.profile, callback=discoCb, errback=discoEb)
+        G.host.bridge.discoInfos(jid_.domain, self.profile, callback=discoCb,
+                                 errback=discoEb)
 
     def onHeaderInputCompleted(self, input_wid, completed_text):
         self._hi_comp_allowed = False
@@ -369,7 +460,8 @@
         For group chat, note will be added on mention, with a desktop notification if
         window has not focus.
         """
-        visible_clones = [w for w in G.host.getVisibleList(self.__class__) if w.target == self.target]
+        visible_clones = [w for w in G.host.getVisibleList(self.__class__)
+                          if w.target == self.target]
         if len(visible_clones) > 1 and visible_clones.index(self) > 0:
             # to avoid multiple notifications in case of multiple cloned widgets
             # we only handle first clone
@@ -404,7 +496,8 @@
         G.host.messageSend(
             self.target,
             {'': input_widget.text}, # TODO: handle language
-            mess_type = C.MESS_TYPE_GROUPCHAT if self.type == C.CHAT_GROUP else C.MESS_TYPE_CHAT, # TODO: put this in QuickChat
+            mess_type = (C.MESS_TYPE_GROUPCHAT
+                if self.type == C.CHAT_GROUP else C.MESS_TYPE_CHAT), # TODO: put this in QuickChat
             profile_key=self.profile
             )
         input_widget.text = ''
@@ -444,7 +537,8 @@
         G.host.messageSend(
             self.target,
             {'': metadata['url']},
-            mess_type = C.MESS_TYPE_GROUPCHAT if self.type == C.CHAT_GROUP else C.MESS_TYPE_CHAT,
+            mess_type = (C.MESS_TYPE_GROUPCHAT
+                if self.type == C.CHAT_GROUP else C.MESS_TYPE_CHAT),
             extra = extra,
             profile_key=profile
             )
@@ -466,21 +560,35 @@
                 "",
                 {"ignore_tls_errors": C.BOOL_TRUE},  # FIXME: should not be the default
                 self.profile,
-                callback = lambda progress_data: self.fileTransferCb(progress_data, cleaning_cb)
+                callback = lambda progress_data: self.fileTransferCb(
+                    progress_data, cleaning_cb)
                 )
         elif transfer_type == C.TRANSFER_SEND:
             if self.type == C.CHAT_GROUP:
                 log.warning(u"P2P transfer is not possible for group chat")
-                # TODO: show an error dialog to user, or better hide the send button for MUC
+                # TODO: show an error dialog to user, or better hide the send button for
+                #       MUC
             else:
                 jid_ = self.target
                 if not jid_.resource:
                     jid_ = G.host.contact_lists[self.profile].getFullJid(jid_)
-                G.host.bridge.fileSend(unicode(jid_), file_path, "", "", profile=self.profile)
+                G.host.bridge.fileSend(unicode(jid_), file_path, "", "",
+                                       profile=self.profile)
                 # TODO: notification of sending/failing
         else:
             raise log.error(u"transfer of type {} are not handled".format(transfer_type))
 
+    def messageEncryptionStarted(self, plugin_data):
+        quick_chat.QuickChat.messageEncryptionStarted(self, plugin_data)
+        self.encryption_btn.symbol = SYMBOL_ENCRYPTED
+        self.encryption_btn.color = COLOR_ENCRYPTED
+        self.encryption_btn.selectAlgo(plugin_data[u'name'])
+
+    def messageEncryptionStopped(self, plugin_data):
+        quick_chat.QuickChat.messageEncryptionStopped(self, plugin_data)
+        self.encryption_btn.symbol = SYMBOL_UNENCRYPTED
+        self.encryption_btn.color = COLOR_UNENCRYPTED
+        self.encryption_btn.selectAlgo(None)
 
     def _mucJoinCb(self, joined_data):
         joined, room_jid_s, occupants, user_nick, subject, profile = joined_data
@@ -511,7 +619,8 @@
     def onDelete(self, force=False):
         if force==True:
             return self._onDelete()
-        if len(list(G.host.widgets.getWidgets(self.__class__, self.target, profiles=self.profiles))) > 1:
+        if len(list(G.host.widgets.getWidgets(
+            self.__class__, self.target, profiles=self.profiles))) > 1:
             # we don't keep duplicate widgets
             return self._onDelete()
         return False