"use strict"; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const crypto = require("libp2p-crypto"); const ws_1 = __importDefault(require("ws")); const contact_address_1 = require("./contact-address"); const contact_book_1 = require("./contact-book"); const contact_item_1 = require("./contact-item"); const content_item_1 = require("./content-item"); const util_1 = require("./util"); class BankClient { constructor(urlBase, ipfsUrlBase, storage, webClient) { this.urlBase = urlBase; this.ipfsUrlBase = ipfsUrlBase; this.storage = storage; this.webClient = webClient; this.wsUrlBase = urlBase.replace(/^http/i, 'ws'); } static parseBankLink(bankLink) { if (!bankLink.startsWith('bank:')) { throw new Error('address must start with bank:'); } const deprefixed = bankLink.substring(5); let host; let address; let topic; if (deprefixed[0] === '/' && deprefixed[1] === '/') { [host, address, topic] = deprefixed.substring(2).split('/'); } else { [address, topic] = deprefixed.split('/'); } if (!address || !topic) { throw new Error('cannot parse address and topic'); } return { host, address, topic }; } getPub() { return new Promise((resolve, reject) => __awaiter(this, void 0, void 0, function* () { yield this.bootstrap(); this.getPriv().id((idErr, pubHash) => { if (idErr) { return reject(idErr); } resolve(pubHash); }); })); } bootstrap() { if (this.bootstrapResult) { return Promise.resolve(this.bootstrapResult); } if (this.bootstrapPromise) { return this.bootstrapPromise; } return this.bootstrapPromise = new Promise((resolve, reject) => { this.storage.get('notaprivatekey').then(privateKeyFromStorage => { if (privateKeyFromStorage == null) { console.log('no private key in storage. generating new'); crypto.keys.generateKeyPair('RSA', 2048, (generateErr, privateKey) => { if (generateErr) { return reject(generateErr); } privateKey.export('password', (exportErr, exportResult) => { if (exportErr) { return reject(exportErr); } this.storage.set('notaprivatekey', exportResult).then(err => { // whatever }).catch(reject); this.privateKey = privateKey; resolve(true); }); }); } else { // console.log('importing privatekey'); crypto.keys.import(privateKeyFromStorage, 'password', (err, importedPrivateKey) => { if (err) { return reject(err); } this.privateKey = importedPrivateKey; // console.log(this.getPublicKeyString()); // console.log(privateKeyFromStorage); resolve(true); }); } }).catch(reject); }); } getNonce() { return __awaiter(this, void 0, void 0, function* () { const nonce = yield this.webClient.request({ method: 'GET', url: this.urlBase + '/bank/nonce' }); return Number(nonce); }); } getBalance() { return __awaiter(this, void 0, void 0, function* () { const nonce = yield this.getNonce(); const retrieveRequest = yield this.makePlaintextPayload(JSON.stringify({ _date: new Date().toISOString(), _nonce: nonce })); const topicURL = this.urlBase + '/bank/getbalance'; const postResponse = yield this.webClient.requestJSON({ body: retrieveRequest, method: 'POST', url: topicURL }); return postResponse.balance; }); } upload(params) { return __awaiter(this, void 0, void 0, function* () { const url = this.urlBase + '/bank/upload'; const formData = {}; formData.creator = yield this.getPub(); if (params.fileData) { formData.file = { value: params.fileData, options: { filename: params.fileName } }; } if (params.thumbFileData) { formData.thumb = { value: params.thumbFileData, options: { filename: params.thumbFileName } }; } if (params.links) { formData.links = JSON.stringify(params.links); } for (const attr of ['title', 'text', 'type', 'dmMe']) { if (params[attr] != null) { formData[attr] = params[attr]; } } // console.log('formData', formData); const uploadResponse = yield this.webClient.requestJSON({ formData, method: 'POST', url }); // console.log('uploadResponse', uploadResponse); return uploadResponse.hash; }); } uploadSlimJSON(item) { return __awaiter(this, void 0, void 0, function* () { const url = this.urlBase + '/bank/upload/slim'; const uploadResponse = yield this.webClient.requestJSON({ body: item, method: 'POST', url }); // console.log('uploadResponse', uploadResponse); return uploadResponse.hash; }); } uploadSlimText(item) { return __awaiter(this, void 0, void 0, function* () { const url = this.urlBase + '/bank/upload/slim'; const uploadResponse = JSON.parse(yield this.webClient.request({ body: item, headers: { 'content-type': 'text/plain' }, method: 'POST', url })); // console.log('uploadResponse', uploadResponse); return uploadResponse.hash; }); } appendPrivate(peerAddr, topic, hash, replaceHash, deleteHash, onlyHash) { return __awaiter(this, void 0, void 0, function* () { const nonce = yield this.getNonce(); const payload = yield this.makePlaintextPayload(JSON.stringify({ _date: new Date().toISOString(), _nonce: nonce, deleteHash, hash, onlyHash, replaceHash, })); const topicURL = this.urlBase + '/bank/private/' + encodeURIComponent(peerAddr) + '/' + encodeURIComponent(topic); const result = yield this.webClient.request({ body: JSON.stringify(payload), headers: { 'content-type': 'application/json' }, method: 'PUT', url: topicURL }); console.log('appended to ', peerAddr, topic, hash, replaceHash, deleteHash, result); }); } retrievePrivate(peerAddr, topic) { return __awaiter(this, void 0, void 0, function* () { const nonce = yield this.getNonce(); const retrieveRequest = yield this.makePlaintextPayload(JSON.stringify({ _date: new Date().toISOString(), _nonce: nonce })); const topicURL = this.urlBase + '/bank/private/' + encodeURIComponent(peerAddr) + '/' + encodeURIComponent(topic); const result = yield this.webClient.request({ body: JSON.stringify(retrieveRequest), headers: { 'content-type': 'application/json' }, method: 'POST', url: topicURL }); return result; }); } subscribePrivate(peerAddr, topic, connectCallback, messageCallback) { return __awaiter(this, void 0, void 0, function* () { yield this.connectWebsocket(peerAddr, topic, connectCallback, messageCallback); }); } getOrCreateContact(peerId, addressType, addressValue, contactBook) { return __awaiter(this, void 0, void 0, function* () { if (contactBook == null) { console.log('warning: inefficient'); contactBook = yield this.getContactBook(peerId); } const existing = contactBook.lookupByAddress(addressType, addressValue); if (existing != null) { return existing; } console.log('creating new contact', peerId, addressType, addressValue); return yield this.createContact(peerId, addressType, addressValue); }); } createContact(peerId, addressType, addressValue) { return __awaiter(this, void 0, void 0, function* () { const contactId = util_1.uuid(); const newItem = { addrs: [], id: contactId }; if (addressType != null && addressValue != null) { newItem.addrs.push(new contact_address_1.ContactAddress(addressType, addressValue).toPrefixedString()); } const newItemHash = yield this.uploadSlimJSON(newItem); yield this.appendPrivate(peerId, '📇', newItemHash); const contactBook2 = yield this.getContactBook(peerId); return (yield contactBook2.lookupById(contactId)); }); } getAllContacts(peerId) { return __awaiter(this, void 0, void 0, function* () { const contactList = yield this.retrievePrivate(peerId, '📇'); const items = yield this.getItemsForCommaList(contactList); return items.map(data => new contact_item_1.ContactItem(data)); }); } getContactBook(peerId) { return __awaiter(this, void 0, void 0, function* () { if (peerId == null) { throw new Error('Missing peerId'); } return new contact_book_1.ContactBook(yield this.getAllContacts(peerId)); }); } updateContact(peerId, contactId, newProperties) { return __awaiter(this, void 0, void 0, function* () { const contactBook = yield this.getContactBook(peerId); const existing = yield contactBook.lookupById(contactId); if (!existing) { throw new Error('missing contact with id ' + contactId); } const existingData = existing.getData(); const newProps = util_1.mergeDeep({}, newProperties); delete newProps.id; const newItem = util_1.mergeDeep(existingData, newProps); delete newItem.hash; newItem.lastChanged = new Date().toISOString(); const newItemHash = yield this.uploadSlimJSON(newItem); yield this.appendPrivate(peerId, '📇', newItemHash, existing.hash); const contactBook2 = yield this.getContactBook(peerId); return (yield contactBook2.lookupById(contactId)); }); } getContentItemByHash(hashInPlaylist) { return __awaiter(this, void 0, void 0, function* () { const hash = this.parseItemHash(hashInPlaylist).hash; const contentParams = (yield this.webClient.requestJSON({ method: 'get', url: this.ipfsUrlBase + '/ipfs/' + hash + '/content.json' })); return new content_item_1.ContentItem(hashInPlaylist, hash, contentParams); }); } getItemsForCommaList(commaList) { return __awaiter(this, void 0, void 0, function* () { const itemHashes = commaList.split(',').filter(x => x.trim() !== ''); const items = yield Promise.all(itemHashes.map(itemId => { const itemHash = this.parseItemHash(itemId).hash; return this.webClient.requestJSON({ method: 'get', url: this.ipfsUrlBase + '/ipfs/' + itemHash, }); })); for (const item of items) { item.hash = itemHashes.shift(); } return items; }); } parseItemHash(itemHash) { let type = null; let timestamp = null; let hash = null; if (itemHash.startsWith('/ipfs/')) { itemHash = itemHash.substring(6); } const matched = itemHash.match(/^([0-9]*)_(..)_(.*)$/); if (matched) { timestamp = matched[1]; type = matched[2]; hash = matched[3]; } if (!type) { type = 'CO'; } if (!hash) { hash = itemHash; } return { type, timestamp, hash }; } runAgent(address, topic, storage, itemProcessCallback) { return __awaiter(this, void 0, void 0, function* () { yield this.subscribePrivate(address, topic, () => { // console.log('websocket connected'); }, () => __awaiter(this, void 0, void 0, function* () { yield agentUpdate(); })); const agentUpdate = () => __awaiter(this, void 0, void 0, function* () { const agentConfig = (yield storage.get('config')) || {}; const items = yield this.retrievePrivate(address, topic); const itemsList = items.split(',').filter((x) => x.trim() !== ''); console.log('itemsList', itemsList); for (const itemId of itemsList) { const processed = agentConfig.processed || []; const failed = agentConfig.failed || []; if (processed.includes(itemId) || failed.includes(itemId)) { continue; } try { const item = yield this.getContentItemByHash(itemId); console.log('gotItem', item); yield itemProcessCallback(item); processed.push(itemId); agentConfig.processed = processed; yield storage.set('config', agentConfig); } catch (e) { console.error('error processing item', itemId, e); failed.push(itemId); agentConfig.failed = failed; yield storage.set('config', agentConfig); } } }); yield agentUpdate(); }); } connectWebsocket(peerAddr, topic, connectCallback, messageCallback) { return new Promise((resolve, reject) => __awaiter(this, void 0, void 0, function* () { const nonce = yield this.getNonce(); const retrieveRequest = yield this.makePlaintextPayload(JSON.stringify({ _date: new Date().toISOString(), _nonce: nonce, addr: peerAddr, topic })); const jsonOutput = JSON.stringify(retrieveRequest); const base64ed = Buffer.from(jsonOutput).toString('base64'); const encoded = encodeURIComponent(base64ed); const ws = new ws_1.default(this.wsUrlBase + '/bank/ws?arg=' + encoded); ws.on('open', () => { connectCallback(); }); ws.on('message', data => { messageCallback(data); }); const reconnect = () => { // console.log('reconnect'); try { ws.terminate(); } finally { console.log('reconnecting in 5s'); setTimeout(() => __awaiter(this, void 0, void 0, function* () { try { yield this.connectWebsocket(peerAddr, topic, connectCallback, messageCallback); } catch (e) { console.error('error reconnecting', e); } }), 5000); } }; ws.on('error', err => { console.error('websocket error', err); }); ws.on('close', err => { reconnect(); }); resolve(); })); } getPriv() { if (!this.privateKey) { throw new Error('missing private key'); } return this.privateKey; } makePlaintextPayload(message) { const messageBytes = Buffer.from(message, 'utf-8'); return new Promise((resolve, reject) => __awaiter(this, void 0, void 0, function* () { yield this.bootstrap(); this.privateKey.sign(messageBytes, (signErr, signatureBytes) => __awaiter(this, void 0, void 0, function* () { if (signErr) { reject(signErr); return; } const publicDERBytes = this.privateKey.public.bytes; this.privateKey.id((idErr, pubHash) => { if (idErr) { reject(idErr); return; } const result = { date: new Date().toISOString(), msg: util_1.encodeHex(messageBytes), pub: util_1.encodeHex(publicDERBytes), pubHash, sig: util_1.encodeHex(signatureBytes), }; // console.log('result', result, signatureBytes); resolve(result); }); })); })); } } exports.BankClient = BankClient; //# sourceMappingURL=index.js.map