UNPKG

identifi-lib

Version:

Basic tools for reading and writing Identifi messages and identities.

851 lines (814 loc) 30.6 kB
import Message from './message'; import Key from './key'; import Identity from './identity'; import Attribute from './attribute'; import util from './util'; import Gun from 'gun'; // eslint-disable-line no-unused-vars import then from 'gun/lib/then'; // eslint-disable-line no-unused-vars import load from 'gun/lib/load'; // eslint-disable-line no-unused-vars const GUN_TIMEOUT = 100; // temp method for GUN search async function searchText(node, callback, query, limit, cursor, desc) { const seen = {}; node.map((value, key) => { const cursorCheck = !cursor || (desc && (key < cursor)) || (!desc && (key > cursor)); if (cursorCheck && key.indexOf(query) === 0) { if (typeof limit === `number` && Object.keys(seen).length >= limit) { return; } if (seen.hasOwnProperty(key)) { return; } if (value && Object.keys(value).length > 1) { seen[key] = true; callback({value, key}); } } }); } // TODO: flush onto IPFS /** * Identifi index root. Contains five indexes: identitiesBySearchKey, identitiesByTrustDistance, * messagesByHash, messagesByTimestamp, messagesByDistance. If you want messages saved to IPFS, pass * options.ipfs = instance. * * When you use someone else's index, initialise it using the Index constructor * @param {Object} gun gun node that contains an Identifi index (e.g. user.get('identifi')) * @param {Object} options see default options in example * @example * Default options: *{ * ipfs: undefined, * indexSync: { * importOnAdd: { * enabled: true, * maxMsgCount: 500, * maxMsgDistance: 2 * }, * subscribe: { * enabled: true, * maxMsgDistance: 1 * }, * query: { * enabled: true * }, * msgTypes: { * all: false, * rating: true, * verification: true, * unverification: true * } * } *} * @returns {Index} Identifi index object */ class Index { constructor(gun: Object, options) { this.gun = gun || new Gun(); this.options = Object.assign({ indexSync: { importOnAdd: { enabled: true, maxMsgCount: 100, maxMsgDistance: 2 }, subscribe: { enabled: true, maxMsgDistance: 1 }, query: { enabled: true }, msgTypes: { all: false, rating: true, verification: true, unverification: true } } }, options); if (options.viewpoint) { this.viewpoint = options.viewpoint; } else { this.gun.get(`viewpoint`).on((val, key, msg, eve) => { if (val) { this.viewpoint = new Attribute(val); eve.off(); } }); } if (this.options.indexSync.subscribe.enabled) { setTimeout(() => { this.gun.get(`trustedIndexes`).map().once((val, uri) => { if (val) { // TODO: only get new messages? this.gun.user(uri).get(`identifi`).get(`messagesByDistance`).map((val, key) => { const d = Number.parseInt(key.split(`:`)[0]); console.log(`got msg with d`, d, key); if (!isNaN(d) && d <= this.options.indexSync.subscribe.maxMsgDistance) { Message.fromSig(val).then(msg => { console.log(`adding msg ${msg.hash} from trusted index`); if (this.options.indexSync.msgTypes.all || this.options.indexSync.msgTypes.hasOwnProperty(msg.signedData.type)) { this.addMessage(msg, {checkIfExists: true}); } }); } }); } }); }, 5000); // TODO: this should be made to work without timeout } } /** * Use this to load an index that you can write to * @param {Object} gun gun instance where the index is stored (e.g. new Gun()) * @param {Object} keypair SEA keypair (can be generated with await identifiLib.Key.generate()) * @param {Object} options see default options in Index constructor's example * @returns {Promise} */ static async create(gun: Object, keypair, options = {}) { if (!keypair) { keypair = await Key.getDefault(); } const user = gun.user(); user.auth(keypair); options.viewpoint = new Attribute(`keyID`, Key.getId(keypair)); const i = new Index(user.get(`identifi`), options); i.gun.get(`viewpoint`).put(options.viewpoint); const uri = options.viewpoint.uri(); const g = i.gun.get(`identitiesBySearchKey`).get(uri); const attrs = {}; attrs[options.viewpoint.uri()] = options.viewpoint; if (options.self) { const keys = Object.keys(options.self); for (let i = 0;i < keys.length;i ++) { const a = new Attribute(keys[i], options.self[keys[i]]); attrs[a.uri()] = a; } } const id = await Identity.create(g, {trustDistance: 0, linkTo: options.viewpoint, attrs}); await i._addIdentityToIndexes(id.gun); if (options.self) { const recipient = Object.assign(options.self, {keyID: options.viewpoint.value}); Message.createVerification({recipient}, keypair).then(msg => { i.addMessage(msg); }); } return i; } static getMsgIndexKey(msg) { let distance = parseInt(msg.distance); distance = Number.isNaN(distance) ? 99 : distance; distance = (`00${distance}`).substring(distance.toString().length); // pad with zeros const key = `${distance}:${Math.floor(Date.parse(msg.timestamp || msg.signedData.timestamp) / 1000)}:${(msg.ipfs_hash || msg.hash).substr(0, 9)}`; return key; } static getMsgIndexKeys(msg) { const keys = {}; let distance = parseInt(msg.distance); distance = Number.isNaN(distance) ? 99 : distance; distance = (`00${distance}`).substring(distance.toString().length); // pad with zeros const hashSlice = msg.getHash().substr(0, 9); keys.messagesByHash = [msg.getHash()]; keys.messagesByTimestamp = [`${Math.floor(Date.parse(msg.timestamp || msg.signedData.timestamp) / 1000)}:${hashSlice}`]; keys.messagesByDistance = [`${distance}:${keys.messagesByTimestamp[0]}`]; keys.messagesByAuthor = []; const authors = msg.getAuthorArray(); for (let i = 0;i < authors.length;i ++) { keys.messagesByAuthor.push(`${authors[i].uri()}:${msg.signedData.timestamp}:${hashSlice}`); } keys.messagesByRecipient = []; const recipients = msg.getRecipientArray(); for (let i = 0;i < recipients.length;i ++) { keys.messagesByRecipient.push(`${recipients[i].uri()}:${msg.signedData.timestamp}:${hashSlice}`); } if ([`verification`, `unverification`].indexOf(msg.signedData.type) > - 1) { keys.verificationsByRecipientAndAuthor = []; for (let i = 0;i < recipients.length;i ++) { const r = recipients[i]; if (!r.isUniqueType()) { continue; } for (let j = 0;j < authors.length;j ++) { const a = authors[j]; if (!a.isUniqueType()) { continue; } keys.verificationsByRecipientAndAuthor.push(`${r.uri()}:${a.uri()}`); } } } else if (msg.signedData.type === `rating`) { keys.ratingsByRecipientAndAuthor = []; for (let i = 0;i < recipients.length;i ++) { const r = recipients[i]; if (!r.isUniqueType()) { continue; } for (let j = 0;j < authors.length;j ++) { const a = authors[j]; if (!a.isUniqueType()) { continue; } keys.ratingsByRecipientAndAuthor.push(`${r.uri()}:${a.uri()}`); } } } return keys; } async getIdentityIndexKeys(identity, hash) { const indexKeys = {identitiesByTrustDistance: [], identitiesBySearchKey: []}; let d; if (identity.linkTo && this.viewpoint.equals(identity.linkTo)) { d = 0; } else { d = await util.timeoutPromise(identity.get(`trustDistance`).then(), GUN_TIMEOUT); } function addIndexKey(a) { if (!(a && a.value && a.type)) { // TODO: this sometimes returns undefined return; } let distance = d !== undefined ? d : parseInt(a.dist); distance = Number.isNaN(distance) ? 99 : distance; distance = (`00${distance}`).substring(distance.toString().length); // pad with zeros const v = a.value || a[1]; const n = a.type || a[0]; const value = encodeURIComponent(v); const lowerCaseValue = encodeURIComponent(v.toLowerCase()); const name = encodeURIComponent(n); let key = `${value}:${name}`; let lowerCaseKey = `${lowerCaseValue}:${name}`; if (!Attribute.isUniqueType(n)) { // allow for multiple index keys with same non-unique attribute key = `${key}:${hash.substr(0, 9)}`; lowerCaseKey = `${lowerCaseKey}:${hash.substr(0, 9)}`; } indexKeys.identitiesBySearchKey.push(key); indexKeys.identitiesByTrustDistance.push(`${distance}:${key}`); if (key !== lowerCaseKey) { indexKeys.identitiesBySearchKey.push(lowerCaseKey); indexKeys.identitiesByTrustDistance.push(`${distance}:${lowerCaseKey}`); } if (v.indexOf(` `) > - 1) { const words = v.toLowerCase().split(` `); for (let l = 0;l < words.length;l += 1) { let k = `${encodeURIComponent(words[l])}:${name}`; if (!Attribute.isUniqueType(n)) { k = `${k}:${hash.substr(0, 9)}`; } indexKeys.identitiesBySearchKey.push(k); indexKeys.identitiesByTrustDistance.push(`${distance}:${k}`); } } if (key.match(/^http(s)?:\/\/.+\/[a-zA-Z0-9_]+$/)) { const split = key.split(`/`); indexKeys.identitiesBySearchKey.push(split[split.length - 1]); indexKeys.identitiesByTrustDistance.push(`${distance}:${split[split.length - 1]}`); } } if (this.viewpoint.equals(identity.linkTo)) { addIndexKey(identity.linkTo); } await identity.get(`attrs`).map().once(addIndexKey).then(); return indexKeys; } /** * @returns {Identity} viewpoint identity of the index */ async getViewpoint() { let vpAttr; if (this.viewpoint) { vpAttr = this.viewpoint; } else { vpAttr = new Attribute(await this.gun.get(`viewpoint`).then()); } return new Identity(this.gun.get(`identitiesBySearchKey`).get(vpAttr.uri())); } /** * Get an identity referenced by an identifier. * get(type, value) * get(Attribute) * get(value) - guesses the type or throws an error * @returns {Identity} identity that is connected to the identifier param */ get(a: String, b: String) { if (!a) { throw new Error(`get failed: param must be a string, received ${typeof a} ${a}`); } let attr = a; if (a.constructor.name !== `Attribute`) { let type, value; if (b) { type = a; value = b; } else { value = a; type = Attribute.guessTypeOf(value); } attr = new Attribute(type, value); } return new Identity(this.gun.get(`identitiesBySearchKey`).get(attr.uri()), attr); } async _getMsgs(msgIndex, callback, limit, cursor, desc, filter) { let results = 0; async function resultFound(result) { if (results >= limit) { return; } const msg = await Message.fromSig(result.value); if (filter && !filter(msg)) { return; } results ++; msg.cursor = result.key; if (result.value && result.value.ipfsUri) { msg.ipfsUri = result.value.ipfsUri; } callback(msg); } searchText(msgIndex, resultFound, ``, undefined, cursor, desc); } async _addIdentityToIndexes(id) { const hash = Gun.node.soul(id) || `todo`; const indexKeys = await this.getIdentityIndexKeys(id, hash.substr(0, 6)); const indexes = Object.keys(indexKeys); for (let i = 0;i < indexes.length;i ++) { const index = indexes[i]; for (let j = 0;j < indexKeys[index].length;j ++) { const key = indexKeys[index][j]; console.log(`adding key ${key}`); await this.gun.get(index).get(key).put(id); } } } /** * Get Messages sent by identity * @param {Identity} identity identity whose sent Messages to get * @param {Function} callback callback function that receives the Messages one by one */ async getSentMsgs(identity: Identity, callback, limit, cursor = ``, filter) { return this._getMsgs(identity.gun.get(`sent`), callback, limit, cursor, filter); } /** * Get Messages received by identity * @param {Identity} identity identity whose received Messages to get * @param {Function} callback callback function that receives the Messages one by one */ async getReceivedMsgs(identity, callback, limit, cursor = ``, filter) { return this._getMsgs(identity.gun.get(`received`), callback, limit, cursor, filter); } async _getAttributeTrustDistance(a) { if (!Attribute.isUniqueType(a.type)) { return; } if (this.viewpoint.equals(a)) { return 0; } const id = this.get(a); let d = await id.gun.get(`trustDistance`).then(); if (isNaN(d)) { d = Infinity; } return d; } /** * @param {Message} msg * @returns {number} trust distance to msg author. Returns undefined if msg signer is not trusted. */ async getMsgTrustDistance(msg) { let shortestDistance = Infinity; const signerAttr = new Attribute(`keyID`, msg.getSignerKeyID()); if (!signerAttr.equals(this.viewpoint)) { const signer = this.get(signerAttr); const d = await signer.gun.get(`trustDistance`).then(); if (isNaN(d)) { return; } } for (const a of msg.getAuthorArray()) { const d = await this._getAttributeTrustDistance(a); if (d < shortestDistance) { shortestDistance = d; } } return shortestDistance < Infinity ? shortestDistance : undefined; } async _updateMsgRecipientIdentity(msg, msgIndexKey, recipient) { const hash = `todo`; const identityIndexKeysBefore = await this.getIdentityIndexKeys(recipient, hash.substr(0, 6)); const attrs = await new Promise(resolve => { recipient.get(`attrs`).load(r => resolve(r)); }); if (msg.signedData.type === `verification`) { for (const a of msg.getRecipientArray()) { let hasAttr = false; Object.keys(attrs).forEach(k => { // TODO: if author is self, mark as self verified if (a.equals(attrs[k])) { attrs[k].conf = (attrs[k].conf || 0) + 1; hasAttr = true; } }); if (!hasAttr) { attrs[a.uri()] = {type: a.type, value: a.value, conf: 1, ref: 0}; } if (msg.goodVerification) { attrs[a.uri()].verified = true; } } recipient.get(`mostVerifiedAttributes`).put(Identity.getMostVerifiedAttributes(attrs)); // TODO: why this needs to be done twice to register? recipient.get(`mostVerifiedAttributes`).put(Identity.getMostVerifiedAttributes(attrs)); recipient.get(`attrs`).put(attrs); recipient.get(`attrs`).put(attrs); } if (msg.signedData.type === `rating`) { const id = await recipient.then(); id.receivedPositive = (id.receivedPositive || 0); id.receivedNegative = (id.receivedNegative || 0); id.receivedNeutral = (id.receivedNeutral || 0); if (msg.isPositive()) { if (typeof id.trustDistance !== `number` || msg.distance + 1 < id.trustDistance) { recipient.get(`trustDistance`).put(msg.distance + 1); } id.receivedPositive ++; } else { if (msg.distance < id.trustDistance) { recipient.get(`trustDistance`).put(false); // TODO: this should take into account the aggregate score of the identity } if (msg.isNegative()) { id.receivedNegative ++; } else { id.receivedNeutral ++; } } recipient.get(`receivedPositive`).put(id.receivedPositive); recipient.get(`receivedNegative`).put(id.receivedNegative); recipient.get(`receivedNeutral`).put(id.receivedNeutral); if (msg.signedData.context === `verifier`) { if (msg.distance === 0) { if (msg.isPositive) { recipient.get(`scores`).get(msg.signedData.context).get(`score`).put(10); } else if (msg.isNegative()) { recipient.get(`scores`).get(msg.signedData.context).get(`score`).put(0); } else { recipient.get(`scores`).get(msg.signedData.context).get(`score`).put(- 10); } } } else { // TODO: generic context-dependent score calculation } } const obj = {sig: msg.sig, pubKey: msg.pubKey}; if (msg.ipfsUri) { obj.ipfsUri = msg.ipfsUri; } recipient.get(`received`).get(msgIndexKey).put(obj); recipient.get(`received`).get(msgIndexKey).put(obj); const identityIndexKeysAfter = await this.getIdentityIndexKeys(recipient, hash.substr(0, 6)); const indexesBefore = Object.keys(identityIndexKeysBefore); for (let i = 0;i < indexesBefore.length;i ++) { const index = indexesBefore[i]; for (let j = 0;j < identityIndexKeysBefore[index].length;j ++) { const key = identityIndexKeysBefore[index][j]; if (!identityIndexKeysAfter[index] || identityIndexKeysAfter[index].indexOf(key) === - 1) { console.log(`removing stale key ${key} from index ${index}`); this.gun.get(index).get(key).put(null); } } } } async _updateMsgAuthorIdentity(msg, msgIndexKey, author) { if (msg.signedData.type === `rating`) { const id = await author.then(); id.sentPositive = (id.sentPositive || 0); id.sentNegative = (id.sentNegative || 0); id.sentNeutral = (id.sentNeutral || 0); if (msg.isPositive()) { id.sentPositive ++; } else if (msg.isNegative()) { id.sentNegative ++; } else { id.sentNeutral ++; } author.get(`sentPositive`).put(id.sentPositive); author.get(`sentNegative`).put(id.sentNegative); author.get(`sentNeutral`).put(id.sentNeutral); } const obj = {sig: msg.sig, pubKey: msg.pubKey}; if (msg.ipfsUri) { obj.ipfsUri = msg.ipfsUri; } author.get(`sent`).get(msgIndexKey).put(obj); // for some reason, doesn't work unless I do it twice author.get(`sent`).get(msgIndexKey).put(obj); return; } async _updateIdentityProfilesByMsg(msg, authorIdentities, recipientIdentities) { let msgIndexKey = Index.getMsgIndexKey(msg); msgIndexKey = msgIndexKey.substr(msgIndexKey.indexOf(`:`) + 1); const ids = Object.values(Object.assign({}, authorIdentities, recipientIdentities)); for (let i = 0;i < ids.length;i ++) { // add new identifiers to identity const data = await ids[i].gun.then(); // TODO: data is sometimes undefined and new identity is not added! const relocated = data ? this.gun.get(`identities`).set(data) : ids[i].gun; // this may screw up real time updates? and create unnecessary `identities` entries if (recipientIdentities.hasOwnProperty(ids[i].gun[`_`].link)) { await this._updateMsgRecipientIdentity(msg, msgIndexKey, ids[i].gun); } if (authorIdentities.hasOwnProperty(ids[i].gun[`_`].link)) { await this._updateMsgAuthorIdentity(msg, msgIndexKey, ids[i].gun); } await this._addIdentityToIndexes(relocated); } } async removeTrustedIndex(gunUri) { this.gun.get(`trustedIndexes`).get(gunUri).put(null); } async addTrustedIndex(gunUri, maxMsgsToCrawl = this.options.indexSync.importOnAdd.maxMsgCount, maxMsgDistance = this.options.indexSync.importOnAdd.maxMsgDistance) { if (gunUri === this.viewpoint.value) { return; } console.log(`addTrustedIndex`, gunUri); const exists = await this.gun.get(`trustedIndexes`).get(gunUri).then(); if (exists) { return; } this.gun.get(`trustedIndexes`).get(gunUri).put(true); const msgs = []; if (this.options.indexSync.importOnAdd.enabled) { await util.timeoutPromise(new Promise(resolve => { this.gun.user(gunUri).get(`identifi`).get(`messagesByDistance`).map((val, key) => { const d = Number.parseInt(key.split(`:`)[0]); if (!isNaN(d) && d <= maxMsgDistance) { Message.fromSig(val).then(msg => { msgs.push(msg); if (msgs.length >= maxMsgsToCrawl) { resolve(); } }); } }); }), 10000); console.log(`adding`, msgs.length, `msgs`); this.addMessages(msgs); } } async _updateIdentityIndexesByMsg(msg) { const recipientIdentities = {}; const authorIdentities = {}; let selfAuthored = false; for (const a of msg.getAuthorArray()) { const id = this.get(a); const td = await util.timeoutPromise(id.gun.get(`trustDistance`).then(), GUN_TIMEOUT); if (!isNaN(td)) { authorIdentities[id.gun[`_`].link] = id; const scores = await id.gun.get(`scores`).then(); if (scores && scores.verifier && msg.signedData.type === `verification`) { msg.goodVerification = true; } if (td === 0) { selfAuthored = true; } } } if (!Object.keys(authorIdentities).length) { return; // unknown author, do nothing } for (const a of msg.getRecipientArray()) { const id = this.get(a); const td = await util.timeoutPromise(id.gun.get(`trustDistance`).then(), GUN_TIMEOUT); if (!isNaN(td)) { recipientIdentities[id.gun[`_`].link] = id; } if (selfAuthored && a.type === `keyID` && a.value !== this.viewpoint.value) { // TODO: not if already added - causes infinite loop? if (msg.isPositive()) { this.addTrustedIndex(a.value); } else { this.removeTrustedIndex(a.value); } } } if (!Object.keys(recipientIdentities).length) { // recipient is previously unknown const attrs = {}; for (const a of msg.getRecipientArray()) { attrs[a.uri()] = a; } const linkTo = Identity.getLinkTo(attrs); const random = Math.floor(Math.random() * Number.MAX_SAFE_INTEGER); // TODO: bubblegum fix const trustDistance = msg.isPositive() && typeof msg.distance === `number` ? msg.distance + 1 : false; const id = await Identity.create(this.gun.get(`identities`).get(random).put({}), {attrs, linkTo, trustDistance}); // {a:1} because inserting {} causes a "no signature on data" error from gun // TODO: take msg author trust into account recipientIdentities[id.gun[`_`].link] = id; } return this._updateIdentityProfilesByMsg(msg, authorIdentities, recipientIdentities); } /** * Add a list of messages to the index. * Useful for example when adding a new WoT dataset that contains previously * unknown authors. * * Iteratively performs sorted merge joins on [previously known identities] and * [new msgs authors], until all messages from within the WoT have been added. * * @param {Array} msgs an array of messages. * @param {Object} ipfs (optional) ipfs instance where the messages are saved * @returns {boolean} true on success */ async addMessages(msgs) { const msgsByAuthor = {}; if (Array.isArray(msgs)) { console.log(`sorting ${msgs.length} messages onto a search tree...`); for (let i = 0;i < msgs.length;i ++) { for (const a of msgs[i].getAuthorArray()) { if (a.isUniqueType()) { const key = `${a.uri()}:${msgs[i].getHash()}`; msgsByAuthor[key] = msgs[i]; } } } console.log(`...done`); } else { throw `msgs param must be an array`; } const msgAuthors = Object.keys(msgsByAuthor).sort(); if (!msgAuthors.length) { return; } let initialMsgCount, msgCountAfterwards; const index = this.gun.get(`identitiesBySearchKey`); do { const knownIdentities = []; let stop = false; searchText(index, result => { if (stop) { return; } knownIdentities.push(result); }, ``); await new Promise(r => setTimeout(r, 2000)); // wait for results to accumulate stop = true; knownIdentities.sort((a, b) => { if (a.key === b.key) { return 0; } else if (a.key > b.key) { return 1; } else { return - 1; } }); let i = 0; let author = msgAuthors[i]; let knownIdentity = knownIdentities.shift(); initialMsgCount = msgAuthors.length; // sort-merge join identitiesBySearchKey and msgsByAuthor while (author && knownIdentity) { if (author.indexOf(knownIdentity.key) === 0) { try { await util.timeoutPromise(this.addMessage(msgsByAuthor[author], {checkIfExists: true}), 10000); } catch (e) { console.log(`adding failed:`, e, JSON.stringify(msgsByAuthor[author], null, 2)); } msgAuthors.splice(i, 1); author = i < msgAuthors.length ? msgAuthors[i] : undefined; //knownIdentity = knownIdentities.shift(); } else if (author < knownIdentity.key) { author = i < msgAuthors.length ? msgAuthors[++ i] : undefined; } else { knownIdentity = knownIdentities.shift(); } } msgCountAfterwards = msgAuthors.length; } while (msgCountAfterwards !== initialMsgCount); return true; } /** * @param msg Message to add to the index * @param ipfs (optional) ipfs instance where the message is additionally saved */ async addMessage(msg: Message, options = {}) { if (msg.constructor.name !== `Message`) { throw new Error(`addMessage failed: param must be a Message, received ${msg.constructor.name}`); } const hash = msg.getHash(); if (true === options.checkIfExists) { const exists = await this.gun.get(`messagesByHash`).get(hash).once().then(); if (exists) { return; } } msg.distance = await this.getMsgTrustDistance(msg); if (msg.distance === undefined) { return false; // do not save messages from untrusted author } const obj = {sig: msg.sig, pubKey: msg.pubKey}; const indexKeys = Index.getMsgIndexKeys(msg); for (const index in indexKeys) { for (let i = 0;i < indexKeys[index].length;i ++) { const key = indexKeys[index][i]; console.log(`adding to index ${index} message key ${key}`); this.gun.get(index).get(key).put(obj); this.gun.get(index).get(key).put(obj); // umm, what? doesn't work unless I write it twice } } if (this.options.ipfs) { try { const ipfsUri = await msg.saveToIpfs(this.options.ipfs); obj.ipfsUri = ipfsUri; this.gun.get(`messagesByHash`).get(ipfsUri).put(obj); this.gun.get(`messagesByHash`).get(ipfsUri).put(obj); } catch (e) { console.error(`adding msg ${msg} to ipfs failed: ${e}`); } } await this._updateIdentityIndexesByMsg(msg); return true; } /** * @param {string} value search string * @param {string} type (optional) type of searched value * @returns {Array} list of matching identities */ async search(value, type, callback, limit) { // TODO: param 'exact', type param const seen = {}; function searchTermCheck(key) { const arr = key.split(`:`); if (arr.length < 3) { return false; } const keyValue = arr[1]; const keyType = arr[2]; if (keyValue.indexOf(encodeURIComponent(value)) !== 0) { return false; } if (type && keyType !== type) { return false; } return true; } this.gun.get(`identitiesByTrustDistance`).map().once((id, key) => { if (Object.keys(seen).length >= limit) { // TODO: turn off .map cb return; } if (!searchTermCheck(key)) { return; } const soul = Gun.node.soul(id); if (soul && !seen.hasOwnProperty(soul)) { seen[soul] = true; const identity = new Identity(this.gun.get(`identitiesByTrustDistance`).get(key)); identity.cursor = key; callback(identity); } }); if (this.options.indexSync.query.enabled) { this.gun.get(`trustedIndexes`).map().once((val, key) => { if (val) { this.gun.user(key).get(`identifi`).get(`identitiesByTrustDistance`).map().once((id, k) => { if (Object.keys(seen).length >= limit) { // TODO: turn off .map cb return; } if (!searchTermCheck(key)) { return; } const soul = Gun.node.soul(id); if (soul && !seen.hasOwnProperty(soul)) { seen[soul] = true; callback( new Identity(this.gun.user(key).get(`identifi`).get(`identitiesByTrustDistance`).get(k)) ); } }); } }); } } /** * @returns {Array} list of messages */ getMessagesByTimestamp(callback, limit, cursor = ``, desc = true, filter) { const seen = {}; const cb = msg => { console.log(`hash`, msg.hash); if ((!limit || Object.keys(seen).length <= limit) && !seen.hasOwnProperty(msg.hash)) { seen[msg.hash] = true; callback(msg); } }; this._getMsgs(this.gun.get(`messagesByTimestamp`), cb, limit, cursor, filter); if (this.options.indexSync.query.enabled) { this.gun.get(`trustedIndexes`).map().once((val, key) => { if (val) { const n = this.gun.user(key).get(`identifi`).get(`messagesByTimestamp`); this._getMsgs(n, cb, limit, cursor, desc, filter); } }); } } /** * @returns {Array} list of messages */ getMessagesByDistance(callback, limit, cursor = ``, desc, filter) { const seen = {}; const cb = msg => { if (!seen.hasOwnProperty(msg.hash)) { if ((!limit || Object.keys(seen).length <= limit) && !seen.hasOwnProperty(msg.hash)) { seen[msg.hash] = true; callback(msg); } } }; this._getMsgs(this.gun.get(`messagesByDistance`), cb, limit, cursor, desc, filter); if (this.options.indexSync.query.enabled) { this.gun.get(`trustedIndexes`).map().once((val, key) => { if (val) { const n = this.gun.user(key).get(`identifi`).get(`messagesByDistance`); this._getMsgs(n, cb, limit, cursor, desc, filter); } }); } } } export default Index;