diff --git a/gajim/chat_control.py b/gajim/chat_control.py
index 7d3d3ab88..31bed9b40 100644
--- a/gajim/chat_control.py
+++ b/gajim/chat_control.py
@@ -55,6 +55,7 @@ from nbxmpp.protocol import NS_JINGLE_ICE_UDP, NS_JINGLE_FILE_TRANSFER_5
from nbxmpp.protocol import NS_CHATSTATES
from gajim.common.connection_handlers_events import MessageOutgoingEvent
from gajim.common.exceptions import GajimGeneralException
+from gajim.common.const import AvatarSize
from gajim.command_system.implementation.hosts import ChatCommands
@@ -197,8 +198,7 @@ class ChatControl(ChatControlBase):
self.handlers[id_] = message_tv_buffer
widget = self.xml.get_object('avatar_eventbox')
- widget.set_property('height-request', app.config.get(
- 'chat_avatar_height'))
+ widget.set_property('height-request', AvatarSize.CHAT)
id_ = widget.connect('enter-notify-event',
self.on_avatar_eventbox_enter_notify_event)
self.handlers[id_] = widget
@@ -296,8 +296,10 @@ class ChatControl(ChatControlBase):
app.ged.register_event_handler('pep-received', ged.GUI1,
self._nec_pep_received)
- app.ged.register_event_handler('vcard-received', ged.GUI1,
- self._nec_vcard_received)
+ if self.TYPE_ID == message_control.TYPE_CHAT:
+ # Dont connect this when PrivateChatControl is used
+ app.ged.register_event_handler('update-roster-avatar', ged.GUI1,
+ self._nec_update_avatar)
app.ged.register_event_handler('failed-decrypt', ged.GUI1,
self._nec_failed_decrypt)
app.ged.register_event_handler('chatstate-received', ged.GUI1,
@@ -579,9 +581,8 @@ class ChatControl(ChatControlBase):
Enter the eventbox area so we under conditions add a timeout to show a
bigger avatar after 0.5 sec
"""
- jid = self.contact.jid
- avatar_pixbuf = gtkgui_helpers.get_avatar_pixbuf_from_cache(jid)
- if avatar_pixbuf in ('ask', None):
+ avatar_pixbuf = app.interface.get_avatar(self.account, self.contact.jid)
+ if avatar_pixbuf is None:
return
avatar_w = avatar_pixbuf.get_width()
avatar_h = avatar_pixbuf.get_height()
@@ -596,7 +597,7 @@ class ChatControl(ChatControlBase):
if self.show_bigger_avatar_timeout_id is not None:
GLib.source_remove(self.show_bigger_avatar_timeout_id)
self.show_bigger_avatar_timeout_id = GLib.timeout_add(500,
- self.show_bigger_avatar, widget)
+ self.show_bigger_avatar, widget, avatar_pixbuf)
def on_avatar_eventbox_leave_notify_event(self, widget, event):
"""
@@ -614,9 +615,15 @@ class ChatControl(ChatControlBase):
if event.button == 3: # right click
menu = Gtk.Menu()
menuitem = Gtk.MenuItem.new_with_mnemonic(_('Save _As'))
+ if self.TYPE_ID == message_control.TYPE_CHAT:
+ sha = app.contacts.get_avatar_sha(
+ self.account, self.contact.jid)
+ name = self.contact.get_shown_name()
+ else:
+ sha = self.gc_contact.avatar_sha
+ name = self.gc_contact.get_shown_name()
id_ = menuitem.connect('activate',
- gtkgui_helpers.on_avatar_save_as_menuitem_activate,
- self.contact.jid, self.contact.get_shown_name())
+ gtkgui_helpers.on_avatar_save_as_menuitem_activate, sha, name)
self.handlers[id_] = menuitem
menu.append(menuitem)
menu.show_all()
@@ -1076,10 +1083,8 @@ class ChatControl(ChatControlBase):
jid = self.contact.jid
if app.config.get('show_avatar_in_tabs'):
- avatar_pixbuf = gtkgui_helpers.get_avatar_pixbuf_from_cache(jid)
- if avatar_pixbuf not in ('ask', None):
- avatar_pixbuf = gtkgui_helpers.get_scaled_pixbuf_by_size(
- avatar_pixbuf, 16, 16)
+ avatar_pixbuf = app.contacts.get_avatar(self.account, jid, size=16)
+ if avatar_pixbuf is not None:
return avatar_pixbuf
if count_unread:
@@ -1200,8 +1205,9 @@ class ChatControl(ChatControlBase):
app.ged.remove_event_handler('pep-received', ged.GUI1,
self._nec_pep_received)
- app.ged.remove_event_handler('vcard-received', ged.GUI1,
- self._nec_vcard_received)
+ if self.TYPE_ID == message_control.TYPE_CHAT:
+ app.ged.remove_event_handler('update-roster-avatar', ged.GUI1,
+ self._nec_update_avatar)
app.ged.remove_event_handler('failed-decrypt', ged.GUI1,
self._nec_failed_decrypt)
app.ged.remove_event_handler('chatstate-received', ged.GUI1,
@@ -1322,37 +1328,15 @@ class ChatControl(ChatControlBase):
if not app.config.get('show_avatar_in_chat'):
return
- jid_with_resource = self.contact.get_full_jid()
- pixbuf = gtkgui_helpers.get_avatar_pixbuf_from_cache(jid_with_resource)
- if pixbuf == 'ask':
- # we don't have the vcard
- if self.TYPE_ID == message_control.TYPE_PM:
- if self.gc_contact.jid:
- # We know the real jid of this contact
- real_jid = self.gc_contact.jid
- if self.gc_contact.resource:
- real_jid += '/' + self.gc_contact.resource
- else:
- real_jid = jid_with_resource
- app.connections[self.account].request_vcard(real_jid,
- jid_with_resource)
- else:
- app.connections[self.account].request_vcard(jid_with_resource)
- return
- elif pixbuf:
- scaled_pixbuf = gtkgui_helpers.get_scaled_pixbuf(pixbuf, 'chat')
- else:
- scaled_pixbuf = None
-
+ pixbuf = app.contacts.get_avatar(
+ self.account, self.contact.jid, AvatarSize.CHAT)
image = self.xml.get_object('avatar_image')
- image.set_from_pixbuf(scaled_pixbuf)
- image.show_all()
+ image.set_from_pixbuf(pixbuf)
- def _nec_vcard_received(self, obj):
- if obj.conn.name != self.account:
+ def _nec_update_avatar(self, obj):
+ if obj.account != self.account:
return
- j = app.get_jid_without_resource(self.contact.jid)
- if obj.jid != j:
+ if obj.jid != self.contact.jid:
return
self.show_avatar()
@@ -1518,28 +1502,14 @@ class ChatControl(ChatControlBase):
elif typ == 'pm':
control.remove_contact(nick)
- def show_bigger_avatar(self, small_avatar):
+ def show_bigger_avatar(self, small_avatar, avatar_pixbuf):
"""
Resize the avatar, if needed, so it has at max half the screen size and
shows it
"""
- #if not small_avatar.window:
- ### Tab has been closed since we hovered the avatar
- #return
- avatar_pixbuf = gtkgui_helpers.get_avatar_pixbuf_from_cache(
- self.contact.jid)
- if avatar_pixbuf in ('ask', None):
- return
# Hide the small avatar
- # this code hides the small avatar when we show a bigger one in case
- # the avatar has a transparency hole in the middle
- # so when we show the big one we avoid seeing the small one behind.
- # It's why I set it transparent.
image = self.xml.get_object('avatar_image')
- pixbuf = image.get_pixbuf()
- pixbuf.fill(0xffffff00) # RGBA
- image.set_from_pixbuf(pixbuf)
- #image.queue_draw()
+ image.hide()
screen_w = Gdk.Screen.width()
screen_h = Gdk.Screen.height()
diff --git a/gajim/common/app.py b/gajim/common/app.py
index 1751f452e..3b212ed5d 100644
--- a/gajim/common/app.py
+++ b/gajim/common/app.py
@@ -34,8 +34,11 @@ import uuid
from distutils.version import LooseVersion as V
import gi
import nbxmpp
+import hashlib
-from gajim.common import config
+from gi.repository import GLib
+
+from gajim.common import config as c_config
from gajim.common import configpaths
from gajim.common import ged as ged_module
from gajim.common.contacts import LegacyContactsAPI
@@ -43,9 +46,10 @@ from gajim.common.events import Events
interface = None # The actual interface (the gtk one for the moment)
thread_interface = lambda *args: None # Interface to run a thread and then a callback
-config = config.Config()
+config = c_config.Config()
version = config.get('version')
connections = {} # 'account name': 'account (connection.Connection) instance'
+avatar_cache = {}
ipython_window = None
app = None # Gtk.Application
@@ -260,7 +264,7 @@ gajim_common_features = [nbxmpp.NS_BYTESTREAM, nbxmpp.NS_SI, nbxmpp.NS_FILE,
nbxmpp.NS_ROSTERX, nbxmpp.NS_SECLABEL, nbxmpp.NS_HASHES_2,
nbxmpp.NS_HASHES_MD5, nbxmpp.NS_HASHES_SHA1, nbxmpp.NS_HASHES_SHA256,
nbxmpp.NS_HASHES_SHA512, nbxmpp.NS_CONFERENCE, nbxmpp.NS_CORRECT,
- nbxmpp.NS_EME]
+ nbxmpp.NS_EME, 'urn:xmpp:avatar:metadata+notify']
# Optional features gajim supports per account
gajim_optional_features = {}
diff --git a/gajim/common/config.py b/gajim/common/config.py
index 40f0e2a3d..4da11fe47 100644
--- a/gajim/common/config.py
+++ b/gajim/common/config.py
@@ -220,12 +220,6 @@ class Config:
'tabs_border': [opt_bool, False, _('Show tabbed notebook border in chat windows?')],
'tabs_close_button': [opt_bool, True, _('Show close button in tab?')],
'esession_modp': [opt_str, '15,16,14', _('A list of modp groups to use in a Diffie-Hellman, highest preference first, separated by commas. Valid groups are 1, 2, 5, 14, 15, 16, 17 and 18. Higher numbers are more secure, but take longer to calculate when you start a session.')],
- 'chat_avatar_width': [opt_int, 52],
- 'chat_avatar_height': [opt_int, 52],
- 'roster_avatar_width': [opt_int, 32],
- 'roster_avatar_height': [opt_int, 32],
- 'tooltip_avatar_width': [opt_int, 125],
- 'tooltip_avatar_height': [opt_int, 125],
'tooltip_status_online_color': [opt_color, '#73D216'],
'tooltip_status_free_for_chat_color': [opt_color, '#3465A4'],
'tooltip_status_away_color': [opt_color, '#EDD400'],
@@ -238,13 +232,9 @@ class Config:
'tooltip_affiliation_owner_color': [opt_color, '#CC0000'],
'tooltip_account_name_color': [opt_color, '#888A85'],
'tooltip_idle_color': [opt_color, '#888A85'],
- 'vcard_avatar_width': [opt_int, 200],
- 'vcard_avatar_height': [opt_int, 200],
'notification_preview_message': [opt_bool, True, _('Preview new messages in notification popup?')],
'notification_position_x': [opt_int, -1],
'notification_position_y': [opt_int, -1],
- 'notification_avatar_width': [opt_int, 48],
- 'notification_avatar_height': [opt_int, 48],
'muc_highlight_words': [opt_str, '', _('A semicolon-separated list of words that will be highlighted in group chats.')],
'quit_on_roster_x_button': [opt_bool, False, _('If True, quits Gajim when X button of Window Manager is clicked. This setting is taken into account only if notification icon is used.')],
'show_unread_tab_icon': [opt_bool, False, _('If True, Gajim will display an icon on each tab containing unread messages. Depending on the theme, this icon may be animated.')],
@@ -255,7 +245,6 @@ class Config:
'show_tunes_in_roster': [opt_bool, True, '', True],
'show_location_in_roster': [opt_bool, True, '', True],
'avatar_position_in_roster': [opt_str, 'right', _('Define the position of the avatar in roster. Can be left or right'), True],
- 'ask_avatars_on_startup': [opt_bool, True, _('If True, Gajim will ask for avatar each contact that did not have an avatar last time or has one cached that is too old.')],
'print_status_in_chats': [opt_bool, False, _('If False, Gajim will no longer print status line in chats when a contact changes his or her status and/or his or her status message.')],
'print_status_in_muc': [opt_str, 'none', _('Can be "none", "all" or "in_and_out". If "none", Gajim will no longer print status line in groupchats when a member changes his or her status and/or his or her status message. If "all" Gajim will print all status messages. If "in_and_out", Gajim will only print FOO enters/leaves group chat.')],
'log_contact_status_changes': [opt_bool, False],
@@ -325,6 +314,7 @@ class Config:
'account_label': [ opt_str, '', '', False ],
'hostname': [ opt_str, '', '', True ],
'anonymous_auth': [ opt_bool, False ],
+ 'avatar_sha': [opt_str, '', '', False],
'client_cert': [ opt_str, '', '', True ],
'client_cert_encrypted': [ opt_bool, False, '', False ],
'savepass': [ opt_bool, False ],
diff --git a/gajim/common/connection.py b/gajim/common/connection.py
index 67cd0c8fc..4c8d4b460 100644
--- a/gajim/common/connection.py
+++ b/gajim/common/connection.py
@@ -779,6 +779,7 @@ class Connection(CommonConnection, ConnectionHandlers):
self.connected = 0
self.time_to_reconnect = None
self.privacy_rules_supported = False
+ self.avatar_presence_sent = False
if on_purpose:
self.sm = Smacks(self)
if self.connection:
@@ -1778,7 +1779,7 @@ class Connection(CommonConnection, ConnectionHandlers):
show='invisible'))
if initial:
# ask our VCard
- self.request_vcard(None)
+ self.request_vcard(self._on_own_avatar_received)
# Get bookmarks from private namespace
self.get_bookmarks()
diff --git a/gajim/common/connection_handlers.py b/gajim/common/connection_handlers.py
index 09ae05b87..d0dec13d9 100644
--- a/gajim/common/connection_handlers.py
+++ b/gajim/common/connection_handlers.py
@@ -44,10 +44,8 @@ from gajim.common import caps_cache as capscache
from gajim.common.pep import LOCATION_DATA
from gajim.common import helpers
from gajim.common import app
-from gajim.common import exceptions
from gajim.common import dataforms
from gajim.common import jingle_xtls
-from gajim.common import sleepy
from gajim.common.commands import ConnectionCommands
from gajim.common.pubsub import ConnectionPubSub
from gajim.common.protocol.caps import ConnectionCaps
@@ -66,8 +64,6 @@ import logging
log = logging.getLogger('gajim.c.connection_handlers')
# kind of events we can wait for an answer
-VCARD_PUBLISHED = 'vcard_published'
-VCARD_ARRIVED = 'vcard_arrived'
AGENT_REMOVED = 'agent_removed'
METACONTACTS_ARRIVED = 'metacontacts_arrived'
ROSTER_ARRIVED = 'roster_arrived'
@@ -262,15 +258,96 @@ class ConnectionDisco:
class ConnectionVcard:
def __init__(self):
- self.vcard_sha = None
- self.vcard_shas = {} # sha of contacts
- # list of gc jids so that vcard are saved in a folder
+ self.own_vcard = None
self.room_jids = []
+ self.avatar_presence_sent = False
+
+ 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)
+
+ def _vcard_presence_received(self, obj):
+ if obj.avatar_sha is None:
+ # No Avatar is advertised
+ return
+
+ 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)
+ 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').info('Remove: %s', obj.jid)
+ app.contacts.set_avatar(self.name, obj.jid, None)
+ app.logger.set_avatar_sha(self.name, obj.jid, None)
+ app.interface.update_avatar(self.name, obj.jid)
+ 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(
+ 'Request (vCard): %s', obj.jid)
+ 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
+
+ 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').info('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(app.AVATAR_PATH, obj.avatar_sha)
+ if not os.path.isfile(path):
+ app.log('avatar').info(
+ 'Request (vCard): %s', obj.nick)
+ 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)
def add_sha(self, p, send_caps=True):
c = p.setTag('x', namespace=nbxmpp.NS_VCARD_UPDATE)
- if self.vcard_sha is not None:
- c.setTagData('photo', self.vcard_sha)
+ 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)
if send_caps:
return self._add_caps(p)
return p
@@ -283,6 +360,14 @@ class ConnectionVcard:
c.setAttr('ver', app.caps_hash[self.name])
return p
+ 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():
@@ -301,99 +386,27 @@ class ConnectionVcard:
dict_[name][c.getName()] = c.getData()
return dict_
- def _save_vcard_to_hd(self, full_jid, card):
- jid, nick = app.get_room_and_nick_from_fjid(full_jid)
- puny_jid = helpers.sanitize_filename(jid)
- path = os.path.join(app.VCARD_PATH, puny_jid)
- if jid in self.room_jids or os.path.isdir(path):
- if not nick:
- return
- # remove room_jid file if needed
- if os.path.isfile(path):
- os.remove(path)
- # create folder if needed
- if not os.path.isdir(path):
- os.mkdir(path, 0o700)
- puny_nick = helpers.sanitize_filename(nick)
- path_to_file = os.path.join(app.VCARD_PATH, puny_jid, puny_nick)
- else:
- path_to_file = path
- try:
- fil = open(path_to_file, 'w', encoding='utf-8')
- fil.write(str(card))
- fil.close()
- except IOError as e:
- app.nec.push_incoming_event(InformationEvent(None, conn=self,
- level='error', pri_txt=_('Disk Write Error'), sec_txt=str(e)))
-
- def get_cached_vcard(self, fjid, is_fake_jid=False):
- """
- Return the vcard as a dict.
- Return {} if vcard was too old.
- Return None if we don't have cached vcard.
- """
- jid, nick = app.get_room_and_nick_from_fjid(fjid)
- puny_jid = helpers.sanitize_filename(jid)
- if is_fake_jid:
- puny_nick = helpers.sanitize_filename(nick)
- path_to_file = os.path.join(app.VCARD_PATH, puny_jid, puny_nick)
- else:
- path_to_file = os.path.join(app.VCARD_PATH, puny_jid)
- if not os.path.isfile(path_to_file):
- return None
- # We have the vcard cached
- f = open(path_to_file, encoding='utf-8')
- c = f.read()
- f.close()
- try:
- card = nbxmpp.Node(node=c)
- except Exception:
- # We are unable to parse it. Remove it
- os.remove(path_to_file)
- return None
- vcard = self._node_to_dict(card)
- if 'PHOTO' in vcard:
- if not isinstance(vcard['PHOTO'], dict):
- del vcard['PHOTO']
- elif 'SHA' in vcard['PHOTO']:
- cached_sha = vcard['PHOTO']['SHA']
- if jid in self.vcard_shas and self.vcard_shas[jid] != \
- cached_sha:
- # user change his vcard so don't use the cached one
- return {}
- vcard['jid'] = jid
- vcard['resource'] = app.get_resource_from_jid(fjid)
- return vcard
-
- def request_vcard(self, jid=None, groupchat_jid=None):
+ def request_vcard(self, callback, jid=None, room=False):
"""
Request the VCARD
-
- If groupchat_jid is not null, it means we request a vcard to a fake jid,
- like in private messages in groupchat. jid can be the real jid of the
- contact, but we want to consider it comes from a fake jid
"""
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)
- id_ = self.connection.getAnID()
- iq.setID(id_)
- j = jid
- if not j:
- j = app.get_jid_from_account(self.name)
- self.awaiting_answers[id_] = (VCARD_ARRIVED, j, groupchat_jid)
- if groupchat_jid:
- room_jid = app.get_room_and_nick_from_fjid(groupchat_jid)[0]
- if not room_jid in self.room_jids:
- self.room_jids.append(room_jid)
- self.groupchat_jids[id_] = groupchat_jid
- self.connection.send(iq)
+ self.connection.SendAndCallForResponse(
+ iq, self._parse_vcard, {'callback': callback})
- def send_vcard(self, vcard):
+ def send_vcard(self, vcard, sha):
if not self.connection or self.connected < 2:
return
iq = nbxmpp.Iq(typ='set')
@@ -413,23 +426,29 @@ class ConnectionVcard:
else:
iq2.addChild(i).setData(vcard[i])
- id_ = self.connection.getAnID()
- iq.setID(id_)
- self.connection.send(iq)
+ self.connection.SendAndCallForResponse(
+ iq, self._avatar_publish_result, {'sha': sha})
- our_jid = app.get_jid_from_account(self.name)
- # Add the sha of the avatar
- if 'PHOTO' in vcard and isinstance(vcard['PHOTO'], dict) and \
- 'BINVAL' in vcard['PHOTO']:
- photo = vcard['PHOTO']['BINVAL']
- photo_decoded = base64.b64decode(photo.encode('utf-8'))
- app.interface.save_avatar_files(our_jid, photo_decoded)
- avatar_sha = hashlib.sha1(photo_decoded).hexdigest()
- iq2.getTag('PHOTO').setTagData('SHA', avatar_sha)
- else:
- app.interface.remove_avatar_files(our_jid)
+ 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))
- self.awaiting_answers[id_] = (VCARD_PUBLISHED, iq2)
+ elif stanza.getType() == 'error':
+ app.nec.push_incoming_event(
+ VcardNotPublishedEvent(None, conn=self))
def _IqCB(self, con, iq_obj):
id_ = iq_obj.getID()
@@ -449,63 +468,7 @@ class ConnectionVcard:
if id_ not in self.awaiting_answers:
return
- if self.awaiting_answers[id_][0] == VCARD_PUBLISHED:
- if iq_obj.getType() == 'result':
- vcard_iq = self.awaiting_answers[id_][1]
- # Save vcard to HD
- if vcard_iq.getTag('PHOTO') and vcard_iq.getTag('PHOTO').getTag(
- 'SHA'):
- new_sha = vcard_iq.getTag('PHOTO').getTagData('SHA')
- else:
- new_sha = ''
-
- # Save it to file
- our_jid = app.get_jid_from_account(self.name)
- self._save_vcard_to_hd(our_jid, vcard_iq)
-
- # Send new presence if sha changed and we are not invisible
- if self.vcard_sha != new_sha and app.SHOW_LIST[
- self.connected] != 'invisible':
- if not self.connection or self.connected < 2:
- del self.awaiting_answers[id_]
- return
- self.vcard_sha = new_sha
- sshow = helpers.get_xmpp_show(app.SHOW_LIST[
- self.connected])
- p = nbxmpp.Presence(typ=None, priority=self.priority,
- show=sshow, status=self.status)
- p = self.add_sha(p)
- self.connection.send(p)
- app.nec.push_incoming_event(VcardPublishedEvent(None,
- conn=self))
- elif iq_obj.getType() == 'error':
- app.nec.push_incoming_event(VcardNotPublishedEvent(None,
- conn=self))
- del self.awaiting_answers[id_]
- elif self.awaiting_answers[id_][0] == VCARD_ARRIVED:
- # If vcard is empty, we send to the interface an empty vcard so that
- # it knows it arrived
- jid = self.awaiting_answers[id_][1]
- groupchat_jid = self.awaiting_answers[id_][2]
- frm = jid
- if groupchat_jid:
- # We do as if it comes from the fake_jid
- frm = groupchat_jid
- our_jid = app.get_jid_from_account(self.name)
- if (not iq_obj.getTag('vCard') and iq_obj.getType() == 'result') or\
- iq_obj.getType() == 'error':
- if id_ in self.groupchat_jids:
- frm = self.groupchat_jids[id_]
- del self.groupchat_jids[id_]
- if frm:
- # Write an empty file
- self._save_vcard_to_hd(frm, '')
- jid, resource = app.get_room_and_nick_from_fjid(frm)
- vcard = {'jid': jid, 'resource': resource}
- app.nec.push_incoming_event(VcardReceivedEvent(None,
- conn=self, vcard_dict=vcard))
- del self.awaiting_answers[id_]
- elif self.awaiting_answers[id_][0] == AGENT_REMOVED:
+ if self.awaiting_answers[id_][0] == AGENT_REMOVED:
jid = self.awaiting_answers[id_][1]
app.nec.push_incoming_event(AgentRemovedEvent(None, conn=self,
agent=jid))
@@ -598,97 +561,104 @@ class ConnectionVcard:
app.nec.push_incoming_event(PEPConfigReceivedEvent(None,
conn=self, node=node, form=form))
- def _vCardCB(self, con, vc):
- """
- Called when we receive a vCard Parse the vCard and send it to plugins
- """
- if not vc.getTag('vCard'):
- return
- if not vc.getTag('vCard').getNamespace() == nbxmpp.NS_VCARD:
- return
- id_ = vc.getID()
- frm_iq = vc.getFrom()
- our_jid = app.get_jid_from_account(self.name)
- resource = ''
- if id_ in self.groupchat_jids:
- who = self.groupchat_jids[id_]
- frm, resource = app.get_room_and_nick_from_fjid(who)
- del self.groupchat_jids[id_]
- elif frm_iq:
- who = helpers.get_full_jid_from_iq(vc)
- frm, resource = app.get_room_and_nick_from_fjid(who)
- else:
- who = frm = our_jid
- card = vc.getChildren()[0]
- vcard = self._node_to_dict(card)
- photo_decoded = None
- if 'PHOTO' in vcard and isinstance(vcard['PHOTO'], dict) and \
- 'BINVAL' in vcard['PHOTO']:
+ def get_vcard_photo(self, vcard):
+ try:
photo = vcard['PHOTO']['BINVAL']
- try:
+ except (KeyError, AttributeError):
+ avatar_sha = None
+ photo_decoded = None
+ else:
+ if photo == '':
+ avatar_sha = None
+ photo_decoded = None
+ else:
photo_decoded = base64.b64decode(photo.encode('utf-8'))
avatar_sha = hashlib.sha1(photo_decoded).hexdigest()
- except Exception:
- avatar_sha = ''
+
+ 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()
+
+ vcard = self._node_to_dict(stanza.getChildren()[0])
+ # handle no vcard set
+
+ 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, 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)
+
+ 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(app.AVATAR_PATH, 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:
- avatar_sha = ''
+ app.interface.save_avatar(photo_decoded)
- if avatar_sha:
- card.getTag('PHOTO').setTagData('SHA', avatar_sha)
+ 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
- # Save it to file
- self._save_vcard_to_hd(who, card)
- # Save the decoded avatar to a separate file too, and generate files
- # for dbus notifications
- puny_jid = helpers.sanitize_filename(frm)
- puny_nick = None
- begin_path = os.path.join(app.AVATAR_PATH, puny_jid)
- frm_jid = frm
- if frm in self.room_jids:
- puny_nick = helpers.sanitize_filename(resource)
- # create folder if needed
- if not os.path.isdir(begin_path):
- os.mkdir(begin_path, 0o700)
- begin_path = os.path.join(begin_path, puny_nick)
- frm_jid += '/' + resource
- if photo_decoded:
- avatar_file = begin_path + '_notif_size_colored.png'
- if frm_jid == our_jid and avatar_sha != self.vcard_sha:
- app.interface.save_avatar_files(frm, photo_decoded, puny_nick)
- elif frm_jid != our_jid and (not os.path.exists(avatar_file) or \
- frm_jid not in self.vcard_shas or \
- avatar_sha != self.vcard_shas[frm_jid]):
- app.interface.save_avatar_files(frm, photo_decoded, puny_nick)
- if avatar_sha:
- self.vcard_shas[frm_jid] = avatar_sha
- elif frm in self.vcard_shas:
- del self.vcard_shas[frm]
+ self.send_avatar_presence()
+ self.avatar_presence_sent = True
+
+ def _on_avatar_received(self, jid, resource, room, vcard):
+ """
+ Called when we receive a vCard Parse the vCard and trigger Events
+ """
+ avatar_sha, photo_decoded = self.get_vcard_photo(vcard)
+ app.interface.save_avatar(photo_decoded)
+
+ # 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:
- for ext in ('.jpeg', '.png', '_notif_size_bw.png',
- '_notif_size_colored.png'):
- path = begin_path + ext
- if os.path.isfile(path):
- os.remove(path)
-
- vcard['jid'] = frm
- vcard['resource'] = resource
- app.nec.push_incoming_event(VcardReceivedEvent(None, conn=self,
- vcard_dict=vcard))
- if frm_jid == our_jid:
- # we re-send our presence with sha if has changed and if we are
- # not invisible
- if self.vcard_sha == avatar_sha:
- return
- self.vcard_sha = avatar_sha
- if app.SHOW_LIST[self.connected] == 'invisible':
- return
- if not self.connection:
- return
- sshow = helpers.get_xmpp_show(app.SHOW_LIST[self.connected])
- p = nbxmpp.Presence(typ=None, priority=self.priority,
- show=sshow, status=self.status)
- p = self.add_sha(p)
- self.connection.send(p)
+ 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):
@@ -940,18 +910,6 @@ class ConnectionHandlersBase:
obj.contact = c
break
- if obj.avatar_sha is not None and obj.ptype != 'error':
- if obj.jid not in self.vcard_shas:
- cached_vcard = self.get_cached_vcard(obj.jid)
- if cached_vcard and 'PHOTO' in cached_vcard and \
- 'SHA' in cached_vcard['PHOTO']:
- self.vcard_shas[obj.jid] = cached_vcard['PHOTO']['SHA']
- else:
- self.vcard_shas[obj.jid] = ''
- if obj.avatar_sha != self.vcard_shas[obj.jid]:
- # avatar has been updated
- self.request_vcard(obj.jid)
-
if obj.contact:
if obj.contact.show in statuss:
obj.old_show = statuss.index(obj.contact.show)
@@ -2007,6 +1965,7 @@ ConnectionHandlersBase, ConnectionJingle, ConnectionIBBytestream):
if obj.conn.name != self.name:
return
our_jid = app.get_jid_from_account(self.name)
+
if self.connected > 1 and self.continue_connect_info:
msg = self.continue_connect_info[1]
sign_msg = self.continue_connect_info[2]
@@ -2043,7 +2002,7 @@ ConnectionHandlersBase, ConnectionJingle, ConnectionIBBytestream):
app.nec.push_incoming_event(RosterInfoEvent(None,
conn=self, jid=jid, nickname=info['name'],
sub=info['subscription'], ask=info['ask'],
- groups=info['groups']))
+ groups=info['groups'], avatar_sha=info['avatar_sha']))
def _send_first_presence(self, signed=''):
show = self.continue_connect_info[0]
@@ -2065,12 +2024,7 @@ ConnectionHandlersBase, ConnectionJingle, ConnectionIBBytestream):
if show not in ['offline', 'online', 'chat', 'away', 'xa', 'dnd']:
return
priority = app.get_priority(self.name, sshow)
- our_jid = helpers.parse_jid(app.get_jid_from_account(self.name))
- vcard = self.get_cached_vcard(our_jid)
- if vcard and 'PHOTO' in vcard and 'SHA' in vcard['PHOTO']:
- self.vcard_sha = vcard['PHOTO']['SHA']
p = nbxmpp.Presence(typ=None, priority=priority, show=sshow)
- p = self.add_sha(p)
if msg:
p.setStatus(msg)
if signed:
@@ -2083,7 +2037,7 @@ ConnectionHandlersBase, ConnectionJingle, ConnectionIBBytestream):
show=show))
if self.vcard_supported:
# ask our VCard
- self.request_vcard(None)
+ self.request_vcard(self._on_own_avatar_received)
# Get bookmarks from private namespace
self.get_bookmarks()
@@ -2169,7 +2123,6 @@ ConnectionHandlersBase, ConnectionJingle, ConnectionIBBytestream):
# We also don't check for namespace, else it cannot stop _messageCB to
# be called
con.RegisterHandler('message', self._pubsubEventCB, makefirst=True)
- con.RegisterHandler('iq', self._vCardCB, 'result', nbxmpp.NS_VCARD)
con.RegisterHandler('iq', self._rosterSetCB, 'set', nbxmpp.NS_ROSTER)
con.RegisterHandler('iq', self._siSetCB, 'set', nbxmpp.NS_SI)
con.RegisterHandler('iq', self._rosterItemExchangeCB, 'set',
diff --git a/gajim/common/connection_handlers_events.py b/gajim/common/connection_handlers_events.py
index 09746fa87..4147cd2a1 100644
--- a/gajim/common/connection_handlers_events.py
+++ b/gajim/common/connection_handlers_events.py
@@ -24,6 +24,7 @@
from calendar import timegm
import datetime
import hashlib
+import base64
import hmac
import logging
import sys
@@ -324,6 +325,7 @@ class RosterReceivedEvent(nec.NetworkIncomingEvent):
self.conn.connection.getRoster().delItem(jid)
elif jid != our_jid: # don't add our jid
self.roster[j] = raw_roster[jid]
+ self.roster[j]['avatar_sha'] = None
else:
# Roster comes from DB
self.received_from_server = False
@@ -376,6 +378,9 @@ class RosterInfoEvent(nec.NetworkIncomingEvent):
name = 'roster-info'
base_network_events = []
+ def init(self):
+ self.avatar_sha = None
+
class MucOwnerReceivedEvent(nec.NetworkIncomingEvent, HelperEvent):
name = 'muc-owner-received'
base_network_events = []
@@ -532,19 +537,13 @@ class PubsubReceivedEvent(nec.NetworkIncomingEvent):
base_network_events = []
def generate(self):
+ self.jid = self.stanza.getFrom()
self.pubsub_node = self.stanza.getTag('pubsub')
if not self.pubsub_node:
return
self.items_node = self.pubsub_node.getTag('items')
if not self.items_node:
return
- self.item_node = self.items_node.getTag('item')
- if not self.item_node:
- return
- children = self.item_node.getChildren()
- if not children:
- return
- self.node = children[0]
return True
class PubsubBookmarksReceivedEvent(nec.NetworkIncomingEvent, BookmarksHelper):
@@ -553,13 +552,51 @@ class PubsubBookmarksReceivedEvent(nec.NetworkIncomingEvent, BookmarksHelper):
def generate(self):
self.conn = self.base_event.conn
- self.storage_node = self.base_event.node
+ self.item_node = self.base_event.items_node.getTag('item')
+ if not self.item_node:
+ return
+ children = self.item_node.getChildren()
+ if not children:
+ return
+ self.storage_node = children[0]
ns = self.storage_node.getNamespace()
if ns != nbxmpp.NS_BOOKMARKS:
return
self.parse_bookmarks()
return True
+class PubsubAvatarReceivedEvent(nec.NetworkIncomingEvent):
+ name = 'pubsub-avatar-received'
+ base_network_events = ['pubsub-received']
+
+ def __init__(self, name, base_event):
+ '''
+ Pre-Generated attributes on self:
+
+ :conn: Connection instance
+ :jid: The from jid
+ :pubsub_node: The 'pubsub' node
+ :items_node: The 'items' node
+ '''
+ self._set_base_event_vars_as_attributes(base_event)
+
+ def generate(self):
+ if self.items_node.getAttr('node') != 'urn:xmpp:avatar:data':
+ return
+ item = self.items_node.getTag('item')
+ self.sha = item.getAttr('id')
+ data_tag = item.getTag('data', namespace='urn:xmpp:avatar:data')
+ if self.sha is None or data_tag is None:
+ log.warning('Received malformed avatar data via pubsub')
+ return
+ self.data = data_tag.getData()
+ if self.data is None:
+ log.warning('Received malformed avatar data via pubsub')
+ return
+ self.data = base64.b64decode(self.data.encode('utf-8'))
+
+ return True
+
class SearchFormReceivedEvent(nec.NetworkIncomingEvent, HelperEvent):
name = 'search-form-received'
base_network_events = []
@@ -874,10 +911,8 @@ class GcPresenceReceivedEvent(nec.NetworkIncomingEvent, HelperEvent):
contact_name=fjid.getResource(),
message=st,
show=show)
- if self.avatar_sha == '':
- # contact has no avatar
- puny_nick = helpers.sanitize_filename(self.nick)
- app.interface.remove_avatar_files(self.room_jid, puny_nick)
+
+
# NOTE: if it's a gc presence, don't ask vcard here.
# We may ask it to real jid in gui part.
self.status_code = []
@@ -2004,16 +2039,20 @@ class VcardReceivedEvent(nec.NetworkIncomingEvent):
base_network_events = []
def generate(self):
- self.nickname = None
- if 'NICKNAME' in self.vcard_dict:
- self.nickname = self.vcard_dict['NICKNAME']
- elif 'FN' in self.vcard_dict:
- self.nickname = self.vcard_dict['FN']
- self.jid = self.vcard_dict['jid']
- self.resource = self.vcard_dict['resource']
- self.fjid = self.jid
- if self.resource:
- self.fjid += '/' + self.resource
+ return True
+
+class UpdateGCAvatarEvent(nec.NetworkIncomingEvent):
+ name = 'update-gc-avatar'
+ base_network_events = []
+
+ def generate(self):
+ return True
+
+class UpdateRosterAvatarEvent(nec.NetworkIncomingEvent):
+ name = 'update-roster-avatar'
+ base_network_events = []
+
+ def generate(self):
return True
class PEPConfigReceivedEvent(nec.NetworkIncomingEvent):
diff --git a/gajim/common/const.py b/gajim/common/const.py
index 8985ced5c..3ab2706b5 100644
--- a/gajim/common/const.py
+++ b/gajim/common/const.py
@@ -28,6 +28,14 @@ class OptionType(IntEnum):
ACTION = 3
DIALOG = 4
+class AvatarSize(IntEnum):
+ ROSTER = 32
+ NOTIFICATION = 48
+ CHAT = 52
+ PROFILE = 64
+ TOOLTIP = 125
+ VCARD = 200
+
THANKS = u"""\
Alexander Futász
diff --git a/gajim/common/contacts.py b/gajim/common/contacts.py
index e4a84315b..3c18892c4 100644
--- a/gajim/common/contacts.py
+++ b/gajim/common/contacts.py
@@ -94,7 +94,7 @@ class Contact(CommonContact):
"""
def __init__(self, jid, account, name='', groups=None, show='', status='',
sub='', ask='', resource='', priority=0, keyID='', client_caps=None,
- our_chatstate=None, chatstate=None, idle_time=None):
+ our_chatstate=None, chatstate=None, idle_time=None, avatar_sha=None):
if not isinstance(jid, str):
print('no str')
if groups is None:
@@ -105,6 +105,7 @@ class Contact(CommonContact):
self.contact_name = '' # nick choosen by contact
self.groups = [i if i else _('General') for i in set(groups)] # filter duplicate values
+ self.avatar_sha = avatar_sha
self.sub = sub
self.ask = ask
@@ -182,7 +183,7 @@ class GC_Contact(CommonContact):
def __init__(self, room_jid, account, name='', show='', status='', role='',
affiliation='', jid='', resource='', our_chatstate=None,
- chatstate=None):
+ chatstate=None, avatar_sha=None):
CommonContact.__init__(self, jid, account, resource, show, status, name,
our_chatstate, chatstate)
@@ -190,6 +191,7 @@ class GC_Contact(CommonContact):
self.room_jid = room_jid
self.role = role
self.affiliation = affiliation
+ self.avatar_sha = avatar_sha
def get_full_jid(self):
return self.room_jid + '/' + self.name
@@ -197,13 +199,16 @@ class GC_Contact(CommonContact):
def get_shown_name(self):
return self.name
+ def get_avatar(self, size=None):
+ return common.app.interface.get_avatar(self.avatar_sha, size)
+
def as_contact(self):
"""
Create a Contact instance from this GC_Contact instance
"""
return Contact(jid=self.get_full_jid(), account=self.account,
name=self.name, groups=[], show=self.show, status=self.status,
- sub='none', client_caps=self.client_caps)
+ sub='none', client_caps=self.client_caps, avatar_sha=self.avatar_sha)
class LegacyContactsAPI:
@@ -245,7 +250,8 @@ class LegacyContactsAPI:
def create_contact(self, jid, account, name='', groups=None, show='',
status='', sub='', ask='', resource='', priority=0, keyID='',
- client_caps=None, our_chatstate=None, chatstate=None, idle_time=None):
+ client_caps=None, our_chatstate=None, chatstate=None, idle_time=None,
+ avatar_sha=None):
if groups is None:
groups = []
# Use Account object if available
@@ -254,7 +260,7 @@ class LegacyContactsAPI:
show=show, status=status, sub=sub, ask=ask, resource=resource,
priority=priority, keyID=keyID, client_caps=client_caps,
our_chatstate=our_chatstate, chatstate=chatstate,
- idle_time=idle_time)
+ idle_time=idle_time, avatar_sha=avatar_sha)
def create_self_contact(self, jid, account, resource, show, status, priority,
name='', keyID=''):
@@ -283,7 +289,7 @@ class LegacyContactsAPI:
resource=contact.resource, priority=contact.priority,
keyID=contact.keyID, client_caps=contact.client_caps,
our_chatstate=contact.our_chatstate, chatstate=contact.chatstate,
- idle_time=contact.idle_time)
+ idle_time=contact.idle_time, avatar_sha=contact.avatar_sha)
def add_contact(self, account, contact):
if account not in self._accounts:
@@ -306,6 +312,15 @@ class LegacyContactsAPI:
def get_contact(self, account, jid, resource=None):
return self._accounts[account].contacts.get_contact(jid, resource=resource)
+ def get_avatar(self, account, jid, size=None):
+ return self._accounts[account].contacts.get_avatar(jid, size)
+
+ def get_avatar_sha(self, account, jid):
+ return self._accounts[account].contacts.get_avatar_sha(jid)
+
+ def set_avatar(self, account, jid, sha):
+ self._accounts[account].contacts.set_avatar(jid, sha)
+
def iter_contacts(self, account):
for contact in self._accounts[account].contacts.iter_contacts():
yield contact
@@ -395,10 +410,10 @@ class LegacyContactsAPI:
raise AttributeError(attr_name)
def create_gc_contact(self, room_jid, account, name='', show='', status='',
- role='', affiliation='', jid='', resource=''):
+ role='', affiliation='', jid='', resource='', avatar_sha=None):
account = self._accounts.get(account, account) # Use Account object if available
return GC_Contact(room_jid, account, name, show, status, role, affiliation, jid,
- resource)
+ resource, avatar_sha=avatar_sha)
def add_gc_contact(self, account, gc_contact):
return self._accounts[account].gc_contacts.add_gc_contact(gc_contact)
@@ -424,6 +439,12 @@ class LegacyContactsAPI:
def get_nb_role_total_gc_contacts(self, account, room_jid, role):
return self._accounts[account].gc_contacts.get_nb_role_total_gc_contacts(room_jid, role)
+ def set_gc_avatar(self, account, room_jid, nick, sha):
+ contact = self.get_gc_contact(account, room_jid, nick)
+ if contact is None:
+ return
+ contact.avatar_sha = sha
+
class Contacts():
"""
@@ -490,6 +511,33 @@ class Contacts():
return c
return self._contacts[jid][0]
+ def get_avatar(self, jid, size=None):
+ if jid not in self._contacts:
+ return None
+
+ for resource in self._contacts[jid]:
+ if resource.avatar_sha is None:
+ continue
+ avatar = common.app.interface.get_avatar(resource.avatar_sha, size)
+ if avatar is None:
+ self.set_avatar(jid, None)
+ return avatar
+
+ def get_avatar_sha(self, jid):
+ if jid not in self._contacts:
+ return None
+
+ for resource in self._contacts[jid]:
+ if resource.avatar_sha is not None:
+ return resource.avatar_sha
+ return None
+
+ def set_avatar(self, jid, sha):
+ if jid not in self._contacts:
+ return
+ for resource in self._contacts[jid]:
+ resource.avatar_sha = sha
+
def iter_contacts(self):
for jid in list(self._contacts.keys()):
for contact in self._contacts[jid][:]:
diff --git a/gajim/common/helpers.py b/gajim/common/helpers.py
index 1036b354c..2cdc33549 100644
--- a/gajim/common/helpers.py
+++ b/gajim/common/helpers.py
@@ -633,24 +633,6 @@ def get_account_status(account):
status = reduce_chars_newlines(account['status_line'], 100, 1)
return status
-def get_avatar_path(prefix):
- """
- Return the filename of the avatar, distinguishes between user- and contact-
- provided one. Return None if no avatar was found at all. prefix is the path
- to the requested avatar just before the ".png" or ".jpeg"
- """
- # First, scan for a local, user-set avatar
- for type_ in ('jpeg', 'png'):
- file_ = prefix + '_local.' + type_
- if os.path.exists(file_):
- return file_
- # If none available, scan for a contact-provided avatar
- for type_ in ('jpeg', 'png'):
- file_ = prefix + '.' + type_
- if os.path.exists(file_):
- return file_
- return None
-
def datetime_tuple(timestamp):
"""
Convert timestamp using strptime and the format: %Y%m%dT%H:%M:%S
@@ -1396,7 +1378,7 @@ def get_subscription_request_msg(account=None):
if account:
s = _('Hello, I am $name.') + ' ' + s
our_jid = app.get_jid_from_account(account)
- vcard = app.connections[account].get_cached_vcard(our_jid)
+ vcard = app.connections[account].own_vcard
name = ''
if vcard:
if 'N' in vcard:
diff --git a/gajim/common/logger.py b/gajim/common/logger.py
index eb2cadb62..3e210982e 100644
--- a/gajim/common/logger.py
+++ b/gajim/common/logger.py
@@ -42,7 +42,6 @@ from enum import IntEnum, unique
from gajim.common import exceptions
from gajim.common import app
-from gajim.common import ged
import sqlite3 as sqlite
@@ -998,7 +997,7 @@ class Logger:
# First we fill data with roster_entry informations
self.cur.execute('''
- SELECT j.jid, re.jid_id, re.name, re.subscription, re.ask
+ SELECT j.jid, re.jid_id, re.name, re.subscription, re.ask, re.avatar_sha
FROM roster_entry re, jids j
WHERE re.account_jid_id=? AND j.jid_id=re.jid_id''', (account_jid_id,))
for row in self.cur:
@@ -1006,6 +1005,7 @@ class Logger:
jid = row.jid
name = row.name
data[jid] = {}
+ data[jid]['avatar_sha'] = row.avatar_sha
if name:
data[jid]['name'] = name
else:
@@ -1135,3 +1135,25 @@ class Logger:
self._timeout_commit()
return lastrowid
+
+ def set_avatar_sha(self, account_jid, jid, sha=None):
+ """
+ Set the avatar sha of a jid on an account
+
+ :param account_jid: The jid of the account
+
+ :param jid: The jid that belongs to the avatar
+
+ :param sha: The sha of the avatar
+
+ """
+
+ account_jid_id = self.get_jid_id(account_jid)
+ jid_id = self.get_jid_id(jid)
+
+ sql = '''
+ UPDATE roster_entry SET avatar_sha = ?
+ WHERE account_jid_id = ? AND jid_id = ?
+ '''
+ self.con.execute(sql, (sha, account_jid_id, jid_id))
+ self._timeout_commit()
diff --git a/gajim/common/pep.py b/gajim/common/pep.py
index 004296c12..ab6c54b4b 100644
--- a/gajim/common/pep.py
+++ b/gajim/common/pep.py
@@ -244,6 +244,7 @@ class AbstractPEP(object):
self._update_contacts(jid, account)
if jid == app.get_jid_from_account(account):
self._update_account(account)
+ self._on_receive(jid, account)
def _extract_info(self, items):
'''To be implemented by subclasses'''
@@ -269,6 +270,10 @@ class AbstractPEP(object):
'''SHOULD be implemented by subclasses'''
return ''
+ def _on_receive(self, jid, account):
+ '''SHOULD be implemented by subclasses'''
+ pass
+
class UserMoodPEP(AbstractPEP):
'''XEP-0107: User Mood'''
@@ -469,5 +474,32 @@ class UserLocationPEP(AbstractPEP):
return location_string.strip()
-SUPPORTED_PERSONAL_USER_EVENTS = [UserMoodPEP, UserTunePEP, UserActivityPEP,
- UserNicknamePEP, UserLocationPEP]
+class AvatarNotificationPEP(AbstractPEP):
+ '''XEP-0084: Avatars'''
+
+ type_ = 'avatar-notification'
+ namespace = 'urn:xmpp:avatar:metadata'
+
+ def _extract_info(self, items):
+ avatar = None
+ for item in items.getTags('item'):
+ info = item.getTag('metadata').getTag('info')
+ self.avatar = info.getAttrs()
+ break
+
+ return (avatar, False)
+
+ def _on_receive(self, jid, account):
+ sha = app.contacts.get_avatar_sha(account, jid)
+ app.log('avatar').info(
+ 'Update (Pubsub): %s %s', jid, self.avatar['id'])
+ if sha == self.avatar['id']:
+ return
+ con = app.connections[account]
+ app.log('avatar').info('Request (Pubsub): %s', jid)
+ con.send_pb_retrieve(jid, 'urn:xmpp:avatar:data', self.avatar['id'])
+
+
+SUPPORTED_PERSONAL_USER_EVENTS = [
+ UserMoodPEP, UserTunePEP, UserActivityPEP,
+ UserNicknamePEP, UserLocationPEP, AvatarNotificationPEP]
diff --git a/gajim/common/pubsub.py b/gajim/common/pubsub.py
index 4718b62ff..49d021b37 100644
--- a/gajim/common/pubsub.py
+++ b/gajim/common/pubsub.py
@@ -27,8 +27,11 @@ from gajim.common import app
#from common.connection_handlers import PEP_CONFIG
PEP_CONFIG = 'pep_config'
from gajim.common import ged
+from gajim.common.nec import NetworkEvent
from gajim.common.connection_handlers_events import PubsubReceivedEvent
from gajim.common.connection_handlers_events import PubsubBookmarksReceivedEvent
+from gajim.common.connection_handlers_events import PubsubAvatarReceivedEvent
+
import logging
log = logging.getLogger('gajim.c.pubsub')
@@ -36,8 +39,11 @@ class ConnectionPubSub:
def __init__(self):
self.__callbacks = {}
app.nec.register_incoming_event(PubsubBookmarksReceivedEvent)
+ app.nec.register_incoming_event(PubsubAvatarReceivedEvent)
app.ged.register_event_handler('pubsub-bookmarks-received',
ged.CORE, self._nec_pubsub_bookmarks_received)
+ app.ged.register_event_handler('pubsub-avatar-received',
+ ged.CORE, self._nec_pubsub_avatar_received)
def cleanup(self):
app.ged.remove_event_handler('pubsub-bookmarks-received',
@@ -97,7 +103,7 @@ class ConnectionPubSub:
self.connection.send(query)
- def send_pb_retrieve(self, jid, node, cb=None, *args, **kwargs):
+ def send_pb_retrieve(self, jid, node, item_id=None, cb=None, *args, **kwargs):
"""
Get items from a node
"""
@@ -106,6 +112,8 @@ class ConnectionPubSub:
query = nbxmpp.Iq('get', to=jid)
r = query.addChild('pubsub', namespace=nbxmpp.NS_PUBSUB)
r = r.addChild('items', {'node': node})
+ if item_id is not None:
+ r.addChild('item', {'id': item_id})
id_ = self.connection.send(query)
if cb:
@@ -202,6 +210,28 @@ class ConnectionPubSub:
# We got bookmarks from pubsub, now get those from xml to merge them
self.get_bookmarks(storage_type='xml')
+ def _nec_pubsub_avatar_received(self, obj):
+ if obj.conn.name != self.name:
+ return
+
+ if obj.jid is None:
+ jid = self.get_own_jid().getStripped()
+ else:
+ jid = obj.jid.getStripped()
+
+ app.log('avatar').info(
+ 'Received Avatar (Pubsub): %s %s', jid, obj.sha)
+ app.interface.save_avatar(obj.data)
+
+ if self.get_own_jid().bareMatch(jid):
+ app.config.set_per('accounts', self.name, 'avatar_sha', obj.sha)
+ else:
+ own_jid = self.get_own_jid().getStripped()
+ app.logger.set_avatar_sha(own_jid, jid, obj.sha)
+ app.contacts.set_avatar(self.name, jid, obj.sha)
+
+ app.interface.update_avatar(self.name, jid)
+
def _PubSubErrorCB(self, conn, stanza):
log.debug('_PubsubErrorCB')
pubsub = stanza.getTag('pubsub')
diff --git a/gajim/common/zeroconf/connection_handlers_zeroconf.py b/gajim/common/zeroconf/connection_handlers_zeroconf.py
index d0aeaf846..3684e4827 100644
--- a/gajim/common/zeroconf/connection_handlers_zeroconf.py
+++ b/gajim/common/zeroconf/connection_handlers_zeroconf.py
@@ -36,23 +36,21 @@ log = logging.getLogger('gajim.c.z.connection_handlers_zeroconf')
STATUS_LIST = ['offline', 'connecting', 'online', 'chat', 'away', 'xa', 'dnd',
'invisible']
# kind of events we can wait for an answer
-VCARD_PUBLISHED = 'vcard_published'
-VCARD_ARRIVED = 'vcard_arrived'
AGENT_REMOVED = 'agent_removed'
from gajim.common import connection_handlers
class ConnectionVcard(connection_handlers.ConnectionVcard):
- def add_sha(self, p, send_caps = True):
+ def add_sha(self, p, *args):
return p
def add_caps(self, p):
return p
- def request_vcard(self, jid = None, is_fake_jid = False):
+ def request_vcard(self, *args):
pass
- def send_vcard(self, vcard):
+ def send_vcard(self, *args):
pass
diff --git a/gajim/data/gui/contact_context_menu.ui b/gajim/data/gui/contact_context_menu.ui
index 98d846d40..069be648c 100644
--- a/gajim/data/gui/contact_context_menu.ui
+++ b/gajim/data/gui/contact_context_menu.ui
@@ -108,15 +108,6 @@
-
-
-