Commit a01cdbf2 authored by Philipp Hörist's avatar Philipp Hörist

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
parent 68f13788
......@@ -55,6 +55,7 @@ from nbxmpp.protocol import NS_JINGLE_ICE_UDP, NS_JINGLE_FILE_TRANSFER_5
from nbxmpp.protocol import NS_CHATSTATES
from gajim.common.connection_handlers_events import MessageOutgoingEvent
from gajim.common.exceptions import GajimGeneralException
from gajim.common.const import AvatarSize
from gajim.command_system.implementation.hosts import ChatCommands
......@@ -197,8 +198,7 @@ class ChatControl(ChatControlBase):
self.handlers[id_] = message_tv_buffer
widget = self.xml.get_object('avatar_eventbox')
widget.set_property('height-request', app.config.get(
'chat_avatar_height'))
widget.set_property('height-request', AvatarSize.CHAT)
id_ = widget.connect('enter-notify-event',
self.on_avatar_eventbox_enter_notify_event)
self.handlers[id_] = widget
......@@ -296,8 +296,10 @@ class ChatControl(ChatControlBase):
app.ged.register_event_handler('pep-received', ged.GUI1,
self._nec_pep_received)
app.ged.register_event_handler('vcard-received', ged.GUI1,
self._nec_vcard_received)
if self.TYPE_ID == message_control.TYPE_CHAT:
# Dont connect this when PrivateChatControl is used
app.ged.register_event_handler('update-roster-avatar', ged.GUI1,
self._nec_update_avatar)
app.ged.register_event_handler('failed-decrypt', ged.GUI1,
self._nec_failed_decrypt)
app.ged.register_event_handler('chatstate-received', ged.GUI1,
......@@ -579,9 +581,8 @@ class ChatControl(ChatControlBase):
Enter the eventbox area so we under conditions add a timeout to show a
bigger avatar after 0.5 sec
"""
jid = self.contact.jid
avatar_pixbuf = gtkgui_helpers.get_avatar_pixbuf_from_cache(jid)
if avatar_pixbuf in ('ask', None):
avatar_pixbuf = app.interface.get_avatar(self.account, self.contact.jid)
if avatar_pixbuf is None:
return
avatar_w = avatar_pixbuf.get_width()
avatar_h = avatar_pixbuf.get_height()
......@@ -596,7 +597,7 @@ class ChatControl(ChatControlBase):
if self.show_bigger_avatar_timeout_id is not None:
GLib.source_remove(self.show_bigger_avatar_timeout_id)
self.show_bigger_avatar_timeout_id = GLib.timeout_add(500,
self.show_bigger_avatar, widget)
self.show_bigger_avatar, widget, avatar_pixbuf)
def on_avatar_eventbox_leave_notify_event(self, widget, event):
"""
......@@ -614,9 +615,15 @@ class ChatControl(ChatControlBase):
if event.button == 3: # right click
menu = Gtk.Menu()
menuitem = Gtk.MenuItem.new_with_mnemonic(_('Save _As'))
if self.TYPE_ID == message_control.TYPE_CHAT:
sha = app.contacts.get_avatar_sha(
self.account, self.contact.jid)
name = self.contact.get_shown_name()
else:
sha = self.gc_contact.avatar_sha
name = self.gc_contact.get_shown_name()
id_ = menuitem.connect('activate',
gtkgui_helpers.on_avatar_save_as_menuitem_activate,
self.contact.jid, self.contact.get_shown_name())
gtkgui_helpers.on_avatar_save_as_menuitem_activate, sha, name)
self.handlers[id_] = menuitem
menu.append(menuitem)
menu.show_all()
......@@ -1076,10 +1083,8 @@ class ChatControl(ChatControlBase):
jid = self.contact.jid
if app.config.get('show_avatar_in_tabs'):
avatar_pixbuf = gtkgui_helpers.get_avatar_pixbuf_from_cache(jid)
if avatar_pixbuf not in ('ask', None):
avatar_pixbuf = gtkgui_helpers.get_scaled_pixbuf_by_size(
avatar_pixbuf, 16, 16)
avatar_pixbuf = app.contacts.get_avatar(self.account, jid, size=16)
if avatar_pixbuf is not None:
return avatar_pixbuf
if count_unread:
......@@ -1200,8 +1205,9 @@ class ChatControl(ChatControlBase):
app.ged.remove_event_handler('pep-received', ged.GUI1,
self._nec_pep_received)
app.ged.remove_event_handler('vcard-received', ged.GUI1,
self._nec_vcard_received)
if self.TYPE_ID == message_control.TYPE_CHAT:
app.ged.remove_event_handler('update-roster-avatar', ged.GUI1,
self._nec_update_avatar)
app.ged.remove_event_handler('failed-decrypt', ged.GUI1,
self._nec_failed_decrypt)
app.ged.remove_event_handler('chatstate-received', ged.GUI1,
......@@ -1322,37 +1328,15 @@ class ChatControl(ChatControlBase):
if not app.config.get('show_avatar_in_chat'):
return
jid_with_resource = self.contact.get_full_jid()
pixbuf = gtkgui_helpers.get_avatar_pixbuf_from_cache(jid_with_resource)
if pixbuf == 'ask':
# we don't have the vcard
if self.TYPE_ID == message_control.TYPE_PM:
if self.gc_contact.jid:
# We know the real jid of this contact
real_jid = self.gc_contact.jid
if self.gc_contact.resource:
real_jid += '/' + self.gc_contact.resource
else:
real_jid = jid_with_resource
app.connections[self.account].request_vcard(real_jid,
jid_with_resource)
else:
app.connections[self.account].request_vcard(jid_with_resource)
return
elif pixbuf:
scaled_pixbuf = gtkgui_helpers.get_scaled_pixbuf(pixbuf, 'chat')
else:
scaled_pixbuf = None
pixbuf = app.contacts.get_avatar(
self.account, self.contact.jid, AvatarSize.CHAT)
image = self.xml.get_object('avatar_image')
image.set_from_pixbuf(scaled_pixbuf)
image.show_all()
image.set_from_pixbuf(pixbuf)
def _nec_vcard_received(self, obj):
if obj.conn.name != self.account:
def _nec_update_avatar(self, obj):
if obj.account != self.account:
return
j = app.get_jid_without_resource(self.contact.jid)
if obj.jid != j:
if obj.jid != self.contact.jid:
return
self.show_avatar()
......@@ -1518,28 +1502,14 @@ class ChatControl(ChatControlBase):
elif typ == 'pm':
control.remove_contact(nick)
def show_bigger_avatar(self, small_avatar):
def show_bigger_avatar(self, small_avatar, avatar_pixbuf):
"""
Resize the avatar, if needed, so it has at max half the screen size and
shows it
"""
#if not small_avatar.window:
### Tab has been closed since we hovered the avatar
#return
avatar_pixbuf = gtkgui_helpers.get_avatar_pixbuf_from_cache(
self.contact.jid)
if avatar_pixbuf in ('ask', None):
return
# Hide the small avatar
# this code hides the small avatar when we show a bigger one in case
# the avatar has a transparency hole in the middle
# so when we show the big one we avoid seeing the small one behind.
# It's why I set it transparent.
image = self.xml.get_object('avatar_image')
pixbuf = image.get_pixbuf()
pixbuf.fill(0xffffff00) # RGBA
image.set_from_pixbuf(pixbuf)
#image.queue_draw()
image.hide()
screen_w = Gdk.Screen.width()
screen_h = Gdk.Screen.height()
......
......@@ -34,8 +34,11 @@ import uuid
from distutils.version import LooseVersion as V
import gi
import nbxmpp
import hashlib
from gajim.common import config
from gi.repository import GLib
from gajim.common import config as c_config
from gajim.common import configpaths
from gajim.common import ged as ged_module
from gajim.common.contacts import LegacyContactsAPI
......@@ -43,9 +46,10 @@ from gajim.common.events import Events
interface = None # The actual interface (the gtk one for the moment)
thread_interface = lambda *args: None # Interface to run a thread and then a callback
config = config.Config()
config = c_config.Config()
version = config.get('version')
connections = {} # 'account name': 'account (connection.Connection) instance'
avatar_cache = {}
ipython_window = None
app = None # Gtk.Application
......@@ -260,7 +264,7 @@ gajim_common_features = [nbxmpp.NS_BYTESTREAM, nbxmpp.NS_SI, nbxmpp.NS_FILE,
nbxmpp.NS_ROSTERX, nbxmpp.NS_SECLABEL, nbxmpp.NS_HASHES_2,
nbxmpp.NS_HASHES_MD5, nbxmpp.NS_HASHES_SHA1, nbxmpp.NS_HASHES_SHA256,
nbxmpp.NS_HASHES_SHA512, nbxmpp.NS_CONFERENCE, nbxmpp.NS_CORRECT,
nbxmpp.NS_EME]
nbxmpp.NS_EME, 'urn:xmpp:avatar:metadata+notify']
# Optional features gajim supports per account
gajim_optional_features = {}
......
......@@ -220,12 +220,6 @@ class Config:
'tabs_border': [opt_bool, False, _('Show tabbed notebook border in chat windows?')],
'tabs_close_button': [opt_bool, True, _('Show close button in tab?')],
'esession_modp': [opt_str, '15,16,14', _('A list of modp groups to use in a Diffie-Hellman, highest preference first, separated by commas. Valid groups are 1, 2, 5, 14, 15, 16, 17 and 18. Higher numbers are more secure, but take longer to calculate when you start a session.')],
'chat_avatar_width': [opt_int, 52],
'chat_avatar_height': [opt_int, 52],
'roster_avatar_width': [opt_int, 32],
'roster_avatar_height': [opt_int, 32],
'tooltip_avatar_width': [opt_int, 125],
'tooltip_avatar_height': [opt_int, 125],
'tooltip_status_online_color': [opt_color, '#73D216'],
'tooltip_status_free_for_chat_color': [opt_color, '#3465A4'],
'tooltip_status_away_color': [opt_color, '#EDD400'],
......@@ -238,13 +232,9 @@ class Config:
'tooltip_affiliation_owner_color': [opt_color, '#CC0000'],
'tooltip_account_name_color': [opt_color, '#888A85'],
'tooltip_idle_color': [opt_color, '#888A85'],
'vcard_avatar_width': [opt_int, 200],
'vcard_avatar_height': [opt_int, 200],
'notification_preview_message': [opt_bool, True, _('Preview new messages in notification popup?')],
'notification_position_x': [opt_int, -1],
'notification_position_y': [opt_int, -1],
'notification_avatar_width': [opt_int, 48],
'notification_avatar_height': [opt_int, 48],
'muc_highlight_words': [opt_str, '', _('A semicolon-separated list of words that will be highlighted in group chats.')],
'quit_on_roster_x_button': [opt_bool, False, _('If True, quits Gajim when X button of Window Manager is clicked. This setting is taken into account only if notification icon is used.')],
'show_unread_tab_icon': [opt_bool, False, _('If True, Gajim will display an icon on each tab containing unread messages. Depending on the theme, this icon may be animated.')],
......@@ -255,7 +245,6 @@ class Config:
'show_tunes_in_roster': [opt_bool, True, '', True],
'show_location_in_roster': [opt_bool, True, '', True],
'avatar_position_in_roster': [opt_str, 'right', _('Define the position of the avatar in roster. Can be left or right'), True],
'ask_avatars_on_startup': [opt_bool, True, _('If True, Gajim will ask for avatar each contact that did not have an avatar last time or has one cached that is too old.')],
'print_status_in_chats': [opt_bool, False, _('If False, Gajim will no longer print status line in chats when a contact changes his or her status and/or his or her status message.')],
'print_status_in_muc': [opt_str, 'none', _('Can be "none", "all" or "in_and_out". If "none", Gajim will no longer print status line in groupchats when a member changes his or her status and/or his or her status message. If "all" Gajim will print all status messages. If "in_and_out", Gajim will only print FOO enters/leaves group chat.')],
'log_contact_status_changes': [opt_bool, False],
......@@ -325,6 +314,7 @@ class Config:
'account_label': [ opt_str, '', '', False ],
'hostname': [ opt_str, '', '', True ],
'anonymous_auth': [ opt_bool, False ],
'avatar_sha': [opt_str, '', '', False],
'client_cert': [ opt_str, '', '', True ],
'client_cert_encrypted': [ opt_bool, False, '', False ],
'savepass': [ opt_bool, False ],
......
......@@ -779,6 +779,7 @@ class Connection(CommonConnection, ConnectionHandlers):
self.connected = 0
self.time_to_reconnect = None
self.privacy_rules_supported = False
self.avatar_presence_sent = False
if on_purpose:
self.sm = Smacks(self)
if self.connection:
......@@ -1778,7 +1779,7 @@ class Connection(CommonConnection, ConnectionHandlers):
show='invisible'))
if initial:
# ask our VCard
self.request_vcard(None)
self.request_vcard(self._on_own_avatar_received)
# Get bookmarks from private namespace
self.get_bookmarks()
......
This diff is collapsed.
......@@ -24,6 +24,7 @@
from calendar import timegm
import datetime
import hashlib
import base64
import hmac
import logging
import sys
......@@ -324,6 +325,7 @@ class RosterReceivedEvent(nec.NetworkIncomingEvent):
self.conn.connection.getRoster().delItem(jid)
elif jid != our_jid: # don't add our jid
self.roster[j] = raw_roster[jid]
self.roster[j]['avatar_sha'] = None
else:
# Roster comes from DB
self.received_from_server = False
......@@ -376,6 +378,9 @@ class RosterInfoEvent(nec.NetworkIncomingEvent):
name = 'roster-info'
base_network_events = []
def init(self):
self.avatar_sha = None
class MucOwnerReceivedEvent(nec.NetworkIncomingEvent, HelperEvent):
name = 'muc-owner-received'
base_network_events = []
......@@ -532,19 +537,13 @@ class PubsubReceivedEvent(nec.NetworkIncomingEvent):
base_network_events = []
def generate(self):
self.jid = self.stanza.getFrom()
self.pubsub_node = self.stanza.getTag('pubsub')
if not self.pubsub_node:
return
self.items_node = self.pubsub_node.getTag('items')
if not self.items_node:
return
self.item_node = self.items_node.getTag('item')
if not self.item_node:
return
children = self.item_node.getChildren()
if not children:
return
self.node = children[0]
return True
class PubsubBookmarksReceivedEvent(nec.NetworkIncomingEvent, BookmarksHelper):
......@@ -553,13 +552,51 @@ class PubsubBookmarksReceivedEvent(nec.NetworkIncomingEvent, BookmarksHelper):
def generate(self):
self.conn = self.base_event.conn
self.storage_node = self.base_event.node
self.item_node = self.base_event.items_node.getTag('item')
if not self.item_node:
return
children = self.item_node.getChildren()
if not children:
return
self.storage_node = children[0]
ns = self.storage_node.getNamespace()
if ns != nbxmpp.NS_BOOKMARKS:
return
self.parse_bookmarks()
return True
class PubsubAvatarReceivedEvent(nec.NetworkIncomingEvent):
name = 'pubsub-avatar-received'
base_network_events = ['pubsub-received']
def __init__(self, name, base_event):
'''
Pre-Generated attributes on self:
:conn: Connection instance
:jid: The from jid
:pubsub_node: The 'pubsub' node
:items_node: The 'items' node
'''
self._set_base_event_vars_as_attributes(base_event)
def generate(self):
if self.items_node.getAttr('node') != 'urn:xmpp:avatar:data':
return
item = self.items_node.getTag('item')
self.sha = item.getAttr('id')
data_tag = item.getTag('data', namespace='urn:xmpp:avatar:data')
if self.sha is None or data_tag is None:
log.warning('Received malformed avatar data via pubsub')
return
self.data = data_tag.getData()
if self.data is None:
log.warning('Received malformed avatar data via pubsub')
return
self.data = base64.b64decode(self.data.encode('utf-8'))
return True
class SearchFormReceivedEvent(nec.NetworkIncomingEvent, HelperEvent):
name = 'search-form-received'
base_network_events = []
......@@ -874,10 +911,8 @@ class GcPresenceReceivedEvent(nec.NetworkIncomingEvent, HelperEvent):
contact_name=fjid.getResource(),
message=st,
show=show)
if self.avatar_sha == '':
# contact has no avatar
puny_nick = helpers.sanitize_filename(self.nick)
app.interface.remove_avatar_files(self.room_jid, puny_nick)
# NOTE: if it's a gc presence, don't ask vcard here.
# We may ask it to real jid in gui part.
self.status_code = []
......@@ -2004,16 +2039,20 @@ class VcardReceivedEvent(nec.NetworkIncomingEvent):
base_network_events = []
def generate(self):
self.nickname = None
if 'NICKNAME' in self.vcard_dict:
self.nickname = self.vcard_dict['NICKNAME']
elif 'FN' in self.vcard_dict:
self.nickname = self.vcard_dict['FN']
self.jid = self.vcard_dict['jid']
self.resource = self.vcard_dict['resource']
self.fjid = self.jid
if self.resource:
self.fjid += '/' + self.resource
return True
class UpdateGCAvatarEvent(nec.NetworkIncomingEvent):
name = 'update-gc-avatar'
base_network_events = []
def generate(self):
return True
class UpdateRosterAvatarEvent(nec.NetworkIncomingEvent):
name = 'update-roster-avatar'
base_network_events = []
def generate(self):
return True
class PEPConfigReceivedEvent(nec.NetworkIncomingEvent):
......
......@@ -28,6 +28,14 @@ class OptionType(IntEnum):
ACTION = 3
DIALOG = 4
class AvatarSize(IntEnum):
ROSTER = 32
NOTIFICATION = 48
CHAT = 52
PROFILE = 64
TOOLTIP = 125
VCARD = 200
THANKS = u"""\
Alexander Futász
......
......@@ -94,7 +94,7 @@ class Contact(CommonContact):
"""
def __init__(self, jid, account, name='', groups=None, show='', status='',
sub='', ask='', resource='', priority=0, keyID='', client_caps=None,
our_chatstate=None, chatstate=None, idle_time=None):
our_chatstate=None, chatstate=None, idle_time=None, avatar_sha=None):
if not isinstance(jid, str):
print('no str')
if groups is None:
......@@ -105,6 +105,7 @@ class Contact(CommonContact):
self.contact_name = '' # nick choosen by contact
self.groups = [i if i else _('General') for i in set(groups)] # filter duplicate values
self.avatar_sha = avatar_sha
self.sub = sub
self.ask = ask
......@@ -182,7 +183,7 @@ class GC_Contact(CommonContact):
def __init__(self, room_jid, account, name='', show='', status='', role='',
affiliation='', jid='', resource='', our_chatstate=None,
chatstate=None):
chatstate=None, avatar_sha=None):
CommonContact.__init__(self, jid, account, resource, show, status, name,
our_chatstate, chatstate)
......@@ -190,6 +191,7 @@ class GC_Contact(CommonContact):
self.room_jid = room_jid
self.role = role
self.affiliation = affiliation
self.avatar_sha = avatar_sha
def get_full_jid(self):
return self.room_jid + '/' + self.name
......@@ -197,13 +199,16 @@ class GC_Contact(CommonContact):
def get_shown_name(self):
return self.name
def get_avatar(self, size=None):
return common.app.interface.get_avatar(self.avatar_sha, size)
def as_contact(self):
"""
Create a Contact instance from this GC_Contact instance
"""
return Contact(jid=self.get_full_jid(), account=self.account,
name=self.name, groups=[], show=self.show, status=self.status,
sub='none', client_caps=self.client_caps)
sub='none', client_caps=self.client_caps, avatar_sha=self.avatar_sha)
class LegacyContactsAPI:
......@@ -245,7 +250,8 @@ class LegacyContactsAPI:
def create_contact(self, jid, account, name='', groups=None, show='',
status='', sub='', ask='', resource='', priority=0, keyID='',
client_caps=None, our_chatstate=None, chatstate=None, idle_time=None):
client_caps=None, our_chatstate=None, chatstate=None, idle_time=None,
avatar_sha=None):
if groups is None:
groups = []
# Use Account object if available
......@@ -254,7 +260,7 @@ class LegacyContactsAPI:
show=show, status=status, sub=sub, ask=ask, resource=resource,
priority=priority, keyID=keyID, client_caps=client_caps,
our_chatstate=our_chatstate, chatstate=chatstate,
idle_time=idle_time)
idle_time=idle_time, avatar_sha=avatar_sha)
def create_self_contact(self, jid, account, resource, show, status, priority,
name='', keyID=''):
......@@ -283,7 +289,7 @@ class LegacyContactsAPI:
resource=contact.resource, priority=contact.priority,
keyID=contact.keyID, client_caps=contact.client_caps,
our_chatstate=contact.our_chatstate, chatstate=contact.chatstate,
idle_time=contact.idle_time)
idle_time=contact.idle_time, avatar_sha=contact.avatar_sha)
def add_contact(self, account, contact):
if account not in self._accounts:
......@@ -306,6 +312,15 @@ class LegacyContactsAPI:
def get_contact(self, account, jid, resource=None):
return self._accounts[account].contacts.get_contact(jid, resource=resource)
def get_avatar(self, account, jid, size=None):
return self._accounts[account].contacts.get_avatar(jid, size)
def get_avatar_sha(self, account, jid):
return self._accounts[account].contacts.get_avatar_sha(jid)
def set_avatar(self, account, jid, sha):
self._accounts[account].contacts.set_avatar(jid, sha)
def iter_contacts(self, account):
for contact in self._accounts[account].contacts.iter_contacts():
yield contact
......@@ -395,10 +410,10 @@ class LegacyContactsAPI:
raise AttributeError(attr_name)
def create_gc_contact(self, room_jid, account, name='', show='', status='',
role='', affiliation='', jid='', resource=''):
role='', affiliation='', jid='', resource='', avatar_sha=None):
account = self._accounts.get(account, account) # Use Account object if available
return GC_Contact(room_jid, account, name, show, status, role, affiliation, jid,
resource)
resource, avatar_sha=avatar_sha)
def add_gc_contact(self, account, gc_contact):
return self._accounts[account].gc_contacts.add_gc_contact(gc_contact)
......@@ -424,6 +439,12 @@ class LegacyContactsAPI:
def get_nb_role_total_gc_contacts(self, account, room_jid, role):
return self._accounts[account].gc_contacts.get_nb_role_total_gc_contacts(room_jid, role)
def set_gc_avatar(self, account, room_jid, nick, sha):
contact = self.get_gc_contact(account, room_jid, nick)
if contact is None:
return
contact.avatar_sha = sha
class Contacts():
"""
......@@ -490,6 +511,33 @@ class Contacts():
return c
return self._contacts[jid][0]
def get_avatar(self, jid, size=None):
if jid not in self._contacts:
return None
for resource in self._contacts[jid]:
if resource.avatar_sha is None:
continue
avatar = common.app.interface.get_avatar(resource.avatar_sha, size)
if avatar is None:
self.set_avatar(jid, None)
return avatar
def get_avatar_sha(self, jid):
if jid not in self._contacts:
return None
for resource in self._contacts[jid]:
if resource.avatar_sha is not None:
return resource.avatar_sha
return None
def set_avatar(self, jid, sha):
if jid not in self._contacts:
return
for resource in self._contacts[jid]:
resource.avatar_sha = sha
def iter_contacts(self):
for jid in list(self._contacts.keys()):
for contact in self._contacts[jid][:]:
......
......@@ -633,24 +633,6 @@ def get_account_status(account):
status = reduce_chars_newlines(account['status_line'], 100, 1)
return status
def get_avatar_path(prefix):
"""
Return the filename of the avatar, distinguishes between user- and contact-
provided one. Return None if no avatar was found at all. prefix is the path
to the requested avatar just before the ".png" or ".jpeg"
"""
# First, scan for a local, user-set avatar
for type_ in ('jpeg', 'png'):
file_ = prefix + '_local.' + type_
if os.path.exists(file_):
return file_
# If none available, scan for a contact-provided avatar
for type_ in ('jpeg', 'png'):
file_ = prefix + '.' + type_
if os.path.exists(file_):
return file_
return None
def datetime_tuple(timestamp):
"""
Convert timestamp using strptime and the format: %Y%m%dT%H:%M:%S
......@@ -1396,7 +1378,7 @@ def get_subscription_request_msg(account=None):
if account:
s = _('Hello, I am $name.') + ' ' + s
our_jid = app.get_jid_from_account(account)
vcard = app.connections[account].get_cached_vcard(our_jid)
vcard = app.connections[account].own_vcard
name = ''
if vcard:
if 'N' in vcard:
......
......@@ -42,7 +42,6 @@ from enum import IntEnum, unique
from gajim.common import exceptions
from gajim.common import app
from gajim.common import ged
import sqlite3 as sqlite
......@@ -998,7 +997,7 @@ class Logger:
# First we fill data with roster_entry informations
self.cur.execute('''
SELECT j.jid, re.jid_id, re.name, re.subscription, re.ask
SELECT j.jid, re.jid_id, re.name, re.subscription, re.ask, re.avatar_sha
FROM roster_entry re, jids j
WHERE re.account_jid_id=? AND j.jid_id=re.jid_id''', (account_jid_id,))
for row in self.cur:
......@@ -1006,6 +1005,7 @@ class Logger:
jid = row.jid
name = row.name
data[jid] = {}
data[jid]['avatar_sha'] = row.avatar_sha
if name:
data[jid]['name'] = name
else:
......@@ -1135,3 +1135,25 @@ class Logger:
self._timeout_commit()
return lastrowid
def set_avatar_sha(self, account_jid, jid, sha=None):
"""
Set the avatar sha of a jid on an account
:param account_jid: The jid of the account
:param jid: The jid that belongs to the avatar
:param sha: The sha of the avatar
"""
account_jid_id = self.get_jid_id(account_jid)
jid_id = self.get_jid_id(jid)
sql = '''
UPDATE roster_entry SET avatar_sha = ?
WHERE account_jid_id = ? AND jid_id = ?
'''
self.con.execute(sql, (sha, account_jid_id, jid_id))
self._timeout_commit()
......@@ -244,6 +244,7 @@ class AbstractPEP(object):
self._update_contacts(jid, account)
if jid == app.get_jid_from_account(account):
self._update_account(account)
self._on_receive(jid, account)
def _extract_info(self, items):
'''To be implemented by subclasses'''
......@@ -269,6 +270,10 @@ class AbstractPEP(object):
'''SHOULD be implemented by subclasses'''
return ''
def _on_receive(self, jid, account):
'''SHOULD be implemented by subclasses'''
pass
class UserMoodPEP(AbstractPEP):
'''XEP-0107: User Mood'''
......@@ -469,5 +474,32 @@ class UserLocationPEP(AbstractPEP):
return location_string.strip()
SUPPORTED_PERSONAL_USER_EVENTS = [UserMoodPEP, UserTunePEP, UserActivityPEP,
UserNicknamePEP, UserLocationPEP]
class AvatarNotificationPEP(AbstractPEP):
'''XEP-0084: Avatars'''
type_ = 'avatar-notification'
namespace = 'urn:xmpp:avatar:metadata'
def _extract_info(self, items):
avatar = None
for item in items.getTags('item'):
info = item.getTag('metadata').getTag('info')
self.avatar = info.getAttrs()
break
return (avatar, False)
def _on_receive(self, jid, account):
sha = app.contacts.get_avatar_sha(account, jid)
app.log('avatar').info(
'Update (Pubsub): %s %s', jid, self.avatar['id'])
if sha == self.avatar['id']:
return
con = app.connections[account]
app.log('avatar').info('Request (Pubsub): %s', jid)
con.send_pb_retrieve(jid, 'urn:xmpp:avatar:data', self.avatar['id'])
SUPPORTED_PERSONAL_USER_EVENTS = [
UserMoodPEP, UserTunePEP, UserActivityPEP,
UserNicknamePEP, UserLocationPEP, AvatarNotificationPEP]
......@@ -27,8 +27,11 @@ from gajim.common import app
#from common.connection_handlers import PEP_CONFIG
PEP_CONFIG = 'pep_config'
from gajim.common import ged
from gajim.common.nec import NetworkEvent