From 8b800f46464571f00dae498da406e5fb8fbd2e6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philipp=20H=C3=B6rist?= Date: Sat, 30 Jun 2018 19:23:10 +0200 Subject: [PATCH] Refactor VCard code into own modules --- gajim/common/app.py | 3 + gajim/common/connection.py | 8 +- gajim/common/connection_handlers.py | 393 +----------------- gajim/common/connection_handlers_events.py | 41 -- gajim/common/const.py | 6 + gajim/common/helpers.py | 10 +- gajim/common/modules/vcard_avatars.py | 192 +++++++++ gajim/common/modules/vcard_temp.py | 308 ++++++++++++++ .../zeroconf/connection_handlers_zeroconf.py | 8 +- gajim/groupchat_control.py | 4 +- gajim/profile_window.py | 7 +- gajim/remote_control.py | 6 +- gajim/vcard.py | 11 +- test/lib/gajim_mocks.py | 3 - 14 files changed, 537 insertions(+), 463 deletions(-) create mode 100644 gajim/common/modules/vcard_avatars.py create mode 100644 gajim/common/modules/vcard_temp.py diff --git a/gajim/common/app.py b/gajim/common/app.py index 8bdad77f3..bcccee4ec 100644 --- a/gajim/common/app.py +++ b/gajim/common/app.py @@ -397,6 +397,9 @@ def account_is_connected(account): else: return False +def is_invisible(account): + return SHOW_LIST[connections[account].connected] == 'invisible' + def account_is_disconnected(account): return not account_is_connected(account) diff --git a/gajim/common/connection.py b/gajim/common/connection.py index 5f464ca80..4fc325604 100644 --- a/gajim/common/connection.py +++ b/gajim/common/connection.py @@ -71,6 +71,8 @@ from gajim.common.modules.annotations import Annotations from gajim.common.modules.roster_item_exchange import RosterItemExchange from gajim.common.modules.last_activity import LastActivity from gajim.common.modules.http_auth import HTTPAuth +from gajim.common.modules.vcard_temp import VCardTemp +from gajim.common.modules.vcard_avatars import VCardAvatars from gajim.common.connection_handlers import * from gajim.common.contacts import GC_Contact from gajim.gtkgui_helpers import get_action @@ -666,6 +668,8 @@ class Connection(CommonConnection, ConnectionHandlers): self.register_module('RosterItemExchange', RosterItemExchange, self) self.register_module('LastActivity', LastActivity, self) self.register_module('HTTPAuth', HTTPAuth, self) + self.register_module('VCardTemp', VCardTemp, self) + self.register_module('VCardAvatars', VCardAvatars, self) app.ged.register_event_handler('privacy-list-received', ged.CORE, self._nec_privacy_list_received) @@ -756,7 +760,7 @@ class Connection(CommonConnection, ConnectionHandlers): self.connected = 0 self.time_to_reconnect = None self.privacy_rules_supported = False - self.avatar_presence_sent = False + self.get_module('VCardAvatars').avatar_advertised = False if on_purpose: self.sm = Smacks(self) if self.connection: @@ -1768,7 +1772,7 @@ class Connection(CommonConnection, ConnectionHandlers): show='invisible')) if initial: # ask our VCard - self.request_vcard(self._on_own_avatar_received) + self.get_module('VCardTemp').request_vcard() # Get bookmarks from private namespace self.get_bookmarks() diff --git a/gajim/common/connection_handlers.py b/gajim/common/connection_handlers.py index 0f6d68acf..be25c7828 100644 --- a/gajim/common/connection_handlers.py +++ b/gajim/common/connection_handlers.py @@ -283,388 +283,6 @@ class ConnectionDisco: app.nec.push_incoming_event(AgentInfoReceivedEvent(None, conn=self, stanza=iq_obj)) -class ConnectionVcard: - def __init__(self): - self.own_vcard = None - self.room_jids = [] - self.avatar_presence_sent = False - self._requested_shas = {} - - app.ged.register_event_handler('presence-received', ged.GUI2, - self._vcard_presence_received) - app.ged.register_event_handler('gc-presence-received', ged.GUI2, - self._vcard_gc_presence_received) - app.ged.register_event_handler('room-avatar-received', ged.GUI2, - self._vcard_presence_received) - - def _vcard_presence_received(self, obj): - if obj.conn.name != self.name: - return - - if obj.avatar_sha is None: - # No Avatar is advertised - return - - room_avatar = False - if isinstance(obj, RoomAvatarReceivedEvent): - room_avatar = True - - if self.get_own_jid().bareMatch(obj.jid): - app.log('avatar').info('Update (vCard): %s %s', - obj.jid, obj.avatar_sha) - current_sha = app.config.get_per( - 'accounts', self.name, 'avatar_sha') - if obj.avatar_sha != current_sha: - app.log('avatar').info( - 'Request (vCard): %s', obj.jid) - self.request_vcard(self._on_own_avatar_received) - else: - app.log('avatar').info( - 'Avatar already known (vCard): %s %s', - obj.jid, obj.avatar_sha) - return - - if obj.avatar_sha == '': - # Empty tag, means no avatar is advertised - app.log('avatar').info( - '%s has no avatar published (vCard)', obj.jid) - - # Remove avatar - app.log('avatar').debug('Remove: %s', obj.jid) - app.contacts.set_avatar(self.name, obj.jid, None) - own_jid = self.get_own_jid().getStripped() - if not room_avatar: - app.logger.set_avatar_sha(own_jid, obj.jid, None) - app.interface.update_avatar( - self.name, obj.jid, room_avatar=room_avatar) - else: - app.log('avatar').info( - 'Update (vCard): %s %s', obj.jid, obj.avatar_sha) - current_sha = app.contacts.get_avatar_sha(self.name, obj.jid) - - if obj.avatar_sha == current_sha: - app.log('avatar').info( - 'Avatar already known (vCard): %s %s', - obj.jid, obj.avatar_sha) - return - - if room_avatar: - # We dont save the room avatar hash in our DB, so check - # if we previously downloaded it - if app.interface.avatar_exists(obj.avatar_sha): - app.contacts.set_avatar(self.name, obj.jid, obj.avatar_sha) - app.interface.update_avatar( - self.name, obj.jid, room_avatar=room_avatar) - elif obj.jid not in self._requested_shas: - app.log('avatar').info( - 'Request (vCard): %s', obj.jid) - self._requested_shas[obj.jid] = obj.avatar_sha - self.request_vcard(self._on_room_avatar_received, obj.jid) - return - - if obj.jid not in self._requested_shas: - app.log('avatar').info( - 'Request (vCard): %s', obj.jid) - self._requested_shas[obj.jid] = obj.avatar_sha - self.request_vcard(self._on_avatar_received, obj.jid) - - - def _vcard_gc_presence_received(self, obj): - if obj.conn.name != self.name: - return - - server = app.get_server_from_jid(obj.room_jid) - if server.startswith('irc') or obj.avatar_sha is None: - return - - if obj.show == 'offline': - return - - gc_contact = app.contacts.get_gc_contact( - self.name, obj.room_jid, obj.nick) - - if gc_contact is None: - app.log('avatar').error('no gc contact found: %s', obj.nick) - return - - if obj.avatar_sha == '': - # Empty tag, means no avatar is advertised, remove avatar - app.log('avatar').info( - '%s has no avatar published (vCard)', obj.nick) - app.log('avatar').debug('Remove: %s', obj.nick) - gc_contact.avatar_sha = None - app.interface.update_avatar(contact=gc_contact) - else: - app.log('avatar').info( - 'Update (vCard): %s %s', obj.nick, obj.avatar_sha) - path = os.path.join(configpaths.get('AVATAR'), obj.avatar_sha) - if not os.path.isfile(path): - if obj.fjid not in self._requested_shas: - app.log('avatar').info( - 'Request (vCard): %s', obj.nick) - self._requested_shas[obj.fjid] = obj.avatar_sha - obj.conn.request_vcard( - self._on_avatar_received, obj.fjid, room=True) - return - - if gc_contact.avatar_sha != obj.avatar_sha: - app.log('avatar').info( - '%s changed his Avatar (vCard): %s', - obj.nick, obj.avatar_sha) - gc_contact.avatar_sha = obj.avatar_sha - app.interface.update_avatar(contact=gc_contact) - else: - app.log('avatar').info( - 'Avatar already known (vCard): %s', obj.nick) - - def send_avatar_presence(self): - show = helpers.get_xmpp_show(app.SHOW_LIST[self.connected]) - p = nbxmpp.Presence(typ=None, priority=self.priority, - show=show, status=self.status) - p = self.add_sha(p) - self.connection.send(p) - app.interface.update_avatar(self.name, self.get_own_jid().getStripped()) - - def _node_to_dict(self, 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 c in info.getChildren(): - entry[c.getName()] = c.getData() - dict_[name].append(entry) - elif info.getChildren() == []: - dict_[name] = info.getData() - else: - dict_[name] = {} - for c in info.getChildren(): - dict_[name][c.getName()] = c.getData() - return dict_ - - def request_vcard(self, callback, jid=None, room=False): - """ - Request the VCARD - """ - if not self.connection or self.connected < 2: - return - - 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(nbxmpp.NS_VCARD) - - self.connection.SendAndCallForResponse( - iq, self._parse_vcard, {'callback': callback}) - - def send_vcard(self, vcard, sha): - if not self.connection or self.connected < 2: - return - iq = nbxmpp.Iq(typ='set') - iq2 = iq.setTag(nbxmpp.NS_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.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=nbxmpp.NS_VCARD) - photo = vcard.addChild('PHOTO') - photo.addChild('TYPE', payload='image/png') - photo.addChild('BINVAL', payload=data) - - self.connection.SendAndCallForResponse( - iq, self._upload_room_avatar_result) - - def _upload_room_avatar_result(self, 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, con, stanza, sha): - if stanza.getType() == 'result': - current_sha = app.config.get_per( - 'accounts', self.name, 'avatar_sha') - if (current_sha != sha and - app.SHOW_LIST[self.connected] != 'invisible'): - if not self.connection or self.connected < 2: - return - app.config.set_per( - 'accounts', self.name, 'avatar_sha', sha or '') - own_jid = self.get_own_jid().getStripped() - app.contacts.set_avatar(self.name, own_jid, sha) - self.send_avatar_presence() - app.log('avatar').info('%s: Published: %s', self.name, sha) - app.nec.push_incoming_event( - VcardPublishedEvent(None, conn=self)) - - elif stanza.getType() == 'error': - app.nec.push_incoming_event( - VcardNotPublishedEvent(None, conn=self)) - - 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: - app.log('avatar').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, con, stanza, callback): - frm_jid = stanza.getFrom() - room = False - if frm_jid is None: - frm_jid = self.get_own_jid() - elif frm_jid.getStripped() in self.room_jids: - room = True - - resource = frm_jid.getResource() - jid = frm_jid.getStripped() - - stanza_error = stanza.getError() - if stanza_error in ('service-unavailable', 'item-not-found', - 'not-allowed'): - app.log('avatar').info('vCard not available: %s %s', - frm_jid, stanza_error) - callback(jid, resource, room, {}) - return - - vcard_node = stanza.getTag('vCard', namespace=nbxmpp.NS_VCARD) - if vcard_node is None: - app.log('avatar').info('vCard not available: %s', frm_jid) - app.log('avatar').debug(stanza) - return - vcard = self._node_to_dict(vcard_node) - - if self.get_own_jid().bareMatch(jid): - if 'NICKNAME' in vcard: - app.nicks[self.name] = vcard['NICKNAME'] - elif 'FN' in vcard: - app.nicks[self.name] = vcard['FN'] - - app.nec.push_incoming_event( - VcardReceivedEvent(None, conn=self, vcard_dict=vcard)) - - callback(jid, resource, room, vcard) - - def _on_own_avatar_received(self, jid, resource, room, vcard): - - avatar_sha, photo_decoded = self._get_vcard_photo(vcard, jid) - - app.log('avatar').info( - 'Received own (vCard): %s', avatar_sha) - - self.own_vcard = vcard - if avatar_sha is None: - app.log('avatar').info('No avatar found (vCard)') - app.config.set_per('accounts', self.name, 'avatar_sha', '') - self.send_avatar_presence() - return - - current_sha = app.config.get_per('accounts', self.name, 'avatar_sha') - if current_sha == avatar_sha: - path = os.path.join(configpaths.get('AVATAR'), current_sha) - if not os.path.isfile(path): - app.log('avatar').info( - 'Caching (vCard): %s', current_sha) - app.interface.save_avatar(photo_decoded) - if self.avatar_presence_sent: - app.log('avatar').debug('Avatar already advertised') - return - else: - app.interface.save_avatar(photo_decoded) - - app.config.set_per('accounts', self.name, 'avatar_sha', avatar_sha) - if app.SHOW_LIST[self.connected] == 'invisible': - app.log('avatar').info( - 'We are invisible, not publishing avatar') - return - - self.send_avatar_presence() - self.avatar_presence_sent = True - - def _on_room_avatar_received(self, jid, resource, room, vcard): - avatar_sha, photo_decoded = self._get_vcard_photo(vcard, jid) - expected_avatar_sha = self._requested_shas[jid] - if expected_avatar_sha != avatar_sha: - app.log('avatar').warning( - 'Avatar mismatch (vCard): %s %s', jid, avatar_sha) - return - - app.interface.save_avatar(photo_decoded) - self._requested_shas.pop(jid) - - app.log('avatar').info('Received (vCard): %s %s', jid, avatar_sha) - app.contacts.set_avatar(self.name, jid, avatar_sha) - app.interface.update_avatar(self.name, jid, room_avatar=True) - - def _on_avatar_received(self, jid, resource, room, vcard): - """ - Called when we receive a vCard Parse the vCard and trigger Events - """ - request_jid = jid - if room: - request_jid = '%s/%s' % (jid, resource) - - avatar_sha, photo_decoded = self._get_vcard_photo(vcard, request_jid) - expected_avatar_sha = self._requested_shas[request_jid] - if expected_avatar_sha != avatar_sha: - app.log('avatar').warning( - 'Avatar mismatch (vCard): %s %s', request_jid, avatar_sha) - return - - app.interface.save_avatar(photo_decoded) - self._requested_shas.pop(request_jid) - - # Received vCard from a contact - if room: - app.log('avatar').info( - 'Received (vCard): %s %s', resource, avatar_sha) - contact = app.contacts.get_gc_contact(self.name, jid, resource) - if contact is not None: - contact.avatar_sha = avatar_sha - app.interface.update_avatar(contact=contact) - else: - app.log('avatar').info('Received (vCard): %s %s', jid, avatar_sha) - own_jid = self.get_own_jid().getStripped() - app.logger.set_avatar_sha(own_jid, jid, avatar_sha) - app.contacts.set_avatar(self.name, jid, avatar_sha) - app.interface.update_avatar(self.name, jid) - class ConnectionPEP(object): @@ -1296,13 +914,12 @@ class ConnectionHandlersBase: return sess class ConnectionHandlers(ConnectionArchive313, -ConnectionVcard, ConnectionSocks5Bytestream, ConnectionDisco, +ConnectionSocks5Bytestream, ConnectionDisco, ConnectionCommands, ConnectionPubSub, ConnectionPEP, ConnectionCaps, ConnectionHandlersBase, ConnectionJingle, ConnectionIBBytestream, ConnectionHTTPUpload): def __init__(self): ConnectionArchive313.__init__(self) - ConnectionVcard.__init__(self) ConnectionSocks5Bytestream.__init__(self) ConnectionIBBytestream.__init__(self) ConnectionCommands.__init__(self) @@ -1398,11 +1015,7 @@ ConnectionHTTPUpload): app.ged.remove_event_handler('blocking', ged.CORE, self._nec_blocking) def add_sha(self, p, send_caps=True): - c = p.setTag('x', namespace=nbxmpp.NS_VCARD_UPDATE) - sha = app.config.get_per('accounts', self.name, 'avatar_sha') - app.log('avatar').info( - '%s: Send avatar presence: %s', self.name, sha or 'empty') - c.setTagData('photo', sha) + p = self.get_module('VCardAvatars').add_update_node(p) if send_caps: return self._add_caps(p) return p @@ -1925,7 +1538,7 @@ ConnectionHTTPUpload): show=show)) if self.vcard_supported: # ask our VCard - self.request_vcard(self._on_own_avatar_received) + self.get_module('VCardTemp').request_vcard() # Get bookmarks from private namespace self.get_bookmarks() diff --git a/gajim/common/connection_handlers_events.py b/gajim/common/connection_handlers_events.py index 5963f0423..4027a22bd 100644 --- a/gajim/common/connection_handlers_events.py +++ b/gajim/common/connection_handlers_events.py @@ -550,16 +550,6 @@ PresenceHelperEvent): tim = helpers.datetime_tuple(time_str) self.idle_time = timegm(tim) - # Check if presence is from the room itself, used when the room - # sends a avatar hash - contact = app.contacts.get_groupchat_contact(self.conn.name, self.fjid) - if contact: - app.nec.push_incoming_event( - RoomAvatarReceivedEvent( - None, conn=self.conn, stanza=self.stanza, - contact=contact, jid=self.jid)) - return - xtags = self.stanza.getTags('x') for x in xtags: namespace = x.getNamespace() @@ -567,9 +557,6 @@ PresenceHelperEvent): self.is_gc = True elif namespace == nbxmpp.NS_SIGNED: sig_tag = x - elif namespace == nbxmpp.NS_VCARD_UPDATE: - self.avatar_sha = x.getTagData('photo') - self.contact_nickname = x.getTagData('nickname') elif namespace == nbxmpp.NS_DELAY and not self.timestamp: # XEP-0091 self._generate_timestamp(self.stanza.timestamp) @@ -1770,14 +1757,6 @@ class ConnectionTypeEvent(nec.NetworkIncomingEvent): name = 'connection-type' base_network_events = [] -class VcardPublishedEvent(nec.NetworkIncomingEvent): - name = 'vcard-published' - base_network_events = [] - -class VcardNotPublishedEvent(nec.NetworkIncomingEvent): - name = 'vcard-not-published' - base_network_events = [] - class StanzaReceivedEvent(nec.NetworkIncomingEvent): name = 'stanza-received' base_network_events = [] @@ -1967,13 +1946,6 @@ class NonAnonymousServerErrorEvent(nec.NetworkIncomingEvent): name = 'non-anonymous-server-error' base_network_events = [] -class VcardReceivedEvent(nec.NetworkIncomingEvent): - name = 'vcard-received' - base_network_events = [] - - def generate(self): - return True - class UpdateGCAvatarEvent(nec.NetworkIncomingEvent): name = 'update-gc-avatar' base_network_events = [] @@ -1995,19 +1967,6 @@ class UpdateRoomAvatarEvent(nec.NetworkIncomingEvent): def generate(self): return True -class RoomAvatarReceivedEvent(nec.NetworkIncomingEvent): - name = 'room-avatar-received' - base_network_events = [] - - def generate(self): - vcard = self.stanza.getTag('x', namespace=nbxmpp.NS_VCARD_UPDATE) - if vcard is None: - app.log('avatar').info( - '%s has no avatar published (vCard)', self.jid) - return - self.avatar_sha = vcard.getTagData('photo') - return True - class PEPConfigReceivedEvent(nec.NetworkIncomingEvent): name = 'pep-config-received' base_network_events = [] diff --git a/gajim/common/const.py b/gajim/common/const.py index 3c7ce2e33..c77cbb37b 100644 --- a/gajim/common/const.py +++ b/gajim/common/const.py @@ -113,6 +113,12 @@ class IdleState(IntEnum): AWAY = 2 AWAKE = 3 +@unique +class RequestAvatar(IntEnum): + SELF = 0 + ROOM = 1 + USER = 2 + SSLError = { 2: _("Unable to get issuer certificate"), 3: _("Unable to get certificate CRL"), diff --git a/gajim/common/helpers.py b/gajim/common/helpers.py index 52321e6ec..ad7a1c7df 100644 --- a/gajim/common/helpers.py +++ b/gajim/common/helpers.py @@ -1398,15 +1398,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 - our_jid = app.get_jid_from_account(account) - vcard = app.connections[account].own_vcard - name = '' - if vcard: - 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'] + name = app.connections[account].get_module('VCardTemp').get_vard_name() nick = app.nicks[account] if name and nick: name += ' (%s)' % nick diff --git a/gajim/common/modules/vcard_avatars.py b/gajim/common/modules/vcard_avatars.py new file mode 100644 index 000000000..bff1f63b3 --- /dev/null +++ b/gajim/common/modules/vcard_avatars.py @@ -0,0 +1,192 @@ +# 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 . + +# XEP-0153: vCard-Based Avatars + +import os +import logging + +import nbxmpp + +from gajim.common import app +from gajim.common import helpers +from gajim.common import configpaths +from gajim.common.const import RequestAvatar + +log = logging.getLogger('gajim.c.m.vcard.avatars') + + +class VCardAvatars: + def __init__(self, con): + self._con = con + self._account = con.name + self._requested_shas = [] + + self.handlers = [ + ('presence', self._presence_received, '', nbxmpp.NS_VCARD_UPDATE), + ] + + self.avatar_advertised = False + + def _presence_received(self, con, stanza): + update = stanza.getTag('x', namespace=nbxmpp.NS_VCARD_UPDATE) + if update is None: + return + + jid = stanza.getFrom() + + avatar_sha = update.getTagData('photo') + if avatar_sha is None: + log.info('%s is not ready to promote an avatar', jid) + # Empty update element, ignore + return + + if self._con.get_own_jid().bareMatch(jid): + if self._con.get_own_jid() == jid: + # Reflection of our own presence + return + self._self_update_received(jid, avatar_sha) + return + + # Check if presence is from a MUC service + contact = app.contacts.get_groupchat_contact(self._account, str(jid)) + if contact is not None: + self._update_received(jid, avatar_sha) + elif stanza.getTag('x', namespace=nbxmpp.NS_MUC_USER): + show = stanza.getShow() + type_ = stanza.getType() + self._gc_update_received(jid, avatar_sha, show, type_) + else: + self._update_received(jid, avatar_sha) + + def _self_update_received(self, jid, avatar_sha): + jid = jid.getStripped() + full_jid = jid + if avatar_sha == '': + # Empty tag, means no avatar is advertised + log.info('%s has no avatar published', full_jid) + return + + log.info('Update: %s %s', jid, avatar_sha) + current_sha = app.config.get_per( + 'accounts', self._account, 'avatar_sha') + + if avatar_sha != current_sha: + log.info('Request : %s', jid) + self._con.get_module('VCardTemp').request_vcard(RequestAvatar.SELF) + else: + log.info('Avatar already known: %s %s', + jid, avatar_sha) + + def _update_received(self, jid, avatar_sha, room=False): + jid = jid.getStripped() + full_jid = jid + if avatar_sha == '': + # Empty tag, means no avatar is advertised + log.info('%s has no avatar published', full_jid) + + # Remove avatar + log.debug('Remove: %s', jid) + app.contacts.set_avatar(self._account, jid, None) + acc_jid = self._con.get_own_jid().getStripped() + if not room: + app.logger.set_avatar_sha(acc_jid, jid, None) + app.interface.update_avatar( + self._account, jid, room_avatar=room) + else: + log.info('Update: %s %s', full_jid, avatar_sha) + current_sha = app.contacts.get_avatar_sha(self._account, jid) + + if avatar_sha == current_sha: + log.info('Avatar already known: %s %s', jid, avatar_sha) + return + + if room: + # We dont save the room avatar hash in our DB, so check + # if we previously downloaded it + if app.interface.avatar_exists(avatar_sha): + app.contacts.set_avatar(self._account, jid, avatar_sha) + app.interface.update_avatar( + self._account, jid, room_avatar=room) + return + + 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) + else: + self._con.get_module('VCardTemp').request_vcard( + RequestAvatar.USER, jid, sha=avatar_sha) + + def _gc_update_received(self, jid, avatar_sha, show, type_): + if show == 'offline' or type_ == 'unavailable': + return + + nick = jid.getResource() + + gc_contact = app.contacts.get_gc_contact( + self._account, jid.getStripped(), nick) + + if gc_contact is None: + log.error('no gc contact found: %s', nick) + return + + if avatar_sha == '': + # Empty tag, means no avatar is advertised, remove avatar + log.info('%s has no avatar published', nick) + log.debug('Remove: %s', nick) + gc_contact.avatar_sha = None + app.interface.update_avatar(contact=gc_contact) + else: + log.info('Update: %s %s', nick, avatar_sha) + path = os.path.join(configpaths.get('AVATAR'), avatar_sha) + if not os.path.isfile(path): + if avatar_sha not in self._requested_shas: + app.log('avatar').info('Request: %s', nick) + self._requested_shas.append(avatar_sha) + self._con.get_module('VCardTemp').request_vcard( + RequestAvatar.USER, str(jid), + room=True, sha=avatar_sha) + return + + if gc_contact.avatar_sha != avatar_sha: + log.info('%s changed his Avatar: %s', nick, avatar_sha) + gc_contact.avatar_sha = avatar_sha + app.interface.update_avatar(contact=gc_contact) + else: + log.info('Avatar already known: %s', nick) + + def send_avatar_presence(self, force=False): + if self.avatar_advertised and not force: + log.debug('Avatar already advertised') + return + show = helpers.get_xmpp_show(app.SHOW_LIST[self._con.connected]) + pres = nbxmpp.Presence(typ=None, priority=self._con.priority, + show=show, status=self._con.status) + pres = self._con.add_sha(pres) + self._con.connection.send(pres) + self.avatar_advertised = True + app.interface.update_avatar(self._account, + self._con.get_own_jid().getStripped()) + + def add_update_node(self, node): + update = node.setTag('x', namespace=nbxmpp.NS_VCARD_UPDATE) + if self._con.get_module('VCardTemp').own_vcard_received: + sha = app.config.get_per('accounts', self._account, 'avatar_sha') + own_jid = self._con.get_own_jid() + log.info('Send avatar presence to: %s %s', + node.getTo() or own_jid, sha or 'no sha advertised') + update.setTagData('photo', sha) + return node diff --git a/gajim/common/modules/vcard_temp.py b/gajim/common/modules/vcard_temp.py new file mode 100644 index 000000000..5a5b53916 --- /dev/null +++ b/gajim/common/modules/vcard_temp.py @@ -0,0 +1,308 @@ +# 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 . + +# XEP-0054: vcard-temp + +import os +import hashlib +import binascii +import base64 +import logging + +import nbxmpp + +from gajim.common import app +from gajim.common import configpaths +from gajim.common.const import RequestAvatar +from gajim.common.nec import NetworkIncomingEvent +from gajim.common.connection_handlers_events import InformationEvent + +log = logging.getLogger('gajim.c.m.vcard') + + +class VCardTemp: + def __init__(self, con): + self._con = con + self._account = con.name + + self.handlers = [] + + self._own_vcard = None + self.own_vcard_received = False + self.room_jids = [] + + def _node_to_dict(self, 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 c in info.getChildren(): + entry[c.getName()] = c.getData() + dict_[name].append(entry) + elif info.getChildren() == []: + dict_[name] = info.getData() + else: + dict_[name] = {} + for c in info.getChildren(): + dict_[name][c.getName()] = c.getData() + return dict_ + + def request_vcard(self, callback=RequestAvatar.SELF, jid=None, + room=False, sha=None): + if not app.account_is_connected(self._account): + return + + if isinstance(callback, RequestAvatar): + if callback == RequestAvatar.SELF: + 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(nbxmpp.NS_VCARD) + + own_jid = self._con.get_own_jid().getStripped() + 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_connected(self._account): + return + + iq = nbxmpp.Iq(typ='set') + iq2 = iq.setTag(nbxmpp.NS_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]) + + 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=nbxmpp.NS_VCARD) + photo = vcard.addChild('PHOTO') + photo.addChild('TYPE', payload='image/png') + photo.addChild('BINVAL', payload=data) + + log.info('Upload avatar: %s %s', room_jid) + self._con.connection.SendAndCallForResponse( + iq, self._upload_room_avatar_result) + + def _upload_room_avatar_result(self, 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, con, stanza, sha): + if stanza.getType() == 'result': + current_sha = app.config.get_per( + 'accounts', self._account, 'avatar_sha') + if (current_sha != sha and not app.is_invisible(self._account)): + if not app.account_is_connected(self._account): + return + app.config.set_per( + 'accounts', self._account, 'avatar_sha', sha or '') + own_jid = self._con.get_own_jid().getStripped() + app.contacts.set_avatar(self._account, own_jid, sha) + self._con.get_module('VCardAvatars').send_avatar_presence( + force=True) + log.info('%s: Published: %s', self._account, sha) + app.nec.push_incoming_event( + VcardPublishedEvent(None, conn=self._con)) + + elif stanza.getType() == 'error': + app.nec.push_incoming_event( + VcardNotPublishedEvent(None, conn=self._con)) + + 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: + 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, con, 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.getStripped() in self.room_jids: + room = True + + resource = frm_jid.getResource() + jid = frm_jid.getStripped() + + stanza_error = stanza.getError() + if stanza_error in ('service-unavailable', 'item-not-found', + 'not-allowed'): + log.info('vCard not available: %s %s', frm_jid, stanza_error) + callback(jid, resource, room, {}, expected_sha) + return + + vcard_node = stanza.getTag('vCard', namespace=nbxmpp.NS_VCARD) + if vcard_node is None: + log.info('vCard not available: %s', frm_jid) + log.debug(stanza) + return + vcard = self._node_to_dict(vcard_node) + + if self._con.get_own_jid().bareMatch(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( + VcardReceivedEvent(None, conn=self._con, 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) + + 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: + log.info('No avatar found') + app.config.set_per('accounts', self._account, 'avatar_sha', '') + self._con.get_module('VCardAvatars').send_avatar_presence(force=True) + return + + current_sha = app.config.get_per('accounts', self._account, 'avatar_sha') + if current_sha == avatar_sha: + path = os.path.join(configpaths.get('AVATAR'), current_sha) + if not os.path.isfile(path): + log.info('Caching: %s', current_sha) + app.interface.save_avatar(photo_decoded) + self._con.get_module('VCardAvatars').send_avatar_presence() + else: + app.interface.save_avatar(photo_decoded) + + app.config.set_per('accounts', self._account, 'avatar_sha', avatar_sha) + if app.is_invisible(self._account): + log.info('We are invisible, not advertising avatar') + return + + self._con.get_module('VCardAvatars').send_avatar_presence(force=True) + + 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: + log.warning('Avatar mismatch: %s %s', jid, avatar_sha) + return + + app.interface.save_avatar(photo_decoded) + + log.info('Received: %s %s', 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: + 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: + 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: + log.info('Received: %s %s', jid, avatar_sha) + own_jid = self._con.get_own_jid().getStripped() + app.logger.set_avatar_sha(own_jid, 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'] + return name + + +class VcardPublishedEvent(NetworkIncomingEvent): + name = 'vcard-published' + base_network_events = [] + + +class VcardNotPublishedEvent(NetworkIncomingEvent): + name = 'vcard-not-published' + base_network_events = [] + + +class VcardReceivedEvent(NetworkIncomingEvent): + name = 'vcard-received' + base_network_events = [] diff --git a/gajim/common/zeroconf/connection_handlers_zeroconf.py b/gajim/common/zeroconf/connection_handlers_zeroconf.py index 3684e4827..f67a75293 100644 --- a/gajim/common/zeroconf/connection_handlers_zeroconf.py +++ b/gajim/common/zeroconf/connection_handlers_zeroconf.py @@ -40,19 +40,13 @@ AGENT_REMOVED = 'agent_removed' from gajim.common import connection_handlers -class ConnectionVcard(connection_handlers.ConnectionVcard): +class ConnectionVcard: def add_sha(self, p, *args): return p def add_caps(self, p): return p - def request_vcard(self, *args): - pass - - def send_vcard(self, *args): - pass - class ConnectionHandlersZeroconf(ConnectionVcard, ConnectionSocks5BytestreamZeroconf, ConnectionCommands, diff --git a/gajim/groupchat_control.py b/gajim/groupchat_control.py index 1761ef5d9..69287f1a9 100644 --- a/gajim/groupchat_control.py +++ b/gajim/groupchat_control.py @@ -762,8 +762,8 @@ class GroupchatControl(ChatControlBase): publish = app.interface.get_avatar(sha, publish=True) avatar = base64.b64encode(publish).decode('utf-8') - - app.connections[self.account].upload_room_avatar( + con = app.connections[self.account] + con.get_module('VCardTemp').upload_room_avatar( self.room_jid, avatar) AvatarChooserDialog(_on_accept, diff --git a/gajim/profile_window.py b/gajim/profile_window.py index 16e8417d3..e178e0e2e 100644 --- a/gajim/profile_window.py +++ b/gajim/profile_window.py @@ -77,7 +77,7 @@ class ProfileWindow: self._nec_vcard_not_published) self.window.show_all() self.xml.get_object('ok_button').grab_focus() - app.connections[account].request_vcard( + app.connections[account].get_module('VCardTemp').request_vcard( self._nec_vcard_received, self.jid) def on_information_notebook_switch_page(self, widget, page, page_num): @@ -261,7 +261,7 @@ class ProfileWindow: self.progressbar.set_fraction(0) self.update_progressbar_timeout_id = None - def _nec_vcard_received(self, jid, resource, room, vcard_): + def _nec_vcard_received(self, jid, resource, room, vcard_, *args): self.set_values(vcard_) def add_to_vcard(self, vcard_, entry, txt): @@ -339,7 +339,8 @@ class ProfileWindow: app.connections[self.account].retract_nickname() nick = app.config.get_per('accounts', self.account, 'name') app.nicks[self.account] = nick - app.connections[self.account].send_vcard(vcard_, sha) + app.connections[self.account].get_module('VCardTemp').send_vcard( + vcard_, sha) self.message_id = self.statusbar.push(self.context_id, _('Sending profileā€¦')) self.progressbar.show() diff --git a/gajim/remote_control.py b/gajim/remote_control.py index da27f147c..36e8836f1 100644 --- a/gajim/remote_control.py +++ b/gajim/remote_control.py @@ -840,10 +840,12 @@ class SignalObject(dbus.service.Object): if avatar_mime_type: vcard['PHOTO']['TYPE'] = avatar_mime_type if account: - app.connections[account].send_vcard(vcard, sha) + app.connections[account].get_module('VCardTemp').send_vcard( + vcard, sha) else: for acc in app.connections: - app.connections[acc].send_vcard(vcard, sha) + app.connections[acc].get_module('VCardTemp').send_vcard( + vcard, sha) @dbus.service.method(INTERFACE, in_signature='ssss', out_signature='') def join_room(self, room_jid, nick, password, account): diff --git a/gajim/vcard.py b/gajim/vcard.py index 29b275bbc..1ad3a64f7 100644 --- a/gajim/vcard.py +++ b/gajim/vcard.py @@ -268,7 +268,7 @@ class VcardWindow: widget.set_text('') self.xml.get_object('DESC_textview').get_buffer().set_text('') - def _nec_vcard_received(self, jid, resource, room, vcard): + def _nec_vcard_received(self, jid, resource, room, vcard, *args): self.clear_values() self._set_values(vcard, jid) @@ -477,10 +477,13 @@ class VcardWindow: self.fill_status_label() if self.gc_contact: - con.request_vcard(self._nec_vcard_received, - self.gc_contact.get_full_jid(), room=True) + con.get_module('VCardTemp').request_vcard( + self._nec_vcard_received, + self.gc_contact.get_full_jid(), + room=True) else: - con.request_vcard(self._nec_vcard_received, self.contact.jid) + con.get_module('VCardTemp').request_vcard( + self._nec_vcard_received, self.contact.jid) def on_close_button_clicked(self, widget): self.window.destroy() diff --git a/test/lib/gajim_mocks.py b/test/lib/gajim_mocks.py index 1d7cc6d76..6efcdc904 100644 --- a/test/lib/gajim_mocks.py +++ b/test/lib/gajim_mocks.py @@ -47,9 +47,6 @@ class MockConnection(Mock, ConnectionHandlers): app.connections[account] = self - def request_vcard(self, *args): - pass - class MockWindow(Mock): def __init__(self, *args): Mock.__init__(self, *args)