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 nbxmpp.protocol import NS_CHATSTATES
from gajim.common.connection_handlers_events import MessageOutgoingEvent from gajim.common.connection_handlers_events import MessageOutgoingEvent
from gajim.common.exceptions import GajimGeneralException from gajim.common.exceptions import GajimGeneralException
from gajim.common.const import AvatarSize
from gajim.command_system.implementation.hosts import ChatCommands from gajim.command_system.implementation.hosts import ChatCommands
@ -197,8 +198,7 @@ class ChatControl(ChatControlBase):
self.handlers[id_] = message_tv_buffer self.handlers[id_] = message_tv_buffer
widget = self.xml.get_object('avatar_eventbox') widget = self.xml.get_object('avatar_eventbox')
widget.set_property('height-request', app.config.get( widget.set_property('height-request', AvatarSize.CHAT)
'chat_avatar_height'))
id_ = widget.connect('enter-notify-event', id_ = widget.connect('enter-notify-event',
self.on_avatar_eventbox_enter_notify_event) self.on_avatar_eventbox_enter_notify_event)
self.handlers[id_] = widget self.handlers[id_] = widget
@ -296,8 +296,10 @@ class ChatControl(ChatControlBase):
app.ged.register_event_handler('pep-received', ged.GUI1, app.ged.register_event_handler('pep-received', ged.GUI1,
self._nec_pep_received) self._nec_pep_received)
app.ged.register_event_handler('vcard-received', ged.GUI1, if self.TYPE_ID == message_control.TYPE_CHAT:
self._nec_vcard_received) # 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, app.ged.register_event_handler('failed-decrypt', ged.GUI1,
self._nec_failed_decrypt) self._nec_failed_decrypt)
app.ged.register_event_handler('chatstate-received', ged.GUI1, 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 Enter the eventbox area so we under conditions add a timeout to show a
bigger avatar after 0.5 sec bigger avatar after 0.5 sec
""" """
jid = self.contact.jid avatar_pixbuf = app.interface.get_avatar(self.account, self.contact.jid)
avatar_pixbuf = gtkgui_helpers.get_avatar_pixbuf_from_cache(jid) if avatar_pixbuf is None:
if avatar_pixbuf in ('ask', None):
return return
avatar_w = avatar_pixbuf.get_width() avatar_w = avatar_pixbuf.get_width()
avatar_h = avatar_pixbuf.get_height() avatar_h = avatar_pixbuf.get_height()
@ -596,7 +597,7 @@ class ChatControl(ChatControlBase):
if self.show_bigger_avatar_timeout_id is not None: if self.show_bigger_avatar_timeout_id is not None:
GLib.source_remove(self.show_bigger_avatar_timeout_id) GLib.source_remove(self.show_bigger_avatar_timeout_id)
self.show_bigger_avatar_timeout_id = GLib.timeout_add(500, 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): def on_avatar_eventbox_leave_notify_event(self, widget, event):
""" """
@ -614,9 +615,15 @@ class ChatControl(ChatControlBase):
if event.button == 3: # right click if event.button == 3: # right click
menu = Gtk.Menu() menu = Gtk.Menu()
menuitem = Gtk.MenuItem.new_with_mnemonic(_('Save _As')) 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', id_ = menuitem.connect('activate',
gtkgui_helpers.on_avatar_save_as_menuitem_activate, gtkgui_helpers.on_avatar_save_as_menuitem_activate, sha, name)
self.contact.jid, self.contact.get_shown_name())
self.handlers[id_] = menuitem self.handlers[id_] = menuitem
menu.append(menuitem) menu.append(menuitem)
menu.show_all() menu.show_all()
@ -1076,10 +1083,8 @@ class ChatControl(ChatControlBase):
jid = self.contact.jid jid = self.contact.jid
if app.config.get('show_avatar_in_tabs'): if app.config.get('show_avatar_in_tabs'):
avatar_pixbuf = gtkgui_helpers.get_avatar_pixbuf_from_cache(jid) avatar_pixbuf = app.contacts.get_avatar(self.account, jid, size=16)
if avatar_pixbuf not in ('ask', None): if avatar_pixbuf is not None:
avatar_pixbuf = gtkgui_helpers.get_scaled_pixbuf_by_size(
avatar_pixbuf, 16, 16)
return avatar_pixbuf return avatar_pixbuf
if count_unread: if count_unread:
@ -1200,8 +1205,9 @@ class ChatControl(ChatControlBase):
app.ged.remove_event_handler('pep-received', ged.GUI1, app.ged.remove_event_handler('pep-received', ged.GUI1,
self._nec_pep_received) self._nec_pep_received)
app.ged.remove_event_handler('vcard-received', ged.GUI1, if self.TYPE_ID == message_control.TYPE_CHAT:
self._nec_vcard_received) app.ged.remove_event_handler('update-roster-avatar', ged.GUI1,
self._nec_update_avatar)
app.ged.remove_event_handler('failed-decrypt', ged.GUI1, app.ged.remove_event_handler('failed-decrypt', ged.GUI1,
self._nec_failed_decrypt) self._nec_failed_decrypt)
app.ged.remove_event_handler('chatstate-received', ged.GUI1, 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'): if not app.config.get('show_avatar_in_chat'):
return return
jid_with_resource = self.contact.get_full_jid() pixbuf = app.contacts.get_avatar(
pixbuf = gtkgui_helpers.get_avatar_pixbuf_from_cache(jid_with_resource) self.account, self.contact.jid, AvatarSize.CHAT)
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
image = self.xml.get_object('avatar_image') image = self.xml.get_object('avatar_image')
image.set_from_pixbuf(scaled_pixbuf) image.set_from_pixbuf(pixbuf)
image.show_all()
def _nec_vcard_received(self, obj): def _nec_update_avatar(self, obj):
if obj.conn.name != self.account: if obj.account != self.account:
return return
j = app.get_jid_without_resource(self.contact.jid) if obj.jid != self.contact.jid:
if obj.jid != j:
return return
self.show_avatar() self.show_avatar()
@ -1518,28 +1502,14 @@ class ChatControl(ChatControlBase):
elif typ == 'pm': elif typ == 'pm':
control.remove_contact(nick) 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 Resize the avatar, if needed, so it has at max half the screen size and
shows it 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 # 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') image = self.xml.get_object('avatar_image')
pixbuf = image.get_pixbuf() image.hide()
pixbuf.fill(0xffffff00) # RGBA
image.set_from_pixbuf(pixbuf)
#image.queue_draw()
screen_w = Gdk.Screen.width() screen_w = Gdk.Screen.width()
screen_h = Gdk.Screen.height() screen_h = Gdk.Screen.height()

View File

@ -34,8 +34,11 @@ import uuid
from distutils.version import LooseVersion as V from distutils.version import LooseVersion as V
import gi import gi
import nbxmpp 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 configpaths
from gajim.common import ged as ged_module from gajim.common import ged as ged_module
from gajim.common.contacts import LegacyContactsAPI 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) 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 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') version = config.get('version')
connections = {} # 'account name': 'account (connection.Connection) instance' connections = {} # 'account name': 'account (connection.Connection) instance'
avatar_cache = {}
ipython_window = None ipython_window = None
app = None # Gtk.Application 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_ROSTERX, nbxmpp.NS_SECLABEL, nbxmpp.NS_HASHES_2,
nbxmpp.NS_HASHES_MD5, nbxmpp.NS_HASHES_SHA1, nbxmpp.NS_HASHES_SHA256, nbxmpp.NS_HASHES_MD5, nbxmpp.NS_HASHES_SHA1, nbxmpp.NS_HASHES_SHA256,
nbxmpp.NS_HASHES_SHA512, nbxmpp.NS_CONFERENCE, nbxmpp.NS_CORRECT, 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 # Optional features gajim supports per account
gajim_optional_features = {} gajim_optional_features = {}

View File

@ -220,12 +220,6 @@ class Config:
'tabs_border': [opt_bool, False, _('Show tabbed notebook border in chat windows?')], 'tabs_border': [opt_bool, False, _('Show tabbed notebook border in chat windows?')],
'tabs_close_button': [opt_bool, True, _('Show close button in tab?')], '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.')], '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_online_color': [opt_color, '#73D216'],
'tooltip_status_free_for_chat_color': [opt_color, '#3465A4'], 'tooltip_status_free_for_chat_color': [opt_color, '#3465A4'],
'tooltip_status_away_color': [opt_color, '#EDD400'], 'tooltip_status_away_color': [opt_color, '#EDD400'],
@ -238,13 +232,9 @@ class Config:
'tooltip_affiliation_owner_color': [opt_color, '#CC0000'], 'tooltip_affiliation_owner_color': [opt_color, '#CC0000'],
'tooltip_account_name_color': [opt_color, '#888A85'], 'tooltip_account_name_color': [opt_color, '#888A85'],
'tooltip_idle_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_preview_message': [opt_bool, True, _('Preview new messages in notification popup?')],
'notification_position_x': [opt_int, -1], 'notification_position_x': [opt_int, -1],
'notification_position_y': [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.')], '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.')], '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.')], '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_tunes_in_roster': [opt_bool, True, '', True],
'show_location_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], '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_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.')], '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], 'log_contact_status_changes': [opt_bool, False],
@ -325,6 +314,7 @@ class Config:
'account_label': [ opt_str, '', '', False ], 'account_label': [ opt_str, '', '', False ],
'hostname': [ opt_str, '', '', True ], 'hostname': [ opt_str, '', '', True ],
'anonymous_auth': [ opt_bool, False ], 'anonymous_auth': [ opt_bool, False ],
'avatar_sha': [opt_str, '', '', False],
'client_cert': [ opt_str, '', '', True ], 'client_cert': [ opt_str, '', '', True ],
'client_cert_encrypted': [ opt_bool, False, '', False ], 'client_cert_encrypted': [ opt_bool, False, '', False ],
'savepass': [ opt_bool, False ], 'savepass': [ opt_bool, False ],

View File

@ -779,6 +779,7 @@ class Connection(CommonConnection, ConnectionHandlers):
self.connected = 0 self.connected = 0
self.time_to_reconnect = None self.time_to_reconnect = None
self.privacy_rules_supported = False self.privacy_rules_supported = False
self.avatar_presence_sent = False
if on_purpose: if on_purpose:
self.sm = Smacks(self) self.sm = Smacks(self)
if self.connection: if self.connection:
@ -1778,7 +1779,7 @@ class Connection(CommonConnection, ConnectionHandlers):
show='invisible')) show='invisible'))
if initial: if initial:
# ask our VCard # ask our VCard
self.request_vcard(None) self.request_vcard(self._on_own_avatar_received)
# Get bookmarks from private namespace # Get bookmarks from private namespace
self.get_bookmarks() 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.pep import LOCATION_DATA
from gajim.common import helpers from gajim.common import helpers
from gajim.common import app from gajim.common import app
from gajim.common import exceptions
from gajim.common import dataforms from gajim.common import dataforms
from gajim.common import jingle_xtls from gajim.common import jingle_xtls
from gajim.common import sleepy
from gajim.common.commands import ConnectionCommands from gajim.common.commands import ConnectionCommands
from gajim.common.pubsub import ConnectionPubSub from gajim.common.pubsub import ConnectionPubSub
from gajim.common.protocol.caps import ConnectionCaps from gajim.common.protocol.caps import ConnectionCaps
@ -66,8 +64,6 @@ import logging
log = logging.getLogger('gajim.c.connection_handlers') log = logging.getLogger('gajim.c.connection_handlers')
# kind of events we can wait for an answer # kind of events we can wait for an answer
VCARD_PUBLISHED = 'vcard_published'
VCARD_ARRIVED = 'vcard_arrived'
AGENT_REMOVED = 'agent_removed' AGENT_REMOVED = 'agent_removed'
METACONTACTS_ARRIVED = 'metacontacts_arrived' METACONTACTS_ARRIVED = 'metacontacts_arrived'
ROSTER_ARRIVED = 'roster_arrived' ROSTER_ARRIVED = 'roster_arrived'
@ -262,15 +258,96 @@ class ConnectionDisco:
class ConnectionVcard: class ConnectionVcard:
def __init__(self): def __init__(self):
self.vcard_sha = None self.own_vcard = None
self.vcard_shas = {} # sha of contacts
# list of gc jids so that vcard are saved in a folder
self.room_jids = [] 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): def add_sha(self, p, send_caps=True):
c = p.setTag('x', namespace=nbxmpp.NS_VCARD_UPDATE) c = p.setTag('x', namespace=nbxmpp.NS_VCARD_UPDATE)
if self.vcard_sha is not None: sha = app.config.get_per('accounts', self.name, 'avatar_sha')
c.setTagData('photo', self.vcard_sha) app.log('avatar').info(
'%s: Send avatar presence: %s', self.name, sha or 'empty')
c.setTagData('photo', sha)
if send_caps: if send_caps:
return self._add_caps(p) return self._add_caps(p)
return p return p
@ -283,6 +360,14 @@ class ConnectionVcard:
c.setAttr('ver', app.caps_hash[self.name]) c.setAttr('ver', app.caps_hash[self.name])
return p 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): def _node_to_dict(self, node):
dict_ = {} dict_ = {}
for info in node.getChildren(): for info in node.getChildren():
@ -301,99 +386,27 @@ class ConnectionVcard:
dict_[name][c.getName()] = c.getData() dict_[name][c.getName()] = c.getData()
return dict_ return dict_
def _save_vcard_to_hd(self, full_jid, card): def request_vcard(self, callback, jid=None, room=False):
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):
""" """
Request the VCARD 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: if not self.connection or self.connected < 2:
return 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') iq = nbxmpp.Iq(typ='get')
if jid: if jid:
iq.setTo(jid) iq.setTo(jid)
iq.setQuery('vCard').setNamespace(nbxmpp.NS_VCARD) iq.setQuery('vCard').setNamespace(nbxmpp.NS_VCARD)
id_ = self.connection.getAnID() self.connection.SendAndCallForResponse(
iq.setID(id_) iq, self._parse_vcard, {'callback': callback})
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)
def send_vcard(self, vcard): def send_vcard(self, vcard, sha):
if not self.connection or self.connected < 2: if not self.connection or self.connected < 2:
return return
iq = nbxmpp.Iq(typ='set') iq = nbxmpp.Iq(typ='set')
@ -413,23 +426,29 @@ class ConnectionVcard:
else: else:
iq2.addChild(i).setData(vcard[i]) iq2.addChild(i).setData(vcard[i])
id_ = self.connection.getAnID() self.connection.SendAndCallForResponse(
iq.setID(id_) iq, self._avatar_publish_result, {'sha': sha})
self.connection.send(iq)
our_jid = app.get_jid_from_account(self.name) def _avatar_publish_result(self, con, stanza, sha):
# Add the sha of the avatar if stanza.getType() == 'result':
if 'PHOTO' in vcard and isinstance(vcard['PHOTO'], dict) and \ current_sha = app.config.get_per(
'BINVAL' in vcard['PHOTO']: 'accounts', self.name, 'avatar_sha')
photo = vcard['PHOTO']['BINVAL'] if (current_sha != sha and
photo_decoded = base64.b64decode(photo.encode('utf-8')) app.SHOW_LIST[self.connected] != 'invisible'):
app.interface.save_avatar_files(our_jid, photo_decoded) if not self.connection or self.connected < 2:
avatar_sha = hashlib.sha1(photo_decoded).hexdigest() return
iq2.getTag('PHOTO').setTagData('SHA', avatar_sha) app.config.set_per(
else: 'accounts', self.name, 'avatar_sha', sha or '')
app.interface.remove_avatar_files(our_jid) 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): def _IqCB(self, con, iq_obj):
id_ = iq_obj.getID() id_ = iq_obj.getID()
@ -449,63 +468,7 @@ class ConnectionVcard:
if id_ not in self.awaiting_answers: if id_ not in self.awaiting_answers:
return return
if self.awaiting_answers[id_][0] == VCARD_PUBLISHED: if self.awaiting_answers[id_][0] == AGENT_REMOVED:
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:
jid = self.awaiting_answers[id_][1] jid = self.awaiting_answers[id_][1]
app.nec.push_incoming_event(AgentRemovedEvent(None, conn=self, app.nec.push_incoming_event(AgentRemovedEvent(None, conn=self,
agent=jid)) agent=jid))
@ -598,97 +561,104 @@ class ConnectionVcard:
app.nec.push_incoming_event(PEPConfigReceivedEvent(None, app.nec.push_incoming_event(PEPConfigReceivedEvent(None,
conn=self, node=node, form=form)) conn=self, node=node, form=form))
def _vCardCB(self, con, vc): def get_vcard_photo(self, vcard):
""" try:
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']:
photo = vcard['PHOTO']['BINVAL'] 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')) photo_decoded = base64.b64decode(photo.encode('utf-8'))
avatar_sha = hashlib.sha1(photo_decoded).hexdigest() 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: else:
avatar_sha = '' app.interface.save_avatar(photo_decoded)
if avatar_sha: app.config.set_per('accounts', self.name, 'avatar_sha', avatar_sha)
card.getTag('PHOTO').setTagData('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.send_avatar_presence()
self._save_vcard_to_hd(who, card) self.avatar_presence_sent = True
# Save the decoded avatar to a separate file too, and generate files
# for dbus notifications def _on_avatar_received(self, jid, resource, room, vcard):
puny_jid = helpers.sanitize_filename(frm) """
puny_nick = None Called when we receive a vCard Parse the vCard and trigger Events
begin_path = os.path.join(app.AVATAR_PATH, puny_jid) """
frm_jid = frm avatar_sha, photo_decoded = self.get_vcard_photo(vcard)
if frm in self.room_jids: app.interface.save_avatar(photo_decoded)
puny_nick = helpers.sanitize_filename(resource)
# create folder if needed # Received vCard from a contact
if not os.path.isdir(begin_path): if room:
os.mkdir(begin_path, 0o700) app.log('avatar').info(
begin_path = os.path.join(begin_path, puny_nick) 'Received (vCard): %s %s', resource, avatar_sha)
frm_jid += '/' + resource contact = app.contacts.get_gc_contact(self.name, jid, resource)
if photo_decoded: if contact is not None:
avatar_file = begin_path + '_notif_size_colored.png' contact.avatar_sha = avatar_sha
if frm_jid == our_jid and avatar_sha != self.vcard_sha: app.interface.update_avatar(contact=contact)
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]
else: else:
for ext in ('.jpeg', '.png', '_notif_size_bw.png', app.log('avatar').info('Received (vCard): %s %s', jid, avatar_sha)
'_notif_size_colored.png'): own_jid = self.get_own_jid().getStripped()
path = begin_path + ext app.logger.set_avatar_sha(own_jid, jid, avatar_sha)
if os.path.isfile(path): app.contacts.set_avatar(self.name, jid, avatar_sha)
os.remove(path) app.interface.update_avatar(self.name, jid)
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)
class ConnectionPEP(object): class ConnectionPEP(object):
@ -940,18 +910,6 @@ class ConnectionHandlersBase:
obj.contact = c obj.contact = c
break 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:
if obj.contact.show in statuss: if obj.contact.show in statuss:
obj.old_show = statuss.index(obj.contact.show) obj.old_show = statuss.index(obj.contact.show)
@ -2007,6 +1965,7 @@ ConnectionHandlersBase, ConnectionJingle, ConnectionIBBytestream):
if obj.conn.name != self.name: if obj.conn.name != self.name:
return return
our_jid = app.get_jid_from_account(self.name) our_jid = app.get_jid_from_account(self.name)
if self.connected > 1 and self.continue_connect_info: if self.connected > 1 and self.continue_connect_info:
msg = self.continue_connect_info[1] msg = self.continue_connect_info[1]
sign_msg = self.continue_connect_info[2] sign_msg = self.continue_connect_info[2]
@ -2043,7 +2002,7 @@ ConnectionHandlersBase, ConnectionJingle, ConnectionIBBytestream):
app.nec.push_incoming_event(RosterInfoEvent(None, app.nec.push_incoming_event(RosterInfoEvent(None,
conn=self, jid=jid, nickname=info['name'], conn=self, jid=jid, nickname=info['name'],
sub=info['subscription'], ask=info['ask'], sub=info['subscription'], ask=info['ask'],
groups=info['groups'])) groups=info['groups'], avatar_sha=info['avatar_sha']))
def _send_first_presence(self, signed=''): def _send_first_presence(self, signed=''):
show = self.continue_connect_info[0] show = self.continue_connect_info[0]
@ -2065,12 +2024,7 @@ ConnectionHandlersBase, ConnectionJingle, ConnectionIBBytestream):
if show not in ['offline', 'online', 'chat', 'away', 'xa', 'dnd']: if show not in ['offline', 'online', 'chat', 'away', 'xa', 'dnd']:
return return
priority = app.get_priority(self.name, sshow) 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 = nbxmpp.Presence(typ=None, priority=priority, show=sshow)
p = self.add_sha(p)
if msg: if msg:
p.setStatus(msg) p.setStatus(msg)
if signed: if signed:
@ -2083,7 +2037,7 @@ ConnectionHandlersBase, ConnectionJingle, ConnectionIBBytestream):
show=show)) show=show))
if self.vcard_supported: if self.vcard_supported:
# ask our VCard # ask our VCard
self.request_vcard(None) self.request_vcard(self._on_own_avatar_received)
# Get bookmarks from private namespace # Get bookmarks from private namespace
self.get_bookmarks() self.get_bookmarks()
@ -2169,7 +2123,6 @@ ConnectionHandlersBase, ConnectionJingle, ConnectionIBBytestream):
# We also don't check for namespace, else it cannot stop _messageCB to # We also don't check for namespace, else it cannot stop _messageCB to
# be called # be called
con.RegisterHandler('message', self._pubsubEventCB, makefirst=True) 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._rosterSetCB, 'set', nbxmpp.NS_ROSTER)
con.RegisterHandler('iq', self._siSetCB, 'set', nbxmpp.NS_SI) con.RegisterHandler('iq', self._siSetCB, 'set', nbxmpp.NS_SI)
con.RegisterHandler('iq', self._rosterItemExchangeCB, 'set', con.RegisterHandler('iq', self._rosterItemExchangeCB, 'set',

View File

@ -24,6 +24,7 @@
from calendar import timegm from calendar import timegm
import datetime import datetime
import hashlib import hashlib
import base64
import hmac import hmac
import logging import logging
import sys import sys
@ -324,6 +325,7 @@ class RosterReceivedEvent(nec.NetworkIncomingEvent):
self.conn.connection.getRoster().delItem(jid) self.conn.connection.getRoster().delItem(jid)
elif jid != our_jid: # don't add our jid elif jid != our_jid: # don't add our jid
self.roster[j] = raw_roster[jid] self.roster[j] = raw_roster[jid]
self.roster[j]['avatar_sha'] = None
else: else:
# Roster comes from DB # Roster comes from DB
self.received_from_server = False self.received_from_server = False
@ -376,6 +378,9 @@ class RosterInfoEvent(nec.NetworkIncomingEvent):
name = 'roster-info' name = 'roster-info'
base_network_events = [] base_network_events = []
def init(self):
self.avatar_sha = None
class MucOwnerReceivedEvent(nec.NetworkIncomingEvent, HelperEvent): class MucOwnerReceivedEvent(nec.NetworkIncomingEvent, HelperEvent):
name = 'muc-owner-received' name = 'muc-owner-received'
base_network_events = [] base_network_events = []
@ -532,19 +537,13 @@ class PubsubReceivedEvent(nec.NetworkIncomingEvent):
base_network_events = [] base_network_events = []
def generate(self): def generate(self):
self.jid = self.stanza.getFrom()
self.pubsub_node = self.stanza.getTag('pubsub') self.pubsub_node = self.stanza.getTag('pubsub')
if not self.pubsub_node: if not self.pubsub_node:
return return
self.items_node = self.pubsub_node.getTag('items') self.items_node = self.pubsub_node.getTag('items')
if not self.items_node: if not self.items_node:
return 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 return True
class PubsubBookmarksReceivedEvent(nec.NetworkIncomingEvent, BookmarksHelper): class PubsubBookmarksReceivedEvent(nec.NetworkIncomingEvent, BookmarksHelper):
@ -553,13 +552,51 @@ class PubsubBookmarksReceivedEvent(nec.NetworkIncomingEvent, BookmarksHelper):
def generate(self): def generate(self):
self.conn = self.base_event.conn 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() ns = self.storage_node.getNamespace()
if ns != nbxmpp.NS_BOOKMARKS: if ns != nbxmpp.NS_BOOKMARKS:
return return
self.parse_bookmarks() self.parse_bookmarks()
return True 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): class SearchFormReceivedEvent(nec.NetworkIncomingEvent, HelperEvent):
name = 'search-form-received' name = 'search-form-received'
base_network_events = [] base_network_events = []
@ -874,10 +911,8 @@ class GcPresenceReceivedEvent(nec.NetworkIncomingEvent, HelperEvent):
contact_name=fjid.getResource(), contact_name=fjid.getResource(),
message=st, message=st,
show=show) 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. # NOTE: if it's a gc presence, don't ask vcard here.
# We may ask it to real jid in gui part. # We may ask it to real jid in gui part.
self.status_code = [] self.status_code = []
@ -2004,16 +2039,20 @@ class VcardReceivedEvent(nec.NetworkIncomingEvent):
base_network_events = [] base_network_events = []
def generate(self): def generate(self):
self.nickname = None return True
if 'NICKNAME' in self.vcard_dict:
self.nickname = self.vcard_dict['NICKNAME'] class UpdateGCAvatarEvent(nec.NetworkIncomingEvent):
elif 'FN' in self.vcard_dict: name = 'update-gc-avatar'
self.nickname = self.vcard_dict['FN'] base_network_events = []
self.jid = self.vcard_dict['jid']
self.resource = self.vcard_dict['resource'] def generate(self):
self.fjid = self.jid return True
if self.resource:
self.fjid += '/' + self.resource class UpdateRosterAvatarEvent(nec.NetworkIncomingEvent):
name = 'update-roster-avatar'
base_network_events = []
def generate(self):
return True return True
class PEPConfigReceivedEvent(nec.NetworkIncomingEvent): class PEPConfigReceivedEvent(nec.NetworkIncomingEvent):

View File

@ -28,6 +28,14 @@ class OptionType(IntEnum):
ACTION = 3 ACTION = 3
DIALOG = 4 DIALOG = 4
class AvatarSize(IntEnum):
ROSTER = 32
NOTIFICATION = 48
CHAT = 52
PROFILE = 64
TOOLTIP = 125
VCARD = 200
THANKS = u"""\ THANKS = u"""\
Alexander Futász Alexander Futász

View File

@ -94,7 +94,7 @@ class Contact(CommonContact):
""" """
def __init__(self, jid, account, name='', groups=None, show='', status='', def __init__(self, jid, account, name='', groups=None, show='', status='',
sub='', ask='', resource='', priority=0, keyID='', client_caps=None, 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): if not isinstance(jid, str):
print('no str') print('no str')
if groups is None: if groups is None:
@ -105,6 +105,7 @@ class Contact(CommonContact):
self.contact_name = '' # nick choosen by contact self.contact_name = '' # nick choosen by contact
self.groups = [i if i else _('General') for i in set(groups)] # filter duplicate values self.groups = [i if i else _('General') for i in set(groups)] # filter duplicate values
self.avatar_sha = avatar_sha
self.sub = sub self.sub = sub
self.ask = ask self.ask = ask
@ -182,7 +183,7 @@ class GC_Contact(CommonContact):
def __init__(self, room_jid, account, name='', show='', status='', role='', def __init__(self, room_jid, account, name='', show='', status='', role='',
affiliation='', jid='', resource='', our_chatstate=None, affiliation='', jid='', resource='', our_chatstate=None,
chatstate=None): chatstate=None, avatar_sha=None):
CommonContact.__init__(self, jid, account, resource, show, status, name, CommonContact.__init__(self, jid, account, resource, show, status, name,
our_chatstate, chatstate) our_chatstate, chatstate)
@ -190,6 +191,7 @@ class GC_Contact(CommonContact):
self.room_jid = room_jid self.room_jid = room_jid
self.role = role self.role = role
self.affiliation = affiliation self.affiliation = affiliation
self.avatar_sha = avatar_sha
def get_full_jid(self): def get_full_jid(self):
return self.room_jid + '/' + self.name return self.room_jid + '/' + self.name
@ -197,13 +199,16 @@ class GC_Contact(CommonContact):
def get_shown_name(self): def get_shown_name(self):
return self.name return self.name
def get_avatar(self, size=None):
return common.app.interface.get_avatar(self.avatar_sha, size)
def as_contact(self): def as_contact(self):
""" """
Create a Contact instance from this GC_Contact instance Create a Contact instance from this GC_Contact instance
""" """
return Contact(jid=self.get_full_jid(), account=self.account, return Contact(jid=self.get_full_jid(), account=self.account,
name=self.name, groups=[], show=self.show, status=self.status, 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: class LegacyContactsAPI:
@ -245,7 +250,8 @@ class LegacyContactsAPI:
def create_contact(self, jid, account, name='', groups=None, show='', def create_contact(self, jid, account, name='', groups=None, show='',
status='', sub='', ask='', resource='', priority=0, keyID='', 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: if groups is None:
groups = [] groups = []
# Use Account object if available # Use Account object if available
@ -254,7 +260,7 @@ class LegacyContactsAPI:
show=show, status=status, sub=sub, ask=ask, resource=resource, show=show, status=status, sub=sub, ask=ask, resource=resource,
priority=priority, keyID=keyID, client_caps=client_caps, priority=priority, keyID=keyID, client_caps=client_caps,
our_chatstate=our_chatstate, chatstate=chatstate, 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, def create_self_contact(self, jid, account, resource, show, status, priority,
name='', keyID=''): name='', keyID=''):
@ -283,7 +289,7 @@ class LegacyContactsAPI:
resource=contact.resource, priority=contact.priority, resource=contact.resource, priority=contact.priority,
keyID=contact.keyID, client_caps=contact.client_caps, keyID=contact.keyID, client_caps=contact.client_caps,
our_chatstate=contact.our_chatstate, chatstate=contact.chatstate, 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): def add_contact(self, account, contact):
if account not in self._accounts: if account not in self._accounts:
@ -306,6 +312,15 @@ class LegacyContactsAPI:
def get_contact(self, account, jid, resource=None): def get_contact(self, account, jid, resource=None):
return self._accounts[account].contacts.get_contact(jid, resource=resource) 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): def iter_contacts(self, account):
for contact in self._accounts[account].contacts.iter_contacts(): for contact in self._accounts[account].contacts.iter_contacts():
yield contact yield contact
@ -395,10 +410,10 @@ class LegacyContactsAPI:
raise AttributeError(attr_name) raise AttributeError(attr_name)
def create_gc_contact(self, room_jid, account, name='', show='', status='', 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 account = self._accounts.get(account, account) # Use Account object if available
return GC_Contact(room_jid, account, name, show, status, role, affiliation, jid, 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): def add_gc_contact(self, account, gc_contact):
return self._accounts[account].gc_contacts.add_gc_contact(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): 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) 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(): class Contacts():
""" """
@ -490,6 +511,33 @@ class Contacts():
return c return c
return self._contacts[jid][0] 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): def iter_contacts(self):
for jid in list(self._contacts.keys()): for jid in list(self._contacts.keys()):
for contact in self._contacts[jid][:]: 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) status = reduce_chars_newlines(account['status_line'], 100, 1)
return status 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): def datetime_tuple(timestamp):
""" """
Convert timestamp using strptime and the format: %Y%m%dT%H:%M:%S 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: if account:
s = _('Hello, I am $name.') + ' ' + s s = _('Hello, I am $name.') + ' ' + s
our_jid = app.get_jid_from_account(account) our_jid = app.get_jid_from_account(account)
vcard = app.connections[account].get_cached_vcard(our_jid) vcard = app.connections[account].own_vcard
name = '' name = ''
if vcard: if vcard:
if 'N' in 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 exceptions
from gajim.common import app from gajim.common import app
from gajim.common import ged
import sqlite3 as sqlite import sqlite3 as sqlite
@ -998,7 +997,7 @@ class Logger:
# First we fill data with roster_entry informations # First we fill data with roster_entry informations
self.cur.execute(''' 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 FROM roster_entry re, jids j
WHERE re.account_jid_id=? AND j.jid_id=re.jid_id''', (account_jid_id,)) WHERE re.account_jid_id=? AND j.jid_id=re.jid_id''', (account_jid_id,))
for row in self.cur: for row in self.cur:
@ -1006,6 +1005,7 @@ class Logger:
jid = row.jid jid = row.jid
name = row.name name = row.name
data[jid] = {} data[jid] = {}
data[jid]['avatar_sha'] = row.avatar_sha
if name: if name:
data[jid]['name'] = name data[jid]['name'] = name
else: else:
@ -1135,3 +1135,25 @@ class Logger:
self._timeout_commit() self._timeout_commit()
return lastrowid 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) self._update_contacts(jid, account)
if jid == app.get_jid_from_account(account): if jid == app.get_jid_from_account(account):
self._update_account(account) self._update_account(account)
self._on_receive(jid, account)
def _extract_info(self, items): def _extract_info(self, items):
'''To be implemented by subclasses''' '''To be implemented by subclasses'''
@ -269,6 +270,10 @@ class AbstractPEP(object):
'''SHOULD be implemented by subclasses''' '''SHOULD be implemented by subclasses'''
return '' return ''
def _on_receive(self, jid, account):
'''SHOULD be implemented by subclasses'''
pass
class UserMoodPEP(AbstractPEP): class UserMoodPEP(AbstractPEP):
'''XEP-0107: User Mood''' '''XEP-0107: User Mood'''
@ -469,5 +474,32 @@ class UserLocationPEP(AbstractPEP):
return location_string.strip() return location_string.strip()
SUPPORTED_PERSONAL_USER_EVENTS = [UserMoodPEP, UserTunePEP, UserActivityPEP, class AvatarNotificationPEP(AbstractPEP):
UserNicknamePEP, UserLocationPEP] '''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 #from common.connection_handlers import PEP_CONFIG
PEP_CONFIG = 'pep_config' PEP_CONFIG = 'pep_config'
from gajim.common import ged 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 PubsubReceivedEvent
from gajim.common.connection_handlers_events import PubsubBookmarksReceivedEvent from gajim.common.connection_handlers_events import PubsubBookmarksReceivedEvent
from gajim.common.connection_handlers_events import PubsubAvatarReceivedEvent
import logging import logging
log = logging.getLogger('gajim.c.pubsub') log = logging.getLogger('gajim.c.pubsub')
@ -36,8 +39,11 @@ class ConnectionPubSub:
def __init__(self): def __init__(self):
self.__callbacks = {} self.__callbacks = {}
app.nec.register_incoming_event(PubsubBookmarksReceivedEvent) app.nec.register_incoming_event(PubsubBookmarksReceivedEvent)
app.nec.register_incoming_event(PubsubAvatarReceivedEvent)
app.ged.register_event_handler('pubsub-bookmarks-received', app.ged.register_event_handler('pubsub-bookmarks-received',
ged.CORE, self._nec_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): def cleanup(self):
app.ged.remove_event_handler('pubsub-bookmarks-received', app.ged.remove_event_handler('pubsub-bookmarks-received',
@ -97,7 +103,7 @@ class ConnectionPubSub:
self.connection.send(query) 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 Get items from a node
""" """
@ -106,6 +112,8 @@ class ConnectionPubSub:
query = nbxmpp.Iq('get', to=jid) query = nbxmpp.Iq('get', to=jid)
r = query.addChild('pubsub', namespace=nbxmpp.NS_PUBSUB) r = query.addChild('pubsub', namespace=nbxmpp.NS_PUBSUB)
r = r.addChild('items', {'node': node}) r = r.addChild('items', {'node': node})
if item_id is not None:
r.addChild('item', {'id': item_id})
id_ = self.connection.send(query) id_ = self.connection.send(query)
if cb: if cb:
@ -202,6 +210,28 @@ class ConnectionPubSub:
# We got bookmarks from pubsub, now get those from xml to merge them # We got bookmarks from pubsub, now get those from xml to merge them
self.get_bookmarks(storage_type='xml') 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): def _PubSubErrorCB(self, conn, stanza):
log.debug('_PubsubErrorCB') log.debug('_PubsubErrorCB')
pubsub = stanza.getTag('pubsub') 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', STATUS_LIST = ['offline', 'connecting', 'online', 'chat', 'away', 'xa', 'dnd',
'invisible'] 'invisible']
# kind of events we can wait for an answer # kind of events we can wait for an answer
VCARD_PUBLISHED = 'vcard_published'
VCARD_ARRIVED = 'vcard_arrived'
AGENT_REMOVED = 'agent_removed' AGENT_REMOVED = 'agent_removed'
from gajim.common import connection_handlers from gajim.common import connection_handlers
class ConnectionVcard(connection_handlers.ConnectionVcard): class ConnectionVcard(connection_handlers.ConnectionVcard):
def add_sha(self, p, send_caps = True): def add_sha(self, p, *args):
return p return p
def add_caps(self, p): def add_caps(self, p):
return p return p
def request_vcard(self, jid = None, is_fake_jid = False): def request_vcard(self, *args):
pass pass
def send_vcard(self, vcard): def send_vcard(self, *args):
pass pass

View File

@ -108,15 +108,6 @@
<signal name="activate" handler="on_assign_openpgp_key_menuitem_activate" swapped="no"/> <signal name="activate" handler="on_assign_openpgp_key_menuitem_activate" swapped="no"/>
</object> </object>
</child> </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> <child>
<object class="GtkMenuItem" id="add_special_notification_menuitem"> <object class="GtkMenuItem" id="add_special_notification_menuitem">
<property name="can_focus">False</property> <property name="can_focus">False</property>

View File

@ -30,6 +30,7 @@
import os import os
import time import time
import locale import locale
from gi.repository import Gtk from gi.repository import Gtk
from gi.repository import Gdk from gi.repository import Gdk
from gi.repository import GdkPixbuf from gi.repository import GdkPixbuf
@ -44,6 +45,7 @@ from gajim import config
from gajim import vcard from gajim import vcard
from gajim import cell_renderer_image from gajim import cell_renderer_image
from gajim import dataforms_widget from gajim import dataforms_widget
from gajim.common.const import AvatarSize
import nbxmpp import nbxmpp
from enum import IntEnum, unique 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.command_system.implementation.hosts import GroupChatCommands
from gajim.common.connection_handlers_events import GcMessageOutgoingEvent from gajim.common.connection_handlers_events import GcMessageOutgoingEvent
import logging import logging
log = logging.getLogger('gajim.groupchat_control') 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 == \ if parent_iter and (model[iter_][Column.AVATAR] or avatar_position == \
'left'): 'left'):
renderer.set_property('visible', True) renderer.set_property('visible', True)
renderer.set_property('width', app.config.get( renderer.set_property('width', AvatarSize.ROSTER)
'roster_avatar_width'))
else: else:
renderer.set_property('visible', False) renderer.set_property('visible', False)
if parent_iter: if parent_iter:
@ -141,6 +143,8 @@ class PrivateChatControl(ChatControl):
self.gc_contact = gc_contact self.gc_contact = gc_contact
ChatControl.__init__(self, parent_win, contact, account, session) ChatControl.__init__(self, parent_win, contact, account, session)
self.TYPE_ID = 'pm' 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, app.ged.register_event_handler('caps-received', ged.GUI1,
self._nec_caps_received_pm) self._nec_caps_received_pm)
app.ged.register_event_handler('gc-presence-received', ged.GUI1, app.ged.register_event_handler('gc-presence-received', ged.GUI1,
@ -151,6 +155,8 @@ class PrivateChatControl(ChatControl):
def shutdown(self): def shutdown(self):
super(PrivateChatControl, self).shutdown() 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, app.ged.remove_event_handler('caps-received', ged.GUI1,
self._nec_caps_received_pm) self._nec_caps_received_pm)
app.ged.remove_event_handler('gc-presence-received', ged.GUI1, app.ged.remove_event_handler('gc-presence-received', ged.GUI1,
@ -240,6 +246,20 @@ class PrivateChatControl(ChatControl):
self.got_connected() self.got_connected()
ChatControl.update_ui(self) 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): def update_contact(self):
self.contact = self.gc_contact.as_contact() self.contact = self.gc_contact.as_contact()
@ -502,8 +522,8 @@ class GroupchatControl(ChatControlBase):
self._nec_gc_message_received) self._nec_gc_message_received)
app.ged.register_event_handler('vcard-published', ged.GUI1, app.ged.register_event_handler('vcard-published', ged.GUI1,
self._nec_vcard_published) self._nec_vcard_published)
app.ged.register_event_handler('vcard-received', ged.GUI1, app.ged.register_event_handler('update-gc-avatar', ged.GUI1,
self._nec_vcard_received) self._nec_update_avatar)
app.ged.register_event_handler('gc-subject-received', ged.GUI1, app.ged.register_event_handler('gc-subject-received', ged.GUI1,
self._nec_gc_subject_received) self._nec_gc_subject_received)
app.ged.register_event_handler('gc-config-changed-received', ged.GUI1, app.ged.register_event_handler('gc-config-changed-received', ged.GUI1,
@ -534,7 +554,8 @@ class GroupchatControl(ChatControlBase):
if widget.get_tooltip_window(): if widget.get_tooltip_window():
return return
widget.set_has_tooltip(True) 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) id_ = widget.connect('query-tooltip', self.query_tooltip)
self.handlers[id_] = widget self.handlers[id_] = widget
@ -1046,12 +1067,12 @@ class GroupchatControl(ChatControlBase):
status = obj.conn.status status = obj.conn.status
obj.conn.send_gc_status(self.nick, self.room_jid, show, status) obj.conn.send_gc_status(self.nick, self.room_jid, show, status)
def _nec_vcard_received(self, obj): def _nec_update_avatar(self, obj):
if obj.conn.name != self.account: if obj.contact.room_jid != self.room_jid:
return return
if obj.jid != self.room_jid: app.log('avatar').debug('Draw Groupchat Avatar: %s %s',
return obj.contact.name, obj.contact.avatar_sha)
self.draw_avatar(obj.resource) self.draw_avatar(obj.contact)
def _nec_gc_message_received(self, obj): def _nec_gc_message_received(self, obj):
if obj.room_jid != self.room_jid or obj.conn.name != self.account: 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.IMG] = image
self.model[iter_][Column.TEXT] = name 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'): if not app.config.get('show_avatars_in_roster'):
return return
iter_ = self.get_contact_iter(nick) iter_ = self.get_contact_iter(gc_contact.name)
if not iter_: if not iter_:
return return
fake_jid = self.room_jid + '/' + nick
pixbuf = gtkgui_helpers.get_avatar_pixbuf_from_cache(fake_jid) pixbuf = app.interface.get_avatar(gc_contact.avatar_sha, AvatarSize.ROSTER)
if pixbuf in ('ask', None): self.model[iter_][Column.AVATAR] = pixbuf or empty_pixbuf
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
def draw_role(self, role): def draw_role(self, role):
role_iter = self.get_role_iter(role) role_iter = self.get_role_iter(role)
@ -1755,31 +1770,6 @@ class GroupchatControl(ChatControlBase):
if obj.nick in self.gc_custom_colors: if obj.nick in self.gc_custom_colors:
self.gc_custom_colors[obj.new_nick] = \ self.gc_custom_colors[obj.new_nick] = \
self.gc_custom_colors[obj.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) self.print_conversation(s, 'info', graphics=False)
elif '321' in obj.status_code: elif '321' in obj.status_code:
s = _('%(nick)s has been removed from the room ' s = _('%(nick)s has been removed from the room '
@ -1831,7 +1821,7 @@ class GroupchatControl(ChatControlBase):
s = _('You are now known as %s') % nick s = _('You are now known as %s') % nick
self.print_conversation(s, 'info', graphics=False) self.print_conversation(s, 'info', graphics=False)
iter_ = self.add_contact_to_roster(obj.nick, obj.show, role, 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 newly_created = True
self.draw_all_roles() self.draw_all_roles()
if obj.status_code and '201' in obj.status_code: 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' % \ log.error('%s has an iter, but no gc_contact instance' % \
obj.nick) obj.nick)
return 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 actual_affiliation = gc_c.affiliation
if affiliation != actual_affiliation: if affiliation != actual_affiliation:
@ -1946,7 +1907,7 @@ class GroupchatControl(ChatControlBase):
self.print_conversation(st, graphics=False) self.print_conversation(st, graphics=False)
def add_contact_to_roster(self, nick, show, role, affiliation, status, 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) role_name = helpers.get_uf_role(role, plural=True)
resource = '' resource = ''
@ -1973,22 +1934,14 @@ class GroupchatControl(ChatControlBase):
gc_contact = app.contacts.create_gc_contact( gc_contact = app.contacts.create_gc_contact(
room_jid=self.room_jid, account=self.account, room_jid=self.room_jid, account=self.account,
name=nick, show=show, status=status, role=role, 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) 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_contact(nick)
self.draw_avatar(nick) self.draw_avatar(gc_contact)
# 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)
if nick == self.nick: # we became online if nick == self.nick: # we became online
self.got_connected() self.got_connected()
if self.list_treeview.get_model(): if self.list_treeview.get_model():
@ -2175,8 +2128,8 @@ class GroupchatControl(ChatControlBase):
self._nec_gc_message_received) self._nec_gc_message_received)
app.ged.remove_event_handler('vcard-published', ged.GUI1, app.ged.remove_event_handler('vcard-published', ged.GUI1,
self._nec_vcard_published) self._nec_vcard_published)
app.ged.remove_event_handler('vcard-received', ged.GUI1, app.ged.remove_event_handler('update-gc-avatar', ged.GUI1,
self._nec_vcard_received) self._nec_update_avatar)
app.ged.remove_event_handler('gc-subject-received', ged.GUI1, app.ged.remove_event_handler('gc-subject-received', ged.GUI1,
self._nec_gc_subject_received) self._nec_gc_subject_received)
app.ged.remove_event_handler('gc-config-changed-received', ged.GUI1, 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: else:
window.props.urgency_hint = False window.props.urgency_hint = False
# feeding the image directly into the pixbuf seems possible, but is error prone and causes image distortions and segfaults. def get_pixbuf_from_data(file_data):
# 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):
""" """
Get image data and returns GdkPixbuf.Pixbuf if want_type is True it also Get image data and returns GdkPixbuf.Pixbuf
returns 'jpeg', 'png' etc
""" """
pixbufloader = GdkPixbuf.PixbufLoader() pixbufloader = GdkPixbuf.PixbufLoader()
try: try:
pixbufloader.write(file_data) pixbufloader.write(file_data)
pixbufloader.close() pixbufloader.close()
pixbuf = pixbufloader.get_pixbuf() pixbuf = pixbufloader.get_pixbuf()
except GLib.GError: # 'unknown image format' except GLib.GError:
pixbufloader.close() pixbufloader.close()
# try to open and convert this image to png using pillow (if available) log.warning('loading avatar using pixbufloader failed, trying to '
log.debug("loading avatar using pixbufloader failed, trying to convert avatar image using pillow (if available)") 'convert avatar image using pillow')
try: try:
avatar = Image.open(BytesIO(file_data)).convert("RGBA") avatar = Image.open(BytesIO(file_data)).convert("RGBA")
arr = GLib.Bytes.new(avatar.tobytes()) array = GLib.Bytes.new(avatar.tobytes())
width, height = avatar.size width, height = avatar.size
pixbuf = GdkPixbuf.Pixbuf.new_from_bytes(arr, GdkPixbuf.Colorspace.RGB, True, 8, width, height, width * 4) pixbuf = GdkPixbuf.Pixbuf.new_from_bytes(
except: array, GdkPixbuf.Colorspace.RGB,
log.info("Could not use pillow to convert avatar image, image cannot be displayed") True, 8, width, height, width * 4)
if want_type: except Exception:
return None, None log.warning('Could not use pillow to convert avatar image, '
else: 'image cannot be displayed', exc_info=True)
return None return
if want_type: return pixbuf
typ = pixbufloader.get_format() and pixbufloader.get_format().get_name() or None
return pixbuf, typ
else:
return pixbuf
def get_cursor(attr): def get_cursor(attr):
display = Gdk.Display.get_default() 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, return Gdk.RGBA(bg.red*p + fg.red*q, bg.green*p + fg.green*q,
bg.blue*p + fg.blue*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): def make_gtk_month_python_month(month):
""" """
GTK starts counting months from 0, so January is 0 but Python's time start 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): def destroy_widget(widget):
widget.destroy() 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): def on_continue(response, file_path):
if response < 0: if response < 0:
return 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] extension = os.path.splitext(file_path)[1]
if not extension: if not extension:
# Silently save as Jpeg image # Silently save as Jpeg image
@ -577,7 +490,7 @@ def on_avatar_save_as_menuitem_activate(widget, jid, default_name=''):
try: try:
pixbuf.savev(file_path, image_format, [], []) pixbuf.savev(file_path, image_format, [], [])
except Exception as e: 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): if os.path.exists(file_path):
os.remove(file_path) os.remove(file_path)
new_file_path = '.'.join(file_path.split('.')[:-1]) + '.jpeg' new_file_path = '.'.join(file_path.split('.')[:-1]) + '.jpeg'

View File

@ -39,13 +39,17 @@ import sys
import re import re
import time import time
import math import math
from subprocess import Popen import hashlib
from gi.repository import Gtk from gi.repository import Gtk
from gi.repository import GdkPixbuf from gi.repository import GdkPixbuf
from gi.repository import GLib 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 app
from gajim.common import events from gajim.common import events
@ -82,12 +86,14 @@ from gajim.common import socks5
from gajim.common import helpers from gajim.common import helpers
from gajim.common import passwords from gajim.common import passwords
from gajim.common import logging_helpers from gajim.common import logging_helpers
from gajim.common.connection_handlers_events import OurShowEvent, \ from gajim.common.connection_handlers_events import (
FileRequestErrorEvent, FileTransferCompletedEvent OurShowEvent, FileRequestErrorEvent, FileTransferCompletedEvent,
UpdateRosterAvatarEvent, UpdateGCAvatarEvent)
from gajim.common.connection import Connection from gajim.common.connection import Connection
from gajim.common.file_props import FilesProp from gajim.common.file_props import FilesProp
from gajim.common import pep from gajim.common import pep
from gajim import emoticons from gajim import emoticons
from gajim.common.const import AvatarSize
from gajim import roster_window from gajim import roster_window
from gajim import profile_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 ( if account in self.show_vcard_when_connect and obj.show not in (
'offline', 'error'): 'offline', 'error'):
self.edit_own_details(account) self.edit_own_details(account)
self.show_vcard_when_connect.remove(self.name)
def edit_own_details(self, account): def edit_own_details(self, account):
jid = app.get_jid_from_account(account) jid = app.get_jid_from_account(account)
if 'profile' not in self.instances[account]: if 'profile' not in self.instances[account]:
self.instances[account]['profile'] = \ self.instances[account]['profile'] = \
profile_window.ProfileWindow(account, app.interface.roster.window) profile_window.ProfileWindow(account, app.interface.roster.window)
app.connections[account].request_vcard(jid)
@staticmethod @staticmethod
def handle_gc_error(gc_control, pritext, sectext): def handle_gc_error(gc_control, pritext, sectext):
@ -359,7 +365,6 @@ class Interface:
# priority, # keyID, timestamp, contact_nickname)) # priority, # keyID, timestamp, contact_nickname))
# #
# Contact changed show # Contact changed show
account = obj.conn.name account = obj.conn.name
jid = obj.jid jid = obj.jid
show = obj.show show = obj.show
@ -558,16 +563,6 @@ class Interface:
dialogs.ErrorDialog(_('Contact with "%s" cannot be established') % \ dialogs.ErrorDialog(_('Contact with "%s" cannot be established') % \
obj.agent, _('Check your connection or try again later.')) 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): def handle_event_gc_config(self, obj):
#('GC_CONFIG', account, (jid, form_node)) config is a dict #('GC_CONFIG', account, (jid, form_node)) config is a dict
account = obj.conn.name account = obj.conn.name
@ -780,7 +775,8 @@ class Interface:
keyID = attached_keys[attached_keys.index(obj.jid) + 1] keyID = attached_keys[attached_keys.index(obj.jid) + 1]
contact = app.contacts.create_contact(jid=obj.jid, contact = app.contacts.create_contact(jid=obj.jid,
account=account, name=obj.nickname, groups=obj.groups, 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) app.contacts.add_contact(account, contact)
self.roster.add_contact(obj.jid, account) self.roster.add_contact(obj.jid, account)
else: else:
@ -1554,7 +1550,6 @@ class Interface:
self.handle_event_subscribed_presence], self.handle_event_subscribed_presence],
'unsubscribed-presence-received': [ 'unsubscribed-presence-received': [
self.handle_event_unsubscribed_presence], self.handle_event_unsubscribed_presence],
'vcard-received': [self.handle_event_vcard],
'zeroconf-name-conflict': [self.handle_event_zc_name_conflict], 'zeroconf-name-conflict': [self.handle_event_zc_name_conflict],
} }
@ -2334,85 +2329,94 @@ class Interface:
sys.exit() sys.exit()
@staticmethod @staticmethod
def save_avatar_files(jid, photo, puny_nick = None, local = False): def update_avatar(account=None, jid=None, contact=None):
""" if contact is None:
Save an avatar to a separate file, and generate files for dbus app.nec.push_incoming_event(
notifications. An avatar can be given as a pixmap directly or as an UpdateRosterAvatarEvent(None, account=account, jid=jid))
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
else: else:
pixbuf, typ = gtkgui_helpers.get_pixbuf_from_data(photo, app.nec.push_incoming_event(
want_type=True) 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: if pixbuf is None:
return return
if typ not in ('jpeg', 'png'): pixbuf = pixbuf.scale_simple(AvatarSize.PROFILE,
log.info('gtkpixbuf cannot save other than jpeg and '\ AvatarSize.PROFILE,
'png formats. saving \'%s\' avatar as png file (originaly: %s)'\ GdkPixbuf.InterpType.BILINEAR)
% (jid, typ)) publish_path = os.path.join(app.AVATAR_PATH, 'temp_publish')
typ = 'png' pixbuf.savev(publish_path, 'png', [], [])
extension = '.' + typ with open(publish_path, 'rb') as file:
path_to_original_file = path_to_file + extension data = file.read()
return self.save_avatar(data)
sha = hashlib.sha1(data).hexdigest()
path = os.path.join(app.AVATAR_PATH, sha)
try: try:
pixbuf.savev(path_to_original_file, typ, [], []) with open(path, "wb") as output_file:
except Exception as e: output_file.write(data)
log.error('Error writing avatar file %s: %s' % ( except Exception:
path_to_original_file, str(e))) app.log('avatar').error('Saving avatar failed', exc_info=True)
# Generate and save the resized, color avatar return
pixbuf = gtkgui_helpers.get_scaled_pixbuf(pixbuf, 'notification')
if pixbuf: return sha
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)))
@staticmethod @staticmethod
def remove_avatar_files(jid, puny_nick = None, local = False): def get_avatar(filename, size=None, publish=False):
""" if filename is None or '':
Remove avatar files of a jid return
"""
puny_jid = helpers.sanitize_filename(jid) if publish:
path_to_file = os.path.join(app.AVATAR_PATH, puny_jid) path = os.path.join(app.AVATAR_PATH, filename)
if puny_nick: with open(path, 'rb') as file:
path_to_file = os.path.join(path_to_file, puny_nick) data = file.read()
for ext in ('.jpeg', '.png'): return data
if local:
ext = '_local' + ext try:
path_to_original_file = path_to_file + ext sha = app.avatar_cache[filename][size]
if os.path.isfile(path_to_file + ext): return sha
os.remove(path_to_file + ext) except KeyError:
if os.path.isfile(path_to_file + '_notif_size_colored' + ext): pass
os.remove(path_to_file + '_notif_size_colored' + ext)
if os.path.isfile(path_to_file + '_notif_size_bw' + ext): path = os.path.join(app.AVATAR_PATH, filename)
os.remove(path_to_file + '_notif_size_bw' + ext) 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): 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') unblock_menuitem = xml.get_object('unblock_menuitem')
ignore_menuitem = xml.get_object('ignore_menuitem') ignore_menuitem = xml.get_object('ignore_menuitem')
unignore_menuitem = xml.get_object('unignore_menuitem') unignore_menuitem = xml.get_object('unignore_menuitem')
set_custom_avatar_menuitem = xml.get_object('set_custom_avatar_menuitem')
# Subscription submenu # Subscription submenu
subscription_menuitem = xml.get_object('subscription_menuitem') subscription_menuitem = xml.get_object('subscription_menuitem')
send_auth_menuitem, ask_auth_menuitem, revoke_auth_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'): if app.config.get_per('accounts', account, 'is_zeroconf'):
for item in (send_custom_status_menuitem, send_single_message_menuitem, for item in (send_custom_status_menuitem, send_single_message_menuitem,
invite_menuitem, block_menuitem, unblock_menuitem, ignore_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): manage_contact_menuitem, convert_to_gc_menuitem):
item.set_no_show_all(True) item.set_no_show_all(True)
item.hide() item.hide()
@ -451,9 +450,6 @@ control=None, gc_contact=None, is_anonymous=True):
add_to_roster_menuitem.set_no_show_all(True) add_to_roster_menuitem.set_no_show_all(True)
subscription_menuitem.set_sensitive(False) 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 # Hide items when it's self contact row
if our_jid: if our_jid:
manage_contact_menuitem.set_sensitive(False) manage_contact_menuitem.set_sensitive(False)

View File

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

View File

@ -592,24 +592,6 @@ class SignalObject(dbus.service.Object):
return return
app.interface.handle_event(account, jid, event.type_) 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') @dbus.service.method(INTERFACE, in_signature='', out_signature='as')
def list_accounts(self): def list_accounts(self):
""" """
@ -891,17 +873,22 @@ class SignalObject(dbus.service.Object):
if not invalid_file and filesize < 16384: if not invalid_file and filesize < 16384:
with open(picture, 'rb') as fd: with open(picture, 'rb') as fd:
data = fd.read() 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 = base64.b64encode(data).decode('utf-8')
avatar_mime_type = mimetypes.guess_type(picture)[0] avatar_mime_type = mimetypes.guess_type(picture)[0]
vcard={} vcard = {}
vcard['PHOTO'] = {'BINVAL': avatar} vcard['PHOTO'] = {'BINVAL': avatar}
if avatar_mime_type: if avatar_mime_type:
vcard['PHOTO']['TYPE'] = avatar_mime_type vcard['PHOTO']['TYPE'] = avatar_mime_type
if account: if account:
app.connections[account].send_vcard(vcard) app.connections[account].send_vcard(vcard, sha)
else: else:
for acc in app.connections: 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='') @dbus.service.method(INTERFACE, in_signature='ssss', out_signature='')
def join_room(self, room_jid, nick, password, account): def join_room(self, room_jid, nick, password, account):

View File

@ -42,6 +42,7 @@ from gi.repository import Gio
import os import os
import time import time
import locale import locale
import hashlib
from enum import IntEnum, unique from enum import IntEnum, unique
@ -57,6 +58,7 @@ from gajim import cell_renderer_image
from gajim import tooltips from gajim import tooltips
from gajim import message_control from gajim import message_control
from gajim import adhoc_commands from gajim import adhoc_commands
from gajim.common.const import AvatarSize
from gajim.common import app from gajim.common import app
from gajim.common import helpers from gajim.common import helpers
@ -1361,13 +1363,12 @@ class RosterWindow:
if not iters or not app.config.get('show_avatars_in_roster'): if not iters or not app.config.get('show_avatars_in_roster'):
return return
jid = self.model[iters[0]][Column.JID] jid = self.model[iters[0]][Column.JID]
pixbuf = gtkgui_helpers.get_avatar_pixbuf_from_cache(jid)
if pixbuf in (None, 'ask'): pixbuf = app.contacts.get_avatar(account, jid, size=AvatarSize.ROSTER)
scaled_pixbuf = empty_pixbuf if pixbuf is None:
else: pixbuf = empty_pixbuf
scaled_pixbuf = gtkgui_helpers.get_scaled_pixbuf(pixbuf, 'roster')
for child_iter in iters: for child_iter in iters:
self.model[child_iter][Column.AVATAR_PIXBUF] = scaled_pixbuf self.model[child_iter][Column.AVATAR_PIXBUF] = pixbuf
return False return False
def draw_completely(self, jid, account): def draw_completely(self, jid, account):
@ -1885,6 +1886,7 @@ class RosterWindow:
array[self_jid] = {'name': app.nicks[account], array[self_jid] = {'name': app.nicks[account],
'groups': ['self_contact'], 'subscription': 'both', 'groups': ['self_contact'], 'subscription': 'both',
'ask': 'none'} 'ask': 'none'}
# .keys() is needed # .keys() is needed
for jid in list(array.keys()): for jid in list(array.keys()):
# Remove the contact in roster. It might has changed # Remove the contact in roster. It might has changed
@ -1918,25 +1920,6 @@ class RosterWindow:
ask=array[jid]['ask'], resource=resource, keyID=keyID) ask=array[jid]['ask'], resource=resource, keyID=keyID)
app.contacts.add_contact(account, contact1) 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 # If we already have chat windows opened, update them with new
# contact instance # contact instance
chat_control = app.interface.msg_win_mgr.get_control(ji, account) chat_control = app.interface.msg_win_mgr.get_control(ji, account)
@ -2591,11 +2574,6 @@ class RosterWindow:
# Update existing iter and group counting # Update existing iter and group counting
self.draw_contact(jid, account) self.draw_contact(jid, account)
self.draw_group(_('Transports'), 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: if obj.contact:
self.chg_contact_status(obj.contact, obj.show, obj.status, account) self.chg_contact_status(obj.contact, obj.show, obj.status, account)
@ -2638,10 +2616,11 @@ class RosterWindow:
resource = '' resource = ''
if app.connections[account].server_resource: if app.connections[account].server_resource:
resource = 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, contact = app.contacts.create_contact(jid=self_jid,
account=account, name=app.nicks[account], account=account, name=app.nicks[account],
groups=['self_contact'], show='offline', sub='both', groups=['self_contact'], show='offline', sub='both',
ask='none', resource=resource) ask='none', resource=resource, avatar_sha=sha)
app.contacts.add_contact(account, contact) app.contacts.add_contact(account, contact)
self.add_contact(self_jid, account) self.add_contact(self_jid, account)
if app.config.get('remember_opened_chat_controls'): if app.config.get('remember_opened_chat_controls'):
@ -2697,11 +2676,9 @@ class RosterWindow:
else: else:
self.draw_pep(obj.jid, obj.conn.name, obj.pep_type) self.draw_pep(obj.jid, obj.conn.name, obj.pep_type)
def _nec_vcard_received(self, obj): def _nec_update_avatar(self, obj):
if obj.resource: app.log('avatar').debug('Draw roster avatar: %s', obj.jid)
# it's a muc occupant vcard self.draw_avatar(obj.jid, obj.account)
return
self.draw_avatar(obj.jid, obj.conn.name)
def _nec_gc_subject_received(self, obj): def _nec_gc_subject_received(self, obj):
contact = app.contacts.get_contact_with_highest_priority( contact = app.contacts.get_contact_with_highest_priority(
@ -3053,46 +3030,6 @@ class RosterWindow:
_('Select a key to apply to the contact'), public_keys, _('Select a key to apply to the contact'), public_keys,
on_key_selected, selected=keyID, transient_for=self.window) 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_): def on_edit_groups(self, widget, list_):
dialogs.EditGroupsDialog(list_) dialogs.EditGroupsDialog(list_)
@ -3429,7 +3366,7 @@ class RosterWindow:
type_ = model[path][Column.TYPE] type_ = model[path][Column.TYPE]
# x_min is the x start position of status icon column # x_min is the x start position of status icon column
if app.config.get('avatar_position_in_roster') == 'left': if app.config.get('avatar_position_in_roster') == 'left':
x_min = app.config.get('roster_avatar_width') x_min = AvatarSize.ROSTER
else: else:
x_min = 0 x_min = 0
if app.single_click and not event.get_state() & Gdk.ModifierType.SHIFT_MASK and \ 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()): for ctrl in list(app.interface.minimized_controls[account].values()):
ctrl.repaint_themed_widgets() 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): def _iconCellDataFunc(self, column, renderer, model, titer, data=None):
""" """
When a row is added, set properties for icon renderer When a row is added, set properties for icon renderer
@ -4966,8 +4894,7 @@ class RosterWindow:
renderer.set_property('visible', False) renderer.set_property('visible', False)
if app.config.get('avatar_position_in_roster') == 'left': if app.config.get('avatar_position_in_roster') == 'left':
renderer.set_property('width', app.config.get( renderer.set_property('width', AvatarSize.ROSTER)
'roster_avatar_width'))
renderer.set_property('xalign', 0.5) renderer.set_property('xalign', 0.5)
else: else:
renderer.set_property('xalign', 1) # align pixbuf to the right renderer.set_property('xalign', 1) # align pixbuf to the right
@ -6063,8 +5990,8 @@ class RosterWindow:
self._nec_agent_removed) self._nec_agent_removed)
app.ged.register_event_handler('pep-received', ged.GUI1, app.ged.register_event_handler('pep-received', ged.GUI1,
self._nec_pep_received) self._nec_pep_received)
app.ged.register_event_handler('vcard-received', ged.GUI1, app.ged.register_event_handler('update-roster-avatar', ged.GUI1,
self._nec_vcard_received) self._nec_update_avatar)
app.ged.register_event_handler('gc-subject-received', ged.GUI1, app.ged.register_event_handler('gc-subject-received', ged.GUI1,
self._nec_gc_subject_received) self._nec_gc_subject_received)
app.ged.register_event_handler('metacontacts-received', ged.GUI2, app.ged.register_event_handler('metacontacts-received', ged.GUI2,

View File

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

View File

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

View File

@ -397,24 +397,6 @@ if dbus_support.supported:
if gajim.events.get_nb_events(): if gajim.events.get_nb_events():
gajim.interface.systray.handle_first_event() 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') @dbus.service.method(INTERFACE, in_signature='', out_signature='as')
def list_accounts(self): def list_accounts(self):
'''list register accounts''' '''list register accounts'''

View File

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