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