# HG changeset patch # User Emmanuel Gil Peyrot # Date 1545424457 -3600 # Node ID 2a8d4e8600d030c15f1c5089f91c1b29bbdd17fe Initial commit. diff --git a/avatar.js b/avatar.js new file mode 100644 --- /dev/null +++ b/avatar.js @@ -0,0 +1,184 @@ +'use strict'; + +function initAvatar(connection) { + const DEFAULT_AVATAR = 'data:image/svg+xml,?'; + + const avatar_data = {}; + const avatar_img = document.getElementById('avatar'); + const avatar_size = document.getElementById('avatar-size'); + const avatar_file = document.getElementById('avatar-file'); + const avatar_upload = document.getElementById('avatar-upload'); + const avatar_change = document.getElementById('avatar-change'); + + avatar_img.src = DEFAULT_AVATAR; + const iq = $iq({type: 'get'}) + .c('pubsub', {xmlns: 'http://jabber.org/protocol/pubsub'}) + .c('items', {node: 'urn:xmpp:avatar:metadata'}); + connection.sendIQ(iq, onAvatarMetadata, onAvatarRetrievalError.bind(null, 'PubSub metadata query failed.')); + + function onAvatarMetadata(result_iq) + { + const item = parseXPath(result_iq, './pubsub:pubsub/pubsub:items/pubsub:item'); + if (item == null) + return onAvatarRetrievalError('no item found.'); + const id = item.getAttributeNS(null, 'id'); + const info = parseXPath(item, './avatar_metadata:metadata/avatar_metadata:info'); + if (info == null) + return onAvatarRetrievalError('no info found, your avatar metadata node is broken.'); + if (id != info.getAttributeNS(null, 'id')) + return onAvatarRetrievalError('invalid id in metadata.'); + + const parsed_info = { + id: id, + type: info.getAttributeNS(null, 'type'), + bytes: info.getAttributeNS(null, 'bytes'), + width: info.getAttributeNS(null, 'width'), + height: info.getAttributeNS(null, 'height'), + }; + const iq = $iq({type: 'get'}) + .c('pubsub', {xmlns: 'http://jabber.org/protocol/pubsub'}) + .c('items', {node: 'urn:xmpp:avatar:data'}) + .c('item', {id: id}); + connection.sendIQ(iq, onAvatarData.bind(null, parsed_info), onAvatarRetrievalError.bind(null, 'PubSub data query failed.')); + } + + function onAvatarData(info, result_iq) + { + const item = parseXPath(result_iq, './pubsub:pubsub/pubsub:items/pubsub:item'); + if (item == null) + return onAvatarRetrievalError('no item found.'); + if (info.id != item.getAttributeNS(null, 'id')) + return onAvatarRetrievalError('invalid id in data.'); + + const data = parseXPath(item, './avatar_data:data').textContent; + const url = 'data:' + info.type + ';base64,' + data; + // TODO: validate the bytes too. + /* + // TODO: figure out why this didn’t work. + avatar_img.onload = function (evt) { + const img = evt.target; + if (img.naturalWidth != info.width || img.naturalHeight != info.height) + return onAvatarRetrievalError('invalid width or height in image data.'); + avatar_img.onload = null; + }; + */ + avatar_img.src = url; + } + + function onAvatarRetrievalError(string) + { + console.log('Failed to retrieve avatar, an empty one is displayed instead: ' + string); + avatar_img.src = DEFAULT_AVATAR; + } + + avatar_upload.addEventListener('click', function (evt) { + avatar_file.click(); + }); + + avatar_change.addEventListener('click', function (evt) { + const metadata_iq = $iq({type: 'set'}) + .c('pubsub', {xmlns: 'http://jabber.org/protocol/pubsub'}) + .c('publish', {node: 'urn:xmpp:avatar:metadata'}) + .c('item', {id: avatar_data.id}) + .c('metadata', {xmlns: 'urn:xmpp:avatar:metadata'}) + .c('info', { + id: avatar_data.id, + type: avatar_data.type, + bytes: avatar_data.bytes, + width: avatar_img.naturalWidth, + height: avatar_img.naturalHeight, + }); + connection.sendIQ(metadata_iq, onAvatarMetadataUpload, onAvatarUploadError); + const data_iq = $iq({type: 'set'}) + .c('pubsub', {xmlns: 'http://jabber.org/protocol/pubsub'}) + .c('publish', {node: 'urn:xmpp:avatar:data'}) + .c('item', {id: avatar_data.id}) + .c('data', {xmlns: 'urn:xmpp:avatar:data'}) + .t(avatar_data.data); + connection.sendIQ(data_iq, onAvatarDataUpload, onAvatarUploadError); + }); + + function onAvatarMetadataUpload(iq) + { + console.log("onAvatarMetadataUpload", iq); + } + + function onAvatarDataUpload(iq) + { + console.log('Avatar successfully uploaded!', iq); + avatar_change.disabled = true; + avatar_size.innerHTML = ''; + } + + function onAvatarUploadError(iq) + { + console.log("onAvatarUploadError", iq); + } + + avatar_file.addEventListener('change', function (evt) { + const file = evt.target.files[0]; + avatar_data.type = file.type; + avatar_data.bytes = file.size; + + // Set the preview. + avatar_img.src = URL.createObjectURL(file); + const [size, unit] = friendlyDataSize(file.size); + avatar_size.innerHTML = Math.round(size) + ' ' + unit; + + // Obtain the base64 version of this file. + const base64_reader = new FileReader(); + base64_reader.onload = function (evt) { + const data = evt.target.result; + avatar_data.data = data.substr(data.indexOf(',') + 1); + } + base64_reader.readAsDataURL(file); + + // Compute the sha1 of this file. + const sha1_reader = new FileReader(); + sha1_reader.onload = async function (evt) { + const data = evt.target.result; + const digest = await window.crypto.subtle.digest('SHA-1', data); + const sha1 = (Array + .from(new Uint8Array(digest)) + .map(b => b.toString(16).padStart(2, "0")) + .join("")); + avatar_data.id = sha1; + avatar_change.disabled = false; + } + sha1_reader.readAsArrayBuffer(file); + }); + + function nsResolver(prefix) { + return { + pubsub: 'http://jabber.org/protocol/pubsub', + avatar_metadata: 'urn:xmpp:avatar:metadata', + avatar_data: 'urn:xmpp:avatar:data', + }[prefix] || null; + } + + function parseXPath(elem, xpath) + { + return elem.getRootNode().evaluate(xpath, elem, nsResolver, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue; + } + + function friendlyDataSize(bytes) { + let unit = 'B' + if (bytes >= 1024) { + bytes /= 1024; + unit = 'KiB'; + } + if (bytes >= 1024) { + bytes /= 1024; + unit = 'MiB'; + } + if (bytes >= 1024) { + bytes /= 1024; + unit = 'GiB'; + } + if (bytes >= 1024) { + bytes /= 1024; + unit = 'TiB'; + } + return [bytes, unit]; + } +} diff --git a/client.js b/client.js new file mode 100644 --- /dev/null +++ b/client.js @@ -0,0 +1,70 @@ +'use strict'; + +const BOSH_SERVICE = 'https://bosh.linkmauve.fr/'; + +function rawInput(data) +{ + console.log('RECV', data); +} + +function rawOutput(data) +{ + console.log('SENT', data); +} + +document.addEventListener('DOMContentLoaded', function () { + const connection = new Strophe.Connection(BOSH_SERVICE); + connection.rawInput = rawInput; + connection.rawOutput = rawOutput; + + const jid_element = document.getElementById('jid'); + const pass_element = document.getElementById('pass'); + const connect_button = document.getElementById('connect'); + + const connected_div = document.getElementById('connected'); + + const avatar_img = document.getElementById('avatar'); + + connect_button.addEventListener('click', function (evt) { + if (connect_button.value == 'connect') { + connection.connect(jid_element.value, + pass_element.value, + onConnect); + } else { + connection.disconnect(); + } + evt.preventDefault(); + }); + + function onConnect(status) + { + if (status == Strophe.Status.CONNECTING) { + console.log('Strophe is connecting.'); + connect_button.value = 'disconnect'; + jid_element.disabled = true; + pass_element.disabled = true; + } else if (status == Strophe.Status.CONNFAIL) { + console.log('Strophe failed to connect.'); + connect_button.value = 'connect'; + jid_element.disabled = false; + pass_element.disabled = false; + } else if (status == Strophe.Status.DISCONNECTING) { + console.log('Strophe is disconnecting.'); + } else if (status == Strophe.Status.DISCONNECTED) { + console.log('Strophe is disconnected.'); + connect_button.value = 'connect'; + jid_element.disabled = false; + pass_element.disabled = false; + } else if (status == Strophe.Status.CONNECTED) { + console.log('Strophe is connected.'); + onConnected(); + } + } + + function onConnected() + { + connected_div.hidden = false; + initNickname(connection); + initAvatar(connection); + } +}); diff --git a/index.xhtml b/index.xhtml new file mode 100644 --- /dev/null +++ b/index.xhtml @@ -0,0 +1,98 @@ + + + + + + Prosody IM account configuration + + + + + + + + + + +
+ +
+
+
+ +
+ + + +
+ + + +