index.js 18 KB

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