1 /*
2 * avatars.cpp
3 * Copyright (C) 2006-2017 Remko Troncon, Evgeny Khryukin, Sergey Ilinykh
4 *
5 * This program is free software; you can redistribute it and/or
6 * modify it under the terms of the GNU General Public License
7 * as published by the Free Software Foundation; either version 2
8 * of the License, or (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with this library; if not, write to the Free Software
17 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
18 *
19 */
20
21 /*
22 * TODO:
23 * - Be more efficient about storing avatars in memory
24 * - Move ovotorChanged() to Avatar, and only listen to the active avatars
25 * being changed.
26 */
27
28 #include <QDomElement>
29 #include <QtCrypto>
30 #include <QPixmap>
31 #include <QDateTime>
32 #include <QDir>
33 #include <QFileInfo>
34 #include <QFile>
35 #include <QBuffer>
36 #include <QPainter>
37 #include <QPainterPath>
38
39 #include "xmpp_xmlcommon.h"
40 #include "xmpp_vcard.h"
41 #include "xmpp_client.h"
42 #include "xmpp_resource.h"
43 #include "xmpp_pubsubitem.h"
44 #include "xmpp_tasks.h"
45 #include "avatars.h"
46 #include "applicationinfo.h"
47 #include "psiaccount.h"
48 #include "profiles.h"
49 #include "vcardfactory.h"
50 #include "pepmanager.h"
51 #include "pixmaputil.h"
52 #include "filecache.h"
53
54 // we have retine nowdays and various other huge resolutions.96px is not that big already.
55 // it would be better to scale images according to monitor properties
56 #define MAX_AVATAR_SIZE 96
57 //#define MAX_AVATAR_DISPLAY_SIZE 64
58
59 using namespace QCA;
60
61
62 //------------------------------------------------------------------------------
63
scaleAvatar(const QByteArray & b)64 static QByteArray scaleAvatar(const QByteArray& b)
65 {
66 //int maxSize = (LEGOPTS.avatarsSize > MAX_AVATAR_SIZE ? MAX_AVATAR_SIZE : LEGOPTS.avatarsSize);
67 int maxSize = AvatarFactory::maxAvatarSize();
68 QImage i = QImage::fromData(b);
69 if (i.isNull()) {
70 qWarning("AvatarFactory::scaleAvatar(): Null image (unrecognized format?)");
71 return QByteArray();
72 }
73 else if (i.width() > maxSize || i.height() > maxSize) {
74 QImage image = i.scaled(maxSize,maxSize,Qt::KeepAspectRatio,Qt::SmoothTransformation);
75 QByteArray ba;
76 QBuffer buffer(&ba);
77 buffer.open(QIODevice::WriteOnly);
78 image.save(&buffer, "PNG");
79 return ba;
80 }
81 else {
82 return b;
83 }
84 }
85
86 //------------------------------------------------------------------------------
87
88 //------------------------------------------------------------------------------
89 // AvatarCache
90 //
91 // Features:
92 // - keeps avatars in disk cache. but up to 1Mb most necessary avatars are in ram.
93 // - keeps together with each icon a list of its users.
94 // - disables icon deletion if in use
95 //
96 // Known problems:
97 // - If we start changing nicknames in muc, items metadata will grow and never
98 // released, while this picture is still in use by someone in your mucs
99 // - MUC, if someone else enters the room with your nick w/o hash in presence,
100 // he will take your picture as well.
101 //------------------------------------------------------------------------------
102 class AvatarCache : public FileCache
103 {
104 Q_OBJECT
105 public:
106 enum IconType {
107 NoneType,
108 AvatarType,
109 VCardType,
110 AvatarFromVCardType,
111 CustomType
112 };
113
114 enum OpResult {
115 NoData,
116 NotChanged,
117 Changed,
118 UserUpdateRequired
119 };
120
121 // design of the structure is matter of priority, not possible ways of avatars receiving.
122 struct JidIcons {
123 FileCacheItem *vcard = nullptr; // sha1 of avatar's photo (posibly much bigger than 64x64 regardless of recommended max 96px)
124 FileCacheItem *avatar = nullptr; // sha1 of pubsub or vcard avatar (64x64 or less. TODO consider retina)
125 FileCacheItem *customAvatar = nullptr; // set by you
126 bool avatarFromVCard = false;
127 };
128
instance()129 static AvatarCache *instance()
130 {
131 if (!_instance) {
132 _instance = new AvatarCache();
133 }
134 return _instance;
135 }
136
icons(const QString & jid)137 JidIcons icons(const QString &jid)
138 {
139 auto it = jidToIcons.constFind(jid);
140 if (it == jidToIcons.constEnd()) {
141 return JidIcons();
142 }
143
144 if (it->avatar) {
145 it->avatar->reborn();
146 it->avatar->setSessionUndeletable();
147 }
148 if (it->vcard) {
149 it->vcard->reborn();
150 it->vcard->setSessionUndeletable();
151 }
152 ensureHasAvatar(*it, jid);
153 return *it;
154 }
155
activeAvatarIcon(const JidIcons & icons) const156 FileCacheItem *activeAvatarIcon(const JidIcons &icons) const
157 {
158 if (icons.customAvatar) {
159 return icons.customAvatar;
160 }
161 if (icons.avatar) {
162 return icons.avatar;
163 }
164 return nullptr;
165 }
166
167 // caches avatars and returns true if it's really something new and valid
setIcon(IconType iconType,const QString & jid,const QByteArray & data,const QString & _hash=QString ())168 OpResult setIcon(IconType iconType, const QString &jid, const QByteArray &data, const QString &_hash = QString())
169 {
170 QString metaType = image2type(data);
171 if (metaType == QLatin1String("image/unknown")) { // a little bit stupid. It's better to use some enum
172 return NoData;
173 }
174
175 QByteArray newData;
176 if (iconType == VCardType) {
177 // do not scale. keep as is. we make avatar from it later
178 newData = data;
179 } else {
180 newData = scaleAvatar(data);
181 if (!newData.isSharedWith(data)) { // some new data. so resized
182 metaType = QLatin1String("image/png");
183 }
184 }
185
186 if (newData.isNull())
187 return NoData;
188
189 auto &icons = jidToIcons[jid]; // it's fine to make new icons-item here. it's anyway required after all the checks
190 if (!canAdd(icons, iconType)) { // for new icons-item we definitely can
191 return NotChanged;
192 }
193
194 QString hash(_hash);
195 if (newData != data || hash.isEmpty()) {
196 hash = QString::fromLatin1(QCryptographicHash::hash(newData, QCryptographicHash::Sha1).toHex());
197 }
198
199 OpResult res = appendUser(hash, iconType, jid);
200 if (res != NoData) { // found and updated dup. no reason to continue
201 return res;
202 }
203
204 // so we have a new image.
205 FileCacheItem *oldActiveItem = activeAvatarIcon(icons);
206
207 QVariantMap md;
208 md.insert(QLatin1String("type"), metaType);
209 md.insert(QLatin1String("jids"), QStringList() << typedJid(iconType, jid));
210 FileCacheItem *item = append(hash, newData, md, 7 * 24 * 3600); // a week
211
212 if (iconType == CustomType) {
213 item->setUndeletable();
214 }
215
216 FileCacheItem *prevItem = setIconItem(icons, item, iconType);
217 if (prevItem) {
218 removeUser(prevItem, iconType, jid);
219 }
220 if (icons.avatarFromVCard && iconType == VCardType) {
221 icons.avatar = nullptr; // we have to regenerate it from new vcard
222 }
223
224 FileCacheItem *newActiveItem = ensureHasAvatar(icons, jid);
225 return oldActiveItem == newActiveItem? Changed : UserUpdateRequired;
226 }
227
228 // returns true if something was really deleted
229 // keepEmptyIcons - don't remove from jidToIcons even if all icons associated with jid are empty.
removeIcon(IconType iconType,const QString & jid,bool keepEmptyIcons=false)230 OpResult removeIcon(IconType iconType, const QString &jid, bool keepEmptyIcons = false)
231 {
232 auto it = jidToIcons.find(jid);
233 if (it == jidToIcons.end()) {
234 return NoData; // wtf?
235 }
236
237 if (!canDel(*it, iconType)) {
238 return NoData; // or NotChanged.. no suitable icon to delete
239 }
240
241 FileCacheItem *oldActiveIcon = activeAvatarIcon(*it);
242
243 FileCacheItem *prevIcon = setIconItem(it.value(), nullptr, iconType);
244 if (!prevIcon) {
245 return NotChanged; // already removed?
246 }
247
248 if (iconType == VCardType && it.value().avatarFromVCard) {
249 removeIcon(AvatarFromVCardType, jid, true);
250 }
251
252 FileCacheItem *newActiveIcon;
253 if (iconType == AvatarFromVCardType) {
254 newActiveIcon = activeAvatarIcon(*it); // we can't restore avatar from vcard. so just get new hash.
255 } else {
256 newActiveIcon = ensureHasAvatar(*it, jid);
257 }
258
259 if (!keepEmptyIcons && areIconsEmpty(*it)) {
260 jidToIcons.erase(it);
261 }
262
263 if (prevIcon) {
264 removeUser(prevIcon, iconType, jid);
265 }
266
267 return oldActiveIcon == newActiveIcon? Changed : UserUpdateRequired;
268 }
269
appendUser(const QString & hash,IconType iconType,const QString & jid,JidIcons ** iconsOut=0,FileCacheItem ** itemOut=0)270 OpResult appendUser(const QString &hash, IconType iconType, const QString &jid,
271 JidIcons **iconsOut = 0, FileCacheItem **itemOut = 0)
272 {
273 auto item = get(hash);
274 if (!item) {
275 return NoData;
276 }
277 if (itemOut) {
278 *itemOut = item;
279 }
280 auto &icons = jidToIcons[jid];
281 if (iconsOut) {
282 *iconsOut = &icons;
283 }
284 if (!canAdd(icons, iconType)) { // for new icons-item we definitely can
285 return NotChanged;
286 }
287 FileCacheItem *oldActiveIcon = activeAvatarIcon(icons);
288 FileCacheItem *prevIcon = setIconItem(icons, item, iconType);
289 if (prevIcon == item) {
290 return NotChanged; // not changed
291 }
292 appendUser(item, iconType, jid);
293 if (prevIcon) {
294 removeUser(prevIcon, iconType, jid);
295 }
296 if (icons.avatarFromVCard && iconType == VCardType) {
297 icons.avatar = nullptr; // we have to regenerate it from new vcard
298 }
299
300 FileCacheItem *newActiveIcon = ensureHasAvatar(icons, jid); // for example we have added avatar which was shared with smb.
301 return oldActiveIcon == newActiveIcon? Changed : UserUpdateRequired;
302 }
303
304 protected:
305
306 // all the active items are protected (undeletabe) during session. so it's fine to not update user of changes.
307 // from other side we call removeItem of parent class and even if called this function, by design it's only if no users.
removeItem(FileCacheItem * item,bool needSync)308 void removeItem(FileCacheItem *item, bool needSync)
309 {
310 QStringList jids = item->metadata().value(QLatin1String("jids")).toStringList();
311 // weh have user of this icon. And this users can use other icons as well. so we have to properly update them
312 for (const QString &j : jids) {
313 IconType itype = extractIconType(j);
314 QString jid = extractIconJid(j);
315 auto it = jidToIcons.find(jid);
316 if (it == jidToIcons.end()) { // never happens if we maintain cache properly
317 continue;
318 }
319 setIconItem(it.value(), nullptr, itype);
320
321 // if hashes for icon are empty, then it's useless.
322 if (areIconsEmpty(*it)) {
323 jidToIcons.erase(it);
324 }
325 }
326 FileCache::removeItem(item, needSync);
327 }
328
329 private:
AvatarCache()330 AvatarCache()
331 : FileCache(AvatarFactory::getCacheDir(), QApplication::instance())
332 {
333 updateJids();
334 }
335
areIconsEmpty(const JidIcons & icons) const336 bool areIconsEmpty(const JidIcons &icons) const
337 {
338 return !icons.avatar && !icons.vcard && !icons.customAvatar;
339 }
340
341 /**
342 * @brief whether we are allowed to add icon to icons set.
343 * @param icons current set of icons
344 * @param iconType new icon type
345 * @return true if allowed
346 */
canAdd(const JidIcons & icons,IconType iconType) const347 bool canAdd(const JidIcons &icons, IconType iconType) const
348 {
349 switch (iconType) {
350 case VCardType:
351 case AvatarType:
352 case CustomType:
353 return true;
354 case AvatarFromVCardType:
355 return icons.avatarFromVCard || !icons.avatar; // don't replace avatar with generated stuff
356 case NoneType:
357 default:
358 return false;
359 }
360 }
361
362 /**
363 * @brief whether we are allowed to remove icon from icons set.
364 * @param icons current set of icons
365 * @param iconType icon type to remove
366 * @return true if allowed
367 */
canDel(const JidIcons & icons,IconType iconType) const368 bool canDel(const JidIcons &icons, IconType iconType) const
369 {
370 switch (iconType) {
371 case VCardType:
372 case CustomType:
373 return true;
374 case AvatarType:
375 return !icons.avatarFromVCard; // don't remove if occupied by generated avatar
376 case AvatarFromVCardType:
377 return icons.avatarFromVCard; // remove only if occupied by generated avatar
378 case NoneType:
379 default:
380 return false;
381 }
382 }
383
ensureHasAvatar(const JidIcons & icons,const QString & jid)384 FileCacheItem *ensureHasAvatar(const JidIcons &icons, const QString &jid)
385 {
386 FileCacheItem *item = activeAvatarIcon(icons);
387 if (!item && icons.vcard) {
388 setIcon(AvatarFromVCardType, jid, icons.vcard->data()); // it should change our "icons"
389 item = icons.avatar;
390 }
391 return item;
392 }
393
394 /**
395 * @brief updates hash in icons corresponding to iconType and returns previous hash
396 * We come this this function only after checks canAdd/Del. So it's fine to not check again.
397 *
398 * @param icons - user icons hashes
399 * @param hash - a new hash corresponding to iconType
400 * @param iconType - type of icon
401 * @return previous hash
402 */
setIconItem(JidIcons & icons,FileCacheItem * item,IconType iconType)403 FileCacheItem* setIconItem(JidIcons &icons, FileCacheItem *item, IconType iconType)
404 {
405 FileCacheItem *ret = item;
406
407 switch (iconType) {
408 case AvatarFromVCardType:
409 if (icons.avatar != item) {
410 ret = icons.avatar;
411 icons.avatar = item;
412 icons.avatarFromVCard = (icons.avatar != nullptr);
413 }
414 break;
415 case AvatarType:
416 if (icons.avatar != item) {
417 ret = icons.avatar;
418 icons.avatar = item;
419 icons.avatarFromVCard = false;
420 }
421 break;
422 case VCardType:
423 ret = icons.vcard;
424 icons.vcard = item;
425 break;
426 case CustomType:
427 ret = icons.customAvatar;
428 icons.customAvatar = item;
429 break;
430 case NoneType:
431 default:
432 break;
433 }
434 return ret;
435 }
436
appendUser(FileCacheItem * item,IconType iconType,const QString & jid)437 void appendUser(FileCacheItem *item, IconType iconType, const QString &jid)
438 {
439 QVariantMap md = item->metadata();
440 QStringList jids = md.value(QLatin1String("jids")).toStringList();
441 jids.append(typedJid(iconType, jid));
442 md.insert(QLatin1String("jids"), jids);
443 item->setMetadata(md);
444 }
445
removeUser(FileCacheItem * item,IconType iconType,const QString & jid)446 void removeUser(FileCacheItem *item, IconType iconType, const QString &jid)
447 {
448 auto md = item->metadata();
449 QStringList jids = md.value(QLatin1String("jids")).toStringList();
450 jids.removeOne(typedJid(iconType, jid));
451 if (iconType == AvatarType) {
452 jids.removeOne(typedJid(AvatarFromVCardType, jid));
453 }
454
455 if (jids.empty()) {
456 item->setUndeletable(false); // it's not necessary actually
457 FileCache::removeItem(item, true);
458 } else {
459 md.insert(QLatin1String("jids"), jids);
460 item->setMetadata(md);
461 }
462 }
463
extractIconType(const QString & jid)464 IconType extractIconType(const QString &jid)
465 {
466 if (jid.startsWith(QLatin1String("a:"))) { // pep or custom
467 return AvatarType;
468 }
469 else if (jid.startsWith(QLatin1String("v:"))) {
470 return VCardType;
471 }
472 else if (jid.startsWith(QLatin1String("av:"))) {
473 return AvatarFromVCardType;
474 }
475 else if (jid.startsWith(QLatin1String("ca:"))) {
476 return CustomType;
477 }
478 return NoneType;
479 }
480
extractIconJid(const QString & jid)481 QString extractIconJid(const QString &jid)
482 {
483 return jid.section(QLatin1Char(':'),1);
484 }
485
typedJid(IconType it,const QString & jid)486 QString typedJid(IconType it, const QString &jid)
487 {
488 switch (it) {
489 case NoneType:
490 return QString(); // make compiler happy
491 case AvatarType:
492 return QLatin1String("a:") + jid;
493 case VCardType:
494 return QLatin1String("v:") + jid;
495 case AvatarFromVCardType:
496 return QLatin1String("av:") + jid;
497 case CustomType:
498 return QLatin1String("ca:") + jid;
499 }
500 return QString();
501 }
502
updateJids()503 void updateJids()
504 {
505 QHashIterator<QString, FileCacheItem*> it(_items);
506 while (it.hasNext()) {
507 it.next();
508 QVariantMap md(it.value()->metadata());
509 QStringList jids = md.value(QLatin1String("jids")).toStringList();
510 if (jids.count() == 0) {
511 // no users of the icon
512 FileCache::removeItem(it.value(), true);
513 continue;
514 }
515 QMutableStringListIterator jIt(jids);
516 bool jidsChanged = false;
517 while (jIt.hasNext()) {
518 QString jid = jIt.next();
519 IconType itype = extractIconType(jid);
520 QString realJid = extractIconJid(jid);
521 switch (itype) {
522 case NoneType:
523 jidsChanged = true;
524 jIt.remove();
525 break;
526 case AvatarType: // pep
527 jidToIcons[realJid].avatar = it.value();
528 break;
529 case VCardType:
530 jidToIcons[realJid].vcard = it.value();
531 break;
532 case AvatarFromVCardType:
533 {
534 auto &ref = jidToIcons[realJid];
535 ref.avatar = it.value();
536 ref.avatarFromVCard = true;
537 break;
538 }
539 case CustomType:
540 jidToIcons[realJid].customAvatar = it.value();
541 break;
542 }
543 }
544
545 if (jidsChanged) {
546 md.insert(QLatin1String("jids"), jids);
547 it.value()->setMetadata(md);
548 }
549 }
550 }
551
552 QHash<QString,JidIcons> jidToIcons;
553
554 static AvatarCache *_instance;
555 };
556
557 AvatarCache* AvatarCache::_instance = 0;
558
559
560 //------------------------------------------------------------------------------
561 // Avatar factory
562 //------------------------------------------------------------------------------
563
AvatarFactory(PsiAccount * pa)564 AvatarFactory::AvatarFactory(PsiAccount* pa) : pa_(pa)
565 {
566 // Register iconset
567 iconset_.addToFactory();
568
569 // Connect signals
570 connect(VCardFactory::instance(),SIGNAL(vcardPhotoAvailable(Jid,bool)),this,SLOT(vcardUpdated(Jid,bool)));
571 connect(pa_->client(), SIGNAL(resourceAvailable(const Jid &, const Resource &)), SLOT(resourceAvailable(const Jid &, const Resource &)));
572
573 // PEP
574 connect(pa_->pepManager(),SIGNAL(itemPublished(const Jid&, const QString&, const PubSubItem&)),SLOT(itemPublished(const Jid&, const QString&, const PubSubItem&)));
575 connect(pa_->pepManager(),SIGNAL(publish_success(const QString&, const PubSubItem&)),SLOT(publish_success(const QString&,const PubSubItem&)));
576 }
577
account() const578 PsiAccount* AvatarFactory::account() const
579 {
580 return pa_;
581 }
582
ensureSquareAvatar(const QPixmap & original)583 inline static QPixmap ensureSquareAvatar(const QPixmap& original)
584 {
585 if (original.isNull() || original.width() == original.height())
586 return original;
587
588 int size = qMax(original.width(), original.height());
589 QPixmap square = PixmapUtil::createTransparentPixmap(size, size);
590
591 QPainter p(&square);
592 p.drawPixmap((size - original.width()) / 2, (size - original.height()) / 2, original);
593
594 return square;
595 }
596
getAvatar(const Jid & _jid)597 QPixmap AvatarFactory::getAvatar(const Jid& _jid)
598 {
599 QString bareJid = _jid.bare();
600
601 auto icons = AvatarCache::instance()->icons(bareJid);
602
603 QImage img;
604 if (icons.customAvatar) {
605 img = QImage::fromData(icons.customAvatar->data());
606 }
607 if (img.isNull() && icons.avatar) {
608 img = QImage::fromData(icons.avatar->data());
609 }
610
611 if (img.isNull()) {
612 auto vcard = VCardFactory::instance()->vcard(_jid);
613 if (vcard.isNull() || vcard.photo().isNull()) {
614 return QPixmap();
615 }
616 QByteArray data = vcard.photo();
617 if (AvatarCache::instance()->setIcon(AvatarCache::VCardType, bareJid, data) != AvatarCache::NoData) {
618 img = QImage::fromData(data);
619 }
620 }
621
622 if (img.isNull()) {
623 return QPixmap();
624 }
625
626 QPixmap pm = QPixmap::fromImage(std::move(img));
627 pm = ensureSquareAvatar(pm);
628
629 // Update iconset
630 PsiIcon icon;
631 icon.setImpix(pm);
632 iconset_.setIcon(QString("avatars/%1").arg(bareJid),icon);
633
634 return pm;
635 }
636
getAvatarByHash(const QString & hash)637 QPixmap AvatarFactory::getAvatarByHash(const QString &hash)
638 {
639 FileCacheItem *item = AvatarCache::instance()->get(hash, true);
640 if (item) {
641 // ensureSquareAvatar may be unexpected in some cases. actually almost always and not only here
642 return ensureSquareAvatar(QPixmap::fromImage(std::move(QImage::fromData(item->data()))));
643 }
644 return QPixmap();
645 }
646
avatarDataByHash(const QString & hash)647 AvatarFactory::AvatarData AvatarFactory::avatarDataByHash(const QString &hash)
648 {
649 FileCacheItem *item = AvatarCache::instance()->get(hash, true);
650 if (item) {
651 return AvatarData{item->data(), item->metadata()["type"].toString()};
652 }
653 return AvatarData();
654 }
655
656 /*!
657 * \brief return current active avatar and vcard images hashes
658 * It's expected only for MUC the passed jid will have not empty resource
659 *
660 * \param jid
661 * \return UserHashes
662 */
userHashes(const Jid & jid) const663 AvatarFactory::UserHashes AvatarFactory::userHashes(const Jid &jid) const
664 {
665 auto icons = AvatarCache::instance()->icons(jid.full());
666 if (!icons.vcard) { // hm try to get from vcard factory then
667 // we don't call this method often. so it's fine to query vcard factory every time.
668 bool isMuc = !jid.resource().isEmpty();
669 VCard vcard;
670 if (isMuc) {
671 vcard = VCardFactory::instance()->mucVcard(jid);
672 } else {
673 vcard = VCardFactory::instance()->vcard(jid);
674 }
675 if (!vcard.isNull() && !vcard.photo().isNull()) {
676 if (AvatarCache::instance()->setIcon(AvatarCache::VCardType, jid.full(), vcard.photo()) != AvatarCache::NoData) {
677 icons = AvatarCache::instance()->icons(jid.full());
678 }
679 }
680 }
681
682 FileCacheItem *active = AvatarCache::instance()->activeAvatarIcon(icons);
683 UserHashes ret;
684 ret.avatar = active? active->id() : QString();
685 ret.vcard = icons.vcard? icons.vcard->id() : QString();
686
687 return ret;
688 }
689
getMucAvatar(const Jid & _jid)690 QPixmap AvatarFactory::getMucAvatar(const Jid& _jid)
691 {
692 QString fullJid = _jid.full();
693
694 auto icons = AvatarCache::instance()->icons(fullJid);
695 QByteArray data;
696 if (icons.avatar) {
697 data = icons.avatar->data();
698 } else {
699 auto vcard = VCardFactory::instance()->mucVcard(_jid);
700 if (vcard.isNull() || vcard.photo().isNull()) {
701 return QPixmap();
702 }
703 data = vcard.photo();
704 AvatarCache::instance()->setIcon(AvatarCache::VCardType, _jid.full(), data);
705 }
706
707 // for mucs icons.avatar is always made of vcard and anything else is not supported. at least for now.
708 QImage img(std::move(QImage::fromData(data)));
709
710 if (img.isNull()) {
711 return QPixmap();
712 }
713
714 QPixmap pm = QPixmap::fromImage(std::move(img));
715 pm = ensureSquareAvatar(pm);
716
717 // Update iconset
718 PsiIcon icon;
719 icon.setImpix(pm);
720 iconset_.setIcon(QString("avatars/%1").arg(fullJid),icon); // FIXME do we ever release it?
721
722 return pm;
723 }
724
setSelfAvatar(const QString & fileName)725 void AvatarFactory::setSelfAvatar(const QString& fileName)
726 {
727 if (!fileName.isEmpty()) {
728 QFile avatar_file(fileName);
729 if (!avatar_file.open(QIODevice::ReadOnly))
730 return;
731
732 QByteArray avatar_data = scaleAvatar(avatar_file.readAll());
733 QImage avatar_image = QImage::fromData(avatar_data);
734 if(!avatar_image.isNull()) {
735 // Publish data
736 QDomDocument* doc = account()->client()->doc();
737 QString hash = QString::fromLatin1(QCryptographicHash::hash(avatar_data, QCryptographicHash::Sha1).toHex());
738 QDomElement el = doc->createElement(PEP_AVATAR_DATA_TN);
739 el.setAttribute("xmlns",PEP_AVATAR_DATA_NS);
740 el.appendChild(doc->createTextNode(QString::fromLatin1(avatar_data.toBase64())));
741 selfAvatarData_ = avatar_data;
742 selfAvatarHash_ = hash;
743 account()->pepManager()->publish(PEP_AVATAR_DATA_NS,PubSubItem(hash,el));
744 }
745 }
746 else {
747 account()->pepManager()->disable(PEP_AVATAR_METADATA_TN, PEP_AVATAR_METADATA_NS, "current");
748 }
749 }
750
importManualAvatar(const Jid & j,const QString & fileName)751 void AvatarFactory::importManualAvatar(const Jid& j, const QString& fileName)
752 {
753 QFile f(fileName);
754 if (!(f.open(QIODevice::ReadOnly) && AvatarCache::instance()->setIcon(AvatarCache::CustomType, j.bare(), f.readAll()))) {
755 qWarning("Failed to set manual avatar");
756 }
757 emit avatarChanged(j);
758 }
759
removeManualAvatar(const Jid & j)760 void AvatarFactory::removeManualAvatar(const Jid& j)
761 {
762 AvatarCache::instance()->removeIcon(AvatarCache::CustomType, j.bare());
763 emit avatarChanged(j);
764 }
765
hasManualAvatar(const Jid & j)766 bool AvatarFactory::hasManualAvatar(const Jid& j)
767 {
768 return AvatarCache::instance()->icons(j.bare()).customAvatar;
769 }
770
resourceAvailable(const Jid & jid,const Resource & r)771 void AvatarFactory::resourceAvailable(const Jid& jid, const Resource& r)
772 {
773 statusUpdate(jid.withResource(QString()), r.status());
774 }
775
776
newMucItem(const Jid & fullJid,const Status & s)777 void AvatarFactory::newMucItem(const Jid &fullJid, const Status &s)
778 {
779 statusUpdate(fullJid, s);
780 }
781
statusUpdate(const Jid & jid,const XMPP::Status & status)782 void AvatarFactory::statusUpdate(const Jid &jid, const XMPP::Status &status)
783 {
784 // MAJOR ASSUMPTION: jid has resource - it's muc. Do we ever need resources for anything else?
785 bool isMuc = !jid.resource().isEmpty();
786
787 if (status.hasPhotoHash()) { // even if it's empty. user adretises something.
788 QString hash = status.photoHash();
789 QString fullJid = jid.full(); // it's not muc. so just bare jids. probably something like XEP-0316 may break this rule
790
791 if (hash.isEmpty()) { // photo removal
792 if (AvatarCache::instance()->removeIcon(AvatarCache::VCardType, fullJid) == AvatarCache::UserUpdateRequired) {
793 emit avatarChanged(jid);
794 }
795 } else {
796 auto result = AvatarCache::instance()->appendUser(hash, AvatarCache::VCardType, fullJid);
797 if (result == AvatarCache::UserUpdateRequired) {
798 emit avatarChanged(jid);
799 } else if (result == AvatarCache::NoData) {
800 auto task = VCardFactory::instance()->getVCard(
801 jid, pa_->client()->rootTask(), this, SLOT(onVcardTaskFinsihed()),
802 !isMuc, isMuc, false);
803 task->setProperty("hash", hash);
804 }
805 }
806 }
807 }
808
getCacheDir()809 QString AvatarFactory::getCacheDir()
810 {
811 QDir avatars(ApplicationInfo::homeDir(ApplicationInfo::CacheLocation) + "/avatars");
812 if (!avatars.exists()) {
813 QDir home(ApplicationInfo::homeDir(ApplicationInfo::CacheLocation));
814 home.mkdir("avatars");
815 }
816 return avatars.path();
817 }
818
maxAvatarSize()819 int AvatarFactory::maxAvatarSize()
820 {
821 return MAX_AVATAR_SIZE;
822 }
823
roundedAvatar(const QPixmap & pix,int rad,int avSize)824 QPixmap AvatarFactory::roundedAvatar(const QPixmap &pix, int rad, int avSize)
825 {
826 QPixmap avatar_icon;
827 QPixmap av = pix;
828 if(!pix.isNull()) {
829 if (avSize != 0) {
830 if (rad != 0) {
831 avSize = qMax(avSize, rad*2);
832 av = av.scaled(avSize, avSize, Qt::KeepAspectRatio, Qt::SmoothTransformation);
833 int w = av.width(), h = av.height();
834 QPainterPath pp;
835 pp.addRoundedRect(0, 0, w, h, rad, rad);
836 avatar_icon = QPixmap(w, h);
837 avatar_icon.fill(QColor(0,0,0,0));
838 QPainter mp(&avatar_icon);
839 mp.setBackgroundMode(Qt::TransparentMode);
840 mp.setRenderHints(QPainter::Antialiasing, true);
841 mp.fillPath(pp, QBrush(av));
842 }
843 else {
844 avatar_icon = av.scaled(avSize, avSize, Qt::KeepAspectRatio, Qt::SmoothTransformation);
845 }
846 }
847 else {
848 avatar_icon = QPixmap();
849 }
850 }
851
852 return avatar_icon;
853 }
854
855
onVcardTaskFinsihed()856 void AvatarFactory::onVcardTaskFinsihed()
857 {
858 auto task = dynamic_cast<JT_VCard*>(sender());
859 QString hash = task->property("hash").toString();
860 if (task->success() && !task->vcard().isNull()) {
861 QByteArray ba = task->vcard().photo();
862 if (!ba.isNull()) {
863 QString fullJid = task->jid().full();
864 if (AvatarCache::instance()->setIcon(AvatarCache::VCardType, fullJid, ba, hash) == AvatarCache::UserUpdateRequired) {
865 emit avatarChanged(task->jid());
866 }
867 }
868 }
869 }
870
vcardUpdated(const Jid & j,bool isMuc)871 void AvatarFactory::vcardUpdated(const Jid &j, bool isMuc)
872 {
873 QByteArray ba;
874 QString fullJid;
875 XMPP::VCard vcard;
876 if (isMuc) {
877 vcard = VCardFactory::instance()->mucVcard(j);
878 fullJid = j.full();
879 } else {
880 vcard = VCardFactory::instance()->vcard(j);
881 fullJid = j.bare();
882 }
883 if (!vcard) {
884 return; // wtf??
885 }
886 ba = vcard.photo();
887 if (!ba.isEmpty()) {
888 if (AvatarCache::instance()->setIcon(AvatarCache::VCardType, fullJid, ba) == AvatarCache::UserUpdateRequired) {
889 emit avatarChanged(j);
890 }
891 }
892 }
893
itemPublished(const Jid & jid,const QString & n,const PubSubItem & item)894 void AvatarFactory::itemPublished(const Jid& jid, const QString& n, const PubSubItem& item)
895 {
896 AvatarCache *cache = AvatarCache::instance();
897 QString jidFull = jid.full();
898 AvatarCache::OpResult result = AvatarCache::Changed;
899
900 if (n == PEP_AVATAR_DATA_NS) {
901 if (item.payload().tagName() == PEP_AVATAR_DATA_TN) {
902 // try append user first. since data may be unexpected and we want to save some cpu cycles.
903 result = cache->appendUser(item.id(), AvatarCache::AvatarType,
904 jidFull);
905 if (result == AvatarCache::NoData) {
906 QByteArray ba = QByteArray::fromBase64(item.payload().text().toLatin1());
907 if (!ba.isEmpty()) {
908 result = cache->setIcon(AvatarCache::AvatarType, jidFull, ba, item.id());
909 }
910 }
911 }
912 else {
913 qWarning("avatars.cpp: Unexpected item payload");
914 }
915 }
916 else if (n == PEP_AVATAR_METADATA_NS) {
917 QString hash = item.id().toLower();
918
919 if (item.payload().tagName() == QLatin1String(PEP_AVATAR_METADATA_TN) && item.payload().firstChildElement().isNull()) {
920 // user wants to stop publishing avatar
921 // previously we used "stop" element. now specs are changed
922 result = AvatarCache::instance()->removeIcon(AvatarCache::AvatarType, jidFull);
923 } else {
924 result = cache->appendUser(item.id(), AvatarCache::AvatarType,
925 jidFull);
926 if (result == AvatarCache::NoData) {
927 pa_->pepManager()->get(jid, PEP_AVATAR_DATA_NS, hash);
928 return;
929 }
930 }
931 }
932
933 if (result == AvatarCache::UserUpdateRequired) {
934 emit avatarChanged(jid);
935 }
936 }
937
publish_success(const QString & n,const PubSubItem & item)938 void AvatarFactory::publish_success(const QString& n, const PubSubItem& item)
939 {
940 if (n == PEP_AVATAR_DATA_NS && item.id() == selfAvatarHash_) {
941 // Publish metadata
942 QDomDocument* doc = account()->client()->doc();
943 QImage avatar_image = QImage::fromData(selfAvatarData_);
944 QDomElement meta_el = doc->createElement(PEP_AVATAR_METADATA_TN);
945 meta_el.setAttribute("xmlns",PEP_AVATAR_METADATA_NS);
946 QDomElement info_el = doc->createElement("info");
947 info_el.setAttribute("id",selfAvatarHash_);
948 info_el.setAttribute("bytes",avatar_image.byteCount());
949 info_el.setAttribute("height",avatar_image.height());
950 info_el.setAttribute("width",avatar_image.width());
951 info_el.setAttribute("type",image2type(selfAvatarData_));
952 meta_el.appendChild(info_el);
953 account()->pepManager()->publish(PEP_AVATAR_METADATA_NS,PubSubItem(selfAvatarHash_,meta_el));
954 selfAvatarData_.clear(); // we don't need it anymore
955 }
956 }
957
958 //------------------------------------------------------------------------------
959
960 #include "avatars.moc"
961