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 = ``;
+ } 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(
+ ""
+ );
+};
+
+})();
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,