index.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468
  1. "use strict";
  2. var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
  3. return new (P || (P = Promise))(function (resolve, reject) {
  4. function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
  5. function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
  6. function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); }
  7. step((generator = generator.apply(thisArg, _arguments || [])).next());
  8. });
  9. };
  10. var __importDefault = (this && this.__importDefault) || function (mod) {
  11. return (mod && mod.__esModule) ? mod : { "default": mod };
  12. };
  13. Object.defineProperty(exports, "__esModule", { value: true });
  14. const crypto = require("libp2p-crypto");
  15. const ws_1 = __importDefault(require("ws"));
  16. const contact_address_1 = require("./contact-address");
  17. const contact_book_1 = require("./contact-book");
  18. const contact_item_1 = require("./contact-item");
  19. const content_item_1 = require("./content-item");
  20. const util_1 = require("./util");
  21. class BankClient {
  22. constructor(urlBase, ipfsUrlBase, storage, webClient) {
  23. this.urlBase = urlBase;
  24. this.ipfsUrlBase = ipfsUrlBase;
  25. this.storage = storage;
  26. this.webClient = webClient;
  27. this.wsUrlBase = urlBase.replace(/^http/i, 'ws');
  28. }
  29. static parseBankLink(bankLink) {
  30. if (!bankLink.startsWith('bank:')) {
  31. throw new Error('address must start with bank:');
  32. }
  33. const deprefixed = bankLink.substring(5);
  34. let host;
  35. let address;
  36. let topic;
  37. if (deprefixed[0] === '/' && deprefixed[1] === '/') {
  38. [host, address, topic] = deprefixed.substring(2).split('/');
  39. }
  40. else {
  41. [address, topic] = deprefixed.split('/');
  42. }
  43. if (!address || !topic) {
  44. throw new Error('cannot parse address and topic');
  45. }
  46. return { host, address, topic };
  47. }
  48. getPub() {
  49. return new Promise((resolve, reject) => __awaiter(this, void 0, void 0, function* () {
  50. yield this.bootstrap();
  51. this.getPriv().id((idErr, pubHash) => {
  52. if (idErr) {
  53. return reject(idErr);
  54. }
  55. resolve(pubHash);
  56. });
  57. }));
  58. }
  59. bootstrap() {
  60. if (this.bootstrapResult) {
  61. return Promise.resolve(this.bootstrapResult);
  62. }
  63. if (this.bootstrapPromise) {
  64. return this.bootstrapPromise;
  65. }
  66. return this.bootstrapPromise = new Promise((resolve, reject) => {
  67. this.storage.get('notaprivatekey').then(privateKeyFromStorage => {
  68. if (privateKeyFromStorage == null) {
  69. console.log('no private key in storage. generating new');
  70. crypto.keys.generateKeyPair('RSA', 2048, (generateErr, privateKey) => {
  71. if (generateErr) {
  72. return reject(generateErr);
  73. }
  74. privateKey.export('password', (exportErr, exportResult) => {
  75. if (exportErr) {
  76. return reject(exportErr);
  77. }
  78. this.storage.set('notaprivatekey', exportResult).then(err => {
  79. // whatever
  80. }).catch(reject);
  81. this.privateKey = privateKey;
  82. resolve(true);
  83. });
  84. });
  85. }
  86. else {
  87. // console.log('importing privatekey');
  88. crypto.keys.import(privateKeyFromStorage, 'password', (err, importedPrivateKey) => {
  89. if (err) {
  90. return reject(err);
  91. }
  92. this.privateKey = importedPrivateKey;
  93. // console.log(this.getPublicKeyString());
  94. // console.log(privateKeyFromStorage);
  95. resolve(true);
  96. });
  97. }
  98. }).catch(reject);
  99. });
  100. }
  101. getNonce() {
  102. return __awaiter(this, void 0, void 0, function* () {
  103. const nonce = yield this.webClient.request({
  104. method: 'GET',
  105. url: this.urlBase + '/bank/nonce'
  106. });
  107. return Number(nonce);
  108. });
  109. }
  110. getBalance() {
  111. return __awaiter(this, void 0, void 0, function* () {
  112. const nonce = yield this.getNonce();
  113. const retrieveRequest = yield this.makePlaintextPayload(JSON.stringify({
  114. _date: new Date().toISOString(),
  115. _nonce: nonce
  116. }));
  117. const topicURL = this.urlBase + '/bank/getbalance';
  118. const postResponse = yield this.webClient.requestJSON({
  119. body: retrieveRequest,
  120. method: 'POST',
  121. url: topicURL
  122. });
  123. return postResponse.balance;
  124. });
  125. }
  126. upload(params) {
  127. return __awaiter(this, void 0, void 0, function* () {
  128. const url = this.urlBase + '/bank/upload';
  129. const formData = {};
  130. formData.creator = yield this.getPub();
  131. if (params.fileData) {
  132. formData.file = {
  133. value: params.fileData,
  134. options: {
  135. filename: params.fileName
  136. }
  137. };
  138. }
  139. if (params.thumbFileData) {
  140. formData.thumb = {
  141. value: params.thumbFileData,
  142. options: {
  143. filename: params.thumbFileName
  144. }
  145. };
  146. }
  147. if (params.links) {
  148. formData.links = JSON.stringify(params.links);
  149. }
  150. for (const attr of ['title', 'text', 'type', 'dmMe']) {
  151. if (params[attr] != null) {
  152. formData[attr] = params[attr];
  153. }
  154. }
  155. // console.log('formData', formData);
  156. const uploadResponse = yield this.webClient.requestJSON({
  157. formData,
  158. method: 'POST',
  159. url
  160. });
  161. // console.log('uploadResponse', uploadResponse);
  162. return uploadResponse.hash;
  163. });
  164. }
  165. uploadSlimJSON(item) {
  166. return __awaiter(this, void 0, void 0, function* () {
  167. const url = this.urlBase + '/bank/upload/slim';
  168. const uploadResponse = yield this.webClient.requestJSON({
  169. body: item,
  170. method: 'POST',
  171. url
  172. });
  173. // console.log('uploadResponse', uploadResponse);
  174. return uploadResponse.hash;
  175. });
  176. }
  177. uploadSlimText(item) {
  178. return __awaiter(this, void 0, void 0, function* () {
  179. const url = this.urlBase + '/bank/upload/slim';
  180. const uploadResponse = JSON.parse(yield this.webClient.request({
  181. body: item,
  182. headers: {
  183. 'content-type': 'text/plain'
  184. },
  185. method: 'POST',
  186. url
  187. }));
  188. // console.log('uploadResponse', uploadResponse);
  189. return uploadResponse.hash;
  190. });
  191. }
  192. appendPrivate(peerAddr, topic, hash, replaceHash, deleteHash, onlyHash) {
  193. return __awaiter(this, void 0, void 0, function* () {
  194. const nonce = yield this.getNonce();
  195. const payload = yield this.makePlaintextPayload(JSON.stringify({
  196. _date: new Date().toISOString(),
  197. _nonce: nonce,
  198. deleteHash,
  199. hash,
  200. onlyHash,
  201. replaceHash,
  202. }));
  203. const topicURL = this.urlBase + '/bank/private/' + encodeURIComponent(peerAddr) + '/' + encodeURIComponent(topic);
  204. const result = yield this.webClient.request({
  205. body: JSON.stringify(payload),
  206. headers: {
  207. 'content-type': 'application/json'
  208. },
  209. method: 'PUT',
  210. url: topicURL
  211. });
  212. console.log('appended to ', peerAddr, topic, hash, replaceHash, deleteHash, result);
  213. });
  214. }
  215. retrievePrivate(peerAddr, topic) {
  216. return __awaiter(this, void 0, void 0, function* () {
  217. const nonce = yield this.getNonce();
  218. const retrieveRequest = yield this.makePlaintextPayload(JSON.stringify({
  219. _date: new Date().toISOString(),
  220. _nonce: nonce
  221. }));
  222. const topicURL = this.urlBase + '/bank/private/' + encodeURIComponent(peerAddr) + '/' + encodeURIComponent(topic);
  223. const result = yield this.webClient.request({
  224. body: JSON.stringify(retrieveRequest),
  225. headers: {
  226. 'content-type': 'application/json'
  227. },
  228. method: 'POST',
  229. url: topicURL
  230. });
  231. return result;
  232. });
  233. }
  234. subscribePrivate(peerAddr, topic, connectCallback, messageCallback) {
  235. return __awaiter(this, void 0, void 0, function* () {
  236. yield this.connectWebsocket(peerAddr, topic, connectCallback, messageCallback);
  237. });
  238. }
  239. getOrCreateContact(peerId, addressType, addressValue, contactBook) {
  240. return __awaiter(this, void 0, void 0, function* () {
  241. if (contactBook == null) {
  242. console.log('warning: inefficient');
  243. contactBook = yield this.getContactBook(peerId);
  244. }
  245. const existing = contactBook.lookupByAddress(addressType, addressValue);
  246. if (existing != null) {
  247. return existing;
  248. }
  249. console.log('creating new contact', peerId, addressType, addressValue);
  250. return yield this.createContact(peerId, addressType, addressValue);
  251. });
  252. }
  253. createContact(peerId, addressType, addressValue) {
  254. return __awaiter(this, void 0, void 0, function* () {
  255. const contactId = util_1.uuid();
  256. const newItem = {
  257. addrs: [],
  258. id: contactId
  259. };
  260. if (addressType != null && addressValue != null) {
  261. newItem.addrs.push(new contact_address_1.ContactAddress(addressType, addressValue).toPrefixedString());
  262. }
  263. const newItemHash = yield this.uploadSlimJSON(newItem);
  264. yield this.appendPrivate(peerId, '📇', newItemHash);
  265. const contactBook2 = yield this.getContactBook(peerId);
  266. return (yield contactBook2.lookupById(contactId));
  267. });
  268. }
  269. getAllContacts(peerId) {
  270. return __awaiter(this, void 0, void 0, function* () {
  271. const contactList = yield this.retrievePrivate(peerId, '📇');
  272. const items = yield this.getItemsForCommaList(contactList);
  273. return items.map(data => new contact_item_1.ContactItem(data));
  274. });
  275. }
  276. getContactBook(peerId) {
  277. return __awaiter(this, void 0, void 0, function* () {
  278. if (peerId == null) {
  279. throw new Error('Missing peerId');
  280. }
  281. return new contact_book_1.ContactBook(yield this.getAllContacts(peerId));
  282. });
  283. }
  284. updateContact(peerId, contactId, newProperties) {
  285. return __awaiter(this, void 0, void 0, function* () {
  286. const contactBook = yield this.getContactBook(peerId);
  287. const existing = yield contactBook.lookupById(contactId);
  288. if (!existing) {
  289. throw new Error('missing contact with id ' + contactId);
  290. }
  291. const existingData = existing.getData();
  292. const newProps = util_1.mergeDeep({}, newProperties);
  293. delete newProps.id;
  294. const newItem = util_1.mergeDeep(existingData, newProps);
  295. delete newItem.hash;
  296. newItem.lastChanged = new Date().toISOString();
  297. const newItemHash = yield this.uploadSlimJSON(newItem);
  298. yield this.appendPrivate(peerId, '📇', newItemHash, existing.hash);
  299. const contactBook2 = yield this.getContactBook(peerId);
  300. return (yield contactBook2.lookupById(contactId));
  301. });
  302. }
  303. getContentItemByHash(hashInPlaylist) {
  304. return __awaiter(this, void 0, void 0, function* () {
  305. const hash = this.parseItemHash(hashInPlaylist).hash;
  306. const contentParams = (yield this.webClient.requestJSON({
  307. method: 'get',
  308. url: this.ipfsUrlBase + '/ipfs/' + hash + '/content.json'
  309. }));
  310. return new content_item_1.ContentItem(hashInPlaylist, hash, contentParams);
  311. });
  312. }
  313. getItemsForCommaList(commaList) {
  314. return __awaiter(this, void 0, void 0, function* () {
  315. const itemHashes = commaList.split(',').filter(x => x.trim() !== '');
  316. const items = yield Promise.all(itemHashes.map(itemId => {
  317. const itemHash = this.parseItemHash(itemId).hash;
  318. return this.webClient.requestJSON({
  319. method: 'get',
  320. url: this.ipfsUrlBase + '/ipfs/' + itemHash,
  321. });
  322. }));
  323. for (const item of items) {
  324. item.hash = itemHashes.shift();
  325. }
  326. return items;
  327. });
  328. }
  329. parseItemHash(itemHash) {
  330. let type = null;
  331. let timestamp = null;
  332. let hash = null;
  333. if (itemHash.startsWith('/ipfs/')) {
  334. itemHash = itemHash.substring(6);
  335. }
  336. const matched = itemHash.match(/^([0-9]*)_(..)_(.*)$/);
  337. if (matched) {
  338. timestamp = matched[1];
  339. type = matched[2];
  340. hash = matched[3];
  341. }
  342. if (!type) {
  343. type = 'CO';
  344. }
  345. if (!hash) {
  346. hash = itemHash;
  347. }
  348. return { type, timestamp, hash };
  349. }
  350. runAgent(address, topic, storage, itemProcessCallback) {
  351. return __awaiter(this, void 0, void 0, function* () {
  352. yield this.subscribePrivate(address, topic, () => {
  353. // console.log('websocket connected');
  354. }, () => __awaiter(this, void 0, void 0, function* () {
  355. yield agentUpdate();
  356. }));
  357. const agentUpdate = () => __awaiter(this, void 0, void 0, function* () {
  358. const agentConfig = (yield storage.get('config')) || {};
  359. const items = yield this.retrievePrivate(address, topic);
  360. const itemsList = items.split(',').filter((x) => x.trim() !== '');
  361. console.log('itemsList', itemsList);
  362. for (const itemId of itemsList) {
  363. const processed = agentConfig.processed || [];
  364. const failed = agentConfig.failed || [];
  365. if (processed.includes(itemId) || failed.includes(itemId)) {
  366. continue;
  367. }
  368. try {
  369. const item = yield this.getContentItemByHash(itemId);
  370. console.log('gotItem', item);
  371. yield itemProcessCallback(item);
  372. processed.push(itemId);
  373. agentConfig.processed = processed;
  374. yield storage.set('config', agentConfig);
  375. }
  376. catch (e) {
  377. console.error('error processing item', itemId, e);
  378. failed.push(itemId);
  379. agentConfig.failed = failed;
  380. yield storage.set('config', agentConfig);
  381. }
  382. }
  383. });
  384. yield agentUpdate();
  385. });
  386. }
  387. connectWebsocket(peerAddr, topic, connectCallback, messageCallback) {
  388. return new Promise((resolve, reject) => __awaiter(this, void 0, void 0, function* () {
  389. const nonce = yield this.getNonce();
  390. const retrieveRequest = yield this.makePlaintextPayload(JSON.stringify({
  391. _date: new Date().toISOString(),
  392. _nonce: nonce,
  393. addr: peerAddr,
  394. topic
  395. }));
  396. const jsonOutput = JSON.stringify(retrieveRequest);
  397. const base64ed = Buffer.from(jsonOutput).toString('base64');
  398. const encoded = encodeURIComponent(base64ed);
  399. const ws = new ws_1.default(this.wsUrlBase + '/bank/ws?arg=' + encoded);
  400. ws.on('open', () => {
  401. connectCallback();
  402. });
  403. ws.on('message', data => {
  404. messageCallback(data);
  405. });
  406. const reconnect = () => {
  407. // console.log('reconnect');
  408. try {
  409. ws.terminate();
  410. }
  411. finally {
  412. console.log('reconnecting in 5s');
  413. setTimeout(() => __awaiter(this, void 0, void 0, function* () {
  414. try {
  415. yield this.connectWebsocket(peerAddr, topic, connectCallback, messageCallback);
  416. }
  417. catch (e) {
  418. console.error('error reconnecting', e);
  419. }
  420. }), 5000);
  421. }
  422. };
  423. ws.on('error', err => {
  424. console.error('websocket error', err);
  425. });
  426. ws.on('close', err => {
  427. reconnect();
  428. });
  429. resolve();
  430. }));
  431. }
  432. getPriv() {
  433. if (!this.privateKey) {
  434. throw new Error('missing private key');
  435. }
  436. return this.privateKey;
  437. }
  438. makePlaintextPayload(message) {
  439. const messageBytes = Buffer.from(message, 'utf-8');
  440. return new Promise((resolve, reject) => __awaiter(this, void 0, void 0, function* () {
  441. yield this.bootstrap();
  442. this.privateKey.sign(messageBytes, (signErr, signatureBytes) => __awaiter(this, void 0, void 0, function* () {
  443. if (signErr) {
  444. reject(signErr);
  445. return;
  446. }
  447. const publicDERBytes = this.privateKey.public.bytes;
  448. this.privateKey.id((idErr, pubHash) => {
  449. if (idErr) {
  450. reject(idErr);
  451. return;
  452. }
  453. const result = {
  454. date: new Date().toISOString(),
  455. msg: util_1.encodeHex(messageBytes),
  456. pub: util_1.encodeHex(publicDERBytes),
  457. pubHash,
  458. sig: util_1.encodeHex(signatureBytes),
  459. };
  460. // console.log('result', result, signatureBytes);
  461. resolve(result);
  462. });
  463. }));
  464. }));
  465. }
  466. }
  467. exports.BankClient = BankClient;
  468. //# sourceMappingURL=index.js.map