Commit a0ed4fb7 authored by Philipp Hörist's avatar Philipp Hörist Committed by Philipp Hörist
Browse files

Refactor VCard and VCardAvatars code

- Move XEP-0153 code from VCard into VCardAvatars
- Rely on new nbxmpp module VCardTemp
- Don’t advertise avatar with XEP-0153
parent f903a0a4
......@@ -57,7 +57,6 @@ def __init__(self, account):
self._priority = 0
self._connect_machine_calls = 0
self.avatar_conversion = False
self.addressing_supported = False
self.is_zeroconf = False
......@@ -451,10 +450,6 @@ def _finish_connect(self):
# We did not resume the stream, so we are not joined any MUCs
self.update_presence(include_muc=False)
if not self.avatar_conversion:
# ask our VCard
self.get_module('VCardTemp').request_vcard()
self.get_module('Bookmarks').request_bookmarks()
self.get_module('SoftwareVersion').set_enabled(True)
self.get_module('Annotations').request_annotations()
......
......@@ -71,7 +71,6 @@ def __init__(self, name):
self.pep = {}
self.roster_supported = True
self.avatar_conversion = False
self._stun_servers = [] # STUN servers of our jabber server
......
......@@ -128,13 +128,6 @@ class IdleState(Enum):
AWAKE = 'online'
@unique
class RequestAvatar(IntEnum):
SELF = 0
ROOM = 1
USER = 2
@unique
class PEPEventType(IntEnum):
ABSTRACT = 0
......
......@@ -824,13 +824,7 @@ def get_subscription_request_msg(account=None):
s = _('I would like to add you to my contact list.')
if account:
s = _('Hello, I am $name.') + ' ' + s
name = app.connections[account].get_module('VCardTemp').get_vard_name()
nick = app.nicks[account]
if name and nick:
name += ' (%s)' % nick
elif nick:
name = nick
s = Template(s).safe_substitute({'name': name})
s = Template(s).safe_substitute({'name': app.nicks[account]})
return s
def get_user_proxy(account):
......
......@@ -112,9 +112,7 @@ def _account_info_received(self, result):
self._con.get_module('PEP').pass_disco(result)
self._con.get_module('PubSub').pass_disco(result)
self._con.get_module('Bookmarks').pass_disco(result)
if 'urn:xmpp:pep-vcard-conversion:0' in result.features:
self._con.avatar_conversion = True
self._con.get_module('VCardAvatars').pass_disco(result)
self._con.get_module('Caps').update_caps()
......
......@@ -362,11 +362,6 @@ def get_presence(self, to=None, typ=None, priority=None,
nick_tag = presence.setTag('nick', namespace=Namespace.NICK)
nick_tag.setData(nick)
if not self._con.avatar_conversion:
# XEP-0398 not supported by server so
# we add the avatar sha to our presence
self._con.get_module('VCardAvatars').add_update_node(presence)
if (idle_time and
app.is_installed('IDLE') and
app.settings.get('autoaway')):
......
......@@ -14,16 +14,14 @@
# XEP-0153: vCard-Based Avatars
from pathlib import Path
from nbxmpp.namespaces import Namespace
from nbxmpp.structs import StanzaHandler
from nbxmpp.const import AvatarState
from nbxmpp.modules.util import is_error
from gajim.common import app
from gajim.common import configpaths
from gajim.common.const import RequestAvatar
from gajim.common.modules.base import BaseModule
from gajim.common.modules.util import task
class VCardAvatars(BaseModule):
......@@ -38,17 +36,55 @@ def __init__(self, con):
priority=51),
]
self.avatar_advertised = False
self._find_own_avatar()
self.avatar_conversion_available = False
def pass_disco(self, info):
is_available = Namespace.VCARD_CONVERSION in info.features
self.avatar_conversion_available = is_available
self._log.info('Discovered Avatar Conversion')
@task
def _request_vcard(self, jid, expected_sha, type_):
_task = yield
vcard = yield self._con.get_module('VCardTemp').request_vcard(jid=jid)
if is_error(vcard):
self._log.warning(vcard)
return
avatar, avatar_sha = vcard.get_avatar()
if avatar is None:
self._log.warning('Avatar missing: %s %s', jid, expected_sha)
return
def _find_own_avatar(self):
sha = app.settings.get_account_setting(self._account, 'avatar_sha')
if not sha:
if expected_sha != avatar_sha:
self._log.warning('Avatar mismatch: %s %s != %s',
jid,
expected_sha,
avatar_sha)
return
path = Path(configpaths.get('AVATAR')) / sha
if not path.exists():
self._log.info('Missing own avatar, reset sha')
app.settings.set_account_setting(self._account, 'avatar_sha', '')
self._log.info('Received: %s %s', jid, avatar_sha)
app.interface.save_avatar(avatar)
if type_ == 'contact':
self._con.get_module('Roster').set_avatar_sha(jid, avatar_sha)
app.contacts.set_avatar(self._account, jid, avatar_sha)
app.interface.update_avatar(self._account, jid)
elif type_ == 'muc':
app.storage.cache.set_muc_avatar_sha(jid, avatar_sha)
app.contacts.set_avatar(self._account, jid, avatar_sha)
app.interface.update_avatar(self._account, jid, room_avatar=True)
elif type_ == 'muc-user':
contact = app.contacts.get_gc_contact(self._account,
jid.bare,
jid.resource)
if contact is not None:
contact.avatar_sha = avatar_sha
app.interface.update_avatar(contact=contact)
def _presence_received(self, _con, _stanza, properties):
if not properties.type.is_available:
......@@ -59,15 +95,6 @@ def _presence_received(self, _con, _stanza, properties):
return
if self._con.get_own_jid().bare_match(properties.jid):
if self._con.get_own_jid() == properties.jid:
# Initial presence reflection
if self._con.avatar_conversion:
# XEP-0398: Tells us the current avatar sha on the
# initial presence reflection
self._self_update_received(properties)
else:
# Presence from another resource of ours
self._self_update_received(properties)
return
if properties.from_muc:
......@@ -78,38 +105,6 @@ def _presence_received(self, _con, _stanza, properties):
str(properties.jid))
self._update_received(properties, room=contact is not None)
def _self_update_received(self, properties):
jid = properties.jid.bare
if properties.avatar_state == AvatarState.EMPTY:
# Empty <photo/> tag, means no avatar is advertised
self._log.info('%s has no avatar published', properties.jid)
app.settings.set_account_setting(self._account, 'avatar_sha', '')
app.contacts.set_avatar(self._account, jid, None)
app.interface.update_avatar(self._account, jid)
return
self._log.info('Update: %s %s', jid, properties.avatar_sha)
current_sha = app.settings.get_account_setting(self._account,
'avatar_sha')
if properties.avatar_sha != current_sha:
path = Path(configpaths.get('AVATAR')) / properties.avatar_sha
if path.exists():
app.settings.set_account_setting(self._account,
'avatar_sha',
properties.avatar_sha)
app.contacts.set_avatar(self._account,
jid,
properties.avatar_sha)
app.interface.update_avatar(self._account, jid)
else:
self._log.info('Request : %s', jid)
self._con.get_module('VCardTemp').request_vcard(
RequestAvatar.SELF)
else:
self._log.info('Avatar already known: %s %s',
jid, properties.avatar_sha)
def muc_disco_info_update(self, disco_info):
if not disco_info.supports(Namespace.VCARD):
return
......@@ -135,9 +130,6 @@ def _process_update(self, jid, state, avatar_sha, room):
if state == AvatarState.EMPTY:
# Empty <photo/> tag, means no avatar is advertised
self._log.info('%s has no avatar published', jid)
# Remove avatar
self._log.debug('Remove: %s', jid)
app.contacts.set_avatar(self._account, jid, None)
if room:
......@@ -169,11 +161,9 @@ def _process_update(self, jid, state, avatar_sha, room):
if avatar_sha not in self._requested_shas:
self._requested_shas.append(avatar_sha)
if room:
self._con.get_module('VCardTemp').request_vcard(
RequestAvatar.ROOM, jid, sha=avatar_sha)
self._request_vcard(jid, avatar_sha, 'muc')
else:
self._con.get_module('VCardTemp').request_vcard(
RequestAvatar.USER, jid, sha=avatar_sha)
self._request_vcard(jid, avatar_sha, 'contact')
def _gc_update_received(self, properties):
nick = properties.jid.resource
......@@ -186,9 +176,8 @@ def _gc_update_received(self, properties):
return
if properties.avatar_state == AvatarState.EMPTY:
# Empty <photo/> tag, means no avatar is advertised, remove avatar
# Empty <photo/> tag, means no avatar is advertised
self._log.info('%s has no avatar published', nick)
self._log.debug('Remove: %s', nick)
gc_contact.avatar_sha = None
app.interface.update_avatar(contact=gc_contact)
else:
......@@ -197,9 +186,9 @@ def _gc_update_received(self, properties):
if properties.avatar_sha not in self._requested_shas:
app.log('avatar').info('Request: %s', nick)
self._requested_shas.append(properties.avatar_sha)
self._con.get_module('VCardTemp').request_vcard(
RequestAvatar.USER, str(properties.jid),
room=True, sha=properties.avatar_sha)
self._request_vcard(properties.jid,
properties.avatar_sha,
'muc-user')
return
if gc_contact.avatar_sha != properties.avatar_sha:
......@@ -210,29 +199,6 @@ def _gc_update_received(self, properties):
else:
self._log.info('Avatar already known: %s', nick)
def send_avatar_presence(self, force=False, after_publish=False):
if self._con.avatar_conversion:
if not after_publish:
# XEP-0398: We only resend presence after we publish a
# new avatar
return
else:
if self.avatar_advertised and not force:
self._log.debug('Avatar already advertised')
return
self._con.update_presence()
self.avatar_advertised = True
def add_update_node(self, node):
update = node.setTag('x', namespace=Namespace.VCARD_UPDATE)
if self._con.get_module('VCardTemp').own_vcard_received:
sha = app.settings.get_account_setting(self._account, 'avatar_sha')
own_jid = self._con.get_own_jid()
self._log.info('Send avatar presence to: %s %s',
node.getTo() or own_jid, sha or 'no sha advertised')
update.setTagData('photo', sha)
def get_instance(*args, **kwargs):
return VCardAvatars(*args, **kwargs), 'VCardAvatars'
......@@ -14,27 +14,25 @@
# XEP-0054: vcard-temp
import hashlib
import binascii
import base64
import nbxmpp
from nbxmpp.namespaces import Namespace
from gajim.common import app
from gajim.common.const import RequestAvatar
from gajim.common.nec import NetworkEvent
from gajim.common.modules.base import BaseModule
from gajim.common.connection_handlers_events import InformationEvent
class VCardTemp(BaseModule):
_nbxmpp_extends = 'VCardTemp'
_nbxmpp_methods = [
'request_vcard',
'set_vcard',
]
def __init__(self, con):
BaseModule.__init__(self, con)
self._own_vcard = None
self.own_vcard_received = False
self.room_jids = []
self.supported = False
def pass_disco(self, info):
......@@ -48,267 +46,6 @@ def pass_disco(self, info):
account=self._account,
feature=Namespace.VCARD))
@staticmethod
def _node_to_dict(node):
dict_ = {}
for info in node.getChildren():
name = info.getName()
if name in ('ADR', 'TEL', 'EMAIL'): # we can have several
dict_.setdefault(name, [])
entry = {}
for child in info.getChildren():
entry[child.getName()] = child.getData()
dict_[name].append(entry)
elif info.getChildren() == []:
dict_[name] = info.getData()
else:
dict_[name] = {}
for child in info.getChildren():
dict_[name][child.getName()] = child.getData()
return dict_
def request_vcard(self, callback=RequestAvatar.SELF, jid=None,
room=False, sha=None):
if not app.account_is_available(self._account):
return
if isinstance(callback, RequestAvatar):
if callback == RequestAvatar.SELF:
if not self.supported:
return
callback = self._on_own_avatar_received
elif callback == RequestAvatar.ROOM:
callback = self._on_room_avatar_received
elif callback == RequestAvatar.USER:
callback = self._on_avatar_received
if room:
room_jid = app.get_room_from_fjid(jid)
if room_jid not in self.room_jids:
self.room_jids.append(room_jid)
iq = nbxmpp.Iq(typ='get')
if jid:
iq.setTo(jid)
iq.setQuery('vCard').setNamespace(Namespace.VCARD)
own_jid = self._con.get_own_jid().bare
self._log.info('Request: %s, expected sha: %s', jid or own_jid, sha)
self._con.connection.SendAndCallForResponse(
iq, self._parse_vcard, {'callback': callback, 'expected_sha': sha})
def send_vcard(self, vcard, sha):
if not app.account_is_available(self._account):
return
iq = nbxmpp.Iq(typ='set')
iq2 = iq.setTag(Namespace.VCARD + ' vCard')
for i in vcard:
if i == 'jid':
continue
if isinstance(vcard[i], dict):
iq3 = iq2.addChild(i)
for j in vcard[i]:
iq3.addChild(j).setData(vcard[i][j])
elif isinstance(vcard[i], list):
for j in vcard[i]:
iq3 = iq2.addChild(i)
for k in j:
iq3.addChild(k).setData(j[k])
else:
iq2.addChild(i).setData(vcard[i])
self._log.info('Upload avatar: %s %s', self._account, sha)
self._con.connection.SendAndCallForResponse(
iq, self._avatar_publish_result, {'sha': sha})
def upload_room_avatar(self, room_jid, data):
iq = nbxmpp.Iq(typ='set', to=room_jid)
vcard = iq.addChild('vCard', namespace=Namespace.VCARD)
photo = vcard.addChild('PHOTO')
photo.addChild('TYPE', payload='image/png')
photo.addChild('BINVAL', payload=data)
self._log.info('Upload avatar: %s', room_jid)
self._con.connection.SendAndCallForResponse(
iq, self._upload_room_avatar_result)
@staticmethod
def _upload_room_avatar_result(_nbxmpp_client, stanza):
if not nbxmpp.isResultNode(stanza):
reason = stanza.getErrorMsg() or stanza.getError()
app.nec.push_incoming_event(InformationEvent(
None, dialog_name='avatar-upload-error', args=reason))
def _avatar_publish_result(self, _nbxmpp_client, stanza, sha):
if stanza.getType() == 'result':
current_sha = app.settings.get_account_setting(self._account,
'avatar_sha')
if current_sha != sha:
if not app.account_is_connected(self._account):
return
app.settings.set_account_setting(
self._account, 'avatar_sha', sha or '')
own_jid = self._con.get_own_jid().bare
app.contacts.set_avatar(self._account, own_jid, sha)
app.interface.update_avatar(
self._account, self._con.get_own_jid().bare)
self._con.get_module('VCardAvatars').send_avatar_presence(
after_publish=True)
self._log.info('%s: Published: %s', self._account, sha)
self._con.get_module('MUC').update_presence()
app.nec.push_incoming_event(
NetworkEvent('vcard-published', account=self._account))
elif stanza.getType() == 'error':
app.nec.push_incoming_event(
NetworkEvent('vcard-not-published', account=self._account))
def _get_vcard_photo(self, vcard, jid):
try:
photo = vcard['PHOTO']['BINVAL']
except (KeyError, AttributeError, TypeError):
avatar_sha = None
photo_decoded = None
else:
if photo == '':
avatar_sha = None
photo_decoded = None
else:
try:
photo_decoded = base64.b64decode(photo.encode('utf-8'))
except binascii.Error as error:
self._log.warning('Invalid avatar for %s: %s', jid, error)
return None, None
avatar_sha = hashlib.sha1(photo_decoded).hexdigest()
return avatar_sha, photo_decoded
def _parse_vcard(self, _nbxmpp_client, stanza, callback, expected_sha):
frm_jid = stanza.getFrom()
room = False
if frm_jid is None:
frm_jid = self._con.get_own_jid()
elif frm_jid.bare in self.room_jids:
room = True
resource = frm_jid.resource
jid = frm_jid.bare
stanza_error = stanza.getError()
if stanza_error in ('service-unavailable', 'item-not-found',
'not-allowed'):
self._log.info('vCard not available: %s %s', frm_jid, stanza_error)
callback(jid, resource, room, {}, expected_sha)
return
vcard_node = stanza.getTag('vCard', namespace=Namespace.VCARD)
if vcard_node is None:
self._log.info('vCard not available: %s', frm_jid)
self._log.debug(stanza)
return
vcard = self._node_to_dict(vcard_node)
if self._con.get_own_jid().bare_match(jid):
if 'NICKNAME' in vcard:
app.nicks[self._account] = vcard['NICKNAME']
elif 'FN' in vcard:
app.nicks[self._account] = vcard['FN']
app.nec.push_incoming_event(NetworkEvent('vcard-received',
account=self._account,
jid=jid,
vcard_dict=vcard))
callback(jid, resource, room, vcard, expected_sha)
def _on_own_avatar_received(self, jid, _resource, _room, vcard, *args):
avatar_sha, photo_decoded = self._get_vcard_photo(vcard, jid)
self._log.info('Received own vcard, avatar sha is: %s', avatar_sha)
self._own_vcard = vcard
self.own_vcard_received = True
if avatar_sha is None:
# No avatar found in vcard
self._log.info('No avatar found')
app.settings.set_account_setting(self._account, 'avatar_sha', '')
app.contacts.set_avatar(self._account, jid, avatar_sha)
self._con.get_module('VCardAvatars').send_avatar_presence(
force=True)
return
# Avatar found in vcard
current_sha = app.settings.get_account_setting(self._account,
'avatar_sha')
if current_sha == avatar_sha:
self._con.get_module('VCardAvatars').send_avatar_presence()
else:
app.interface.save_avatar(photo_decoded)
app.contacts.set_avatar(self._account, jid, avatar_sha)
app.settings.set_account_setting(
self._account, 'avatar_sha', avatar_sha)
self._con.get_module('VCardAvatars').send_avatar_presence(
force=True)
app.interface.update_avatar(self._account, jid)
def _on_room_avatar_received(self, jid, _resource, _room, vcard,
expected_sha):
avatar_sha, photo_decoded = self._get_vcard_photo(vcard, jid)
if expected_sha != avatar_sha:
self._log.warning('Avatar mismatch: %s %s', jid, avatar_sha)
return
app.interface.save_avatar(photo_decoded)
self._log.info('Received: %s %s', jid, avatar_sha)
app.storage.cache.set_muc_avatar_sha(jid, avatar_sha)
app.contacts.set_avatar(self._account, jid, avatar_sha)
app.interface.update_avatar(self._account, jid, room_avatar=True)
def _on_avatar_received(self, jid, resource, room, vcard, expected_sha):
request_jid = jid
if room:
request_jid = '%s/%s' % (jid, resource)
avatar_sha, photo_decoded = self._get_vcard_photo(vcard, request_jid)
if expected_sha != avatar_sha:
self._log.warning('Received: avatar mismatch: %s %s',
request_jid, avatar_sha)
return
app.interface.save_avatar(photo_decoded)
# Received vCard from a contact
if room:
self._log.info('Received: %s %s', resource, avatar_sha)
contact = app.contacts.get_gc_contact(self._account, jid, resource)
if contact is not None:
contact.avatar_sha = avatar_sha
app.interface.update_avatar(contact=contact)
else:
self._log.info('Received: %s %s', jid, avatar_sha)
self._con.get_module('Roster').set_avatar_sha(jid, avatar_sha)
app.contacts.set_avatar(self._account, jid, avatar_sha)
app.interface.update_avatar(self._account, jid)
def get_vard_name(self):
name = ''
vcard = self._own_vcard
if not vcard:
return name
if 'N' in vcard:
if 'GIVEN' in vcard['N'] and 'FAMILY' in vcard['N']:
name = vcard['N']['GIVEN'] + ' ' + vcard['N']['FAMILY']
if not name and 'FN' in vcard:
name = vcard['FN']