From 9502fa3f5a35254f24ecb419489f7976b2abe270 Mon Sep 17 00:00:00 2001 From: the-djmaze <> Date: Wed, 30 Nov 2022 17:03:07 +0100 Subject: [PATCH] Avatars plugin added other identicon types (needs config settings) --- plugins/avatars/avatars.js | 28 ++++++++++++--- plugins/avatars/identicon.js | 69 ++++++++++++++++++++++++++++++++++++ plugins/avatars/index.php | 4 ++- plugins/avatars/jdenticon.js | 19 +--------- 4 files changed, 97 insertions(+), 23 deletions(-) create mode 100644 plugins/avatars/identicon.js diff --git a/plugins/avatars/avatars.js b/plugins/avatars/avatars.js index a6a0c85c8..ceb7daac6 100644 --- a/plugins/avatars/avatars.js +++ b/plugins/avatars/avatars.js @@ -11,6 +11,21 @@ return `${bimi}/${from.email.toLowerCase()}`; }, getAvatar = msg => ncAvatars.get(msg.from[0].email.toLowerCase()) || avatars.get(getAvatarUid(msg)), + hash = async txt => { + if (/^[0-9a-f]{15,}$/i.test(txt)) { + return txt; + } + const hashArray = Array.from(new Uint8Array( +// await crypto.subtle.digest('SHA-256', (new TextEncoder()).encode(txt.toLowerCase())) + await crypto.subtle.digest('SHA-1', (new TextEncoder()).encode(txt.toLowerCase())) + )); + return hashArray.map(b => b.toString(16).padStart(2, '0')).join(''); // convert bytes to hex string + }, + fromChars = from => + ((from.name || from.email).split(/[^\p{L}]+/gu) || []) + .reduce((a, s) => a + (s[0] || ''), '') + .slice(0,2) + .toUpperCase(), runQueue = (() => { let item = queue.shift(); while (item) { @@ -27,7 +42,8 @@ item[1](url); } else if (window.identiconSvg) { // rl.pluginSettingsGet('avatars', 'identicon'); - window.identiconSvg(from.email).then(url => { + hash(from.email).then(hash => { + let url = window.identiconSvg(hash, fromChars(from)); avatars.set(getAvatarUid(item[0]), url); item[1](url); }); @@ -81,7 +97,8 @@ for (let i = 0; i < responseList.length; ++i) { const item = responseList.item(i); if (1 == getElementValue(item, nsNC, 'has-photo')) { - [...getElementValue(item, nsCard, 'address-data').matchAll(/EMAIL.*?:([^@\r\n]+@[^@\r\n]+)/g)].forEach(match => { + [...getElementValue(item, nsCard, 'address-data').matchAll(/EMAIL.*?:([^@\r\n]+@[^@\r\n]+)/g)] + .forEach(match => { ncAvatars.set( match[1].toLowerCase(), getElementValue(item, nsDAV, 'href') + '?photo' @@ -102,10 +119,13 @@ if (url) { fn(url); } else if (msg.avatar) { - let bimi = 'pass' == msg.from[0].dkimStatus ? 1 : 0; + let from = msg.from[0], + bimi = 'pass' == from.dkimStatus ? 1 : 0; if (window.identiconSvg) { // rl.pluginSettingsGet('avatars', 'identicon'); - element.onerror = () => window.identiconSvg(msg.from[0].email).then(fn); + element.onerror = () => hash(from.email).then(hash => + fn(window.identiconSvg(hash, fromChars(from))) + ); } fn(`?Avatar/${bimi}/${msg.avatar}`); } else { diff --git a/plugins/avatars/identicon.js b/plugins/avatars/identicon.js new file mode 100644 index 000000000..8a124d7ff --- /dev/null +++ b/plugins/avatars/identicon.js @@ -0,0 +1,69 @@ + +(()=>{ + +const size = 50, + margin = 0.08; + +window.identiconSvg = (hash, txt) => { + // color defaults to last 7 chars as hue at 70% saturation, 50% brightness + // hsl2rgb adapted from: https://gist.github.com/aemkei/1325937 + let h = (parseInt(hash.substr(-7), 16) / 0xfffffff) * 6, s = 0.7, l = 0.5, + v = [ + l += s *= l < .5 ? l : 1 - l, + l - h % 1 * s * 2, + l -= s *= 2, + l, + l + h % 1 * s, + l + s + ]; + + if (txt) { + const color = 'rgb(' + [ + v[ ~~h % 6 ] * 255 / 2, // red + v[ (h | 16) % 6 ] * 255 / 2, // green + v[ (h | 8) % 6 ] * 255 / 2 // blue + ].map(Math.round).join(',') + ')'; + txt = ` + + ${txt} + `; + } else { + const color = 'rgb(' + [ + v[ ~~h % 6 ] * 255, // red + v[ (h | 16) % 6 ] * 255, // green + v[ (h | 8) % 6 ] * 255 // blue + ].map(Math.round).join(',') + ')', + cell = Math.floor((size - ((Math.floor(size * margin)) * 2)) / 5), + imargin = Math.floor((size - cell * 5) / 2), + rectangles = [], + add = (x, y) => rectangles.push(""); + + // the first 15 characters of the hash control the pixels (even/odd) + // they are drawn down the middle first, then mirrored outwards + for (let i = 0; i < 15; ++i) { + if (!(parseInt(hash.charAt(i), 16) % 2)) { + if (i < 5) { + add(2, i); + } else if (i < 10) { + add(1, (i - 5)); + add(3, (i - 5)); + } else if (i < 15) { + add(0, (i - 10)); + add(4, (i - 10)); + } + } + } + txt = "" + + rectangles.join('') + + ""; + } + return 'data:image/svg+xml;base64,' + btoa( + "" + txt + "" + ); +}; + +})(); diff --git a/plugins/avatars/index.php b/plugins/avatars/index.php index 4754a541e..ec8f0376b 100644 --- a/plugins/avatars/index.php +++ b/plugins/avatars/index.php @@ -19,7 +19,9 @@ class AvatarsPlugin extends \RainLoop\Plugins\AbstractPlugin $this->addJs('avatars.js'); $this->addJsonHook('Avatar', 'DoAvatar'); $this->addPartHook('Avatar', 'ServiceAvatar'); - $this->Config()->Get('plugin', 'identicon', false) && $this->addJs('jdenticon.js'); +// $this->Config()->Get('plugin', 'identicon', false) && $this->addJs('jdenticon.js'); + // GitHub-style + $this->Config()->Get('plugin', 'identicon', false) && $this->addJs('identicon.js'); // https://github.com/the-djmaze/snappymail/issues/714 $this->Config()->Get('plugin', 'delay', true) || $this->addHook('filter.json-response', 'FilterJsonResponse'); } diff --git a/plugins/avatars/jdenticon.js b/plugins/avatars/jdenticon.js index 35847e938..efaf9f69d 100644 --- a/plugins/avatars/jdenticon.js +++ b/plugins/avatars/jdenticon.js @@ -386,21 +386,6 @@ function outerShape(index, g, cell) { ); } -/** - * Computes a SHA1 hash for any value and returns it as a hexadecimal string. - * - * This function is optimized for minimal code size and rather short messages. - * - * @param {string} message - */ -async function computeHash(message) { - const hashArray = Array.from(new Uint8Array( -// await crypto.subtle.digest('SHA-256', (new TextEncoder()).encode(message)) - await crypto.subtle.digest('SHA-1', (new TextEncoder()).encode(message)) - )); - return hashArray.map(b => b.toString(16).padStart(2, '0')).join(''); // convert bytes to hex string -} - /** * Prepares a measure to be used as a measure in an SVG path, by * rounding the measure to a single decimal. This reduces the file @@ -597,11 +582,9 @@ class SvgWriter * @param {*} hashOrValue - A hexadecimal hash string or any value that will be hashed by Jdenticon. * @returns {string} SVG string */ -window.identiconSvg = async (hashOrValue) => { +window.identiconSvg = hash => { const writer = new SvgWriter(50), renderer = new SvgRenderer(writer), - hash = (/^[0-9a-f]{11,}$/i.test(hashOrValue) && hashOrValue) - || await computeHash(hashOrValue == null ? "" : "" + hashOrValue), config = { p/*colorSaturation*/: 0.5, H/*grayscaleSaturation*/: 0,