roster_window.py 220 KB
Newer Older
Philipp Hörist's avatar
Philipp Hörist committed
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
# Copyright (C) 2003-2014 Yann Leboulanger <asterix AT lagaule.org>
# Copyright (C) 2005 Alex Mauer <hawke AT hawkesnest.net>
#                    Stéphan Kochen <stephan AT kochen.nl>
# Copyright (C) 2005-2006 Dimitur Kirov <dkirov AT gmail.com>
# Copyright (C) 2005-2007 Travis Shirk <travis AT pobox.com>
#                         Nikos Kouremenos <kourem AT gmail.com>
# Copyright (C) 2006 Stefan Bethge <stefan AT lanpartei.de>
# Copyright (C) 2006-2008 Jean-Marie Traissard <jim AT lapin.org>
# Copyright (C) 2007 Lukas Petrovicky <lukas AT petrovicky.net>
#                    James Newton <redshodan AT gmail.com>
#                    Tomasz Melcer <liori AT exroot.org>
#                    Julien Pivotto <roidelapluie AT gmail.com>
# Copyright (C) 2007-2008 Stephan Erb <steve-e AT h3c.de>
# Copyright (C) 2008 Brendan Taylor <whateley AT gmail.com>
#                    Jonathan Schleifer <js-gajim AT webkeks.org>
#
# 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/>.
30

31 32 33 34 35 36 37
import os
import sys
import time
import locale
import logging
from enum import IntEnum, unique

38 39 40 41
from gi.repository import Gtk
from gi.repository import Gdk
from gi.repository import Pango
from gi.repository import GObject
Yann Leboulanger's avatar
Yann Leboulanger committed
42
from gi.repository import GLib
43
from gi.repository import Gio
Philipp Hörist's avatar
Philipp Hörist committed
44
from nbxmpp.namespaces import Namespace
45
from nbxmpp.structs import MoodData
Philipp Hörist's avatar
Philipp Hörist committed
46
from nbxmpp.structs import ActivityData
47

André's avatar
André committed
48 49 50 51
from gajim import dialogs
from gajim import vcard
from gajim import gtkgui_helpers
from gajim import gui_menu_builder
Philipp Hörist's avatar
Philipp Hörist committed
52

53
from gajim.common import app
André's avatar
André committed
54 55 56
from gajim.common import helpers
from gajim.common.exceptions import GajimGeneralException
from gajim.common import i18n
57
from gajim.common.helpers import save_roster_position
58
from gajim.common.i18n import _
Philipp Hörist's avatar
Philipp Hörist committed
59
from gajim.common.const import PEPEventType, AvatarSize, StyleAttr
Philipp Hörist's avatar
Philipp Hörist committed
60 61
from gajim.common.dbus import location

André's avatar
André committed
62 63
from gajim.common import ged
from gajim.message_window import MessageWindowMgr
64

65 66 67
from gajim.gtk.dialogs import DialogButton
from gajim.gtk.dialogs import NewConfirmationDialog
from gajim.gtk.dialogs import NewConfirmationCheckDialog
68 69 70 71
from gajim.gtk.dialogs import ErrorDialog
from gajim.gtk.dialogs import InputDialog
from gajim.gtk.dialogs import WarningDialog
from gajim.gtk.dialogs import InformationDialog
72
from gajim.gtk.dialogs import InvitationReceivedDialog
73 74 75
from gajim.gtk.single_message import SingleMessageWindow
from gajim.gtk.add_contact import AddNewContactWindow
from gajim.gtk.service_registration import ServiceRegistration
76
from gajim.gtk.discovery import ServiceDiscoveryWindow
77
from gajim.gtk.tooltips import RosterTooltip
78
from gajim.gtk.adhoc import AdHocCommand
79
from gajim.gtk.util import get_icon_name
80
from gajim.gtk.util import resize_window
81
from gajim.gtk.util import restore_roster_position
82
from gajim.gtk.util import get_metacontact_surface
83
from gajim.gtk.util import get_builder
84
from gajim.gtk.util import set_urgency_hint
Philipp Hörist's avatar
Philipp Hörist committed
85
from gajim.gtk.util import get_activity_icon_name
86
from gajim.gtk.util import open_window
87

88

89 90
log = logging.getLogger('gajim.roster')

91
@unique
92 93
class Column(IntEnum):
    IMG = 0  # image to show state (online, new message etc)
Daniel Brötzmann's avatar
Daniel Brötzmann committed
94
    NAME = 1  # cellrenderer text that holds contact nickname
95 96 97 98 99
    TYPE = 2  # account, group or contact?
    JID = 3  # the jid of the row
    ACCOUNT = 4  # cellrenderer text that holds account name
    MOOD_PIXBUF = 5
    ACTIVITY_PIXBUF = 6
100 101
    TUNE_ICON = 7
    LOCATION_ICON = 8
102
    AVATAR_IMG = 9  # avatar_sha
103
    PADLOCK_PIXBUF = 10  # use for account row only
104
    VISIBLE = 11
105

Dicson's avatar
Dicson committed
106

107
class RosterWindow:
108
    """
Daniel Brötzmann's avatar
Daniel Brötzmann committed
109
    Class for main window of the GTK interface
110 111 112 113
    """

    def _get_account_iter(self, name, model=None):
        """
114
        Return the Gtk.TreeIter of the given account or None if not found
115 116 117 118 119 120

        Keyword arguments:
        name -- the account name
        model -- the data model (default TreeFilterModel)
        """
        if model is None:
121 122 123 124
            model = self.modelfilter
            if model is None:
                return

125
        if self.regroup:
126
            name = 'MERGED'
127 128
        if name not in self._iters:
            return None
129
        it = self._iters[name]['account']
130 131 132 133

        if model == self.model or it is None:
            return it
        try:
134 135 136 137
            (ok, it) = self.modelfilter.convert_child_iter_to_iter(it)
            if ok:
                return it
            return None
138 139
        except RuntimeError:
            return None
140 141


142
    def _get_group_iter(self, name, account, model=None):
143
        """
144
        Return the Gtk.TreeIter of the given group or None if not found
145 146 147 148 149 150

        Keyword arguments:
        name -- the group name
        account -- the account name
        model -- the data model (default TreeFilterModel)
        """
151
        if model is None:
152
            model = self.modelfilter
153 154 155
            if model is None:
                return

156 157 158
        if self.regroup:
            account = 'MERGED'

159 160
        if account not in self._iters:
            return None
161 162 163 164 165 166 167
        if name not in self._iters[account]['groups']:
            return None

        it = self._iters[account]['groups'][name]
        if model == self.model or it is None:
            return it
        try:
168 169 170 171
            (ok, it) = self.modelfilter.convert_child_iter_to_iter(it)
            if ok:
                return it
            return None
172 173
        except RuntimeError:
            return None
174 175 176 177


    def _get_self_contact_iter(self, account, model=None):
        """
178
        Return the Gtk.TreeIter of SelfContact or None if not found
179 180 181 182 183

        Keyword arguments:
        account -- the account of SelfContact
        model -- the data model (default TreeFilterModel)
        """
184
        jid = app.get_jid_from_account(account)
185 186 187
        its = self._get_contact_iter(jid, account, model=model)
        if its:
            return its[0]
188 189 190 191 192
        return None


    def _get_contact_iter(self, jid, account, contact=None, model=None):
        """
193
        Return a list of Gtk.TreeIter of the given contact
194 195 196 197 198 199 200

        Keyword arguments:
        jid -- the jid without resource
        account -- the account
        contact -- the contact (default None)
        model -- the data model (default TreeFilterModel)
        """
201
        if model is None:
202 203 204 205 206 207
            model = self.modelfilter
            # when closing Gajim model can be none (async pbs?)
            if model is None:
                return []

        if not contact:
208
            contact = app.contacts.get_first_contact_from_jid(account, jid)
209 210 211 212
            if not contact:
                # We don't know this contact
                return []

213 214 215
        if account not in self._iters:
            return []

216 217 218 219 220 221 222 223 224 225 226 227 228 229
        if jid not in self._iters[account]['contacts']:
            return []

        its = self._iters[account]['contacts'][jid]

        if not its:
            return []

        if model == self.model:
            return its

        its2 = []
        for it in its:
            try:
230 231 232
                (ok, it) = self.modelfilter.convert_child_iter_to_iter(it)
                if ok:
                    its2.append(it)
233 234 235
            except RuntimeError:
                pass
        return its2
236

Philipp Hörist's avatar
Philipp Hörist committed
237 238
    @staticmethod
    def _iter_is_separator(model, titer):
239 240 241 242 243
        """
        Return True if the given iter is a separator

        Keyword arguments:
        model -- the data model
244
        iter -- the Gtk.TreeIter to test
245 246 247 248 249
        """
        if model[titer][0] == 'SEPARATOR':
            return True
        return False

Philipp Hörist's avatar
Philipp Hörist committed
250 251 252 253 254 255 256 257 258
    @staticmethod
    def _status_cell_data_func(cell_layout, cell, tree_model, iter_):
        if isinstance(cell, Gtk.CellRendererPixbuf):
            icon_name = tree_model[iter_][1]
            if icon_name is None:
                return
            if tree_model[iter_][2] == 'status':
                cell.set_property('icon_name', icon_name)
            else:
259
                iconset_name = get_icon_name(icon_name)
Philipp Hörist's avatar
Philipp Hörist committed
260 261 262 263 264 265 266 267 268
                cell.set_property('icon_name', iconset_name)
        else:
            show = tree_model[iter_][0]
            id_ = tree_model[iter_][2]
            if id_ not in ('status', 'desync'):
                show = helpers.get_uf_show(show)
            cell.set_property('text', show)


269

Yann Leboulanger's avatar
Yann Leboulanger committed
270
#############################################################################
271
### Methods for adding and removing roster window items
steve-e's avatar
steve-e committed
272
#############################################################################
Yann Leboulanger's avatar
Yann Leboulanger committed
273

274 275 276 277 278 279 280 281 282 283 284
    def add_account(self, account):
        """
        Add account to roster and draw it. Do nothing if it is already in
        """
        if self._get_account_iter(account):
            # Will happen on reconnect or for merged accounts
            return

        if self.regroup:
            # Merged accounts view
            show = helpers.get_global_show()
285
            it = self.model.append(None, [get_icon_name(show),
286
                _('Merged accounts'), 'account', '', 'all', None, None, None,
287
                None, None, None, True] + [None] * self.nb_ext_renderers)
288
            self._iters['MERGED']['account'] = it
289
        else:
290
            show = helpers.get_connection_status(account)
291
            our_jid = app.get_jid_from_account(account)
292

293
            it = self.model.append(None, [get_icon_name(show),
Yann Leboulanger's avatar
Yann Leboulanger committed
294
                GLib.markup_escape_text(account), 'account', our_jid,
Philipp Hörist's avatar
Philipp Hörist committed
295
                account, None, None, None, None, None, None, True] +
296
                [None] * self.nb_ext_renderers)
297
            self._iters[account]['account'] = it
298 299 300 301

        self.draw_account(account)


302 303
    def add_account_contacts(self, account, improve_speed=True,
    draw_contacts=True):
304
        """
305 306
        Add all contacts and groups of the given account to roster, draw them
        and account
307
        """
308 309
        if improve_speed:
            self._before_fill()
310
        jids = app.contacts.get_jid_list(account)
311 312 313 314

        for jid in jids:
            self.add_contact(jid, account)

315 316 317 318 319
        if draw_contacts:
            # Do not freeze the GUI when drawing the contacts
            if jids:
                # Overhead is big, only invoke when needed
                self._idle_draw_jids_of_account(jids, account)
320

321
            # Draw all known groups
322
            for group in app.groups[account]:
323 324
                self.draw_group(group, account)
            self.draw_account(account)
325

326 327
        if improve_speed:
            self._after_fill()
328

329 330 331 332 333 334 335 336
    def _add_group_iter(self, account, group):
        """
        Add a group iter in roster and return the newly created iter
        """
        if self.regroup:
            account_group = 'MERGED'
        else:
            account_group = account
Philipp Hörist's avatar
Philipp Hörist committed
337
        delimiter = app.connections[account].get_module('Delimiter').delimiter
338 339
        group_splited = group.split(delimiter)
        parent_group = delimiter.join(group_splited[:-1])
340
        if len(group_splited) > 1 and parent_group in self._iters[account_group]['groups']:
341 342 343
            iter_parent = self._iters[account_group]['groups'][parent_group]
        elif parent_group:
            iter_parent = self._add_group_iter(account, parent_group)
344
            if parent_group not in app.groups[account]:
345 346 347 348
                if account + parent_group in self.collapsed_rows:
                    is_expanded = False
                else:
                    is_expanded = True
349
                app.groups[account][parent_group] = {'expand': is_expanded}
350 351 352
        else:
            iter_parent = self._get_account_iter(account, self.model)
        iter_group = self.model.append(iter_parent,
353
            [get_icon_name('closed'),
Yann Leboulanger's avatar
Yann Leboulanger committed
354
            GLib.markup_escape_text(group), 'group', group, account, None,
355
            None, None, None, None, None, False] + [None] * self.nb_ext_renderers)
356 357 358
        self.draw_group(group, account)
        self._iters[account_group]['groups'][group] = iter_group
        return iter_group
359 360

    def _add_entity(self, contact, account, groups=None,
361
    big_brother_contact=None, big_brother_account=None):
362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377
        """
        Add the given contact to roster data model

        Contact is added regardless if he is already in roster or not. Return
        list of newly added iters.

        Keyword arguments:
        contact -- the contact to add
        account -- the contacts account
        groups -- list of groups to add the contact to.
                  (default groups in contact.get_shown_groups()).
                Parameter ignored when big_brother_contact is specified.
        big_brother_contact -- if specified contact is added as child
                  big_brother_contact. (default None)
        """
        added_iters = []
378
        visible = self.contact_is_visible(contact, account)
379 380 381 382 383 384 385 386
        if big_brother_contact:
            # Add contact under big brother

            parent_iters = self._get_contact_iter(
                    big_brother_contact.jid, big_brother_account,
                    big_brother_contact, self.model)

            # Do not confuse get_contact_iter: Sync groups of family members
387
            contact.groups = big_brother_contact.groups[:]
388

389 390
            image = self._get_avatar_image(account, contact.jid)

391
            for child_iter in parent_iters:
392
                it = self.model.append(child_iter, [None,
393
                    contact.get_shown_name(), 'contact', contact.jid, account,
394
                    None, None, None, None, image, None, visible] + \
395
                    [None] * self.nb_ext_renderers)
396
                added_iters.append(it)
397 398 399 400
                if contact.jid in self._iters[account]['contacts']:
                    self._iters[account]['contacts'][contact.jid].append(it)
                else:
                    self._iters[account]['contacts'][contact.jid] = [it]
401 402 403 404 405 406
        else:
            # We are a normal contact. Add us to our groups.
            if not groups:
                groups = contact.get_shown_groups()
            for group in groups:
                child_iterG = self._get_group_iter(group, account,
407
                    model=self.model)
408 409
                if not child_iterG:
                    # Group is not yet in roster, add it!
410
                    child_iterG = self._add_group_iter(account, group)
411 412 413

                if contact.is_transport():
                    typestr = 'agent'
414
                elif contact.is_groupchat:
415 416 417 418
                    typestr = 'groupchat'
                else:
                    typestr = 'contact'

419 420
                image = self._get_avatar_image(account, contact.jid)

421 422
                # we add some values here. see draw_contact
                # for more
423 424
                i_ = self.model.append(child_iterG, [None,
                    contact.get_shown_name(), typestr, contact.jid, account,
425
                    None, None, None, None, image, None, visible] + \
426
                    [None] * self.nb_ext_renderers)
427
                added_iters.append(i_)
428 429 430 431
                if contact.jid in self._iters[account]['contacts']:
                    self._iters[account]['contacts'][contact.jid].append(i_)
                else:
                    self._iters[account]['contacts'][contact.jid] = [i_]
432 433 434 435 436 437

                # Restore the group expand state
                if account + group in self.collapsed_rows:
                    is_expanded = False
                else:
                    is_expanded = True
438 439
                if group not in app.groups[account]:
                    app.groups[account][group] = {'expand': is_expanded}
440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456

        return added_iters

    def _remove_entity(self, contact, account, groups=None):
        """
        Remove the given contact from roster data model

        Empty groups after contact removal are removed too.
        Return False if contact still has children and deletion was
        not performed.
        Return True on success.

        Keyword arguments:
        contact -- the contact to add
        account -- the contacts account
        groups -- list of groups to remove the contact from.
        """
457 458
        iters = self._get_contact_iter(contact.jid, account, contact,
            self.model)
459 460

        parent_iter = self.model.iter_parent(iters[0])
461
        parent_type = self.model[parent_iter][Column.TYPE]
462 463 464 465 466 467 468 469 470 471 472 473 474 475

        if groups:
            # Only remove from specified groups
            all_iters = iters[:]
            group_iters = [self._get_group_iter(group, account)
                    for group in groups]
            iters = [titer for titer in all_iters
                    if self.model.iter_parent(titer) in group_iters]

        iter_children = self.model.iter_children(iters[0])

        if iter_children:
            # We have children. We cannot be removed!
            return False
Yann Leboulanger's avatar
Yann Leboulanger committed
476 477 478
        # Remove us and empty groups from the model
        for i in iters:
            parent_i = self.model.iter_parent(i)
479
            parent_type = self.model[parent_i][Column.TYPE]
Yann Leboulanger's avatar
Yann Leboulanger committed
480 481 482 483 484 485 486 487

            to_be_removed = i
            while parent_type == 'group' and \
            self.model.iter_n_children(parent_i) == 1:
                if self.regroup:
                    account_group = 'MERGED'
                else:
                    account_group = account
488
                group = self.model[parent_i][Column.JID]
489 490
                if group in app.groups[account]:
                    del app.groups[account][group]
Yann Leboulanger's avatar
Yann Leboulanger committed
491 492 493
                to_be_removed = parent_i
                del self._iters[account_group]['groups'][group]
                parent_i = self.model.iter_parent(parent_i)
494
                parent_type = self.model[parent_i][Column.TYPE]
Yann Leboulanger's avatar
Yann Leboulanger committed
495
            self.model.remove(to_be_removed)
496

Yann Leboulanger's avatar
Yann Leboulanger committed
497 498
        del self._iters[account]['contacts'][contact.jid]
        return True
499 500 501 502 503 504 505 506 507 508 509 510 511 512 513

    def _add_metacontact_family(self, family, account):
        """
        Add the give Metacontact family to roster data model

        Add Big Brother to his groups and all others under him.
        Return list of all added (contact, account) tuples with
        Big Brother as first element.

        Keyword arguments:
        family -- the family, see Contacts.get_metacontacts_family()
        """

        nearby_family, big_brother_jid, big_brother_account = \
                self._get_nearby_family_and_big_brother(family, account)
514 515
        if not big_brother_jid:
            return []
516
        big_brother_contact = app.contacts.get_first_contact_from_jid(
517 518 519 520 521 522 523 524 525
                big_brother_account, big_brother_jid)

        self._add_entity(big_brother_contact, big_brother_account)

        brothers = []
        # Filter family members
        for data in nearby_family:
            _account = data['account']
            _jid = data['jid']
526
            _contact = app.contacts.get_first_contact_from_jid(
527 528 529 530 531 532 533 534
                    _account, _jid)

            if not _contact or _contact == big_brother_contact:
                # Corresponding account is not connected
                # or brother already added
                continue

            self._add_entity(_contact, _account,
535 536
                    big_brother_contact=big_brother_contact,
                    big_brother_account=big_brother_account)
537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552
            brothers.append((_contact, _account))

        brothers.insert(0, (big_brother_contact, big_brother_account))
        return brothers

    def _remove_metacontact_family(self, family, account):
        """
        Remove the given Metacontact family from roster data model

        See Contacts.get_metacontacts_family() and
        RosterWindow._remove_entity()
        """
        nearby_family = self._get_nearby_family_and_big_brother(
                family, account)[0]

        # Family might has changed (actual big brother not on top).
Alexander Krotov's avatar
Alexander Krotov committed
553
        # Remove children first then big brother
554 555 556 557
        family_in_roster = False
        for data in nearby_family:
            _account = data['account']
            _jid = data['jid']
558
            _contact = app.contacts.get_first_contact_from_jid(_account, _jid)
559 560 561 562 563 564 565 566 567 568

            iters = self._get_contact_iter(_jid, _account, _contact, self.model)
            if not iters or not _contact:
                # Family might not be up to date.
                # Only try to remove what is actually in the roster
                continue

            family_in_roster = True

            parent_iter = self.model.iter_parent(iters[0])
569
            parent_type = self.model[parent_iter][Column.TYPE]
570 571 572 573 574 575 576

            if parent_type != 'contact':
                # The contact on top
                old_big_account = _account
                old_big_contact = _contact
                continue

Philipp Hörist's avatar
Philipp Hörist committed
577
            self._remove_entity(_contact, _account)
578 579 580 581

        if not family_in_roster:
            return False

Philipp Hörist's avatar
Philipp Hörist committed
582
        self._remove_entity(old_big_contact, old_big_account)
583 584 585 586 587 588 589 590 591 592

        return True

    def _recalibrate_metacontact_family(self, family, account):
        """
        Regroup metacontact family if necessary
        """

        brothers = []
        nearby_family, big_brother_jid, big_brother_account = \
593
            self._get_nearby_family_and_big_brother(family, account)
594
        big_brother_contact = app.contacts.get_contact(big_brother_account,
595 596 597
            big_brother_jid)
        child_iters = self._get_contact_iter(big_brother_jid,
            big_brother_account, model=self.model)
598 599
        if child_iters:
            parent_iter = self.model.iter_parent(child_iters[0])
600
            parent_type = self.model[parent_iter][Column.TYPE]
601 602 603 604 605

            # Check if the current BigBrother has even been before.
            if parent_type == 'contact':
                for data in nearby_family:
                    # recalibrate after remove to keep highlight
606
                    if data['jid'] in app.to_be_removed[data['account']]:
607 608 609 610 611 612 613 614 615 616 617 618 619 620
                        return

                self._remove_metacontact_family(family, account)
                brothers = self._add_metacontact_family(family, account)

                for c, acc in brothers:
                    self.draw_completely(c.jid, acc)

        # Check is small brothers are under the big brother
        for child in nearby_family:
            _jid = child['jid']
            _account = child['account']
            if _account == big_brother_account and _jid == big_brother_jid:
                continue
621 622
            child_iters = self._get_contact_iter(_jid, _account,
                model=self.model)
623 624 625
            if not child_iters:
                continue
            parent_iter = self.model.iter_parent(child_iters[0])
626
            parent_type = self.model[parent_iter][Column.TYPE]
627
            if parent_type != 'contact':
628
                _contact = app.contacts.get_contact(_account, _jid)
629 630 631 632 633 634
                self._remove_entity(_contact, _account)
                self._add_entity(_contact, _account, groups=None,
                        big_brother_contact=big_brother_contact,
                        big_brother_account=big_brother_account)

    def _get_nearby_family_and_big_brother(self, family, account):
635
        return app.contacts.get_nearby_family_and_big_brother(family, account)
636 637 638 639 640 641 642

    def _add_self_contact(self, account):
        """
        Add account's SelfContact to roster and draw it and the account

        Return the SelfContact contact instance
        """
643 644
        jid = app.get_jid_from_account(account)
        contact = app.contacts.get_first_contact_from_jid(account, jid)
645 646

        child_iterA = self._get_account_iter(account, self.model)
647
        self._iters[account]['contacts'][jid] = [self.model.append(child_iterA,
648
            [None, app.nicks[account], 'self_contact', jid, account, None,
649
            None, None, None, None, None, True] + [None] * self.nb_ext_renderers)]
650 651 652 653 654 655 656

        self.draw_completely(jid, account)
        self.draw_account(account)

        return contact

    def redraw_metacontacts(self, account):
657
        for family in app.contacts.iter_metacontacts_families(account):
658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674
            self._recalibrate_metacontact_family(family, account)

    def add_contact(self, jid, account):
        """
        Add contact to roster and draw him

        Add contact to all its group and redraw the groups, the contact and the
        account. If it's a Metacontact, add and draw the whole family.
        Do nothing if the contact is already in roster.

        Return the added contact instance. If it is a Metacontact return
        Big Brother.

        Keyword arguments:
        jid -- the contact's jid or SelfJid to add SelfContact
        account -- the corresponding account.
        """
675
        contact = app.contacts.get_contact_with_highest_priority(account, jid)
676
        if self._get_contact_iter(jid, account, contact, self.model):
677 678 679
            # If contact already in roster, do nothing
            return

680
        if jid == app.get_jid_from_account(account):
Philipp Hörist's avatar
Philipp Hörist committed
681
            return self._add_self_contact(account)
682 683 684 685

        is_observer = contact.is_observer()
        if is_observer:
            # if he has a tag, remove it
686
            app.contacts.remove_metacontact(account, jid)
687 688

        # Add contact to roster
689
        family = app.contacts.get_metacontacts_family(account, jid)
690 691 692 693 694 695 696 697 698 699
        contacts = []
        if family:
            # We have a family. So we are a metacontact.
            # Add all family members that we shall be grouped with
            if self.regroup:
                # remove existing family members to regroup them
                self._remove_metacontact_family(family, account)
            contacts = self._add_metacontact_family(family, account)
        else:
            # We are a normal contact
700
            contacts = [(contact, account), ]
701 702 703 704 705 706 707 708 709 710 711 712 713
            self._add_entity(contact, account)

        # Draw the contact and its groups contact
        if not self.starting:
            for c, acc in contacts:
                self.draw_completely(c.jid, acc)
            for group in contact.get_shown_groups():
                self.draw_group(group, account)
                self._adjust_group_expand_collapse_state(group, account)
            self.draw_account(account)

        return contacts[0][0] # it's contact/big brother with highest priority

Philipp Hörist's avatar
Philipp Hörist committed
714
    def remove_contact(self, jid, account, force=False, backend=False, maximize=False):
715 716 717 718 719 720 721 722 723 724 725 726 727 728 729
        """
        Remove contact from roster

        Remove contact from all its group. Remove empty groups or redraw
        otherwise.
        Draw the account.
        If it's a Metacontact, remove the whole family.
        Do nothing if the contact is not in roster.

        Keyword arguments:
        jid -- the contact's jid or SelfJid to remove SelfContact
        account -- the corresponding account.
        force -- remove contact even it has pending evens (Default False)
        backend -- also remove contact instance (Default False)
        """
730
        contact = app.contacts.get_contact_with_highest_priority(account, jid)
731 732 733
        if not contact:
            return

734 735 736
        if not force and self.contact_has_pending_roster_events(contact,
        account):
            return False
737 738 739 740 741

        iters = self._get_contact_iter(jid, account, contact, self.model)
        if iters:
            # no more pending events
            # Remove contact from roster directly
742
            family = app.contacts.get_metacontacts_family(account, jid)
743 744 745 746 747 748
            if family:
                # We have a family. So we are a metacontact.
                self._remove_metacontact_family(family, account)
            else:
                self._remove_entity(contact, account)

749 750
        old_grps = []
        if backend:
751
            if not app.interface.msg_win_mgr.get_control(jid, account) or \
752 753 754 755
            force:
                # If a window is still opened: don't remove contact instance
                # Remove contact before redrawing, otherwise the old
                # numbers will still be show
Philipp Hörist's avatar
Philipp Hörist committed
756
                if not maximize:
Alexander Krotov's avatar
Alexander Krotov committed
757
                    # Don't remove contact when we maximize a room
Philipp Hörist's avatar
Philipp Hörist committed
758
                    app.contacts.remove_jid(account, jid, remove_meta=True)
759 760 761 762 763 764 765 766 767 768
                if iters:
                    rest_of_family = [data for data in family
                        if account != data['account'] or jid != data['jid']]
                    if rest_of_family:
                        # reshow the rest of the family
                        brothers = self._add_metacontact_family(rest_of_family,
                            account)
                        for c, acc in brothers:
                            self.draw_completely(c.jid, acc)
            else:
769
                for c in app.contacts.get_contacts(account, jid):
770 771 772 773
                    c.sub = 'none'
                    c.show = 'not in roster'
                    c.status = ''
                    old_grps = c.get_shown_groups()
774
                    c.groups = [_('Not in contact list')]
775 776
                    self._add_entity(c, account)
                    self.draw_contact(jid, account)
777 778 779

        if iters:
            # Draw all groups of the contact
780
            for group in contact.get_shown_groups() + old_grps:
781 782 783 784 785 786 787 788 789 790 791 792 793 794
                self.draw_group(group, account)
            self.draw_account(account)

        return True

    def rename_self_contact(self, old_jid, new_jid, account):
        """
        Rename the self_contact jid

        Keyword arguments:
        old_jid -- our old jid
        new_jid -- our new jid
        account -- the corresponding account.
        """
795
        app.contacts.change_contact_jid(old_jid, new_jid, account)
796 797 798
        self_iter = self._get_self_contact_iter(account, model=self.model)
        if not self_iter:
            return
799
        self.model[self_iter][Column.JID] = new_jid
800 801
        self.draw_contact(new_jid, account)

Philipp Hörist's avatar
Philipp Hörist committed
802 803 804 805 806 807
    def minimize_groupchat(self, account, jid, status=''):
        gc_control = app.interface.msg_win_mgr.get_gc_control(jid, account)
        app.interface.minimized_controls[account][jid] = gc_control
        self.add_groupchat(jid, account)

    def add_groupchat(self, jid, account):
808 809 810
        """
        Add groupchat to roster and draw it. Return the added contact instance
        """
Philipp Hörist's avatar
Philipp Hörist committed
811 812
        contact = app.contacts.get_groupchat_contact(account, jid)
        show = 'offline'
813
        if app.account_is_available(account):
814 815
            show = 'online'

Philipp Hörist's avatar
Philipp Hörist committed
816 817
        contact.show = show
        self.add_contact(jid, account)
818 819 820

        return contact

Philipp Hörist's avatar
Philipp Hörist committed
821
    def remove_groupchat(self, jid, account, maximize=False):
822 823 824
        """
        Remove groupchat from roster and redraw account and group
        """
825
        contact = app.contacts.get_contact_with_highest_priority(account, jid)
826
        if contact.is_groupchat:
827 828
            if jid in app.interface.minimized_controls[account]:
                del app.interface.minimized_controls[account][jid]
Philipp Hörist's avatar
Philipp Hörist committed
829
            self.remove_contact(jid, account, force=True, backend=True, maximize=maximize)
830
            return True
831
        return False
832 833 834 835 836 837

    # FIXME: This function is yet unused! Port to new API
    def add_transport(self, jid, account):
        """
        Add transport to roster and draw it. Return the added contact instance
        """
838
        contact = app.contacts.get_contact_with_highest_priority(account, jid)
839
        if contact is None:
840
            contact = app.contacts.create_contact(jid=jid, account=account,
841 842
                name=jid, groups=[_('Transports')], show='offline',
                status='offline', sub='from')
843
            app.contacts.add_contact(account, contact)
844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867
        self.add_contact(jid, account)
        return contact

    def remove_transport(self, jid, account):
        """
        Remove transport from roster and redraw account and group
        """
        self.remove_contact(jid, account, force=True, backend=True)
        return True

    def rename_group(self, old_name, new_name, account):
        """
        Rename a roster group
        """
        if old_name == new_name:
            return

        # Groups may not change name from or to a special groups
        for g in helpers.special_groups:
            if g in (new_name, old_name):
                return

        # update all contacts in the given group
        if self.regroup:
868
            accounts = app.connections.keys()
869
        else:
870
            accounts = [account, ]
871 872 873

        for acc in accounts:
            changed_contacts = []
874 875
            for jid in app.contacts.get_jid_list(acc):
                contact = app.contacts.get_first_contact_from_jid(acc, jid)
876 877 878 879 880 881 882 883 884
                if old_name not in contact.groups:
                    continue

                self.remove_contact(jid, acc, force=True)

                contact.groups.remove(old_name)
                if new_name not in contact.groups:
                    contact.groups.append(new_name)

885 886
                changed_contacts.append({'jid': jid, 'name': contact.name,
                    'groups':contact.groups})
887

888 889
            app.connections[acc].get_module('Roster').update_contacts(
                changed_contacts)
890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913

            for c in changed_contacts:
                self.add_contact(c['jid'], acc)

            self._adjust_group_expand_collapse_state(new_name, acc)

            self.draw_group(old_name, acc)
            self.draw_group(new_name, acc)


    def add_contact_to_groups(self, jid, account, groups, update=True):
        """
        Add contact to given groups and redraw them

        Contact on server is updated too. When the contact has a family,
        the action will be performed for all members.

        Keyword Arguments:
        jid -- the jid
        account -- the corresponding account
        groups -- list of Groups to add the contact to.
        update -- update contact on the server
        """
        self.remove_contact(jid, account, force=True)
914
        for contact in app.contacts.get_contacts(account, jid):
915 916 917 918 919
            for group in groups:
                if group not in contact.groups:
                    # we might be dropped from meta to group
                    contact.groups.append(group)
            if update:
920 921 922
                con = app.connections[account]
                con.get_module('Roster').update_contact(
                    jid, contact.name, contact.groups)
923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942

        self.add_contact(jid, account)

        for group in groups:
            self._adjust_group_expand_collapse_state(group, account)

    def remove_contact_from_groups(self, jid, account, groups, update=True):
        """
        Remove contact from given groups and redraw them

        Contact on server is updated too. When the contact has a family,
        the action will be performed for all members.

        Keyword Arguments:
        jid -- the jid
        account -- the corresponding account
        groups -- list of Groups to remove the contact from
        update -- update contact on the server
        """
        self.remove_contact(jid, account, force=True)
943
        for contact in app.contacts.get_contacts(account, jid):
944 945 946 947 948
            for group in groups:
                if group in contact.groups:
                    # Needed when we remove from "General" or "Observers"
                    contact.groups.remove(group)
            if update:
949 950 951
                con = app.connections[account]
                con.get_module('Roster').update_contact(
                    jid, contact.name, contact.groups)
952 953 954 955 956 957
        self.add_contact(jid, account)

        # Also redraw old groups
        for group in groups:
            self.draw_group(group, account)

958
    # FIXME: maybe move to app.py
959
    def remove_newly_added(self, jid, account):
960
        if account not in app.newly_added:
961 962
            # Account has been deleted during the timeout that called us
            return
963 964
        if jid in app.newly_added[account]:
            app.newly_added[account].remove(jid)
965 966
            self.draw_contact(jid, account)

967
    # FIXME: maybe move to app.py
968
    def remove_to_be_removed(self, jid, account):
969
        if account not in app.interface.instances:
970 971
            # Account has been deleted during the timeout that called us
            return
972
        if jid in app.newly_added[account]:
973
            return
974 975 976
        if jid in app.to_be_removed[account]:
            app.to_be_removed[account].remove(jid)
            family = app.contacts.get_metacontacts_family(account, jid)
977
            if family:
Alexander Krotov's avatar
Alexander Krotov committed
978
                # Perform delayed recalibration
979 980
                self._recalibrate_metacontact_family(family, account)
            self.draw_contact(jid, account)
Alexander Krotov's avatar
Alexander Krotov committed
981
            # Hide Group if all children are hidden
982
            contact = app.contacts.get_contact(account, jid)
Weblate's avatar
Weblate committed
983 984
            if not contact:
                return
985 986
            for group in contact.get_shown_groups():
                self.draw_group(group, account)
987 988

    # FIXME: integrate into add_contact()
Philipp Hörist's avatar
Philipp Hörist committed
989 990 991 992
    def add_to_not_in_the_roster(self, account, jid, nick='', resource='',
                                 groupchat=False):
        contact = app.contacts.create_not_in_roster_contact(
            jid=jid, account=account, resource=resource, name=nick,
Philipp Hörist's avatar
Philipp Hörist committed
993
            groupchat=groupchat)
994
        app.contacts.add_contact(account, contact)
995 996
        self.add_contact(contact.jid, account)
        return contact
Yann Leboulanger's avatar
Yann Leboulanger committed
997 998 999


################################################################################
1000
### Methods for adding and removing roster window items
Yann Leboulanger's avatar
Yann Leboulanger committed
1001
################################################################################
1002

Yann Leboulanger's avatar