gajim-plural/gajim/common/modules/roster.py

358 lines
11 KiB
Python

# This file is part of Gajim.
#
# Gajim is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published
# by the Free Software Foundation; version 3 only.
#
# Gajim is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Gajim. If not, see <http://www.gnu.org/licenses/>.
# Roster
import logging
from collections import namedtuple
import nbxmpp
from gajim.common import app
from gajim.common.nec import NetworkEvent
log = logging.getLogger('gajim.c.m.roster')
RosterItem = namedtuple('RosterItem', 'jid data')
class Roster:
def __init__(self, con):
self._con = con
self._account = con.name
self.handlers = [
('iq', self._roster_push_received, 'set', nbxmpp.NS_ROSTER),
('presence', self._presence_received)
]
self._data = {}
self._set = None
def load_roster(self):
log.info('Load from database')
account_jid = self._con.get_own_jid().getStripped()
data = app.logger.get_roster(account_jid)
if data:
self.set_raw(data)
for jid, item in self._data.items():
app.nec.push_incoming_event(NetworkEvent(
'roster-info',
conn=self._con,
jid=jid,
nickname=item['name'],
sub=item['subscription'],
ask=item['ask'],
groups=item['groups'],
avatar_sha=item['avatar_sha']))
else:
log.info('Database empty, reset roster version')
app.config.set_per(
'accounts', self._account, 'roster_version', '')
app.nec.push_incoming_event(NetworkEvent(
'roster-received',
conn=self._con,
roster=self._data.copy(),
received_from_server=False))
def request_roster(self):
version = None
features = self._con.connection.Dispatcher.Stream.features
if features and features.getTag('ver', namespace=nbxmpp.NS_ROSTER_VER):
version = app.config.get_per(
'accounts', self._account, 'roster_version')
log.info('Requested from server')
iq = nbxmpp.Iq('get', nbxmpp.NS_ROSTER)
if version is not None:
iq.setTagAttr('query', 'ver', version)
log.info('Request version: %s', version)
self._con.connection.SendAndCallForResponse(
iq, self._roster_received)
def _roster_received(self, stanza):
if not nbxmpp.isResultNode(stanza):
log.warning('Unable to retrive roster: %s', stanza.getError())
else:
log.info('Received Roster')
received_from_server = False
if stanza.getTag('query') is not None:
# clear Roster
self._data = {}
version = self._parse_roster(stanza)
log.info('New version: %s', version)
app.logger.replace_roster(self._account, version, self._data)
received_from_server = True
app.nec.push_incoming_event(NetworkEvent(
'roster-received',
conn=self._con,
roster=self._data.copy(),
received_from_server=received_from_server))
self._con.connect_machine()
def _roster_push_received(self, _con, stanza):
log.info('Push received')
sender = stanza.getFrom()
if sender is not None:
if not self._con.get_own_jid().bareMatch(sender):
log.warning('Wrong JID %s', stanza.getFrom())
return
push_items, version = self._parse_push(stanza)
self._ack_roster_push(stanza)
for item in push_items:
attrs = item.data
app.nec.push_incoming_event(NetworkEvent(
'roster-info',
conn=self._con,
jid=item.jid,
nickname=attrs['name'],
sub=attrs['subscription'],
ask=attrs['ask'],
groups=attrs['groups'],
avatar_sha=None))
account_jid = self._con.get_own_jid().getStripped()
app.logger.add_or_update_contact(
account_jid, item.jid, attrs['name'],
attrs['subscription'], attrs['ask'], attrs['groups'])
log.info('New version: %s', version)
app.config.set_per(
'accounts', self._account, 'roster_version', version)
raise nbxmpp.NodeProcessed
def _parse_roster(self, stanza):
query = stanza.getTag('query')
version = query.getAttr('ver')
for item in query.getTags('item'):
jid = item.getAttr('jid')
self._data[jid] = self._get_item_attrs(item, update=False)
log.info('Item %s: %s', jid, self._data[jid])
return version
@staticmethod
def _get_item_attrs(item, update=True):
'''
update: True
returns only the attrs that are present in the item
update: False
returns the attrs of the item but fills missing
attrs with default values
'''
default_attrs = {'name': None,
'ask': None,
'subscription': None,
'groups': [],
'avatar_sha': None}
attrs = item.getAttrs()
del attrs['jid']
groups = {group.getData() for group in item.getTags('group')}
attrs['groups'] = list(groups)
if update:
return attrs
default_attrs.update(attrs)
return default_attrs
def _parse_push(self, stanza):
query = stanza.getTag('query')
version = query.getAttr('ver')
push_items = []
for item in query.getTags('item'):
push_items.append(self._update_roster_item(item))
for item in push_items:
log.info('Push: %s', item)
return push_items, version
def _update_roster_item(self, item):
jid = item.getAttr('jid')
if item.getAttr('subscription') == 'remove':
self._data.pop(jid, None)
attrs = self._get_item_attrs(item, update=False)
return RosterItem(jid, attrs)
if jid not in self._data:
self._data[jid] = self._get_item_attrs(item, update=False)
else:
self._data[jid].update(self._get_item_attrs(item))
return RosterItem(jid, self._data[jid])
def _ack_roster_push(self, stanza):
iq = nbxmpp.Iq('result',
to=stanza.getFrom(),
frm=stanza.getTo(),
attrs={'id': stanza.getID()})
self._con.connection.send(iq)
def _presence_received(self, _con, pres):
'''
Add contacts that request subscription to our internal
roster and also to the database. The contact is put into the
'Not in roster' group and because we save it to the database
it is also after a restart available.
'''
if pres.getType() != 'subscribe':
return
jid = pres.getFrom().getStripped()
if jid in self._data:
return
log.info('Add Contact from presence %s', jid)
self._data[jid] = {'name': None,
'ask': None,
'subscription':
'none',
'groups': ['Not in roster']}
account_jid = self._con.get_own_jid().getStripped()
app.logger.add_or_update_contact(
account_jid, jid,
self._data[jid]['name'],
self._data[jid]['subscription'],
self._data[jid]['ask'],
self._data[jid]['groups'])
def _get_item_data(self, jid, dataname):
"""
Return specific jid's representation in internal format.
"""
jid = jid[:(jid + '/').find('/')]
return self._data[jid][dataname]
def del_item(self, jid):
"""
Delete contact 'jid' from roster
"""
self._con.connection.send(
nbxmpp.Iq('set', nbxmpp.NS_ROSTER, payload=[
nbxmpp.Node('item', {'jid': jid, 'subscription': 'remove'})]))
def get_groups(self, jid):
"""
Return groups list that contact 'jid' belongs to
"""
return self._get_item_data(jid, 'groups')
def get_name(self, jid):
"""
Return name of contact 'jid'
"""
return self._get_item_data(jid, 'name')
def set_item(self, jid, name=None, groups=None):
"""
Rename contact 'jid' and sets the groups list that it now belongs to
"""
iq = nbxmpp.Iq('set', nbxmpp.NS_ROSTER)
query = iq.getTag('query')
attrs = {'jid': jid}
if name:
attrs['name'] = name
item = query.setTag('item', attrs)
if groups is not None:
for group in groups:
item.addChild(node=nbxmpp.Node('group', payload=[group]))
self._con.connection.send(iq)
def set_item_multi(self, items):
"""
Rename multiple contacts and sets their group lists
"""
for i in items:
iq = nbxmpp.Iq('set', nbxmpp.NS_ROSTER)
query = iq.getTag('query')
attrs = {'jid': i['jid']}
if i['name']:
attrs['name'] = i['name']
item = query.setTag('item', attrs)
for group in i['groups']:
item.addChild(node=nbxmpp.Node('group', payload=[group]))
self._con.connection.send(iq)
def get_items(self):
"""
Return list of all [bare] JIDs that the roster is currently tracks
"""
return list(self._data.keys())
def keys(self):
"""
Same as get_items. Provided for the sake of dictionary interface
"""
return list(self._data.keys())
def __getitem__(self, item):
"""
Get the contact in the internal format.
Raises KeyError if JID 'item' is not in roster
"""
return self._data[item]
def get_item(self, item):
"""
Get the contact in the internal format (or None if JID 'item' is not in
roster)
"""
if item in self._data:
return self._data[item]
def unsubscribe(self, jid):
"""
Ask for removing our subscription for JID 'jid'
"""
self._con.connection.send(nbxmpp.Presence(jid, 'unsubscribe'))
def get_raw(self):
"""
Return the internal data representation of the roster
"""
return self._data
def set_raw(self, data):
"""
Set the internal data representation of the roster
"""
own_jid = self._con.get_own_jid().getStripped()
self._data = data
self._data[own_jid] = {
'resources': {},
'name': None,
'ask': None,
'subscription': None,
'groups': None,
'avatar_sha': None
}
def get_instance(*args, **kwargs):
return Roster(*args, **kwargs), 'Roster'