fix call to get_contacts_jid_list. Fixes #5570
[gajim.git] / src / common / contacts.py
blob59a57c047efce505ce4412d6f237e6e819abdf28
1 # -*- coding:utf-8 -*-
2 ## src/common/contacts.py
3 ##
4 ## Copyright (C) 2006 Dimitur Kirov <dkirov AT gmail.com>
5 ## Travis Shirk <travis AT pobox.com>
6 ## Nikos Kouremenos <kourem AT gmail.com>
7 ## Copyright (C) 2006-2008 Yann Leboulanger <asterix AT lagaule.org>
8 ## Jean-Marie Traissard <jim AT lapin.org>
9 ## Copyright (C) 2007 Lukas Petrovicky <lukas AT petrovicky.net>
10 ## Tomasz Melcer <liori AT exroot.org>
11 ## Julien Pivotto <roidelapluie AT gmail.com>
12 ## Copyright (C) 2007-2008 Stephan Erb <steve-e AT h3c.de>
13 ## Copyright (C) 2008 Brendan Taylor <whateley AT gmail.com>
14 ## Jonathan Schleifer <js-gajim AT webkeks.org>
16 ## This file is part of Gajim.
18 ## Gajim is free software; you can redistribute it and/or modify
19 ## it under the terms of the GNU General Public License as published
20 ## by the Free Software Foundation; version 3 only.
22 ## Gajim is distributed in the hope that it will be useful,
23 ## but WITHOUT ANY WARRANTY; without even the implied warranty of
24 ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
25 ## GNU General Public License for more details.
27 ## You should have received a copy of the GNU General Public License
28 ## along with Gajim. If not, see <http://www.gnu.org/licenses/>.
31 import common.gajim
33 class Contact:
34 '''Information concerning each contact'''
35 def __init__(self, jid='', name='', groups=[], show='', status='', sub='',
36 ask='', resource='', priority=0, keyID='', caps_node=None,
37 caps_hash_method=None, caps_hash=None, our_chatstate=None, chatstate=None,
38 last_status_time=None, msg_id = None, composing_xep = None, mood={}, tune={},
39 activity={}):
40 self.jid = jid
41 self.name = name
42 self.contact_name = '' # nick choosen by contact
43 self.groups = groups
44 self.show = show
45 self.status = status
46 self.sub = sub
47 self.ask = ask
48 self.resource = resource
49 self.priority = priority
50 self.keyID = keyID
52 # Capabilities; filled by caps.py/ConnectionCaps object
53 # every time it gets these from presence stanzas
54 self.caps_node = caps_node
55 self.caps_hash_method = caps_hash_method
56 self.caps_hash = caps_hash
58 # please read xep-85 http://www.xmpp.org/extensions/xep-0085.html
59 # we keep track of xep85 support with the peer by three extra states:
60 # None, False and 'ask'
61 # None if no info about peer
62 # False if peer does not support xep85
63 # 'ask' if we sent the first 'active' chatstate and are waiting for reply
64 # this holds what WE SEND to contact (our current chatstate)
65 self.our_chatstate = our_chatstate
66 self.msg_id = msg_id
67 # tell which XEP we're using for composing state
68 # None = have to ask, XEP-0022 = use this xep,
69 # XEP-0085 = use this xep, False = no composing support
70 self.composing_xep = composing_xep
71 # this is contact's chatstate
72 self.chatstate = chatstate
73 self.last_status_time = last_status_time
74 self.mood = mood.copy()
75 self.tune = tune.copy()
76 self.activity = activity.copy()
78 def get_full_jid(self):
79 if self.resource:
80 return self.jid + '/' + self.resource
81 return self.jid
83 def get_shown_name(self):
84 if self.name:
85 return self.name
86 if self.contact_name:
87 return self.contact_name
88 return self.jid.split('@')[0]
90 def get_shown_groups(self):
91 if self.is_observer():
92 return [_('Observers')]
93 elif self.is_groupchat():
94 return [_('Groupchats')]
95 elif self.is_transport():
96 return [_('Transports')]
97 elif not self.groups:
98 return [_('General')]
99 else:
100 return self.groups
102 def is_hidden_from_roster(self):
103 '''if contact should not be visible in roster'''
104 # XEP-0162: http://www.xmpp.org/extensions/xep-0162.html
105 if self.is_transport():
106 return False
107 if self.sub in ('both', 'to'):
108 return False
109 if self.sub in ('none', 'from') and self.ask == 'subscribe':
110 return False
111 if self.sub in ('none', 'from') and (self.name or len(self.groups)):
112 return False
113 if _('Not in Roster') in self.groups:
114 return False
115 return True
117 def is_observer(self):
118 # XEP-0162: http://www.xmpp.org/extensions/xep-0162.html
119 is_observer = False
120 if self.sub == 'from' and not self.is_transport()\
121 and self.is_hidden_from_roster():
122 is_observer = True
123 return is_observer
125 def is_groupchat(self):
126 for account in common.gajim.gc_connected:
127 if self.jid in common.gajim.gc_connected[account]:
128 return True
129 return False
131 def is_transport(self):
132 # if not '@' or '@' starts the jid then contact is transport
133 if self.jid.find('@') <= 0:
134 return True
135 return False
138 class GC_Contact:
139 '''Information concerning each groupchat contact'''
140 def __init__(self, room_jid='', name='', show='', status='', role='',
141 affiliation='', jid = '', resource = '', our_chatstate = None,
142 composing_xep = None, chatstate = None):
143 self.room_jid = room_jid
144 self.name = name
145 self.show = show
146 self.status = status
147 self.role = role
148 self.affiliation = affiliation
149 self.jid = jid
150 self.resource = resource
151 self.caps_node = None
152 self.caps_hash_method = None
153 self.caps_hash = None
154 self.our_chatstate = our_chatstate
155 self.composing_xep = composing_xep
156 self.chatstate = chatstate
158 def get_full_jid(self):
159 return self.room_jid + '/' + self.name
161 def get_shown_name(self):
162 return self.name
164 class Contacts:
165 '''Information concerning all contacts and groupchat contacts'''
166 def __init__(self):
167 self._contacts = {} # list of contacts {acct: {jid1: [C1, C2]}, } one Contact per resource
168 self._gc_contacts = {} # list of contacts that are in gc {acct: {room_jid: {nick: C}}}
170 # For meta contacts:
171 self._metacontacts_tags = {}
173 def change_account_name(self, old_name, new_name):
174 self._contacts[new_name] = self._contacts[old_name]
175 self._gc_contacts[new_name] = self._gc_contacts[old_name]
176 self._metacontacts_tags[new_name] = self._metacontacts_tags[old_name]
177 del self._contacts[old_name]
178 del self._gc_contacts[old_name]
179 del self._metacontacts_tags[old_name]
181 def change_contact_jid(self, old_jid, new_jid, account):
182 if account not in self._contacts:
183 return
184 if old_jid not in self._contacts[account]:
185 return
186 self._contacts[account][new_jid] = []
187 for _contact in self._contacts[account][old_jid]:
188 _contact.jid = new_jid
189 self._contacts[account][new_jid].append(_contact)
190 del self._contacts[account][old_jid]
192 def add_account(self, account):
193 self._contacts[account] = {}
194 self._gc_contacts[account] = {}
195 if account not in self._metacontacts_tags:
196 self._metacontacts_tags[account] = {}
198 def get_accounts(self):
199 return self._contacts.keys()
201 def remove_account(self, account):
202 del self._contacts[account]
203 del self._gc_contacts[account]
204 del self._metacontacts_tags[account]
206 def create_contact(self, jid='', name='', groups=[], show='', status='',
207 sub='', ask='', resource='', priority=0, keyID='', caps_node=None,
208 caps_hash_method=None, caps_hash=None, our_chatstate=None,
209 chatstate=None, last_status_time=None, composing_xep=None,
210 mood={}, tune={}, activity={}):
212 # We don't want duplicated group values
213 groups_unique = []
214 for group in groups:
215 if group not in groups_unique:
216 groups_unique.append(group)
218 return Contact(jid=jid, name=name, groups=groups_unique, show=show,
219 status=status, sub=sub, ask=ask, resource=resource, priority=priority,
220 keyID=keyID, caps_node=caps_node, caps_hash_method=caps_hash_method,
221 caps_hash=caps_hash, our_chatstate=our_chatstate, chatstate=chatstate,
222 last_status_time=last_status_time, composing_xep=composing_xep,
223 mood=mood, tune=tune, activity=activity)
225 def copy_contact(self, contact):
226 return self.create_contact(jid=contact.jid, name=contact.name,
227 groups=contact.groups, show=contact.show, status=contact.status,
228 sub=contact.sub, ask=contact.ask, resource=contact.resource,
229 priority=contact.priority, keyID=contact.keyID,
230 caps_node=contact.caps_node, caps_hash_method=contact.caps_hash_method,
231 caps_hash=contact.caps_hash, our_chatstate=contact.our_chatstate,
232 chatstate=contact.chatstate, last_status_time=contact.last_status_time)
234 def add_contact(self, account, contact):
235 # No such account before ?
236 if account not in self._contacts:
237 self._contacts[account] = {contact.jid : [contact]}
238 return
239 # No such jid before ?
240 if contact.jid not in self._contacts[account]:
241 self._contacts[account][contact.jid] = [contact]
242 return
243 contacts = self._contacts[account][contact.jid]
244 # We had only one that was offline, remove it
245 if len(contacts) == 1 and contacts[0].show == 'offline':
246 # Do not use self.remove_contact: it deteles
247 # self._contacts[account][contact.jid]
248 contacts.remove(contacts[0])
249 # If same JID with same resource already exists, use the new one
250 for c in contacts:
251 if c.resource == contact.resource:
252 self.remove_contact(account, c)
253 break
254 contacts.append(contact)
256 def remove_contact(self, account, contact):
257 if account not in self._contacts:
258 return
259 if contact.jid not in self._contacts[account]:
260 return
261 if contact in self._contacts[account][contact.jid]:
262 self._contacts[account][contact.jid].remove(contact)
263 if len(self._contacts[account][contact.jid]) == 0:
264 del self._contacts[account][contact.jid]
266 def clear_contacts(self, account):
267 self._contacts[account] = {}
269 def remove_jid(self, account, jid, remove_meta=True):
270 '''Removes all contacts for a given jid'''
271 if account not in self._contacts:
272 return
273 if jid not in self._contacts[account]:
274 return
275 del self._contacts[account][jid]
276 if remove_meta:
277 # remove metacontacts info
278 self.remove_metacontact(account, jid)
280 def get_contacts(self, account, jid):
281 '''Returns the list of contact instances for this jid.'''
282 if jid in self._contacts[account]:
283 return self._contacts[account][jid]
284 else:
285 return []
287 def get_contact(self, account, jid, resource=None):
288 ### WARNING ###
289 # This function returns a *RANDOM* resource if resource = None!
290 # Do *NOT* use if you need to get the contact to which you
291 # send a message for example, as a bare JID in Jabber means
292 # highest available resource, which this function ignores!
293 '''Returns the contact instance for the given resource if it's given else
294 the first contact is no resource is given or None if there is not'''
295 if jid in self._contacts[account]:
296 if not resource:
297 return self._contacts[account][jid][0]
298 for c in self._contacts[account][jid]:
299 if c.resource == resource:
300 return c
301 return None
303 def iter_contacts(self, account):
304 if account in self._contacts:
305 for jid in self._contacts[account].keys():
306 for contact in self._contacts[account][jid][:]:
307 yield contact
309 def get_contact_from_full_jid(self, account, fjid):
310 ''' Get Contact object for specific resource of given jid'''
311 barejid, resource = common.gajim.get_room_and_nick_from_fjid(fjid)
312 return self.get_contact(account, barejid, resource)
314 def get_highest_prio_contact_from_contacts(self, contacts):
315 if not contacts:
316 return None
317 prim_contact = contacts[0]
318 for contact in contacts[1:]:
319 if int(contact.priority) > int(prim_contact.priority):
320 prim_contact = contact
321 return prim_contact
323 def get_contact_with_highest_priority(self, account, jid):
324 contacts = self.get_contacts(account, jid)
325 if not contacts and '/' in jid:
326 # jid may be a fake jid, try it
327 room, nick = jid.split('/', 1)
328 contact = self.get_gc_contact(account, room, nick)
329 return contact
330 return self.get_highest_prio_contact_from_contacts(contacts)
332 def get_first_contact_from_jid(self, account, jid):
333 if jid in self._contacts[account]:
334 return self._contacts[account][jid][0]
335 return None
337 def get_contacts_from_group(self, account, group):
338 '''Returns all contacts in the given group'''
339 group_contacts = []
340 for jid in self._contacts[account]:
341 contacts = self.get_contacts(account, jid)
342 if group in contacts[0].groups:
343 group_contacts += contacts
344 return group_contacts
346 def get_nb_online_total_contacts(self, accounts=[], groups=[]):
347 '''Returns the number of online contacts and the total number of
348 contacts'''
349 if accounts == []:
350 accounts = self.get_accounts()
351 nbr_online = 0
352 nbr_total = 0
353 for account in accounts:
354 our_jid = common.gajim.get_jid_from_account(account)
355 for jid in self.get_jid_list(account):
356 if jid == our_jid:
357 continue
358 if common.gajim.jid_is_transport(jid) and not \
359 _('Transports') in groups:
360 # do not count transports
361 continue
362 if self.has_brother(account, jid, accounts) and not \
363 self.is_big_brother(account, jid, accounts):
364 # count metacontacts only once
365 continue
366 contact = self.get_contact_with_highest_priority(account, jid)
367 if _('Not in roster') in contact.groups:
368 continue
369 in_groups = False
370 if groups == []:
371 in_groups = True
372 else:
373 for group in groups:
374 if group in contact.get_shown_groups():
375 in_groups = True
376 break
378 if in_groups:
379 if contact.show not in ('offline', 'error'):
380 nbr_online += 1
381 nbr_total += 1
382 return nbr_online, nbr_total
384 def define_metacontacts(self, account, tags_list):
385 self._metacontacts_tags[account] = tags_list
387 def get_new_metacontacts_tag(self, jid):
388 if not jid in self._metacontacts_tags.keys():
389 return jid
390 #FIXME: can this append ?
391 assert False
393 def get_metacontacts_tags(self, account):
394 '''return a list of tags for a given account'''
395 if not account in self._metacontacts_tags:
396 return []
397 return self._metacontacts_tags[account].keys()
399 def get_metacontacts_tag(self, account, jid):
400 '''Returns the tag of a jid'''
401 if not account in self._metacontacts_tags:
402 return None
403 for tag in self._metacontacts_tags[account]:
404 for data in self._metacontacts_tags[account][tag]:
405 if data['jid'] == jid:
406 return tag
407 return None
409 def add_metacontact(self, brother_account, brother_jid, account, jid, order=None):
410 tag = self.get_metacontacts_tag(brother_account, brother_jid)
411 if not tag:
412 tag = self.get_new_metacontacts_tag(brother_jid)
413 self._metacontacts_tags[brother_account][tag] = [{'jid': brother_jid,
414 'tag': tag}]
415 if brother_account != account:
416 common.gajim.connections[brother_account].store_metacontacts(
417 self._metacontacts_tags[brother_account])
418 # be sure jid has no other tag
419 old_tag = self.get_metacontacts_tag(account, jid)
420 while old_tag:
421 self.remove_metacontact(account, jid)
422 old_tag = self.get_metacontacts_tag(account, jid)
423 if tag not in self._metacontacts_tags[account]:
424 self._metacontacts_tags[account][tag] = [{'jid': jid, 'tag': tag}]
425 else:
426 if order:
427 self._metacontacts_tags[account][tag].append({'jid': jid,
428 'tag': tag, 'order': order})
429 else:
430 self._metacontacts_tags[account][tag].append({'jid': jid,
431 'tag': tag})
432 common.gajim.connections[account].store_metacontacts(
433 self._metacontacts_tags[account])
435 def remove_metacontact(self, account, jid):
436 found = None
437 for tag in self._metacontacts_tags[account]:
438 for data in self._metacontacts_tags[account][tag]:
439 if data['jid'] == jid:
440 found = data
441 break
442 if found:
443 self._metacontacts_tags[account][tag].remove(found)
444 common.gajim.connections[account].store_metacontacts(
445 self._metacontacts_tags[account])
446 break
448 def has_brother(self, account, jid, accounts):
449 tag = self.get_metacontacts_tag(account, jid)
450 if not tag:
451 return False
452 meta_jids = self.get_metacontacts_jids(tag, accounts)
453 return len(meta_jids) > 1 or len(meta_jids[account]) > 1
455 def is_big_brother(self, account, jid, accounts):
456 family = self.get_metacontacts_family(account, jid)
457 if family:
458 nearby_family = [data for data in family
459 if account in accounts]
460 bb_data = self.get_metacontacts_big_brother(nearby_family)
461 if bb_data['jid'] == jid and bb_data['account'] == account:
462 return True
463 return False
465 def get_metacontacts_jids(self, tag, accounts):
466 '''Returns all jid for the given tag in the form {acct: [jid1, jid2],.}'''
467 answers = {}
468 for account in self._metacontacts_tags:
469 if tag in self._metacontacts_tags[account]:
470 if account not in accounts:
471 continue
472 answers[account] = []
473 for data in self._metacontacts_tags[account][tag]:
474 answers[account].append(data['jid'])
475 return answers
477 def get_metacontacts_family(self, account, jid):
478 '''return the family of the given jid, including jid in the form:
479 [{'account': acct, 'jid': jid, 'order': order}, ]
480 'order' is optional'''
481 tag = self.get_metacontacts_tag(account, jid)
482 return self.get_metacontacts_family_from_tag(account, tag)
484 def get_metacontacts_family_from_tag(self, account, tag):
485 if not tag:
486 return []
487 answers = []
488 for account in self._metacontacts_tags:
489 if tag in self._metacontacts_tags[account]:
490 for data in self._metacontacts_tags[account][tag]:
491 data['account'] = account
492 answers.append(data)
493 return answers
495 def compare_metacontacts(self, data1, data2):
496 '''compare 2 metacontacts.
497 Data is {'jid': jid, 'account': account, 'order': order}
498 order is optional'''
499 jid1 = data1['jid']
500 jid2 = data2['jid']
501 account1 = data1['account']
502 account2 = data2['account']
503 contact1 = self.get_contact_with_highest_priority(account1, jid1)
504 contact2 = self.get_contact_with_highest_priority(account2, jid2)
505 show_list = ['not in roster', 'error', 'offline', 'invisible', 'dnd',
506 'xa', 'away', 'chat', 'online', 'requested', 'message']
507 # contact can be null when a jid listed in the metacontact data
508 # is not in our roster
509 if not contact1:
510 if contact2:
511 return -1 # prefer the known contact
512 else:
513 show1 = 0
514 priority1 = 0
515 else:
516 show1 = show_list.index(contact1.show)
517 priority1 = contact1.priority
518 if not contact2:
519 if contact1:
520 return 1 # prefer the known contact
521 else:
522 show2 = 0
523 priority2 = 0
524 else:
525 show2 = show_list.index(contact2.show)
526 priority2 = contact2.priority
527 # If only one is offline, it's always second
528 if show1 > 2 and show2 < 3:
529 return 1
530 if show2 > 2 and show1 < 3:
531 return -1
532 if 'order' in data1 and 'order' in data2:
533 if data1['order'] > data2['order']:
534 return 1
535 if data1['order'] < data2['order']:
536 return -1
537 if 'order' in data1:
538 return 1
539 if 'order' in data2:
540 return -1
541 transport1 = common.gajim.get_transport_name_from_jid(jid1)
542 transport2 = common.gajim.get_transport_name_from_jid(jid2)
543 if transport2 and not transport1:
544 return 1
545 if transport1 and not transport2:
546 return -1
547 if show1 > show2:
548 return 1
549 if show2 > show1:
550 return -1
551 if priority1 > priority2:
552 return 1
553 if priority2 > priority1:
554 return -1
555 server1 = common.gajim.get_server_from_jid(jid1)
556 server2 = common.gajim.get_server_from_jid(jid2)
557 myserver1 = common.gajim.config.get_per('accounts', account1, 'hostname')
558 myserver2 = common.gajim.config.get_per('accounts', account2, 'hostname')
559 if server1 == myserver1:
560 if server2 != myserver2:
561 return 1
562 elif server2 == myserver2:
563 return -1
564 if jid1 > jid2:
565 return 1
566 if jid2 > jid1:
567 return -1
568 # If all is the same, compare accounts, they can't be the same
569 if account1 > account2:
570 return 1
571 if account2 > account1:
572 return -1
573 return 0
575 def get_metacontacts_big_brother(self, family):
576 '''which of the family will be the big brother under wich all
577 others will be ?'''
578 family.sort(cmp=self.compare_metacontacts)
579 return family[-1]
581 def is_pm_from_jid(self, account, jid):
582 '''Returns True if the given jid is a private message jid'''
583 if jid in self._contacts[account]:
584 return False
585 return True
587 def is_pm_from_contact(self, account, contact):
588 '''Returns True if the given contact is a private message contact'''
589 if isinstance(contact, Contact):
590 return False
591 return True
593 def get_jid_list(self, account):
594 return self._contacts[account].keys()
596 def get_contacts_jid_list(self):
597 return [jid for jid, contact in self._contacts.iteritems() if not
598 contact[0].is_groupchat()]
600 def contact_from_gc_contact(self, gc_contact):
601 '''Create a Contact instance from a GC_Contact instance'''
602 jid = gc_contact.get_full_jid()
603 return Contact(jid=jid, resource=gc_contact.resource,
604 name=gc_contact.name, groups=[], show=gc_contact.show,
605 status=gc_contact.status, sub='none', caps_node=gc_contact.caps_node,
606 caps_hash_method=gc_contact.caps_hash_method,
607 caps_hash=gc_contact.caps_hash)
609 def create_gc_contact(self, room_jid='', name='', show='', status='',
610 role='', affiliation='', jid='', resource=''):
611 return GC_Contact(room_jid, name, show, status, role, affiliation, jid,
612 resource)
614 def add_gc_contact(self, account, gc_contact):
615 # No such account before ?
616 if account not in self._gc_contacts:
617 self._contacts[account] = {gc_contact.room_jid : {gc_contact.name: \
618 gc_contact}}
619 return
620 # No such room_jid before ?
621 if gc_contact.room_jid not in self._gc_contacts[account]:
622 self._gc_contacts[account][gc_contact.room_jid] = {gc_contact.name: \
623 gc_contact}
624 return
625 self._gc_contacts[account][gc_contact.room_jid][gc_contact.name] = \
626 gc_contact
628 def remove_gc_contact(self, account, gc_contact):
629 if account not in self._gc_contacts:
630 return
631 if gc_contact.room_jid not in self._gc_contacts[account]:
632 return
633 if gc_contact.name not in self._gc_contacts[account][
634 gc_contact.room_jid]:
635 return
636 del self._gc_contacts[account][gc_contact.room_jid][gc_contact.name]
637 # It was the last nick in room ?
638 if not len(self._gc_contacts[account][gc_contact.room_jid]):
639 del self._gc_contacts[account][gc_contact.room_jid]
641 def remove_room(self, account, room_jid):
642 if account not in self._gc_contacts:
643 return
644 if room_jid not in self._gc_contacts[account]:
645 return
646 del self._gc_contacts[account][room_jid]
648 def get_gc_list(self, account):
649 if account not in self._gc_contacts:
650 return []
651 return self._gc_contacts[account].keys()
653 def get_nick_list(self, account, room_jid):
654 gc_list = self.get_gc_list(account)
655 if not room_jid in gc_list:
656 return []
657 return self._gc_contacts[account][room_jid].keys()
659 def get_gc_contact(self, account, room_jid, nick):
660 nick_list = self.get_nick_list(account, room_jid)
661 if not nick in nick_list:
662 return None
663 return self._gc_contacts[account][room_jid][nick]
665 def get_nb_role_total_gc_contacts(self, account, room_jid, role):
666 '''Returns the number of group chat contacts for the given role and the
667 total number of group chat contacts'''
668 if account not in self._gc_contacts:
669 return 0, 0
670 if room_jid not in self._gc_contacts[account]:
671 return 0, 0
672 nb_role = nb_total = 0
673 for nick in self._gc_contacts[account][room_jid]:
674 if self._gc_contacts[account][room_jid][nick].role == role:
675 nb_role += 1
676 nb_total += 1
677 return nb_role, nb_total
678 # vim: se ts=3: