diff options
author | sualko <klaus@jsxc.org> | 2021-12-29 17:48:52 +0300 |
---|---|---|
committer | sualko <klaus@jsxc.org> | 2021-12-29 17:50:05 +0300 |
commit | 26756e0b52ca33f7a55ab34caa1c704c791e3c2c (patch) | |
tree | 7813c965e54a3400406b63e755c7f25781284582 | |
parent | 5b39de7b423de2ecad1edf009f26b373894069ac (diff) |
fix: pep based avatars
close #1021
-rw-r--r-- | src/connection/services/PEP.ts | 8 | ||||
-rw-r--r-- | src/plugins/AvatarPEPPlugin.ts | 269 | ||||
-rw-r--r-- | src/util/ImageHelper.ts | 24 |
3 files changed, 147 insertions, 154 deletions
diff --git a/src/connection/services/PEP.ts b/src/connection/services/PEP.ts index 04473818..ca801bd5 100644 --- a/src/connection/services/PEP.ts +++ b/src/connection/services/PEP.ts @@ -54,7 +54,7 @@ export default class PEP extends AbstractService { return this.sendIQ(iqStanza); } - public retrieveItems(node: string, jid?: string) { + public retrieveItems(node: string, jid?: string, id?: string) { let iq = $iq({ to: jid, type: 'get', @@ -67,6 +67,12 @@ export default class PEP extends AbstractService { node, }); + if (id) { + iq.c('item', { + id, + }); + } + return this.sendIQ(iq); } } diff --git a/src/plugins/AvatarPEPPlugin.ts b/src/plugins/AvatarPEPPlugin.ts index acea9466..c96e88d4 100644 --- a/src/plugins/AvatarPEPPlugin.ts +++ b/src/plugins/AvatarPEPPlugin.ts @@ -8,15 +8,24 @@ import Hash from '@util/Hash'; import { IAvatar } from '@src/Avatar.interface'; import AvatarUI from '../ui/AvatarSet'; import FileHelper from '@util/FileHelper'; +import ImageHelper from '@util/ImageHelper'; const MIN_VERSION = '4.0.0'; const MAX_VERSION = '99.0.0'; const UANS_BASE = 'urn:xmpp:avatar'; -const UANS_METADATA = +UANS_BASE + ':metadata'; -const UANS_NOTIFY = UANS_METADATA + '+notify'; +const UANS_METADATA = UANS_BASE + ':metadata'; const UANS_DATA = UANS_BASE + ':data'; +type AvatarMetaData = { + bytes?: number; + id: string; + type?: string; + height?: number; + width?: number; + url?: string; +}; + export default class AvatarPEPPlugin extends AbstractPlugin { public static getId(): string { return 'pep-avatars'; @@ -44,113 +53,87 @@ export default class AvatarPEPPlugin extends AbstractPlugin { let connection = pluginAPI.getConnection(); - connection.registerHandler(this.onMessageAvatarUpdate, 'http://jabber.org/protocol/pubsub#event', 'message'); + connection.getPEPService().subscribe(UANS_BASE, this.onMessageAvatarUpdate); pluginAPI.addAvatarProcessor(this.avatarProcessor, 49); pluginAPI.addPublishAvatarProcessor(this.publishAvatarProcessor, 49); - pluginAPI.addFeature(UANS_NOTIFY); } public getStorage() { return this.pluginAPI.getStorage(); } - private publishAvatarProcessor = (avatar: IAvatar | null): Promise<[IAvatar]> => { - let connection = this.pluginAPI.getConnection(); + private publishAvatarProcessor = async (avatar: IAvatar | null): Promise<[IAvatar]> => { + let pepService = this.pluginAPI.getConnection().getPEPService(); - if (!avatar || avatar.getData() === undefined) { + if (!avatar || !avatar.getData()) { let item = $build('metadata', { xmlns: UANS_METADATA }); - return connection - .getPEPService() - .publish(UANS_METADATA, item.tree(), UANS_METADATA) - .then(function (result) { - if ($(result).attr('type') === 'result') { - return [undefined]; - } else { - return [avatar]; - } - }); - } else { - let data = $build('data', { xmlns: UANS_DATA }).t(avatar.getData()).tree(); - - return connection - .getPEPService() - .publish(UANS_DATA, data, UANS_DATA) - .then((result: any) => { - if ($(result).attr('type') === 'result') { - let metadata; - let hash = Hash.SHA1FromBase64(avatar.getData()); - let i = new Image(); - let iheight = 0; - let iwidth = 0; - let size = FileHelper.getFileSizeFromBase64(avatar.getData()); - - i.onload = function () { - iheight = i.height; - iwidth = i.width; - }; - i.src = 'data:' + avatar.getType() + ';base64,' + avatar.getData(); - metadata = $build('metadata', { xmlns: UANS_METADATA }) - .c('info', { bytes: size, id: hash, height: iheight, width: iwidth, type: avatar.getType() }) - .tree(); - - return connection - .getPEPService() - .publish(UANS_METADATA, metadata, UANS_METADATA) - .then(function (result) { - if ($(result).attr('type') === 'result') { - return [undefined]; - } else { - return [avatar]; - } - }); - } else { - return [avatar]; - } - }); + await pepService.publish(UANS_METADATA, item.tree(), UANS_METADATA); + + return [avatar]; } + + const avatarData = avatar.getData(); + const imageDataUrl = avatar.getType() !== 'image/png' ? await ImageHelper.convertToPNG(avatarData) : avatarData; + const imageData = imageDataUrl.replace(/^.+;base64,/, ''); + const imageId = Hash.SHA1FromBase64(imageDataUrl); + + let dataElement = $build('data', { xmlns: UANS_DATA }).t(imageData).tree(); + + await pepService.publish(UANS_DATA, dataElement, imageId); + + let imageSize = FileHelper.getFileSizeFromBase64(imageDataUrl); + + let metadataElement = $build('metadata', { + xmlns: UANS_METADATA, + }) + .c('info', { + bytes: imageSize, + id: imageId, + type: 'image/png', + }) + .tree(); + + await pepService.publish(UANS_METADATA, metadataElement, imageId); + + return [avatar]; }; private onMessageAvatarUpdate = stanza => { let from = new JID($(stanza).attr('from')); - let metadata = $(stanza).find('metadata[xmlns="urn:xmpp:avatar:metadata"]'); - let data = $(stanza).find('data[xmlns="urn:xmpp:avatar:data"]'); - - if (metadata.length > 0) { - let info = metadata.find('info'); - let contact = this.pluginAPI.getContact(from); - if (!contact) { - this.pluginAPI.Log.warn('No contact found for', from); - return true; - } + let metadata = $(stanza).find(`metadata[xmlns="${UANS_METADATA}"]`); - if (info.length > 0) { - let hash = $(info).attr('id'); - - let storedHash = this.getStorage().getItem(from.bare); - if (storedHash === undefined || hash !== storedHash) { - let avatarUI = AvatarUI.get(contact); - this.getStorage().setItem(contact.getJid().bare, hash); - avatarUI.reload(); - } - } else { - let avatarUI = AvatarUI.get(contact); - this.getStorage().setItem(contact.getJid().bare, ''); - avatarUI.reload(); - } - } else if (data.length > 0) { - let contact = this.pluginAPI.getContact(from); - if (!contact) { - this.pluginAPI.Log.warn('No contact found for', from); - return true; - } + if (metadata.length === 0) { + return true; + } + + const contact = this.pluginAPI.getContact(from); + + if (!contact) { + this.pluginAPI.Log.warn(`Ignore PEP avatar notification for ${from.full}, because we do not know him.`); + return true; + } + + const info = metadata.find('>info[type="image/png"]'); + const avatarUI = AvatarUI.get(contact); + const meta: AvatarMetaData = + info.length > 0 + ? { + id: info.attr('id'), + type: info.attr('type'), + bytes: info.attr('bytes') ? parseInt(info.attr('bytes'), 10) : undefined, + width: info.attr('width') ? parseInt(info.attr('width'), 10) : undefined, + height: info.attr('height') ? parseInt(info.attr('height'), 10) : undefined, + } + : { + id: '', + }; + const cachedMeta = this.getStorage().getItem<AvatarMetaData>(from.bare); + + if (!cachedMeta || meta.id !== cachedMeta.id) { + this.getStorage().setItem<AvatarMetaData>(contact.getJid().bare, meta); - let src = data.text().replace(/[\t\r\n\f]/gi, ''); - const b64str = src.replace(/^.+;base64,/, ''); - let hash = Hash.SHA1FromBase64(b64str); - let avatarUI = AvatarUI.get(contact); - this.getStorage().setItem(contact.getJid().bare, hash); avatarUI.reload(); } @@ -159,42 +142,21 @@ export default class AvatarPEPPlugin extends AbstractPlugin { private avatarProcessor = async (contact: IContact, avatar: IAvatar): Promise<[IContact, IAvatar]> => { let storage = this.getStorage(); - let hash = storage.getItem(contact.getJid().bare); + let meta = storage.getItem<AvatarMetaData>(contact.getJid().bare); - if (!hash && !avatar) { - try { - const avatarObject = await this.getAvatar(contact); - const data = avatarObject.src.replace(/^.+;base64,/, ''); - avatar = new Avatar(Hash.SHA1FromBase64(data), avatarObject.type, avatarObject.src); - - this.getStorage().setItem(contact.getJid().bare, avatar.getHash() || ''); - - let avatarUI = AvatarUI.get(contact); - avatarUI.reload(); - } catch (err) { - // we could not find any avatar - } - } - - if (!hash || avatar) { + if (!meta || !meta.id) { return [contact, avatar]; } try { - avatar = new Avatar(hash); + avatar = new Avatar(meta.id); } catch (err) { try { const avatarObject = await this.getAvatar(contact); - - if (avatarObject) { - avatar = new Avatar(hash, avatarObject.type, avatarObject.src); - return [contact, avatar]; - } else { - this.pluginAPI.Log.warn('No local cached avatar found'); - return [contact, avatar]; - } + avatar = new Avatar(meta.id, avatarObject.type, avatarObject.src); } catch (err) { - this.pluginAPI.Log.warn('Error during avatar retrieval', err); + this.pluginAPI.Log.warn('Error during pep avatar retrieval', err); + return [contact, avatar]; } } @@ -203,43 +165,44 @@ export default class AvatarPEPPlugin extends AbstractPlugin { }; private async getAvatar(contact: IContact): Promise<{ src: string; type: string }> { - let connection = this.pluginAPI.getConnection(); + const connection = this.pluginAPI.getConnection(); + const cachedMetaData = this.getStorage().getItem<AvatarMetaData>(contact.getJid().bare); - return connection + if (!cachedMetaData) { + throw new Error('No avatar meta data is cached'); + } + + if (!cachedMetaData.id) { + throw new Error('User has no avatar'); + } + + const stanza = await connection .getPEPService() - .retrieveItems(UANS_METADATA, contact.getJid().bare) - .then(meta => { - let metadata = $(meta).find('metadata[xmlns="urn:xmpp:avatar:metadata"]'); - - if (metadata.length > 0) { - let info = metadata.find('info'); - - if (info && info.length > 0) { - let hash = $(info).attr('id'); - if (hash && hash.length > 0) { - let typeval = $(info).attr('type'); - - let result = connection - .getPEPService() - .retrieveItems(UANS_DATA, contact.getJid().bare) - .then(data => { - if (data && $(data).text() && $(data).text().trim().length > 0) { - let src = $(data) - .text() - .replace(/[\t\r\n\f]/gi, ''); - const b64str = src.replace(/^.+;base64,/, ''); - this.getStorage().setItem(contact.getJid().bare, Hash.SHA1FromBase64(b64str)); - return { src: 'data:' + typeval + ';base64,' + src, type: typeval }; - } else { - throw new Error('No photo available'); - } - }); - return result; - } - } - } - - throw new Error('No photo available'); - }); + .retrieveItems(UANS_DATA, contact.getJid().bare, cachedMetaData.id); + const dataStanza = $(stanza).find(`data[xmlns="${UANS_DATA}"]`); + + if (dataStanza.length !== 1) { + throw new Error('Could not retrieve avatar pep item'); + } + + const data = dataStanza.text().replace(/[\t\r\n\f]/gim, ''); + + try { + window.atob(data); + } catch (_) { + throw new Error('Received invalid base64 encoded string'); + } + + const src = 'data:' + cachedMetaData.type + ';base64,' + data; + const imageId = Hash.SHA1FromBase64(src); + + if (cachedMetaData.id !== imageId) { + this.pluginAPI.Log.info(`Cached image id (${cachedMetaData.id}) is different to the retrieved image (${imageId}).`); + } + + return { + src, + type: cachedMetaData.type, + }; } } diff --git a/src/util/ImageHelper.ts b/src/util/ImageHelper.ts index 12249914..e428149c 100644 --- a/src/util/ImageHelper.ts +++ b/src/util/ImageHelper.ts @@ -42,4 +42,28 @@ export default class ImageHelper { img.src = data; }); } + + public static convertToPNG(data: string): Promise<string> { + const canvas = <HTMLCanvasElement>$('<canvas>').get(0); + const img = new Image(); + + return new Promise((resolve, reject) => { + img.onload = () => { + canvas.width = img.width; + canvas.height = img.height; + + const ctx = canvas.getContext('2d'); + + ctx.drawImage(img, 0, 0, img.width, img.width, 0, 0, img.width, img.width); + + resolve(canvas.toDataURL()); + }; + + img.onerror = () => { + reject(new Error('Could not load image')); + }; + + img.src = data; + }); + } } |