# Copyright 2010-2011 Florent Le Coz # # This file is part of Poezio. # # Poezio is free software: you can redistribute it and/or modify # it under the terms of the zlib license. See the COPYING file. """ Defines the Roster and RosterGroup classes """ import logging log = logging.getLogger(__name__) from config import config from contact import Contact from roster_sorting import SORTING_METHODS, GROUP_SORTING_METHODS from os import path as p from datetime import datetime from common import safeJID from sleekxmpp import JID from sleekxmpp.exceptions import IqError, IqTimeout class Roster(object): """ The proxy class to get the roster from SleekXMPP. Caches Contact and RosterGroup objects. """ def __init__(self): """ node: the RosterSingle from SleekXMPP """ self.__node = None self.contact_filter = None # A tuple(function, *args) # function to filter contacts, # on search, for example self.folded_groups = set(config.get( 'folded_roster_groups', '', section='var').split(':')) self.groups = {} self.contacts = {} # Used for caching roster infos self.last_built = datetime.now() self.last_modified = datetime.now() def modified(self): self.last_modified = datetime.now() @property def needs_rebuild(self): return self.last_modified >= self.last_built def __getitem__(self, key): """Get a Contact from his bare JID""" key = safeJID(key).bare if key in self.contacts and self.contacts[key] is not None: return self.contacts[key] if key in self.jids(): contact = Contact(self.__node[key]) self.contacts[key] = contact return contact def __setitem__(self, key, value): """Set the a Contact value for the bare jid key""" self.contacts[key] = value def __delitem__(self, jid): """Remove a contact from the roster""" jid = safeJID(jid).bare contact = self[jid] if not contact: return for group in list(self.groups.values()): group.remove(contact) if not group: del self.groups[group.name] del self.contacts[contact.bare_jid] if jid in self.jids(): try: self.__node[jid].send_presence(ptype='unavailable') self.__node.remove(jid) except (IqError, IqTimeout): import traceback log.debug('IqError when removing %s:\n%s', jid, traceback.format_exc()) def __iter__(self): """Iterate over the jids of the contacts""" return iter(self.contacts.values()) def __contains__(self, key): """True if the bare jid is in the roster, false otherwise""" return safeJID(key).bare in self.jids() @property def jid(self): """Our JID""" return self.__node.jid def set_node(self, value): """Set the SleekXMPP RosterSingle for our roster""" self.__node = value def get_groups(self, sort=''): """Return a list of the RosterGroups""" group_list = sorted( filter( lambda x: bool(x), self.groups.values() ), key=lambda x: x.name.lower() if x.name else '' ) log.debug("Current groups: %s", group_list) for sorting in sort.split(':'): if sorting == 'reverse': group_list = list(reversed(group_list)) else: method = GROUP_SORTING_METHODS.get(sorting, lambda x: 0) group_list = sorted(group_list, key=method) return group_list def get_group(self, name): """Return a group or create it if not present""" if name in self.groups: return self.groups[name] self.groups[name] = RosterGroup(name, folded=name in self.folded_groups) def add(self, jid): """Subscribe to a jid""" self.__node.subscribe(jid) def jids(self): """List of the contact JIDS""" return [key for key in self.__node.keys() if key != self.jid] def get_contacts(self): """ Return a list of all the contacts """ return [self[jid] for jid in self.jids()] def save_to_config_file(self): """ Save various information to the config file e.g. the folded groups """ folded_groups = ':'.join([group.name for group in self.groups.values()\ if group.folded]) log.debug('folded:%s\n' %folded_groups) return config.silent_set('folded_roster_groups', folded_groups, 'var') def get_nb_connected_contacts(self): """ Get the number of connected contacts """ n = 0 for contact in self: if len(contact): n += 1 return n def update_contact_groups(self, contact): """Regenerate the RosterGroups when receiving a contact update""" if not isinstance(contact, Contact): contact = self[contact] if not contact: return for name, group in self.groups.items(): if name in contact.groups and contact not in group: group.add(contact) elif contact in group and name not in contact.groups: group.remove(contact) for group in contact.groups: if not group in self.groups: self.groups[group] = RosterGroup(group, folded=group in self.folded_groups) self.groups[group].add(contact) def __len__(self): """ Return the number of line that would be printed for the whole roster """ length = 0 show_offline = config.get('roster_show_offline', 'false') == 'true' for group in self.groups.values(): if not show_offline and group.get_nb_connected_contacts() == 0: continue before = length if not group.name in self.folded_groups: for contact in group.get_contacts(self.contact_filter): # We do not count the offline contacts (depending on config) if not show_offline and\ len(contact) == 0: continue length += 1 # One for the contact's line if not contact.folded(group.name): # One for each resource, if the contact is unfolded length += len(contact) if not self.contact_filter or before != length: length += 1 # One for the group's line itself if needed return length def __repr__(self): ret = '== Roster:\nContacts:\n' for contact in self.contacts.values(): ret += '%s\n' % (contact,) ret += 'Groups\n' for group in self.groups: ret += '%s\n' % (group,) return ret + '\n' def export(self, path): """Export a list of bare jids to a given file""" if p.isfile(path): return try: f = open(path, 'w+', encoding='utf-8') f.writelines([i + "\n" for i in self.contacts]) f.close() return True except IOError: return class RosterGroup(object): """ A RosterGroup is a group containing contacts It can be Friends/Family etc, but also can be Online/Offline or whatever """ def __init__(self, name, contacts=None, folded=False): if not contacts: contacts = [] self.contacts = set(contacts) self.name = name if name is not None else '' self.folded = folded # if the group content is to be shown def __iter__(self): """Iterate over the contacts""" return iter(self.contacts) def __repr__(self): return '' % (self.name, self.contacts) def __len__(self): """Number of contacts in the group""" return len(self.contacts) def __contains__(self, contact): """ Return a bool, telling if the contact is in the group """ return contact in self.contacts def add(self, contact): """Add a contact to the group""" self.contacts.add(contact) def remove(self, contact): """Remove a contact from the group if present""" if contact in self.contacts: self.contacts.remove(contact) def get_contacts(self, contact_filter=None, sort=''): """Return the group contacts, filtered and sorted""" contact_list = self.contacts.copy() if not contact_filter\ else [contact for contact in self.contacts.copy() if contact_filter[0](contact, contact_filter[1])] contact_list = sorted(contact_list, key=SORTING_METHODS['name']) for sorting in sort.split(':'): if sorting == 'reverse': contact_list = list(reversed(contact_list)) else: method = SORTING_METHODS.get(sorting, lambda x: 0) contact_list = sorted(contact_list, key=method) return contact_list def toggle_folded(self): """Fold/unfold the group in the roster""" self.folded = not self.folded if self.folded: if self.name not in roster.folded_groups: roster.folded_groups.add(self.name) else: if self.name in roster.folded_groups: roster.folded_groups.remove(self.name) def get_nb_connected_contacts(self): """Return the number of connected contacts""" return len([1 for contact in self.contacts if len(contact)]) # Shared roster object roster = Roster()