1# Copyright (C) 2006 Dimitur Kirov <dkirov AT gmail.com>
2#                    Travis Shirk <travis AT pobox.com>
3#                    Nikos Kouremenos <kourem AT gmail.com>
4# Copyright (C) 2006-2014 Yann Leboulanger <asterix AT lagaule.org>
5#                         Jean-Marie Traissard <jim AT lapin.org>
6# Copyright (C) 2007 Lukas Petrovicky <lukas AT petrovicky.net>
7#                    Tomasz Melcer <liori AT exroot.org>
8#                    Julien Pivotto <roidelapluie AT gmail.com>
9# Copyright (C) 2007-2008 Stephan Erb <steve-e AT h3c.de>
10# Copyright (C) 2008 Brendan Taylor <whateley AT gmail.com>
11#                    Jonathan Schleifer <js-gajim AT webkeks.org>
12#
13# This file is part of Gajim.
14#
15# Gajim is free software; you can redistribute it and/or modify
16# it under the terms of the GNU General Public License as published
17# by the Free Software Foundation; version 3 only.
18#
19# Gajim is distributed in the hope that it will be useful,
20# but WITHOUT ANY WARRANTY; without even the implied warranty of
21# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22# GNU General Public License for more details.
23#
24# You should have received a copy of the GNU General Public License
25# along with Gajim. If not, see <http://www.gnu.org/licenses/>.
26
27from functools import partial
28
29try:
30    from gajim.common import app
31    from gajim.common.i18n import _
32    from gajim.common.account import Account
33    from gajim import common
34    from gajim.common.const import Chatstate
35except ImportError as e:
36    if __name__ != "__main__":
37        raise ImportError(str(e))
38
39
40class ContactSettings:
41    def __init__(self, account, jid):
42        self.get = partial(app.settings.get_contact_setting, account, jid)
43        self.set = partial(app.settings.set_contact_setting, account, jid)
44
45
46class GroupChatSettings:
47    def __init__(self, account, jid):
48        self.get = partial(app.settings.get_group_chat_setting, account, jid)
49        self.set = partial(app.settings.set_group_chat_setting, account, jid)
50
51
52class XMPPEntity:
53    """
54    Base representation of entities in XMPP
55    """
56
57    def __init__(self, jid, account, resource):
58        self.jid = jid
59        self.resource = resource
60        self.account = account
61
62class CommonContact(XMPPEntity):
63
64    def __init__(self, jid, account, resource, show, presence, status, name,
65                 chatstate):
66
67        XMPPEntity.__init__(self, jid, account, resource)
68
69        self._show = show
70        self._presence = presence
71        self.status = status
72        self.name = name
73
74        # this is contact's chatstate
75        self._chatstate = chatstate
76
77        self._is_pm_contact = False
78
79    @property
80    def show(self):
81        return self._show
82
83    @show.setter
84    def show(self, value):
85        self._show = value
86
87    @property
88    def presence(self):
89        return self._presence
90
91    @presence.setter
92    def presence(self, value):
93        self._presence = value
94
95    @property
96    def is_available(self):
97        return self._presence.is_available
98
99    @property
100    def chatstate_enum(self):
101        return self._chatstate
102
103    @property
104    def chatstate(self):
105        if self._chatstate is None:
106            return
107        return str(self._chatstate)
108
109    @chatstate.setter
110    def chatstate(self, value):
111        if value is None:
112            self._chatstate = value
113        else:
114            self._chatstate = Chatstate[value.upper()]
115
116    @property
117    def is_gc_contact(self):
118        return isinstance(self, GC_Contact)
119
120    @property
121    def is_pm_contact(self):
122        return self._is_pm_contact
123
124    @property
125    def is_groupchat(self):
126        return False
127
128    def get_full_jid(self):
129        raise NotImplementedError
130
131    def get_shown_name(self):
132        raise NotImplementedError
133
134    def supports(self, requested_feature):
135        """
136        Return True if the contact has advertised to support the feature
137        identified by the given namespace. False otherwise.
138        """
139        if self.show == 'offline':
140            # Unfortunately, if all resources are offline, the contact
141            # includes the last resource that was online. Check for its
142            # show, so we can be sure it's existent. Otherwise, we still
143            # return caps for a contact that has no resources left.
144            return False
145
146        disco_info = app.storage.cache.get_last_disco_info(self.get_full_jid())
147        if disco_info is None:
148            return False
149
150        return disco_info.supports(requested_feature)
151
152    @property
153    def uses_phone(self):
154        disco_info = app.storage.cache.get_last_disco_info(self.get_full_jid())
155        if disco_info is None:
156            return False
157
158        return disco_info.has_category('phone')
159
160
161class Contact(CommonContact):
162    """
163    Information concerning a contact
164    """
165    def __init__(self, jid, account, name='', groups=None, show='', status='',
166    sub='', ask='', resource='', priority=0,
167    chatstate=None, idle_time=None, avatar_sha=None, groupchat=False,
168    is_pm_contact=False):
169        if not isinstance(jid, str):
170            print('no str')
171        if groups is None:
172            groups = []
173
174        CommonContact.__init__(self, jid, account, resource, show,
175                               None, status, name, chatstate)
176
177        self.contact_name = '' # nick chosen by contact
178        self.groups = [i if i else _('General') for i in set(groups)] # filter duplicate values
179        self.avatar_sha = avatar_sha
180        self._is_groupchat = groupchat
181        self._is_pm_contact = is_pm_contact
182
183        if groupchat:
184            self.settings = GroupChatSettings(account.name, jid)
185        else:
186            self.settings = ContactSettings(account.name, jid)
187
188        self.sub = sub
189        self.ask = ask
190
191        self.priority = priority
192        self.idle_time = idle_time
193
194        self.pep = {}
195
196    def connect_signal(self, setting, func):
197        app.settings.connect_signal(
198            setting, func, self.account.name, self.jid)
199
200    def get_full_jid(self):
201        if self.resource:
202            return self.jid + '/' + self.resource
203        return self.jid
204
205    def get_shown_name(self):
206        if self._is_groupchat:
207            return self._get_groupchat_name()
208        if self.name:
209            return self.name
210        if self.contact_name:
211            return self.contact_name
212        return self.jid.split('@')[0]
213
214    def _get_groupchat_name(self):
215        from gajim.common.helpers import get_groupchat_name
216        con = app.connections[self.account.name]
217        return get_groupchat_name(con, self.jid)
218
219    def get_shown_groups(self):
220        if self.is_observer():
221            return [_('Observers')]
222        if self.is_groupchat:
223            return [_('Group chats')]
224        if self.is_transport():
225            return [_('Transports')]
226        if not self.groups:
227            return [_('General')]
228        return self.groups
229
230    def is_hidden_from_roster(self):
231        """
232        If contact should not be visible in roster
233        """
234        # XEP-0162: http://www.xmpp.org/extensions/xep-0162.html
235        if self.is_transport():
236            return False
237        if self.sub in ('both', 'to'):
238            return False
239        if self.sub in ('none', 'from') and self.ask == 'subscribe':
240            return False
241        if self.sub in ('none', 'from') and (self.name or self.groups):
242            return False
243        if _('Not in contact list') in self.groups:
244            return False
245        return True
246
247    def is_observer(self):
248        # XEP-0162: http://www.xmpp.org/extensions/xep-0162.html
249        is_observer = False
250        if self.sub == 'from' and not self.is_transport()\
251        and self.is_hidden_from_roster():
252            is_observer = True
253        return is_observer
254
255    @property
256    def is_groupchat(self):
257        return self._is_groupchat
258
259    @property
260    def is_connected(self):
261        try:
262            return app.gc_connected[self.account.name][self.jid]
263        except Exception:
264            return False
265
266    def is_transport(self):
267        # if not '@' or '@' starts the jid then contact is transport
268        return self.jid.find('@') <= 0
269
270    def can_notify(self):
271        if not self.is_groupchat:
272            raise ValueError
273
274        all_ = app.settings.get('notify_on_all_muc_messages')
275        room = self.settings.get('notify_on_all_messages')
276        return all_ or room
277
278
279class GC_Contact(CommonContact):
280    """
281    Information concerning each groupchat contact
282    """
283
284    def __init__(self, room_jid, account, name='', show='', presence=None,
285                 status='', role='', affiliation='', jid='', resource='',
286                 chatstate=None, avatar_sha=None):
287
288        CommonContact.__init__(self, jid, account, resource, show,
289                               presence, status, name, chatstate)
290
291        self.room_jid = room_jid
292        self.role = role
293        self.affiliation = affiliation
294        self.avatar_sha = avatar_sha
295
296        self.settings = ContactSettings(account.name, jid)
297
298    def get_full_jid(self):
299        return self.room_jid + '/' + self.name
300
301    def get_shown_name(self):
302        return self.name
303
304    def get_avatar(self, *args, **kwargs):
305        return common.app.interface.get_avatar(self, *args, **kwargs)
306
307    def as_contact(self):
308        """
309        Create a Contact instance from this GC_Contact instance
310        """
311        return Contact(jid=self.get_full_jid(), account=self.account,
312            name=self.name, groups=[], show=self.show, status=self.status,
313            sub='none', avatar_sha=self.avatar_sha,
314            is_pm_contact=True)
315
316
317class LegacyContactsAPI:
318    """
319    This is a GOD class for accessing contact and groupchat information.
320    The API has several flaws:
321
322            * it mixes concerns because it deals with contacts, groupchats,
323              groupchat contacts and metacontacts
324            * some methods like get_contact() may return None. This leads to
325              a lot of duplication all over Gajim because it is not sure
326              if we receive a proper contact or just None.
327
328    It is a long way to cleanup this API. Therefore just stick with it
329    and use it as before. We will try to figure out a migration path.
330    """
331    def __init__(self):
332        self._metacontact_manager = MetacontactManager(self)
333        self._accounts = {}
334
335    def add_account(self, account_name):
336        self._accounts[account_name] = Account(account_name, Contacts(),
337                GC_Contacts())
338        self._metacontact_manager.add_account(account_name)
339
340    def get_accounts(self, zeroconf=True):
341        accounts = list(self._accounts.keys())
342        if not zeroconf:
343            if 'Local' in accounts:
344                accounts.remove('Local')
345        return accounts
346
347    def remove_account(self, account):
348        del self._accounts[account]
349        self._metacontact_manager.remove_account(account)
350
351    def create_contact(self, jid, account, name='', groups=None, show='',
352    status='', sub='', ask='', resource='', priority=0,
353    chatstate=None, idle_time=None,
354    avatar_sha=None, groupchat=False):
355        if groups is None:
356            groups = []
357        # Use Account object if available
358        account = self._accounts.get(account, account)
359        return Contact(jid=jid, account=account, name=name, groups=groups,
360            show=show, status=status, sub=sub, ask=ask, resource=resource,
361            priority=priority,
362            chatstate=chatstate, idle_time=idle_time, avatar_sha=avatar_sha,
363            groupchat=groupchat)
364
365    def create_self_contact(self, jid, account, resource, show, status, priority,
366    name=''):
367        conn = common.app.connections[account]
368        nick = name or common.app.nicks[account]
369        account = self._accounts.get(account, account) # Use Account object if available
370        self_contact = self.create_contact(jid=jid, account=account,
371                name=nick, groups=['self_contact'], show=show, status=status,
372                sub='both', ask='none', priority=priority,
373                resource=resource)
374        self_contact.pep = conn.pep
375        return self_contact
376
377    def create_not_in_roster_contact(self, jid, account, resource='', name='',
378                                     groupchat=False):
379        # Use Account object if available
380        account = self._accounts.get(account, account)
381        return self.create_contact(jid=jid, account=account, resource=resource,
382            name=name, groups=[_('Not in contact list')], show='not in roster',
383            status='', sub='none', groupchat=groupchat)
384
385    def copy_contact(self, contact):
386        return self.create_contact(contact.jid, contact.account,
387            name=contact.name, groups=contact.groups, show=contact.show,
388            status=contact.status, sub=contact.sub, ask=contact.ask,
389            resource=contact.resource, priority=contact.priority,
390            chatstate=contact.chatstate_enum,
391            idle_time=contact.idle_time, avatar_sha=contact.avatar_sha)
392
393    def add_contact(self, account, contact):
394        if account not in self._accounts:
395            self.add_account(account)
396        return self._accounts[account].contacts.add_contact(contact)
397
398    def remove_contact(self, account, contact):
399        if account not in self._accounts:
400            return
401        return self._accounts[account].contacts.remove_contact(contact)
402
403    def remove_jid(self, account, jid, remove_meta=True):
404        self._accounts[account].contacts.remove_jid(jid)
405        if remove_meta:
406            self._metacontact_manager.remove_metacontact(account, jid)
407
408    def get_groupchat_contact(self, account, jid):
409        return self._accounts[account].contacts.get_groupchat_contact(jid)
410
411    def get_contacts(self, account, jid):
412        return self._accounts[account].contacts.get_contacts(jid)
413
414    def get_contact(self, account, jid, resource=None):
415        return self._accounts[account].contacts.get_contact(jid, resource=resource)
416
417    def get_contact_strict(self, account, jid, resource):
418        return self._accounts[account].contacts.get_contact_strict(jid, resource)
419
420    def get_avatar(self, account, *args, **kwargs):
421        return self._accounts[account].contacts.get_avatar(*args, **kwargs)
422
423    def get_avatar_sha(self, account, jid):
424        return self._accounts[account].contacts.get_avatar_sha(jid)
425
426    def set_avatar(self, account, jid, sha):
427        self._accounts[account].contacts.set_avatar(jid, sha)
428
429    def iter_contacts(self, account):
430        for contact in self._accounts[account].contacts.iter_contacts():
431            yield contact
432
433    def get_contact_from_full_jid(self, account, fjid):
434        return self._accounts[account].contacts.get_contact_from_full_jid(fjid)
435
436    def get_first_contact_from_jid(self, account, jid):
437        return self._accounts[account].contacts.get_first_contact_from_jid(jid)
438
439    def get_contacts_from_group(self, account, group):
440        return self._accounts[account].contacts.get_contacts_from_group(group)
441
442    def get_contacts_jid_list(self, account):
443        return self._accounts[account].contacts.get_contacts_jid_list()
444
445    def get_jid_list(self, account):
446        return self._accounts[account].contacts.get_jid_list()
447
448    def change_contact_jid(self, old_jid, new_jid, account):
449        return self._accounts[account].change_contact_jid(old_jid, new_jid)
450
451    def get_highest_prio_contact_from_contacts(self, contacts):
452        if not contacts:
453            return None
454        prim_contact = contacts[0]
455        for contact in contacts[1:]:
456            if int(contact.priority) > int(prim_contact.priority):
457                prim_contact = contact
458        return prim_contact
459
460    def get_contact_with_highest_priority(self, account, jid):
461        contacts = self.get_contacts(account, jid)
462        if not contacts and '/' in jid:
463            # jid may be a fake jid, try it
464            room, nick = jid.split('/', 1)
465            contact = self.get_gc_contact(account, room, nick)
466            return contact
467        return self.get_highest_prio_contact_from_contacts(contacts)
468
469    def get_nb_online_total_contacts(self, accounts=None, groups=None):
470        """
471        Return the number of online contacts and the total number of contacts
472        """
473        if not accounts:
474            accounts = self.get_accounts()
475        if groups is None:
476            groups = []
477        nbr_online = 0
478        nbr_total = 0
479        for account in accounts:
480            our_jid = common.app.get_jid_from_account(account)
481            for jid in self.get_jid_list(account):
482                if jid == our_jid:
483                    continue
484                if (common.app.jid_is_transport(jid) and
485                        _('Transports') not in groups):
486                    # do not count transports
487                    continue
488                if self.has_brother(account, jid, accounts) and not \
489                self.is_big_brother(account, jid, accounts):
490                    # count metacontacts only once
491                    continue
492                contact = self._accounts[account].contacts._contacts[jid][0]
493                if _('Not in contact list') in contact.groups:
494                    continue
495                in_groups = False
496                if groups == []:
497                    in_groups = True
498                else:
499                    for group in groups:
500                        if group in contact.get_shown_groups():
501                            in_groups = True
502                            break
503
504                if in_groups:
505                    if contact.show not in ('offline', 'error'):
506                        nbr_online += 1
507                    nbr_total += 1
508        return nbr_online, nbr_total
509
510    def __getattr__(self, attr_name):
511        # Only called if self has no attr_name
512        if hasattr(self._metacontact_manager, attr_name):
513            return getattr(self._metacontact_manager, attr_name)
514        raise AttributeError(attr_name)
515
516    def create_gc_contact(self, room_jid, account, name='', show='',
517                          presence=None, status='', role='',
518                          affiliation='', jid='', resource='', avatar_sha=None):
519        account = self._accounts.get(account, account) # Use Account object if available
520        return GC_Contact(room_jid, account, name, show, presence, status,
521                          role, affiliation, jid, resource, avatar_sha=avatar_sha)
522
523    def add_gc_contact(self, account, gc_contact):
524        return self._accounts[account].gc_contacts.add_gc_contact(gc_contact)
525
526    def remove_gc_contact(self, account, gc_contact):
527        return self._accounts[account].gc_contacts.remove_gc_contact(gc_contact)
528
529    def remove_room(self, account, room_jid):
530        return self._accounts[account].gc_contacts.remove_room(room_jid)
531
532    def get_gc_list(self, account):
533        return self._accounts[account].gc_contacts.get_gc_list()
534
535    def get_nick_list(self, account, room_jid):
536        return self._accounts[account].gc_contacts.get_nick_list(room_jid)
537
538    def get_gc_contact_list(self, account, room_jid):
539        return self._accounts[account].gc_contacts.get_gc_contact_list(room_jid)
540
541    def get_gc_contact(self, account, room_jid, nick):
542        return self._accounts[account].gc_contacts.get_gc_contact(room_jid, nick)
543
544    def is_gc_contact(self, account, jid):
545        return self._accounts[account].gc_contacts.is_gc_contact(jid)
546
547    def get_nb_role_total_gc_contacts(self, account, room_jid, role):
548        return self._accounts[account].gc_contacts.get_nb_role_total_gc_contacts(room_jid, role)
549
550    def set_gc_avatar(self, account, room_jid, nick, sha):
551        contact = self.get_gc_contact(account, room_jid, nick)
552        if contact is None:
553            return
554        contact.avatar_sha = sha
555
556    def get_combined_chatstate(self, account, jid):
557        return self._accounts[account].contacts.get_combined_chatstate(jid)
558
559
560class Contacts():
561    """
562    This is a breakout of the contact related behavior of the old
563    Contacts class (which is not called LegacyContactsAPI)
564    """
565    def __init__(self):
566        # list of contacts  {jid1: [C1, C2]}, } one Contact per resource
567        self._contacts = {}
568
569    def add_contact(self, contact):
570        if contact.jid not in self._contacts or contact.is_groupchat:
571            self._contacts[contact.jid] = [contact]
572            return
573        contacts = self._contacts[contact.jid]
574        # We had only one that was offline, remove it
575        if len(contacts) == 1 and contacts[0].show == 'offline':
576            # Do not use self.remove_contact: it deletes
577            # self._contacts[account][contact.jid]
578            contacts.remove(contacts[0])
579        # If same JID with same resource already exists, use the new one
580        for c in contacts:
581            if c.resource == contact.resource:
582                self.remove_contact(c)
583                break
584        contacts.append(contact)
585
586    def remove_contact(self, contact):
587        if contact.jid not in self._contacts:
588            return
589        if contact in self._contacts[contact.jid]:
590            self._contacts[contact.jid].remove(contact)
591        if not self._contacts[contact.jid]:
592            del self._contacts[contact.jid]
593
594    def remove_jid(self, jid):
595        """
596        Remove all contacts for a given jid
597        """
598        if jid in self._contacts:
599            del self._contacts[jid]
600
601    def get_contacts(self, jid):
602        """
603        Return the list of contact instances for this jid
604        """
605        return list(self._contacts.get(jid, []))
606
607    def get_contact(self, jid, resource=None):
608        ### WARNING ###
609        # This function returns a *RANDOM* resource if resource = None!
610        # Do *NOT* use if you need to get the contact to which you
611        # send a message for example, as a bare JID in XMPP means
612        # highest available resource, which this function ignores!
613        """
614        Return the contact instance for the given resource if it's given else the
615        first contact is no resource is given or None if there is not
616        """
617        if jid in self._contacts:
618            if not resource:
619                return self._contacts[jid][0]
620            for c in self._contacts[jid]:
621                if c.resource == resource:
622                    return c
623            return self._contacts[jid][0]
624
625    def get_contact_strict(self, jid, resource):
626        """
627        Return the contact instance for the given resource or None
628        """
629        if jid in self._contacts:
630            for c in self._contacts[jid]:
631                if c.resource == resource:
632                    return c
633
634    def get_groupchat_contact(self, jid):
635        if jid in self._contacts:
636            contacts = self._contacts[jid]
637            if contacts[0].is_groupchat:
638                return contacts[0]
639
640    def get_avatar(self, jid, size, scale, show=None):
641        if jid not in self._contacts:
642            return None
643
644        for resource in self._contacts[jid]:
645            avatar = common.app.interface.get_avatar(
646                resource, size, scale, show)
647            if avatar is None:
648                self.set_avatar(jid, None)
649            return avatar
650
651    def get_avatar_sha(self, jid):
652        if jid not in self._contacts:
653            return None
654
655        for resource in self._contacts[jid]:
656            if resource.avatar_sha is not None:
657                return resource.avatar_sha
658        return None
659
660    def set_avatar(self, jid, sha):
661        if jid not in self._contacts:
662            return
663        for resource in self._contacts[jid]:
664            resource.avatar_sha = sha
665
666    def iter_contacts(self):
667        for jid in list(self._contacts.keys()):
668            for contact in self._contacts[jid][:]:
669                yield contact
670
671    def get_jid_list(self):
672        return list(self._contacts.keys())
673
674    def get_contacts_jid_list(self):
675        return [jid for jid, contact in self._contacts.items() if not
676                contact[0].is_groupchat]
677
678    def get_contact_from_full_jid(self, fjid):
679        """
680        Get Contact object for specific resource of given jid
681        """
682        barejid, resource = common.app.get_room_and_nick_from_fjid(fjid)
683        return self.get_contact_strict(barejid, resource)
684
685    def get_first_contact_from_jid(self, jid):
686        if jid in self._contacts:
687            return self._contacts[jid][0]
688
689    def get_contacts_from_group(self, group):
690        """
691        Return all contacts in the given group
692        """
693        group_contacts = []
694        for jid in self._contacts:
695            contacts = self.get_contacts(jid)
696            if group in contacts[0].groups:
697                group_contacts += contacts
698        return group_contacts
699
700    def change_contact_jid(self, old_jid, new_jid):
701        if old_jid not in self._contacts:
702            return
703        self._contacts[new_jid] = []
704        for _contact in self._contacts[old_jid]:
705            _contact.jid = new_jid
706            self._contacts[new_jid].append(_contact)
707        del self._contacts[old_jid]
708
709    def get_combined_chatstate(self, jid):
710        if jid not in self._contacts:
711            return
712        contacts = self._contacts[jid]
713        states = []
714        for contact in contacts:
715            if contact.chatstate_enum is None:
716                continue
717            states.append(contact.chatstate_enum)
718
719        return str(min(states)) if states else None
720
721
722class GC_Contacts():
723
724    def __init__(self):
725        # list of contacts that are in gc {room_jid: {nick: C}}}
726        self._rooms = {}
727
728    def add_gc_contact(self, gc_contact):
729        if gc_contact.room_jid not in self._rooms:
730            self._rooms[gc_contact.room_jid] = {gc_contact.name: gc_contact}
731        else:
732            self._rooms[gc_contact.room_jid][gc_contact.name] = gc_contact
733
734    def remove_gc_contact(self, gc_contact):
735        if gc_contact.room_jid not in self._rooms:
736            return
737        if gc_contact.name not in self._rooms[gc_contact.room_jid]:
738            return
739        del self._rooms[gc_contact.room_jid][gc_contact.name]
740        # It was the last nick in room ?
741        if not self._rooms[gc_contact.room_jid]:
742            del self._rooms[gc_contact.room_jid]
743
744    def remove_room(self, room_jid):
745        if room_jid in self._rooms:
746            del self._rooms[room_jid]
747
748    def get_gc_list(self):
749        return self._rooms.keys()
750
751    def get_nick_list(self, room_jid):
752        gc_list = self.get_gc_list()
753        if not room_jid in gc_list:
754            return []
755        return list(self._rooms[room_jid].keys())
756
757    def get_gc_contact_list(self, room_jid):
758        try:
759            return list(self._rooms[room_jid].values())
760        except Exception:
761            return []
762
763    def get_gc_contact(self, room_jid, nick):
764        try:
765            return self._rooms[room_jid][nick]
766        except KeyError:
767            return None
768
769    def is_gc_contact(self, jid):
770        """
771        >>> gc = GC_Contacts()
772        >>> gc._rooms = {'gajim@conference.gajim.org' : {'test' : True}}
773        >>> gc.is_gc_contact('gajim@conference.gajim.org/test')
774        True
775        >>> gc.is_gc_contact('test@jabbim.com')
776        False
777        """
778        jid = jid.split('/')
779        if len(jid) != 2:
780            return False
781        gcc = self.get_gc_contact(jid[0], jid[1])
782        return gcc is not None
783
784    def get_nb_role_total_gc_contacts(self, room_jid, role):
785        """
786        Return the number of group chat contacts for the given role and the total
787        number of group chat contacts
788        """
789        if room_jid not in self._rooms:
790            return 0, 0
791        nb_role = nb_total = 0
792        for nick in self._rooms[room_jid]:
793            if self._rooms[room_jid][nick].role == role:
794                nb_role += 1
795            nb_total += 1
796        return nb_role, nb_total
797
798
799class MetacontactManager():
800
801    def __init__(self, contacts):
802        self._metacontacts_tags = {}
803        self._contacts = contacts
804
805    def add_account(self, account):
806        if account not in self._metacontacts_tags:
807            self._metacontacts_tags[account] = {}
808
809    def remove_account(self, account):
810        del self._metacontacts_tags[account]
811
812    def define_metacontacts(self, account, tags_list):
813        self._metacontacts_tags[account] = tags_list
814
815    def _get_new_metacontacts_tag(self, jid):
816        if not jid in self._metacontacts_tags:
817            return jid
818        #FIXME: can this append ?
819        assert False
820
821    def iter_metacontacts_families(self, account):
822        for tag in self._metacontacts_tags[account]:
823            family = self._get_metacontacts_family_from_tag(account, tag)
824            yield family
825
826    def _get_metacontacts_tag(self, account, jid):
827        """
828        Return the tag of a jid
829        """
830        if not account in self._metacontacts_tags:
831            return None
832        for tag in self._metacontacts_tags[account]:
833            for data in self._metacontacts_tags[account][tag]:
834                if data['jid'] == jid:
835                    return tag
836        return None
837
838    def add_metacontact(self, brother_account, brother_jid, account, jid, order=None):
839        tag = self._get_metacontacts_tag(brother_account, brother_jid)
840        if not tag:
841            tag = self._get_new_metacontacts_tag(brother_jid)
842            self._metacontacts_tags[brother_account][tag] = [{'jid': brother_jid,
843                    'tag': tag}]
844            if brother_account != account:
845                con = common.app.connections[brother_account]
846                con.get_module('MetaContacts').store_metacontacts(
847                    self._metacontacts_tags[brother_account])
848        # be sure jid has no other tag
849        old_tag = self._get_metacontacts_tag(account, jid)
850        while old_tag:
851            self.remove_metacontact(account, jid)
852            old_tag = self._get_metacontacts_tag(account, jid)
853        if tag not in self._metacontacts_tags[account]:
854            self._metacontacts_tags[account][tag] = [{'jid': jid, 'tag': tag}]
855        else:
856            if order:
857                self._metacontacts_tags[account][tag].append({'jid': jid,
858                        'tag': tag, 'order': order})
859            else:
860                self._metacontacts_tags[account][tag].append({'jid': jid,
861                        'tag': tag})
862        con = common.app.connections[account]
863        con.get_module('MetaContacts').store_metacontacts(
864            self._metacontacts_tags[account])
865
866    def remove_metacontact(self, account, jid):
867        if account not in self._metacontacts_tags:
868            return
869
870        found = None
871        for tag in self._metacontacts_tags[account]:
872            for data in self._metacontacts_tags[account][tag]:
873                if data['jid'] == jid:
874                    found = data
875                    break
876            if found:
877                self._metacontacts_tags[account][tag].remove(found)
878                con = common.app.connections[account]
879                con.get_module('MetaContacts').store_metacontacts(
880                    self._metacontacts_tags[account])
881                break
882
883    def has_brother(self, account, jid, accounts):
884        tag = self._get_metacontacts_tag(account, jid)
885        if not tag:
886            return False
887        meta_jids = self._get_metacontacts_jids(tag, accounts)
888        return len(meta_jids) > 1 or len(meta_jids[account]) > 1
889
890    def is_big_brother(self, account, jid, accounts):
891        family = self.get_metacontacts_family(account, jid)
892        if family:
893            nearby_family = [data for data in family
894                    if account in accounts]
895            bb_data = self._get_metacontacts_big_brother(nearby_family)
896            if bb_data['jid'] == jid and bb_data['account'] == account:
897                return True
898        return False
899
900    def _get_metacontacts_jids(self, tag, accounts):
901        """
902        Return all jid for the given tag in the form {acct: [jid1, jid2],.}
903        """
904        answers = {}
905        for account in self._metacontacts_tags:
906            if tag in self._metacontacts_tags[account]:
907                if account not in accounts:
908                    continue
909                answers[account] = []
910                for data in self._metacontacts_tags[account][tag]:
911                    answers[account].append(data['jid'])
912        return answers
913
914    def get_metacontacts_family(self, account, jid):
915        """
916        Return the family of the given jid, including jid in the form:
917        [{'account': acct, 'jid': jid, 'order': order}, ] 'order' is optional
918        """
919        tag = self._get_metacontacts_tag(account, jid)
920        return self._get_metacontacts_family_from_tag(account, tag)
921
922    def _get_metacontacts_family_from_tag(self, account, tag):
923        if not tag:
924            return []
925        answers = []
926        if tag in self._metacontacts_tags[account]:
927            for data in self._metacontacts_tags[account][tag]:
928                data['account'] = account
929                answers.append(data)
930        return answers
931
932    def _metacontact_key(self, data):
933        """
934        Data is {'jid': jid, 'account': account, 'order': order} order is
935        optional
936        """
937        show_list = ['not in roster', 'error', 'offline', 'dnd',
938                     'xa', 'away', 'chat', 'online', 'requested', 'message']
939
940        jid = data['jid']
941        account = data['account']
942        # contact can be null when a jid listed in the metacontact data
943        # is not in our roster
944        contact = self._contacts.get_contact_with_highest_priority(
945            account, jid)
946        show = show_list.index(contact.show) if contact else 0
947        priority = contact.priority if contact else 0
948        has_order = 'order' in data
949        order = data.get('order', 0)
950        transport = common.app.get_transport_name_from_jid(jid)
951        server = common.app.get_server_from_jid(jid)
952        myserver = app.settings.get_account_setting(account, 'hostname')
953        return (bool(contact), show > 2, has_order, order, bool(transport),
954                show, priority, server == myserver, jid, account)
955
956    def get_nearby_family_and_big_brother(self, family, account):
957        """
958        Return the nearby family and its Big Brother
959
960        Nearby family is the part of the family that is grouped with the
961        metacontact.  A metacontact may be over different accounts. If accounts
962        are not merged then the given family is split account wise.
963
964        (nearby_family, big_brother_jid, big_brother_account)
965        """
966        if app.settings.get('mergeaccounts'):
967            # group all together
968            nearby_family = family
969        else:
970            # we want one nearby_family per account
971            nearby_family = [data for data in family if account == data['account']]
972
973        if not nearby_family:
974            return (None, None, None)
975        big_brother_data = self._get_metacontacts_big_brother(nearby_family)
976        big_brother_jid = big_brother_data['jid']
977        big_brother_account = big_brother_data['account']
978
979        return (nearby_family, big_brother_jid, big_brother_account)
980
981    def _get_metacontacts_big_brother(self, family):
982        """
983        Which of the family will be the big brother under which all others will be
984        ?
985        """
986        return max(family, key=self._metacontact_key)
987
988
989if __name__ == "__main__":
990    import doctest
991    doctest.testmod()
992