Refactor Avatars

- Add support for Pubsub Avatars
- Dont poll for vCard Updates, only use XEP-0153
- Dont cache vCards
- Store the avatar SHA of roster contacts in the DB
- Store the current SHA of each contact in the Contacts Object
- Move some code into the ConnectionVcard Class
This commit is contained in:
Philipp Hörist 2017-09-16 11:49:31 +02:00
parent 68f13788ed
commit a01cdbf271
25 changed files with 778 additions and 1025 deletions

View File

@ -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()

View File

@ -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 = {}

View File

@ -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 ],

View File

@ -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()

View File

@ -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 <photo/> 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 <photo/> 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',

View File

@ -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):

View File

@ -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

View File

@ -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][:]:

View File

@ -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:

View File

@ -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()

View File

@ -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]

View File

@ -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')

View File

@ -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

View File

@ -108,15 +108,6 @@
<signal name="activate" handler="on_assign_openpgp_key_menuitem_activate" swapped="no"/>
</object>
</child>
<child>
<object class="GtkMenuItem" id="set_custom_avatar_menuitem">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<property name="label" translatable="yes">Set Custom _Avatar...</property>
<property name="use_underline">True</property>
</object>
</child>
<child>
<object class="GtkMenuItem" id="add_special_notification_menuitem">
<property name="can_focus">False</property>

View File

@ -30,6 +30,7 @@
import os
import time
import locale
from gi.repository import Gtk
from gi.repository import Gdk
from gi.repository import GdkPixbuf
@ -44,6 +45,7 @@ from gajim import config
from gajim import vcard
from gajim import cell_renderer_image
from gajim import dataforms_widget
from gajim.common.const import AvatarSize
import nbxmpp
from enum import IntEnum, unique
@ -62,6 +64,7 @@ from gajim.command_system.implementation.hosts import PrivateChatCommands
from gajim.command_system.implementation.hosts import GroupChatCommands
from gajim.common.connection_handlers_events import GcMessageOutgoingEvent
import logging
log = logging.getLogger('gajim.groupchat_control')
@ -91,8 +94,7 @@ def tree_cell_data_func(column, renderer, model, iter_, tv=None):
if parent_iter and (model[iter_][Column.AVATAR] or avatar_position == \
'left'):
renderer.set_property('visible', True)
renderer.set_property('width', app.config.get(
'roster_avatar_width'))
renderer.set_property('width', AvatarSize.ROSTER)
else:
renderer.set_property('visible', False)
if parent_iter:
@ -141,6 +143,8 @@ class PrivateChatControl(ChatControl):
self.gc_contact = gc_contact
ChatControl.__init__(self, parent_win, contact, account, session)
self.TYPE_ID = 'pm'
app.ged.register_event_handler('update-gc-avatar', ged.GUI1,
self._nec_update_avatar)
app.ged.register_event_handler('caps-received', ged.GUI1,
self._nec_caps_received_pm)
app.ged.register_event_handler('gc-presence-received', ged.GUI1,
@ -151,6 +155,8 @@ class PrivateChatControl(ChatControl):
def shutdown(self):
super(PrivateChatControl, self).shutdown()
app.ged.remove_event_handler('update-gc-avatar', ged.GUI1,
self._nec_update_avatar)
app.ged.remove_event_handler('caps-received', ged.GUI1,
self._nec_caps_received_pm)
app.ged.remove_event_handler('gc-presence-received', ged.GUI1,
@ -240,6 +246,20 @@ class PrivateChatControl(ChatControl):
self.got_connected()
ChatControl.update_ui(self)
def _nec_update_avatar(self, obj):
if obj.contact != self.gc_contact:
return
self.show_avatar()
def show_avatar(self):
if not app.config.get('show_avatar_in_chat'):
return
pixbuf = app.interface.get_avatar(
self.gc_contact.avatar_sha, AvatarSize.CHAT)
image = self.xml.get_object('avatar_image')
image.set_from_pixbuf(pixbuf)
def update_contact(self):
self.contact = self.gc_contact.as_contact()
@ -502,8 +522,8 @@ class GroupchatControl(ChatControlBase):
self._nec_gc_message_received)
app.ged.register_event_handler('vcard-published', ged.GUI1,
self._nec_vcard_published)
app.ged.register_event_handler('vcard-received', ged.GUI1,
self._nec_vcard_received)
app.ged.register_event_handler('update-gc-avatar', ged.GUI1,
self._nec_update_avatar)
app.ged.register_event_handler('gc-subject-received', ged.GUI1,
self._nec_gc_subject_received)
app.ged.register_event_handler('gc-config-changed-received', ged.GUI1,
@ -534,7 +554,8 @@ class GroupchatControl(ChatControlBase):
if widget.get_tooltip_window():
return
widget.set_has_tooltip(True)
widget.set_tooltip_window(tooltips.GCTooltip(self.parent_win.window))
widget.set_tooltip_window(tooltips.GCTooltip(
self.account, self.parent_win.window))
id_ = widget.connect('query-tooltip', self.query_tooltip)
self.handlers[id_] = widget
@ -1046,12 +1067,12 @@ class GroupchatControl(ChatControlBase):
status = obj.conn.status
obj.conn.send_gc_status(self.nick, self.room_jid, show, status)
def _nec_vcard_received(self, obj):
if obj.conn.name != self.account:
def _nec_update_avatar(self, obj):
if obj.contact.room_jid != self.room_jid:
return
if obj.jid != self.room_jid:
return
self.draw_avatar(obj.resource)
app.log('avatar').debug('Draw Groupchat Avatar: %s %s',
obj.contact.name, obj.contact.avatar_sha)
self.draw_avatar(obj.contact)
def _nec_gc_message_received(self, obj):
if obj.room_jid != self.room_jid or obj.conn.name != self.account:
@ -1584,21 +1605,15 @@ class GroupchatControl(ChatControlBase):
self.model[iter_][Column.IMG] = image
self.model[iter_][Column.TEXT] = name
def draw_avatar(self, nick):
def draw_avatar(self, gc_contact):
if not app.config.get('show_avatars_in_roster'):
return
iter_ = self.get_contact_iter(nick)
iter_ = self.get_contact_iter(gc_contact.name)
if not iter_:
return
fake_jid = self.room_jid + '/' + nick
pixbuf = gtkgui_helpers.get_avatar_pixbuf_from_cache(fake_jid)
if pixbuf in ('ask', None):
scaled_pixbuf = empty_pixbuf
else:
scaled_pixbuf = gtkgui_helpers.get_scaled_pixbuf(pixbuf, 'roster')
if not scaled_pixbuf:
scaled_pixbuf = empty_pixbuf
self.model[iter_][Column.AVATAR] = scaled_pixbuf
pixbuf = app.interface.get_avatar(gc_contact.avatar_sha, AvatarSize.ROSTER)
self.model[iter_][Column.AVATAR] = pixbuf or empty_pixbuf
def draw_role(self, role):
role_iter = self.get_role_iter(role)
@ -1755,31 +1770,6 @@ class GroupchatControl(ChatControlBase):
if obj.nick in self.gc_custom_colors:
self.gc_custom_colors[obj.new_nick] = \
self.gc_custom_colors[obj.nick]
# rename vcard / avatar
puny_jid = helpers.sanitize_filename(self.room_jid)
puny_nick = helpers.sanitize_filename(obj.nick)
puny_new_nick = helpers.sanitize_filename(obj.new_nick)
old_path = os.path.join(app.VCARD_PATH, puny_jid,
puny_nick)
new_path = os.path.join(app.VCARD_PATH, puny_jid,
puny_new_nick)
files = {old_path: new_path}
path = os.path.join(app.AVATAR_PATH, puny_jid)
# possible extensions
for ext in ('.png', '.jpeg', '_notif_size_bw.png',
'_notif_size_colored.png'):
files[os.path.join(path, puny_nick + ext)] = \
os.path.join(path, puny_new_nick + ext)
for old_file in files:
if os.path.exists(old_file) and old_file != \
files[old_file]:
if os.path.exists(files[old_file]) and \
helpers.windowsify(old_file) != helpers.windowsify(
files[old_file]):
# Windows require this, but os.remove('test')
# will also remove 'TEST'
os.remove(files[old_file])
os.rename(old_file, files[old_file])
self.print_conversation(s, 'info', graphics=False)
elif '321' in obj.status_code:
s = _('%(nick)s has been removed from the room '
@ -1831,7 +1821,7 @@ class GroupchatControl(ChatControlBase):
s = _('You are now known as %s') % nick
self.print_conversation(s, 'info', graphics=False)
iter_ = self.add_contact_to_roster(obj.nick, obj.show, role,
affiliation, obj.status, obj.real_jid)
affiliation, obj.status, obj.real_jid, obj.avatar_sha)
newly_created = True
self.draw_all_roles()
if obj.status_code and '201' in obj.status_code:
@ -1845,35 +1835,6 @@ class GroupchatControl(ChatControlBase):
log.error('%s has an iter, but no gc_contact instance' % \
obj.nick)
return
# Re-get vcard if avatar has changed
# We do that here because we may request it to the real JID if
# we knows it. connections.py doesn't know it.
con = app.connections[self.account]
if gc_c and gc_c.jid:
real_jid = gc_c.jid
else:
real_jid = obj.fjid
if obj.fjid in obj.conn.vcard_shas:
if obj.avatar_sha != obj.conn.vcard_shas[obj.fjid]:
server = app.get_server_from_jid(self.room_jid)
if not server.startswith('irc'):
obj.conn.request_vcard(real_jid, obj.fjid)
else:
cached_vcard = obj.conn.get_cached_vcard(obj.fjid, True)
if cached_vcard and 'PHOTO' in cached_vcard and \
'SHA' in cached_vcard['PHOTO']:
cached_sha = cached_vcard['PHOTO']['SHA']
else:
cached_sha = ''
if cached_sha != obj.avatar_sha:
# avatar has been updated
# sha in mem will be updated later
server = app.get_server_from_jid(self.room_jid)
if not server.startswith('irc'):
obj.conn.request_vcard(real_jid, obj.fjid)
else:
# save sha in mem NOW
obj.conn.vcard_shas[obj.fjid] = obj.avatar_sha
actual_affiliation = gc_c.affiliation
if affiliation != actual_affiliation:
@ -1946,7 +1907,7 @@ class GroupchatControl(ChatControlBase):
self.print_conversation(st, graphics=False)
def add_contact_to_roster(self, nick, show, role, affiliation, status,
jid=''):
jid='', avatar_sha=None):
role_name = helpers.get_uf_role(role, plural=True)
resource = ''
@ -1973,22 +1934,14 @@ class GroupchatControl(ChatControlBase):
gc_contact = app.contacts.create_gc_contact(
room_jid=self.room_jid, account=self.account,
name=nick, show=show, status=status, role=role,
affiliation=affiliation, jid=j, resource=resource)
affiliation=affiliation, jid=j, resource=resource,
avatar_sha=avatar_sha)
app.contacts.add_gc_contact(self.account, gc_contact)
else:
gc_contact = app.contacts.get_gc_contact(self.account, self.room_jid, nick)
self.draw_contact(nick)
self.draw_avatar(nick)
# Do not ask avatar to irc rooms as irc transports reply with messages
server = app.get_server_from_jid(self.room_jid)
if app.config.get('ask_avatars_on_startup') and \
not server.startswith('irc'):
fake_jid = self.room_jid + '/' + nick
pixbuf = gtkgui_helpers.get_avatar_pixbuf_from_cache(fake_jid)
if pixbuf == 'ask':
if j and not self.is_anonymous:
app.connections[self.account].request_vcard(j, fake_jid)
else:
app.connections[self.account].request_vcard(fake_jid,
fake_jid)
self.draw_avatar(gc_contact)
if nick == self.nick: # we became online
self.got_connected()
if self.list_treeview.get_model():
@ -2175,8 +2128,8 @@ class GroupchatControl(ChatControlBase):
self._nec_gc_message_received)
app.ged.remove_event_handler('vcard-published', ged.GUI1,
self._nec_vcard_published)
app.ged.remove_event_handler('vcard-received', ged.GUI1,
self._nec_vcard_received)
app.ged.remove_event_handler('update-gc-avatar', ged.GUI1,
self._nec_update_avatar)
app.ged.remove_event_handler('gc-subject-received', ged.GUI1,
self._nec_gc_subject_received)
app.ged.remove_event_handler('gc-config-changed-received', ged.GUI1,

View File

@ -301,42 +301,33 @@ def set_unset_urgency_hint(window, unread_messages_no):
else:
window.props.urgency_hint = False
# feeding the image directly into the pixbuf seems possible, but is error prone and causes image distortions and segfaults.
# see http://stackoverflow.com/a/8892894/3528174
# and https://developer.gnome.org/gdk-pixbuf/unstable/gdk-pixbuf-Image-Data-in-Memory.html#gdk-pixbuf-new-from-bytes
# to learn how this could be done (or look into the mercurial history)
def get_pixbuf_from_data(file_data, want_type = False):
def get_pixbuf_from_data(file_data):
"""
Get image data and returns GdkPixbuf.Pixbuf if want_type is True it also
returns 'jpeg', 'png' etc
Get image data and returns GdkPixbuf.Pixbuf
"""
pixbufloader = GdkPixbuf.PixbufLoader()
try:
pixbufloader.write(file_data)
pixbufloader.close()
pixbuf = pixbufloader.get_pixbuf()
except GLib.GError: # 'unknown image format'
except GLib.GError:
pixbufloader.close()
# try to open and convert this image to png using pillow (if available)
log.debug("loading avatar using pixbufloader failed, trying to convert avatar image using pillow (if available)")
log.warning('loading avatar using pixbufloader failed, trying to '
'convert avatar image using pillow')
try:
avatar = Image.open(BytesIO(file_data)).convert("RGBA")
arr = GLib.Bytes.new(avatar.tobytes())
array = GLib.Bytes.new(avatar.tobytes())
width, height = avatar.size
pixbuf = GdkPixbuf.Pixbuf.new_from_bytes(arr, GdkPixbuf.Colorspace.RGB, True, 8, width, height, width * 4)
except:
log.info("Could not use pillow to convert avatar image, image cannot be displayed")
if want_type:
return None, None
else:
return None
pixbuf = GdkPixbuf.Pixbuf.new_from_bytes(
array, GdkPixbuf.Colorspace.RGB,
True, 8, width, height, width * 4)
except Exception:
log.warning('Could not use pillow to convert avatar image, '
'image cannot be displayed', exc_info=True)
return
if want_type:
typ = pixbufloader.get_format() and pixbufloader.get_format().get_name() or None
return pixbuf, typ
else:
return pixbuf
return pixbuf
def get_cursor(attr):
display = Gdk.Display.get_default()
@ -445,90 +436,6 @@ def get_fade_color(treeview, selected, focused):
return Gdk.RGBA(bg.red*p + fg.red*q, bg.green*p + fg.green*q,
bg.blue*p + fg.blue*q)
def get_scaled_pixbuf_by_size(pixbuf, width, height):
# Pixbuf size
pix_width = pixbuf.get_width()
pix_height = pixbuf.get_height()
# don't make avatars bigger than they are
if pix_width < width and pix_height < height:
return pixbuf # we don't want to make avatar bigger
ratio = float(pix_width) / float(pix_height)
if ratio > 1:
w = width
h = int(w / ratio)
else:
h = height
w = int(h * ratio)
scaled_buf = pixbuf.scale_simple(w, h, GdkPixbuf.InterpType.HYPER)
return scaled_buf
def get_scaled_pixbuf(pixbuf, kind):
"""
Return scaled pixbuf, keeping ratio etc or None kind is either "chat",
"roster", "notification", "tooltip", "vcard"
"""
# resize to a width / height for the avatar not to have distortion
# (keep aspect ratio)
width = app.config.get(kind + '_avatar_width')
height = app.config.get(kind + '_avatar_height')
if width < 1 or height < 1:
return None
return get_scaled_pixbuf_by_size(pixbuf, width, height)
def get_avatar_pixbuf_from_cache(fjid, use_local=True):
"""
Check if jid has cached avatar and if that avatar is valid image (can be
shown)
Returns None if there is no image in vcard/
Returns 'ask' if cached vcard should not be used (user changed his vcard, so
we have new sha) or if we don't have the vcard
"""
jid, nick = app.get_room_and_nick_from_fjid(fjid)
if app.config.get('hide_avatar_of_transport') and\
app.jid_is_transport(jid):
# don't show avatar for the transport itself
return None
if any(jid in app.contacts.get_gc_list(acc) for acc in \
app.contacts.get_accounts()):
is_groupchat_contact = True
else:
is_groupchat_contact = False
puny_jid = helpers.sanitize_filename(jid)
if is_groupchat_contact:
puny_nick = helpers.sanitize_filename(nick)
path = os.path.join(app.VCARD_PATH, puny_jid, puny_nick)
local_avatar_basepath = os.path.join(app.AVATAR_PATH, puny_jid,
puny_nick) + '_local'
else:
path = os.path.join(app.VCARD_PATH, puny_jid)
local_avatar_basepath = os.path.join(app.AVATAR_PATH, puny_jid) + \
'_local'
if use_local:
for extension in ('.png', '.jpeg'):
local_avatar_path = local_avatar_basepath + extension
if os.path.isfile(local_avatar_path):
avatar_file = open(local_avatar_path, 'rb')
avatar_data = avatar_file.read()
avatar_file.close()
return get_pixbuf_from_data(avatar_data)
if not os.path.isfile(path):
return 'ask'
vcard_dict = list(app.connections.values())[0].get_cached_vcard(fjid,
is_groupchat_contact)
if not vcard_dict: # This can happen if cached vcard is too old
return 'ask'
if 'PHOTO' not in vcard_dict:
return None
pixbuf = vcard.get_avatar_pixbuf_encoded_mime(vcard_dict['PHOTO'])[0]
return pixbuf
def make_gtk_month_python_month(month):
"""
GTK starts counting months from 0, so January is 0 but Python's time start
@ -558,11 +465,17 @@ def get_possible_button_event(event):
def destroy_widget(widget):
widget.destroy()
def on_avatar_save_as_menuitem_activate(widget, jid, default_name=''):
def on_avatar_save_as_menuitem_activate(widget, avatar, default_name=''):
def on_continue(response, file_path):
if response < 0:
return
pixbuf = get_avatar_pixbuf_from_cache(jid)
if isinstance(avatar, str):
# We got a SHA
pixbuf = app.interface.get_avatar(avatar)
else:
# We got a pixbuf
pixbuf = avatar
extension = os.path.splitext(file_path)[1]
if not extension:
# Silently save as Jpeg image
@ -577,7 +490,7 @@ def on_avatar_save_as_menuitem_activate(widget, jid, default_name=''):
try:
pixbuf.savev(file_path, image_format, [], [])
except Exception as e:
log.debug('Error saving avatar: %s' % str(e))
log.error('Error saving avatar: %s' % str(e))
if os.path.exists(file_path):
os.remove(file_path)
new_file_path = '.'.join(file_path.split('.')[:-1]) + '.jpeg'

View File

@ -39,13 +39,17 @@ import sys
import re
import time
import math
from subprocess import Popen
import hashlib
from gi.repository import Gtk
from gi.repository import GdkPixbuf
from gi.repository import GLib
from gajim.common import i18n
try:
from PIL import Image
except:
pass
from gajim.common import app
from gajim.common import events
@ -82,12 +86,14 @@ from gajim.common import socks5
from gajim.common import helpers
from gajim.common import passwords
from gajim.common import logging_helpers
from gajim.common.connection_handlers_events import OurShowEvent, \
FileRequestErrorEvent, FileTransferCompletedEvent
from gajim.common.connection_handlers_events import (
OurShowEvent, FileRequestErrorEvent, FileTransferCompletedEvent,
UpdateRosterAvatarEvent, UpdateGCAvatarEvent)
from gajim.common.connection import Connection
from gajim.common.file_props import FilesProp
from gajim.common import pep
from gajim import emoticons
from gajim.common.const import AvatarSize
from gajim import roster_window
from gajim import profile_window
@ -242,13 +248,13 @@ class Interface:
if account in self.show_vcard_when_connect and obj.show not in (
'offline', 'error'):
self.edit_own_details(account)
self.show_vcard_when_connect.remove(self.name)
def edit_own_details(self, account):
jid = app.get_jid_from_account(account)
if 'profile' not in self.instances[account]:
self.instances[account]['profile'] = \
profile_window.ProfileWindow(account, app.interface.roster.window)
app.connections[account].request_vcard(jid)
@staticmethod
def handle_gc_error(gc_control, pritext, sectext):
@ -359,7 +365,6 @@ class Interface:
# priority, # keyID, timestamp, contact_nickname))
#
# Contact changed show
account = obj.conn.name
jid = obj.jid
show = obj.show
@ -558,16 +563,6 @@ class Interface:
dialogs.ErrorDialog(_('Contact with "%s" cannot be established') % \
obj.agent, _('Check your connection or try again later.'))
def handle_event_vcard(self, obj):
# ('VCARD', account, data)
'''vcard holds the vcard data'''
our_jid = app.get_jid_from_account(obj.conn.name)
if obj.jid == our_jid:
if obj.nickname:
app.nicks[obj.conn.name] = obj.nickname
if obj.conn.name in self.show_vcard_when_connect:
self.show_vcard_when_connect.remove(obj.conn.name)
def handle_event_gc_config(self, obj):
#('GC_CONFIG', account, (jid, form_node)) config is a dict
account = obj.conn.name
@ -780,7 +775,8 @@ class Interface:
keyID = attached_keys[attached_keys.index(obj.jid) + 1]
contact = app.contacts.create_contact(jid=obj.jid,
account=account, name=obj.nickname, groups=obj.groups,
show='offline', sub=obj.sub, ask=obj.ask, keyID=keyID)
show='offline', sub=obj.sub, ask=obj.ask, keyID=keyID,
avatar_sha=obj.avatar_sha)
app.contacts.add_contact(account, contact)
self.roster.add_contact(obj.jid, account)
else:
@ -1554,7 +1550,6 @@ class Interface:
self.handle_event_subscribed_presence],
'unsubscribed-presence-received': [
self.handle_event_unsubscribed_presence],
'vcard-received': [self.handle_event_vcard],
'zeroconf-name-conflict': [self.handle_event_zc_name_conflict],
}
@ -2334,85 +2329,94 @@ class Interface:
sys.exit()
@staticmethod
def save_avatar_files(jid, photo, puny_nick = None, local = False):
"""
Save an avatar to a separate file, and generate files for dbus
notifications. An avatar can be given as a pixmap directly or as an
decoded image
"""
puny_jid = helpers.sanitize_filename(jid)
path_to_file = os.path.join(app.AVATAR_PATH, puny_jid)
if puny_nick:
path_to_file = os.path.join(path_to_file, puny_nick)
# remove old avatars
for typ in ('jpeg', 'png'):
if local:
path_to_original_file = path_to_file + '_local'+ '.' + typ
else:
path_to_original_file = path_to_file + '.' + typ
if os.path.isfile(path_to_original_file):
os.remove(path_to_original_file)
if local and photo:
pixbuf = photo
typ = 'png'
extension = '_local.png' # save local avatars as png file
def update_avatar(account=None, jid=None, contact=None):
if contact is None:
app.nec.push_incoming_event(
UpdateRosterAvatarEvent(None, account=account, jid=jid))
else:
pixbuf, typ = gtkgui_helpers.get_pixbuf_from_data(photo,
want_type=True)
app.nec.push_incoming_event(
UpdateGCAvatarEvent(None, contact=contact))
def save_avatar(self, data, publish=False):
if data is None:
return
if publish:
pixbuf = gtkgui_helpers.get_pixbuf_from_data(data)
if pixbuf is None:
return
if typ not in ('jpeg', 'png'):
log.info('gtkpixbuf cannot save other than jpeg and '\
'png formats. saving \'%s\' avatar as png file (originaly: %s)'\
% (jid, typ))
typ = 'png'
extension = '.' + typ
path_to_original_file = path_to_file + extension
pixbuf = pixbuf.scale_simple(AvatarSize.PROFILE,
AvatarSize.PROFILE,
GdkPixbuf.InterpType.BILINEAR)
publish_path = os.path.join(app.AVATAR_PATH, 'temp_publish')
pixbuf.savev(publish_path, 'png', [], [])
with open(publish_path, 'rb') as file:
data = file.read()
return self.save_avatar(data)
sha = hashlib.sha1(data).hexdigest()
path = os.path.join(app.AVATAR_PATH, sha)
try:
pixbuf.savev(path_to_original_file, typ, [], [])
except Exception as e:
log.error('Error writing avatar file %s: %s' % (
path_to_original_file, str(e)))
# Generate and save the resized, color avatar
pixbuf = gtkgui_helpers.get_scaled_pixbuf(pixbuf, 'notification')
if pixbuf:
path_to_normal_file = path_to_file + '_notif_size_colored' + \
extension
try:
pixbuf.savev(path_to_normal_file, 'png', [], [])
except Exception as e:
log.error('Error writing avatar file %s: %s' % \
(path_to_original_file, str(e)))
# Generate and save the resized, black and white avatar
bwbuf = gtkgui_helpers.get_scaled_pixbuf(
gtkgui_helpers.make_pixbuf_grayscale(pixbuf), 'notification')
if bwbuf:
path_to_bw_file = path_to_file + '_notif_size_bw' + extension
try:
bwbuf.savev(path_to_bw_file, 'png', [], [])
except Exception as e:
log.error('Error writing avatar file %s: %s' % \
(path_to_original_file, str(e)))
with open(path, "wb") as output_file:
output_file.write(data)
except Exception:
app.log('avatar').error('Saving avatar failed', exc_info=True)
return
return sha
@staticmethod
def remove_avatar_files(jid, puny_nick = None, local = False):
"""
Remove avatar files of a jid
"""
puny_jid = helpers.sanitize_filename(jid)
path_to_file = os.path.join(app.AVATAR_PATH, puny_jid)
if puny_nick:
path_to_file = os.path.join(path_to_file, puny_nick)
for ext in ('.jpeg', '.png'):
if local:
ext = '_local' + ext
path_to_original_file = path_to_file + ext
if os.path.isfile(path_to_file + ext):
os.remove(path_to_file + ext)
if os.path.isfile(path_to_file + '_notif_size_colored' + ext):
os.remove(path_to_file + '_notif_size_colored' + ext)
if os.path.isfile(path_to_file + '_notif_size_bw' + ext):
os.remove(path_to_file + '_notif_size_bw' + ext)
def get_avatar(filename, size=None, publish=False):
if filename is None or '':
return
if publish:
path = os.path.join(app.AVATAR_PATH, filename)
with open(path, 'rb') as file:
data = file.read()
return data
try:
sha = app.avatar_cache[filename][size]
return sha
except KeyError:
pass
path = os.path.join(app.AVATAR_PATH, filename)
if not os.path.isfile(path):
return
pixbuf = None
try:
if size is not None:
pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_size(
path, size, size)
else:
pixbuf = GdkPixbuf.Pixbuf.new_from_file(path)
except GLib.GError as error:
app.log('avatar').info(
'loading avatar %s failed. Try to convert '
'avatar image using pillow', filename)
try:
avatar = Image.open(path).convert("RGBA")
except NameError:
app.log('avatar').warning('Pillow convert failed: %s', filename)
app.log('avatar').debug('Error', exc_info=True)
return
array = GLib.Bytes.new(avatar.tobytes())
width, height = avatar.size
pixbuf = GdkPixbuf.Pixbuf.new_from_bytes(
array, GdkPixbuf.Colorspace.RGB, True,
8, width, height, width * 4)
if size:
pixbuf = pixbuf.scale_simple(
size, size, GdkPixbuf.InterpType.BILINEAR)
if filename not in app.avatar_cache:
app.avatar_cache[filename] = {}
app.avatar_cache[filename][size] = pixbuf
return pixbuf
def auto_join_bookmarks(self, account):
"""

View File

@ -235,7 +235,6 @@ control=None, gc_contact=None, is_anonymous=True):
unblock_menuitem = xml.get_object('unblock_menuitem')
ignore_menuitem = xml.get_object('ignore_menuitem')
unignore_menuitem = xml.get_object('unignore_menuitem')
set_custom_avatar_menuitem = xml.get_object('set_custom_avatar_menuitem')
# Subscription submenu
subscription_menuitem = xml.get_object('subscription_menuitem')
send_auth_menuitem, ask_auth_menuitem, revoke_auth_menuitem = \
@ -339,7 +338,7 @@ control=None, gc_contact=None, is_anonymous=True):
if app.config.get_per('accounts', account, 'is_zeroconf'):
for item in (send_custom_status_menuitem, send_single_message_menuitem,
invite_menuitem, block_menuitem, unblock_menuitem, ignore_menuitem,
unignore_menuitem, set_custom_avatar_menuitem, subscription_menuitem,
unignore_menuitem, subscription_menuitem,
manage_contact_menuitem, convert_to_gc_menuitem):
item.set_no_show_all(True)
item.hide()
@ -451,9 +450,6 @@ control=None, gc_contact=None, is_anonymous=True):
add_to_roster_menuitem.set_no_show_all(True)
subscription_menuitem.set_sensitive(False)
set_custom_avatar_menuitem.connect('activate',
roster.on_set_custom_avatar_activate, contact, account)
# Hide items when it's self contact row
if our_jid:
manage_contact_menuitem.set_sensitive(False)

View File

@ -24,18 +24,18 @@
from gi.repository import Gtk
from gi.repository import Gdk
from gi.repository import GdkPixbuf
from gi.repository import GObject
from gi.repository import GLib
from gi.repository import GdkPixbuf
import base64
import mimetypes
import os
import time
import logging
import hashlib
from gajim import gtkgui_helpers
from gajim import dialogs
from gajim import vcard
from gajim.common.const import AvatarSize
from gajim.common import app
from gajim.common import ged
@ -61,6 +61,7 @@ class ProfileWindow:
self.dialog = None
self.avatar_mime_type = None
self.avatar_encoded = None
self.avatar_sha = None
self.message_id = self.statusbar.push(self.context_id,
_('Retrieving profile…'))
self.update_progressbar_timeout_id = GLib.timeout_add(100,
@ -75,10 +76,10 @@ class ProfileWindow:
self._nec_vcard_published)
app.ged.register_event_handler('vcard-not-published', ged.GUI1,
self._nec_vcard_not_published)
app.ged.register_event_handler('vcard-received', ged.GUI1,
self._nec_vcard_received)
self.window.show_all()
self.xml.get_object('ok_button').grab_focus()
app.connections[account].request_vcard(
self._nec_vcard_received, self.jid)
def on_information_notebook_switch_page(self, widget, page, page_num):
GLib.idle_add(self.xml.get_object('ok_button').grab_focus)
@ -100,8 +101,6 @@ class ProfileWindow:
self._nec_vcard_published)
app.ged.remove_event_handler('vcard-not-published', ged.GUI1,
self._nec_vcard_not_published)
app.ged.remove_event_handler('vcard-received', ged.GUI1,
self._nec_vcard_received)
del app.interface.instances[self.account]['profile']
if self.dialog: # Image chooser dialog
self.dialog.destroy()
@ -119,71 +118,35 @@ class ProfileWindow:
text_button = self.xml.get_object('NOPHOTO_button')
text_button.show()
self.avatar_encoded = None
self.avatar_sha = None
self.avatar_mime_type = None
def on_set_avatar_button_clicked(self, widget):
def on_ok(widget, path_to_file):
must_delete = False
filesize = os.path.getsize(path_to_file) # in bytes
invalid_file = False
msg = ''
if os.path.isfile(path_to_file):
stat = os.stat(path_to_file)
if stat[6] == 0:
invalid_file = True
msg = _('File is empty')
else:
invalid_file = True
msg = _('File does not exist')
if not invalid_file and filesize > 16384: # 16 kb
try:
pixbuf = GdkPixbuf.Pixbuf.new_from_file(path_to_file)
# get the image at 'notification size'
# and hope that user did not specify in ACE crazy size
scaled_pixbuf = gtkgui_helpers.get_scaled_pixbuf(pixbuf,
'tooltip')
except GObject.GError as msg: # unknown format
# msg should be string, not object instance
msg = str(msg)
invalid_file = True
if invalid_file:
if True: # keep identation
dialogs.ErrorDialog(_('Could not load image'), msg,
transient_for=self.window)
return
if filesize > 16384:
if scaled_pixbuf:
path_to_file = os.path.join(app.TMP,
'avatar_scaled.png')
scaled_pixbuf.savev(path_to_file, 'png', [], [])
must_delete = True
with open(path_to_file, 'rb') as fd:
data = fd.read()
pixbuf = gtkgui_helpers.get_pixbuf_from_data(data)
try:
# rescale it
pixbuf = gtkgui_helpers.get_scaled_pixbuf(pixbuf, 'vcard')
except AttributeError: # unknown format
dialogs.ErrorDialog(_('Could not load image'),
transient_for=self.window)
with open(path_to_file, 'rb') as file:
data = file.read()
sha = app.interface.save_avatar(data, publish=True)
if sha is None:
dialogs.ErrorDialog(
_('Could not load image'), transient_for=self.window)
return
self.dialog.destroy()
self.dialog = None
pixbuf = app.interface.get_avatar(sha, AvatarSize.VCARD)
button = self.xml.get_object('PHOTO_button')
image = button.get_image()
image.set_from_pixbuf(pixbuf)
button.show()
text_button = self.xml.get_object('NOPHOTO_button')
text_button.hide()
self.avatar_encoded = base64.b64encode(data).decode('utf-8')
# returns None if unknown type
self.avatar_mime_type = mimetypes.guess_type(path_to_file)[0]
if must_delete:
try:
os.remove(path_to_file)
except OSError:
log.debug('Cannot remove %s' % path_to_file)
self.avatar_sha = sha
publish = app.interface.get_avatar(sha, publish=True)
self.avatar_encoded = base64.b64encode(publish).decode('utf-8')
self.avatar_mime_type = 'image/jpeg'
def on_clear(widget):
self.dialog.destroy()
@ -197,27 +160,25 @@ class ProfileWindow:
if self.dialog:
self.dialog.present()
else:
self.dialog = dialogs.AvatarChooserDialog(on_response_ok = on_ok,
on_response_cancel = on_cancel, on_response_clear = on_clear)
self.dialog = dialogs.AvatarChooserDialog(
on_response_ok=on_ok, on_response_cancel=on_cancel,
on_response_clear=on_clear)
def on_PHOTO_button_press_event(self, widget, event):
"""
If right-clicked, show popup
"""
if event.button == 3 and self.avatar_encoded: # right click
pixbuf = self.xml.get_object('PHOTO_button').get_image().get_pixbuf()
if event.button == 3 and pixbuf: # right click
menu = Gtk.Menu()
# Try to get pixbuf
pixbuf = gtkgui_helpers.get_avatar_pixbuf_from_cache(self.jid,
use_local=False)
if pixbuf not in (None, 'ask'):
nick = app.config.get_per('accounts', self.account, 'name')
menuitem = Gtk.MenuItem.new_with_mnemonic(_('Save _As'))
menuitem.connect('activate',
gtkgui_helpers.on_avatar_save_as_menuitem_activate,
self.jid, nick)
menu.append(menuitem)
nick = app.config.get_per('accounts', self.account, 'name')
sha = app.contacts.get_avatar_sha(self.account, self.jid)
menuitem = Gtk.MenuItem.new_with_mnemonic(_('Save _As'))
menuitem.connect('activate',
gtkgui_helpers.on_avatar_save_as_menuitem_activate,
sha, nick)
menu.append(menuitem)
# show clear
menuitem = Gtk.MenuItem.new_with_mnemonic(_('_Clear'))
menuitem.connect('activate', self.on_clear_button_clicked)
@ -265,14 +226,16 @@ class ProfileWindow:
text_button.show()
for i in vcard_.keys():
if i == 'PHOTO':
pixbuf, self.avatar_encoded, self.avatar_mime_type = \
vcard.get_avatar_pixbuf_encoded_mime(vcard_[i])
if not pixbuf:
image.set_from_pixbuf(None)
button.hide()
text_button.show()
photo_encoded = vcard_[i]['BINVAL']
if photo_encoded == '':
continue
pixbuf = gtkgui_helpers.get_scaled_pixbuf(pixbuf, 'vcard')
photo_decoded = base64.b64decode(photo_encoded.encode('utf-8'))
pixbuf = gtkgui_helpers.get_pixbuf_from_data(photo_decoded)
if pixbuf is None:
continue
pixbuf = pixbuf.scale_simple(
AvatarSize.PROFILE, AvatarSize.PROFILE,
GdkPixbuf.InterpType.BILINEAR)
image.set_from_pixbuf(pixbuf)
button.show()
text_button.hide()
@ -305,12 +268,8 @@ class ProfileWindow:
self.progressbar.set_fraction(0)
self.update_progressbar_timeout_id = None
def _nec_vcard_received(self, obj):
if obj.conn.name != self.account:
return
if obj.jid != self.jid:
return
self.set_values(obj.vcard_dict)
def _nec_vcard_received(self, jid, resource, room, vcard_):
self.set_values(vcard_)
def add_to_vcard(self, vcard_, entry, txt):
"""
@ -367,7 +326,7 @@ class ProfileWindow:
vcard_['PHOTO'] = {'BINVAL': self.avatar_encoded}
if self.avatar_mime_type:
vcard_['PHOTO']['TYPE'] = self.avatar_mime_type
return vcard_
return vcard_, self.avatar_sha
def on_ok_button_clicked(self, widget):
if self.update_progressbar_timeout_id:
@ -378,7 +337,7 @@ class ProfileWindow:
_('Without a connection, you can not publish your contact '
'information.'), transient_for=self.window)
return
vcard_ = self.make_vcard()
vcard_, sha = self.make_vcard()
nick = ''
if 'NICKNAME' in vcard_:
nick = vcard_['NICKNAME']
@ -387,7 +346,7 @@ class ProfileWindow:
app.connections[self.account].retract_nickname()
nick = app.config.get_per('accounts', self.account, 'name')
app.nicks[self.account] = nick
app.connections[self.account].send_vcard(vcard_)
app.connections[self.account].send_vcard(vcard_, sha)
self.message_id = self.statusbar.push(self.context_id,
_('Sending profile…'))
self.progressbar.show()

View File

@ -592,24 +592,6 @@ class SignalObject(dbus.service.Object):
return
app.interface.handle_event(account, jid, event.type_)
@dbus.service.method(INTERFACE, in_signature='s', out_signature='a{sv}')
def contact_info(self, jid):
"""
Get vcard info for a contact. Return cached value of the vcard
"""
if not isinstance(jid, str):
jid = str(jid)
if not jid:
raise dbus_support.MissingArgument()
jid = self._get_real_jid(jid)
cached_vcard = list(app.connections.values())[0].get_cached_vcard(jid)
if cached_vcard:
return get_dbus_struct(cached_vcard)
# return empty dict
return DBUS_DICT_SV()
@dbus.service.method(INTERFACE, in_signature='', out_signature='as')
def list_accounts(self):
"""
@ -891,17 +873,22 @@ class SignalObject(dbus.service.Object):
if not invalid_file and filesize < 16384:
with open(picture, 'rb') as fd:
data = fd.read()
sha = app.interface.save_avatar(data, publish=True)
if sha is None:
return
app.config.set_per('accounts', self.name, 'avatar_sha', sha)
data = app.interface.get_avatar(sha, publish=True)
avatar = base64.b64encode(data).decode('utf-8')
avatar_mime_type = mimetypes.guess_type(picture)[0]
vcard={}
vcard = {}
vcard['PHOTO'] = {'BINVAL': avatar}
if avatar_mime_type:
vcard['PHOTO']['TYPE'] = avatar_mime_type
if account:
app.connections[account].send_vcard(vcard)
app.connections[account].send_vcard(vcard, sha)
else:
for acc in app.connections:
app.connections[acc].send_vcard(vcard)
app.connections[acc].send_vcard(vcard, sha)
@dbus.service.method(INTERFACE, in_signature='ssss', out_signature='')
def join_room(self, room_jid, nick, password, account):

View File

@ -42,6 +42,7 @@ from gi.repository import Gio
import os
import time
import locale
import hashlib
from enum import IntEnum, unique
@ -57,6 +58,7 @@ from gajim import cell_renderer_image
from gajim import tooltips
from gajim import message_control
from gajim import adhoc_commands
from gajim.common.const import AvatarSize
from gajim.common import app
from gajim.common import helpers
@ -1361,13 +1363,12 @@ class RosterWindow:
if not iters or not app.config.get('show_avatars_in_roster'):
return
jid = self.model[iters[0]][Column.JID]
pixbuf = gtkgui_helpers.get_avatar_pixbuf_from_cache(jid)
if pixbuf in (None, 'ask'):
scaled_pixbuf = empty_pixbuf
else:
scaled_pixbuf = gtkgui_helpers.get_scaled_pixbuf(pixbuf, 'roster')
pixbuf = app.contacts.get_avatar(account, jid, size=AvatarSize.ROSTER)
if pixbuf is None:
pixbuf = empty_pixbuf
for child_iter in iters:
self.model[child_iter][Column.AVATAR_PIXBUF] = scaled_pixbuf
self.model[child_iter][Column.AVATAR_PIXBUF] = pixbuf
return False
def draw_completely(self, jid, account):
@ -1885,6 +1886,7 @@ class RosterWindow:
array[self_jid] = {'name': app.nicks[account],
'groups': ['self_contact'], 'subscription': 'both',
'ask': 'none'}
# .keys() is needed
for jid in list(array.keys()):
# Remove the contact in roster. It might has changed
@ -1918,25 +1920,6 @@ class RosterWindow:
ask=array[jid]['ask'], resource=resource, keyID=keyID)
app.contacts.add_contact(account, contact1)
if app.config.get('ask_avatars_on_startup'):
pixbuf = gtkgui_helpers.get_avatar_pixbuf_from_cache(ji)
if pixbuf == 'ask':
transport = app.get_transport_name_from_jid(contact1.jid)
if not transport or app.jid_is_transport(contact1.jid):
jid_with_resource = contact1.jid
if contact1.resource:
jid_with_resource += '/' + contact1.resource
app.connections[account].request_vcard(
jid_with_resource)
else:
host = app.get_server_from_jid(contact1.jid)
if host not in app.transport_avatar[account]:
app.transport_avatar[account][host] = \
[contact1.jid]
else:
app.transport_avatar[account][host].append(
contact1.jid)
# If we already have chat windows opened, update them with new
# contact instance
chat_control = app.interface.msg_win_mgr.get_control(ji, account)
@ -2591,11 +2574,6 @@ class RosterWindow:
# Update existing iter and group counting
self.draw_contact(jid, account)
self.draw_group(_('Transports'), account)
if obj.new_show > 1 and jid in app.transport_avatar[account]:
# transport just signed in.
# request avatars
for jid_ in app.transport_avatar[account][jid]:
obj.conn.request_vcard(jid_)
if obj.contact:
self.chg_contact_status(obj.contact, obj.show, obj.status, account)
@ -2638,10 +2616,11 @@ class RosterWindow:
resource = ''
if app.connections[account].server_resource:
resource = app.connections[account].server_resource
sha = app.config.get_per('accounts', account, 'avatar_sha')
contact = app.contacts.create_contact(jid=self_jid,
account=account, name=app.nicks[account],
groups=['self_contact'], show='offline', sub='both',
ask='none', resource=resource)
ask='none', resource=resource, avatar_sha=sha)
app.contacts.add_contact(account, contact)
self.add_contact(self_jid, account)
if app.config.get('remember_opened_chat_controls'):
@ -2697,11 +2676,9 @@ class RosterWindow:
else:
self.draw_pep(obj.jid, obj.conn.name, obj.pep_type)
def _nec_vcard_received(self, obj):
if obj.resource:
# it's a muc occupant vcard
return
self.draw_avatar(obj.jid, obj.conn.name)
def _nec_update_avatar(self, obj):
app.log('avatar').debug('Draw roster avatar: %s', obj.jid)
self.draw_avatar(obj.jid, obj.account)
def _nec_gc_subject_received(self, obj):
contact = app.contacts.get_contact_with_highest_priority(
@ -3053,46 +3030,6 @@ class RosterWindow:
_('Select a key to apply to the contact'), public_keys,
on_key_selected, selected=keyID, transient_for=self.window)
def on_set_custom_avatar_activate(self, widget, contact, account):
def on_ok(widget, path_to_file):
filesize = os.path.getsize(path_to_file) # in bytes
invalid_file = False
msg = ''
if os.path.isfile(path_to_file):
stat = os.stat(path_to_file)
if stat[6] == 0:
invalid_file = True
msg = _('File is empty')
else:
invalid_file = True
msg = _('File does not exist')
if invalid_file:
dialogs.ErrorDialog(_('Could not load image'), msg)
return
try:
pixbuf = GdkPixbuf.Pixbuf.new_from_file(path_to_file)
if filesize > 16384: # 16 kb
# get the image at 'tooltip size'
# and hope that user did not specify in ACE crazy size
pixbuf = gtkgui_helpers.get_scaled_pixbuf(pixbuf, 'tooltip')
except GObject.GError as msg: # unknown format
# msg should be string, not object instance
msg = str(msg)
dialogs.ErrorDialog(_('Could not load image'), msg)
return
app.interface.save_avatar_files(contact.jid, pixbuf, local=True)
dlg.destroy()
self.update_avatar_in_gui(contact.jid, account)
def on_clear(widget):
dlg.destroy()
# Delete file:
app.interface.remove_avatar_files(contact.jid, local=True)
self.update_avatar_in_gui(contact.jid, account)
dlg = dialogs.AvatarChooserDialog(on_response_ok=on_ok,
on_response_clear=on_clear)
def on_edit_groups(self, widget, list_):
dialogs.EditGroupsDialog(list_)
@ -3429,7 +3366,7 @@ class RosterWindow:
type_ = model[path][Column.TYPE]
# x_min is the x start position of status icon column
if app.config.get('avatar_position_in_roster') == 'left':
x_min = app.config.get('roster_avatar_width')
x_min = AvatarSize.ROSTER
else:
x_min = 0
if app.single_click and not event.get_state() & Gdk.ModifierType.SHIFT_MASK and \
@ -4805,15 +4742,6 @@ class RosterWindow:
for ctrl in list(app.interface.minimized_controls[account].values()):
ctrl.repaint_themed_widgets()
def update_avatar_in_gui(self, jid, account):
# Update roster
self.draw_avatar(jid, account)
# Update chat window
ctrl = app.interface.msg_win_mgr.get_control(jid, account)
if ctrl:
ctrl.show_avatar()
def _iconCellDataFunc(self, column, renderer, model, titer, data=None):
"""
When a row is added, set properties for icon renderer
@ -4966,8 +4894,7 @@ class RosterWindow:
renderer.set_property('visible', False)
if app.config.get('avatar_position_in_roster') == 'left':
renderer.set_property('width', app.config.get(
'roster_avatar_width'))
renderer.set_property('width', AvatarSize.ROSTER)
renderer.set_property('xalign', 0.5)
else:
renderer.set_property('xalign', 1) # align pixbuf to the right
@ -6063,8 +5990,8 @@ class RosterWindow:
self._nec_agent_removed)
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)
app.ged.register_event_handler('update-roster-avatar', ged.GUI1,
self._nec_update_avatar)
app.ged.register_event_handler('gc-subject-received', ged.GUI1,
self._nec_gc_subject_received)
app.ged.register_event_handler('metacontacts-received', ged.GUI2,

View File

@ -38,7 +38,7 @@ from datetime import datetime
from datetime import timedelta
from gajim import gtkgui_helpers
from gajim.common.const import AvatarSize
from gajim.common import app
from gajim.common import helpers
from gajim.common.i18n import Q_
@ -311,8 +311,9 @@ class NotificationAreaTooltip(BaseTooltip, StatusTable):
class GCTooltip(Gtk.Window):
# pylint: disable=E1101
def __init__(self, parent):
def __init__(self, account, parent):
Gtk.Window.__init__(self, type=Gtk.WindowType.POPUP, transient_for=parent)
self.account = account
self.row = None
self.set_title('tooltip')
self.set_border_width(3)
@ -379,17 +380,14 @@ class GCTooltip(Gtk.Window):
self.affiliation.show()
# Avatar
puny_name = helpers.sanitize_filename(contact.name)
puny_room = helpers.sanitize_filename(contact.room_jid)
file_ = helpers.get_avatar_path(os.path.join(app.AVATAR_PATH,
puny_room, puny_name))
if file_:
with open(file_, 'rb') as file_data:
pix = gtkgui_helpers.get_pixbuf_from_data(file_data.read())
pix = gtkgui_helpers.get_scaled_pixbuf(pix, 'tooltip')
self.avatar.set_from_pixbuf(pix)
self.avatar.show()
self.fillelement.show()
if contact.avatar_sha is not None:
app.log('avatar').debug(
'Load GCTooltip: %s %s', contact.name, contact.avatar_sha)
pixbuf = app.interface.get_avatar(contact.avatar_sha, AvatarSize.TOOLTIP)
if pixbuf is not None:
self.avatar.set_from_pixbuf(pixbuf)
self.avatar.show()
self.fillelement.show()
@staticmethod
def colorize_affiliation(affiliation):
@ -662,25 +660,22 @@ class RosterTooltip(Gtk.Window, StatusTable):
self._set_idle_time(contact)
# Avatar
puny_jid = helpers.sanitize_filename(self.prim_contact.jid)
file_ = helpers.get_avatar_path(os.path.join(app.AVATAR_PATH,
puny_jid))
if file_:
with open(file_, 'rb') as file_data:
pix = gtkgui_helpers.get_pixbuf_from_data(file_data.read())
pix = gtkgui_helpers.get_scaled_pixbuf(pix, 'tooltip')
self.avatar.set_from_pixbuf(pix)
self.avatar.show()
pixbuf = app.contacts.get_avatar(
account, self.prim_contact.jid, AvatarSize.TOOLTIP)
if pixbuf is None:
return
self.avatar.set_from_pixbuf(pixbuf)
self.avatar.show()
# Sets the Widget that is at the bottom to expand.
# This is needed in case the Picture takes more Space then the Labels
i = 1
while i < 15:
if self.tooltip_grid.get_child_at(0, i):
if self.tooltip_grid.get_child_at(0, i).get_visible():
self.last_widget = self.tooltip_grid.get_child_at(0, i)
i += 1
self.last_widget.set_vexpand(True)
# Sets the Widget that is at the bottom to expand.
# This is needed in case the Picture takes more Space then the Labels
i = 1
while i < 15:
if self.tooltip_grid.get_child_at(0, i):
if self.tooltip_grid.get_child_at(0, i).get_visible():
self.last_widget = self.tooltip_grid.get_child_at(0, i)
i += 1
self.last_widget.set_vexpand(True)
def _append_pep_info(self, contact):
"""

View File

@ -34,6 +34,7 @@
from gi.repository import Gtk
from gi.repository import GLib
from gi.repository import Gdk
from gi.repository import GdkPixbuf
import base64
import time
import locale
@ -46,38 +47,10 @@ from gajim.common import helpers
from gajim.common import app
from gajim.common import ged
from gajim.common.i18n import Q_
from gajim.common.const import AvatarSize
# log = logging.getLogger('gajim.vcard')
def get_avatar_pixbuf_encoded_mime(photo):
"""
Return the pixbuf of the image
Photo is a dictionary containing PHOTO information.
"""
if not isinstance(photo, dict):
return None, None, None
img_decoded = None
avatar_encoded = None
avatar_mime_type = None
if 'BINVAL' in photo:
img_encoded = photo['BINVAL']
avatar_encoded = img_encoded
try:
img_decoded = base64.b64decode(img_encoded.encode('utf-8'))
except Exception:
pass
if img_decoded:
if 'TYPE' in photo:
avatar_mime_type = photo['TYPE']
pixbuf = gtkgui_helpers.get_pixbuf_from_data(img_decoded)
else:
pixbuf, avatar_mime_type = gtkgui_helpers.get_pixbuf_from_data(
img_decoded, want_type=True)
else:
pixbuf = None
return pixbuf, avatar_encoded, avatar_mime_type
class VcardWindow:
"""
Class for contact's information window
@ -92,6 +65,7 @@ class VcardWindow:
self.contact = contact
self.account = account
self.gc_contact = gc_contact
self.avatar = None
# Get real jid
if gc_contact:
@ -122,8 +96,6 @@ class VcardWindow:
image.show()
self.xml.get_object('custom_avatar_label').show()
break
self.avatar_mime_type = None
self.avatar_encoded = None
self.vcard_arrived = False
self.os_info_arrived = False
self.entity_time_arrived = False
@ -136,8 +108,6 @@ class VcardWindow:
self.set_os_info)
app.ged.register_event_handler('time-result-received', ged.GUI1,
self.set_entity_time)
app.ged.register_event_handler('vcard-received', ged.GUI1,
self._nec_vcard_received)
self.fill_jabber_page()
annotations = app.connections[self.account].annotations
@ -181,8 +151,6 @@ class VcardWindow:
self.set_os_info)
app.ged.remove_event_handler('time-result-received', ged.GUI1,
self.set_entity_time)
app.ged.remove_event_handler('vcard-received', ged.GUI1,
self._nec_vcard_received)
def on_vcard_information_window_key_press_event(self, widget, event):
if event.keyval == Gdk.KEY_Escape:
@ -198,9 +166,17 @@ class VcardWindow:
if event.button == 3: # right click
menu = Gtk.Menu()
menuitem = Gtk.MenuItem.new_with_mnemonic(_('Save _As'))
if self.gc_contact:
sha = self.gc_contact.avatar_sha
name = self.gc_contact.get_shown_name()
else:
sha = app.contacts.get_avatar_sha(
self.account, self.contact.jid)
name = self.contact.get_shown_name()
if sha is None:
sha = self.avatar
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)
menu.append(menuitem)
menu.connect('selection-done', lambda w:w.destroy())
# show the menu
@ -229,17 +205,23 @@ class VcardWindow:
for i in vcard.keys():
if i == 'PHOTO' and self.xml.get_object('information_notebook').\
get_n_pages() > 4:
pixbuf, self.avatar_encoded, self.avatar_mime_type = \
get_avatar_pixbuf_encoded_mime(vcard[i])
image = self.xml.get_object('PHOTO_image')
image.show()
self.xml.get_object('user_avatar_label').show()
if not pixbuf:
image.set_from_icon_name('stock_person',
Gtk.IconSize.DIALOG)
if 'BINVAL' not in vcard[i]:
continue
pixbuf = gtkgui_helpers.get_scaled_pixbuf(pixbuf, 'vcard')
photo_encoded = vcard[i]['BINVAL']
if photo_encoded == '':
continue
photo_decoded = base64.b64decode(photo_encoded.encode('utf-8'))
pixbuf = gtkgui_helpers.get_pixbuf_from_data(photo_decoded)
if pixbuf is None:
continue
pixbuf = pixbuf.scale_simple(
AvatarSize.PROFILE, AvatarSize.PROFILE,
GdkPixbuf.InterpType.BILINEAR)
image = self.xml.get_object('PHOTO_image')
image.set_from_pixbuf(pixbuf)
image.show()
self.avatar = pixbuf
self.xml.get_object('user_avatar_label').show()
continue
if i in ('ADR', 'TEL', 'EMAIL'):
for entry in vcard[i]:
@ -276,19 +258,9 @@ class VcardWindow:
widget.set_text('')
self.xml.get_object('DESC_textview').get_buffer().set_text('')
def _nec_vcard_received(self, obj):
if obj.conn.name != self.account:
return
if obj.resource:
# It's a muc occupant vcard
if obj.fjid != self.contact.jid:
return
else:
if obj.jid != self.contact.jid:
return
def _nec_vcard_received(self, jid, resource, room, vcard):
self.clear_values()
self.set_values(obj.vcard_dict)
self.set_values(vcard)
def set_os_info(self, obj):
if obj.conn.name != self.account:
@ -492,12 +464,12 @@ class VcardWindow:
self.fill_status_label()
con = app.connections[self.account]
if self.gc_contact:
# If we know the real jid, remove the resource from vcard request
app.connections[self.account].request_vcard(self.real_jid_for_vcard,
self.gc_contact.get_full_jid())
con.request_vcard(self._nec_vcard_received,
self.gc_contact.get_full_jid(), room=True)
else:
app.connections[self.account].request_vcard(self.contact.jid)
con.request_vcard(self._nec_vcard_received, self.contact.jid)
def on_close_button_clicked(self, widget):
self.window.destroy()
@ -513,9 +485,6 @@ class ZeroconfVcardWindow:
self.account = account
self.is_fake = is_fake
# self.avatar_mime_type = None
# self.avatar_encoded = None
self.fill_contact_page()
self.fill_personal_page()
@ -538,7 +507,7 @@ class ZeroconfVcardWindow:
menuitem = Gtk.MenuItem.new_with_mnemonic(_('Save _As'))
menuitem.connect('activate',
gtkgui_helpers.on_avatar_save_as_menuitem_activate,
self.contact.jid, self.contact.get_shown_name())
self.contact.avatar_sha, self.contact.get_shown_name())
menu.append(menuitem)
menu.connect('selection-done', lambda w:w.destroy())
# show the menu

View File

@ -397,24 +397,6 @@ if dbus_support.supported:
if gajim.events.get_nb_events():
gajim.interface.systray.handle_first_event()
@dbus.service.method(INTERFACE, in_signature='s', out_signature='a{sv}')
def contact_info(self, jid):
'''get vcard info for a contact. Return cached value of the vcard.
'''
if not isinstance(jid, unicode):
jid = unicode(jid)
if not jid:
raise MissingArgument
return DBUS_DICT_SV()
jid = self._get_real_jid(jid)
cached_vcard = list(gajim.connections.values())[0].get_cached_vcard(jid)
if cached_vcard:
return get_dbus_struct(cached_vcard)
# return empty dict
return DBUS_DICT_SV()
@dbus.service.method(INTERFACE, in_signature='', out_signature='as')
def list_accounts(self):
'''list register accounts'''

View File

@ -47,7 +47,7 @@ class MockConnection(Mock, ConnectionHandlers):
app.connections[account] = self
def request_vcard(self, jid):
def request_vcard(self, *args):
pass
class MockWindow(Mock):