Philipp Hörist pushed to branch master at gajim / gajim
Commits: f903a0a4 by lovetox at 2020-09-24T09:43:26+02:00 Add task decoractor
- - - - - a0ed4fb7 by lovetox at 2020-09-24T09:43:26+02:00 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
- - - - - a0d8b200 by lovetox at 2020-09-24T09:43:26+02:00 Refactor UserAvatar module
- Adapt to nbxmpp changes
- - - - -
16 changed files:
- gajim/common/app.py - gajim/common/client.py - gajim/common/connection.py - gajim/common/const.py - gajim/common/helpers.py - gajim/common/modules/discovery.py - gajim/common/modules/presence.py - gajim/common/modules/user_avatar.py - gajim/common/modules/util.py - gajim/common/modules/vcard_avatars.py - gajim/common/modules/vcard_temp.py - gajim/dialog_messages.py - gajim/groupchat_control.py - gajim/gtk/profile.py - gajim/gtk/server_info.py - gajim/vcard.py
Changes:
===================================== gajim/common/app.py ===================================== @@ -36,6 +36,7 @@ import logging import uuid from pathlib import Path from collections import namedtuple +from collections import defaultdict
import nbxmpp from gi.repository import Gdk @@ -151,6 +152,7 @@ _dependencies = { 'IDLE': False, }
+_tasks = defaultdict(list)
def print_version(): log('gajim').info('Gajim Version: %s', gajim.__version__) @@ -661,3 +663,27 @@ def get_groupchat_control(account, jid): return app.interface.minimized_controls[account][jid] except Exception: return None + + +def register_task(self, task): + _tasks[id(self)].append(task) + + +def remove_task(task, id_): + try: + _tasks[id_].remove(task) + except Exception: + pass + else: + if not _tasks[id_]: + del _tasks[id_] + + +def cancel_tasks(obj): + id_ = id(obj) + if id_ not in _tasks: + return + + task_list = _tasks[id_] + for task in task_list: + task.cancel()
===================================== gajim/common/client.py ===================================== @@ -57,7 +57,6 @@ class Client(ConnectionHandlers):
self._priority = 0 self._connect_machine_calls = 0 - self.avatar_conversion = False self.addressing_supported = False
self.is_zeroconf = False @@ -451,10 +450,6 @@ class Client(ConnectionHandlers): # 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()
===================================== gajim/common/connection.py ===================================== @@ -71,7 +71,6 @@ class CommonConnection: self.pep = {}
self.roster_supported = True - self.avatar_conversion = False
self._stun_servers = [] # STUN servers of our jabber server
===================================== gajim/common/const.py ===================================== @@ -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
===================================== gajim/common/helpers.py ===================================== @@ -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):
===================================== gajim/common/modules/discovery.py ===================================== @@ -112,9 +112,7 @@ class Discovery(BaseModule): 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()
===================================== gajim/common/modules/presence.py ===================================== @@ -362,11 +362,6 @@ class Presence(BaseModule): 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')):
===================================== gajim/common/modules/user_avatar.py ===================================== @@ -15,18 +15,19 @@ # XEP-0084: User Avatar
from nbxmpp.namespaces import Namespace -from nbxmpp.util import is_error_result +from nbxmpp.modules.util import is_error
from gajim.common import app from gajim.common.modules.base import BaseModule from gajim.common.modules.util import event_node - +from gajim.common.modules.util import task
class UserAvatar(BaseModule):
_nbxmpp_extends = 'UserAvatar' _nbxmpp_methods = [ - 'request_avatar' + 'request_avatar', + 'set_avatar', ]
def __init__(self, con): @@ -38,12 +39,11 @@ class UserAvatar(BaseModule): if properties.pubsub_event.retracted: return
- data = properties.pubsub_event.data + metadata = properties.pubsub_event.data jid = str(properties.jid)
- if data is None: - # Remove avatar - self._log.info('Remove: %s', jid) + if metadata is None or not metadata.infos: + self._log.info('No avatar published: %s', jid) app.contacts.set_avatar(self._account, jid, None) self._con.get_module('Roster').set_avatar_sha(jid, None) app.interface.update_avatar(self._account, jid) @@ -54,32 +54,39 @@ class UserAvatar(BaseModule): else: sha = app.contacts.get_avatar_sha(self._account, jid)
- if sha == data.id: - self._log.info('Avatar already known: %s %s', jid, data.id) + if sha in metadata.avatar_shas: + self._log.info('Avatar already known: %s %s', jid, sha) return
- self._log.info('Request: %s %s', jid, data.id) - self._nbxmpp('UserAvatar').request_avatar( - jid, data.id, callback=self._avatar_received) + avatar_info = metadata.infos[0] + self._log.info('Request: %s %s', jid, avatar_info.id) + self._request_avatar(jid, avatar_info) + + @task + def _request_avatar(self, jid, avatar_info): + _task = yield + + avatar = yield self._nbxmpp('UserAvatar').request_avatar( + avatar_info, jid=jid)
- def _avatar_received(self, result): - if is_error_result(result): - self._log.info(result) + if is_error(avatar): + self._log.warning(avatar) return
- self._log.info('Received Avatar: %s %s', result.jid, result.sha) - app.interface.save_avatar(result.data) + self._log.info('Received Avatar: %s %s', jid, avatar.sha) + app.interface.save_avatar(avatar.data)
- if self._con.get_own_jid().bare_match(result.jid): - app.settings.set_account_setting(self._account, - 'avatar_sha', - result.sha) + if self._con.get_own_jid().bare_match(jid): + app.config.set_per('accounts', + self._account, + 'avatar_sha', + avatar.sha) else: - self._con.get_module('Roster').set_avatar_sha(str(result.jid), - result.sha) + self._con.get_module('Roster').set_avatar_sha( + str(jid), avatar.sha)
- app.contacts.set_avatar(self._account, str(result.jid), result.sha) - app.interface.update_avatar(self._account, str(result.jid)) + app.contacts.set_avatar(self._account, str(jid), avatar.sha) + app.interface.update_avatar(self._account, str(jid))
def get_instance(*args, **kwargs):
===================================== gajim/common/modules/util.py ===================================== @@ -20,6 +20,8 @@ from logging import LoggerAdapter from functools import wraps from functools import partial
+from nbxmpp.task import Task + from gajim.common import app from gajim.common.const import EME_MESSAGES
@@ -88,3 +90,17 @@ def get_eme_message(eme_data): class LogAdapter(LoggerAdapter): def process(self, msg, kwargs): return '(%s) %s' % (self.extra['account'], msg), kwargs + + +def task(func): + @wraps(func) + def func_wrapper(self, *args, callback=None, user_data=None, **kwargs): + task_ = Task(func(self, *args, **kwargs)) + app.register_task(self, task_) + task_.set_finalize_func(app.remove_task, id(self)) + task_.set_user_data(user_data) + if callback is not None: + task_.add_done_callback(callback) + task_.start() + return task_ + return func_wrapper
===================================== gajim/common/modules/vcard_avatars.py ===================================== @@ -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 @@ class VCardAvatars(BaseModule): 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 @@ class VCardAvatars(BaseModule): 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 @@ class VCardAvatars(BaseModule): 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 @@ class VCardAvatars(BaseModule): 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 @@ class VCardAvatars(BaseModule): 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 @@ class VCardAvatars(BaseModule): 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 @@ class VCardAvatars(BaseModule): 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 @@ class VCardAvatars(BaseModule): 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'
===================================== gajim/common/modules/vcard_temp.py ===================================== @@ -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 @@ class VCardTemp(BaseModule): 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'] - return name -
def get_instance(*args, **kwargs): return VCardTemp(*args, **kwargs), 'VCardTemp'
===================================== gajim/dialog_messages.py ===================================== @@ -133,10 +133,6 @@ messages = { 'for the chosen encryption.'), ErrorDialog),
- 'avatar-upload-error': Message( - _('Avatar Upload Failed'), - '%s', - ErrorDialog), }
===================================== gajim/groupchat_control.py ===================================== @@ -35,6 +35,7 @@ from nbxmpp.const import StatusCode from nbxmpp.const import Affiliation from nbxmpp.const import PresenceType from nbxmpp.util import is_error_result +from nbxmpp.modules.vcard_temp import VCard
from gi.repository import Gtk from gi.repository import Gdk @@ -43,7 +44,7 @@ from gi.repository import Gio
from gajim import gtkgui_helpers from gajim import gui_menu_builder -from gajim import vcard +from gajim.vcard import VcardWindow
from gajim.common import events from gajim.common import app @@ -65,7 +66,6 @@ from gajim.command_system.implementation.hosts import GroupChatCommands
from gajim.gtk.dialogs import DialogButton from gajim.gtk.dialogs import ConfirmationCheckDialog -from gajim.gtk.dialogs import ErrorDialog from gajim.gtk.dialogs import ConfirmationDialog from gajim.gtk.filechoosers import AvatarChooserDialog from gajim.gtk.groupchat_config import GroupchatConfig @@ -573,20 +573,32 @@ class GroupchatControl(ChatControlBase): data, sha = app.interface.avatar_storage.prepare_for_publish( filename) if sha is None: - ErrorDialog( - _('Could not load image'), - transient_for=self.parent_win.window) + self.add_info_message(_('Loading avatar failed')) return
avatar = base64.b64encode(data).decode('utf-8') + vcard = VCard() + vcard.set_avatar(avatar, 'image/png') + con = app.connections[self.account] - con.get_module('VCardTemp').upload_room_avatar( - self.room_jid, avatar) + con.get_module('VCardTemp').set_vcard( + vcard, + jid=self.room_jid, + callback=self._on_upload_avatar_result)
AvatarChooserDialog(_on_accept, transient_for=self.parent_win.window, modal=True)
+ def _on_upload_avatar_result(self, task): + try: + task.finish() + except Exception as error: + self.add_info_message(_(f'Avatar upload failed: {error}')) + + else: + self.add_info_message(_('Avatar upload successful')) + def _on_contact_information(self, _action, param): nick = param.get_string() gc_contact = app.contacts.get_gc_contact(self.account, @@ -598,7 +610,7 @@ class GroupchatControl(ChatControlBase): window.present() else: app.interface.instances[self.account]['infos'][contact.jid] = \ - vcard.VcardWindow(contact, self.account, gc_contact) + VcardWindow(contact, self.account, gc_contact)
def _on_kick(self, _action, param): nick = param.get_string()
===================================== gajim/gtk/profile.py ===================================== @@ -16,37 +16,35 @@ # You should have received a copy of the GNU General Public License # along with Gajim. If not, see http://www.gnu.org/licenses/.
-import base64 import time import logging -import hashlib
from gi.repository import Gtk from gi.repository import Gdk from gi.repository import GLib
+from nbxmpp.errors import is_error +from nbxmpp.modules.vcard_temp import VCard + from gajim.common import app -from gajim.common import ged from gajim.common.i18n import _ from gajim.common.const import AvatarSize -from gajim.common.helpers import event_filter +from gajim.common.modules.util import task
from gajim import gtkgui_helpers
from gajim.gtk.dialogs import ErrorDialog from gajim.gtk.dialogs import InformationDialog from gajim.gtk.util import get_builder -from gajim.gtk.util import EventHelper from gajim.gtk.filechoosers import AvatarChooserDialog
log = logging.getLogger('gajim.profile')
-class ProfileWindow(Gtk.ApplicationWindow, EventHelper): +class ProfileWindow(Gtk.ApplicationWindow): def __init__(self, account): Gtk.ApplicationWindow.__init__(self) - EventHelper.__init__(self) self.set_application(app.app) self.set_position(Gtk.WindowPosition.CENTER) self.set_show_menubar(False) @@ -69,8 +67,7 @@ class ProfileWindow(Gtk.ApplicationWindow, EventHelper):
self.dialog = None self.avatar_mime_type = None - self.avatar_encoded = None - self.avatar_sha = None + self._avatar_bytes = None self.message_id = self.statusbar.push(self.context_id, _('Retrieving profile…')) self.update_progressbar_timeout_id = GLib.timeout_add( @@ -78,15 +75,10 @@ class ProfileWindow(Gtk.ApplicationWindow, EventHelper): self.remove_statusbar_timeout_id = None
self.xml.connect_signals(self) - self.register_events([ - ('vcard-published', ged.GUI1, self._nec_vcard_published), - ('vcard-not-published', ged.GUI1, self._nec_vcard_not_published), - ])
self.show_all() self.xml.get_object('ok_button').grab_focus() - app.connections[account].get_module('VCardTemp').request_vcard( - self._nec_vcard_received, self.jid) + self._request_vcard()
def on_information_notebook_switch_page(self, widget, page, page_num): GLib.idle_add(self.xml.get_object('ok_button').grab_focus) @@ -100,6 +92,7 @@ class ProfileWindow(Gtk.ApplicationWindow, EventHelper): self.remove_statusbar_timeout_id = None
def on_profile_window_destroy(self, widget): + app.cancel_tasks(self) if self.update_progressbar_timeout_id is not None: GLib.source_remove(self.update_progressbar_timeout_id) if self.remove_statusbar_timeout_id is not None: @@ -120,8 +113,7 @@ class ProfileWindow(Gtk.ApplicationWindow, EventHelper): button.hide() text_button = self.xml.get_object('NOPHOTO_button') text_button.show() - self.avatar_encoded = None - self.avatar_sha = None + self._avatar_bytes = None self.avatar_mime_type = None
def _on_set_avatar_clicked(self, _button): @@ -144,8 +136,7 @@ class ProfileWindow(Gtk.ApplicationWindow, EventHelper): text_button = self.xml.get_object('NOPHOTO_button') text_button.hide()
- self.avatar_sha = sha - self.avatar_encoded = base64.b64encode(data).decode('utf-8') + self._avatar_bytes = data self.avatar_mime_type = 'image/png'
AvatarChooserDialog(on_ok, transient_for=self) @@ -176,37 +167,43 @@ class ProfileWindow(Gtk.ApplicationWindow, EventHelper): except AttributeError: pass
- def set_values(self, vcard_): + def _set_avatar(self, vcard): + avatar, _ = vcard.get_avatar() + button = self.xml.get_object('PHOTO_button') image = self.xml.get_object('PHOTO_image') text_button = self.xml.get_object('NOPHOTO_button') - if 'PHOTO' not in vcard_: - # set default image + + if avatar is None: + image.set_from_pixbuf(None) + button.hide() + text_button.show() + return + + try: + surface = self._get_surface_from_data(avatar) + except Exception: + log.exception('Error while loading avatar') image.set_from_pixbuf(None) button.hide() text_button.show() + return + + self._avatar_bytes = avatar + self.avatar_mime_type = vcard.data['PHOTO']['TYPE'] + + image.set_from_surface(surface) + button.show() + text_button.hide() + + def _get_surface_from_data(self, data): + scale = self.get_scale_factor() + pixbuf = gtkgui_helpers.scale_pixbuf_from_data(data, AvatarSize.VCARD) + return Gdk.cairo_surface_create_from_pixbuf(pixbuf, scale) + + def set_values(self, vcard_): for i in vcard_.keys(): if i == 'PHOTO': - photo_encoded = vcard_[i]['BINVAL'] - if photo_encoded == '': - continue - self.avatar_encoded = photo_encoded - photo_decoded = base64.b64decode(photo_encoded.encode('utf-8')) - self.avatar_sha = hashlib.sha1(photo_decoded).hexdigest() - if 'TYPE' in vcard_[i]: - self.avatar_mime_type = vcard_[i]['TYPE'] - - scale = self.get_scale_factor() - surface = app.interface.avatar_storage.surface_from_filename( - self.avatar_sha, AvatarSize.VCARD, scale) - if surface is None: - pixbuf = gtkgui_helpers.scale_pixbuf_from_data( - photo_decoded, AvatarSize.VCARD) - surface = Gdk.cairo_surface_create_from_pixbuf(pixbuf, - scale) - image.set_from_surface(surface) - button.show() - text_button.hide() continue if i in ('ADR', 'TEL', 'EMAIL'): for entry in vcard_[i]: @@ -236,9 +233,6 @@ class ProfileWindow(Gtk.ApplicationWindow, EventHelper): self.progressbar.set_fraction(0) self.update_progressbar_timeout_id = None
- def _nec_vcard_received(self, jid, resource, room, vcard_, *args): - self.set_values(vcard_) - def add_to_vcard(self, vcard_, entry, txt): """ Add an information to the vCard dictionary @@ -291,11 +285,10 @@ class ProfileWindow(Gtk.ApplicationWindow, EventHelper): vcard_['DESC'] = txt
# Avatar - if self.avatar_encoded: - vcard_['PHOTO'] = {'BINVAL': self.avatar_encoded} - if self.avatar_mime_type: - vcard_['PHOTO']['TYPE'] = self.avatar_mime_type - return vcard_, self.avatar_sha + vcard = VCard(vcard_) + if self._avatar_bytes: + vcard.set_avatar(self._avatar_bytes, self.avatar_mime_type) + return vcard
def on_ok_button_clicked(self, widget): if self.update_progressbar_timeout_id: @@ -308,43 +301,69 @@ class ProfileWindow(Gtk.ApplicationWindow, EventHelper): 'information.'), transient_for=self) return - vcard_, sha = self.make_vcard() - nick = vcard_.get('NICKNAME') or None - app.connections[self.account].get_module('UserNickname').set_nickname(nick) - if not nick: - nick = app.settings.get_account_setting(self.account, 'name') - app.nicks[self.account] = nick - app.connections[self.account].get_module('VCardTemp').send_vcard( - vcard_, sha) + + vcard_ = self.make_vcard() + + client = app.get_client(self.account) + client.get_module('VCardTemp').set_vcard( + vcard_, callback=self._on_set_vcard_result) + self._set_nickname(vcard_) + self.message_id = self.statusbar.push( self.context_id, _('Sending profile…')) self.progressbar.show() self.update_progressbar_timeout_id = GLib.timeout_add( 100, self.update_progressbar)
- @event_filter(['account']) - def _nec_vcard_published(self, _event): - if self.update_progressbar_timeout_id is not None: - GLib.source_remove(self.update_progressbar_timeout_id) - self.update_progressbar_timeout_id = None - self.destroy() + @task + def _request_vcard(self): + _task = yield
- @event_filter(['account']) - def _nec_vcard_not_published(self, _event): - if self.message_id: - self.statusbar.remove(self.context_id, self.message_id) - self.message_id = self.statusbar.push( - self.context_id, _('Information NOT published')) - self.remove_statusbar_timeout_id = GLib.timeout_add_seconds( - 3, self.remove_statusbar, self.message_id) - if self.update_progressbar_timeout_id is not None: - GLib.source_remove(self.update_progressbar_timeout_id) - self.progressbar.set_fraction(0) - self.update_progressbar_timeout_id = None - InformationDialog( - _('vCard publication failed'), - _('There was an error while publishing your personal information, ' - 'try again later.'), transient_for=self) + client = app.get_client(self.account) + vcard = yield client.get_module('VCardTemp').request_vcard() + + if is_error(vcard): + return + + self._set_avatar(vcard) + self.set_values(vcard.data) + + def _on_set_vcard_result(self, task_): + try: + task_.finish() + except Exception as error: + log.warning(error) + + if self.message_id: + self.statusbar.remove(self.context_id, self.message_id) + self.message_id = self.statusbar.push( + self.context_id, _('Information NOT published')) + self.remove_statusbar_timeout_id = GLib.timeout_add_seconds( + 3, self.remove_statusbar, self.message_id) + if self.update_progressbar_timeout_id is not None: + GLib.source_remove(self.update_progressbar_timeout_id) + self.progressbar.set_fraction(0) + self.update_progressbar_timeout_id = None + InformationDialog( + _('vCard publication failed'), + _('There was an error while publishing your personal information, ' + 'try again later.'), transient_for=self) + + else: + if self.update_progressbar_timeout_id is not None: + GLib.source_remove(self.update_progressbar_timeout_id) + self.update_progressbar_timeout_id = None + self.destroy() + + def _set_nickname(self, vcard): + nick = vcard.data.get('NICKNAME') or None + + client = app.get_client(self.account) + client.get_module('UserNickname').set_nickname(nick) + + if not nick: + nick = app.config.get_per('accounts', self.account, 'name') + app.nicks[self.account] = nick
def on_cancel_button_clicked(self, widget): self.destroy()
===================================== gajim/gtk/server_info.py ===================================== @@ -299,7 +299,7 @@ class ServerInfo(Gtk.ApplicationWindow, EventHelper): con.get_module('HTTPUpload').available, http_upload_info), Feature('XEP-0398: Avatar Conversion', - con.avatar_conversion), + con.get_module('VCardAvatars').avatar_conversion_available), Feature('XEP-0411: Bookmarks Conversion', con.get_module('Bookmarks').conversion) ]
===================================== gajim/vcard.py ===================================== @@ -26,8 +26,7 @@ # along with Gajim. If not, see http://www.gnu.org/licenses/.
import os -import base64 -import binascii +import logging
from gi.repository import Gtk from gi.repository import GLib @@ -35,6 +34,7 @@ from gi.repository import Gdk from nbxmpp.structs import AnnotationNote from nbxmpp.util import is_error_result from nbxmpp.protocol import JID +from nbxmpp.errors import is_error
from gajim import gtkgui_helpers from gajim.common import helpers @@ -45,10 +45,12 @@ from gajim.common.i18n import Q_ from gajim.common.i18n import _ from gajim.common.const import AvatarSize from gajim.common.nec import EventHelper +from gajim.common.modules.util import task
from gajim.gtk.util import get_builder
-# log = logging.getLogger('gajim.vcard') + +log = logging.getLogger('gajim.vcard')
class VcardWindow(EventHelper): @@ -151,6 +153,7 @@ class VcardWindow(EventHelper): new_note = AnnotationNote(jid=self.contact.jid, data=new_annotation) con.get_module('Annotations').set_note(new_note) self.unregister_events() + app.cancel_tasks(self)
def on_vcard_information_window_key_press_event(self, widget, event): if event.keyval == Gdk.KEY_Escape: @@ -181,31 +184,7 @@ class VcardWindow(EventHelper):
def _set_values(self, vcard, jid): for i in vcard.keys(): - if i == 'PHOTO' and self.xml.get_object('information_notebook').\ - get_n_pages() > 4: - if 'BINVAL' not in vcard[i]: - continue - photo_encoded = vcard[i]['BINVAL'] - if photo_encoded == '': - continue - try: - photo_decoded = base64.b64decode( - photo_encoded.encode('utf-8')) - except binascii.Error as error: - app.log('avatar').warning('Invalid avatar for %s: %s', jid, error) - continue - - pixbuf = gtkgui_helpers.get_pixbuf_from_data(photo_decoded) - if pixbuf is None: - continue - self.avatar = pixbuf - pixbuf = gtkgui_helpers.scale_pixbuf(pixbuf, AvatarSize.VCARD) - surface = Gdk.cairo_surface_create_from_pixbuf( - pixbuf, self.window.get_scale_factor()) - image = self.xml.get_object('PHOTO_image') - image.set_from_surface(surface) - image.show() - self.xml.get_object('user_avatar_label').show() + if i == 'PHOTO': continue if i in ('ADR', 'TEL', 'EMAIL'): for entry in vcard[i]: @@ -225,6 +204,32 @@ class VcardWindow(EventHelper): self.set_value(i + '_label', vcard[i]) self.vcard_arrived = True
+ def _load_avatar(self, vcard): + if not self.xml.get_object('information_notebook').get_n_pages() > 4: + # TODO: why? + return + + try: + avatar, _ = vcard.get_avatar() + except Exception as error: + log.warning('Failed to load avatar: %s', error) + return + + if avatar is None: + return + + pixbuf = gtkgui_helpers.get_pixbuf_from_data(avatar) + pixbuf = gtkgui_helpers.scale_pixbuf(pixbuf, AvatarSize.VCARD) + + surface = Gdk.cairo_surface_create_from_pixbuf( + pixbuf, self.window.get_scale_factor()) + image = self.xml.get_object('PHOTO_image') + image.set_from_surface(surface) + image.show() + + self.avatar = pixbuf + self.xml.get_object('user_avatar_label').show() + def clear_values(self): for l in ('FN', 'NICKNAME', 'N_FAMILY', 'N_GIVEN', 'N_MIDDLE', 'N_PREFIX', 'N_SUFFIX', 'EMAIL_HOME_USERID', 'TEL_HOME_NUMBER', 'BDAY', @@ -242,9 +247,19 @@ class VcardWindow(EventHelper): widget.set_text('') self.xml.get_object('DESC_textview').get_buffer().set_text('')
- def _nec_vcard_received(self, jid, resource, room, vcard, *args): + @task + def _request_vcard(self, jid): + _task = yield + + con = app.connections[self.account] + vcard = yield con.get_module('VCardTemp').request_vcard(jid=jid) + + if is_error(vcard): + return + self.clear_values() - self._set_values(vcard, jid) + self._set_values(vcard.data, str(jid)) + self._load_avatar(vcard)
def set_os_info(self, result, jid): if self.xml.get_object('information_notebook').get_n_pages() < 5: @@ -448,14 +463,11 @@ class VcardWindow(EventHelper):
self.fill_status_label()
+ jid = self.contact.jid if self.gc_contact: - con.get_module('VCardTemp').request_vcard( - self._nec_vcard_received, - self.gc_contact.get_full_jid(), - room=True) - else: - con.get_module('VCardTemp').request_vcard( - self._nec_vcard_received, self.contact.jid) + jid = self.gc_contact.get_full_jid() + + self._request_vcard(jid)
def on_close_button_clicked(self, widget): self.window.destroy()
View it on GitLab: https://dev.gajim.org/gajim/gajim/-/compare/84137fcdd10260ff905bc87efdd9a5b9...