index.js 19 KB

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