groupchat_control.py 84.4 KB
Newer Older
Philipp Hörist's avatar
Philipp Hörist committed
1 2 3 4 5 6 7 8 9 10
# Copyright (C) 2003-2014 Yann Leboulanger <asterix AT lagaule.org>
# Copyright (C) 2005-2007 Nikos Kouremenos <kourem AT gmail.com>
# Copyright (C) 2006 Dimitur Kirov <dkirov AT gmail.com>
#                    Alex Mauer <hawke AT hawkesnest.net>
# Copyright (C) 2006-2008 Jean-Marie Traissard <jim AT lapin.org>
#                         Travis Shirk <travis AT pobox.com>
# Copyright (C) 2007-2008 Julien Pivotto <roidelapluie AT gmail.com>
#                         Stephan Erb <steve-e AT h3c.de>
# Copyright (C) 2008 Brendan Taylor <whateley AT gmail.com>
#                    Jonathan Schleifer <js-gajim AT webkeks.org>
11
# Copyright (C) 2018 Marcin Mielniczuk <marmistrz dot dev at zoho dot eu>
Philipp Hörist's avatar
Philipp Hörist committed
12 13 14 15 16 17 18 19 20 21 22 23 24 25
#
# This file is part of Gajim.
#
# Gajim is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published
# by the Free Software Foundation; version 3 only.
#
# Gajim is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Gajim. If not, see <http://www.gnu.org/licenses/>.
nicfit's avatar
nicfit committed
26

nicfit's avatar
nicfit committed
27
import time
28
import base64
Philipp Hörist's avatar
Philipp Hörist committed
29
import logging
Philipp Hörist's avatar
Philipp Hörist committed
30

Philipp Hörist's avatar
Philipp Hörist committed
31
import nbxmpp
32 33
from nbxmpp.protocol import InvalidJid
from nbxmpp.protocol import validate_resourcepart
Philipp Hörist's avatar
Philipp Hörist committed
34
from nbxmpp.const import StatusCode
Philipp Hörist's avatar
Philipp Hörist committed
35 36
from nbxmpp.const import Affiliation
from nbxmpp.const import PresenceType
37
from nbxmpp.util import is_error_result
38

39
from gi.repository import Gtk
Dicson's avatar
Dicson committed
40
from gi.repository import Gdk
Yann Leboulanger's avatar
Yann Leboulanger committed
41
from gi.repository import GLib
Philipp Hörist's avatar
Philipp Hörist committed
42
from gi.repository import Gio
Philipp Hörist's avatar
Philipp Hörist committed
43

André's avatar
André committed
44 45 46 47
from gajim import gtkgui_helpers
from gajim import gui_menu_builder
from gajim import message_control
from gajim import vcard
48

Philipp Hörist's avatar
Philipp Hörist committed
49
from gajim.common.const import AvatarSize
André's avatar
André committed
50
from gajim.common import events
51
from gajim.common import app
André's avatar
André committed
52
from gajim.common import helpers
Philipp Hörist's avatar
Philipp Hörist committed
53
from gajim.common.helpers import event_filter
54
from gajim.common.helpers import to_user_string
André's avatar
André committed
55
from gajim.common import ged
56
from gajim.common.i18n import _
57
from gajim.common import contacts
58
from gajim.common.const import Chatstate
59
from gajim.common.const import MUCJoinedState
60

André's avatar
André committed
61
from gajim.chat_control_base import ChatControlBase
62

André's avatar
André committed
63 64
from gajim.command_system.implementation.hosts import GroupChatCommands
from gajim.common.connection_handlers_events import GcMessageOutgoingEvent
65

66 67
from gajim.gtk.dialogs import DialogButton
from gajim.gtk.dialogs import NewConfirmationCheckDialog
68
from gajim.gtk.dialogs import ErrorDialog
69
from gajim.gtk.dialogs import NewConfirmationDialog
70
from gajim.gtk.filechoosers import AvatarChooserDialog
71
from gajim.gtk.groupchat_config import GroupchatConfig
72
from gajim.gtk.adhoc import AdHocCommand
73
from gajim.gtk.dataform import DataFormWidget
Philipp Hörist's avatar
Philipp Hörist committed
74
from gajim.gtk.groupchat_info import GroupChatInfoScrolled
Philipp Hörist's avatar
Philipp Hörist committed
75
from gajim.gtk.groupchat_roster import GroupchatRoster
76
from gajim.gtk.util import NickCompletionGenerator
77
from gajim.gtk.util import get_icon_name
78

Philipp Hörist's avatar
Philipp Hörist committed
79

80 81
log = logging.getLogger('gajim.groupchat_control')

82

nicfit's avatar
nicfit committed
83
class GroupchatControl(ChatControlBase):
84 85 86 87 88 89
    TYPE_ID = message_control.TYPE_GC

    # Set a command host to bound to. Every command given through a group chat
    # will be processed with this command host.
    COMMAND_HOST = GroupChatCommands

90
    def __init__(self, parent_win, contact, muc_data, acct):
91
        ChatControlBase.__init__(self, self.TYPE_ID, parent_win,
92
                                 'groupchat_control', contact, acct)
93
        self.force_non_minimizable = False
94 95
        self.is_anonymous = True

Philipp Hörist's avatar
Philipp Hörist committed
96 97 98
        self.emoticons_button = self.xml.get_object('emoticons_button')
        self.toggle_emoticons()

99 100 101
        formattings_button = self.xml.get_object('formattings_button')
        formattings_button.set_sensitive(False)

Philipp Hörist's avatar
Philipp Hörist committed
102 103 104 105 106 107 108 109 110 111 112 113 114
        self.room_jid = self.contact.jid
        self._muc_data = muc_data

        # Stores nickname we want to kick
        self._kick_nick = None

        # Stores nickname we want to ban
        self._ban_jid = None

        self.roster = GroupchatRoster(self.account, self.room_jid, self)
        self.xml.roster_revealer.add(self.roster)
        self.roster.connect('row-activated', self._on_roster_row_activated)

115
        if parent_win is not None:
116
            # On AutoJoin with minimize Groupchats are created without parent
Philipp Hörist's avatar
Philipp Hörist committed
117
            # Tooltip Window and Actions have to be created with parent
Philipp Hörist's avatar
Philipp Hörist committed
118
            self.roster.enable_tooltips()
Philipp Hörist's avatar
Philipp Hörist committed
119
            self.add_actions()
120
            GLib.idle_add(self.update_actions)
121 122 123
            self.scale_factor = parent_win.window.get_scale_factor()
        else:
            self.scale_factor = app.interface.roster.scale_factor
lovetox's avatar
lovetox committed
124

125 126
        if not app.config.get('hide_groupchat_banner'):
            self.xml.banner_eventbox.set_no_show_all(False)
127 128 129 130 131

        # muc attention flag (when we are mentioned in a muc)
        # if True, the room has mentioned us
        self.attention_flag = False

Philipp Hörist's avatar
Philipp Hörist committed
132 133 134
        # True if we initiated room destruction
        self._wait_for_destruction = False

135 136 137
        # sorted list of nicks who mentioned us (last at the end)
        self.attention_list = []
        self.nick_hits = []
Philipp Hörist's avatar
Philipp Hörist committed
138
        self._nick_completion = NickCompletionGenerator(muc_data.nick)
139 140 141 142 143
        self.last_key_tabs = False

        self.name_label = self.xml.get_object('banner_name_label')
        self.event_box = self.xml.get_object('banner_eventbox')

Philipp Hörist's avatar
Philipp Hörist committed
144
        self.setup_seclabel()
145

146 147 148 149 150
        # Send file
        self.sendfile_button = self.xml.get_object('sendfile_button')
        self.sendfile_button.set_action_name('win.send-file-' + \
                                             self.control_id)

151 152 153 154 155 156 157 158 159 160 161
        # Encryption
        self.lock_image = self.xml.get_object('lock_image')
        self.authentication_button = self.xml.get_object(
            'authentication_button')
        id_ = self.authentication_button.connect('clicked',
            self._on_authentication_button_clicked)
        self.handlers[id_] = self.authentication_button
        self.set_lock_image()

        self.encryption_menu = self.xml.get_object('encryption_menu')
        self.encryption_menu.set_menu_model(
162
            gui_menu_builder.get_encryption_menu(self.control_id, self.type_id))
163
        self.set_encryption_menu_icon()
164

Philipp Hörist's avatar
Philipp Hörist committed
165 166 167
        # Banner
        self.banner_actionbar = self.xml.get_object('banner_actionbar')
        self.hide_roster_button = Gtk.Button.new_from_icon_name(
168
            'go-next-symbolic', Gtk.IconSize.MENU)
169
        self.hide_roster_button.set_valign(Gtk.Align.CENTER)
Philipp Hörist's avatar
Philipp Hörist committed
170 171 172 173
        self.hide_roster_button.connect('clicked',
                                        lambda *args: self.show_roster())
        self.banner_actionbar.pack_end(self.hide_roster_button)

Philipp Hörist's avatar
Philipp Hörist committed
174 175 176
        # Holds CaptchaRequest widget
        self._captcha_request = None

Philipp Hörist's avatar
Philipp Hörist committed
177 178 179 180 181
        # MUC Info
        self._subject_data = None
        self._muc_info_box = GroupChatInfoScrolled(self.account, {'width': 600})
        self.xml.info_grid.attach(self._muc_info_box, 0, 0, 1, 1)

182 183 184
        self.control_menu = gui_menu_builder.get_groupchat_menu(self.control_id,
                                                                self.account,
                                                                self.room_jid)
Philipp Hörist's avatar
Philipp Hörist committed
185 186
        settings_menu = self.xml.get_object('settings_menu')
        settings_menu.set_menu_model(self.control_menu)
Philipp Hörist's avatar
Philipp Hörist committed
187

Philipp Hörist's avatar
Philipp Hörist committed
188
        self._event_handlers = [
Philipp Hörist's avatar
Philipp Hörist committed
189
            ('muc-creation-failed', ged.GUI1, self._on_muc_creation_failed),
190
            ('muc-joined', ged.GUI1, self._on_muc_joined),
191
            ('muc-join-failed', ged.GUI1, self._on_muc_join_failed),
Philipp Hörist's avatar
Philipp Hörist committed
192 193 194 195 196 197 198 199
            ('muc-user-joined', ged.GUI1, self._on_user_joined),
            ('muc-user-left', ged.GUI1, self._on_user_left),
            ('muc-nickname-changed', ged.GUI1, self._on_nickname_changed),
            ('muc-self-presence', ged.GUI1, self._on_self_presence),
            ('muc-self-kicked', ged.GUI1, self._on_self_kicked),
            ('muc-user-affiliation-changed', ged.GUI1, self._on_affiliation_changed),
            ('muc-user-status-show-changed', ged.GUI1, self._on_status_show_changed),
            ('muc-user-role-changed', ged.GUI1, self._on_role_changed),
Philipp Hörist's avatar
Philipp Hörist committed
200 201
            ('muc-destroyed', ged.GUI1, self._on_destroyed),
            ('muc-presence-error', ged.GUI1, self._on_presence_error),
202
            ('muc-password-required', ged.GUI1, self._on_password_required),
Philipp Hörist's avatar
Philipp Hörist committed
203 204 205
            ('muc-config-changed', ged.GUI1, self._on_config_changed),
            ('muc-subject', ged.GUI1, self._on_subject),
            ('muc-captcha-challenge', ged.GUI1, self._on_captcha_challenge),
206
            ('muc-captcha-error', ged.GUI1, self._on_captcha_error),
207
            ('muc-voice-request', ged.GUI1, self._on_voice_request),
208 209
            ('muc-disco-update', ged.GUI1, self._on_disco_update),
            ('muc-configuration-finished', ged.GUI1, self._on_configuration_finished),
Philipp Hörist's avatar
Philipp Hörist committed
210
            ('muc-configuration-failed', ged.GUI1, self._on_configuration_failed),
Philipp Hörist's avatar
Philipp Hörist committed
211 212 213 214 215 216
            ('gc-message-received', ged.GUI1, self._nec_gc_message_received),
            ('mam-decrypted-message-received', ged.GUI1, self._nec_mam_decrypted_message_received),
            ('update-room-avatar', ged.GUI1, self._nec_update_room_avatar),
            ('signed-in', ged.GUI1, self._nec_signed_in),
            ('decrypted-message-received', ged.GUI2, self._nec_decrypted_message_received),
            ('gc-stanza-message-outgoing', ged.OUT_POSTCORE, self._message_sent),
217
            ('bookmarks-received', ged.GUI2, self._on_bookmarks_received),
Philipp Hörist's avatar
Philipp Hörist committed
218 219 220 221 222
        ]

        for handler in self._event_handlers:
            app.ged.register_event_handler(*handler)

223
        self.is_connected = False
224 225 226
        # disable win, we are not connected yet
        ChatControlBase.got_disconnected(self)

227 228
        # Stack
        self.xml.stack.show_all()
229 230
        self.xml.stack.set_visible_child_name('progress')
        self.xml.progress_spinner.start()
231

232 233
        self.update_ui()
        self.widget.show_all()
234 235 236 237 238

        if app.config.get('hide_groupchat_occupants_list'):
            # Roster is shown by default, so toggle the roster button to hide it
            self.show_roster()

239 240
        # PluginSystem: adding GUI extension point for this GroupchatControl
        # instance object
241
        app.plugin_manager.gui_extension_point('groupchat_control', self)
242

Philipp Hörist's avatar
Philipp Hörist committed
243 244 245 246
    @property
    def nick(self):
        return self._muc_data.nick

247 248 249 250 251 252
    @property
    def subject(self):
        if self._subject_data is None:
            return ''
        return self._subject_data.subject

253 254 255 256
    @property
    def room_name(self):
        return self.contact.get_shown_name()

257 258 259 260
    @property
    def disco_info(self):
        return app.logger.get_last_disco_info(self.contact.jid)

Philipp Hörist's avatar
Philipp Hörist committed
261
    def add_actions(self):
262
        super().add_actions()
Philipp Hörist's avatar
Philipp Hörist committed
263
        actions = [
Philipp Hörist's avatar
Philipp Hörist committed
264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279
            ('change-subject-', None, self._on_change_subject),
            ('change-nickname-', None, self._on_change_nick),
            ('disconnect-', None, self._on_disconnect),
            ('destroy-', None, self._on_destroy_room),
            ('configure-', None, self._on_configure_room),
            ('request-voice-', None, self._on_request_voice),
            ('upload-avatar-', None, self._on_upload_avatar),
            ('information-', None, self._on_information),
            ('contact-information-', 's', self._on_contact_information),
            ('execute-command-', 's', self._on_execute_command),
            ('block-', 's', self._on_block),
            ('unblock-', 's', self._on_unblock),
            ('ban-', 's', self._on_ban),
            ('kick-', 's', self._on_kick),
            ('change-role-', 'as', self._on_change_role),
            ('change-affiliation-', 'as', self._on_change_affiliation),
280
        ]
Philipp Hörist's avatar
Philipp Hörist committed
281 282

        for action in actions:
Philipp Hörist's avatar
Philipp Hörist committed
283 284 285 286
            action_name, variant, func = action
            if variant is not None:
                variant = GLib.VariantType.new(variant)
            act = Gio.SimpleAction.new(action_name + self.control_id, variant)
Philipp Hörist's avatar
Philipp Hörist committed
287 288 289
            act.connect("activate", func)
            self.parent_win.window.add_action(act)

290 291
        minimize = app.config.get_per(
            'rooms', self.contact.jid, 'minimize_on_close', True)
Philipp Hörist's avatar
Philipp Hörist committed
292 293

        act = Gio.SimpleAction.new_stateful(
294 295 296 297 298 299 300 301 302 303 304 305
            'minimize-on-close-' + self.control_id, None,
            GLib.Variant.new_boolean(minimize))
        act.connect('change-state', self._on_minimize_on_close)
        self.parent_win.window.add_action(act)

        minimize = app.config.get_per(
            'rooms', self.contact.jid, 'minimize_on_autojoin', True)

        act = Gio.SimpleAction.new_stateful(
            'minimize-on-autojoin-' + self.control_id, None,
            GLib.Variant.new_boolean(minimize))
        act.connect('change-state', self._on_minimize_on_autojoin)
Philipp Hörist's avatar
Philipp Hörist committed
306 307
        self.parent_win.window.add_action(act)

308
        default_muc_chatstate = app.config.get('send_chatstate_muc_default')
309
        chatstate = app.config.get_per(
Philipp Hörist's avatar
Philipp Hörist committed
310
            'rooms', self.contact.jid, 'send_chatstate', default_muc_chatstate)
311 312 313 314 315 316 317 318

        act = Gio.SimpleAction.new_stateful(
            'send-chatstate-' + self.control_id,
            GLib.VariantType.new("s"),
            GLib.Variant("s", chatstate))
        act.connect('change-state', self._on_send_chatstate)
        self.parent_win.window.add_action(act)

Philipp Hörist's avatar
Philipp Hörist committed
319
        # Enable notify on all for private rooms
320 321 322 323
        members_only = False
        if self.disco_info is not None:
            members_only = self.disco_info.muc_is_members_only

Philipp Hörist's avatar
Philipp Hörist committed
324
        value = app.config.get_per(
Philipp Hörist's avatar
Philipp Hörist committed
325
            'rooms', self.contact.jid, 'notify_on_all_messages', members_only)
Philipp Hörist's avatar
Philipp Hörist committed
326 327 328 329 330 331 332

        act = Gio.SimpleAction.new_stateful(
            'notify-on-message-' + self.control_id,
            None, GLib.Variant.new_boolean(value))
        act.connect('change-state', self._on_notify_on_all_messages)
        self.parent_win.window.add_action(act)

333
        status_default = app.config.get('print_status_muc_default')
334 335
        value = app.config.get_per('rooms', self.contact.jid,
                                   'print_status', status_default)
Philipp Hörist's avatar
Philipp Hörist committed
336 337 338 339 340 341 342

        act = Gio.SimpleAction.new_stateful(
            'print-status-' + self.control_id,
            None, GLib.Variant.new_boolean(value))
        act.connect('change-state', self._on_print_status)
        self.parent_win.window.add_action(act)

343
        join_default = app.config.get('print_join_left_default')
344 345
        value = app.config.get_per('rooms', self.contact.jid,
                                   'print_join_left', join_default)
Philipp Hörist's avatar
Philipp Hörist committed
346 347 348 349 350 351 352

        act = Gio.SimpleAction.new_stateful(
            'print-join-left-' + self.control_id,
            None, GLib.Variant.new_boolean(value))
        act.connect('change-state', self._on_print_join_left)
        self.parent_win.window.add_action(act)

353 354 355 356 357 358 359 360 361 362 363
        archive_info = app.logger.get_archive_infos(self.contact.jid)
        threshold = helpers.get_sync_threshold(self.contact.jid,
                                               archive_info)

        inital = GLib.Variant.new_string(str(threshold))
        act = Gio.SimpleAction.new_stateful(
            'choose-sync-' + self.control_id,
            inital.get_type(), inital)
        act.connect('change-state', self._on_sync_threshold)
        self.parent_win.window.add_action(act)

Philipp Hörist's avatar
Philipp Hörist committed
364 365 366
    def update_actions(self):
        if self.parent_win is None:
            return
Philipp Hörist's avatar
Philipp Hörist committed
367

Philipp Hörist's avatar
Philipp Hörist committed
368 369
        contact = app.contacts.get_gc_contact(
            self.account, self.room_jid, self.nick)
Philipp Hörist's avatar
Philipp Hörist committed
370
        con = app.connections[self.account]
Philipp Hörist's avatar
Philipp Hörist committed
371 372

        # Destroy Room
373 374
        self._get_action('destroy-').set_enabled(self.is_connected and
                                                 contact.affiliation.is_owner)
Philipp Hörist's avatar
Philipp Hörist committed
375 376

        # Configure Room
377
        self._get_action('configure-').set_enabled(
Philipp Hörist's avatar
Philipp Hörist committed
378 379
            self.is_connected and contact.affiliation in (Affiliation.ADMIN,
                                                          Affiliation.OWNER))
Philipp Hörist's avatar
Philipp Hörist committed
380

381
        self._get_action('request-voice-').set_enabled(self.is_connected and
Philipp Hörist's avatar
Philipp Hörist committed
382
                                                       contact.role.is_visitor)
Philipp Hörist's avatar
Philipp Hörist committed
383 384

        # Change Subject
385
        subject_change = self._is_subject_change_allowed()
386
        self._get_action('change-subject-').set_enabled(self.is_connected and
387
                                                        subject_change)
Philipp Hörist's avatar
Philipp Hörist committed
388 389

        # Change Nick
390
        self._get_action('change-nickname-').set_enabled(self.is_connected)
Philipp Hörist's avatar
Philipp Hörist committed
391

392
        # Execute command
393
        self._get_action('execute-command-').set_enabled(self.is_connected)
394

395
        # Send file (HTTP File Upload)
396 397 398 399 400
        httpupload = self._get_action(
            'send-file-httpupload-')
        httpupload.set_enabled(self.is_connected and
                               con.get_module('HTTPUpload').available)
        self._get_action('send-file-').set_enabled(httpupload.get_enabled())
401

402
        if self.is_connected and httpupload.get_enabled():
403
            tooltip_text = _('Send File…')
404 405 406 407
            max_file_size = con.get_module('HTTPUpload').max_file_size
            if max_file_size is not None:
                max_file_size = max_file_size / (1024 * 1024)
                tooltip_text = _('Send File (max. %s MiB)…') % max_file_size
408 409
        else:
            tooltip_text = _('No File Transfer available')
410 411
        self.sendfile_button.set_tooltip_text(tooltip_text)

412
        # Upload Avatar
413 414 415
        vcard_support = False
        if self.disco_info is not None:
            vcard_support = self.disco_info.supports(nbxmpp.NS_VCARD)
416 417 418 419
        self._get_action('upload-avatar-').set_enabled(
            self.is_connected and
            vcard_support and
            contact.affiliation.is_owner)
420

421 422 423 424 425 426 427 428 429 430 431 432 433 434
        # Print join/left
        join_default = app.config.get('print_join_left_default')
        value = app.config.get_per('rooms', self.contact.jid,
                                   'print_join_left', join_default)
        self._get_action('print-join-left-').set_state(
            GLib.Variant.new_boolean(value))

        # Print join/left
        status_default = app.config.get('print_status_muc_default')
        value = app.config.get_per('rooms', self.contact.jid,
                                   'print_status', status_default)
        self._get_action('print-status-').set_state(
            GLib.Variant.new_boolean(value))

Philipp Hörist's avatar
Philipp Hörist committed
435 436 437 438 439 440 441 442 443 444 445 446 447 448 449
        self._get_action('contact-information-').set_enabled(self.is_connected)

        self._get_action('execute-command-').set_enabled(self.is_connected)

        block_supported = con.get_module('PrivacyLists').supported
        self._get_action('block-').set_enabled(self.is_connected and
                                               block_supported)

        self._get_action('unblock-').set_enabled(self.is_connected and
                                                 block_supported)

        self._get_action('ban-').set_enabled(self.is_connected)

        self._get_action('kick-').set_enabled(self.is_connected)

450 451 452 453 454 455 456 457 458 459 460
    def _is_subject_change_allowed(self):
        contact = app.contacts.get_gc_contact(
            self.account, self.room_jid, self.nick)
        if contact is None:
            return False

        if contact.affiliation in (Affiliation.OWNER, Affiliation.ADMIN):
            return True

        if self.disco_info is None:
            return False
461
        return self.disco_info.muc_subjectmod or False
462

463 464 465
    def _get_action(self, name):
        win = self.parent_win.window
        return win.lookup_action(name + self.control_id)
466

467 468 469 470
    def _show_page(self, name):
        transition = Gtk.StackTransitionType.SLIDE_DOWN
        if name == 'groupchat':
            transition = Gtk.StackTransitionType.SLIDE_UP
471
            self.msg_textview.grab_focus()
472 473 474 475
        if name == 'muc-info':
            # Set focus on the close button, otherwise one of the selectable labels
            # of the GroupchatInfo box gets focus, which means it is fully selected
            self.xml.info_close_button.grab_focus()
476 477
        self.xml.stack.set_visible_child_full(name, transition)

478 479 480
    def _get_current_page(self):
        return self.xml.stack.get_visible_child_name()

481 482
    @event_filter(['account', 'room_jid'])
    def _on_disco_update(self, _event):
483 484 485 486
        if self.parent_win is None:
            return
        win = self.parent_win.window
        self.update_actions()
487
        self.draw_banner_text()
488 489

        # After the room has been created, reevaluate threshold
490
        if self.disco_info.has_mam:
491 492 493 494 495 496
            archive_info = app.logger.get_archive_infos(self.contact.jid)
            threshold = helpers.get_sync_threshold(self.contact.jid,
                                                   archive_info)
            win.change_action_state('choose-sync-%s' % self.control_id,
                                    GLib.Variant('s', str(threshold)))

Philipp Hörist's avatar
Philipp Hörist committed
497 498 499
    # Actions

    def _on_disconnect(self, action, param):
500
        self.leave()
Philipp Hörist's avatar
Philipp Hörist committed
501

Philipp Hörist's avatar
Philipp Hörist committed
502
    def _on_information(self, action, param):
503
        self._muc_info_box.set_from_disco_info(self.disco_info)
Philipp Hörist's avatar
Philipp Hörist committed
504 505 506 507 508 509
        if self._subject_data is not None:
            self._muc_info_box.set_subject(self._subject_data.subject)
            self._muc_info_box.set_author(self._subject_data.nickname,
                                          self._subject_data.user_timestamp)
        self._show_page('muc-info')

510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539
    def _on_destroy_room(self, _action, _param):
        self.xml.destroy_reason_entry.grab_focus()
        self.xml.destroy_button.grab_default()
        self._show_page('destroy')

    def _on_destroy_alternate_changed(self, entry, _param):
        jid = entry.get_text()
        if jid:
            try:
                jid = helpers.validate_jid(jid)
            except Exception:
                icon = 'dialog-warning-symbolic'
                text = _('Invalid XMPP Address')
                self.xml.destroy_alternate_entry.set_icon_from_icon_name(
                    Gtk.EntryIconPosition.SECONDARY, icon)
                self.xml.destroy_alternate_entry.set_icon_tooltip_text(
                    Gtk.EntryIconPosition.SECONDARY, text)
                self.xml.destroy_button.set_sensitive(False)
                return
        self.xml.destroy_alternate_entry.set_icon_from_icon_name(
            Gtk.EntryIconPosition.SECONDARY, None)
        self.xml.destroy_button.set_sensitive(True)

    def _on_destroy_confirm(self, _button):
        reason = self.xml.destroy_reason_entry.get_text()
        jid = self.xml.destroy_alternate_entry.get_text()
        self._wait_for_destruction = True
        con = app.connections[self.account]
        con.get_module('MUC').destroy(self.room_jid, reason, jid)
        self._show_page('groupchat')
Philipp Hörist's avatar
Philipp Hörist committed
540

541
    def _on_configure_room(self, _action, _param):
542 543 544 545 546
        win = app.get_app_window('GroupchatConfig', self.account, self.room_jid)
        if win is not None:
            win.present()
            return

547
        contact = app.contacts.get_gc_contact(
Philipp Hörist's avatar
Philipp Hörist committed
548
            self.account, self.room_jid, self.nick)
Philipp Hörist's avatar
Philipp Hörist committed
549
        if contact.affiliation.is_owner:
550
            con = app.connections[self.account]
551 552
            con.get_module('MUC').request_config(
                self.room_jid, callback=self._on_configure_form_received)
Philipp Hörist's avatar
Philipp Hörist committed
553
        elif contact.affiliation.is_admin:
554 555 556 557 558 559
            GroupchatConfig(self.account,
                            self.room_jid,
                            contact.affiliation.value)

    def _on_configure_form_received(self, result):
        if is_error_result(result):
560
            log.info(result)
561 562
            return
        GroupchatConfig(self.account, result.jid, 'owner', result.form)
Philipp Hörist's avatar
Philipp Hörist committed
563 564 565 566 567 568 569 570 571 572

    def _on_print_join_left(self, action, param):
        action.set_state(param)
        app.config.set_per('rooms', self.contact.jid,
                           'print_join_left', param.get_boolean())

    def _on_print_status(self, action, param):
        action.set_state(param)
        app.config.set_per('rooms', self.contact.jid,
                           'print_status', param.get_boolean())
Philipp Hörist's avatar
Philipp Hörist committed
573 574 575 576 577

    def _on_request_voice(self, action, param):
        """
        Request voice in the current room
        """
Philipp Hörist's avatar
Philipp Hörist committed
578 579
        con = app.connections[self.account]
        con.get_module('MUC').request_voice(self.room_jid)
Philipp Hörist's avatar
Philipp Hörist committed
580

581
    def _on_minimize_on_close(self, action, param):
Philipp Hörist's avatar
Philipp Hörist committed
582
        action.set_state(param)
Philipp Hörist's avatar
Philipp Hörist committed
583 584
        app.config.set_per('rooms', self.contact.jid,
                           'minimize_on_close', param.get_boolean())
Philipp Hörist's avatar
Philipp Hörist committed
585

586 587
    def _on_minimize_on_autojoin(self, action, param):
        action.set_state(param)
Philipp Hörist's avatar
Philipp Hörist committed
588 589
        app.config.set_per('rooms', self.contact.jid,
                           'minimize_on_autojoin', param.get_boolean())
Philipp Hörist's avatar
Philipp Hörist committed
590

591 592 593 594 595
    def _on_send_chatstate(self, action, param):
        action.set_state(param)
        app.config.set_per('rooms', self.contact.jid,
                           'send_chatstate', param.get_string())

Philipp Hörist's avatar
Philipp Hörist committed
596 597 598 599 600
    def _on_notify_on_all_messages(self, action, param):
        action.set_state(param)
        app.config.set_per('rooms', self.contact.jid,
                           'notify_on_all_messages', param.get_boolean())

601 602 603 604 605
    def _on_sync_threshold(self, action, param):
        threshold = param.get_string()
        action.set_state(param)
        app.logger.set_archive_infos(self.contact.jid, sync_threshold=threshold)

Philipp Hörist's avatar
Philipp Hörist committed
606 607 608 609 610 611
    def _on_execute_command(self, _action, param):
        jid = self.room_jid
        nick = param.get_string()
        if nick:
            jid += '/' + nick
        AdHocCommand(self.account, jid)
612

613 614
    def _on_upload_avatar(self, action, param):
        def _on_accept(filename):
Philipp Hörist's avatar
Philipp Hörist committed
615 616
            data, sha = app.interface.avatar_storage.prepare_for_publish(
                filename)
617
            if sha is None:
618
                ErrorDialog(
619 620 621 622
                    _('Could not load image'),
                    transient_for=self.parent_win.window)
                return

Philipp Hörist's avatar
Philipp Hörist committed
623
            avatar = base64.b64encode(data).decode('utf-8')
624 625
            con = app.connections[self.account]
            con.get_module('VCardTemp').upload_room_avatar(
626 627 628 629 630 631
                self.room_jid, avatar)

        AvatarChooserDialog(_on_accept,
                            transient_for=self.parent_win.window,
                            modal=True)

Philipp Hörist's avatar
Philipp Hörist committed
632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687
    def _on_contact_information(self, _action, param):
        nick = param.get_string()
        gc_contact = app.contacts.get_gc_contact(self.account,
                                                 self.room_jid,
                                                 nick)
        contact = gc_contact.as_contact()
        if contact.jid in app.interface.instances[self.account]['infos']:
            app.interface.instances[self.account]['infos'][contact.jid].\
                window.present()
        else:
            app.interface.instances[self.account]['infos'][contact.jid] = \
                vcard.VcardWindow(contact, self.account, gc_contact)

    def _on_block(self, _action, param):
        nick = param.get_string()
        fjid = self.room_jid + '/' + nick
        con = app.connections[self.account]
        con.get_module('PrivacyLists').block_gc_contact(fjid)
        self.roster.draw_contact(nick)

    def _on_unblock(self, _action, param):
        nick = param.get_string()
        fjid = self.room_jid + '/' + nick
        con = app.connections[self.account]
        con.get_module('PrivacyLists').unblock_gc_contact(fjid)
        self.roster.draw_contact(nick)

    def _on_kick(self, _action, param):
        nick = param.get_string()
        self._kick_nick = nick
        self.xml.kick_label.set_text(_('Kick %s' % nick))
        self.xml.kick_reason_entry.grab_focus()
        self.xml.kick_participant_button.grab_default()
        self._show_page('kick')

    def _on_ban(self, _action, param):
        jid = param.get_string()
        self._ban_jid = jid
        nick = app.get_nick_from_jid(jid)
        self.xml.ban_label.set_text(_('Ban %s' % nick))
        self.xml.ban_reason_entry.grab_focus()
        self.xml.ban_participant_button.grab_default()
        self._show_page('ban')

    def _on_change_role(self, _action, param):
        nick, role = param.get_strv()
        con = app.connections[self.account]
        con.get_module('MUC').set_role(self.room_jid, nick, role)

    def _on_change_affiliation(self, _action, param):
        jid, affiliation = param.get_strv()
        con = app.connections[self.account]
        con.get_module('MUC').set_affiliation(
            self.room_jid,
            {jid: {'affiliation': affiliation}})

Philipp Hörist's avatar
Philipp Hörist committed
688
    def show_roster(self):
Philipp Hörist's avatar
Philipp Hörist committed
689 690
        show = not self.xml.roster_revealer.get_reveal_child()
        icon = 'go-next-symbolic' if show else 'go-previous-symbolic'
Philipp Hörist's avatar
Philipp Hörist committed
691
        image = self.hide_roster_button.get_image()
Philipp Hörist's avatar
Philipp Hörist committed
692 693 694 695 696 697 698
        image.set_from_icon_name(icon, Gtk.IconSize.MENU)

        transition = Gtk.RevealerTransitionType.SLIDE_RIGHT
        if show:
            transition = Gtk.RevealerTransitionType.SLIDE_LEFT
        self.xml.roster_revealer.set_transition_type(transition)
        self.xml.roster_revealer.set_reveal_child(show)
Philipp Hörist's avatar
Philipp Hörist committed
699

700
    def on_groupchat_maximize(self):
Philipp Hörist's avatar
Philipp Hörist committed
701
        self.roster.enable_tooltips()
Philipp Hörist's avatar
Philipp Hörist committed
702 703
        self.add_actions()
        self.update_actions()
704
        self.set_lock_image()
705
        self.draw_banner_text()
706

Philipp Hörist's avatar
Philipp Hörist committed
707 708
    def _on_roster_row_activated(self, _roster, nick):
        self._start_private_message(nick)
709 710 711 712 713 714 715

    def on_msg_textview_populate_popup(self, textview, menu):
        """
        Override the default context menu and we prepend Clear
        and the ability to insert a nick
        """
        ChatControlBase.on_msg_textview_populate_popup(self, textview, menu)
Dicson's avatar
Dicson committed
716
        item = Gtk.SeparatorMenuItem.new()
717 718
        menu.prepend(item)

719
        item = Gtk.MenuItem.new_with_label(_('Insert Nickname'))
720
        menu.prepend(item)
721
        submenu = Gtk.Menu()
722 723
        item.set_submenu(submenu)

724
        for nick in sorted(app.contacts.get_nick_list(self.account,
725
        self.room_jid)):
726 727
            item = Gtk.MenuItem.new_with_label(nick)
            item.set_use_underline(False)
728
            submenu.append(item)
729 730
            id_ = item.connect('activate', self.append_nick_in_msg_textview,
                nick)
731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746
            self.handlers[id_] = item

        menu.show_all()

    def get_tab_label(self, chatstate):
        """
        Markup the label if necessary. Returns a tuple such as: (new_label_str,
        color) either of which can be None if chatstate is given that means we
        have HE SENT US a chatstate
        """

        has_focus = self.parent_win.window.get_property('has-toplevel-focus')
        current_tab = self.parent_win.get_active_control() == self
        color = None
        if chatstate == 'attention' and (not has_focus or not current_tab):
            self.attention_flag = True
Philipp Hörist's avatar
Philipp Hörist committed
747
            color = 'tab-muc-directed-msg'
748 749 750 751 752 753
        elif chatstate == 'active' or (current_tab and has_focus):
            self.attention_flag = False
            # get active color from gtk
            color = 'active'
        elif chatstate == 'newmsg' and (not has_focus or not current_tab) \
        and not self.attention_flag:
Philipp Hörist's avatar
Philipp Hörist committed
754
            color = 'tab-muc-msg'
755

756
        label_str = GLib.markup_escape_text(self.room_name)
757 758 759 760 761 762 763

        # count waiting highlighted messages
        unread = ''
        num_unread = self.get_nb_unread()
        if num_unread == 1:
            unread = '*'
        elif num_unread > 1:
Yann Leboulanger's avatar
Yann Leboulanger committed
764
            unread = '[' + str(num_unread) + ']'
765 766 767 768 769
        label_str = unread + label_str
        return (label_str, color)

    def get_tab_image(self, count_unread=True):
        tab_image = None
770
        if self.is_connected:
771
            tab_image = get_icon_name('muc-active')
772
        else:
773
            tab_image = get_icon_name('muc-inactive')
774 775
        return tab_image

776
    def set_lock_image(self):
777
        encryption_state = {'visible': self.encryption is not None,
778 779 780
                            'enc_type': self.encryption,
                            'authenticated': False}

781
        if self.encryption:
782
            app.plugin_manager.extension_point(
783
                'encryption_state' + self.encryption, self, encryption_state)
784 785 786

        self._show_lock_image(**encryption_state)

787
    def _show_lock_image(self, visible, enc_type='', authenticated=False):
788 789 790 791 792
        """
        Set lock icon visibility and create tooltip
        """
        if authenticated:
            authenticated_string = _('and authenticated')
793
            self.lock_image.set_from_icon_name(
794
                'security-high-symbolic', Gtk.IconSize.MENU)
795 796
        else:
            authenticated_string = _('and NOT authenticated')
797
            self.lock_image.set_from_icon_name(
798
                'security-low-symbolic', Gtk.IconSize.MENU)
799 800 801 802 803 804 805 806 807

        tooltip = _('%(type)s encryption is active %(authenticated)s.') % {
            'type': enc_type, 'authenticated': authenticated_string}

        self.authentication_button.set_tooltip_text(tooltip)
        self.widget_set_visible(self.authentication_button, not visible)
        self.lock_image.set_sensitive(visible)

    def _on_authentication_button_clicked(self, widget):
808
        app.plugin_manager.extension_point(
809 810
            'encryption_dialog' + self.encryption, self)

811
    def _update_banner_state_image(self):
812 813 814 815 816 817 818
        surface = app.interface.avatar_storage.get_muc_surface(
            self.account,
            self.contact.jid,
            AvatarSize.CHAT,
            self.scale_factor)

        self.xml.gc_banner_status_image.set_from_surface(surface)
819 820 821 822

    def draw_banner_text(self):
        """
        Draw the text in the fat line at the top of the window that houses the
823
        room jid
824
        """
825
        self.name_label.set_text(self.room_name)
826

Philipp Hörist's avatar
Philipp Hörist committed
827 828 829 830 831
    def _nec_update_room_avatar(self, obj):
        if obj.jid != self.room_jid:
            return
        self._update_banner_state_image()

832 833 834 835 836 837 838
    @event_filter(['account'])
    def _on_bookmarks_received(self, _event):
        if self.parent_win is None:
            return
        self.parent_win.redraw_tab(self)
        self.draw_banner_text()

Philipp Hörist's avatar
Philipp Hörist committed
839
    @event_filter(['account', 'room_jid'])
840 841 842 843 844 845 846 847 848 849 850
    def _on_voice_request(self, event):
        def on_approve():
            con = app.connections[self.account]
            con.get_module('MUC').approve_voice_request(self.room_jid,
                                                        event.voice_request)
        NewConfirmationDialog(
            _('Voice Request'),
            _('Voice Request'),
            _('<b>%s</b> from <b>%s</b> requests voice') % (
                event.voice_request.nick, self.room_name),
            [DialogButton.make('Cancel'),
851 852
             DialogButton.make('Accept',
                               text=_('_Approve'),
853 854
                               callback=on_approve)],
            modal=False).show()
Philipp Hörist's avatar
Philipp Hörist committed
855

Philipp Hörist's avatar
Philipp Hörist committed
856
    @event_filter(['account'])
Philipp Hörist's avatar
Philipp Hörist committed
857 858 859
    def _nec_mam_decrypted_message_received(self, obj):
        if not obj.groupchat:
            return
Philipp Hörist's avatar
Philipp Hörist committed
860
        if obj.archive_jid != self.room_jid:
Philipp Hörist's avatar
Philipp Hörist committed
861
            return
862
        self.add_message(
Philipp Hörist's avatar
Philipp Hörist committed
863
            obj.msgtxt, contact=obj.nick,
864 865
            tim=obj.timestamp, correct_id=obj.correct_id,
            encrypted=obj.encrypted,
866
            message_id=obj.message_id,
Philipp Hörist's avatar
Philipp Hörist committed
867 868
            additional_data=obj.additional_data)

Philipp Hörist's avatar
Philipp Hörist committed
869
    @event_filter(['account', 'room_jid'])
870 871
    def _nec_gc_message_received(self, obj):
        if not obj.nick:
872
            # message from server
873
            self.add_message(
874 875 876
                obj.msgtxt, tim=obj.timestamp,
                xhtml=obj.xhtml_msgtxt, displaymarking=obj.displaymarking,
                additional_data=obj.additional_data)
877
        else:
878 879 880 881 882 883
            if obj.nick == self.nick:
                self.last_sent_txt = obj.msgtxt
            self.add_message(
                obj.msgtxt, contact=obj.nick,
                tim=obj.timestamp, xhtml=obj.xhtml_msgtxt,
                displaymarking=obj.displaymarking, encrypted=obj.encrypted,
884
                correct_id=obj.correct_id, message_id=obj.message_id,
885
                additional_data=obj.additional_data)
886
        obj.needs_highlight = self.needs_visual_notification(obj.msgtxt)
887

888
    def on_private_message(self, nick, sent, msg, tim, xhtml, session, msg_log_id=None,
889
    encrypted=False, displaymarking=None):
890 891 892
        # Do we have a queue?
        fjid = self.room_jid + '/' + nick

893 894
        event = events.PmEvent(msg, '', 'incoming', tim, encrypted, '',
            msg_log_id, xhtml=xhtml, session=session, form_node=None,
895
            displaymarking=displaymarking, sent_forwarded=sent)
896
        app.events.add_event(self.account, fjid, event)
897

898 899
        autopopup = app.config.get('autopopup')
        autopopupaway = app.config.get('autopopupaway')
900
        if not autopopup or (not autopopupaway and \
901
        app.connections[self.account].connected > 2):
Philipp Hörist's avatar
Philipp Hörist committed
902
            self.roster.draw_contact(nick)
903 904 905 906 907
            if self.parent_win:
                self.parent_win.show_title()
                self.parent_win.redraw_tab(self)
        else:
            self._start_private_message(nick)
Philipp Hörist's avatar
Philipp Hörist committed
908

909
        contact = app.contacts.get_contact_with_highest_priority(
910
            self.account, self.room_jid)
911
        if contact:
912
            app.interface.roster.draw_contact(self.room_jid, self.account)
913

914
    def add_message(self, text, contact='', tim=None, xhtml=None,
Philipp Hörist's avatar
Philipp Hörist committed
915 916
                    displaymarking=None, correct_id=None, message_id=None,
                    encrypted=None, additional_data=None):
917
        """
Philipp Hörist's avatar
Philipp Hörist committed
918
        Add message to the ConversationsTextview
919

Philipp Hörist's avatar
Philipp Hörist committed
920
        If contact is set: it's a message from someone
921 922
        If contact is not set: it's a message from the server or help.
        """
Philipp Hörist's avatar
Philipp Hörist committed
923

924 925
        other_tags_for_name = []
        other_tags_for_text = []
926

927 928 929 930
        if not contact:
            # Message from the server
            kind = 'status'
        elif contact == self.nick: # it's us
931
            kind = 'outgoing'
932
        else:
933 934 935 936
            kind = 'incoming'
            # muc-specific chatstate
            if self.parent_win:
                self.parent_win.redraw_tab(self, 'newmsg')
937 938 939

        if kind == 'incoming': # it's a message NOT from us
            # highlighting and sounds
940
            highlight, _sound = self.highlighting_for_message(text, tim)
941
            other_tags_for_name.append('muc_nickname_color_%s' % contact)
942 943 944 945 946 947 948 949 950
            if highlight:
                # muc-specific chatstate
                if self.parent_win:
                    self.parent_win.redraw_tab(self, 'attention')
                else:
                    self.attention_flag = True
                other_tags_for_name.append('bold')
                other_tags_for_text.append('marked')

951
            self._nick_completion.record_message(contact, highlight)
952 953 954

            self.check_and_possibly_add_focus_out_line()

955
        ChatControlBase.add_message(self, text, kind, contact, tim,
956
            other_tags_for_name, [], other_tags_for_text, xhtml=xhtml,
Philipp Hörist's avatar
Philipp Hörist committed
957
            displaymarking=displaymarking,
958
            correct_id=correct_id, message_id=message_id, encrypted=encrypted,
959
            additional_data=additional_data)
960 961 962

    def get_nb_unread(self):
        type_events = ['printed_marked_gc_msg']
Philipp Hörist's avatar
Philipp Hörist committed
963
        if app.config.notify_for_muc(self.room_jid):
964
            type_events.append('printed_gc_msg')
965
        nb = len(app.events.get_events(self.account, self.room_jid,
966
            type_events))
967 968 969 970 971
        nb += self.get_nb_unread_pm()
        return nb

    def get_nb_unread_pm(self):
        nb = 0
972 973
        for nick in app.contacts.get_nick_list(self.account, self.room_jid):
            nb += len(app.events.get_events(self.account, self.room_jid + \
974
                '/' + nick, ['pm']))
975 976 977 978 979 980 981
        return nb

    def highlighting_for_message(self, text, tim):
        """
        Returns a 2-Tuple. The first says whether or not to highlight the text,
        the second, what sound to play
        """
Philipp Hörist's avatar
Philipp Hörist committed
982 983 984 985 986 987
        highlight, sound = None, None

        notify = app.config.notify_for_muc(self.room_jid)
        message_sound_enabled = app.config.get_per('soundevents',
                                                   'muc_message_received',
                                                   'enabled')
988 989 990 991

        # Are any of the defined highlighting words in the text?
        if self.needs_visual_notification(text):
            highlight = True
Philipp Hörist's avatar
Philipp Hörist committed
992 993 994
            if app.config.get_per('soundevents',
                                  'muc_message_highlight',
                                  'enabled'):
995 996 997
                sound = 'highlight'

        # Do we play a sound on every muc message?
Philipp Hörist's avatar
Philipp Hörist committed
998
        elif notify and message_sound_enabled:
999 1000 1001
            sound = 'received'

        # Is it a history message? Don't want sound-floods when we join.
1002
        if tim is not None and time.mktime(time.localtime()) - tim > 1:
1003 1004
            sound = None

Philipp Hörist's avatar
Philipp Hörist committed
1005
        return highlight, sound
1006 1007 1008 1009 1010 1011 1012

    def check_and_possibly_add_focus_out_line(self):
        """
        Check and possibly add focus out line for room_jid if it needs it and
        does not already have it as last event. If it goes to add this line
        - remove previous line first
        """
1013
        win = app.interface.msg_win_mgr.get_window(self.room_jid,
1014
            self.account)
1015 1016 1017 1018 1019 1020 1021
        if win and self.room_jid == win.get_active_jid() and\
        win.window.get_property('has-toplevel-focus') and\
        self.parent_win.get_active_control() == self:
            # it's the current room and it's the focused window.
            # we have full focus (we are reading it!)
            return

Philipp Hörist's avatar
Philipp Hörist committed
1022
        self.conv_textview.show_focus_out_line()
1023 1024 1025 1026 1027 1028

    def needs_visual_notification(self, text):
        """
        Check text to see whether any of the words in (muc_highlight_words and
        nick) appear
        """
1029
        special_words = app.config.get('muc_highlight_words').split(';')
1030
        special_words.append(self.nick)
1031 1032
        con = app.connections[self.account]
        special_words.append(con.get_own_jid().getStripped())
1033 1034 1035 1036 1037 1038 1039
        # Strip empties: ''.split(';') == [''] and would highlight everything.
        # Also lowercase everything for case insensitive compare.
        special_words = [word.lower() for word in special_words if word]
        text = text.lower()

        for special_word in special_words:
            found_here = text.find(special_word)
1040
            while found_here > -1:
1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051
                end_here = found_here + len(special_word)
                if (found_here == 0 or not text[found_here - 1].isalpha()) and \
                (end_here == len(text) or not text[end_here].isalpha()):
                    # It is beginning of text or char before is not alpha AND
                    # it is end of text or char after is not alpha
                    return True
                # continue searching
                start = found_here + 1
                found_here = text.find(special_word, start)
        return False

Philipp Hörist's avatar
Philipp Hörist committed
1052
    @event_filter(['account', 'room_jid'])
Philipp Hörist's avatar
Philipp Hörist committed
1053
    def _on_subject(self, event):
1054
        if self.subject == event.subject or event.is_fake:
1055 1056
            # Probably a rejoin, we already showed that subject
            return
1057 1058 1059

        self._subject_data = event

1060
        text = _('%(nick)s has set the subject to %(subject)s') % {
Philipp Hörist's avatar
Philipp Hörist committed
1061
            'nick': event.nickname, 'subject': event.subject}
1062

Philipp Hörist's avatar
Philipp Hörist committed
1063
        if event.user_timestamp:
1064
            date = time.strftime('%d-%m-%Y %H:%M:%S',
Philipp Hörist's avatar
Philipp Hörist committed
1065
                                 time.localtime(event.user_timestamp))
1066
            text = '%s - %s' % (text, date)
1067

1068
        if (app.config.get('show_subject_on_join') or
1069
                self._muc_data.state != MUCJoinedState.JOINING):
1070
            self.add_info_message(text)
1071

Philipp Hörist's avatar
Philipp Hörist committed
1072
    @event_filter(['account', 'room_jid'])
Philipp Hörist's avatar
Philipp Hörist committed
1073
    def _on_config_changed(self, event):
1074 1075
        # http://www.xmpp.org/extensions/xep-0045.html#roomconfig-notify
        changes = []
Philipp Hörist's avatar
Philipp Hörist committed
1076
        if StatusCode.SHOWING_UNAVAILABLE in event.status_codes:
1077
            changes.append(_('Group chat now shows unavailable members'))
Philipp Hörist's avatar
Philipp Hörist committed
1078

Philipp Hörist's avatar
Philipp Hörist committed
1079
        if StatusCode.NOT_SHOWING_UNAVAILABLE in event.status_codes:
1080
            changes.append(_('Group chat now does not show unavailable members'))
Philipp Hörist's avatar
Philipp Hörist committed
1081

Philipp Hörist's avatar
Philipp Hörist committed
1082
        if StatusCode.CONFIG_NON_PRIVACY_RELATED in event.status_codes:
Yann Leboulanger's avatar