2 ## src/common/contacts.py
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/>.
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
={},
42 self
.contact_name
= '' # nick choosen by contact
48 self
.resource
= resource
49 self
.priority
= priority
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
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
):
80 return self
.jid
+ '/' + self
.resource
83 def get_shown_name(self
):
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')]
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():
107 if self
.sub
in ('both', 'to'):
109 if self
.sub
in ('none', 'from') and self
.ask
== 'subscribe':
111 if self
.sub
in ('none', 'from') and (self
.name
or len(self
.groups
)):
113 if _('Not in Roster') in self
.groups
:
117 def is_observer(self
):
118 # XEP-0162: http://www.xmpp.org/extensions/xep-0162.html
120 if self
.sub
== 'from' and not self
.is_transport()\
121 and self
.is_hidden_from_roster():
125 def is_groupchat(self
):
126 for account
in common
.gajim
.gc_connected
:
127 if self
.jid
in common
.gajim
.gc_connected
[account
]:
131 def is_transport(self
):
132 # if not '@' or '@' starts the jid then contact is transport
133 if self
.jid
.find('@') <= 0:
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
148 self
.affiliation
= affiliation
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
):
165 '''Information concerning all contacts and groupchat contacts'''
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}}}
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
:
184 if old_jid
not in self
._contacts
[account
]:
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
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
]}
239 # No such jid before ?
240 if contact
.jid
not in self
._contacts
[account
]:
241 self
._contacts
[account
][contact
.jid
] = [contact
]
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
251 if c
.resource
== contact
.resource
:
252 self
.remove_contact(account
, c
)
254 contacts
.append(contact
)
256 def remove_contact(self
, account
, contact
):
257 if account
not in self
._contacts
:
259 if contact
.jid
not in self
._contacts
[account
]:
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
:
273 if jid
not in self
._contacts
[account
]:
275 del self
._contacts
[account
][jid
]
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
]
287 def get_contact(self
, account
, jid
, resource
=None):
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
]:
297 return self
._contacts
[account
][jid
][0]
298 for c
in self
._contacts
[account
][jid
]:
299 if c
.resource
== resource
:
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
][:]:
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
):
317 prim_contact
= contacts
[0]
318 for contact
in contacts
[1:]:
319 if int(contact
.priority
) > int(prim_contact
.priority
):
320 prim_contact
= 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
)
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]
337 def get_contacts_from_group(self
, account
, group
):
338 '''Returns all contacts in the given group'''
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
350 accounts
= self
.get_accounts()
353 for account
in accounts
:
354 our_jid
= common
.gajim
.get_jid_from_account(account
)
355 for jid
in self
.get_jid_list(account
):
358 if common
.gajim
.jid_is_transport(jid
) and not \
359 _('Transports') in groups
:
360 # do not count transports
362 if self
.has_brother(account
, jid
, accounts
) and not \
363 self
.is_big_brother(account
, jid
, accounts
):
364 # count metacontacts only once
366 contact
= self
.get_contact_with_highest_priority(account
, jid
)
367 if _('Not in roster') in contact
.groups
:
374 if group
in contact
.get_shown_groups():
379 if contact
.show
not in ('offline', 'error'):
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():
390 #FIXME: can this append ?
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
:
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
:
403 for tag
in self
._metacontacts
_tags
[account
]:
404 for data
in self
._metacontacts
_tags
[account
][tag
]:
405 if data
['jid'] == jid
:
409 def add_metacontact(self
, brother_account
, brother_jid
, account
, jid
, order
=None):
410 tag
= self
.get_metacontacts_tag(brother_account
, brother_jid
)
412 tag
= self
.get_new_metacontacts_tag(brother_jid
)
413 self
._metacontacts
_tags
[brother_account
][tag
] = [{'jid': brother_jid
,
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
)
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
}]
427 self
._metacontacts
_tags
[account
][tag
].append({'jid': jid
,
428 'tag': tag
, 'order': order
})
430 self
._metacontacts
_tags
[account
][tag
].append({'jid': jid
,
432 common
.gajim
.connections
[account
].store_metacontacts(
433 self
._metacontacts
_tags
[account
])
435 def remove_metacontact(self
, account
, jid
):
437 for tag
in self
._metacontacts
_tags
[account
]:
438 for data
in self
._metacontacts
_tags
[account
][tag
]:
439 if data
['jid'] == jid
:
443 self
._metacontacts
_tags
[account
][tag
].remove(found
)
444 common
.gajim
.connections
[account
].store_metacontacts(
445 self
._metacontacts
_tags
[account
])
448 def has_brother(self
, account
, jid
, accounts
):
449 tag
= self
.get_metacontacts_tag(account
, jid
)
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
)
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
:
465 def get_metacontacts_jids(self
, tag
, accounts
):
466 '''Returns all jid for the given tag in the form {acct: [jid1, jid2],.}'''
468 for account
in self
._metacontacts
_tags
:
469 if tag
in self
._metacontacts
_tags
[account
]:
470 if account
not in accounts
:
472 answers
[account
] = []
473 for data
in self
._metacontacts
_tags
[account
][tag
]:
474 answers
[account
].append(data
['jid'])
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
):
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
495 def compare_metacontacts(self
, data1
, data2
):
496 '''compare 2 metacontacts.
497 Data is {'jid': jid, 'account': account, 'order': order}
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
511 return -1 # prefer the known contact
516 show1
= show_list
.index(contact1
.show
)
517 priority1
= contact1
.priority
520 return 1 # prefer the known contact
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:
530 if show2
> 2 and show1
< 3:
532 if 'order' in data1
and 'order' in data2
:
533 if data1
['order'] > data2
['order']:
535 if data1
['order'] < data2
['order']:
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
:
545 if transport1
and not transport2
:
551 if priority1
> priority2
:
553 if priority2
> priority1
:
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
:
562 elif server2
== myserver2
:
568 # If all is the same, compare accounts, they can't be the same
569 if account1
> account2
:
571 if account2
> account1
:
575 def get_metacontacts_big_brother(self
, family
):
576 '''which of the family will be the big brother under wich all
578 family
.sort(cmp=self
.compare_metacontacts
)
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
]:
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
):
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
,
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
: \
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
: \
625 self
._gc
_contacts
[account
][gc_contact
.room_jid
][gc_contact
.name
] = \
628 def remove_gc_contact(self
, account
, gc_contact
):
629 if account
not in self
._gc
_contacts
:
631 if gc_contact
.room_jid
not in self
._gc
_contacts
[account
]:
633 if gc_contact
.name
not in self
._gc
_contacts
[account
][
634 gc_contact
.room_jid
]:
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
:
644 if room_jid
not in self
._gc
_contacts
[account
]:
646 del self
._gc
_contacts
[account
][room_jid
]
648 def get_gc_list(self
, account
):
649 if account
not in self
._gc
_contacts
:
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
:
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
:
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
:
670 if room_jid
not in self
._gc
_contacts
[account
]:
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
:
677 return nb_role
, nb_total