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