identifi-lib
Version:
Basic tools for reading and writing Identifi messages and identities.
299 lines (269 loc) • 11.7 kB
JavaScript
import Identicon from 'identicon.js';
import Attribute from './attribute';
import util from './util';
/**
* An Identifi identity profile. Usually you don't create them yourself, but get them
* from Index methods such as search().
*/
class Identity {
/**
* @param {Object} gun node where the Identity data lives
*/
constructor(gun: Object, linkTo) {
this.gun = gun;
this.linkTo = linkTo;
}
static async create(gun, data) {
if (!data.linkTo && !data.attrs) {
throw new Error(`You must specify either data.linkTo or data.attrs`);
}
if (data.linkTo && !data.attrs) {
const linkTo = new Attribute(data.linkTo);
data.attrs = {};
if (!data.attrs.hasOwnProperty(linkTo.uri())) {
data.attrs[linkTo.uri()] = linkTo;
}
} else {
data.linkTo = Identity.getLinkTo(data.attrs);
}
await gun.put(data);
return new Identity(gun, data.linkTo);
}
static getLinkTo(attrs) {
const mva = Identity.getMostVerifiedAttributes(attrs);
const keys = Object.keys(mva);
let linkTo;
for (let i = 0;i < keys.length;i ++) {
if (keys[i] === `keyID`) {
linkTo = mva[keys[i]].attribute;
break;
} else if (Attribute.isUniqueType(keys[i])) {
linkTo = mva[keys[i]].attribute;
}
}
return linkTo;
}
static getMostVerifiedAttributes(attrs) {
const mostVerifiedAttributes = {};
Object.keys(attrs).forEach(k => {
const a = attrs[k];
const keyExists = Object.keys(mostVerifiedAttributes).indexOf(a.type) > - 1;
a.conf = isNaN(a.conf) ? 1 : a.conf;
a.ref = isNaN(a.ref) ? 0 : a.ref;
if (a.conf * 2 > a.ref * 3 && (!keyExists || a.conf - a.ref > mostVerifiedAttributes[a.type].verificationScore)) {
mostVerifiedAttributes[a.type] = {
attribute: a,
verificationScore: a.conf - a.ref
};
if (a.verified) {
mostVerifiedAttributes[a.type].verified = true;
}
}
});
return mostVerifiedAttributes;
}
/**
* @param {string} attribute attribute type
* @returns {string} most verified value of the param type
*/
async verified(attribute: String) {
const attrs = await this.gun.get(`attrs`).then();
const mva = Identity.getMostVerifiedAttributes(attrs);
return mva.hasOwnProperty(attribute) ? mva[attribute].attribute.value : undefined;
}
/**
* @param {Object} ipfs (optional) an IPFS instance that is used to fetch images
* @returns {HTMLElement} profile card html element describing the identity
*/
profileCard(ipfs: Object) {
const card = document.createElement(`div`);
card.className = `identifi-card`;
const identicon = this.identicon(60, null, null, ipfs);
identicon.style.order = 1;
identicon.style.flexShrink = 0;
identicon.style.marginRight = `15px`;
const details = document.createElement(`div`);
details.style.padding = `5px`;
details.style.order = 2;
details.style.flexGrow = 1;
const linkEl = document.createElement(`span`);
const links = document.createElement(`small`);
card.appendChild(identicon);
card.appendChild(details);
details.appendChild(linkEl);
details.appendChild(links);
this.gun.on(async data => {
if (!data) {
return;
}
const attrs = await new Promise(resolve => { this.gun.get(`attrs`).load(r => resolve(r)); });
const linkTo = await this.gun.get(`linkTo`).then();
const link = `https://identi.fi/#/identities/${linkTo.type}/${linkTo.value}`;
const mva = Identity.getMostVerifiedAttributes(attrs);
linkEl.innerHTML = `<a href="${link}">${(mva.type && mva.type.attribute.value) || (mva.nickname && mva.nickname.attribute.value) || `${linkTo.type}:${linkTo.value}`}</a><br>`;
linkEl.innerHTML += `<small>Received: <span class="identifi-pos">+${data.receivedPositive || 0}</span> / <span class="identifi-neg">-${data.receivedNegative || 0}</span></small><br>`;
links.innerHTML = ``;
Object.keys(attrs).forEach(k => {
const a = attrs[k];
if (a.link) {
links.innerHTML += `${a.type}: <a href="${a.link}">${a.value}</a> `;
}
});
});
/*
const template = ```
<tr ng-repeat="result in ids.list" id="result{$index}" ng-hide="!result.linkTo" ui-sref="identities.show({ type: result.linkTo.type, value: result.linkTo.value })" class="search-result-row" ng-class="{active: result.active}">
<td class="gravatar-col"><identicon id="result" border="3" width="46" positive-score="result.pos" negative-score="result.neg"></identicon></td>
<td>
<span ng-if="result.distance == 0" class="label label-default pull-right">viewpoint</span>
<span ng-if="result.distance > 0" ng-bind="result.distance | ordinal" class="label label-default pull-right"></span>
<a ng-bind-html="result.name|highlight:query.term" ui-sref="identities.show({ type: result.linkTo.type, value: result.linkTo.value })"></a>
<small ng-if="!result.name" class="list-group-item-text">
<span ng-bind-html="result[0][0]|highlight:query.term"></span>
</small><br>
<small>
<span ng-if="result.nickname && result.name != result.nickname" ng-bind-html="result.nickname|highlight:query.term" class="mar-right10"></span>
<span ng-if="result.email" class="mar-right10">
<span class="glyphicon glyphicon-envelope"></span> <span ng-bind-html="result.email|highlight:query.term"></span>
</span>
<span ng-if="result.facebook" class="mar-right10">
<span class="fa fa-facebook"></span> <span ng-bind-html="result.facebook|highlight:query.term"></span>
</span>
<span ng-if="result.twitter" class="mar-right10">
<span class="fa fa-twitter"></span> <span ng-bind-html="result.twitter|highlight:query.term"></span>
</span>
<span ng-if="result.googlePlus" class="mar-right10">
<span class="fa fa-google-plus"></span> <span ng-bind-html="result.googlePlus|highlight:query.term"></span>
</span>
<span ng-if="result.bitcoin" class="mar-right10">
<span class="fa fa-bitcoin"></span> <span ng-bind-html="result.bitcoin|highlight:query.term"></span>
</span>
</small>
</td>
</tr>
```;*/
return card;
}
/**
* Appends a search widget to the given HTMLElement
* @param {HTMLElement} parentElement element where the search widget is added and event listener attached
* @param {Index} index index root to use for search
*/
static appendSearchWidget(parentElement, index) {
const form = document.createElement(`form`);
const input = document.createElement(`input`);
input.type = `text`;
input.placeholder = `Search`;
input.id = `identifiSearchInput`;
form.innerHTML += `<div id="identifiSearchResults"></div>`;
const searchResults = document.createElement(`div`);
parentElement.appendChild(form);
form.appendChild(input);
form.appendChild(searchResults);
input.addEventListener(`keyup`, async () => {
const r = await index.search(input.value);
searchResults.innerHTML = ``;
r.sort((a, b) => {return a.trustDistance - b.trustDistance;});
r.forEach(i => {
searchResults.appendChild(i.profileCard());
});
});
}
static _ordinal(n) {
const s = [`th`, `st`, `nd`, `rd`];
const v = n % 100;
return n + (s[(v - 20) % 10] || s[v] || s[0]);
}
/**
* @param {number} width of the identicon
* @param {number} border identicon border (aura) width
* @param {boolean} showDistance whether to show web of trust distance ordinal
* @param {Object} ipfs (optional) an IPFS instance that is used to fetch images
* @returns {HTMLElement} identicon element that can be appended to DOM
*/
identicon(width, border = 4, showDistance = true, ipfs) {
util.injectCss(); // some other way that is not called on each identicon generation?
const identicon = document.createElement(`div`);
identicon.className = `identifi-identicon`;
identicon.style.width = `${width}px`;
identicon.style.height = `${width}px`;
const pie = document.createElement(`div`);
pie.className = `identifi-pie`;
pie.style.width = `${width}px`;
const img = document.createElement(`img`);
img.alt = ``;
img.width = width;
img.height = width;
img.style.borderWidth = `${border}px`;
let distance;
if (showDistance) {
distance = document.createElement(`span`);
distance.className = `identifi-distance`;
distance.style.fontSize = width > 50 ? `${width / 4}px` : `10px`;
identicon.appendChild(distance);
}
identicon.appendChild(pie);
identicon.appendChild(img);
function setPie(data) {
if (!data) {
return;
}
// Define colors etc
let bgColor = `rgba(0,0,0,0.2)`;
let bgImage = `none`;
let transform = ``;
let boxShadow = `0px 0px 0px 0px #82FF84`;
if (data.receivedPositive > data.receivedNegative * 20) {
boxShadow = `0px 0px ${border * data.receivedPositive / 50}px 0px #82FF84`;
} else if (data.receivedPositive < data.receivedNegative * 3) {
boxShadow = `0px 0px ${border * data.receivedNegative / 10}px 0px #BF0400`;
}
if (data.receivedPositive + data.receivedNegative > 0) {
if (data.receivedPositive > data.receivedNegative) {
transform = `rotate(${((- data.receivedPositive / (data.receivedPositive + data.receivedNegative) * 360 - 180) / 2)}deg)`;
bgColor = `#A94442`;
bgImage = `linear-gradient(${data.receivedPositive / (data.receivedPositive + data.receivedNegative) * 360}deg, transparent 50%, #3C763D 50%), linear-gradient(0deg, #3C763D 50%, transparent 50%)`;
} else {
transform = `rotate(${((- data.receivedNegative / (data.receivedPositive + data.receivedNegative) * 360 - 180) / 2) + 180}deg)`;
bgColor = `#3C763D`;
bgImage = `linear-gradient(${data.receivedNegative / (data.receivedPositive + data.receivedNegative) * 360}deg, transparent 50%, #A94442 50%), linear-gradient(0deg, #A94442 50%, transparent 50%)`;
}
}
pie.style.backgroundColor = bgColor;
pie.style.backgroundImage = bgImage;
pie.style.boxShadow = boxShadow;
pie.style.transform = transform;
pie.style.opacity = (data.receivedPositive + data.receivedNegative) / 10 * 0.5 + 0.35;
if (showDistance) {
distance.textContent = typeof data.trustDistance === `number` ? Identity._ordinal(data.trustDistance) : `✕`;
}
}
function setIdenticonImg(data) {
const hash = util.getHash(`${encodeURIComponent(data.type)}:${encodeURIComponent(data.value)}`, `hex`);
const identiconImg = new Identicon(hash, {width, format: `svg`});
img.src = img.src || `data:image/svg+xml;base64,${identiconImg.toString()}`;
}
if (this.linkTo) {
setIdenticonImg(this.linkTo);
} else {
this.gun.get(`linkTo`).on(setIdenticonImg);
}
this.gun.on(setPie);
if (ipfs) {
this.gun.get(`attrs`).open(attrs => {
const mva = Identity.getMostVerifiedAttributes(attrs);
if (mva.profilePhoto) {
const go = () => {
ipfs.cat(mva.profilePhoto.attribute.value).then(file => {
const f = ipfs.types.Buffer.from(file).toString(`base64`);
img.src = `data:image;base64,${f}`;
});
};
ipfs.isOnline() ? go() : ipfs.on(`ready`, go);
}
});
}
return identicon;
}
}
export default Identity;