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