plugin XEP-0054, quick frontend(app): various improvments:
- memory.cache is now used
- getAvatar and setAvatar has been renamed to avatarGet and avatarSet to follow new convention
- getAvatar now return (optionally) full path, and may request vCard or only return avatar in cache
- getAvatarFile has been removed as it is not needed anymore (avatarGet can return full path)
- getCard has been removed from bridge as it was only used to request avatar
- new internal method getBareOfFull return jid to use depending of the jid being part of a MUC or not
- cache is now set per profile in client, instead of a general one for all profiles
- thanks to the use of memory.cache, correct extension is now used in saved file, according to MIME type
- fixed and better cache handling
- a warning message is shown if given avatar hash differs from computed one
- empty hash value is now in a constant, and ignored if received
- QuickApp has been updated to follow new behaviour
- Primitivus has been fixed (it was not declaring not using avatars correclty)
- jp has been updated to follow new methods name
This commit is contained in:
parent
1a33c3389f
commit
4eaf5d626f
7 changed files with 326 additions and 217 deletions
|
|
@ -46,7 +46,7 @@ class Set(base.CommandBase):
|
|||
self.disp(_(u"file [{}] doesn't exist !").format(path), error=True)
|
||||
self.host.quit(1)
|
||||
path = os.path.abspath(path)
|
||||
self.host.bridge.setAvatar(path, self.profile, callback=self._avatarCb, errback=self._avatarEb)
|
||||
self.host.bridge.avatarSet(path, self.profile, callback=self._avatarCb, errback=self._avatarEb)
|
||||
|
||||
def _avatarCb(self):
|
||||
self.disp(_("avatar has been set"), 1)
|
||||
|
|
@ -85,7 +85,7 @@ class Get(base.CommandBase):
|
|||
import webbrowser
|
||||
webbrowser.open(path)
|
||||
|
||||
def _getAvatarCb(self, avatar_path):
|
||||
def _avatarGetCb(self, avatar_path):
|
||||
if not avatar_path:
|
||||
self.disp(_(u"No avatar found."), 1)
|
||||
self.host.quit(C.EXIT_NOT_FOUND)
|
||||
|
|
@ -96,12 +96,12 @@ class Get(base.CommandBase):
|
|||
|
||||
self.host.quit()
|
||||
|
||||
def _getAvatarEb(self, failure_):
|
||||
def _avatarGetEb(self, failure_):
|
||||
self.disp(_("error while getting avatar: {msg}").format(msg=failure_), error=True)
|
||||
self.host.quit(C.EXIT_ERROR)
|
||||
|
||||
def start(self):
|
||||
self.host.bridge.getAvatar(self.args.jid, self.profile, callback=self._getAvatarCb, errback=self._getAvatarEb)
|
||||
self.host.bridge.avatarGet(self.args.jid, False, False, self.profile, callback=self._avatarGetCb, errback=self._avatarGetEb)
|
||||
|
||||
|
||||
class Avatar(base.CommandBase):
|
||||
|
|
|
|||
|
|
@ -280,7 +280,7 @@ class PrimitivusTopWidget(sat_widgets.FocusPile):
|
|||
|
||||
class PrimitivusApp(QuickApp, InputHistory):
|
||||
MB_HANDLER = False
|
||||
AVATAR_HANDLER = False
|
||||
AVATARS_HANDLER = False
|
||||
|
||||
def __init__(self):
|
||||
bridge_module = dynamic_import.bridge(bridge_name, 'sat_frontends.bridge')
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ class ProfileManager(object):
|
|||
# and a way to keep some XMLUI request between sessions is expected in backend
|
||||
host = None
|
||||
bridge = None
|
||||
cache_keys_to_get = ['avatar']
|
||||
# cache_keys_to_get = ['avatar']
|
||||
|
||||
def __init__(self, profile):
|
||||
self.profile = profile
|
||||
|
|
@ -106,7 +106,10 @@ class ProfileManager(object):
|
|||
|
||||
def _plug_profile_getFeaturesCb(self, features):
|
||||
self.host.features = features
|
||||
self.host.bridge.getEntitiesData([], ProfileManager.cache_keys_to_get, profile=self.profile, callback=self._plug_profile_gotCachedValues, errback=self._plug_profile_failedCachedValues)
|
||||
# FIXME: we don't use cached value at the moment, but keep the code for later use
|
||||
# it was previously used for avatars, but as we don't get full path here, it's better to request later
|
||||
# self.host.bridge.getEntitiesData([], ProfileManager.cache_keys_to_get, profile=self.profile, callback=self._plug_profile_gotCachedValues, errback=self._plug_profile_failedCachedValues)
|
||||
self._plug_profile_gotCachedValues({})
|
||||
|
||||
def _plug_profile_failedCachedValues(self, failure):
|
||||
log.error(u"Couldn't get cached values: {}".format(failure))
|
||||
|
|
@ -817,15 +820,9 @@ class QuickApp(object):
|
|||
if entity in self.contact_lists[profile]:
|
||||
self.contact_lists[profile].setCache(entity, 'nick', value)
|
||||
self.callListeners('nick', entity, value, profile=profile)
|
||||
elif key == "avatar":
|
||||
if value: # and entity in self.contact_lists[profile]:
|
||||
# FIXME: contact_list check is removed to work around 'avatar' issues
|
||||
# (avatar can't be requested if we don't save them here)
|
||||
# avatar need to be refactored properly in backend and here
|
||||
def gotFilename(filename):
|
||||
self.contact_lists[profile].setCache(entity, 'avatar', filename)
|
||||
self.callListeners('avatar', entity, filename, profile=profile)
|
||||
self.bridge.getAvatarFile(value, callback=gotFilename)
|
||||
elif key == "avatar" and self.AVATARS_HANDLER:
|
||||
if value and entity in self.contact_lists[profile]:
|
||||
self.getAvatar(entity, ignore_cache=True, profile=profile)
|
||||
|
||||
def actionManager(self, action_data, callback=None, ui_show_cb=None, user_action=True, profile=C.PROF_KEY_NONE):
|
||||
"""Handle backend action
|
||||
|
|
@ -886,15 +883,42 @@ class QuickApp(object):
|
|||
|
||||
self.bridge.launchAction(callback_id, data, profile, callback=action_cb, errback=self.dialogFailure)
|
||||
|
||||
def getAvatar(self, entity, profile):
|
||||
"""return avatar path for an entity"""
|
||||
def _avatarGetCb(self, avatar_path, entity, contact_list, profile):
|
||||
path = avatar_path or self.getDefaultAvatar(entity)
|
||||
contact_list.setCache(entity, "avatar", path)
|
||||
self.callListeners('avatar', entity, path, profile=profile)
|
||||
|
||||
def _avatarGetEb(self, failure, entity, contact_list):
|
||||
log.warning(u"Can't get avatar: {}".format(failure))
|
||||
contact_list.setCache(entity, "avatar", self.getDefaultAvatar(entity))
|
||||
|
||||
def getAvatar(self, entity, cache_only=True, hash_only=False, ignore_cache=False, profile=C.PROF_KEY_NONE):
|
||||
"""return avatar path for an entity
|
||||
|
||||
@param entity(jid.JID): entity to get avatar from
|
||||
@param cache_only(bool): if False avatar will be requested if not in cache
|
||||
with current vCard based implementation, it's better to keep True
|
||||
except if we request avatars for roster items
|
||||
@param hash_only(bool): if True avatar hash is returned, else full path
|
||||
@param ignore_cache(bool): if False, won't check local cache and will request backend in every case
|
||||
@return (unicode, None): avatar full path (None if no avatar found)
|
||||
"""
|
||||
contact_list = self.contact_lists[profile]
|
||||
avatar = contact_list.getCache(entity, "avatar")
|
||||
if ignore_cache:
|
||||
avatar = None
|
||||
else:
|
||||
avatar = contact_list.getCache(entity, "avatar", bare_default=None)
|
||||
if avatar is None:
|
||||
self.bridge.getCard(unicode(entity), profile)
|
||||
self.bridge.avatarGet(
|
||||
unicode(entity),
|
||||
cache_only,
|
||||
hash_only,
|
||||
profile=profile,
|
||||
callback=lambda path: self._avatarGetCb(path, entity, contact_list, profile),
|
||||
errback=lambda failure: self._avatarGetEb(failure, entity, contact_list))
|
||||
# we set avatar to empty string to avoid requesting several time the same avatar
|
||||
# while we are waiting for avatarGet result
|
||||
contact_list.setCache(entity, "avatar", "")
|
||||
if not avatar:
|
||||
avatar = self.getDefaultAvatar(entity)
|
||||
return avatar
|
||||
|
||||
def getDefaultAvatar(self, entity=None):
|
||||
|
|
|
|||
|
|
@ -126,7 +126,9 @@ class Message(object):
|
|||
|
||||
@property
|
||||
def avatar(self):
|
||||
return self.host.getAvatar(self.from_jid, self.profile)
|
||||
"""avatar full path or None if no avatar is found"""
|
||||
ret = self.host.getAvatar(self.from_jid, profile=self.profile)
|
||||
return ret
|
||||
|
||||
def getNick(self, entity):
|
||||
"""Return nick of an entity when possible"""
|
||||
|
|
@ -583,16 +585,25 @@ class QuickChat(quick_widgets.QuickWidget):
|
|||
|
||||
def onAvatar(self, entity, filename, profile):
|
||||
if self.type == C.CHAT_GROUP:
|
||||
if entity.bare == self.target and entity.resource in self.occupants:
|
||||
self.occupants[entity.resource].update({'avatar': filename})
|
||||
if entity.bare == self.target:
|
||||
try:
|
||||
self.occupants[entity.resource].update({'avatar': filename})
|
||||
except KeyError:
|
||||
# can happen for a message in history where the
|
||||
# entity is not here anymore
|
||||
pass
|
||||
|
||||
for m in self.messages.values():
|
||||
if m.nick == entity.resource:
|
||||
for w in m.widgets:
|
||||
w.update({'avatar': filename})
|
||||
else:
|
||||
if entity.bare == self.target.bare:
|
||||
log.info("entity avatar updated")
|
||||
# TODO: call a specific method
|
||||
if entity.bare == self.target.bare or entity.bare == self.host.profiles[profile].whoami.bare:
|
||||
log.info(u"avatar updated for {}".format(entity))
|
||||
for m in self.messages.values():
|
||||
if m.from_jid.bare == entity.bare:
|
||||
for w in m.widgets:
|
||||
w.update({'avatar': filename})
|
||||
|
||||
|
||||
quick_widgets.register(QuickChat)
|
||||
|
|
|
|||
|
|
@ -219,6 +219,9 @@ class ProfileContactList(object):
|
|||
@return: full cache if no name is given, or value of "name", or None
|
||||
"""
|
||||
# FIXME: resource handling need to be reworked
|
||||
# FIXME: bare_default work for requesting full jid to get bare jid, but not the other way
|
||||
# e.g.: if we have set an avatar for user@server.tld/resource and we request user@server.tld
|
||||
# we won't get the avatar set in the resource
|
||||
try:
|
||||
cache = self._cache[entity.bare]
|
||||
except KeyError:
|
||||
|
|
|
|||
|
|
@ -285,6 +285,7 @@ class Const(object):
|
|||
IGNORE = 'ignore'
|
||||
NO_LIMIT = -1 # used in bridge when a integer value is expected
|
||||
DEFAULT_MAX_AGE = 1209600 # default max age of cached files, in seconds
|
||||
HASH_SHA1_EMPTY = 'da39a3ee5e6b4b0d3255bfef95601890afd80709'
|
||||
|
||||
|
||||
## ANSI escape sequences ##
|
||||
|
|
|
|||
|
|
@ -24,10 +24,8 @@ from sat.core.log import getLogger
|
|||
log = getLogger(__name__)
|
||||
from twisted.internet import threads, defer
|
||||
from twisted.words.protocols.jabber import jid
|
||||
from twisted.words.protocols.jabber.xmlstream import IQ
|
||||
from twisted.words.xish import domish
|
||||
from twisted.python.failure import Failure
|
||||
import os.path
|
||||
|
||||
from zope.interface import implements
|
||||
|
||||
|
|
@ -37,6 +35,7 @@ from base64 import b64decode, b64encode
|
|||
from hashlib import sha1
|
||||
from sat.core import exceptions
|
||||
from sat.memory import persistent
|
||||
import mimetypes
|
||||
try:
|
||||
from PIL import Image
|
||||
except:
|
||||
|
|
@ -49,7 +48,7 @@ except ImportError:
|
|||
from wokkel.subprotocols import XMPPHandler
|
||||
|
||||
AVATAR_PATH = "avatars"
|
||||
AVATAR_DIM = (64, 64)
|
||||
AVATAR_DIM = (64, 64) # FIXME: dim are not adapted to modern resolutions !
|
||||
|
||||
IQ_GET = '/iq[@type="get"]'
|
||||
NS_VCARD = 'vcard-temp'
|
||||
|
|
@ -60,6 +59,7 @@ NS_VCARD_UPDATE = 'vcard-temp:x:update'
|
|||
VCARD_UPDATE = PRESENCE + '/x[@xmlns="' + NS_VCARD_UPDATE + '"]'
|
||||
|
||||
CACHED_DATA = {'avatar', 'nick'}
|
||||
MAX_AGE = 60 * 60 * 24 * 365
|
||||
|
||||
PLUGIN_INFO = {
|
||||
"name": "XEP 0054 Plugin",
|
||||
|
|
@ -80,36 +80,17 @@ class XEP_0054(object):
|
|||
# - get missing values
|
||||
|
||||
def __init__(self, host):
|
||||
log.info(_("Plugin XEP_0054 initialization"))
|
||||
log.info(_(u"Plugin XEP_0054 initialization"))
|
||||
self.host = host
|
||||
self.avatar_path = os.path.join(self.host.memory.getConfig('', 'local_dir'), AVATAR_PATH)
|
||||
if not os.path.exists(self.avatar_path):
|
||||
os.makedirs(self.avatar_path)
|
||||
self.cache = {}
|
||||
host.bridge.addMethod("getCard", ".plugin", in_sign='ss', out_sign='', method=self._getCard, async=True)
|
||||
host.bridge.addMethod("getAvatarFile", ".plugin", in_sign='s', out_sign='s', method=self.getAvatarFile)
|
||||
host.bridge.addMethod("getAvatar", ".plugin", in_sign='ss', out_sign='s', method=self._getAvatar, async=True)
|
||||
host.bridge.addMethod("setAvatar", ".plugin", in_sign='ss', out_sign='', method=self.setAvatar, async=True)
|
||||
host.trigger.add("presence_available", self.presenceAvailableTrigger)
|
||||
host.memory.setSignalOnUpdate("avatar")
|
||||
host.memory.setSignalOnUpdate("nick")
|
||||
host.bridge.addMethod(u"avatarGet", u".plugin", in_sign=u'sbbs', out_sign=u's', method=self._getAvatar, async=True)
|
||||
host.bridge.addMethod(u"avatarSet", u".plugin", in_sign=u'ss', out_sign=u'', method=self._setAvatar, async=True)
|
||||
host.trigger.add(u"presence_available", self.presenceAvailableTrigger)
|
||||
host.memory.setSignalOnUpdate(u"avatar")
|
||||
host.memory.setSignalOnUpdate(u"nick")
|
||||
|
||||
def getHandler(self, profile):
|
||||
return XEP_0054_handler(self)
|
||||
|
||||
def presenceAvailableTrigger(self, presence_elt, client):
|
||||
if client.jid.userhost() in self.cache[client.profile]:
|
||||
try:
|
||||
avatar_hash = self.cache[client.profile][client.jid.userhost()]['avatar']
|
||||
except KeyError:
|
||||
log.info(u"No avatar in cache for {}".format(client.jid.userhost()))
|
||||
return True
|
||||
x_elt = domish.Element((NS_VCARD_UPDATE, 'x'))
|
||||
x_elt.addElement('photo', content=avatar_hash)
|
||||
presence_elt.addChild(x_elt)
|
||||
|
||||
return True
|
||||
|
||||
def isRoom(self, client, entity_jid):
|
||||
"""Tell if a jid is a MUC one
|
||||
|
||||
|
|
@ -123,28 +104,56 @@ class XEP_0054(object):
|
|||
else:
|
||||
return True
|
||||
|
||||
def _fillCachedValues(self, profile):
|
||||
#FIXME: this is really suboptimal, need to be reworked
|
||||
# the current naive approach keeps a map between all jids of all profiles
|
||||
# in persistent cache, then put avatar hashs in memory.
|
||||
# Hashes should be shared between profiles
|
||||
for jid_s, data in self.cache[profile].iteritems():
|
||||
jid_ = jid.JID(jid_s)
|
||||
for name in CACHED_DATA:
|
||||
try:
|
||||
self.host.memory.updateEntityData(jid_, name, data[name], silent=True, profile_key=profile)
|
||||
except KeyError:
|
||||
pass
|
||||
def getBareOrFull(self, client, jid_):
|
||||
"""use full jid if jid_ is an occupant of a room, bare jid else
|
||||
|
||||
@param jid_(jid.JID): entity to test
|
||||
@return (jid.JID): bare or full jid
|
||||
"""
|
||||
if jid_.resource:
|
||||
if not self.isRoom(client, jid_):
|
||||
return jid_.userhostJID()
|
||||
return jid_
|
||||
|
||||
def presenceAvailableTrigger(self, presence_elt, client):
|
||||
if client.jid.userhost() in client._cache_0054:
|
||||
try:
|
||||
avatar_hash = client._cache_0054[client.jid.userhost()]['avatar']
|
||||
except KeyError:
|
||||
log.info(u"No avatar in cache for {}".format(client.jid.userhost()))
|
||||
return True
|
||||
x_elt = domish.Element((NS_VCARD_UPDATE, 'x'))
|
||||
x_elt.addElement('photo', content=avatar_hash)
|
||||
presence_elt.addChild(x_elt)
|
||||
return True
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def profileConnecting(self, profile):
|
||||
self.cache[profile] = persistent.PersistentBinaryDict(NS_VCARD, profile)
|
||||
yield self.cache[profile].load()
|
||||
client = self.host.getClient(profile)
|
||||
client._cache_0054 = persistent.PersistentBinaryDict(NS_VCARD, profile)
|
||||
yield client._cache_0054.load()
|
||||
self._fillCachedValues(profile)
|
||||
|
||||
def profileDisconnected(self, profile):
|
||||
log.debug(u"Deleting profile cache for avatars")
|
||||
del self.cache[profile]
|
||||
def _fillCachedValues(self, profile):
|
||||
#FIXME: this may need to be reworked
|
||||
# the current naive approach keeps a map between all jids
|
||||
# in persistent cache, then put avatar hashs in memory.
|
||||
# Hashes should be shared between profiles (or not ? what
|
||||
# if the avatar is different depending on who is requesting it
|
||||
# this is not possible with vcard-tmp, but it is with XEP-0084).
|
||||
# Loading avatar on demand per jid may be a option to investigate.
|
||||
client = self.host.getClient(profile)
|
||||
for jid_s, data in client._cache_0054.iteritems():
|
||||
jid_ = jid.JID(jid_s)
|
||||
for name in CACHED_DATA:
|
||||
try:
|
||||
value = data[name]
|
||||
if value is None:
|
||||
log.error(u"{name} value for {jid_} is None, ignoring".format(name=name, jid_=jid_))
|
||||
continue
|
||||
self.host.memory.updateEntityData(jid_, name, data[name], silent=True, profile_key=profile)
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
def updateCache(self, client, jid_, name, value):
|
||||
"""update cache value
|
||||
|
|
@ -152,19 +161,29 @@ class XEP_0054(object):
|
|||
save value in memory in case of change
|
||||
@param jid_(jid.JID): jid of the owner of the vcard
|
||||
@param name(str): name of the item which changed
|
||||
@param value(unicode): new value of the item
|
||||
@param value(unicode, None): new value of the item
|
||||
None to delete
|
||||
"""
|
||||
if jid_.resource:
|
||||
if not self.isRoom(client, jid_):
|
||||
# VCard are retrieved with bare jid
|
||||
# but MUC room is a special case
|
||||
jid_ = jid.userhostJID()
|
||||
jid_ = self.getBareOrFull(client, jid_)
|
||||
jid_s = jid_.full()
|
||||
|
||||
self.host.memory.updateEntityData(jid_, name, value, profile_key=client.profile)
|
||||
if name in CACHED_DATA:
|
||||
jid_s = jid_.userhost()
|
||||
self.cache[client.profile].setdefault(jid_s, {})[name] = value
|
||||
self.cache[client.profile].force(jid_s)
|
||||
if value is None:
|
||||
try:
|
||||
self.host.memory.delEntityDatum(jid_, name, client.profile)
|
||||
except (KeyError, exceptions.UnknownEntityError):
|
||||
pass
|
||||
if name in CACHED_DATA:
|
||||
try:
|
||||
del client._cache_0054[jid_s][name]
|
||||
except KeyError:
|
||||
pass
|
||||
else:
|
||||
client._cache_0054.force(jid_s)
|
||||
else:
|
||||
self.host.memory.updateEntityData(jid_, name, value, profile_key=client.profile)
|
||||
if name in CACHED_DATA:
|
||||
client._cache_0054.setdefault(jid_s, {})[name] = value
|
||||
client._cache_0054.force(jid_s)
|
||||
|
||||
def getCache(self, client, entity_jid, name):
|
||||
"""return cached value for jid
|
||||
|
|
@ -172,105 +191,106 @@ class XEP_0054(object):
|
|||
@param entity_jid: target contact
|
||||
@param name: name of the value ('nick' or 'avatar')
|
||||
@return: wanted value or None"""
|
||||
if entity_jid.resource:
|
||||
if not self.isRoom(client, entity_jid):
|
||||
# VCard are retrieved with bare jid
|
||||
# but MUC room is a special case
|
||||
entity_jid = jid.userhostJID()
|
||||
entity_jid = self.getBareOrFull(client, entity_jid)
|
||||
try:
|
||||
data = self.host.memory.getEntityData(entity_jid, [name], client.profile)
|
||||
except exceptions.UnknownEntityError:
|
||||
return None
|
||||
return data.get(name)
|
||||
|
||||
def _getFilename(self, hash_):
|
||||
"""Get filename from hash
|
||||
|
||||
@param hash_: hash of the avatar
|
||||
@return (str): absolute filename of the avatar
|
||||
"""
|
||||
return os.path.join(self.avatar_path, hash_)
|
||||
|
||||
def saveAvatarFile(self, data, hash_):
|
||||
"""Save the avatar picture if it doesn't already exists
|
||||
|
||||
@param data(str): binary image of the avatar
|
||||
@param hash_(str): hash of the binary data (will be used for the filename)
|
||||
"""
|
||||
filename = self._getFilename(hash_)
|
||||
if not os.path.exists(filename):
|
||||
with open(filename, 'wb') as file_:
|
||||
file_.write(data)
|
||||
log.debug(_(u"file saved to %s") % hash_)
|
||||
else:
|
||||
log.debug(_(u"file [%s] already in cache") % hash_)
|
||||
|
||||
def savePhoto(self, photo_elt, target):
|
||||
def savePhoto(self, client, photo_elt, entity_jid):
|
||||
"""Parse a <PHOTO> photo_elt and save the picture"""
|
||||
# XXX: this method is launched in a separate thread
|
||||
try:
|
||||
type_ = unicode(photo_elt.elements(NS_VCARD, 'TYPE').next())
|
||||
mime_type = unicode(photo_elt.elements(NS_VCARD, 'TYPE').next())
|
||||
except StopIteration:
|
||||
type_ = None
|
||||
log.warning(u"no MIME type found, assuming image/png")
|
||||
mime_type = u"image/png"
|
||||
else:
|
||||
if type_ and type_ not in ("image/gif", "image/jpeg", "image/png"):
|
||||
# TODO: handle other image formats (svg?)
|
||||
log.warning(u"following avatar image format is not handled: {type} [{jid}]".format(
|
||||
type=type_, jid=target.full()))
|
||||
return
|
||||
log.debug(u'Photo of type {type} found [{jid}]'.format(
|
||||
type=type_, jid=target.full()))
|
||||
if not mime_type:
|
||||
log.warning(u"empty MIME type, assuming image/png")
|
||||
mime_type = u"image/png"
|
||||
elif mime_type not in ("image/gif", "image/jpeg", "image/png"):
|
||||
if mime_type == "image/x-png":
|
||||
# XXX: this old MIME type is still used by some clients
|
||||
mime_type = "image/png"
|
||||
else:
|
||||
# TODO: handle other image formats (svg?)
|
||||
log.warning(u"following avatar image format is not handled: {type} [{jid}]".format(
|
||||
type=mime_type, jid=entity_jid.full()))
|
||||
raise Failure(exceptions.DataError())
|
||||
|
||||
ext = mimetypes.guess_extension(mime_type, strict=False)
|
||||
assert ext is not None
|
||||
log.debug(u'photo of type {type} with extension {ext} found [{jid}]'.format(
|
||||
type=mime_type, ext=ext, jid=entity_jid.full()))
|
||||
try:
|
||||
bin_elt = photo_elt.elements(NS_VCARD, 'BINVAL').next()
|
||||
buf = str(photo_elt.elements(NS_VCARD, 'BINVAL').next())
|
||||
except StopIteration:
|
||||
return
|
||||
buf = str(bin_elt)
|
||||
log.warning(u"BINVAL element not found")
|
||||
raise Failure(exceptions.NotFound())
|
||||
if not buf:
|
||||
log.warning(u"empty avatar for {jid}".format(jid=target.full()))
|
||||
return
|
||||
log.warning(u"empty avatar for {jid}".format(jid=entity_jid.full()))
|
||||
raise Failure(exceptions.NotFound())
|
||||
log.debug(_(u'Decoding binary'))
|
||||
decoded = b64decode(buf)
|
||||
del buf
|
||||
image_hash = sha1(decoded).hexdigest()
|
||||
self.saveAvatarFile(decoded, image_hash)
|
||||
with client.cache.cacheData(
|
||||
PLUGIN_INFO['import_name'],
|
||||
image_hash,
|
||||
mime_type,
|
||||
# we keep in cache for 1 year
|
||||
MAX_AGE
|
||||
) as f:
|
||||
f.write(decoded)
|
||||
return image_hash
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def vCard2Dict(self, client, vcard, target):
|
||||
def vCard2Dict(self, client, vcard, entity_jid):
|
||||
"""Convert a VCard to a dict, and save binaries"""
|
||||
log.debug(_("parsing vcard"))
|
||||
dictionary = {}
|
||||
log.debug((u"parsing vcard"))
|
||||
vcard_dict = {}
|
||||
|
||||
for elem in vcard.elements():
|
||||
if elem.name == 'FN':
|
||||
dictionary['fullname'] = unicode(elem)
|
||||
vcard_dict['fullname'] = unicode(elem)
|
||||
elif elem.name == 'NICKNAME':
|
||||
dictionary['nick'] = unicode(elem)
|
||||
self.updateCache(client, target, 'nick', dictionary['nick'])
|
||||
vcard_dict['nick'] = unicode(elem)
|
||||
self.updateCache(client, entity_jid, 'nick', vcard_dict['nick'])
|
||||
elif elem.name == 'URL':
|
||||
dictionary['website'] = unicode(elem)
|
||||
vcard_dict['website'] = unicode(elem)
|
||||
elif elem.name == 'EMAIL':
|
||||
dictionary['email'] = unicode(elem)
|
||||
vcard_dict['email'] = unicode(elem)
|
||||
elif elem.name == 'BDAY':
|
||||
dictionary['birthday'] = unicode(elem)
|
||||
vcard_dict['birthday'] = unicode(elem)
|
||||
elif elem.name == 'PHOTO':
|
||||
# TODO: handle EXTVAL
|
||||
avatar_hash = yield threads.deferToThread(self.savePhoto, elem, target)
|
||||
if avatar_hash is None: # can happen e.g. in case of empty photo elem
|
||||
continue
|
||||
dictionary['avatar'] = avatar_hash
|
||||
self.updateCache(client, target, 'avatar', dictionary['avatar'])
|
||||
try:
|
||||
avatar_hash = yield threads.deferToThread(
|
||||
self.savePhoto, client, elem, entity_jid)
|
||||
except (exceptions.DataError, exceptions.NotFound) as e:
|
||||
avatar_hash = ''
|
||||
vcard_dict['avatar'] = avatar_hash
|
||||
except Exception as e:
|
||||
log.error(u"avatar saving error: {}".format(e))
|
||||
avatar_hash = None
|
||||
else:
|
||||
vcard_dict['avatar'] = avatar_hash
|
||||
self.updateCache(client, entity_jid, 'avatar', avatar_hash)
|
||||
else:
|
||||
log.debug(_('FIXME: [%s] VCard tag is not managed yet') % elem.name)
|
||||
log.debug(u'FIXME: [{}] VCard tag is not managed yet'.format(elem.name))
|
||||
|
||||
# if a data in cache doesn't exist anymore, we need to reset it
|
||||
# so we check CACHED_DATA no gotten (i.e. not in dictionary keys)
|
||||
# if a data in cache doesn't exist anymore, we need to delete it
|
||||
# so we check CACHED_DATA no gotten (i.e. not in vcard_dict keys)
|
||||
# and we reset them
|
||||
for datum in CACHED_DATA.difference(dictionary.keys()):
|
||||
log.debug(u"reseting vcard datum [{datum}] for {entity}".format(datum=datum, entity=target.full()))
|
||||
self.updateCache(client, target, datum, '')
|
||||
for datum in CACHED_DATA.difference(vcard_dict.keys()):
|
||||
log.debug(u"reseting vcard datum [{datum}] for {entity}".format(datum=datum, entity=entity_jid.full()))
|
||||
self.updateCache(client, entity_jid, datum, None)
|
||||
|
||||
defer.returnValue(dictionary)
|
||||
defer.returnValue(vcard_dict)
|
||||
|
||||
def _getCardCb(self, iq_elt, to_jid, client):
|
||||
def _vCardCb(self, iq_elt, to_jid, client):
|
||||
"""Called after the first get IQ"""
|
||||
log.debug(_("VCard found"))
|
||||
|
||||
|
|
@ -286,71 +306,89 @@ class XEP_0054(object):
|
|||
d = self.vCard2Dict(client, vcard_elt, from_jid)
|
||||
return d
|
||||
|
||||
def _getCardEb(self, failure_, to_jid, client):
|
||||
def _vCardEb(self, failure_, to_jid, client):
|
||||
"""Called when something is wrong with registration"""
|
||||
log.warning(u"Can't get vCard for {jid}: {failure}".format(jid=to_jid.full, failure=failure_))
|
||||
self.updateCache(client, to_jid, "avatar", '')
|
||||
self.updateCache(client, to_jid, "avatar", None)
|
||||
|
||||
def _getCard(self, target_s, profile_key=C.PROF_KEY_NONE):
|
||||
client = self.host.getClient(profile_key)
|
||||
return self.getCard(client, jid.JID(target_s))
|
||||
|
||||
def getCard(self, client, target):
|
||||
def getCard(self, client, entity_jid):
|
||||
"""Ask server for VCard
|
||||
|
||||
@param target(jid.JID): jid from which we want the VCard
|
||||
@param entity_jid(jid.JID): jid from which we want the VCard
|
||||
@result: id to retrieve the profile
|
||||
"""
|
||||
target_bare = target.userhostJID()
|
||||
to_jid = target if self.isRoom(client, target_bare) else target_bare
|
||||
log.debug(_(u"Asking for %s's VCard") % to_jid.userhost())
|
||||
entity_jid = self.getBareOrFull(client, entity_jid)
|
||||
log.debug(u"Asking for {}'s VCard".format(entity_jid.full()))
|
||||
reg_request = client.IQ('get')
|
||||
reg_request["from"] = client.jid.full()
|
||||
reg_request["to"] = to_jid.full()
|
||||
reg_request["to"] = entity_jid.full()
|
||||
reg_request.addElement('vCard', NS_VCARD)
|
||||
d = reg_request.send(to_jid.full()).addCallbacks(self._getCardCb, self._getCardEb, callbackArgs=[to_jid, client], errbackArgs=[to_jid, client])
|
||||
d = reg_request.send(entity_jid.full()).addCallbacks(self._vCardCb, self._vCardEb, callbackArgs=[entity_jid, client], errbackArgs=[entity_jid, client])
|
||||
return d
|
||||
|
||||
def getAvatarFile(self, avatar_hash):
|
||||
"""Give the full path of avatar from hash
|
||||
@param avatar_hash(unicode): SHA1 hash
|
||||
@return(unicode): full_path or empty string if avatar_hash is empty
|
||||
or if avatar is not found in cache
|
||||
"""
|
||||
if not avatar_hash:
|
||||
return ""
|
||||
filename = self.avatar_path + '/' + avatar_hash
|
||||
if not os.path.exists(filename):
|
||||
log.error(_(u"Asking for an uncached avatar [%s]") % avatar_hash)
|
||||
return ""
|
||||
return filename
|
||||
def _getCardCb(self, dummy, client, entity):
|
||||
try:
|
||||
return client._cache_0054[entity.full()]['avatar']
|
||||
except KeyError:
|
||||
raise Failure(exceptions.NotFound())
|
||||
|
||||
def _getAvatar(self, entity, profile):
|
||||
def _getAvatar(self, entity, cache_only, hash_only, profile):
|
||||
client = self.host.getClient(profile)
|
||||
return self.getAvatar(client, jid.JID(entity))
|
||||
d = self.getAvatar(client, jid.JID(entity))
|
||||
d.addErrback(lambda dummy: '')
|
||||
|
||||
def _getAvatarGotCard(self, dummy, client, entity):
|
||||
try:
|
||||
self.cache[client.profile][entity.full()]['avatar']
|
||||
except KeyError:
|
||||
return ""
|
||||
|
||||
def getAvatar(self, client, entity):
|
||||
try:
|
||||
avatar_hash = self.cache[client.profile][entity.full()]['avatar']
|
||||
except KeyError:
|
||||
d = self.getCard(client, entity)
|
||||
d.addCallback(self._getAvatarGotCard, client, entity)
|
||||
else:
|
||||
d = defer.succeed(avatar_hash)
|
||||
d.addCallback(self.getAvatarFile)
|
||||
return d
|
||||
|
||||
def _buildSetAvatar(self, vcard_set, filepath):
|
||||
def getAvatar(self, client, entity, cache_only=True, hash_only=False):
|
||||
"""get avatar full path or hash
|
||||
|
||||
if avatar is not in local cache, it will be requested to the server
|
||||
@param entity(jid.JID): entity to get avatar from
|
||||
@param cache_only(bool): if False, will request vCard if avatar is
|
||||
not in cache
|
||||
@param hash_only(bool): if True only return hash, not full path
|
||||
@raise exceptions.NotFound: no avatar found
|
||||
"""
|
||||
entity = self.getBareOrFull(client, entity)
|
||||
full_path = None
|
||||
|
||||
try:
|
||||
img = Image.open(filepath)
|
||||
# we first check if we have avatar in cache
|
||||
avatar_hash = client._cache_0054[entity.full()]['avatar']
|
||||
if avatar_hash:
|
||||
# avatar is known and exists
|
||||
full_path = client.cache.getFilePath(avatar_hash)
|
||||
if full_path is None:
|
||||
# cache file is not available (probably expired)
|
||||
raise KeyError
|
||||
else:
|
||||
# avatar has already been checked but it is not set
|
||||
full_path = u''
|
||||
except KeyError:
|
||||
# avatar is not in cache
|
||||
if cache_only:
|
||||
return defer.fail(Failure(exceptions.NotFound()))
|
||||
# we request vCard to get avatar
|
||||
d = self.getCard(client, entity)
|
||||
d.addCallback(self._getCardCb, client, entity)
|
||||
else:
|
||||
# avatar is in cache, we can return hash
|
||||
d = defer.succeed(avatar_hash)
|
||||
|
||||
if not hash_only:
|
||||
# full path is requested
|
||||
if full_path is None:
|
||||
d.addCallback(client.cache.getFilePath)
|
||||
else:
|
||||
d.addCallback(lambda dummy: full_path)
|
||||
return d
|
||||
|
||||
def _buildSetAvatar(self, client, vcard_set, file_path):
|
||||
# XXX: this method is executed in a separate thread
|
||||
try:
|
||||
img = Image.open(file_path)
|
||||
except IOError:
|
||||
return Failure(exceptions.DataError("Can't open image"))
|
||||
return Failure(exceptions.DataError(u"Can't open image"))
|
||||
|
||||
if img.size != AVATAR_DIM:
|
||||
img.thumbnail(AVATAR_DIM, Image.ANTIALIAS)
|
||||
|
|
@ -372,24 +410,34 @@ class XEP_0054(object):
|
|||
photo_elt = vcard_elt.addElement('PHOTO')
|
||||
photo_elt.addElement('TYPE', content='image/png')
|
||||
photo_elt.addElement('BINVAL', content=b64encode(img_buf.getvalue()))
|
||||
img_hash = sha1(img_buf.getvalue()).hexdigest()
|
||||
self.saveAvatarFile(img_buf.getvalue(), img_hash)
|
||||
return (vcard_set, img_hash)
|
||||
image_hash = sha1(img_buf.getvalue()).hexdigest()
|
||||
with client.cache.cacheData(
|
||||
PLUGIN_INFO['import_name'],
|
||||
image_hash,
|
||||
"image/png",
|
||||
MAX_AGE
|
||||
) as f:
|
||||
f.write(img_buf.getvalue())
|
||||
return vcard_set, image_hash
|
||||
|
||||
def setAvatar(self, filepath, profile_key=C.PROF_KEY_NONE):
|
||||
def _setAvatar(self, file_path, profile_key=C.PROF_KEY_NONE):
|
||||
client = self.host.getClient(profile_key)
|
||||
return self.setAvatar(client, file_path)
|
||||
|
||||
def setAvatar(self, client, file_path):
|
||||
"""Set avatar of the profile
|
||||
@param filepath: path of the image of the avatar"""
|
||||
|
||||
@param file_path: path of the image of the avatar
|
||||
"""
|
||||
#TODO: This is a temporary way of setting the avatar, as other VCard information is not managed.
|
||||
# A proper full VCard management should be done (and more generaly a public/private profile)
|
||||
client = self.host.getClient(profile_key)
|
||||
|
||||
vcard_set = IQ(client.xmlstream, 'set')
|
||||
d = threads.deferToThread(self._buildSetAvatar, vcard_set, filepath)
|
||||
vcard_set = client.IQ()
|
||||
d = threads.deferToThread(self._buildSetAvatar, client, vcard_set, file_path)
|
||||
|
||||
def elementBuilt(result):
|
||||
"""Called once the image is at the right size/format, and the vcard set element is build"""
|
||||
set_avatar_elt, img_hash = result
|
||||
self.updateCache(client, client.jid.userhostJID(), 'avatar', img_hash)
|
||||
set_avatar_elt, image_hash = result
|
||||
self.updateCache(client, client.jid.userhostJID(), 'avatar', image_hash)
|
||||
return set_avatar_elt.send().addCallback(lambda ignore: client.presence.available()) # FIXME: should send the current presence, not always "available" !
|
||||
|
||||
d.addCallback(elementBuilt)
|
||||
|
|
@ -413,15 +461,24 @@ class XEP_0054_handler(XMPPHandler):
|
|||
def getDiscoItems(self, requestor, target, nodeIdentifier=''):
|
||||
return []
|
||||
|
||||
def _checkAvatarHash(self, dummy, client, entity, given_hash):
|
||||
"""check that hash in cash (i.e. computed hash) is the same as given one"""
|
||||
# XXX: if they differ, the avater will be requested on each connection
|
||||
# TODO: try to avoid re-requesting avatar in this case
|
||||
computed_hash = self.plugin_parent.getCache(client, entity, 'avatar')
|
||||
if computed_hash != given_hash:
|
||||
log.warning(u"computed hash differs from given hash for {entity}:\n"
|
||||
"computed: {computed}\ngiven: {given}".format(
|
||||
entity=entity, computed=computed_hash, given=given_hash))
|
||||
|
||||
def update(self, presence):
|
||||
"""Called on <presence/> stanza with vcard data
|
||||
|
||||
Check for avatar information, and get VCard if needed
|
||||
@param presend(domish.Element): <presence/> stanza
|
||||
"""
|
||||
from_jid = jid.JID(presence['from'])
|
||||
if from_jid.resource and not self.plugin_parent.isRoom(self.parent, from_jid):
|
||||
from_jid = from_jid.userhostJID()
|
||||
client = self.parent
|
||||
entity_jid = self.plugin_parent.getBareOrFull(client, jid.JID(presence['from']))
|
||||
#FIXME: wokkel's data_form should be used here
|
||||
try:
|
||||
x_elt = presence.elements(NS_VCARD_UPDATE, 'x').next()
|
||||
|
|
@ -433,22 +490,35 @@ class XEP_0054_handler(XMPPHandler):
|
|||
except StopIteration:
|
||||
return
|
||||
|
||||
hash_ = str(photo_elt)
|
||||
if not hash_:
|
||||
return
|
||||
old_avatar = self.plugin_parent.getCache(self.parent, from_jid, 'avatar')
|
||||
filename = self.plugin_parent._getFilename(hash_)
|
||||
if not old_avatar or old_avatar != hash_:
|
||||
if os.path.exists(filename):
|
||||
log.debug(u"New avatar found for [{}], it's already in cache, we use it".format(from_jid.full()))
|
||||
self.plugin_parent.updateCache(self.parent, from_jid, 'avatar', hash_)
|
||||
else:
|
||||
log.debug(u'New avatar found for [{}], requesting vcard'.format(from_jid.full()))
|
||||
self.plugin_parent.getCard(self.parent, from_jid)
|
||||
else:
|
||||
if os.path.exists(filename):
|
||||
log.debug(u"avatar for {} already in cache".format(from_jid.full()))
|
||||
else:
|
||||
log.error(u"Avatar for [{}] should be in cache but it is not ! We get it".format(from_jid.full()))
|
||||
self.plugin_parent.getCard(self.parent, from_jid)
|
||||
hash_ = unicode(photo_elt).strip()
|
||||
if hash_ == C.HASH_SHA1_EMPTY:
|
||||
hash_ = u''
|
||||
old_avatar = self.plugin_parent.getCache(client, entity_jid, 'avatar')
|
||||
|
||||
if old_avatar == hash_:
|
||||
# no change, we can return...
|
||||
if hash_:
|
||||
# ...but we double check that avatar is in cache
|
||||
file_path = client.cache.getFilePath(hash_)
|
||||
if file_path is None:
|
||||
log.error(u"Avatar for [{}] should be in cache but it is not! We get it".format(entity_jid.full()))
|
||||
self.plugin_parent.getCard(client, entity_jid)
|
||||
else:
|
||||
log.debug(u"avatar for {} already in cache".format(entity_jid.full()))
|
||||
return
|
||||
|
||||
if not hash_:
|
||||
# the avatar has been removed
|
||||
# XXX: we use empty string instead of None to indicate that we took avatar
|
||||
# but it is empty on purpose
|
||||
self.plugin_parent.updateCache(client, entity_jid, 'avatar', '')
|
||||
return
|
||||
|
||||
file_path = client.cache.getFilePath(hash_)
|
||||
if file_path is not None:
|
||||
log.debug(u"New avatar found for [{}], it's already in cache, we use it".format(entity_jid.full()))
|
||||
self.plugin_parent.updateCache(client, entity_jid, 'avatar', hash_)
|
||||
else:
|
||||
log.debug(u'New avatar found for [{}], requesting vcard'.format(entity_jid.full()))
|
||||
d = self.plugin_parent.getCard(client, entity_jid)
|
||||
d.addCallback(self._checkAvatarHash, client, entity_jid, hash_)
|
||||
|
|
|
|||
Loading…
Reference in a new issue