Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/jsxc/jsxc.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorsualko <klaus@jsxc.org>2021-04-01 18:21:14 +0300
committersualko <klaus@jsxc.org>2021-12-29 17:49:50 +0300
commitbb0e27b8892c5c1da003a92545bdb5e106944b3f (patch)
tree03b04c6bb5a230b40985fe5b83a832375ec417ab
parent922ef54b738f3b9da4dc1721cf29960b52c57e4f (diff)
feat: add user avatar (XEP-0084)
-rw-r--r--src/bootstrap/plugins.ts2
-rw-r--r--src/plugins/AvatarPEPPlugin.ts245
-rw-r--r--src/ui/AvatarSet.ts1
-rw-r--r--src/ui/dialogs/avatarupload.ts2
-rw-r--r--src/util/FileHelper.ts9
-rw-r--r--src/util/Hash.ts14
-rw-r--r--src/util/Utils.ts11
7 files changed, 270 insertions, 14 deletions
diff --git a/src/bootstrap/plugins.ts b/src/bootstrap/plugins.ts
index dcb8fb4a..933ef013 100644
--- a/src/bootstrap/plugins.ts
+++ b/src/bootstrap/plugins.ts
@@ -17,6 +17,7 @@ import CommandPlugin from '@src/plugins/CommandPlugin';
import VersionPlugin from '@src/plugins/VersionPlugin';
import TimePlugin from '../plugins/TimePlugin';
import JingleMessageInitiationPlugin from '../plugins/JingleMessageInitiationPlugin';
+import AvatarPEPPlugin from '../plugins/AvatarPEPPlugin';
Client.addPlugin(OTRPlugin);
Client.addPlugin(OMEMOPlugin);
@@ -36,3 +37,4 @@ Client.addPlugin(CommandPlugin);
Client.addPlugin(VersionPlugin);
Client.addPlugin(TimePlugin);
Client.addPlugin(JingleMessageInitiationPlugin);
+Client.addPlugin(AvatarPEPPlugin);
diff --git a/src/plugins/AvatarPEPPlugin.ts b/src/plugins/AvatarPEPPlugin.ts
new file mode 100644
index 00000000..acea9466
--- /dev/null
+++ b/src/plugins/AvatarPEPPlugin.ts
@@ -0,0 +1,245 @@
+import { AbstractPlugin, IMetaData } from '../plugin/AbstractPlugin';
+import PluginAPI from '../plugin/PluginAPI';
+import { IContact } from '../Contact.interface';
+import Avatar from '../Avatar';
+import JID from '../JID';
+import Translation from '@util/Translation';
+import Hash from '@util/Hash';
+import { IAvatar } from '@src/Avatar.interface';
+import AvatarUI from '../ui/AvatarSet';
+import FileHelper from '@util/FileHelper';
+
+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_DATA = UANS_BASE + ':data';
+
+export default class AvatarPEPPlugin extends AbstractPlugin {
+ public static getId(): string {
+ return 'pep-avatars';
+ }
+
+ public static getName(): string {
+ return 'PEP-based Avatars';
+ }
+
+ public static getMetaData(): IMetaData {
+ return {
+ description: Translation.t('setting-pep-avatar-enable'),
+ xeps: [
+ {
+ id: 'XEP-0084',
+ name: 'User Avatar',
+ version: '1.1.4',
+ },
+ ],
+ };
+ }
+
+ constructor(pluginAPI: PluginAPI) {
+ super(MIN_VERSION, MAX_VERSION, pluginAPI);
+
+ let connection = pluginAPI.getConnection();
+
+ connection.registerHandler(this.onMessageAvatarUpdate, 'http://jabber.org/protocol/pubsub#event', 'message');
+
+ 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();
+
+ if (!avatar || avatar.getData() === undefined) {
+ 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];
+ }
+ });
+ }
+ };
+
+ 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;
+ }
+
+ 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;
+ }
+
+ 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();
+ }
+
+ return true;
+ };
+
+ private avatarProcessor = async (contact: IContact, avatar: IAvatar): Promise<[IContact, IAvatar]> => {
+ let storage = this.getStorage();
+ let hash = storage.getItem(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) {
+ return [contact, avatar];
+ }
+
+ try {
+ avatar = new Avatar(hash);
+ } 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];
+ }
+ } catch (err) {
+ this.pluginAPI.Log.warn('Error during avatar retrieval', err);
+ return [contact, avatar];
+ }
+ }
+
+ return [contact, avatar];
+ };
+
+ private async getAvatar(contact: IContact): Promise<{ src: string; type: string }> {
+ let connection = this.pluginAPI.getConnection();
+
+ return 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');
+ });
+ }
+}
diff --git a/src/ui/AvatarSet.ts b/src/ui/AvatarSet.ts
index 761e7d7e..880045f6 100644
--- a/src/ui/AvatarSet.ts
+++ b/src/ui/AvatarSet.ts
@@ -14,7 +14,6 @@ export default class AvatarSet {
if (!avatar) {
avatar = AvatarSet.avatars[contact.getUid()] = new AvatarSet(contact);
}
-
return avatar;
}
diff --git a/src/ui/dialogs/avatarupload.ts b/src/ui/dialogs/avatarupload.ts
index 5117242e..6aa1cc10 100644
--- a/src/ui/dialogs/avatarupload.ts
+++ b/src/ui/dialogs/avatarupload.ts
@@ -43,7 +43,7 @@ export default function () {
$('.jsxc-avatarimage img').attr('src', thumb);
const mimetype = file.type;
- const data = thumb.replace(/^.+;base64,/, '');
+ const data = thumb.replace(/^.+;base64,/, '').replace(/[\t\r\n\f ]/gi, '');
const hash = Hash.SHA1FromBase64(data);
avatar = new Avatar(hash, mimetype, thumb);
diff --git a/src/util/FileHelper.ts b/src/util/FileHelper.ts
index d4743c88..b238e4aa 100644
--- a/src/util/FileHelper.ts
+++ b/src/util/FileHelper.ts
@@ -1,3 +1,5 @@
+import Utils from './Utils';
+
export default class FileHelper {
public static getDataURLFromFile(file: File): Promise<string> {
return new Promise((resolve, reject) => {
@@ -12,4 +14,11 @@ export default class FileHelper {
reader.readAsDataURL(file);
});
}
+
+ public static getFileSizeFromBase64(data: string): number {
+ let base64 = data.replace(/^.+;base64,/, '');
+ let buffer = Utils.base64ToArrayBuffer(base64);
+
+ return buffer.byteLength;
+ }
}
diff --git a/src/util/Hash.ts b/src/util/Hash.ts
index e77aee72..217f0044 100644
--- a/src/util/Hash.ts
+++ b/src/util/Hash.ts
@@ -1,4 +1,5 @@
import * as sha1 from 'js-sha1';
+import Utils from './Utils';
export default class Hash {
public static String(value: string) {
@@ -18,19 +19,8 @@ export default class Hash {
public static SHA1FromBase64(data: string): string {
let base64 = data.replace(/^.+;base64,/, '');
- let buffer = base64ToArrayBuffer(base64);
+ let buffer = Utils.base64ToArrayBuffer(base64);
return sha1(buffer);
}
}
-
-function base64ToArrayBuffer(base64String: string) {
- let binaryString = window.atob(base64String);
- let bytes = new Uint8Array(binaryString.length);
-
- for (let i = 0; i < binaryString.length; i++) {
- bytes[i] = binaryString.charCodeAt(i);
- }
-
- return bytes.buffer;
-}
diff --git a/src/util/Utils.ts b/src/util/Utils.ts
index f77510c9..d9e06448 100644
--- a/src/util/Utils.ts
+++ b/src/util/Utils.ts
@@ -51,4 +51,15 @@ export default class Utils {
public static prettifyHex(hex: string) {
return hex.replace(/(.{8})/g, '$1 ').replace(/ $/, '');
}
+
+ public static base64ToArrayBuffer(base64String: string) {
+ let binaryString = window.atob(base64String);
+ let bytes = new Uint8Array(binaryString.length);
+
+ for (let i = 0; i < binaryString.length; i++) {
+ bytes[i] = binaryString.charCodeAt(i);
+ }
+
+ return bytes.buffer;
+ }
}