diff --git a/.eslintrc.json b/.eslintrc.json index bfcdeedb..6f4fbc0d 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -4,7 +4,6 @@ "rules": { "prettier/prettier": "error", "strict": "off", - "id-length": ["error", { "exceptions": ["_"] }], "no-console": "off", @@ -65,7 +64,18 @@ "no-throw-literal": "off", // lightning has sync methods and this rule bans them - "no-sync": "off" + "no-sync": "off", + + "id-length": "off", + + // typescript does this + "no-unused-vars": "off", + + // https://github.com/prettier/eslint-config-prettier/issues/132 + "line-comment-position": "off", + + // if someone does this it's probably intentional + "no-useless-concat": "off" }, "parser": "babel-eslint", "env": { diff --git a/package.json b/package.json index 5edf3bd1..7867869d 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,7 @@ "google-proto-files": "^1.0.3", "graphviz": "0.0.8", "grpc": "^1.21.1", - "gun": "^0.2019.1211", + "gun": "git://github.com/amark/gun#c59e0e95f92779ce6bb3aab823d318bc16b20c33", "husky": "^3.0.9", "jsonfile": "^4.0.0", "jsonwebtoken": "^8.3.0", @@ -62,6 +62,7 @@ "@types/express": "^4.17.1", "@types/gun": "^0.9.1", "@types/jest": "^24.0.18", + "@types/jsonwebtoken": "^8.3.7", "@types/lodash": "^4.14.141", "@types/socket.io": "^2.1.3", "@types/socket.io-client": "^1.4.32", diff --git a/services/gunDB/Mediator/index.js b/services/gunDB/Mediator/index.js index 18d8f401..31c8d2ae 100644 --- a/services/gunDB/Mediator/index.js +++ b/services/gunDB/Mediator/index.js @@ -2,9 +2,11 @@ * @format */ const Gun = require('gun') +// @ts-ignore +require('gun/lib/open') const debounce = require('lodash/debounce') -const once = require('lodash/once') const Encryption = require('../../../utils/encryptionStore') +const logger = require('winston') /** @type {import('../contact-api/SimpleGUN').ISEA} */ // @ts-ignore @@ -21,7 +23,10 @@ const IS_GUN_AUTH = 'IS_GUN_AUTH' mySEA.encrypt = (msg, secret) => { if (typeof msg !== 'string') { - throw new TypeError('mySEA.encrypt() -> expected msg to be an string') + throw new TypeError( + 'mySEA.encrypt() -> expected msg to be an string instead got: ' + + typeof msg + ) } if (msg.length === 0) { @@ -40,7 +45,10 @@ mySEA.encrypt = (msg, secret) => { mySEA.decrypt = (encMsg, secret) => { if (typeof encMsg !== 'string') { - throw new TypeError('mySEA.encrypt() -> expected encMsg to be an string') + throw new TypeError( + 'mySEA.encrypt() -> expected encMsg to be an string instead got: ' + + typeof encMsg + ) } if (encMsg.length === 0) { @@ -79,6 +87,12 @@ mySEA.decrypt = (encMsg, secret) => { } mySEA.secret = (recipientOrSenderEpub, recipientOrSenderSEA) => { + if (typeof recipientOrSenderEpub !== 'string') { + throw new TypeError('epub has to be an string') + } + if (typeof recipientOrSenderSEA !== 'object') { + throw new TypeError('sea has to be an object') + } if (recipientOrSenderEpub === recipientOrSenderSEA.pub) { throw new Error('Do not use pub for mysecret') } @@ -117,6 +131,7 @@ const Event = require('../event-constants') * @typedef {object} SimpleSocket * @prop {(eventName: string, data: Emission) => void} emit * @prop {(eventName: string, handler: (data: any) => void) => void} on + * @prop {{ query: { 'x-shockwallet-device-id': string }}} handshake */ /* eslint-disable init-declarations */ @@ -133,6 +148,11 @@ let user let _currentAlias = '' let _currentPass = '' +let mySec = '' + +/** @returns {string} */ +const getMySecret = () => mySec + let _isAuthenticating = false let _isRegistering = false @@ -179,19 +199,16 @@ const authenticate = async (alias, pass) => { if (typeof ack.err === 'string') { throw new Error(ack.err) } else if (typeof ack.sea === 'object') { - API.Jobs.onAcceptedRequests(user, mySEA) - API.Jobs.onOrders(user, gun, mySEA) - - const mySec = await mySEA.secret(user._.sea.epub, user._.sea) - if (typeof mySec !== 'string') { - throw new TypeError('mySec not an string') - } + mySec = await mySEA.secret(user._.sea.epub, user._.sea) _currentAlias = user.is ? user.is.alias : '' _currentPass = await mySEA.encrypt(pass, mySec) await new Promise(res => setTimeout(res, 5000)) + API.Jobs.onAcceptedRequests(user, mySEA) + API.Jobs.onOrders(user, gun, mySEA) + return ack.sea.pub } else { throw new Error('Unknown error.') @@ -305,6 +322,7 @@ class Mediator { this.socket.on(Action.SET_DISPLAY_NAME, this.setDisplayName) this.socket.on(Action.SEND_PAYMENT, this.sendPayment) this.socket.on(Action.SET_BIO, this.setBio) + this.socket.on(Action.DISCONNECT, this.disconnect) this.socket.on(Event.ON_AVATAR, this.onAvatar) this.socket.on(Event.ON_BLACKLIST, this.onBlacklist) @@ -319,20 +337,26 @@ class Mediator { this.socket.on(IS_GUN_AUTH, this.isGunAuth) } + /** @param {SimpleSocket} socket */ encryptSocketInstance = socket => { return { + /** + * @type {SimpleSocket['on']} + */ on: (eventName, cb) => { const deviceId = socket.handshake.query['x-shockwallet-device-id'] - socket.on(eventName, data => { + socket.on(eventName, _data => { try { if (Encryption.isNonEncrypted(eventName)) { - return cb(data) + return cb(_data) } - if (!data) { - return cb(data) + if (!_data) { + return cb(_data) } + let data = _data + if (!deviceId) { const error = { field: 'deviceId', @@ -350,16 +374,9 @@ class Mediator { console.error('Unknown Device', error) return false } - console.log('Emitting Data...', data) if (typeof data === 'string') { data = JSON.parse(data) } - console.log('Event:', eventName) - console.log('Data:', data) - console.log('Decrypt params:', { - deviceId, - message: data.encryptedKey - }) const decryptedKey = Encryption.decryptKey({ deviceId, message: data.encryptedKey @@ -377,6 +394,7 @@ class Mediator { } }) }, + /** @type {SimpleSocket['emit']} */ emit: (eventName, data) => { try { if (Encryption.isNonEncrypted(eventName)) { @@ -392,8 +410,10 @@ class Mediator { deviceId }) : data + socket.emit(eventName, encryptedMessage) } catch (err) { + logger.error(err.message) console.error(err) } } @@ -436,29 +456,6 @@ class Mediator { msg: null, origBody: body }) - - // refresh received requests - API.Events.onSimplerReceivedRequests( - debounce( - once(receivedRequests => { - if (Config.SHOW_LOG) { - console.log('---received requests---') - console.log(receivedRequests) - console.log('-----------------------') - } - - this.socket.emit(Event.ON_RECEIVED_REQUESTS, { - msg: receivedRequests, - ok: true, - origBody: body - }) - }), - 300 - ), - gun, - user, - mySEA - ) } catch (err) { console.log(err) this.socket.emit(Action.ACCEPT_REQUEST, { @@ -508,7 +505,7 @@ class Mediator { await throwOnInvalidToken(token) - await API.Actions.generateHandshakeAddress(user) + await API.Actions.generateHandshakeAddress() this.socket.emit(Action.GENERATE_NEW_HANDSHAKE_NODE, { ok: true, @@ -562,22 +559,6 @@ class Mediator { msg: null, origBody: body }) - - API.Events.onSimplerSentRequests( - debounce( - once(srs => { - this.socket.emit(Event.ON_SENT_REQUESTS, { - ok: true, - msg: srs, - origBody: body - }) - }), - 350 - ), - gun, - user, - mySEA - ) } catch (err) { if (Config.SHOW_LOG) { console.log('\n') @@ -815,24 +796,19 @@ class Mediator { await throwOnInvalidToken(token) - API.Events.onChats( - chats => { - if (Config.SHOW_LOG) { - console.log('---chats---') - console.log(chats) - console.log('-----------------------') - } + API.Events.onChats(chats => { + if (Config.SHOW_LOG) { + console.log('---chats---') + console.log(chats) + console.log('-----------------------') + } - this.socket.emit(Event.ON_CHATS, { - msg: chats, - ok: true, - origBody: body - }) - }, - gun, - user, - mySEA - ) + this.socket.emit(Event.ON_CHATS, { + msg: chats, + ok: true, + origBody: body + }) + }) } catch (err) { console.log(err) this.socket.emit(Event.ON_CHATS, { @@ -916,24 +892,19 @@ class Mediator { await throwOnInvalidToken(token) - API.Events.onSimplerReceivedRequests( - receivedRequests => { - if (Config.SHOW_LOG) { - console.log('---receivedRequests---') - console.log(receivedRequests) - console.log('-----------------------') - } + API.Events.onSimplerReceivedRequests(receivedRequests => { + if (Config.SHOW_LOG) { + console.log('---receivedRequests---') + console.log(receivedRequests) + console.log('-----------------------') + } - this.socket.emit(Event.ON_RECEIVED_REQUESTS, { - msg: receivedRequests, - ok: true, - origBody: body - }) - }, - gun, - user, - mySEA - ) + this.socket.emit(Event.ON_RECEIVED_REQUESTS, { + msg: receivedRequests, + ok: true, + origBody: body + }) + }) } catch (err) { console.log(err) this.socket.emit(Event.ON_RECEIVED_REQUESTS, { @@ -944,6 +915,8 @@ class Mediator { } } + onSentRequestsSubbed = false + /** * @param {Readonly<{ token: string }>} body */ @@ -953,24 +926,22 @@ class Mediator { await throwOnInvalidToken(token) - await API.Events.onSimplerSentRequests( - sentRequests => { - if (Config.SHOW_LOG) { - console.log('---sentRequests---') - console.log(sentRequests) - console.log('-----------------------') - } + if (!this.onSentRequestsSubbed) { + this.onSentRequestsSubbed = true - this.socket.emit(Event.ON_SENT_REQUESTS, { - msg: sentRequests, - ok: true, - origBody: body - }) - }, - gun, - user, - mySEA - ) + API.Events.onSimplerSentRequests( + debounce(sentRequests => { + console.log( + `new Reqss in mediator: ${JSON.stringify(sentRequests)}` + ) + this.socket.emit(Event.ON_SENT_REQUESTS, { + msg: sentRequests, + ok: true, + origBody: body + }) + }, 1000) + ) + } } catch (err) { console.log(err) this.socket.emit(Event.ON_SENT_REQUESTS, { @@ -1062,6 +1033,29 @@ class Mediator { }) } } + + /** @param {Readonly<{ pub: string, token: string }>} body */ + disconnect = async body => { + try { + const { pub, token } = body + + await throwOnInvalidToken(token) + + await API.Actions.disconnect(pub) + + this.socket.emit(Action.DISCONNECT, { + ok: true, + msg: null, + origBody: body + }) + } catch (err) { + this.socket.emit(Action.DISCONNECT, { + ok: false, + msg: err.message, + origBody: body + }) + } + } } /** @@ -1101,9 +1095,6 @@ const register = async (alias, pass) => { _isRegistering = false const mySecret = await mySEA.secret(user._.sea.epub, user._.sea) - if (typeof mySecret !== 'string') { - throw new Error('Could not generate secret for user.') - } if (typeof ack.err === 'string') { throw new Error(ack.err) @@ -1123,7 +1114,7 @@ const register = async (alias, pass) => { return authenticate(alias, pass).then(async pub => { await API.Actions.setDisplayName('anon' + pub.slice(0, 8), user) - await API.Actions.generateHandshakeAddress(user) + await API.Actions.generateHandshakeAddress() await API.Actions.generateOrderAddress(user) return pub }) @@ -1163,5 +1154,6 @@ module.exports = { instantiateGun, getGun, getUser, - mySEA + mySEA, + getMySecret } diff --git a/services/gunDB/action-constants.js b/services/gunDB/action-constants.js index 546bd5b7..bcb52aed 100644 --- a/services/gunDB/action-constants.js +++ b/services/gunDB/action-constants.js @@ -8,7 +8,8 @@ const Actions = { SEND_PAYMENT: "SEND_PAYMENT", SET_AVATAR: "SET_AVATAR", SET_DISPLAY_NAME: "SET_DISPLAY_NAME", - SET_BIO: "SET_BIO" + SET_BIO: "SET_BIO", + DISCONNECT: "DISCONNECT" }; module.exports = Actions; diff --git a/services/gunDB/contact-api/SimpleGUN.ts b/services/gunDB/contact-api/SimpleGUN.ts index d9270915..25f9e8ba 100644 --- a/services/gunDB/contact-api/SimpleGUN.ts +++ b/services/gunDB/contact-api/SimpleGUN.ts @@ -23,6 +23,10 @@ export type ListenerObj = Record & { export type ListenerData = Primitive | null | ListenerObj | undefined +interface OpenListenerDataObj { + [k: string]: OpenListenerData +} + export type Listener = (data: ListenerData, key: string) => void export type Callback = (ack: Ack) => void @@ -31,14 +35,19 @@ export interface Soul { put: Primitive | null | object | undefined } -export interface GUNNode { +export type OpenListenerData = Primitive | null | OpenListenerDataObj +export type OpenListener = (data: OpenListenerData, key: string) => void + +export interface GUNNodeBase { _: Soul - get(key: string): GUNNode + map(): GUNNode - put(data: ValidDataValue | GUNNode, cb?: Callback): GUNNode + on(this: GUNNode, cb: Listener): void once(this: GUNNode, cb?: Listener): GUNNode - set(data: ValidDataValue | GUNNode, cb?: Callback): GUNNode + + open(this: GUNNode, cb?: OpenListener): GUNNode + off(): void user(): UserGUNNode user(epub: string): GUNNode @@ -47,6 +56,12 @@ export interface GUNNode { then(cb: (v: ListenerData) => T): Promise } +export interface GUNNode extends GUNNodeBase { + get(key: string): GUNNode + put(data: ValidDataValue | GUNNode, cb?: Callback): GUNNode + set(data: ValidDataValue | GUNNode, cb?: Callback): GUNNode +} + export interface CreateAck { pub: string | undefined err: string | undefined diff --git a/services/gunDB/contact-api/actions.js b/services/gunDB/contact-api/actions.js index 1bb3d347..0235b296 100644 --- a/services/gunDB/contact-api/actions.js +++ b/services/gunDB/contact-api/actions.js @@ -9,6 +9,7 @@ const ErrorCode = require('./errorCode') const Getters = require('./getters') const Key = require('./key') const Utils = require('./utils') +// const { promisifyGunNode: p } = Utils const { isHandshakeRequest } = require('./schema') /** * @typedef {import('./SimpleGUN').GUNNode} GUNNode @@ -28,14 +29,6 @@ const { isHandshakeRequest } = require('./schema') */ const INITIAL_MSG = '$$__SHOCKWALLET__INITIAL__MESSAGE' -/** - * @returns {Message} - */ -const __createInitialMessage = () => ({ - body: INITIAL_MSG, - timestamp: Date.now() -}) - /** * Create a an outgoing feed. The feed will have an initial special acceptance * message. Returns a promise that resolves to the id of the newly-created @@ -57,13 +50,12 @@ const __createOutgoingFeed = async (withPublicKey, user, SEA) => { throw new Error(ErrorCode.NOT_AUTH) } - const mySecret = await SEA.secret(user._.sea.epub, user._.sea) - if (typeof mySecret !== 'string') { - throw new TypeError( - "__createOutgoingFeed() -> typeof mySecret !== 'string'" - ) - } + const mySecret = require('../Mediator').getMySecret() const encryptedForMeRecipientPub = await SEA.encrypt(withPublicKey, mySecret) + const ourSecret = await SEA.secret( + await Utils.pubToEpub(withPublicKey), + user._.sea + ) const maybeEncryptedForMeOutgoingFeedID = await Utils.tryAndWait( (_, user) => @@ -103,12 +95,18 @@ const __createOutgoingFeed = async (withPublicKey, user, SEA) => { throw new TypeError('typeof newOutgoingFeedID !== "string"') } + /** @type {Message} */ + const initialMsg = { + body: await SEA.encrypt(INITIAL_MSG, ourSecret), + timestamp: Date.now() + } + await new Promise((res, rej) => { user .get(Key.OUTGOINGS) .get(newOutgoingFeedID) .get(Key.MESSAGES) - .set(__createInitialMessage(), ack => { + .set(initialMsg, ack => { if (ack.err) { rej(new Error(ack.err)) } else { @@ -122,12 +120,6 @@ const __createOutgoingFeed = async (withPublicKey, user, SEA) => { mySecret ) - if (typeof encryptedForMeNewOutgoingFeedID === 'undefined') { - throw new TypeError( - "typeof encryptedForMeNewOutgoingFeedID === 'undefined'" - ) - } - await new Promise((res, rej) => { user .get(Key.RECIPIENT_TO_OUTGOING) @@ -253,10 +245,7 @@ const acceptRequest = async ( SEA ) - const mySecret = await SEA.secret(user._.sea.epub, user._.sea) - if (typeof mySecret !== 'string') { - throw new TypeError("acceptRequest() -> typeof mySecret !== 'string'") - } + const mySecret = require('../Mediator').getMySecret() const encryptedForMeIncomingID = await SEA.encrypt(incomingID, mySecret) await new Promise((res, rej) => { @@ -363,17 +352,15 @@ const blacklist = (publicKey, user) => }) /** - * @param {UserGUNNode} user * @returns {Promise} */ -const generateHandshakeAddress = user => - new Promise((res, rej) => { - if (!user.is) { - throw new Error(ErrorCode.NOT_AUTH) - } +const generateHandshakeAddress = async () => { + const gun = require('../Mediator').getGun() + const user = require('../Mediator').getUser() - const address = uuidv1() + const address = uuidv1() + await new Promise((res, rej) => { user.get(Key.CURRENT_HANDSHAKE_ADDRESS).put(address, ack => { if (ack.err) { rej(new Error(ack.err)) @@ -383,6 +370,86 @@ const generateHandshakeAddress = user => }) }) + await new Promise((res, rej) => { + gun + .get(Key.HANDSHAKE_NODES) + .get(address) + .put({ unused: 0 }, ack => { + if (ack.err) { + rej(new Error(ack.err)) + } else { + res() + } + }) + }) +} + +/** + * + * @param {string} pub + * @throws {Error} + * @returns {Promise} + */ +const cleanup = async pub => { + const user = require('../Mediator').getUser() + + const outGoingID = await Utils.recipientToOutgoingID(pub) + + await new Promise((res, rej) => { + user + .get(Key.USER_TO_INCOMING) + .get(pub) + .put(null, ack => { + if (ack.err) { + rej(new Error(ack.err)) + } else { + res() + } + }) + }) + + await new Promise((res, rej) => { + user + .get(Key.RECIPIENT_TO_OUTGOING) + .get(pub) + .put(null, ack => { + if (ack.err) { + rej(new Error(ack.err)) + } else { + res() + } + }) + }) + + await new Promise((res, rej) => { + user + .get(Key.USER_TO_LAST_REQUEST_SENT) + .get(pub) + .put(null, ack => { + if (ack.err) { + rej(new Error(ack.err)) + } else { + res() + } + }) + }) + + if (outGoingID) { + await new Promise((res, rej) => { + user + .get(Key.OUTGOINGS) + .get(outGoingID) + .put(null, ack => { + if (ack.err) { + rej(new Error(ack.err)) + } else { + res() + } + }) + }) + } +} + /** * @param {string} recipientPublicKey * @param {GUNNode} gun @@ -396,6 +463,8 @@ const sendHandshakeRequest = async (recipientPublicKey, gun, user, SEA) => { throw new Error(ErrorCode.NOT_AUTH) } + await cleanup(recipientPublicKey) + if (typeof recipientPublicKey !== 'string') { throw new TypeError( `recipientPublicKey is not string, got: ${typeof recipientPublicKey}` @@ -406,6 +475,10 @@ const sendHandshakeRequest = async (recipientPublicKey, gun, user, SEA) => { throw new TypeError('recipientPublicKey is an string of length 0') } + if (recipientPublicKey === user.is.pub) { + throw new Error('Do not send a request to yourself') + } + console.log('sendHR() -> before recipientEpub') /** @type {string} */ @@ -413,7 +486,7 @@ const sendHandshakeRequest = async (recipientPublicKey, gun, user, SEA) => { console.log('sendHR() -> before mySecret') - const mySecret = await SEA.secret(user._.sea.epub, user._.sea) + const mySecret = require('../Mediator').getMySecret() console.log('sendHR() -> before ourSecret') const ourSecret = await SEA.secret(recipientEpub, user._.sea) @@ -509,6 +582,11 @@ const sendHandshakeRequest = async (recipientPublicKey, gun, user, SEA) => { timestamp } + const encryptedForMeRecipientPublicKey = await SEA.encrypt( + recipientPublicKey, + mySecret + ) + console.log('sendHR() -> before newHandshakeRequestID') /** @type {string} */ const newHandshakeRequestID = await new Promise((res, rej) => { @@ -537,32 +615,8 @@ const sendHandshakeRequest = async (recipientPublicKey, gun, user, SEA) => { }) }) - // save request id to REQUEST_TO_USER - - const encryptedForMeRecipientPublicKey = await SEA.encrypt( - recipientPublicKey, - mySecret - ) - // This needs to come before the write to sent requests. Because that write // triggers Jobs.onAcceptedRequests and it in turn reads from request-to-user - // This also triggers Events.onSimplerSentRequests - await new Promise((res, rej) => { - user - .get(Key.REQUEST_TO_USER) - .get(newHandshakeRequestID) - .put(encryptedForMeRecipientPublicKey, ack => { - if (ack.err) { - rej( - new Error( - `Error saving recipient public key to request to user: ${ack.err}` - ) - ) - } else { - res() - } - }) - }) /** * @type {StoredReq} @@ -625,11 +679,7 @@ const sendMessage = async (recipientPublicKey, body, user, SEA) => { ) } - const outgoingID = await Utils.recipientToOutgoingID( - recipientPublicKey, - user, - SEA - ) + const outgoingID = await Utils.recipientToOutgoingID(recipientPublicKey) if (outgoingID === null) { throw new Error( @@ -668,10 +718,9 @@ const sendMessage = async (recipientPublicKey, body, user, SEA) => { * @param {string} recipientPub * @param {string} msgID * @param {UserGUNNode} user - * @param {ISEA} SEA * @returns {Promise} */ -const deleteMessage = async (recipientPub, msgID, user, SEA) => { +const deleteMessage = async (recipientPub, msgID, user) => { if (!user.is) { throw new Error(ErrorCode.NOT_AUTH) } @@ -700,7 +749,7 @@ const deleteMessage = async (recipientPub, msgID, user, SEA) => { ) } - const outgoingID = await Utils.recipientToOutgoingID(recipientPub, user, SEA) + const outgoingID = await Utils.recipientToOutgoingID(recipientPub) if (outgoingID === null) { throw new Error(`Could not fetch an outgoing id for user: ${recipientPub}`) @@ -1037,7 +1086,7 @@ const saveSeedBackup = async (mnemonicPhrase, user, SEA) => { throw new TypeError('expected mnemonicPhrase to be an string array') } - const mySecret = await SEA.secret(user._.sea.epub, user._.sea) + const mySecret = require('../Mediator').getMySecret() const encryptedSeed = await SEA.encrypt(mnemonicPhrase.join(' '), mySecret) return new Promise((res, rej) => { @@ -1051,8 +1100,21 @@ const saveSeedBackup = async (mnemonicPhrase, user, SEA) => { }) } +/** + * @param {string} pub + * @returns {Promise} + */ +const disconnect = async pub => { + if (!(await Utils.successfulHandshakeAlreadyExists(pub))) { + throw new Error('No handshake exists for this pub') + } + + await cleanup(pub) + + await generateHandshakeAddress() +} + module.exports = { - INITIAL_MSG, __createOutgoingFeed, acceptRequest, authenticate, @@ -1067,5 +1129,6 @@ module.exports = { sendPayment, generateOrderAddress, setBio, - saveSeedBackup + saveSeedBackup, + disconnect } diff --git a/services/gunDB/contact-api/events.js b/services/gunDB/contact-api/events.js deleted file mode 100644 index a0c14d9f..00000000 --- a/services/gunDB/contact-api/events.js +++ /dev/null @@ -1,1275 +0,0 @@ -/** - * @prettier - */ -const debounce = require('lodash/debounce') - -const Actions = require('./actions') -const ErrorCode = require('./errorCode') -const Key = require('./key') -const Schema = require('./schema') -const Utils = require('./utils') -const Config = require('../config') -/** - * @typedef {import('./SimpleGUN').UserGUNNode} UserGUNNode - * @typedef {import('./SimpleGUN').GUNNode} GUNNode - * @typedef {import('./SimpleGUN').ISEA} ISEA - * @typedef {import('./SimpleGUN').ListenerData} ListenerData - * @typedef {import('./schema').HandshakeRequest} HandshakeRequest - * @typedef {import('./schema').Message} Message - * @typedef {import('./schema').Outgoing} Outgoing - * @typedef {import('./schema').PartialOutgoing} PartialOutgoing - * @typedef {import('./schema').Chat} Chat - * @typedef {import('./schema').ChatMessage} ChatMessage - * @typedef {import('./schema').SimpleSentRequest} SimpleSentRequest - * @typedef {import('./schema').SimpleReceivedRequest} SimpleReceivedRequest - */ - -const DEBOUNCE_WAIT_TIME = 500 - -/** - * @param {string} outgoingKey - * @param {(message: Message, key: string) => void} cb - * @param {UserGUNNode} user - * @param {ISEA} SEA - * @returns {Promise} - */ -const __onOutgoingMessage = async (outgoingKey, cb, user, SEA) => { - if (!user.is) { - throw new Error(ErrorCode.NOT_AUTH) - } - - const mySecret = await SEA.secret(user._.sea.epub, user._.sea) - if (typeof mySecret !== 'string') { - throw new TypeError("typeof mySecret !== 'string'") - } - - const callb = debounce(cb, DEBOUNCE_WAIT_TIME) - /** @type {string} */ - const encryptedForMeRecipientPublicKey = await Utils.tryAndWait( - (_, user) => - new Promise((res, rej) => { - user - .get(Key.OUTGOINGS) - .get(outgoingKey) - .get('with') - .once(erpk => { - if (typeof erpk !== 'string') { - rej( - new TypeError("Expected outgoing.get('with') to be an string.") - ) - } else if (erpk.length === 0) { - rej( - new TypeError( - "Expected outgoing.get('with') to be a populated." - ) - ) - } else { - res(erpk) - } - }) - }) - ) - - const recipientPublicKey = await SEA.decrypt( - encryptedForMeRecipientPublicKey, - mySecret - ) - - if (typeof recipientPublicKey !== 'string') { - throw new TypeError( - "__onOutgoingMessage() -> typeof recipientPublicKey !== 'string'" - ) - } - - /** @type {string} */ - const recipientEpub = await Utils.pubToEpub(recipientPublicKey) - - const ourSecret = await SEA.secret(recipientEpub, user._.sea) - - if (typeof ourSecret !== 'string') { - throw new TypeError( - "__onOutgoingMessage() -> typeof ourSecret !== 'string'" - ) - } - - user - .get(Key.OUTGOINGS) - .get(outgoingKey) - .get(Key.MESSAGES) - .map() - .on(async (msg, key) => { - if (!Schema.isMessage(msg)) { - console.warn('non message received: ' + JSON.stringify(msg)) - return - } - - let { body } = msg - - if (body !== Actions.INITIAL_MSG) { - const decrypted = await SEA.decrypt(body, ourSecret) - - if (typeof decrypted !== 'string') { - console.log("__onOutgoingMessage() -> typeof decrypted !== 'string'") - } else { - body = decrypted - } - } - - callb( - { - body, - timestamp: msg.timestamp - }, - key - ) - }) -} - -/** - * Maps a sent request ID to the public key of the user it was sent to. - * @param {(requestToUser: Record) => void} cb - * @param {UserGUNNode} user Pass only for testing purposes. - * @param {ISEA} SEA - * @returns {Promise} - */ -const __onSentRequestToUser = async (cb, user, SEA) => { - if (!user.is) { - throw new Error(ErrorCode.NOT_AUTH) - } - - const callb = debounce(cb, DEBOUNCE_WAIT_TIME) - - /** @type {Record} */ - const requestToUser = {} - callb(requestToUser) - - const mySecret = await SEA.secret(user._.sea.epub, user._.sea) - if (typeof mySecret !== 'string') { - throw new TypeError( - "__onSentRequestToUser() -> typeof mySecret !== 'string'" - ) - } - - user - .get(Key.REQUEST_TO_USER) - .map() - .on(async (encryptedUserPub, requestID) => { - if (typeof encryptedUserPub !== 'string') { - console.error('got a non string value') - return - } - if (encryptedUserPub.length === 0) { - console.error('got an empty string value') - return - } - - const userPub = await SEA.decrypt(encryptedUserPub, mySecret) - if (typeof userPub !== 'string') { - console.log(`__onSentRequestToUser() -> typeof userPub !== 'string'`) - return - } - - requestToUser[requestID] = userPub - callb(requestToUser) - }) -} - -/** - * @param {(userToOutgoing: Record) => void} cb - * @param {UserGUNNode} user Pass only for testing purposes. - * @param {ISEA} SEA - * @returns {Promise} - */ -const __onUserToIncoming = async (cb, user, SEA) => { - if (!user.is) { - throw new Error(ErrorCode.NOT_AUTH) - } - - const callb = debounce(cb, DEBOUNCE_WAIT_TIME) - - /** @type {Record} */ - const userToOutgoing = {} - - const mySecret = await SEA.secret(user._.sea.epub, user._.sea) - if (typeof mySecret !== 'string') { - throw new TypeError("__onUserToIncoming() -> typeof mySecret !== 'string'") - } - - user - .get(Key.USER_TO_INCOMING) - .map() - .on(async (encryptedIncomingID, userPub) => { - if (typeof encryptedIncomingID !== 'string') { - console.error('got a non string value') - return - } - - if (encryptedIncomingID.length === 0) { - console.error('got an empty string value') - return - } - - const incomingID = await SEA.decrypt(encryptedIncomingID, mySecret) - - if (typeof incomingID === 'undefined') { - console.warn('could not decrypt incomingID inside __onUserToIncoming') - return - } - - userToOutgoing[userPub] = incomingID - - callb(userToOutgoing) - }) -} - -/** - * @param {(avatar: string|null) => void} cb - * @param {UserGUNNode} user Pass only for testing purposes. - * @throws {Error} If user hasn't been auth. - * @returns {void} - */ -const onAvatar = (cb, user) => { - if (!user.is) { - throw new Error(ErrorCode.NOT_AUTH) - } - - const callb = debounce(cb, DEBOUNCE_WAIT_TIME) - // Initial value if avvatar is undefined in gun - callb(null) - - user - .get(Key.PROFILE) - .get(Key.AVATAR) - .on(avatar => { - if (typeof avatar === 'string' || avatar === null) { - callb(avatar) - } - }) -} - -/** - * @param {(blacklist: string[]) => void} cb - * @param {UserGUNNode} user - * @returns {void} - */ -const onBlacklist = (cb, user) => { - /** @type {string[]} */ - const blacklist = [] - - if (!user.is) { - throw new Error(ErrorCode.NOT_AUTH) - } - - const callb = debounce(cb, DEBOUNCE_WAIT_TIME) - - // Initial value if no items are in blacklist in gun - callb(blacklist) - - user - .get(Key.BLACKLIST) - .map() - .on(publicKey => { - if (typeof publicKey === 'string' && publicKey.length > 0) { - blacklist.push(publicKey) - callb(blacklist) - } else { - console.warn('Invalid public key received for blacklist') - } - }) -} - -/** - * @param {(currentHandshakeAddress: string|null) => void} cb - * @param {UserGUNNode} user - * @returns {void} - */ -const onCurrentHandshakeAddress = (cb, user) => { - if (!user.is) { - throw new Error(ErrorCode.NOT_AUTH) - } - - const callb = debounce(cb, DEBOUNCE_WAIT_TIME) - - // If undefined, callback below wont be called. Let's supply null as the - // initial value. - callb(null) - - user.get(Key.CURRENT_HANDSHAKE_ADDRESS).on(addr => { - if (typeof addr !== 'string') { - console.error('expected handshake address to be an string') - - callb(null) - - return - } - - callb(addr) - }) -} - -/** - * @param {(displayName: string|null) => void} cb - * @param {UserGUNNode} user Pass only for testing purposes. - * @throws {Error} If user hasn't been auth. - * @returns {void} - */ -const onDisplayName = (cb, user) => { - if (!user.is) { - throw new Error(ErrorCode.NOT_AUTH) - } - - const callb = debounce(cb, DEBOUNCE_WAIT_TIME) - - // Initial value if display name is undefined in gun - callb(null) - - user - .get(Key.PROFILE) - .get(Key.DISPLAY_NAME) - .on(displayName => { - if (typeof displayName === 'string' || displayName === null) { - callb(displayName) - } - }) -} - -/** - * @param {(messages: Record) => void} cb - * @param {string} userPK Public key of the user from whom the incoming - * messages will be obtained. - * @param {string} incomingFeedID ID of the outgoing feed from which the - * incoming messages will be obtained. - * @param {GUNNode} gun (Pass only for testing purposes) - * @param {UserGUNNode} user - * @param {ISEA} SEA - * @returns {void} - */ -const onIncomingMessages = (cb, userPK, incomingFeedID, gun, user, SEA) => { - if (!user.is) { - throw new Error(ErrorCode.NOT_AUTH) - } - - const callb = debounce(cb, DEBOUNCE_WAIT_TIME) - - const otherUser = gun.user(userPK) - - /** - * @type {Record} - */ - const messages = {} - - callb(messages) - - otherUser - .get(Key.OUTGOINGS) - .get(incomingFeedID) - .get(Key.MESSAGES) - .map() - .on(async (data, key) => { - if (!Schema.isMessage(data)) { - console.warn('non-message received') - return - } - - /** @type {string} */ - const recipientEpub = await Utils.pubToEpub(userPK) - - const secret = await SEA.secret(recipientEpub, user._.sea) - - let { body } = data - - if (body !== Actions.INITIAL_MSG) { - const decrypted = await SEA.decrypt(body, secret) - - if (typeof decrypted !== 'string') { - console.log("onIncommingMessages() -> typeof decrypted !== 'string'") - return - } - - body = decrypted - } - - messages[key] = { - body, - timestamp: data.timestamp - } - - callb(messages) - }) -} - -/** - * - * @param {(outgoings: Record) => void} cb - * @param {UserGUNNode} user - * @param {ISEA} SEA - * @param {typeof __onOutgoingMessage} onOutgoingMessage - */ -const onOutgoing = async ( - cb, - user, - SEA, - onOutgoingMessage = __onOutgoingMessage -) => { - if (!user.is) { - throw new Error(ErrorCode.NOT_AUTH) - } - - const callb = debounce(cb, DEBOUNCE_WAIT_TIME) - - const mySecret = await SEA.secret(user._.sea.epub, user._.sea) - if (typeof mySecret !== 'string') { - throw new TypeError("onOutgoing() -> typeof mySecret !== 'string'") - } - - /** - * @type {Record} - */ - const outgoings = {} - - callb(outgoings) - - /** - * @type {string[]} - */ - const outgoingsWithMessageListeners = [] - - user - .get(Key.OUTGOINGS) - .map() - .on(async (data, key) => { - if (!Schema.isPartialOutgoing(data)) { - console.warn('not partial outgoing') - console.warn(JSON.stringify(data)) - return - } - - const decryptedRecipientPublicKey = await SEA.decrypt(data.with, mySecret) - - if (typeof decryptedRecipientPublicKey !== 'string') { - console.log( - "onOutgoing() -> typeof decryptedRecipientPublicKey !== 'string'" - ) - return - } - - outgoings[key] = { - messages: outgoings[key] ? outgoings[key].messages : {}, - with: decryptedRecipientPublicKey - } - - if (!outgoingsWithMessageListeners.includes(key)) { - outgoingsWithMessageListeners.push(key) - - onOutgoingMessage( - key, - (msg, msgKey) => { - outgoings[key].messages = { - ...outgoings[key].messages, - [msgKey]: msg - } - - callb(outgoings) - }, - user, - SEA - ) - } - - callb(outgoings) - }) -} - -/** - * Massages all of the more primitive data structures into a more manageable - * 'Chat' paradigm. - * @param {(chats: Chat[]) => void} cb - * @param {GUNNode} gun - * @param {UserGUNNode} user - * @param {ISEA} SEA - * @returns {void} - */ -const onChats = (cb, gun, user, SEA) => { - if (!user.is) { - throw new Error(ErrorCode.NOT_AUTH) - } - - /** - * @type {Record} - */ - const recipientPKToChat = {} - - /** - * Keep track of the users for which we already set up avatar listeners. - * @type {string[]} - */ - const usersWithAvatarListeners = [] - - /** - * Keep track of the users for which we already set up display name listeners. - * @type {string[]} - */ - const usersWithDisplayNameListeners = [] - - /** - * Keep track of the user for which we already set up incoming feed listeners. - * @type {string[]} - */ - const usersWithIncomingListeners = [] - - const _callCB = () => { - // Only provide chats that have incoming listeners which would be contacts - // that were actually accepted / are going on - // Only provide chats that have received at least 1 message from gun - const chats = Object.values(recipientPKToChat) - .filter(chat => - usersWithIncomingListeners.includes(chat.recipientPublicKey) - ) - .filter(chat => Schema.isChat(chat)) - .filter(chat => chat.messages.length > 0) - - // in case someone else elsewhere forgets about sorting - chats.forEach(chat => { - chat.messages = chat.messages - .slice(0) - .sort((msgA, msgB) => msgA.timestamp - msgB.timestamp) - }) - - cb(chats) - } - - // chats seem to require a bit more of debounce time - const callCB = debounce(_callCB, DEBOUNCE_WAIT_TIME + 200) - - callCB() - - onOutgoing( - outgoings => { - for (const outgoing of Object.values(outgoings)) { - const recipientPK = outgoing.with - - if (!recipientPKToChat[recipientPK]) { - recipientPKToChat[recipientPK] = { - messages: [], - recipientAvatar: '', - recipientDisplayName: Utils.defaultName(recipientPK), - recipientPublicKey: recipientPK - } - } - - const { messages } = recipientPKToChat[recipientPK] - - for (const [msgK, msg] of Object.entries(outgoing.messages)) { - if (!messages.find(_msg => _msg.id === msgK)) { - messages.push({ - body: msg.body, - id: msgK, - outgoing: true, - timestamp: msg.timestamp - }) - } - } - } - - callCB() - }, - user, - SEA - ) - - __onUserToIncoming( - uti => { - for (const [recipientPK, incomingFeedID] of Object.entries(uti)) { - if (!recipientPKToChat[recipientPK]) { - recipientPKToChat[recipientPK] = { - messages: [], - recipientAvatar: '', - recipientDisplayName: Utils.defaultName(recipientPK), - recipientPublicKey: recipientPK - } - } - - const chat = recipientPKToChat[recipientPK] - - if (!usersWithIncomingListeners.includes(recipientPK)) { - usersWithIncomingListeners.push(recipientPK) - - onIncomingMessages( - msgs => { - for (const [msgK, msg] of Object.entries(msgs)) { - const { messages } = chat - - if (!messages.find(_msg => _msg.id === msgK)) { - messages.push({ - body: msg.body, - id: msgK, - outgoing: false, - timestamp: msg.timestamp - }) - } - } - - callCB() - }, - recipientPK, - incomingFeedID, - gun, - user, - SEA - ) - } - - if (!usersWithAvatarListeners.includes(recipientPK)) { - usersWithAvatarListeners.push(recipientPK) - - gun - .user(recipientPK) - .get(Key.PROFILE) - .get(Key.AVATAR) - .on(avatar => { - if (typeof avatar === 'string') { - chat.recipientAvatar = avatar - callCB() - } - }) - } - - if (!usersWithDisplayNameListeners.includes(recipientPK)) { - usersWithDisplayNameListeners.push(recipientPK) - - gun - .user(recipientPK) - .get(Key.PROFILE) - .get(Key.DISPLAY_NAME) - .on(displayName => { - if (typeof displayName === 'string') { - chat.recipientDisplayName = displayName - callCB() - } - }) - } - } - }, - user, - SEA - ) -} - -/** - * - * @param {(simpleReceivedRequests: SimpleReceivedRequest[]) => void} cb - * @param {GUNNode} gun - * @param {UserGUNNode} user - * @param {ISEA} SEA - * @returns {void} - */ -const onSimplerReceivedRequests = (cb, gun, user, SEA) => { - try { - if (!user.is) { - throw new Error(ErrorCode.NOT_AUTH) - } - - /** @type {Record} */ - const idToRequest = {} - - /** @type {string[]} */ - const requestorsWithAvatarListeners = [] - - /** @type {string[]} */ - const requestorsWithDisplayNameListeners = [] - - /** - * @type {Partial>} - */ - const requestorToAvatar = {} - - /** - * @type {Partial>} - */ - const requestorToDisplayName = {} - - /** @type {Set} */ - const requestorsAlreadyAccepted = new Set() - - /** - * We cannot call gun.off(), so keep track of the current handshake addres. - * And only run the listeners for the handshake nodes if they are for the - * current handshake address node. - */ - let currentHandshakeAddress = '' - - //////////////////////////////////////////////////////////////////////////// - - const _callCB = async () => { - try { - const requestEntries = Object.entries(idToRequest) - - if (Config.SHOW_LOG) { - console.log('requestorsAlreadyAccepted') - console.log(requestorsAlreadyAccepted) - console.log('/requestorsAlreadyAccepted') - } - - if (Config.SHOW_LOG) { - console.log('raw requests:') - console.log(idToRequest) - console.log('/raw requests') - } - - // avoid race conditions due to gun's reactive nature. - const onlyInCurrentHandshakeNode = await Utils.asyncFilter( - requestEntries, - async ([id]) => { - try { - const HNAddr = await Utils.tryAndWait(async (_, user) => { - const data = await user - .get(Key.CURRENT_HANDSHAKE_ADDRESS) - .then() - - if (typeof data !== 'string') { - throw new Error('handshake address not an string') - } - - return data - }) - - const maybeHreq = await Utils.tryAndWait(gun => - gun - .get(Key.HANDSHAKE_NODES) - .get(HNAddr) - .get(id) - .then() - ) - - return Schema.isHandshakeRequest(maybeHreq) - } catch (err) { - console.log(`error for request ID: ${id}`) - throw err - } - } - ) - - if (Config.SHOW_LOG) { - console.log('onlyInCurrentHandshakeNode') - console.log(onlyInCurrentHandshakeNode) - console.log('/onlyInCurrentHandshakeNode') - } - - // USER-TO-INCOMING (which indicates acceptance of this request) write - // might not be in there by the time we are looking at these requests. - // Let's account for this. - const notAccepted = await Utils.asyncFilter( - onlyInCurrentHandshakeNode, - async ([reqID, req]) => { - try { - if (requestorsAlreadyAccepted.has(req.from)) { - return false - } - - const requestorEpub = await Utils.pubToEpub(req.from) - - const ourSecret = await SEA.secret(requestorEpub, user._.sea) - if (typeof ourSecret !== 'string') { - throw new TypeError('typeof ourSecret !== "string"') - } - - const decryptedResponse = await SEA.decrypt( - req.response, - ourSecret - ) - - if (typeof decryptedResponse !== 'string') { - throw new TypeError('typeof decryptedResponse !== "string"') - } - - const outfeedID = decryptedResponse - - if (Config.SHOW_LOG) { - console.log('\n') - console.log('--------outfeedID----------') - console.log(outfeedID) - console.log('------------------') - console.log('\n') - } - - const maybeOutfeed = await Utils.tryAndWait(gun => - gun - .user(req.from) - .get(Key.OUTGOINGS) - .get(outfeedID) - .then() - ) - - if (Config.SHOW_LOG) { - console.log('\n') - console.log('--------maybeOutfeed----------') - console.log(maybeOutfeed) - console.log('------------------') - console.log('\n') - } - - const wasAccepted = Schema.isHandshakeRequest(maybeOutfeed) - - return !wasAccepted - } catch (err) { - console.log(`error for request ID: ${reqID}`) - throw err - } - } - ) - - if (Config.SHOW_LOG) { - console.log('notAccepted') - console.log(notAccepted) - console.log('/notAccepted') - } - - const simpleReceivedReqs = notAccepted.map(([reqID, req]) => { - try { - const { from: requestorPub } = req - - /** @type {SimpleReceivedRequest} */ - const simpleReceivedReq = { - id: reqID, - requestorAvatar: requestorToAvatar[requestorPub] || null, - requestorDisplayName: - requestorToDisplayName[requestorPub] || - Utils.defaultName(requestorPub), - requestorPK: requestorPub, - response: req.response, - timestamp: req.timestamp - } - - return simpleReceivedReq - } catch (err) { - console.log(`error for request ID: ${reqID}`) - throw err - } - }) - - cb(simpleReceivedReqs) - } catch (err) { - console.error(err) - } - } - - const callCB = debounce(_callCB, DEBOUNCE_WAIT_TIME) - callCB() - - user - .get(Key.USER_TO_INCOMING) - .map() - .on((_, userPK) => { - requestorsAlreadyAccepted.add(userPK) - - callCB() - }) - - //////////////////////////////////////////////////////////////////////////// - /** - * @param {string} addr - * @returns {(req: ListenerData, reqID: string) => void} - */ - const listenerForAddr = addr => (req, reqID) => { - try { - if (addr !== currentHandshakeAddress) { - console.log( - 'onSimplerReceivedRequests() -> listenerForAddr() -> stale handshake address, quitting' - ) - return - } - - if (!Schema.isHandshakeRequest(req)) { - console.log( - 'onSimplerReceivedRequests() -> listenerForAddr() -> bad handshake request, quitting' - ) - console.log(req) - return - } - - idToRequest[reqID] = req - callCB() - - if (!requestorsWithAvatarListeners.includes(req.from)) { - requestorsWithAvatarListeners.push(req.from) - - gun - .user(req.from) - .get(Key.PROFILE) - .get(Key.AVATAR) - .on(avatar => { - if (typeof avatar === 'string' || avatar === null) { - // || handles empty strings - requestorToAvatar[req.from] = avatar || null - - callCB() - } - }) - } - - if (!requestorsWithDisplayNameListeners.includes(req.from)) { - requestorsWithDisplayNameListeners.push(req.from) - - gun - .user(req.from) - .get(Key.PROFILE) - .get(Key.DISPLAY_NAME) - .on(displayName => { - if (typeof displayName === 'string' || displayName === null) { - // || handles empty strings - requestorToDisplayName[req.from] = displayName || null - - callCB() - } - }) - } - } catch (err) { - console.log('onSimplerReceivedRequests() -> listenerForAddr() ->') - console.log(err) - } - - callCB() - } - //////////////////////////////////////////////////////////////////////////// - user.get(Key.CURRENT_HANDSHAKE_ADDRESS).on(addr => { - if (typeof addr !== 'string') { - throw new TypeError('current handshake address not an string') - } - - console.log( - `onSimplerReceivedRequests() -> setting current address to ${addr}` - ) - currentHandshakeAddress = addr - - gun - .get(Key.HANDSHAKE_NODES) - .get(addr) - .map() - .on(listenerForAddr(addr)) - - callCB() - }) - } catch (err) { - console.log(`onSimplerReceivedRequests() -> ${err.message}`) - } -} - -/** - * @param {(sentRequests: SimpleSentRequest[]) => void} cb - * @param {GUNNode} gun - * @param {UserGUNNode} user - * @param {ISEA} SEA - * @returns {Promise} - */ -const onSimplerSentRequests = async (cb, gun, user, SEA) => { - if (!user.is) { - throw new Error(ErrorCode.NOT_AUTH) - } - /** - * @type {Record} - */ - const storedReqs = {} - /** - * @type {Partial>} - */ - const recipientToAvatar = {} - /** - * @type {Partial>} - */ - const recipientToDisplayName = {} - /** - * @type {Partial>} - */ - const recipientToCurrentHandshakeAddress = {} - /** - * @type {Record} - */ - const simpleSentRequests = {} - /** - * Keep track of recipients that already have listeners for their avatars. - * @type {string[]} - */ - const recipientsWithAvatarListener = [] - /** - * Keep track of recipients that already have listeners for their display - * name. - * @type {string[]} - */ - const recipientsWithDisplayNameListener = [] - /** - * Keep track of recipients that already have listeners for their current - * handshake node. - * @type {string[]} - */ - const recipientsWithCurrentHandshakeAddressListener = [] - - const mySecret = await SEA.secret(user._.sea.epub, user._.sea) - - if (typeof mySecret !== 'string') { - throw new TypeError("typeof mySecret !== 'string'") - } - - const callCB = debounce(async () => { - try { - console.log('\n') - console.log('------simplerSentRequests: rawRequests ------') - console.log(storedReqs) - console.log('\n') - - const entries = Object.entries(storedReqs) - - /** @type {Promise[]} */ - const promises = entries.map(([, storedReq]) => - (async () => { - const recipientPub = await SEA.decrypt( - storedReq.recipientPub, - mySecret - ) - if (typeof recipientPub !== 'string') { - throw new TypeError() - } - const requestAddress = await SEA.decrypt( - storedReq.handshakeAddress, - mySecret - ) - if (typeof requestAddress !== 'string') { - throw new TypeError() - } - const sentReqID = await SEA.decrypt(storedReq.sentReqID, mySecret) - if (typeof sentReqID !== 'string') { - throw new TypeError() - } - - /** @type {Schema.HandshakeRequest} */ - const sentReq = await Utils.tryAndWait(async gun => { - const data = await gun - .get(Key.HANDSHAKE_NODES) - .get(requestAddress) - .get(sentReqID) - .then() - - if (Schema.isHandshakeRequest(data)) { - return data - } - - throw new TypeError('sent req not a handshake request') - }) - - const latestReqIDForRecipient = await Utils.recipientPubToLastReqSentID( - recipientPub - ) - - if ( - await Utils.reqWasAccepted( - sentReq.response, - recipientPub, - user, - SEA - ) - ) { - return null - } - - if ( - !recipientsWithCurrentHandshakeAddressListener.includes( - recipientPub - ) - ) { - recipientsWithCurrentHandshakeAddressListener.push(recipientPub) - - gun - .user(recipientPub) - .get(Key.CURRENT_HANDSHAKE_ADDRESS) - .on(addr => { - if (typeof addr !== 'string') { - console.log( - "onSimplerSentRequests() -> typeof addr !== 'string'" - ) - - return - } - - recipientToCurrentHandshakeAddress[recipientPub] = addr - - callCB() - }) - } - - if (!recipientsWithAvatarListener.includes(recipientPub)) { - recipientsWithAvatarListener.push(recipientPub) - - gun - .user(recipientPub) - .get(Key.PROFILE) - .get(Key.AVATAR) - .on(avatar => { - if (typeof avatar === 'string' || avatar === null) { - recipientToAvatar[recipientPub] = avatar - callCB() - } - }) - } - - if (!recipientsWithDisplayNameListener.includes(recipientPub)) { - recipientsWithDisplayNameListener.push(recipientPub) - - gun - .user(recipientPub) - .get(Key.PROFILE) - .get(Key.DISPLAY_NAME) - .on(displayName => { - if (typeof displayName === 'string' || displayName === null) { - recipientToDisplayName[recipientPub] = displayName - callCB() - } - }) - } - - const isStaleRequest = latestReqIDForRecipient !== sentReqID - - if (isStaleRequest) { - return null - } - - /** - * @type {SimpleSentRequest} - */ - const res = { - id: sentReqID, - recipientAvatar: recipientToAvatar[recipientPub] || null, - recipientChangedRequestAddress: false, - recipientDisplayName: - recipientToDisplayName[recipientPub] || - Utils.defaultName(recipientPub), - recipientPublicKey: recipientPub, - timestamp: sentReq.timestamp - } - - return res - })() - ) - - const reqsOrNulls = await Promise.all(promises) - - console.log('\n') - console.log('------simplerSentRequests: reqsOrNulls ------') - console.log(reqsOrNulls) - console.log('\n') - - /** @type {SimpleSentRequest[]} */ - // @ts-ignore - const reqs = reqsOrNulls.filter(item => item !== null) - - for (const req of reqs) { - simpleSentRequests[req.id] = req - } - } catch (err) { - console.log(`onSimplerSentRequests() -> callCB() -> ${err.message}`) - } finally { - cb(Object.values(simpleSentRequests)) - } - }, DEBOUNCE_WAIT_TIME) - - callCB() - - // force a refresh when a request is accepted - user.get(Key.USER_TO_INCOMING).on(() => { - callCB() - }) - - user - .get(Key.STORED_REQS) - .map() - .on((sentRequest, sentRequestID) => { - try { - if (!Schema.isStoredRequest(sentRequest)) { - console.log('\n') - console.log( - '------simplerSentRequests: !Schema.isHandshakeRequest(sentRequest) ------' - ) - console.log(sentRequest) - console.log('\n') - - return - } - - storedReqs[sentRequestID] = sentRequest - - callCB() - } catch (err) { - console.log( - `onSimplerSentRequests() -> sentRequestID: ${sentRequestID} -> ${err.message}` - ) - } - }) -} - -/** @type {string|null} */ -let currentBio = null - -/** - * @param {(bio: string|null) => void} cb - * @param {UserGUNNode} user Pass only for testing purposes. - * @throws {Error} If user hasn't been auth. - * @returns {void} - */ -const onBio = (cb, user) => { - if (!user.is) { - throw new Error(ErrorCode.NOT_AUTH) - } - - const callb = debounce(cb, DEBOUNCE_WAIT_TIME) - // Initial value if avvatar is undefined in gun - callb(currentBio) - - user.get(Key.BIO).on(bio => { - if (typeof bio === 'string' || bio === null) { - currentBio = bio - callb(bio) - } - }) -} - -/** @type {string|null} */ -let currentSeedBackup = null - -/** - * @param {(seedBackup: string|null) => void} cb - * @param {UserGUNNode} user - * @param {ISEA} SEA - * @throws {Error} If user hasn't been auth. - * @returns {Promise} - */ -const onSeedBackup = async (cb, user, SEA) => { - if (!user.is) { - throw new Error(ErrorCode.NOT_AUTH) - } - - const mySecret = await SEA.secret(user._.sea.epub, user._.sea) - - const callb = debounce(cb, DEBOUNCE_WAIT_TIME) - callb(currentSeedBackup) - - user.get(Key.SEED_BACKUP).on(async seedBackup => { - if (typeof seedBackup === 'string') { - currentSeedBackup = await SEA.decrypt(seedBackup, mySecret) - callb(currentSeedBackup) - } - }) -} - -module.exports = { - __onSentRequestToUser, - __onUserToIncoming, - onAvatar, - onBlacklist, - onCurrentHandshakeAddress, - onDisplayName, - onIncomingMessages, - onOutgoing, - onChats, - onSimplerReceivedRequests, - onSimplerSentRequests, - onBio, - onSeedBackup -} diff --git a/services/gunDB/contact-api/events/index.js b/services/gunDB/contact-api/events/index.js new file mode 100644 index 00000000..6421d2ec --- /dev/null +++ b/services/gunDB/contact-api/events/index.js @@ -0,0 +1,535 @@ +/** + * @prettier + */ +const debounce = require('lodash/debounce') + +const ErrorCode = require('../errorCode') +const Key = require('../key') +const Schema = require('../schema') +const Streams = require('../streams') +const Utils = require('../utils') +/** + * @typedef {import('../SimpleGUN').UserGUNNode} UserGUNNode + * @typedef {import('../SimpleGUN').GUNNode} GUNNode + * @typedef {import('../SimpleGUN').ISEA} ISEA + * @typedef {import('../SimpleGUN').ListenerData} ListenerData + * @typedef {import('../schema').HandshakeRequest} HandshakeRequest + * @typedef {import('../schema').Message} Message + * @typedef {import('../schema').Outgoing} Outgoing + * @typedef {import('../schema').PartialOutgoing} PartialOutgoing + * @typedef {import('../schema').Chat} Chat + * @typedef {import('../schema').ChatMessage} ChatMessage + * @typedef {import('../schema').SimpleSentRequest} SimpleSentRequest + * @typedef {import('../schema').SimpleReceivedRequest} SimpleReceivedRequest + */ + +const DEBOUNCE_WAIT_TIME = 500 + +/** + * @param {(userToIncoming: Record) => void} cb + * @param {UserGUNNode} user Pass only for testing purposes. + * @param {ISEA} SEA + * @returns {void} + */ +const __onUserToIncoming = (cb, user, SEA) => { + if (!user.is) { + throw new Error(ErrorCode.NOT_AUTH) + } + + const callb = debounce(cb, DEBOUNCE_WAIT_TIME) + + /** @type {Record} */ + const userToIncoming = {} + + const mySecret = require('../../Mediator').getMySecret() + + user + .get(Key.USER_TO_INCOMING) + .map() + .on(async (encryptedIncomingID, userPub) => { + if (typeof encryptedIncomingID !== 'string') { + if (encryptedIncomingID === null) { + // on disconnect + delete userToIncoming[userPub] + } else { + console.error( + 'got a non string non null value inside user to incoming' + ) + } + return + } + + if (encryptedIncomingID.length === 0) { + console.error('got an empty string value') + return + } + + const incomingID = await SEA.decrypt(encryptedIncomingID, mySecret) + + if (typeof incomingID === 'undefined') { + console.warn('could not decrypt incomingID inside __onUserToIncoming') + return + } + + userToIncoming[userPub] = incomingID + + callb(userToIncoming) + }) +} + +/** + * @param {(avatar: string|null) => void} cb + * @param {UserGUNNode} user Pass only for testing purposes. + * @throws {Error} If user hasn't been auth. + * @returns {void} + */ +const onAvatar = (cb, user) => { + if (!user.is) { + throw new Error(ErrorCode.NOT_AUTH) + } + + const callb = debounce(cb, DEBOUNCE_WAIT_TIME) + // Initial value if avvatar is undefined in gun + callb(null) + + user + .get(Key.PROFILE) + .get(Key.AVATAR) + .on(avatar => { + if (typeof avatar === 'string' || avatar === null) { + callb(avatar) + } + }) +} + +/** + * @param {(blacklist: string[]) => void} cb + * @param {UserGUNNode} user + * @returns {void} + */ +const onBlacklist = (cb, user) => { + /** @type {string[]} */ + const blacklist = [] + + if (!user.is) { + throw new Error(ErrorCode.NOT_AUTH) + } + + const callb = debounce(cb, DEBOUNCE_WAIT_TIME) + + // Initial value if no items are in blacklist in gun + callb(blacklist) + + user + .get(Key.BLACKLIST) + .map() + .on(publicKey => { + if (typeof publicKey === 'string' && publicKey.length > 0) { + blacklist.push(publicKey) + callb(blacklist) + } else { + console.warn('Invalid public key received for blacklist') + } + }) +} + +/** + * @param {(currentHandshakeAddress: string|null) => void} cb + * @param {UserGUNNode} user + * @returns {void} + */ +const onCurrentHandshakeAddress = (cb, user) => { + if (!user.is) { + throw new Error(ErrorCode.NOT_AUTH) + } + + const callb = debounce(cb, DEBOUNCE_WAIT_TIME) + + // If undefined, callback below wont be called. Let's supply null as the + // initial value. + callb(null) + + user.get(Key.CURRENT_HANDSHAKE_ADDRESS).on(addr => { + if (typeof addr !== 'string') { + console.error('expected handshake address to be an string') + + callb(null) + + return + } + + callb(addr) + }) +} + +/** + * @param {(displayName: string|null) => void} cb + * @param {UserGUNNode} user Pass only for testing purposes. + * @throws {Error} If user hasn't been auth. + * @returns {void} + */ +const onDisplayName = (cb, user) => { + if (!user.is) { + throw new Error(ErrorCode.NOT_AUTH) + } + + const callb = debounce(cb, DEBOUNCE_WAIT_TIME) + + // Initial value if display name is undefined in gun + callb(null) + + user + .get(Key.PROFILE) + .get(Key.DISPLAY_NAME) + .on(displayName => { + if (typeof displayName === 'string' || displayName === null) { + callb(displayName) + } + }) +} + +/** + * @param {(messages: Record) => void} cb + * @param {string} userPK Public key of the user from whom the incoming + * messages will be obtained. + * @param {string} incomingFeedID ID of the outgoing feed from which the + * incoming messages will be obtained. + * @param {GUNNode} gun (Pass only for testing purposes) + * @param {UserGUNNode} user + * @param {ISEA} SEA + * @returns {void} + */ +const onIncomingMessages = (cb, userPK, incomingFeedID, gun, user, SEA) => { + if (!user.is) { + throw new Error(ErrorCode.NOT_AUTH) + } + + const callb = debounce(cb, DEBOUNCE_WAIT_TIME) + + const otherUser = gun.user(userPK) + + /** + * @type {Record} + */ + const messages = {} + + callb(messages) + + otherUser + .get(Key.OUTGOINGS) + .get(incomingFeedID) + .get(Key.MESSAGES) + .map() + .on(async (data, key) => { + if (!Schema.isMessage(data)) { + console.warn('non-message received') + return + } + + /** @type {string} */ + const recipientEpub = await Utils.pubToEpub(userPK) + + const secret = await SEA.secret(recipientEpub, user._.sea) + + let { body } = data + body = await SEA.decrypt(body, secret) + + messages[key] = { + body, + timestamp: data.timestamp + } + + callb(messages) + }) +} + +/** + * @typedef {Record} Outgoings + * @typedef {(outgoings: Outgoings) => void} OutgoingsListener + */ + +/** + * @type {Outgoings} + */ +let currentOutgoings = {} + +const getCurrentOutgoings = () => currentOutgoings + +/** @type {Set} */ +const outgoingsListeners = new Set() + +const notifyOutgoingsListeners = () => { + outgoingsListeners.forEach(l => l(currentOutgoings)) +} + +let outSubbed = false + +/** + * @param {OutgoingsListener} cb + * @returns {() => void} + */ +const onOutgoing = cb => { + outgoingsListeners.add(cb) + cb(currentOutgoings) + + if (!outSubbed) { + const user = require('../../Mediator').getUser() + user.get(Key.OUTGOINGS).open( + debounce(async data => { + try { + if (typeof data !== 'object' || data === null) { + currentOutgoings = {} + notifyOutgoingsListeners() + return + } + + /** @type {Record} */ + const newOuts = {} + + const SEA = require('../../Mediator').mySEA + const mySecret = await Utils.mySecret() + + await Utils.asyncForEach(Object.entries(data), async ([id, out]) => { + if (typeof out !== 'object') { + return + } + + if (out === null) { + newOuts[id] = null + return + } + + const { with: encPub, messages } = out + + if (typeof encPub !== 'string') { + return + } + + const pub = await SEA.decrypt(encPub, mySecret) + + if (!newOuts[id]) { + newOuts[id] = { + with: pub, + messages: {} + } + } + + const ourSec = await SEA.secret( + await Utils.pubToEpub(pub), + user._.sea + ) + + if (typeof messages === 'object' && messages !== null) { + await Utils.asyncForEach( + Object.entries(messages), + async ([mid, msg]) => { + if (typeof msg === 'object' && msg !== null) { + if ( + typeof msg.body === 'string' && + typeof msg.timestamp === 'number' + ) { + const newOut = newOuts[id] + if (!newOut) { + return + } + newOut.messages[mid] = { + body: await SEA.decrypt(msg.body, ourSec), + timestamp: msg.timestamp + } + } + } + } + ) + } + }) + + currentOutgoings = newOuts + notifyOutgoingsListeners() + } catch (e) { + console.log('--------------------------') + console.log('Events -> onOutgoing') + console.log(e) + console.log('--------------------------') + } + }, 400) + ) + + outSubbed = true + } + + return () => { + outgoingsListeners.delete(cb) + } +} +//////////////////////////////////////////////////////////////////////////////// +/** + * @typedef {(chats: Chat[]) => void} ChatsListener + */ + +/** @type {Chat[]} */ +let currentChats = [] + +/** @type {Set} */ +const chatsListeners = new Set() + +const notifyChatsListeners = () => { + chatsListeners.forEach(l => l(currentChats)) +} + +const processChats = debounce(() => { + const pubToAvatar = Streams.getPubToAvatar() + const pubToDn = Streams.getPubToDn() + const existingOutgoings = /** @type {[string, Outgoing][]} */ (Object.entries( + getCurrentOutgoings() + ).filter(([_, o]) => o !== null)) + const pubToFeed = Streams.getPubToFeed() + + /** @type {Chat[]} */ + const newChats = [] + + for (const [outID, out] of existingOutgoings) { + if (typeof pubToAvatar[out.with] === 'undefined') { + // eslint-disable-next-line no-empty-function + Streams.onAvatar(() => {}, out.with) + } + if (typeof pubToDn[out.with] === 'undefined') { + // eslint-disable-next-line no-empty-function + Streams.onDisplayName(() => {}, out.with) + } + + /** @type {ChatMessage[]} */ + let msgs = Object.entries(out.messages).map(([mid, m]) => ({ + id: mid, + outgoing: true, + body: m.body, + timestamp: m.timestamp + })) + + const incoming = pubToFeed[out.with] + + if (Array.isArray(incoming)) { + msgs = [...msgs, ...incoming] + } + + /** @type {Chat} */ + const chat = { + recipientPublicKey: out.with, + didDisconnect: pubToFeed[out.with] === 'disconnected', + id: out.with + outID, + messages: msgs, + recipientAvatar: pubToAvatar[out.with] || null, + recipientDisplayName: pubToDn[out.with] || null + } + + newChats.push(chat) + } + + currentChats = newChats.filter( + c => + Array.isArray(pubToFeed[c.recipientPublicKey]) || + pubToFeed[c.recipientPublicKey] === 'disconnected' + ) + + notifyChatsListeners() +}, 750) + +let onChatsSubbed = false + +/** + * Massages all of the more primitive data structures into a more manageable + * 'Chat' paradigm. + * @param {ChatsListener} cb + * @returns {() => void} + */ +const onChats = cb => { + if (!chatsListeners.add(cb)) { + throw new Error('Tried to subscribe twice') + } + cb(currentChats) + + if (!onChatsSubbed) { + onOutgoing(processChats) + Streams.onAvatar(processChats) + Streams.onDisplayName(processChats) + Streams.onPubToFeed(processChats) + + onChatsSubbed = true + } + + return () => { + if (!chatsListeners.delete(cb)) { + throw new Error('Tried to unsubscribe twice') + } + } +} + +/** @type {string|null} */ +let currentBio = null + +/** + * @param {(bio: string|null) => void} cb + * @param {UserGUNNode} user Pass only for testing purposes. + * @throws {Error} If user hasn't been auth. + * @returns {void} + */ +const onBio = (cb, user) => { + if (!user.is) { + throw new Error(ErrorCode.NOT_AUTH) + } + + const callb = debounce(cb, DEBOUNCE_WAIT_TIME) + // Initial value if avvatar is undefined in gun + callb(currentBio) + + user.get(Key.BIO).on(bio => { + if (typeof bio === 'string' || bio === null) { + currentBio = bio + callb(bio) + } + }) +} + +/** @type {string|null} */ +let currentSeedBackup = null + +/** + * @param {(seedBackup: string|null) => void} cb + * @param {UserGUNNode} user + * @param {ISEA} SEA + * @throws {Error} If user hasn't been auth. + * @returns {void} + */ +const onSeedBackup = (cb, user, SEA) => { + if (!user.is) { + throw new Error(ErrorCode.NOT_AUTH) + } + + const mySecret = require('../../Mediator').getMySecret() + + const callb = debounce(cb, DEBOUNCE_WAIT_TIME) + callb(currentSeedBackup) + + user.get(Key.SEED_BACKUP).on(async seedBackup => { + if (typeof seedBackup === 'string') { + currentSeedBackup = await SEA.decrypt(seedBackup, mySecret) + callb(currentSeedBackup) + } + }) +} + +module.exports = { + __onUserToIncoming, + onAvatar, + onBlacklist, + onCurrentHandshakeAddress, + onDisplayName, + onIncomingMessages, + onOutgoing, + getCurrentOutgoings, + onSimplerReceivedRequests: require('./onReceivedReqs').onReceivedReqs, + onSimplerSentRequests: require('./onSentReqs').onSentReqs, + getCurrentSentReqs: require('./onSentReqs').getCurrentSentReqs, + onBio, + onSeedBackup, + onChats +} diff --git a/services/gunDB/contact-api/events/onReceivedReqs.js b/services/gunDB/contact-api/events/onReceivedReqs.js new file mode 100644 index 00000000..3a174069 --- /dev/null +++ b/services/gunDB/contact-api/events/onReceivedReqs.js @@ -0,0 +1,147 @@ +/** @format */ +const debounce = require('lodash/debounce') + +const Key = require('../key') +const Schema = require('../schema') +const Streams = require('../streams') + +/** + * @typedef {Readonly} SimpleReceivedRequest + * @typedef {(reqs: ReadonlyArray) => void} Listener + */ + +/** @type {Set} */ +const listeners = new Set() + +/** @type {string|null} */ +let currentAddress = null + +/** @type {Record} */ +let currReceivedReqsMap = {} + +/** + * Unprocessed requests in current handshake node. + * @type {Record} + */ +let currAddressData = {} + +/** @returns {SimpleReceivedRequest[]} */ +const getReceivedReqs = () => Object.values(currReceivedReqsMap) +/** @param {Record} reqs */ +const setReceivedReqsMap = reqs => { + currReceivedReqsMap = reqs + listeners.forEach(l => l(getReceivedReqs())) +} + +listeners.add(() => { + console.log( + `new received reqs: ${JSON.stringify(getReceivedReqs(), null, 4)}` + ) +}) + +const react = debounce(() => { + /** @type {Record} */ + const newReceivedReqsMap = {} + + const pubToFeed = Streams.getPubToFeed() + const pubToAvatar = Streams.getPubToAvatar() + const pubToDn = Streams.getPubToDn() + + for (const [id, req] of Object.entries(currAddressData)) { + const inContact = Array.isArray(pubToFeed[req.from]) + + if (typeof pubToAvatar[req.from] === 'undefined') { + // eslint-disable-next-line no-empty-function + Streams.onAvatar(() => {}, req.from)() + } + if (typeof pubToDn[req.from] === 'undefined') { + // eslint-disable-next-line no-empty-function + Streams.onDisplayName(() => {}, req.from)() + } + + if (!inContact) { + newReceivedReqsMap[req.from] = { + id, + requestorAvatar: pubToAvatar[req.from] || null, + requestorDisplayName: pubToDn[req.from] || null, + requestorPK: req.from, + timestamp: req.timestamp + } + } + } + + setReceivedReqsMap(newReceivedReqsMap) +}, 750) + +/** + * @param {string} addr + * @returns {(data: import('../SimpleGUN').OpenListenerData) => void} + */ +const listenerForAddr = addr => data => { + // did invalidate + if (addr !== currentAddress) { + return + } + + if (typeof data !== 'object' || data === null) { + currAddressData = {} + } else { + for (const [id, req] of Object.entries(data)) { + // no need to update them just write them once + if (Schema.isHandshakeRequest(req) && !currAddressData[id]) { + currAddressData[id] = req + } + } + } + + console.log('data for address: ' + addr) + console.log(JSON.stringify(data, null, 4)) + + react() +} + +let subbed = false + +/** + * @param {Listener} cb + * @returns {() => void} + */ +const onReceivedReqs = cb => { + listeners.add(cb) + cb(getReceivedReqs()) + + if (!subbed) { + require('./index').onCurrentHandshakeAddress(addr => { + if (currentAddress === addr) { + return + } + + currentAddress = addr + currAddressData = {} + setReceivedReqsMap({}) + + if (typeof addr === 'string') { + require('../../Mediator') + .getGun() + .get(Key.HANDSHAKE_NODES) + .get(addr) + .open(listenerForAddr(addr)) + } + }, require('../../Mediator').getUser()) + + Streams.onAvatar(react) + Streams.onDisplayName(react) + Streams.onPubToFeed(react) + + subbed = true + } + + return () => { + listeners.delete(cb) + } +} + +module.exports = { + getReceivedReqs, + onReceivedReqs +} diff --git a/services/gunDB/contact-api/events/onSentReqs.js b/services/gunDB/contact-api/events/onSentReqs.js new file mode 100644 index 00000000..f30bb9d2 --- /dev/null +++ b/services/gunDB/contact-api/events/onSentReqs.js @@ -0,0 +1,124 @@ +/** @format */ +const debounce = require('lodash/debounce') + +const Streams = require('../streams') +/** + * @typedef {import('../SimpleGUN').UserGUNNode} UserGUNNode + * @typedef {import('../SimpleGUN').GUNNode} GUNNode + * @typedef {import('../SimpleGUN').ISEA} ISEA + * @typedef {import('../SimpleGUN').ListenerData} ListenerData + * @typedef {import('../schema').HandshakeRequest} HandshakeRequest + * @typedef {import('../schema').Message} Message + * @typedef {import('../schema').Outgoing} Outgoing + * @typedef {import('../schema').PartialOutgoing} PartialOutgoing + * @typedef {import('../schema').Chat} Chat + * @typedef {import('../schema').ChatMessage} ChatMessage + * @typedef {import('../schema').SimpleSentRequest} SimpleSentRequest + * @typedef {import('../schema').SimpleReceivedRequest} SimpleReceivedRequest + */ + +/** + * @typedef {(chats: SimpleSentRequest[]) => void} Listener + */ + +/** @type {Set} */ +const listeners = new Set() + +/** @type {SimpleSentRequest[]} */ +let currentReqs = [] + +listeners.add(() => { + console.log(`new sent reqs: ${JSON.stringify(currentReqs)}`) +}) + +const getCurrentSentReqs = () => currentReqs + +const react = debounce(() => { + /** @type {SimpleSentRequest[]} */ + const newReqs = [] + + const pubToHAddr = Streams.getAddresses() + const storedReqs = Streams.getStoredReqs() + const pubToLastSentReqID = Streams.getSentReqIDs() + const pubToFeed = Streams.getPubToFeed() + const pubToAvatar = Streams.getPubToAvatar() + const pubToDN = Streams.getPubToDn() + + console.log( + `pubToLastSentREqID: ${JSON.stringify(pubToLastSentReqID, null, 4)}` + ) + + for (const storedReq of storedReqs) { + const { handshakeAddress, recipientPub, sentReqID, timestamp } = storedReq + const currAddress = pubToHAddr[recipientPub] + + const lastReqID = pubToLastSentReqID[recipientPub] + const isStale = typeof lastReqID !== 'undefined' && lastReqID !== sentReqID + const isConnected = Array.isArray(pubToFeed[recipientPub]) + + if (isStale || isConnected) { + // eslint-disable-next-line no-continue + continue + } + + if (typeof currAddress === 'undefined') { + // eslint-disable-next-line no-empty-function + Streams.onAddresses(() => {}, recipientPub)() + } + if (typeof pubToAvatar[recipientPub] === 'undefined') { + // eslint-disable-next-line no-empty-function + Streams.onAvatar(() => {}, recipientPub)() + } + if (typeof pubToDN[recipientPub] === 'undefined') { + // eslint-disable-next-line no-empty-function + Streams.onDisplayName(() => {}, recipientPub)() + } + + newReqs.push({ + id: sentReqID, + recipientAvatar: pubToAvatar[recipientPub] || null, + recipientChangedRequestAddress: + typeof currAddress !== 'undefined' && handshakeAddress !== currAddress, + recipientDisplayName: pubToDN[recipientPub] || null, + recipientPublicKey: recipientPub, + timestamp + }) + } + + currentReqs = newReqs + + listeners.forEach(l => l(currentReqs)) +}, 750) + +let subbed = false + +/** + * Massages all of the more primitive data structures into a more manageable + * 'Chat' paradigm. + * @param {Listener} cb + * @returns {() => void} + */ +const onSentReqs = cb => { + listeners.add(cb) + cb(currentReqs) + + if (!subbed) { + Streams.onAddresses(react) + Streams.onStoredReqs(react) + Streams.onLastSentReqIDs(react) + Streams.onPubToFeed(react) + Streams.onAvatar(react) + Streams.onDisplayName(react) + + subbed = true + } + + return () => { + listeners.delete(cb) + } +} + +module.exports = { + onSentReqs, + getCurrentSentReqs +} diff --git a/services/gunDB/contact-api/getters.js b/services/gunDB/contact-api/getters.js index fa704e37..cc1104f7 100644 --- a/services/gunDB/contact-api/getters.js +++ b/services/gunDB/contact-api/getters.js @@ -13,4 +13,17 @@ exports.currentOrderAddress = async (pub) => { } return currAddr +} + +/** + * @param {string} pub + * @returns {Promise} + */ +exports.userToIncomingID = async (pub) => { + const incomingID = await require('../Mediator').getUser().get(Key.USER_TO_INCOMING).get(pub).then() + + if (typeof incomingID === 'string') return incomingID + + return null + } \ No newline at end of file diff --git a/services/gunDB/contact-api/jobs/onAcceptedRequests.js b/services/gunDB/contact-api/jobs/onAcceptedRequests.js index 34888ca9..b8d3b54c 100644 --- a/services/gunDB/contact-api/jobs/onAcceptedRequests.js +++ b/services/gunDB/contact-api/jobs/onAcceptedRequests.js @@ -16,14 +16,14 @@ const Utils = require('../utils') * @throws {Error} NOT_AUTH * @param {UserGUNNode} user * @param {ISEA} SEA - * @returns {Promise} + * @returns {void} */ -const onAcceptedRequests = async (user, SEA) => { +const onAcceptedRequests = (user, SEA) => { if (!user.is) { throw new Error(ErrorCode.NOT_AUTH) } - const mySecret = await SEA.secret(user._.sea.epub, user._.sea) + const mySecret = require('../../Mediator').getMySecret() if (typeof mySecret !== 'string') { console.log("Jobs.onAcceptedRequests() -> typeof mySecret !== 'string'") @@ -33,7 +33,7 @@ const onAcceptedRequests = async (user, SEA) => { user .get(Key.STORED_REQS) .map() - .once(async storedReq => { + .once(async (storedReq, id) => { try { if (!Schema.isStoredRequest(storedReq)) { throw new TypeError('Stored request not an StoredRequest') @@ -123,10 +123,31 @@ const onAcceptedRequests = async (user, SEA) => { mySecret ) - user - .get(Key.USER_TO_INCOMING) - .get(recipientPub) - .put(encryptedForMeIncomingID) + await new Promise((res, rej) => { + user + .get(Key.USER_TO_INCOMING) + .get(recipientPub) + .put(encryptedForMeIncomingID, ack => { + if (ack.err) { + rej(new Error(ack.err)) + } else { + res() + } + }) + }) + + await new Promise((res, rej) => { + user + .get(Key.STORED_REQS) + .get(id) + .put(null, ack => { + if (ack.err) { + rej(new Error(ack.err)) + } else { + res() + } + }) + }) // ensure this listeners gets called at least once res() diff --git a/services/gunDB/contact-api/key.js b/services/gunDB/contact-api/key.js index 0902a454..22f83dc6 100644 --- a/services/gunDB/contact-api/key.js +++ b/services/gunDB/contact-api/key.js @@ -12,7 +12,6 @@ exports.RECIPIENT_TO_OUTGOING = 'recipientToOutgoing' exports.USER_TO_INCOMING = 'userToIncoming' exports.STORED_REQS = 'storedReqs' -exports.REQUEST_TO_USER = 'requestToUser' exports.BLACKLIST = 'blacklist' diff --git a/services/gunDB/contact-api/schema.js b/services/gunDB/contact-api/schema.js index 9a7d57bf..d4281ee6 100644 --- a/services/gunDB/contact-api/schema.js +++ b/services/gunDB/contact-api/schema.js @@ -65,10 +65,13 @@ exports.isChatMessage = item => { * outgoing/incoming feed paradigm. It combines both the outgoing and incoming * messages into one data structure plus metada about the chat. * @typedef {object} Chat + * @prop {string} id Chats now have IDs because of disconnect. + * RecipientPublicKey will no longer be unique. * @prop {string|null} recipientAvatar Base64 encoded image. * @prop {string} recipientPublicKey A way to uniquely identify each chat. * @prop {ChatMessage[]} messages Sorted from most recent to least recent. * @prop {string|null} recipientDisplayName + * @prop {boolean} didDisconnect True if the recipient performed a disconnect. */ /** @@ -102,6 +105,14 @@ exports.isChat = item => { return false } + if (typeof obj.didDisconnect !== 'boolean') { + return false + } + + if (typeof obj.id !== 'string') { + return false + } + return obj.messages.every(msg => exports.isChatMessage(msg)) } @@ -135,6 +146,8 @@ exports.isStoredRequest = item => { const obj = /** @type {StoredRequest} */ (item) if (typeof obj.recipientPub !== 'string') return false if (typeof obj.handshakeAddress !== 'string') return false + if (typeof obj.handshakeAddress !== 'string') return false + if (typeof obj.timestamp !== 'number') return false return true } @@ -200,7 +213,6 @@ exports.isSimpleSentRequest = item => { * @prop {string|null} requestorAvatar * @prop {string|null} requestorDisplayName * @prop {string} requestorPK - * @prop {string} response * @prop {number} timestamp */ @@ -238,10 +250,6 @@ exports.isSimpleReceivedRequest = item => { return false } - if (typeof obj.response !== 'string') { - return false - } - if (typeof obj.timestamp !== 'number') { return false } diff --git a/services/gunDB/contact-api/streams/addresses.js b/services/gunDB/contact-api/streams/addresses.js new file mode 100644 index 00000000..1a599066 --- /dev/null +++ b/services/gunDB/contact-api/streams/addresses.js @@ -0,0 +1,53 @@ +/** @format */ +const Key = require('../key') +/** + * @typedef {Record} Addresses + */ + +/** @type {Addresses} */ +const pubToAddress = {} + +/** @type {Set<() => void>} */ +const listeners = new Set() + +listeners.add(() => { + console.log(`pubToAddress: ${JSON.stringify(pubToAddress, null, 4)}`) +}) + +const notify = () => listeners.forEach(l => l()) + +/** @type {Set} */ +const subbedPublicKeys = new Set() + +/** + * @param {() => void} cb + * @param {string=} pub + */ +const onAddresses = (cb, pub) => { + listeners.add(cb) + cb() + if (pub && subbedPublicKeys.add(pub)) { + require('../../Mediator') + .getGun() + .user(pub) + .get(Key.CURRENT_HANDSHAKE_ADDRESS) + .on(addr => { + if (typeof addr === 'string' || addr === null) { + pubToAddress[pub] = addr + } else { + pubToAddress[pub] = null + } + notify() + }) + } + return () => { + listeners.delete(cb) + } +} + +const getAddresses = () => pubToAddress + +module.exports = { + onAddresses, + getAddresses +} diff --git a/services/gunDB/contact-api/streams/index.js b/services/gunDB/contact-api/streams/index.js new file mode 100644 index 00000000..b1eb872f --- /dev/null +++ b/services/gunDB/contact-api/streams/index.js @@ -0,0 +1,191 @@ +/** @format */ +const Key = require('../key') +const Schema = require('../schema') +const Utils = require('../utils') +/** + * @typedef {Record} Avatars + * @typedef {(avatars: Avatars) => void} AvatarListener + */ + +/** @type {Avatars} */ +const pubToAvatar = {} + +const getPubToAvatar = () => pubToAvatar + +/** @type {Set} */ +const avatarListeners = new Set() + +const notifyAvatarListeners = () => { + avatarListeners.forEach(l => l(pubToAvatar)) +} + +/** @type {Set} */ +const pubsWithAvatarListeners = new Set() + +/** + * @param {AvatarListener} cb + * @param {string=} pub + */ +const onAvatar = (cb, pub) => { + avatarListeners.add(cb) + cb(pubToAvatar) + if (pub && pubsWithAvatarListeners.add(pub)) { + require('../../Mediator') + .getGun() + .user(pub) + .get(Key.PROFILE) + .get(Key.AVATAR) + .on(av => { + if (typeof av === 'string' || av === null) { + pubToAvatar[pub] = av || null + } else { + pubToAvatar[pub] = null + } + notifyAvatarListeners() + }) + } + return () => { + avatarListeners.delete(cb) + } +} + +/** + * @typedef {Record} DisplayNames + * @typedef {(avatars: Avatars) => void} DisplayNameListener + */ + +/** @type {DisplayNames} */ +const pubToDisplayName = {} + +const getPubToDn = () => pubToDisplayName + +/** @type {Set} */ +const displayNameListeners = new Set() + +const notifyDisplayNameListeners = () => { + displayNameListeners.forEach(l => l(pubToDisplayName)) +} + +/** @type {Set} */ +const pubsWithDisplayNameListeners = new Set() + +/** + * @param {DisplayNameListener} cb + * @param {string=} pub + */ +const onDisplayName = (cb, pub) => { + displayNameListeners.add(cb) + cb(pubToDisplayName) + if (pub && pubsWithDisplayNameListeners.add(pub)) { + require('../../Mediator') + .getGun() + .user(pub) + .get(Key.PROFILE) + .get(Key.DISPLAY_NAME) + .on(dn => { + if (typeof dn === 'string' || dn === null) { + pubToDisplayName[pub] = dn || null + } else { + pubToDisplayName[pub] = null + } + notifyDisplayNameListeners() + }) + } + return () => { + displayNameListeners.delete(cb) + } +} + +/** + * @typedef {import('../schema').StoredRequest} StoredRequest + * @typedef {(reqs: StoredRequest[]) => void} StoredRequestsListener + */ + +/** @type {Set} */ +const storedRequestsListeners = new Set() + +/** + * @type {StoredRequest[]} + */ +let encryptedStoredReqs = [] + +/** + * @type {StoredRequest[]} + */ +let currentStoredReqs = [] + +const getStoredReqs = () => currentStoredReqs + +const processStoredReqs = async () => { + const ereqs = encryptedStoredReqs + encryptedStoredReqs = [] + const mySecret = await Utils.mySecret() + const SEA = require('../../Mediator').mySEA + const finalReqs = await Utils.asyncMap(ereqs, async er => { + /** @type {StoredRequest} */ + const r = { + handshakeAddress: await SEA.decrypt(er.handshakeAddress, mySecret), + recipientPub: await SEA.decrypt(er.recipientPub, mySecret), + sentReqID: await SEA.decrypt(er.sentReqID, mySecret), + timestamp: er.timestamp + } + + return r + }) + currentStoredReqs = finalReqs + storedRequestsListeners.forEach(l => l(currentStoredReqs)) +} + +let storedReqsSubbed = false + +/** + * @param {StoredRequestsListener} cb + */ +const onStoredReqs = cb => { + storedRequestsListeners.add(cb) + + if (!storedReqsSubbed) { + require('../../Mediator') + .getUser() + .get(Key.STORED_REQS) + .open(d => { + if (typeof d === 'object' && d !== null) { + encryptedStoredReqs = /** @type {StoredRequest[]} */ (Object.values( + d + ).filter(i => Schema.isStoredRequest(i))) + } + + processStoredReqs() + }) + + storedReqsSubbed = true + } + + cb(currentStoredReqs) + + return () => { + storedRequestsListeners.delete(cb) + } +} + +module.exports = { + onAvatar, + getPubToAvatar, + onDisplayName, + getPubToDn, + + onPubToIncoming: require('./pubToIncoming').onPubToIncoming, + getPubToIncoming: require('./pubToIncoming').getPubToIncoming, + setPubToIncoming: require('./pubToIncoming').setPubToIncoming, + + onPubToFeed: require('./pubToFeed').onPubToFeed, + getPubToFeed: require('./pubToFeed').getPubToFeed, + + onStoredReqs, + getStoredReqs, + onAddresses: require('./addresses').onAddresses, + getAddresses: require('./addresses').getAddresses, + onLastSentReqIDs: require('./lastSentReqID').onLastSentReqIDs, + getSentReqIDs: require('./lastSentReqID').getSentReqIDs, + PubToIncoming: require('./pubToIncoming') +} diff --git a/services/gunDB/contact-api/streams/lastSentReqID.js b/services/gunDB/contact-api/streams/lastSentReqID.js new file mode 100644 index 00000000..0c552922 --- /dev/null +++ b/services/gunDB/contact-api/streams/lastSentReqID.js @@ -0,0 +1,50 @@ +/** @format */ +const Key = require('../key') + +/** @type {Record} */ +let pubToLastSentReqID = {} + +/** @type {Set<() => void>} */ +const listeners = new Set() +const notify = () => listeners.forEach(l => l()) + +let subbed = false + +/** + * @param {() => void} cb + */ +const onLastSentReqIDs = cb => { + listeners.add(cb) + cb() + + if (!subbed) { + require('../../Mediator') + .getUser() + .get(Key.USER_TO_LAST_REQUEST_SENT) + .open(data => { + if (typeof data === 'object' && data !== null) { + for (const [pub, id] of Object.entries(data)) { + if (typeof id === 'string' || id === null) { + pubToLastSentReqID[pub] = id + } + } + } else { + pubToLastSentReqID = {} + } + + notify() + }) + subbed = true + } + + return () => { + listeners.delete(cb) + } +} + +const getSentReqIDs = () => pubToLastSentReqID + +module.exports = { + onLastSentReqIDs, + getSentReqIDs +} diff --git a/services/gunDB/contact-api/streams/pubToFeed.js b/services/gunDB/contact-api/streams/pubToFeed.js new file mode 100644 index 00000000..d8b5a933 --- /dev/null +++ b/services/gunDB/contact-api/streams/pubToFeed.js @@ -0,0 +1,251 @@ +/** @format */ +const uuidv1 = require('uuid/v1') +const logger = require('winston') +const debounce = require('lodash/debounce') + +const Schema = require('../schema') +const Key = require('../key') +const Utils = require('../utils') +/** + * @typedef {import('../schema').ChatMessage} Message + * @typedef {import('../SimpleGUN').OpenListenerData} OpenListenerData + */ + +const PubToIncoming = require('./pubToIncoming') + +/** + * @typedef {Record} Feeds + * @typedef {(feeds: Feeds) => void} FeedsListener + */ + +/** @type {Set} */ +const feedsListeners = new Set() + +/** + * @type {Feeds} + */ +let pubToFeed = {} + +const getPubToFeed = () => pubToFeed + +feedsListeners.add(() => { + console.log(`new pubToFeed: ${JSON.stringify(getPubToFeed())}`) +}) + +/** @param {Feeds} ptf */ +const setPubToFeed = ptf => { + pubToFeed = ptf + feedsListeners.forEach(l => { + l(pubToFeed) + }) +} + +/** + * If at one point we subscribed to a feed, record it here. Keeps track of it + * for unsubbing. + * + * Since we can't really unsub in GUN, what we do is that each listener created + * checks the last incoming feed, if it was created for other feed that is not + * the latest, it becomes inactive. + * @type {Record} + */ +const pubToLastIncoming = {} + +/** + * Any pub-feed pair listener will write its update id here when fired up. Avoid + * race conditions between different listeners and between different invocations + * of the same listener. + * @type {Record} + */ +const pubToLastUpdate = {} + +/** + * Performs a sub to a pub feed pair that will only emit if it is the last + * subbed feed for that pub, according to `pubToLastIncoming`. This listener is + * not in charge of writing to the cache. + * @param {[ string , string ]} param0 + * @returns {(data: OpenListenerData) => void} + */ +const onOpenForPubFeedPair = ([pub, feed]) => + debounce(async data => { + // did invalidate + if (pubToLastIncoming[pub] !== feed) { + return + } + + if ( + // did disconnect + data === null || + // interpret as disconnect + typeof data !== 'object' + ) { + // invalidate this listener. If a reconnection happens it will be for a + // different pub-feed pair. + pubToLastIncoming[pub] = null + setImmediate(() => { + logger.info( + `onOpenForPubFeedPair -> didDisconnect -> pub: ${pub} - feed: ${feed}` + ) + }) + // signal disconnect to listeners listeners should rely on pubToFeed for + // disconnect status instead of pub-to-incoming. Only the latter will + // detect remote disconnection + setPubToFeed({ + ...getPubToFeed(), + [pub]: /** @type {'disconnected'} */ ('disconnected') + }) + return + } + + const incoming = /** @type {Schema.Outgoing} */ (data) + + // incomplete data, let's not assume anything + if ( + typeof incoming.with !== 'string' || + typeof incoming.messages !== 'object' + ) { + return + } + + /** @type {Schema.ChatMessage[]} */ + const newMsgs = Object.entries(incoming.messages) + // filter out messages with incomplete data + .filter(([_, msg]) => Schema.isMessage(msg)) + .map(([id, msg]) => { + /** @type {Schema.ChatMessage} */ + const m = { + // we'll decrypt later + body: msg.body, + id, + outgoing: false, + timestamp: msg.timestamp + } + + return m + }) + + if (newMsgs.length === 0) { + setPubToFeed({ + ...getPubToFeed(), + [pub]: [] + }) + return + } + + const thisUpdate = uuidv1() + pubToLastUpdate[pub] = thisUpdate + + const user = require('../../Mediator').getUser() + const SEA = require('../../Mediator').mySEA + + const ourSecret = await SEA.secret(await Utils.pubToEpub(pub), user._.sea) + + const decryptedMsgs = await Utils.asyncMap(newMsgs, async m => { + /** @type {Schema.ChatMessage} */ + const decryptedMsg = { + ...m, + body: await SEA.decrypt(m.body, ourSecret) + } + + return decryptedMsg + }) + + // this listener got invalidated while we were awaiting the async operations + // above. + if (pubToLastUpdate[pub] !== thisUpdate) { + return + } + + setPubToFeed({ + ...getPubToFeed(), + [pub]: decryptedMsgs + }) + }, 750) + +const react = () => { + const pubToIncoming = PubToIncoming.getPubToIncoming() + + const gun = require('../../Mediator').getGun() + + /** @type {Feeds} */ + const newPubToFeed = {} + + for (const [pub, inc] of Object.entries(pubToIncoming)) { + /** + * empty string -> null + * @type {string|null} + */ + const newIncoming = inc || null + + if ( + // if disconnected, the same incoming feed will try to overwrite the + // nulled out pubToLastIncoming[pub] entry. Making the listener for that + // pub feed pair fire up again, etc. Now. When the user disconnects from + // this side of things. He will overwrite the pub to incoming with null. + // Let's allow that. + newIncoming === pubToLastIncoming[pub] && + !(pubToFeed[pub] === 'disconnected' && newIncoming === null) + ) { + // eslint-disable-next-line no-continue + continue + } + + // will invalidate stale listeners (a listener for an outdated incoming feed + // id) + pubToLastIncoming[pub] = newIncoming + // Invalidate pending writes from stale listener(s) for the old incoming + // address. + pubToLastUpdate[pub] = uuidv1() + newPubToFeed[pub] = newIncoming ? [] : null + + // sub to this incoming feed + if (typeof newIncoming === 'string') { + // perform sub to pub-incoming_feed pair + // leave all of the sideffects from this for the next tick + setImmediate(() => { + gun + .user(pub) + .get(Key.OUTGOINGS) + .get(newIncoming) + .open(onOpenForPubFeedPair([pub, newIncoming])) + }) + } + } + + if (Object.keys(newPubToFeed).length > 0) { + setPubToFeed({ + ...getPubToFeed(), + ...newPubToFeed + }) + } +} + +let subbed = false + +/** + * Array.isArray(pubToFeed[pub]) means a Handshake is in place, look for + * incoming messages here. + * pubToIncoming[pub] === null means a disconnection took place. + * typeof pubToIncoming[pub] === 'undefined' means none of the above. + * @param {FeedsListener} cb + * @returns {() => void} + */ +const onPubToFeed = cb => { + feedsListeners.add(cb) + cb(getPubToFeed()) + + if (!subbed) { + PubToIncoming.onPubToIncoming(react) + subbed = true + } + + return () => { + feedsListeners.delete(cb) + } +} + +module.exports = { + getPubToFeed, + setPubToFeed, + onPubToFeed +} diff --git a/services/gunDB/contact-api/streams/pubToIncoming.js b/services/gunDB/contact-api/streams/pubToIncoming.js new file mode 100644 index 00000000..ba2b156e --- /dev/null +++ b/services/gunDB/contact-api/streams/pubToIncoming.js @@ -0,0 +1,93 @@ +/** @format */ +const uuidv1 = require('uuid/v1') +const debounce = require('lodash/debounce') + +const { USER_TO_INCOMING } = require('../key') +const { asyncForEach } = require('../utils') +/** @typedef {import('../SimpleGUN').OpenListenerData} OpenListenerData */ + +/** + * @typedef {Record} PubToIncoming + */ + +/** @type {Set<() => void>} */ +const listeners = new Set() + +/** @type {PubToIncoming} */ +let pubToIncoming = {} + +const getPubToIncoming = () => pubToIncoming +/** + * @param {PubToIncoming} pti + * @returns {void} + */ +const setPubToIncoming = pti => { + pubToIncoming = pti + listeners.forEach(l => l()) +} + +let latestUpdate = uuidv1() + +const onOpen = debounce(async uti => { + const SEA = require('../../Mediator').mySEA + const mySec = require('../../Mediator').getMySecret() + const thisUpdate = uuidv1() + latestUpdate = thisUpdate + + if (typeof uti !== 'object' || uti === null) { + setPubToIncoming({}) + return + } + + /** @type {PubToIncoming} */ + const newPubToIncoming = {} + + await asyncForEach(Object.entries(uti), async ([pub, encFeedID]) => { + if (encFeedID === null) { + newPubToIncoming[pub] = null + return + } + + if (typeof encFeedID === 'string') { + newPubToIncoming[pub] = await SEA.decrypt(encFeedID, mySec) + } + }) + + // avoid old data from overwriting new data if decrypting took longer to + // process for the older open() call than for the newer open() call + if (latestUpdate === thisUpdate) { + setPubToIncoming(newPubToIncoming) + } +}, 750) + +let subbed = false + +/** + * @param {() => void} cb + * @returns {() => void} + */ +const onPubToIncoming = cb => { + if (!listeners.add(cb)) { + throw new Error('Tried to subscribe twice') + } + + cb() + + if (!subbed) { + const user = require('../../Mediator').getUser() + user.get(USER_TO_INCOMING).open(onOpen) + subbed = true + } + + return () => { + if (!listeners.delete(cb)) { + throw new Error('Tried to unsubscribe twice') + } + } +} + +module.exports = { + getPubToIncoming, + setPubToIncoming, + onPubToIncoming +} diff --git a/services/gunDB/contact-api/utils/PGUNNode.ts b/services/gunDB/contact-api/utils/PGUNNode.ts new file mode 100644 index 00000000..5c74b0e1 --- /dev/null +++ b/services/gunDB/contact-api/utils/PGUNNode.ts @@ -0,0 +1,8 @@ +/** @format */ +import { GUNNode, GUNNodeBase, ValidDataValue } from '../SimpleGUN' + +export interface PGUNNode extends GUNNodeBase { + get(key: string): PGUNNode + put(data: ValidDataValue | GUNNode): Promise + set(data: ValidDataValue | GUNNode): Promise +} diff --git a/services/gunDB/contact-api/utils.js b/services/gunDB/contact-api/utils/index.js similarity index 54% rename from services/gunDB/contact-api/utils.js rename to services/gunDB/contact-api/utils/index.js index d24173c0..1294f1c5 100644 --- a/services/gunDB/contact-api/utils.js +++ b/services/gunDB/contact-api/utils/index.js @@ -1,13 +1,13 @@ /** * @format */ -const ErrorCode = require('./errorCode') -const Key = require('./key') +const ErrorCode = require('../errorCode') +const Key = require('../key') /** - * @typedef {import('./SimpleGUN').GUNNode} GUNNode - * @typedef {import('./SimpleGUN').ISEA} ISEA - * @typedef {import('./SimpleGUN').UserGUNNode} UserGUNNode + * @typedef {import('../SimpleGUN').GUNNode} GUNNode + * @typedef {import('../SimpleGUN').ISEA} ISEA + * @typedef {import('../SimpleGUN').UserGUNNode} UserGUNNode */ /** @@ -16,6 +16,11 @@ const Key = require('./key') */ const delay = ms => new Promise(res => setTimeout(res, ms)) +/** + * @returns {Promise} + */ +const mySecret = () => Promise.resolve(require('../../Mediator').getMySecret()) + /** * @template T * @param {Promise} promise @@ -41,8 +46,8 @@ const timeout10 = promise => { const tryAndWait = promGen => timeout10( promGen( - require('../Mediator/index').getGun(), - require('../Mediator/index').getUser() + require('../../Mediator/index').getGun(), + require('../../Mediator/index').getUser() ) ) @@ -74,39 +79,11 @@ const pubToEpub = async pub => { } } -/** - * @param {string} reqID - * @param {ISEA} SEA - * @param {string} mySecret - * @returns {Promise} - */ -const reqToRecipientPub = async (reqID, SEA, mySecret) => { - const maybeEncryptedForMeRecipientPub = await tryAndWait(async (_, user) => { - const reqToUser = user.get(Key.REQUEST_TO_USER) - const data = await reqToUser.get(reqID).then() - - if (typeof data !== 'string') { - throw new TypeError("typeof maybeEncryptedForMeRecipientPub !== 'string'") - } - - return data - }) - - const encryptedForMeRecipientPub = maybeEncryptedForMeRecipientPub - - const recipientPub = await SEA.decrypt(encryptedForMeRecipientPub, mySecret) - - if (typeof recipientPub !== 'string') { - throw new TypeError("typeof recipientPub !== 'string'") - } - - return recipientPub -} - /** * Should only be called with a recipient pub that has already been contacted. + * If returns null, a disconnect happened. * @param {string} recipientPub - * @returns {Promise} + * @returns {Promise} */ const recipientPubToLastReqSentID = async recipientPub => { const lastReqSentID = await tryAndWait(async (_, user) => { @@ -114,7 +91,7 @@ const recipientPubToLastReqSentID = async recipientPub => { const data = await userToLastReqSent.get(recipientPub).then() if (typeof data !== 'string') { - throw new TypeError("typeof latestReqSentID !== 'string'") + return null } return data @@ -134,31 +111,33 @@ const successfulHandshakeAlreadyExists = async recipientPub => { return userToIncoming.get(recipientPub).then() }) - return typeof maybeIncomingID === 'string' + const maybeOutgoingID = await tryAndWait((_, user) => { + const recipientToOutgoing = user.get(Key.RECIPIENT_TO_OUTGOING) + + return recipientToOutgoing.get(recipientPub).then() + }) + + return ( + typeof maybeIncomingID === 'string' && typeof maybeOutgoingID === 'string' + ) } /** * @param {string} recipientPub - * @param {UserGUNNode} user - * @param {ISEA} SEA * @returns {Promise} */ -const recipientToOutgoingID = async (recipientPub, user, SEA) => { - const mySecret = await SEA.secret(user._.sea.epub, user._.sea) - - if (typeof mySecret !== 'string') { - throw new TypeError('could not get mySecret') - } - - const maybeEncryptedOutgoingID = await tryAndWait((_, user) => - user - .get(Key.RECIPIENT_TO_OUTGOING) - .get(recipientPub) - .then() - ) +const recipientToOutgoingID = async recipientPub => { + const maybeEncryptedOutgoingID = await require('../../Mediator/index') + .getUser() + .get(Key.RECIPIENT_TO_OUTGOING) + .get(recipientPub) + .then() if (typeof maybeEncryptedOutgoingID === 'string') { - const outgoingID = await SEA.decrypt(maybeEncryptedOutgoingID, mySecret) + const outgoingID = await require('../../Mediator/index').mySEA.decrypt( + maybeEncryptedOutgoingID, + await mySecret() + ) return outgoingID || null } @@ -166,52 +145,6 @@ const recipientToOutgoingID = async (recipientPub, user, SEA) => { return null } -/** - * @param {string} reqResponse - * @param {string} recipientPub - * @param {UserGUNNode} user - * @param {ISEA} SEA - * @returns {Promise} - */ -const reqWasAccepted = async (reqResponse, recipientPub, user, SEA) => { - try { - const recipientEpub = await pubToEpub(recipientPub) - const ourSecret = await SEA.secret(recipientEpub, user._.sea) - if (typeof ourSecret !== 'string') { - throw new TypeError('typeof ourSecret !== "string"') - } - - const decryptedResponse = await SEA.decrypt(reqResponse, ourSecret) - - if (typeof decryptedResponse !== 'string') { - throw new TypeError('typeof decryptedResponse !== "string"') - } - - const myFeedID = await recipientToOutgoingID(recipientPub, user, SEA) - - if (typeof myFeedID === 'string' && decryptedResponse === myFeedID) { - return false - } - - const recipientFeedID = decryptedResponse - - const maybeFeed = await tryAndWait(gun => - gun - .user(recipientPub) - .get(Key.OUTGOINGS) - .get(recipientFeedID) - .then() - ) - - const feedExistsOnRecipient = - typeof maybeFeed === 'object' && maybeFeed !== null - - return feedExistsOnRecipient - } catch (err) { - throw new Error(`reqWasAccepted() -> ${err.message}`) - } -} - /** * * @param {string} userPub @@ -228,6 +161,18 @@ const currHandshakeAddress = async userPub => { return typeof maybeAddr === 'string' ? maybeAddr : null } +/** + * @template T + * @param {T[]} arr + * @param {(item: T) => void} cb + * @returns {Promise} + */ +const asyncForEach = async (arr, cb) => { + const promises = arr.map(item => cb(item)) + + await Promise.all(promises) +} + /** * @template T * @template U @@ -266,8 +211,8 @@ const asyncFilter = async (arr, cb) => { } /** - * @param {import('./SimpleGUN').ListenerData} listenerData - * @returns {listenerData is import('./SimpleGUN').ListenerObj} + * @param {import('../SimpleGUN').ListenerData} listenerData + * @returns {listenerData is import('../SimpleGUN').ListenerObj} */ const dataHasSoul = listenerData => typeof listenerData === 'object' && listenerData !== null @@ -278,6 +223,22 @@ const dataHasSoul = listenerData => */ const defaultName = pub => 'anon' + pub.slice(0, 8) +/** + * @param {string} pub + * @param {string} incomingID + * @returns {Promise} + */ +const didDisconnect = async (pub, incomingID) => { + const feed = await require('../../Mediator/index') + .getGun() + .user(pub) + .get(Key.OUTGOINGS) + .get(incomingID) + .then() + + return feed === null +} + module.exports = { asyncMap, asyncFilter, @@ -285,11 +246,13 @@ module.exports = { defaultName, delay, pubToEpub, - reqToRecipientPub, recipientPubToLastReqSentID, successfulHandshakeAlreadyExists, recipientToOutgoingID, - reqWasAccepted, currHandshakeAddress, - tryAndWait + tryAndWait, + mySecret, + promisifyGunNode: require('./promisifygun'), + didDisconnect, + asyncForEach } diff --git a/services/gunDB/contact-api/utils/promisifygun.js b/services/gunDB/contact-api/utils/promisifygun.js new file mode 100644 index 00000000..cc045f9d --- /dev/null +++ b/services/gunDB/contact-api/utils/promisifygun.js @@ -0,0 +1,47 @@ +/** + * @format + * @typedef {import("../SimpleGUN").ValidDataValue} ValidDataValue + * @typedef {import('../SimpleGUN').GUNNode} GUNNode + * @typedef {import('./PGUNNode').PGUNNode} PGUNNode + */ + +/** + * @param {GUNNode} node + * @returns {PGUNNode} + */ +const promisify = node => { + const oldPut = node.put.bind(node) + const oldSet = node.set.bind(node) + const oldGet = node.get.bind(node) + + const _pnode = /** @type {unknown} */ (node) + const pnode = /** @type {PGUNNode} */ (_pnode) + + pnode.put = data => + new Promise((res, rej) => { + oldPut(data, ack => { + if (ack.err) { + rej(new Error(ack.err)) + } else { + res() + } + }) + }) + + pnode.set = data => + new Promise((res, rej) => { + oldSet(data, ack => { + if (ack.err) { + rej(new Error(ack.err)) + } else { + res() + } + }) + }) + + pnode.get = key => promisify(oldGet(key)) + + return pnode +} + +module.exports = promisify diff --git a/yarn.lock b/yarn.lock index 7abb87b8..349712f4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -385,24 +385,24 @@ asn1js "^2.0.22" tslib "^1.9.3" -"@peculiar/json-schema@^1.1.5": - version "1.1.5" - resolved "https://registry.yarnpkg.com/@peculiar/json-schema/-/json-schema-1.1.5.tgz#376e0e978d2bd7132487a5679ab375a34313e73f" - integrity sha512-y5XYA3pf9+c+YKVpWnPtQbNmlNCs2ehNHyMLJvq4K5Fjwc1N64YGy7MNecKW3uYLga+sqbGTQSUdOdlnaRRbpA== +"@peculiar/json-schema@^1.1.6": + version "1.1.9" + resolved "https://registry.yarnpkg.com/@peculiar/json-schema/-/json-schema-1.1.9.tgz#b746e046b787607a1b2804f64437fda2527b3e62" + integrity sha512-F2ST2y/IQPgY+1QMw1Q33sqJbGDCeO3lGqI69SL3Hgo0++7iHqprUB1QyxB/A7bN3tuM65MBxoM2JLbwh42lsQ== dependencies: - tslib "^1.9.3" + tslib "^1.10.0" -"@peculiar/webcrypto@^1.0.19": - version "1.0.19" - resolved "https://registry.yarnpkg.com/@peculiar/webcrypto/-/webcrypto-1.0.19.tgz#45dd199c57ee655f9efd0332aca6fd53801d89f7" - integrity sha512-+lF69A18LJBLp0/gJIQatCARLah6cTUmLwY0Cdab0zsk+Z53BcpjKQyyP4LIN8oW601ZXv28mWEQ4Cm7MllF6w== +"@peculiar/webcrypto@^1.0.22": + version "1.0.22" + resolved "https://registry.yarnpkg.com/@peculiar/webcrypto/-/webcrypto-1.0.22.tgz#9dae652fce6bacd9df15bc91965797cee33adf67" + integrity sha512-NP6H6ZGXUvJnQJCWzUgnRcQv+9nMCNwLUDhTwOxRUwPFvtHauMOl0oPTKUjbhInCMaE55gJqB4yc0YKbde6Exw== dependencies: "@peculiar/asn1-schema" "^1.0.3" - "@peculiar/json-schema" "^1.1.5" + "@peculiar/json-schema" "^1.1.6" asn1js "^2.0.26" - pvtsutils "^1.0.6" + pvtsutils "^1.0.9" tslib "^1.10.0" - webcrypto-core "^1.0.14" + webcrypto-core "^1.0.17" "@protobufjs/aspromise@^1.1.1", "@protobufjs/aspromise@^1.1.2": version "1.1.2" @@ -579,6 +579,13 @@ resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.3.tgz#bdfd69d61e464dcc81b25159c270d75a73c1a636" integrity sha512-Il2DtDVRGDcqjDtE+rF8iqg1CArehSK84HZJCT7AMITlyXRBpuPhqGLDQMowraqqu1coEaimg4ZOqggt6L6L+A== +"@types/jsonwebtoken@^8.3.7": + version "8.3.7" + resolved "https://registry.yarnpkg.com/@types/jsonwebtoken/-/jsonwebtoken-8.3.7.tgz#ab79ad55b9435834d24cca3112f42c08eedb1a54" + integrity sha512-B5SSifLkjB0ns7VXpOOtOUlynE78/hKcY8G8pOAhkLJZinwofIBYqz555nRj2W9iDWZqFhK5R+7NZDaRmKWAoQ== + dependencies: + "@types/node" "*" + "@types/lodash@^4.14.141": version "4.14.141" resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.141.tgz#d81f4d0c562abe28713406b571ffb27692a82ae6" @@ -604,11 +611,6 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-10.14.17.tgz#b96d4dd3e427382482848948041d3754d40fd5ce" integrity sha512-p/sGgiPaathCfOtqu2fx5Mu1bcjuP8ALFg4xpGgNkcin7LwRyzUKniEHBKdcE1RPsenq5JVPIpMTJSygLboygQ== -"@types/node@^10.14.17": - version "10.14.19" - resolved "https://registry.yarnpkg.com/@types/node/-/node-10.14.19.tgz#f52742c7834a815dedf66edfc8a51547e2a67342" - integrity sha512-j6Sqt38ssdMKutXBUuAcmWF8QtHW1Fwz/mz4Y+Wd9mzpBiVFirjpNQf363hG5itkG+yGaD+oiLyb50HxJ36l9Q== - "@types/normalize-package-data@^2.4.0": version "2.4.0" resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz#e486d0d97396d79beedd0a6e33f4534ff6b4973e" @@ -680,6 +682,22 @@ lodash.unescape "4.0.1" semver "5.5.0" +"@unimodules/core@*": + version "5.0.0" + resolved "https://registry.yarnpkg.com/@unimodules/core/-/core-5.0.0.tgz#e1e3ca3f91f3d27dbc93c6eebc03a40c711da755" + integrity sha512-PswccfzFIviX61Lm8h6/QyC94bWe+6cARwhzgzTCKa6aR6azmi4732ExhX4VxfQjJNHB0szYVXGXVEDsFkj+tQ== + dependencies: + compare-versions "^3.4.0" + +"@unimodules/react-native-adapter@*": + version "5.0.0" + resolved "https://registry.yarnpkg.com/@unimodules/react-native-adapter/-/react-native-adapter-5.0.0.tgz#af9835821a2bf38390b9f09f3231c0b7546ee510" + integrity sha512-qb5p5wUQoi3TRa/33aLLHSnS7sewV99oBxIo9gnzNI3VFzbOm3rsbTjOJNcR2hx0raUolTtnQT75VbgagVQx4w== + dependencies: + invariant "^2.2.4" + lodash "^4.5.0" + prop-types "^15.6.1" + abab@^2.0.0: version "2.0.2" resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.2.tgz#a2fba1b122c69a85caa02d10f9270c7219709a9d" @@ -873,7 +891,12 @@ ascli@~1: colour "~0.7.1" optjs "~3.2.2" -asn1@^0.2.4, asn1@~0.2.3: +asmcrypto.js@^0.22.0: + version "0.22.0" + resolved "https://registry.yarnpkg.com/asmcrypto.js/-/asmcrypto.js-0.22.0.tgz#38fc1440884d802c7bd37d1d23c2b26a5cd5d2d2" + integrity sha512-usgMoyXjMbx/ZPdzTSXExhMPur2FTdz/Vo5PVx2gIaBcdAAJNOFlsdgqveM8Cff7W0v+xrf9BwjOV26JSAF9qA== + +asn1@~0.2.3: version "0.2.4" resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.4.tgz#8d2475dfab553bb33e77b54e59e880bb8ce23136" integrity sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg== @@ -907,7 +930,7 @@ async-each@^1.0.1: resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.3.tgz#b727dbf87d7651602f06f4d4ac387f47d91b0cbf" integrity sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ== -async-limiter@^1.0.0, async-limiter@~1.0.0: +async-limiter@~1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.1.tgz#dd379e94f0db8310b08291f9d64c3209766617fd" integrity sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ== @@ -945,6 +968,20 @@ axios@0.19.0, axios@^0.19.0: follow-redirects "1.5.10" is-buffer "^2.0.2" +b64-lite@^1.3.1, b64-lite@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/b64-lite/-/b64-lite-1.4.0.tgz#e62442de11f1f21c60e38b74f111ac0242283d3d" + integrity sha512-aHe97M7DXt+dkpa8fHlCcm1CnskAHrJqEfMI0KN7dwqlzml/aUe1AGt6lk51HzrSfVD67xOso84sOpr+0wIe2w== + dependencies: + base-64 "^0.1.0" + +b64u-lite@^1.0.1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/b64u-lite/-/b64u-lite-1.1.0.tgz#a581b7df94cbd4bed7cbb19feae816654f0b1bf0" + integrity sha512-929qWGDVCRph7gQVTC6koHqQIpF4vtVaSbwLltFQo44B1bYUquALswZdBKFfrJCPEnsCOvWkJsPdQYZ/Ukhw8A== + dependencies: + b64-lite "^1.4.0" + babel-code-frame@^6.26.0: version "6.26.0" resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.26.0.tgz#63fd43f7dc1e3bb7ce35947db8fe369a3f58c74b" @@ -1088,6 +1125,11 @@ balanced-match@^1.0.0: resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c= +base-64@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/base-64/-/base-64-0.1.0.tgz#780a99c84e7d600260361511c4877613bf24f6bb" + integrity sha1-eAqZyE59YAJgNhURxId2E78k9rs= + base-x@^3.0.2: version "3.0.7" resolved "https://registry.yarnpkg.com/base-x/-/base-x-3.0.7.tgz#1c5a7fafe8f66b4114063e8da102799d4e7c408f" @@ -1100,6 +1142,11 @@ base64-arraybuffer@0.1.5: resolved "https://registry.yarnpkg.com/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz#73926771923b5a19747ad666aa5cd4bf9c6e9ce8" integrity sha1-c5JncZI7Whl0etZmqlzUv5xunOg= +base64-js@*, base64-js@^1.0.2, base64-js@^1.3.0: + version "1.3.1" + resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.3.1.tgz#58ece8cb75dd07e71ed08c736abc5fac4dbf8df1" + integrity sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g== + base64id@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/base64id/-/base64id-1.0.0.tgz#47688cb99bb6804f0e06d3e763b1c32e57d8e6b6" @@ -1275,6 +1322,14 @@ buffer-from@^1.0.0: resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef" integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A== +buffer@^5.4.3: + version "5.4.3" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.4.3.tgz#3fbc9c69eb713d323e3fc1a895eee0710c072115" + integrity sha512-zvj65TkFeIt3i6aj5bIvJDzjjQQGs4o/sNoezg1F1kYap9Nu2jcUdpwzRSJTHMMzG0H7bZkn4rNQpImhuxWX2A== + dependencies: + base64-js "^1.0.2" + ieee754 "^1.1.4" + bytebuffer@~5: version "5.0.1" resolved "https://registry.yarnpkg.com/bytebuffer/-/bytebuffer-5.0.1.tgz#582eea4b1a873b6d020a48d58df85f0bba6cfddd" @@ -1539,6 +1594,11 @@ commander@~2.20.0: resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.1.tgz#3863ce3ca92d0831dcf2a102f5fb4b5926afd0f9" integrity sha512-cCuLsMhJeWQ/ZpsFTbE765kvVfoeSddc4nU3up4fV+fDBcfUXnbITJ+JzhkdjzOqhURjZgujxaioam4RM9yGUg== +compare-versions@^3.4.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/compare-versions/-/compare-versions-3.6.0.tgz#1a5689913685e5a87637b8d3ffca75514ec41d62" + integrity sha512-W6Af2Iw1z4CB7q4uU4hv646dW9GQuBM+YpC0UvUCWSD8w90SJjp+ujJuXaEMtAXBtSqGfMPuFOVn4/+FlaqfBA== + component-bind@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/component-bind/-/component-bind-1.0.0.tgz#00c608ab7dcd93897c0009651b1d3a8e1e73bbd1" @@ -2273,6 +2333,13 @@ expect@^24.9.0: jest-message-util "^24.9.0" jest-regex-util "^24.9.0" +expo-random@*: + version "8.0.0" + resolved "https://registry.yarnpkg.com/expo-random/-/expo-random-8.0.0.tgz#bbcc7a189d29ae9b709b36a6cf84256c22cc16f6" + integrity sha512-ukDC3eGSEliBsnobX1bQRAwti9GE8ZEW53AHFf1fVy+JuAhhZm+M5HW7T0ptdbLjm46VpTDGIO4vq+qxeXAl7g== + dependencies: + base64-js "^1.3.0" + express-session@^1.15.1: version "1.16.2" resolved "https://registry.yarnpkg.com/express-session/-/express-session-1.16.2.tgz#59f36d7770e94872d19b163b6708a2d16aa6848c" @@ -2711,15 +2778,15 @@ grpc@^1.21.1: node-pre-gyp "^0.13.0" protobufjs "^5.0.3" -gun@^0.2019.1211: - version "0.2019.1211" - resolved "https://registry.yarnpkg.com/gun/-/gun-0.2019.1211.tgz#37aa58217f86256b5d6f126450c8860f7bca384e" - integrity sha512-wueemUJBtNfVcZDeXK7wYbth+eu7tNFo5T54gAbA3N0VAN3zoPNE12saeekzrkVeMiKVxG8/kiR35BhykQ+CJg== +"gun@git://github.com/amark/gun#c59e0e95f92779ce6bb3aab823d318bc16b20c33": + version "0.2020.116" + resolved "git://github.com/amark/gun#c59e0e95f92779ce6bb3aab823d318bc16b20c33" dependencies: - ws "~>7.1.0" + buffer "^5.4.3" + ws "^7.1.2" optionalDependencies: - "@peculiar/webcrypto" "^1.0.19" emailjs "^2.2.0" + isomorphic-webcrypto "^2.3.2" text-encoding "^0.7.0" handlebars@^4.1.2: @@ -2902,6 +2969,11 @@ iconv-lite@0.4.24, iconv-lite@^0.4.24, iconv-lite@^0.4.4: dependencies: safer-buffer ">= 2.1.2 < 3" +ieee754@^1.1.4: + version "1.1.13" + resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.13.tgz#ec168558e95aa181fd87d37f55c32bbcb6708b84" + integrity sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg== + ignore-by-default@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/ignore-by-default/-/ignore-by-default-1.0.1.tgz#48ca6d72f6c6a3af00a9ad4ae6876be3889e2b09" @@ -3297,6 +3369,24 @@ isobject@^3.0.0, isobject@^3.0.1: resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" integrity sha1-TkMekrEalzFjaqH5yNHMvP2reN8= +isomorphic-webcrypto@^2.3.2: + version "2.3.4" + resolved "https://registry.yarnpkg.com/isomorphic-webcrypto/-/isomorphic-webcrypto-2.3.4.tgz#6d18ee7f795ab2f5e9fd7a5b489abaf9ee2e6af5" + integrity sha512-SnOCsm0Vls8jeWP4c26ItHFajzfDlRPcKK4YRUv6jukYGzJwl2tKNwSIAiCh1INdoRttaKhJrLc3HBer1om4HA== + dependencies: + "@peculiar/webcrypto" "^1.0.22" + asmcrypto.js "^0.22.0" + b64-lite "^1.3.1" + b64u-lite "^1.0.1" + msrcrypto "^1.5.6" + str2buf "^1.3.0" + webcrypto-shim "^0.1.4" + optionalDependencies: + "@unimodules/core" "*" + "@unimodules/react-native-adapter" "*" + expo-random "*" + react-native-securerandom "^0.1.1" + isstream@0.1.x, isstream@~0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" @@ -4015,7 +4105,7 @@ lodash@=4.17.4: resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae" integrity sha1-eCA6TRwyiuHYbcpkYONptX9AVa4= -lodash@^4.17.11, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.4, lodash@^4.17.5: +lodash@^4.17.11, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.5.0: version "4.17.15" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548" integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A== @@ -4030,7 +4120,7 @@ long@~3: resolved "https://registry.yarnpkg.com/long/-/long-3.2.0.tgz#d821b7138ca1cb581c172990ef14db200b5c474b" integrity sha1-2CG3E4yhy1gcFymQ7xTbIAtcR0s= -loose-envify@^1.0.0: +loose-envify@^1.0.0, loose-envify@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== @@ -4232,6 +4322,11 @@ ms@^2.1.1: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== +msrcrypto@^1.5.6: + version "1.5.8" + resolved "https://registry.yarnpkg.com/msrcrypto/-/msrcrypto-1.5.8.tgz#be419be4945bf134d8af52e9d43be7fa261f4a1c" + integrity sha512-ujZ0TRuozHKKm6eGbKHfXef7f+esIhEckmThVnz7RNyiOJd7a6MXj2JGBoL9cnPDW+JMG16MoTUh5X+XXjI66Q== + mute-stream@0.0.8: version "0.0.8" resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.8.tgz#1630c42b2251ff81e2a283de96a5497ea92e5e0d" @@ -4341,13 +4436,6 @@ node-pre-gyp@^0.13.0: semver "^5.3.0" tar "^4" -node-rsa@^1.0.7: - version "1.0.7" - resolved "https://registry.yarnpkg.com/node-rsa/-/node-rsa-1.0.7.tgz#85b7a6d6fa8ee624be6402a6b41be49272d58055" - integrity sha512-idwRXma6scFufZmbaKkHpJoLL93yynRefP6yur13wZ5i9FR35ex451KCoF2OORDeJanyRVahmjjiwmUlCnTqJA== - dependencies: - asn1 "^0.2.4" - nodemon@^1.19.3: version "1.19.3" resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-1.19.3.tgz#db71b3e62aef2a8e1283a9fa00164237356102c0" @@ -4446,7 +4534,7 @@ oauth-sign@~0.9.0: resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455" integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ== -object-assign@^4, object-assign@^4.1.0: +object-assign@^4, object-assign@^4.1.0, object-assign@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= @@ -4887,6 +4975,15 @@ prompts@^2.0.1: kleur "^3.0.3" sisteransi "^1.0.3" +prop-types@^15.6.1: + version "15.7.2" + resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5" + integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ== + dependencies: + loose-envify "^1.4.0" + object-assign "^4.1.1" + react-is "^16.8.1" + protobufjs@^5.0.3: version "5.0.3" resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-5.0.3.tgz#e4dfe9fb67c90b2630d15868249bcc4961467a17" @@ -4957,12 +5054,11 @@ punycode@^2.1.0, punycode@^2.1.1: resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== -pvtsutils@^1.0.4, pvtsutils@^1.0.6: - version "1.0.6" - resolved "https://registry.yarnpkg.com/pvtsutils/-/pvtsutils-1.0.6.tgz#e3883fd77abdd4c124131f6a49f3914cd9f21290" - integrity sha512-0yNrOdJyLE7FZzmeEHTKanwBr5XbmDAd020cKa4ZiTYuGMBYBZmq7vHOhcOqhVllh6gghDBbaz1lnVdOqiB7cw== +pvtsutils@^1.0.9: + version "1.0.9" + resolved "https://registry.yarnpkg.com/pvtsutils/-/pvtsutils-1.0.9.tgz#0eb6106f27878ccaa55e7dfbf6bd2c75af461dee" + integrity sha512-/kDsuCKPqJuIzn37w6+iN+TiSrN+zrwPEd7FjT61oNbRvceGdsS94fMEWZ4/h6QZU5EZhBMiV+79IYedroP/Yw== dependencies: - "@types/node" "^10.14.17" tslib "^1.10.0" pvutils@latest: @@ -5015,11 +5111,23 @@ rc@^1.0.1, rc@^1.1.6, rc@^1.2.7: minimist "^1.2.0" strip-json-comments "~2.0.1" +react-is@^16.8.1: + version "16.12.0" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.12.0.tgz#2cc0fe0fba742d97fd527c42a13bec4eeb06241c" + integrity sha512-rPCkf/mWBtKc97aLL9/txD8DZdemK0vkA3JMLShjlJB3Pj3s+lpf1KaBzMfQrAmhMQB0n1cU/SUGgKKBCe837Q== + react-is@^16.8.4: version "16.10.1" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.10.1.tgz#0612786bf19df406502d935494f0450b40b8294f" integrity sha512-BXUMf9sIOPXXZWqr7+c5SeOKJykyVr2u0UDzEf4LNGc6taGkQe1A9DFD07umCIXz45RLr9oAAwZbAJ0Pkknfaw== +react-native-securerandom@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/react-native-securerandom/-/react-native-securerandom-0.1.1.tgz#f130623a412c338b0afadedbc204c5cbb8bf2070" + integrity sha1-8TBiOkEsM4sK+t7bwgTFy7i/IHA= + dependencies: + base64-js "*" + read-pkg-up@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-1.0.1.tgz#9d63c13276c065918d57f002a57f40a1b643fb02" @@ -5697,6 +5805,11 @@ stealthy-require@^1.1.1: resolved "https://registry.yarnpkg.com/stealthy-require/-/stealthy-require-1.1.1.tgz#35b09875b4ff49f26a777e509b3090a3226bf24b" integrity sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks= +str2buf@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/str2buf/-/str2buf-1.3.0.tgz#a4172afff4310e67235178e738a2dbb573abead0" + integrity sha512-xIBmHIUHYZDP4HyoXGHYNVmxlXLXDrtFHYT0eV6IOdEj3VO9ccaF1Ejl9Oq8iFjITllpT8FhaXb4KsNmw+3EuA== + string-length@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/string-length/-/string-length-2.0.0.tgz#d40dbb686a3ace960c1cffca562bf2c45f8363ed" @@ -6221,14 +6334,19 @@ walker@^1.0.7, walker@~1.0.5: dependencies: makeerror "1.0.x" -webcrypto-core@^1.0.14: - version "1.0.14" - resolved "https://registry.yarnpkg.com/webcrypto-core/-/webcrypto-core-1.0.14.tgz#c015088fbc9c235ebd8b35047a131c5ff58f7152" - integrity sha512-iGZQcH/o3Jv6mpvCbzan6uAcUcLTTnUCil6RVYakcNh5/QXIKRRC06EFxHru9lHgVKucZy3gG4OBiup0IsOr0g== +webcrypto-core@^1.0.17: + version "1.0.17" + resolved "https://registry.yarnpkg.com/webcrypto-core/-/webcrypto-core-1.0.17.tgz#a9354bc0b1ba6735e882f4137ede2c4366e6ad9b" + integrity sha512-7jxTLgtM+TahBPErx/Dd2XvxFDfWJrHxjVeTSvIa4LSgiYrmCPlC2INiAMAfb8MbtHiwJKKqF5sPS0AWNjBbXw== dependencies: - pvtsutils "^1.0.4" + pvtsutils "^1.0.9" tslib "^1.10.0" +webcrypto-shim@^0.1.4: + version "0.1.5" + resolved "https://registry.yarnpkg.com/webcrypto-shim/-/webcrypto-shim-0.1.5.tgz#13e34a010ccc544edecfe8a2642204502841bcf0" + integrity sha512-mE+E00gulvbLjHaAwl0kph60oOLQRsKyivEFgV9DMM/3Y05F1vZvGq12hAcNzHRnYxyEOABBT/XMtwGSg5xA7A== + webidl-conversions@^4.0.2: version "4.0.2" resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-4.0.2.tgz#a855980b1f0b6b359ba1d5d9fb39ae941faa63ad" @@ -6383,6 +6501,11 @@ ws@^5.2.0: dependencies: async-limiter "~1.0.0" +ws@^7.1.2: + version "7.2.1" + resolved "https://registry.yarnpkg.com/ws/-/ws-7.2.1.tgz#03ed52423cd744084b2cf42ed197c8b65a936b8e" + integrity sha512-sucePNSafamSKoOqoNfBd8V0StlkzJKL2ZAhGQinCfNQ+oacw+Pk7lcdAElecBF2VkLNZRiIb5Oi1Q5lVUVt2A== + ws@~6.1.0: version "6.1.4" resolved "https://registry.yarnpkg.com/ws/-/ws-6.1.4.tgz#5b5c8800afab925e94ccb29d153c8d02c1776ef9" @@ -6390,13 +6513,6 @@ ws@~6.1.0: dependencies: async-limiter "~1.0.0" -ws@~>7.1.0: - version "7.1.2" - resolved "https://registry.yarnpkg.com/ws/-/ws-7.1.2.tgz#c672d1629de8bb27a9699eb599be47aeeedd8f73" - integrity sha512-gftXq3XI81cJCgkUiAVixA0raD9IVmXqsylCrjRygw4+UOOGzPoxnQ6r/CnVL9i+mDncJo94tSkyrtuuQVBmrg== - dependencies: - async-limiter "^1.0.0" - xdg-basedir@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-3.0.0.tgz#496b2cc109eca8dbacfe2dc72b603c17c5870ad4"