├── .eslintignore ├── .tern-config ├── CODEOWNERS ├── src ├── modules │ ├── schemas │ │ ├── defaults │ │ │ ├── blob.json │ │ │ ├── strings.json │ │ │ ├── json.json │ │ │ ├── index.ts │ │ │ ├── cameraRoll.json │ │ │ ├── avatar.json │ │ │ └── media.json │ │ └── index.ts │ ├── file.ts │ ├── __test__ │ │ ├── feed.test.ts │ │ ├── __static__ │ │ │ ├── simple.ts │ │ │ ├── config.ts │ │ │ ├── contacts.ts │ │ │ ├── threads.ts │ │ │ ├── files.ts │ │ │ ├── responses.ts │ │ │ └── feed.ts │ │ ├── account.test.ts │ │ ├── utils.test.ts │ │ ├── schema-miller.test.ts │ │ ├── config.test.ts │ │ ├── profile.test.ts │ │ ├── tokens.test.ts │ │ ├── likes.test.ts │ │ ├── messages.test.ts │ │ ├── comments.test.ts │ │ ├── contacts.test.ts │ │ └── cafes.test.ts │ ├── notifications.ts │ ├── observe.ts │ ├── utils.ts │ ├── logs.ts │ ├── account.ts │ ├── likes.ts │ ├── mills.ts │ ├── config.ts │ ├── comments.ts │ ├── feed.ts │ ├── messages.ts │ ├── invites.ts │ ├── profile.ts │ ├── tokens.ts │ ├── cafes.ts │ ├── contacts.ts │ ├── snapshots.ts │ ├── files.ts │ ├── blocks.ts │ ├── ipfs.ts │ ├── threads.ts │ └── events.ts ├── core │ ├── isomorphic-form-data.d.ts │ └── api.ts ├── helpers │ ├── __test__ │ │ └── handlers.test.ts │ ├── schema-miller.ts │ └── handlers.ts ├── models │ ├── mobile_pb.ts │ ├── threads_service_pb.ts │ ├── query_pb.ts │ ├── index.ts │ ├── cafe_service_pb.ts │ ├── message_pb.ts │ ├── view_pb.ts │ └── model_pb.ts └── index.ts ├── docs ├── assets │ └── images │ │ ├── icons.png │ │ ├── icons@2x.png │ │ ├── widgets.png │ │ └── widgets@2x.png └── interfaces │ ├── cafenonce.html │ ├── cafestoreack.html │ ├── threadflag.html │ └── threadlike.html ├── .eslintrc.json ├── tsconfig.webpack.json ├── scripts └── contributors.js ├── jest.config.js ├── webpack.config.js ├── examples └── browser │ └── index.html ├── LICENSE ├── tsconfig.json ├── .gitignore ├── tslint.json ├── config └── DOCS_TEMPLATE.md ├── .circleci └── config.yml ├── package.json ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md └── README.md /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | docs 3 | examples 4 | jest.config.js -------------------------------------------------------------------------------- /.tern-config: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": 3 | { 4 | "node": {} 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @sanderpick @asutula @carsonfarmer @andrewxhill 2 | /docs/ @carsonfarmer 3 | -------------------------------------------------------------------------------- /src/modules/schemas/defaults/blob.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "blob", 3 | "pin": true, 4 | "mill": "/blob" 5 | } 6 | -------------------------------------------------------------------------------- /docs/assets/images/icons.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/textileio/js-http-client/HEAD/docs/assets/images/icons.png -------------------------------------------------------------------------------- /docs/assets/images/icons@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/textileio/js-http-client/HEAD/docs/assets/images/icons@2x.png -------------------------------------------------------------------------------- /docs/assets/images/widgets.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/textileio/js-http-client/HEAD/docs/assets/images/widgets.png -------------------------------------------------------------------------------- /docs/assets/images/widgets@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/textileio/js-http-client/HEAD/docs/assets/images/widgets@2x.png -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ "airbnb-base", "prettier" ], 3 | "plugins": ["prettier"], 4 | "rules": { 5 | "prettier/prettier": "error" 6 | }, 7 | "env": { 8 | "browser": true, 9 | "node": true 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /tsconfig.webpack.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig", 3 | "compilerOptions": { 4 | "sourceMap": true, 5 | "paths": { 6 | "../../core/*": ["src/core/*"], 7 | "../../models/*": ["src/models/*"], 8 | } 9 | }, 10 | "exclude": [ 11 | "**/__test__/*", 12 | "node_modules" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /src/core/isomorphic-form-data.d.ts: -------------------------------------------------------------------------------- 1 | // For the purposes of js-http-client development, this is the minimal 2 | // required interface to support FormData in Nodejs an browsers 3 | 4 | declare module 'isomorphic-form-data' { 5 | export default class FormData { 6 | append(key: string, value: File, filename?: string): void 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /scripts/contributors.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | const markdownMagic = require('markdown-magic') 4 | 5 | const config = { 6 | transforms: { 7 | CONTRIBUTORS: require('markdown-magic-github-contributors') 8 | } 9 | } 10 | 11 | const markdownPath = path.join(__dirname, '../README.md') 12 | markdownMagic(markdownPath, config) -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "roots": [ 3 | "/src" 4 | ], 5 | "transform": { 6 | "^.+\\.tsx?$": "ts-jest" 7 | }, 8 | "testRegex": "(/__tests__/.*|(\\.|/)(test|spec))\\.tsx?$", 9 | "moduleFileExtensions": [ 10 | "ts", 11 | "tsx", 12 | "js", 13 | "jsx", 14 | "json", 15 | "node" 16 | ], 17 | "testEnvironment": "node" 18 | } -------------------------------------------------------------------------------- /src/modules/schemas/defaults/strings.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "strings", 3 | "pin": true, 4 | "mill": "/json", 5 | "json_schema": { 6 | "$id": "https://textile.io/string.schema.json", 7 | "$schema": "http://json-schema.org/draft-07/schema#", 8 | "title": "Strings", 9 | "description": "This is a schema that is restricted to raw strings.", 10 | "type": "strings" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/modules/schemas/defaults/json.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "json", 3 | "pin": true, 4 | "mill": "/json", 5 | "json_schema": { 6 | "$id": "https://textile.io/json.schema.json", 7 | "$schema": "http://json-schema.org/draft-07/schema#", 8 | "title": "JSON", 9 | "description": "This is a schema that matches anything.", 10 | "type": ["object", "array", "boolean", "null", "string", "numeric"] 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/modules/schemas/defaults/index.ts: -------------------------------------------------------------------------------- 1 | import avatar from './avatar.json' 2 | import blob from './blob.json' 3 | import cameraRoll from './cameraRoll.json' 4 | import json from './json.json' 5 | import media from './media.json' 6 | import strings from './strings.json' 7 | 8 | const defaults: {[key: string]: object} = { 9 | avatar, 10 | blob, 11 | cameraRoll, 12 | json, 13 | media, 14 | strings 15 | } 16 | 17 | export default defaults 18 | -------------------------------------------------------------------------------- /src/modules/schemas/defaults/cameraRoll.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "camera_roll", 3 | "pin": true, 4 | "links": { 5 | "raw": { 6 | "use": ":file", 7 | "mill": "/blob" 8 | }, 9 | "exif": { 10 | "use": "raw", 11 | "mill": "/image/exif" 12 | }, 13 | "thumb": { 14 | "use": "raw", 15 | "pin": true, 16 | "mill": "/image/resize", 17 | "opts": { 18 | "width": "320", 19 | "quality": "80" 20 | } 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/modules/schemas/defaults/avatar.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "avatar", 3 | "pin": true, 4 | "links": { 5 | "large": { 6 | "use": ":file", 7 | "pin": true, 8 | "plaintext": true, 9 | "mill": "/image/resize", 10 | "opts": { 11 | "width": "320", 12 | "quality": "75" 13 | } 14 | }, 15 | "small": { 16 | "use": ":file", 17 | "pin": true, 18 | "plaintext": true, 19 | "mill": "/image/resize", 20 | "opts": { 21 | "width": "100", 22 | "quality": "75" 23 | } 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/modules/schemas/defaults/media.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "media", 3 | "pin": true, 4 | "links": { 5 | "large": { 6 | "use": ":file", 7 | "mill": "/image/resize", 8 | "opts": { 9 | "width": "800", 10 | "quality": "80" 11 | } 12 | }, 13 | "small": { 14 | "use": ":file", 15 | "mill": "/image/resize", 16 | "opts": { 17 | "width": "320", 18 | "quality": "80" 19 | } 20 | }, 21 | "thumb": { 22 | "use": "large", 23 | "pin": true, 24 | "mill": "/image/resize", 25 | "opts": { 26 | "width": "100", 27 | "quality": "80" 28 | } 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | 3 | const extensions = [".tsx", ".ts", ".js", "json"]; 4 | 5 | module.exports = { 6 | entry: "./src/index.ts", 7 | devtool: "inline-source-map", 8 | module: { 9 | rules: [ 10 | { 11 | test: /\.ts?$/, 12 | include: path.resolve(__dirname, "src"), 13 | loader: "ts-loader?configFile=tsconfig.webpack.json" 14 | } 15 | ] 16 | }, 17 | resolve: { 18 | extensions 19 | }, 20 | output: { 21 | filename: "bundle.js", 22 | path: path.resolve(__dirname, "dist"), 23 | library: "textile", 24 | libraryTarget: "var" 25 | }, 26 | devServer: { 27 | filename: "bundle.js", 28 | publicPath: "/dist/" 29 | } 30 | }; 31 | -------------------------------------------------------------------------------- /examples/browser/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /src/modules/file.ts: -------------------------------------------------------------------------------- 1 | import { API } from '../core/api' 2 | 3 | /** 4 | * File is an API module for requesting information about a Textile File 5 | * 6 | * @extends {API} 7 | */ 8 | export default class File extends API { 9 | 10 | /** 11 | * 12 | * Get raw data for a File 13 | * 14 | * @param hash The hash for the requested file 15 | * @returns Raw data 16 | */ 17 | async content(hash: string) { 18 | const response = await this.sendGet(`file/${hash}/content`) 19 | // @todo: change this (back) to `response.arrayBuffer()`? 20 | return response.blob() 21 | } 22 | 23 | /** 24 | * 25 | * Get metadata for a File 26 | * 27 | * @param target The hash for the target file 28 | * @returns Metadata object 29 | */ 30 | async meta(target: string) { 31 | const response = await this.sendGet(`file/${target}/meta`) 32 | return response.json() 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/modules/__test__/feed.test.ts: -------------------------------------------------------------------------------- 1 | import nock from 'nock' 2 | import Feed from '../feed' 3 | import { ApiOptions } from '../../models' 4 | import { stacks, chrono } from './__static__/feed' 5 | 6 | const opts: ApiOptions = { 7 | url: 'http://127.0.0.1', 8 | port: 40600, 9 | version: 0 10 | } 11 | 12 | const ROOT = `${opts.url}:${opts.port}` 13 | const feed = new Feed(opts) 14 | 15 | describe('feed get stacked', () => { 16 | it('should resolve to a feed list object as stacks of items', async () => { 17 | nock(ROOT) 18 | .get('/api/v0/feed') 19 | .reply(200, stacks) 20 | 21 | expect(await feed.list(undefined, undefined, 5, 'stacks')).toEqual(stacks) 22 | }) 23 | }) 24 | 25 | describe('feed get chrono (default)', () => { 26 | it('should resolve to a feed list object in chronological order', async () => { 27 | nock(ROOT) 28 | .get('/api/v0/feed') 29 | .reply(200, chrono) 30 | 31 | expect(await feed.list()).toEqual(chrono) 32 | }) 33 | }) 34 | -------------------------------------------------------------------------------- /src/helpers/__test__/handlers.test.ts: -------------------------------------------------------------------------------- 1 | import { streamHandler } from '../handlers' 2 | import { ReadableStream } from 'web-streams-polyfill/ponyfill' 3 | import { TextEncoder } from 'util' 4 | 5 | describe('streamHandler', () => { 6 | const mockStream = new ReadableStream() 7 | const mockRead = jest.fn() 8 | 9 | mockStream.getReader = jest.fn().mockReturnValue({ 10 | read: mockRead 11 | }) 12 | const encoder = new TextEncoder() 13 | 14 | beforeEach(() => { 15 | jest.clearAllMocks() 16 | }) 17 | 18 | it('backward compatible with nodejs <= 11', async () => { 19 | // the test should pass in ci with older/new nodejs versions 20 | expect.assertions(1) 21 | const data = {data: 'can you see me?'} 22 | mockRead 23 | .mockResolvedValueOnce({ done: false, value: encoder.encode(JSON.stringify(data)) }) 24 | .mockResolvedValueOnce({ done: true }) 25 | const result = await streamHandler(mockStream).getReader().read() 26 | expect(result.value).toEqual(data) 27 | }) 28 | }) 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2019 Textile.io 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/modules/__test__/__static__/simple.ts: -------------------------------------------------------------------------------- 1 | export const schema = { 2 | name: 'media', 3 | pin: true, 4 | plaintext: false, 5 | mill: 'skip', 6 | opts: {}, 7 | json_schema: {}, 8 | links: { 9 | thumb: { 10 | use: 'large', 11 | pin: true, 12 | plaintext: false, 13 | mill: '/image/resize', 14 | opts: { 15 | quality: '80', 16 | width: '100' 17 | }, 18 | json_schema: {} 19 | }, 20 | large: { 21 | use: ':file', 22 | pin: false, 23 | plaintext: false, 24 | mill: '/image/resize', 25 | opts: { 26 | quality: '80', 27 | width: '800' 28 | }, 29 | json_schema: {} 30 | }, 31 | small: { 32 | use: ':file', 33 | pin: false, 34 | plaintext: false, 35 | mill: '/image/resize', 36 | opts: { 37 | quality: '80', 38 | width: '320' 39 | }, 40 | json_schema: {} 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/modules/notifications.ts: -------------------------------------------------------------------------------- 1 | import { API } from '../core/api' 2 | import { NotificationList } from '../models' 3 | 4 | /** 5 | * Notifications is an API module for managing notifications 6 | * 7 | * Notifications are generated by thread and account activity. 8 | * 9 | * @extends API 10 | */ 11 | export default class Notifications extends API { 12 | /** 13 | * Retrieves all notifications generated by thread and account activity 14 | * @returns An array of Notification objects 15 | */ 16 | async list() { 17 | const response = await this.sendGet('notifications') 18 | return response.json() as Promise 19 | } 20 | 21 | /** 22 | * Marks a notifiction as read by ID 23 | * 24 | * @param id ID of the target notification 25 | * @returns Whether the operation was successful 26 | */ 27 | async read(id: string) { 28 | const response = await this.sendPost(`notifications/${id}/read`) 29 | return response.status === 200 30 | } 31 | 32 | /** 33 | * Marks all notifictions as read 34 | * @returns Whether the operation was successful 35 | */ 36 | async readAll() { 37 | return this.read('all') 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "lib": ["es6", "dom", "esnext"], 5 | "module": "commonjs", 6 | "moduleResolution": "node", 7 | "declaration": true, 8 | "allowJs": false, 9 | "resolveJsonModule": true, 10 | "allowSyntheticDefaultImports": true, 11 | "esModuleInterop": true, 12 | "strict": true, 13 | "outDir": "dist", 14 | "baseUrl": ".", 15 | }, 16 | "include": [ 17 | "src" 18 | ], 19 | "exclude": [ 20 | "scripts", 21 | "node_modules", 22 | "examples", 23 | "docs", 24 | "dist", 25 | ], 26 | "compileOnSave": true, 27 | "typedocOptions": { 28 | "readme": "./config/DOCS_TEMPLATE.md", 29 | "mode": "file", 30 | "out": "docs", 31 | "include": "src/", 32 | "exclude": "**/__test__/**/*", 33 | "gitRevision": "master", 34 | "excludePrivate": true, 35 | "excludeNotExported": true, 36 | "excludeProtected": true, 37 | "hideGenerator": true, 38 | "target": "ES6", 39 | "moduleResolution": "node", 40 | "preserveConstEnums": true, 41 | "stripInternal": true, 42 | "module": "commonjs", 43 | "mdHideSources": true 44 | } 45 | } -------------------------------------------------------------------------------- /src/models/mobile_pb.ts: -------------------------------------------------------------------------------- 1 | /* GENERATED FROM mobile.proto. DO NOT EDIT MANUALLY. */ 2 | /* tslint:disabled */ 3 | /* eslint-disable */ 4 | 5 | import * as view from './view_pb' 6 | import * as query from './query_pb' 7 | import * as message from './message_pb' 8 | 9 | export interface MobileWalletAccount { 10 | seed: string 11 | address: string 12 | } 13 | 14 | export interface MobilePreparedFiles { 15 | dir: view.Directory 16 | pin: MobilePreparedFiles.Pin 17 | } 18 | 19 | export namespace MobilePreparedFiles { 20 | export interface Pin { 21 | [k: string]: string 22 | } 23 | } 24 | 25 | export interface MobileQueryEvent { 26 | id: string 27 | type: MobileQueryEvent.Type 28 | data: query.QueryResult 29 | error: message.Error 30 | } 31 | 32 | export namespace MobileQueryEvent { 33 | export enum Type { 34 | DATA = 'DATA', 35 | DONE = 'DONE', 36 | ERROR = 'ERROR' 37 | } 38 | } 39 | 40 | export enum MobileEventType { 41 | NODE_START = 'NODE_START', 42 | NODE_ONLINE = 'NODE_ONLINE', 43 | NODE_STOP = 'NODE_STOP', 44 | WALLET_UPDATE = 'WALLET_UPDATE', 45 | THREAD_UPDATE = 'THREAD_UPDATE', 46 | NOTIFICATION = 'NOTIFICATION', 47 | QUERY_RESPONSE = 'QUERY_RESPONSE' 48 | } 49 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Build files 2 | dist/ 3 | 4 | # Logs 5 | logs 6 | *.log 7 | npm-debug.log* 8 | yarn-debug.log* 9 | yarn-error.log* 10 | 11 | # Runtime data 12 | pids 13 | *.pid 14 | *.seed 15 | *.pid.lock 16 | 17 | # Directory for instrumented libs generated by jscoverage/JSCover 18 | lib-cov 19 | 20 | # Coverage directory used by tools like istanbul 21 | coverage 22 | 23 | # nyc test coverage 24 | .nyc_output 25 | 26 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 27 | .grunt 28 | 29 | # Bower dependency directory (https://bower.io/) 30 | bower_components 31 | 32 | # node-waf configuration 33 | .lock-wscript 34 | 35 | # Compiled binary addons (https://nodejs.org/api/addons.html) 36 | build/Release 37 | 38 | # Dependency directories 39 | node_modules/ 40 | jspm_packages/ 41 | 42 | # TypeScript v1 declaration files 43 | typings/ 44 | 45 | # Optional npm cache directory 46 | .npm 47 | 48 | # Optional eslint cache 49 | .eslintcache 50 | 51 | # Optional REPL history 52 | .node_repl_history 53 | 54 | # Output of 'npm pack' 55 | *.tgz 56 | 57 | # Yarn Integrity file 58 | .yarn-integrity 59 | 60 | # dotenv environment variables file 61 | .env 62 | 63 | # next.js build output 64 | .next 65 | 66 | # VSCode settings 67 | .vscode 68 | -------------------------------------------------------------------------------- /src/modules/__test__/account.test.ts: -------------------------------------------------------------------------------- 1 | import nock from 'nock' 2 | import Account from '../account' 3 | import { ApiOptions } from '../../models' 4 | import { account as response } from './__static__/responses' 5 | 6 | const opts: ApiOptions = { 7 | url: 'http://127.0.0.1', 8 | port: 40600, 9 | version: 0 10 | } 11 | 12 | const ROOT = `${opts.url}:${opts.port}` 13 | const account = new Account(opts) 14 | 15 | describe('account address', () => { 16 | it('should resolve to account address as string', async () => { 17 | nock(ROOT) 18 | .get('/api/v0/account/address') 19 | .reply(200, response.address) 20 | 21 | expect(await account.address()).toEqual(response.address) 22 | }) 23 | }) 24 | 25 | describe('account seed', () => { 26 | it('should resolve to account seed as string', async () => { 27 | nock(ROOT) 28 | .get('/api/v0/account/seed') 29 | .reply(200, response.seed) 30 | 31 | expect(await account.seed()).toEqual(response.seed) 32 | }) 33 | }) 34 | 35 | describe('account contact', () => { 36 | it('should resolve with local node contact info', async () => { 37 | nock(ROOT) 38 | .get('/api/v0/account') 39 | .reply(200, response.contact) 40 | 41 | expect(await account.contact()).toEqual(response.contact) 42 | }) 43 | }) 44 | -------------------------------------------------------------------------------- /src/modules/__test__/utils.test.ts: -------------------------------------------------------------------------------- 1 | import nock from 'nock' 2 | import Utils from '../utils' 3 | import URL from 'url-parse' 4 | import { ApiOptions } from '../../models' 5 | import { utils as response } from './__static__/responses' 6 | 7 | const opts: ApiOptions = { 8 | url: 'http://127.0.0.1', 9 | port: 40600, 10 | version: 0 11 | } 12 | 13 | const ROOT = `${opts.url}:${opts.port}` 14 | const utils = new Utils(opts) 15 | 16 | describe('node version', () => { 17 | it('should resolve to latest go-textile version', async () => { 18 | const versionString = '0.0.0' 19 | nock(ROOT) 20 | .get('/') 21 | .reply(200, { node_version: versionString }) 22 | 23 | expect((await utils.version()).node_version).toEqual(versionString) 24 | }) 25 | }) 26 | 27 | describe('node summary', () => { 28 | it('should resolve a full node summary object', async () => { 29 | nock(ROOT) 30 | .get('/api/v0/summary') 31 | .reply(200, response.summary) 32 | 33 | expect(await utils.summary()).toEqual(response.summary) 34 | }) 35 | }) 36 | 37 | describe('node online', () => { 38 | it('should resolve to a boolean (true)', async () => { 39 | nock(ROOT) 40 | .get('/health') 41 | .reply(204) 42 | 43 | expect(await utils.online()).toEqual(true) 44 | }) 45 | }) 46 | -------------------------------------------------------------------------------- /src/modules/__test__/schema-miller.test.ts: -------------------------------------------------------------------------------- 1 | import SchemaMiller from '../../helpers/schema-miller' 2 | // import { Directory } from '../../models' 3 | import { schema } from './__static__/simple' 4 | 5 | const hash = 'QMABCDDDKLSFDKLSDFLKSDF34523LKSSDDLFK23LK43K46' 6 | 7 | describe('simple schema', () => { 8 | it('should be sorted by dependencies', () => { 9 | const node = schema 10 | const sorted = SchemaMiller.sortLinksByDependency(node.links) 11 | 12 | expect(sorted[2].name).toEqual('thumb') 13 | }) 14 | 15 | it.skip('should have normalized options', () => { 16 | const { links: { thumb } } = schema 17 | const node = thumb 18 | const normalized = SchemaMiller.normalizeOptions(node) 19 | 20 | expect(normalized.pin).toEqual(true) 21 | expect(normalized.plaintext).toEqual(false) 22 | }) 23 | 24 | // it('should have resolved use', () => { 25 | // const { links: { large, thumb } } = response 26 | // const payloadsByName: Directory = { files: { large: { hash } } } 27 | 28 | // const resolvedOne = SchemaMiller.resolveDependency(large, payloadsByName) 29 | // const resolvedTwo = SchemaMiller.resolveDependency(thumb, payloadsByName) 30 | 31 | // expect(resolvedOne.opts.use).toEqual(hash) 32 | // expect(resolvedTwo.opts.use).toEqual(hash) 33 | // }) 34 | }) 35 | -------------------------------------------------------------------------------- /src/modules/observe.ts: -------------------------------------------------------------------------------- 1 | import { API } from '../core/api' 2 | import { streamHandler } from '../helpers/handlers' 3 | import { FeedItem, Block } from '../models' 4 | import { ReadableStream } from 'web-streams-polyfill/ponyfill' 5 | 6 | /** 7 | * Observe is an API to stream updates from a thread or all threads 8 | */ 9 | export default class Observe extends API { 10 | /** 11 | * Observe to updates in a thread or all threads 12 | * An update is generated when a new block is added to a thread. See [[Block.BlockType]] for 13 | * details and a list of block types to which you can observe. 14 | * 15 | * @param types List of event types to stream (e.g., ['FILES', 'COMMENTS', 'LIKES']. Leave 16 | * undefined or empty to include all types. 17 | * @param thread Limit updates to a single thread. Leave undefined or an empty stream to 18 | * stream events across all threads. 19 | * @returns A ReadableStream of FeedItem objects. 20 | */ 21 | async events(types?: Block.BlockType[], thread?: string) { 22 | const response = await this.sendGet(`observe${thread ? `/${thread}` : ''}`, undefined, { 23 | type: (types || []).join('|') 24 | }) 25 | if (!response.body) { 26 | throw Error('Empty response stream') 27 | } 28 | return streamHandler(response.body as ReadableStream) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/modules/__test__/__static__/config.ts: -------------------------------------------------------------------------------- 1 | export const config = { 2 | API: { 3 | HTTPHeaders: { 4 | 'Access-Control-Allow-Headers': [ 5 | 'Content-Type', 6 | 'Method', 7 | 'X-Textile-Args', 8 | 'X-Textile-Opts', 9 | 'X-Requested-With' 10 | ], 11 | 'Access-Control-Allow-Methods': [ 12 | 'GET', 13 | 'POST', 14 | 'DELETE', 15 | 'OPTIONS' 16 | ], 17 | 'Access-Control-Allow-Origin': [ 18 | 'http://localhost:*', 19 | 'http://127.0.0.1:*' 20 | ], 21 | 'Server': [ 22 | 'go-textile/0.0.0' 23 | ] 24 | }, 25 | SizeLimit: 0 26 | }, 27 | Account: { 28 | Address: 'P4RaCnWZYhKCztWK7mii1WQZmXVAtBVdRAyBU6Em51agxTfW', 29 | Thread: 'thread' 30 | }, 31 | Addresses: { 32 | API: '127.0.0.1:40600', 33 | CafeAPI: '127.0.0.1:40601', 34 | Gateway: '127.0.0.1:5050' 35 | }, 36 | Cafe: { 37 | Client: { 38 | Mobile: { 39 | P2PWireLimit: 0 40 | } 41 | }, 42 | Host: { 43 | NeighborURL: '', 44 | Open: false, 45 | PublicIP: '', 46 | SizeLimit: 0, 47 | URL: '' 48 | } 49 | }, 50 | IsMobile: false, 51 | IsServer: true, 52 | Logs: { 53 | LogToDisk: true 54 | }, 55 | Threads: { 56 | Defaults: { 57 | ID: '' 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/modules/utils.ts: -------------------------------------------------------------------------------- 1 | import { API } from '../core/api' 2 | import { Versions, Summary } from '../models' 3 | 4 | /** 5 | * Utils is an API module for various Textile node utilities 6 | * 7 | * @extends API 8 | */ 9 | export default class Utils extends API { 10 | /** 11 | * Get the current node's API, and application versions 12 | * @returns Version of Cafe API and node binary release 13 | */ 14 | async version() { 15 | const response = await this.sendGet('../../') 16 | return response.json() as Promise 17 | } 18 | 19 | /** 20 | * Get a summary of all local node data 21 | * @returns Summary of node activity 22 | */ 23 | async summary() { 24 | const response = await this.sendGet('summary') 25 | return response.json() as Promise 26 | } 27 | 28 | /** 29 | * Pings another peer on the network, returning wherther they are online or offline. 30 | * @param id The peer id 31 | * @returns Whether the peer is online 32 | */ 33 | async ping(id: string) { 34 | const response = await this.sendGet('ping', [id]) 35 | return response.text() 36 | } 37 | 38 | /** 39 | * Check whether the underlying node's API is online 40 | * @returns Whether the API is online 41 | */ 42 | async online() { 43 | const response = await this.sendGet('../../health') 44 | return response.status === 204 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/models/threads_service_pb.ts: -------------------------------------------------------------------------------- 1 | /* GENERATED FROM threads_service.proto. DO NOT EDIT MANUALLY. */ 2 | /* tslint:disabled */ 3 | /* eslint-disable */ 4 | 5 | import * as model from './model_pb' 6 | 7 | export interface ThreadEnvelope { 8 | thread: string 9 | hash: string 10 | ciphertext: string 11 | sig: string 12 | } 13 | 14 | export interface ThreadBlock { 15 | header: ThreadBlockHeader 16 | type: model.Block.BlockType 17 | payload: any 18 | } 19 | 20 | export interface ThreadBlockHeader { 21 | date: string 22 | parents: string[] 23 | author: string 24 | address: string 25 | } 26 | 27 | export interface ThreadAdd { 28 | inviter: model.Peer 29 | thread: model.Thread 30 | } 31 | 32 | export interface ThreadIgnore { 33 | target: string 34 | } 35 | 36 | export interface ThreadFlag { 37 | target: string 38 | } 39 | 40 | export interface ThreadJoin { 41 | inviter: string 42 | peer: model.Peer 43 | } 44 | 45 | export interface ThreadAnnounce { 46 | peer: model.Peer 47 | name: string 48 | } 49 | 50 | export interface ThreadMessage { 51 | body: string 52 | } 53 | 54 | export interface ThreadFiles { 55 | target: string 56 | body: string 57 | keys: ThreadFiles.Keys 58 | } 59 | 60 | export namespace ThreadFiles { 61 | export interface Keys { 62 | [k: string]: string 63 | } 64 | } 65 | 66 | export interface ThreadComment { 67 | target: string 68 | body: string 69 | } 70 | 71 | export interface ThreadLike { 72 | target: string 73 | } 74 | -------------------------------------------------------------------------------- /src/modules/__test__/config.test.ts: -------------------------------------------------------------------------------- 1 | import nock from 'nock' 2 | import Config from '../config' 3 | import { ApiOptions } from '../../models' 4 | import { config as response } from './__static__/config' 5 | 6 | const opts: ApiOptions = { 7 | url: 'http://127.0.0.1', 8 | port: 40600, 9 | version: 0 10 | } 11 | 12 | const ROOT = `${opts.url}:${opts.port}` 13 | const config = new Config(opts) 14 | 15 | describe('config get', () => { 16 | it('should resolve to valid return value', async () => { 17 | nock(ROOT) 18 | .get('/api/v0/config/Addresses/API') 19 | .reply(200, `"${ROOT}"`) 20 | nock(ROOT) 21 | .get('/api/v0/config/Not/Valid') 22 | .replyWithError({ 23 | message: 'empty struct value', 24 | code: 400 25 | }) 26 | 27 | expect(await config.get('Addresses.API')).toEqual(ROOT) 28 | }) 29 | }) 30 | 31 | describe('config get whole thing', () => { 32 | it('should resolve to valid return value', async () => { 33 | nock(ROOT) 34 | .get('/api/v0/config') 35 | .reply(200, response) 36 | 37 | expect(await config.get()).toEqual(response) 38 | }) 39 | }) 40 | 41 | describe('config set', () => { 42 | it('should resolve to boolean', async () => { 43 | // const patch = [{ op: 'replace', path: 'IsServer', value: true }] 44 | // TODO: Specify patch body matching 45 | nock(ROOT) 46 | .patch('/api/v0/config') 47 | .reply(204) 48 | expect(await config.set('IsServer', true)).toEqual(true) 49 | }) 50 | }) 51 | -------------------------------------------------------------------------------- /src/modules/__test__/profile.test.ts: -------------------------------------------------------------------------------- 1 | import nock from 'nock' 2 | import Profile from '../profile' 3 | import { ApiOptions } from '../../models' 4 | import { profile as response } from './__static__/responses' 5 | 6 | const opts: ApiOptions = { 7 | url: 'http://127.0.0.1', 8 | port: 40600, 9 | version: 0 10 | } 11 | 12 | const ROOT = `${opts.url}:${opts.port}` 13 | const profile = new Profile(opts) 14 | 15 | describe('profile set name', () => { 16 | it('should resolve to boolean', async () => { 17 | nock(ROOT) 18 | .post('/api/v0/profile/name') 19 | .reply(201) 20 | 21 | expect(await profile.setName('displayname')).toEqual(true) 22 | }) 23 | }) 24 | 25 | describe('profile get name', () => { 26 | it('should resolve to name string', async () => { 27 | nock(ROOT) 28 | .get('/api/v0/profile') 29 | .reply(200, response.get) 30 | 31 | expect(await profile.name()).toEqual('displayname') 32 | }) 33 | }) 34 | 35 | describe('profile get avatar', () => { 36 | it('should resolve to avatar hash string', async () => { 37 | nock(ROOT) 38 | .get('/api/v0/profile') 39 | .reply(200, response.get) 40 | 41 | expect(await profile.avatar()).toEqual(response.get.avatar) 42 | }) 43 | }) 44 | 45 | describe('profile get', () => { 46 | it('should resolve to full profile', async () => { 47 | nock(ROOT) 48 | .get('/api/v0/profile') 49 | .reply(200, response.get) 50 | 51 | expect(await profile.get()).toEqual(response.get) 52 | }) 53 | }) 54 | -------------------------------------------------------------------------------- /src/modules/__test__/tokens.test.ts: -------------------------------------------------------------------------------- 1 | import nock from 'nock' 2 | import Tokens from '../tokens' 3 | import { ApiOptions } from '../../models' 4 | 5 | const token = 'RgUbEwnaifcr6WoM2ZfnnGr9dAf1QtWhdWVChzd6ui55To52F1DRkGsRni4H' 6 | 7 | const opts: ApiOptions = { 8 | url: 'http://127.0.0.1', 9 | port: 40600, 10 | version: 0 11 | } 12 | 13 | const ROOT = `${opts.url}:${opts.port}` 14 | const tokens = new Tokens(opts) 15 | 16 | describe('tokens create', () => { 17 | it('should resolve to a new valid cafe token', async () => { 18 | nock(ROOT) 19 | .post('/api/v0/tokens') 20 | .reply(201, token) 21 | 22 | expect(await tokens.add()).toEqual(token) 23 | }) 24 | }) 25 | 26 | describe('tokens list', () => { 27 | it('should resolve to a list of token hashes', async () => { 28 | nock(ROOT) 29 | .get('/api/v0/tokens') 30 | .reply(200, [token]) 31 | 32 | expect(await tokens.list()).toEqual([token]) 33 | }) 34 | }) 35 | 36 | describe('tokens validate', () => { 37 | it('should resolve to boolean (true) on valid token', async () => { 38 | nock(ROOT) 39 | .get(`/api/v0/tokens/${token}`) 40 | .reply(200) 41 | 42 | expect(await tokens.validate(token)).toEqual(true) 43 | }) 44 | }) 45 | 46 | describe('tokens remove', () => { 47 | it('should resolve to boolean (true) on successfull removal', async () => { 48 | nock(ROOT) 49 | .delete(`/api/v0/tokens/${token}`) 50 | .reply(204) 51 | 52 | expect(await tokens.remove(token)).toEqual(true) 53 | }) 54 | }) 55 | -------------------------------------------------------------------------------- /src/modules/__test__/likes.test.ts: -------------------------------------------------------------------------------- 1 | import nock from 'nock' 2 | import Likes from '../likes' 3 | import { ApiOptions } from '../../models' 4 | import { likes as response, ignore } from './__static__/responses' 5 | 6 | const opts: ApiOptions = { 7 | url: 'http://127.0.0.1', 8 | port: 40600, 9 | version: 0 10 | } 11 | 12 | const ROOT = `${opts.url}:${opts.port}` 13 | const likes = new Likes(opts) 14 | 15 | describe('likes add', () => { 16 | it('should resolve to block update', async () => { 17 | nock(ROOT) 18 | .post('/api/v0/blocks/id/likes') 19 | .reply(201, response.add) 20 | 21 | expect(await likes.add('id')).toEqual(response.add) 22 | }) 23 | }) 24 | 25 | describe('likes get', () => { 26 | it('should resolve to block update', async () => { 27 | nock(ROOT) 28 | .get('/api/v0/blocks/id/like') 29 | .reply(200, response.add) 30 | 31 | expect(await likes.get('id')).toEqual(response.add) 32 | }) 33 | }) 34 | 35 | describe('likes list', () => { 36 | it('should resolve to array of block updates', async () => { 37 | nock(ROOT) 38 | .get('/api/v0/blocks/id/likes') 39 | .reply(200, { items: [response.add] }) 40 | 41 | expect(await likes.list('id')).toEqual({ items: [response.add] }) 42 | }) 43 | }) 44 | 45 | describe('likes ignore', () => { 46 | it('should resolve to ignore block', async () => { 47 | nock(ROOT) 48 | .delete('/api/v0/blocks/id') 49 | .reply(201, ignore) 50 | 51 | expect(await likes.ignore('id')).toEqual(ignore) 52 | }) 53 | }) 54 | -------------------------------------------------------------------------------- /src/modules/__test__/__static__/contacts.ts: -------------------------------------------------------------------------------- 1 | import { Contact } from '../../../models' 2 | export namespace contacts { 3 | export const search = { 4 | id: 'id', 5 | date: '2019-03-20T20:29:37.981888051Z', 6 | value: { 7 | '@type': '/Contact', 8 | 'address': 'address', 9 | 'name': 'displayname', 10 | 'avatar': 'avatar', 11 | 'peers': [ 12 | { 13 | id: 'id', 14 | address: 'address', 15 | name: 'displayname', 16 | avatar: 'avatar', 17 | inboxes: [ 18 | { 19 | peer: 'peer', 20 | address: 'address', 21 | api: 'v0', 22 | protocol: '/textile/cafe/1.0.0', 23 | node: '1.0.0-rc33', 24 | url: 'http://xx.xxx.xxx.xx:40601', 25 | swarm: [ 26 | '/ip4/xx.xxx.xxx.xx/tcp/4001' 27 | ] 28 | }, 29 | { 30 | peer: 'peer', 31 | address: 'address', 32 | api: 'v0', 33 | protocol: '/textile/cafe/1.0.0', 34 | node: '1.0.0-rc28', 35 | url: 'http://127.0.0.1:42601', 36 | swarm: [ 37 | '/ip4/127.0.0.1/tcp/5789' 38 | ] 39 | } 40 | ], 41 | created: '2019-01-18T16:17:35.902912Z', 42 | updated: '2019-03-20T20:29:37.981888051Z' 43 | } 44 | ] 45 | } 46 | } 47 | export const contact: Contact = { ...contacts.search.value, threads: [] } 48 | } 49 | -------------------------------------------------------------------------------- /src/modules/__test__/messages.test.ts: -------------------------------------------------------------------------------- 1 | import nock from 'nock' 2 | import Messages from '../messages' 3 | import { ApiOptions } from '../../models' 4 | import { messages as response, ignore } from './__static__/responses' 5 | 6 | const opts: ApiOptions = { 7 | url: 'http://127.0.0.1', 8 | port: 40600, 9 | version: 0 10 | } 11 | 12 | const ROOT = `${opts.url}:${opts.port}` 13 | const messages = new Messages(opts) 14 | 15 | describe('messages add', () => { 16 | it('should resolve to block update', async () => { 17 | nock(ROOT) 18 | .post('/api/v0/threads/thread/messages') 19 | .reply(201, response.add) 20 | 21 | expect(await messages.add('thread', 'body')).toEqual(response.add) 22 | }) 23 | }) 24 | 25 | describe('messages get', () => { 26 | it('should resolve to block update', async () => { 27 | nock(ROOT) 28 | .get('/api/v0/messages/id') 29 | .reply(200, response.add) 30 | 31 | expect(await messages.get('id')).toEqual(response.add) 32 | }) 33 | }) 34 | 35 | describe('messages list', () => { 36 | it('should resolve to array of block updates', async () => { 37 | nock(ROOT) 38 | .get('/api/v0/messages') 39 | .reply(200, response.list) 40 | expect(await messages.list()).toEqual(response.list) 41 | }) 42 | }) 43 | 44 | describe('messages ignore', () => { 45 | it('should resolve to ignore block', async () => { 46 | nock(ROOT) 47 | .delete('/api/v0/blocks/id') 48 | .reply(201, ignore) 49 | 50 | expect(await messages.ignore('id')).toEqual(ignore) 51 | }) 52 | }) 53 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "error", 3 | "extends": [ 4 | "@textile/tslint-rules/tslint" 5 | ], 6 | "linterOptions": { 7 | "exclude": [ 8 | "**/*.json", 9 | "**/*.js", 10 | "node_modules" 11 | ] 12 | }, 13 | "jsRules": { 14 | "quotemark": [true, "single", "jsx-single", "avoid-escape"], 15 | "semicolon": [true, "never"], 16 | "jsx-no-multiline-js": false, 17 | "no-shadowed-variable": false, 18 | "no-null-keyword": true, 19 | "no-string-literal": false, 20 | "no-this-assignment": false, 21 | "no-submodule-imports": false, 22 | "variable-name": false, 23 | "no-console": [true, "log"], 24 | "no-implicit-dependencies": [true, "dev"], 25 | "max-classes-per-file": false, 26 | "object-literal-sort-keys": false, 27 | "ordered-imports": false, 28 | "space-before-function-paren": false, 29 | "trailing-comma": [ 30 | true, 31 | { 32 | "multiline": "never", 33 | "singleline": "never" 34 | } 35 | ], 36 | "max-line-length": [true, 200] 37 | }, 38 | "rules": { 39 | "member-access": false, 40 | "quotemark": [true, "single", "jsx-single", "avoid-escape"], 41 | "semicolon": [true, "never"], 42 | "jsx-no-multiline-js": false, 43 | "no-object-literal-type-assertion": false, 44 | "trailing-comma": [ 45 | true, 46 | { 47 | "multiline": "never", 48 | "singleline": "never" 49 | } 50 | ], 51 | "max-line-length": [true, 200] 52 | }, 53 | "rulesDirectory": [] 54 | } 55 | -------------------------------------------------------------------------------- /src/modules/__test__/comments.test.ts: -------------------------------------------------------------------------------- 1 | import nock from 'nock' 2 | import Comments from '../comments' 3 | import { ApiOptions } from '../../models' 4 | import { comments as response, ignore } from './__static__/responses' 5 | 6 | const opts: ApiOptions = { 7 | url: 'http://127.0.0.1', 8 | port: 40600, 9 | version: 0 10 | } 11 | 12 | const ROOT = `${opts.url}:${opts.port}` 13 | const comments = new Comments(opts) 14 | 15 | describe('comments add', () => { 16 | it('should resolve to block update', async () => { 17 | nock(ROOT) 18 | .post('/api/v0/blocks/id/comments') 19 | .reply(201, response.add) 20 | 21 | expect(await comments.add('id', 'comment')).toEqual(response.add) 22 | }) 23 | }) 24 | 25 | describe('comments get', () => { 26 | it('should resolve to block update', async () => { 27 | nock(ROOT) 28 | .get('/api/v0/blocks/id/comment') 29 | .reply(200, response.add) 30 | 31 | expect(await comments.get('id')).toEqual(response.add) 32 | }) 33 | }) 34 | 35 | describe('comments list', () => { 36 | it('should resolve to array of block updates', async () => { 37 | nock(ROOT) 38 | .get('/api/v0/blocks/id/comments') 39 | .reply(200, { items: [response.add] }) 40 | 41 | expect(await comments.list('id')).toEqual({ items: [response.add] }) 42 | }) 43 | }) 44 | 45 | describe('comments ignore', () => { 46 | it('should resolve to ignore block', async () => { 47 | nock(ROOT) 48 | .delete('/api/v0/blocks/id') 49 | .reply(201, ignore) 50 | 51 | expect(await comments.ignore('id')).toEqual(ignore) 52 | }) 53 | }) 54 | -------------------------------------------------------------------------------- /src/models/query_pb.ts: -------------------------------------------------------------------------------- 1 | /* GENERATED FROM query.proto. DO NOT EDIT MANUALLY. */ 2 | /* tslint:disabled */ 3 | /* eslint-disable */ 4 | 5 | export interface QueryOptions { 6 | localOnly: boolean 7 | remoteOnly: boolean 8 | limit: number 9 | wait: number 10 | filter: QueryOptions.FilterType 11 | exclude: string[] 12 | } 13 | 14 | export namespace QueryOptions { 15 | export enum FilterType { 16 | NO_FILTER = 'NO_FILTER', 17 | HIDE_OLDER = 'HIDE_OLDER' 18 | } 19 | } 20 | 21 | export interface Query { 22 | id: string 23 | token: string 24 | type: Query.Type 25 | options: QueryOptions 26 | payload: any 27 | } 28 | 29 | export namespace Query { 30 | export enum Type { 31 | THREAD_SNAPSHOTS = 'THREAD_SNAPSHOTS', 32 | CONTACTS = 'CONTACTS' 33 | } 34 | } 35 | 36 | export interface PubSubQuery { 37 | id: string 38 | type: Query.Type 39 | payload: any 40 | responseType: PubSubQuery.ResponseType 41 | exclude: string[] 42 | topic: string 43 | timeout: number 44 | } 45 | 46 | export namespace PubSubQuery { 47 | export enum ResponseType { 48 | P2P = 'P2P', 49 | PUBSUB = 'PUBSUB' 50 | } 51 | } 52 | 53 | export interface QueryResult { 54 | id: string 55 | date: string 56 | local: boolean 57 | value: any 58 | } 59 | 60 | export interface QueryResults { 61 | type: Query.Type 62 | items: QueryResult[] 63 | } 64 | 65 | export interface PubSubQueryResults { 66 | id: string 67 | results: QueryResults 68 | } 69 | 70 | export interface ContactQuery { 71 | address: string 72 | name: string 73 | } 74 | 75 | export interface ThreadSnapshotQuery { 76 | address: string 77 | } 78 | -------------------------------------------------------------------------------- /src/modules/logs.ts: -------------------------------------------------------------------------------- 1 | import { API } from '../core/api' 2 | 3 | /** 4 | * Logs is an API module for managing the verbosity of one or all subsystems logs 5 | * 6 | * Textile logs piggyback on the IPFS event logs 7 | * 8 | * @extends API 9 | */ 10 | export default class Logs extends API { 11 | /** 12 | * List the verbosity of one or all subsystems logs 13 | * 14 | * @param subsystem Subsystem logging identifier (omit for all) 15 | * @param tex Whether to list only Textile subsystems, or all available subsystems 16 | * @returns An object of (current) key- (subsystem) value (level) pairs 17 | */ 18 | async get(subsystem?: string, tex?: boolean) { 19 | const response = await this.sendGet( 20 | `logs${subsystem ? `/${subsystem}` : ''}`, 21 | undefined, 22 | { 'tex-only': tex || false } 23 | ) 24 | return response.json() as Promise> 25 | } 26 | 27 | /** 28 | * Set the verbosity of one or all subsystems logs 29 | * 30 | * @param level Log level, must be one of: debug, info, warning, error, or critical. 31 | * @param subsystem Subsystem logging identifier (omit for all) 32 | * @param tex Whether to change only Textile subsystems, or all available subsystems 33 | * @returns An object of (updated) key- (subsystem) value (level) pairs 34 | */ 35 | async set(level: string, subsystem?: string, tex?: boolean) { 36 | const response = await this.sendPost( 37 | `logs${subsystem ? `/${subsystem}` : ''}`, 38 | undefined, 39 | { level, 'tex-only': tex || false } 40 | ) 41 | return response.json() as Promise> 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/modules/schemas/index.ts: -------------------------------------------------------------------------------- 1 | import { API } from '../../core/api' 2 | import defaults from './defaults' 3 | import { Node, FileIndex } from '../../models' 4 | 5 | /** 6 | * Schemas is an API module for managing Textile schemas 7 | * 8 | * @extends API 9 | */ 10 | export default class Schemas extends API { 11 | /** 12 | * Default Textile schemas 13 | * @returns An object with various commonly used schemas 14 | */ 15 | async defaults() { 16 | return defaults 17 | } 18 | 19 | /** 20 | * Default Textile schemas 21 | * @returns An object with various commonly used schemas 22 | */ 23 | async defaultByName(name: string) { 24 | const key = Object.keys(defaults).find((known: string) => known === name) 25 | if (key) { 26 | return defaults[key] 27 | } 28 | return 29 | } 30 | 31 | /** 32 | * Creates and validates a new schema from input JSON 33 | * 34 | * @param schema Input JSON-based thread schema 35 | * @returns The milled schema as a file index 36 | */ 37 | async add(schema: object) { 38 | const response = await this.sendPost( 39 | `/api/v0/mills/schema`, 40 | undefined, 41 | undefined, 42 | schema, 43 | { 'Content-Type': 'application/json' } 44 | ) 45 | return response.json() as Promise 46 | } 47 | 48 | /** 49 | * Retrieves a schema by thread ID 50 | * 51 | * @param thread ID of the thread 52 | * @returns The schema of the target thread 53 | */ 54 | async get(thread: string) { 55 | const response = await this.sendGet(`/api/v0/threads/${thread}`) 56 | return (await response.json()).schema_node as Node 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/modules/__test__/__static__/threads.ts: -------------------------------------------------------------------------------- 1 | export const threads = { 2 | add: { 3 | block_count: 1, 4 | head: 'head', 5 | head_block: { 6 | author: '12D3KooWJKPeAzZ6Sig3p1BwMdz5aMmnKhhgEdgJqn4wd1dsTMZB', 7 | date: '2019-03-28T17:21:44.405854Z', 8 | id: 'id', 9 | parents: [], 10 | thread: 'thread', 11 | type: 'JOIN', 12 | user: { 13 | address: 'P4RaCnWZYhKCztWK7mii1WQZmXVAtBVdRAyBU6Em51agxTfW', 14 | name: 'P4RaCnW' 15 | } 16 | }, 17 | id: 'thread', 18 | initiator: 'P4RaCnWZYhKCztWK7mii1WQZmXVAtBVdRAyBU6Em51agxTfW', 19 | key: '1234', 20 | whitelist: [ 21 | 'P4RaCnWZYhKCztWK7mii1WQZmXVAtBVdRAyBU6Em51agxTfW' 22 | ], 23 | name: 'test', 24 | peer_count: 1, 25 | schema: 'QmeVa8vUbyjHaYaeki8RZRshsn3JeYGi8QCnLCWXh6euEh', 26 | schema_node: { 27 | links: { 28 | large: { 29 | mill: '/image/resize', 30 | opts: { 31 | quality: '80', 32 | width: '800' 33 | }, 34 | use: ':file' 35 | }, 36 | small: { 37 | mill: '/image/resize', 38 | opts: { 39 | quality: '80', 40 | width: '320' 41 | }, 42 | use: ':file' 43 | }, 44 | thumb: { 45 | mill: '/image/resize', 46 | opts: { 47 | quality: '80', 48 | width: '100' 49 | }, 50 | pin: true, 51 | use: 'large' 52 | } 53 | }, 54 | name: 'media', 55 | pin: true 56 | }, 57 | sharing: 'SHARED', 58 | sk: 'secret_key', 59 | state: 'LOADED', 60 | type: 'OPEN' 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/modules/__test__/contacts.test.ts: -------------------------------------------------------------------------------- 1 | import nock from 'nock' 2 | import Contacts from '../contacts' 3 | import { Readable } from 'stream' 4 | import { ApiOptions, Contact } from '../../models' 5 | import { contacts as response } from './__static__/contacts' 6 | 7 | const opts: ApiOptions = { 8 | url: 'http://127.0.0.1', 9 | port: 40600, 10 | version: 0 11 | } 12 | 13 | const ROOT = `${opts.url}:${opts.port}` 14 | const contacts = new Contacts(opts) 15 | 16 | describe('contacts add', () => { 17 | it('should resolve to boolean', async () => { 18 | nock(ROOT) 19 | .put('/api/v0/contacts/address', (body: Contact) => { 20 | return body.address === response.contact.address 21 | }) 22 | .reply(204) 23 | 24 | expect(await contacts.add('address', response.contact)).toEqual(true) 25 | }) 26 | }) 27 | 28 | describe('contacts get', () => { 29 | it('should resolve to a valid contact object', async () => { 30 | nock(ROOT) 31 | .get('/api/v0/contacts/address') 32 | .reply(200, response.contact) 33 | 34 | expect(await contacts.get('address')).toEqual(response.contact) 35 | }) 36 | }) 37 | 38 | describe('contacts list', () => { 39 | it('should resolve to array of contacts', async () => { 40 | nock(ROOT) 41 | .get('/api/v0/contacts') 42 | .reply(200, { items: [response.contact] }) 43 | 44 | expect(await contacts.list()).toEqual({ items: [response.contact] }) 45 | }) 46 | }) 47 | 48 | describe('contacts remove', () => { 49 | it('should resolve to boolean', async () => { 50 | nock(ROOT) 51 | .delete('/api/v0/contacts/address') 52 | .reply(204) 53 | 54 | expect(await contacts.remove('address')).toEqual(true) 55 | }) 56 | }) 57 | -------------------------------------------------------------------------------- /src/modules/__test__/cafes.test.ts: -------------------------------------------------------------------------------- 1 | import nock from 'nock' 2 | import Cafes from '../cafes' 3 | import { ApiOptions } from '../../models' 4 | import { cafes as response } from './__static__/responses' 5 | 6 | const opts: ApiOptions = { 7 | url: 'http://127.0.0.1', 8 | port: 40600, 9 | version: 0 10 | } 11 | 12 | const ROOT = `${opts.url}:${opts.port}` 13 | const cafes = new Cafes(opts) 14 | 15 | describe('cafes add', () => { 16 | it('should resolve to valid cafe session', async () => { 17 | nock(ROOT) 18 | .post('/api/v0/cafes') 19 | .reply(201, response.add) 20 | 21 | expect(await cafes.add('http://fake.cafe', 'token')).toEqual(response.add) 22 | }) 23 | }) 24 | 25 | describe('cafes get', () => { 26 | it('should resolve to valid safe session', async () => { 27 | nock(ROOT) 28 | .get('/api/v0/cafes/id') 29 | .reply(200, response.add) 30 | 31 | expect(await cafes.get('id')).toEqual(response.add) 32 | }) 33 | }) 34 | 35 | describe('cafes list', () => { 36 | it('should respond with plain text account seed', async () => { 37 | nock(ROOT) 38 | .get('/api/v0/cafes') 39 | .reply(200, { items: [response.add] }) 40 | 41 | expect(await cafes.list()).toEqual({ items: [ response.add ] }) 42 | }) 43 | }) 44 | 45 | describe('cafes checkMessages', () => { 46 | it('should resolve to boolean', async () => { 47 | nock(ROOT) 48 | .post('/api/v0/cafes/messages') 49 | .reply(200, 'ok') 50 | 51 | expect(await cafes.messages()).toEqual(true) 52 | }) 53 | }) 54 | 55 | describe('cafes remove', () => { 56 | it('should resolve to boolean', async () => { 57 | nock(ROOT) 58 | .delete('/api/v0/cafes/id') 59 | .reply(204, 'ok') 60 | 61 | expect(await cafes.remove('id')).toEqual(true) 62 | }) 63 | }) 64 | -------------------------------------------------------------------------------- /src/modules/account.ts: -------------------------------------------------------------------------------- 1 | import { API, DEFAULT_API_OPTIONS } from '../core/api' 2 | import { ApiOptions, Contact } from '../models' 3 | import Snapshots from './snapshots' 4 | 5 | /** 6 | * Account is an API module for managing a node account 7 | * 8 | * @extends API 9 | */ 10 | export default class Account extends API { 11 | private snapshots: Snapshots 12 | constructor(opts: ApiOptions = DEFAULT_API_OPTIONS) { 13 | super(opts) 14 | this.snapshots = new Snapshots(opts) 15 | } 16 | 17 | /** 18 | * Retrieve the local node account address 19 | * 20 | * @returns The current node account's address 21 | */ 22 | async address() { 23 | const response = await this.sendGet('account/address') 24 | return response.text() 25 | } 26 | 27 | /** 28 | * Retrieve the local node account seed 29 | * 30 | * @returns The current node account's seed 31 | */ 32 | async seed() { 33 | const response = await this.sendGet('account/seed') 34 | return response.text() 35 | } 36 | 37 | /** 38 | * Retrieve the local node account's own contact info 39 | * 40 | * @returns The current node account's contact info 41 | */ 42 | async contact(): Promise { 43 | const response = await this.sendGet('account') 44 | return response.json() 45 | } 46 | 47 | /** 48 | * Syncs the local node account with all thread snapshots found on the network 49 | * 50 | * @param apply Whether to apply the discovered thread snapshots as they are found (default false) 51 | * @param wait Stops searching after 'wait' seconds have elapsed (max 30 default 2) 52 | */ 53 | sync(apply?: boolean, wait?: number) { 54 | if (apply) { 55 | return this.snapshots.apply(undefined, wait) 56 | } 57 | return this.snapshots.search(wait) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/modules/likes.ts: -------------------------------------------------------------------------------- 1 | import { API } from '../core/api' 2 | import { Like, LikeList, Block } from '../models' 3 | 4 | /** 5 | * Likes is an API module for managing thread/block likes 6 | * 7 | * Likes are added as blocks in a thread, which target another block, usually a file(s). 8 | * 9 | * @extends API 10 | */ 11 | export default class Likes extends API { 12 | /** 13 | * Adds a like to a block 14 | * 15 | * @param block Target block ID. Usually a file(s) block. 16 | * @returns The generated like block 17 | */ 18 | async add(block: string) { 19 | const response = await this.sendPost(`blocks/${block}/likes`) 20 | return response.json() as Promise 21 | } 22 | 23 | /** 24 | * Retrieves a like by ID 25 | * 26 | * @param id ID of the target like 27 | * @returns The target like block 28 | */ 29 | async get(id: string) { 30 | const response = await this.sendGet(`blocks/${id}/like`) 31 | return response.json() as Promise 32 | } 33 | 34 | /** 35 | * Retrieves a list of likes on a target block 36 | * 37 | * @param block ID of the target block 38 | * @returns An array of likes associated with the target block 39 | */ 40 | async list(block: string) { 41 | const response = await this.sendGet(`blocks/${block}/likes`) 42 | return response.json() as Promise 43 | } 44 | 45 | /** 46 | * Ignores a block like by its ID 47 | * 48 | * This adds an 'ignore' thread block targeted at the like. 49 | * Ignored blocks are by default not returned when listing. 50 | * 51 | * @param id ID of the like 52 | * @returns The added ignore block 53 | */ 54 | async ignore(id: string) { 55 | const response = await this.sendDelete(`blocks/${id}`) 56 | return response.json() as Promise 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/modules/mills.ts: -------------------------------------------------------------------------------- 1 | import { API } from '../core/api' 2 | import { KeyValue, FileIndex } from '../models' 3 | import FormData from 'isomorphic-form-data' 4 | 5 | /** 6 | * Mills is an API module for processing Textile mills 7 | * 8 | * @extends API 9 | */ 10 | export default class Mills extends API { 11 | /** 12 | * Run a mill over a given payload 13 | * 14 | * Currently supports: 15 | * * `/blob` - Process raw data blobs 16 | * * `/image/exif` - Extract EXIF data from an image 17 | * * `/image/resize` - Resize an image 18 | * * `/json` - Process (and validate according to schema.org definition) input JSON data 19 | * * `/schema` - Validate, add, and pin a new JSON-based Schema 20 | * 21 | * @param {string} name Name of the mill. (Relative uri). See above description for details 22 | * @param {KeyValue} options Schema options for the mill 23 | * @param {any} payload A multi-part form containing the payload 24 | * @param {KeyValue} [headers] Extra headers to send in the request 25 | * @returns The generated FileIndex object 26 | */ 27 | async run(name: string, options: KeyValue, payload: any, headers: KeyValue): Promise { 28 | let data: any 29 | if (payload && name !== '/json' && name !== '/schema') { 30 | // Unless explicitly using a json-based mill, assume binary data 31 | data = new FormData() 32 | data.append('file', payload, payload.name || 'milled') 33 | } else if (payload) { 34 | // Otherwise, assume JSON data 35 | data = JSON.stringify(payload) 36 | } 37 | const response = await this.sendPost( 38 | `mills${name}`, 39 | [], 40 | options, 41 | data, 42 | headers, 43 | true // Don't strinify on the other end 44 | ) 45 | return response.json() 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/modules/config.ts: -------------------------------------------------------------------------------- 1 | import { API } from '../core/api' 2 | 3 | /** 4 | * Config is an API module for controling peer node configuration variables 5 | * 6 | * It works much like 'git config'. The configuration values are stored in a config file 7 | * inside your Textile repository. Getting config values will report the currently active 8 | * config settings. This may differ from the values specifed when setting values. 9 | * 10 | * @extends API 11 | */ 12 | export default class Config extends API { 13 | /** 14 | * Report the currently active config settings 15 | * 16 | * The reported settings may differ from the values specifed when setting/patching values. 17 | * 18 | * @param path Config settings path (e.g., Addresses.API). Omit for full config file. 19 | * @returns A JSON representation of the current config setting at the given path 20 | */ 21 | async get(path?: string) { 22 | const cleanPath = path ? `/${path.replace(/\./g, '/')}` : '' 23 | const response = await this.sendGet(`config${cleanPath}`) 24 | return response.json() as Promise 25 | } 26 | 27 | /** 28 | * Replace or update config settings 29 | * 30 | * See https://tools.ietf.org/html/rfc6902 for details on RFC6902 JSON patch format. 31 | * Be sure to restart the daemon for changes to take effect. 32 | * 33 | * @param path Config settings path (e.g., Addresses.API). 34 | * @param value JSON config settings (can be any valid JSON type) 35 | * @returns Whether the operation was successfull 36 | */ 37 | async set(path: string, value: any) { 38 | const cleanPath = `/${path.replace(/\./g, '/')}` 39 | const patch = [{ op: 'replace', path: cleanPath, value }] 40 | const response = await this.sendPatch(`config`, undefined, undefined, patch) 41 | return response.status === 204 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/modules/comments.ts: -------------------------------------------------------------------------------- 1 | import { API } from '../core/api' 2 | import { Comment, CommentList, Block } from '../models' 3 | 4 | /** 5 | * Comments is an API module for managing thread/block comments 6 | * 7 | * Comments are added as blocks in a thread, which target another block, usually a file(s). 8 | * 9 | * @extends API 10 | */ 11 | export default class Comments extends API { 12 | /** 13 | * Adds a comment to a block 14 | * 15 | * @param block Target block ID. Usually a file(s) block. 16 | * @param body Comment body 17 | * @returns The generated comment block 18 | */ 19 | async add(block: string, body: string) { 20 | const response = await this.sendPost( 21 | `blocks/${block}/comments`, [body] 22 | ) 23 | return response.json() as Promise 24 | } 25 | 26 | /** 27 | * Retrieves a comment by ID 28 | * 29 | * @param id ID of the target comment 30 | * @returns The target comment block 31 | */ 32 | async get(id: string) { 33 | const response = await this.sendGet(`blocks/${id}/comment`) 34 | return response.json() as Promise 35 | } 36 | 37 | /** 38 | * Retrieves a list of comments on a target block 39 | * 40 | * @param block ID of the target block 41 | * @returns An array of comment blocks 42 | */ 43 | async list(block: string) { 44 | const response = await this.sendGet(`blocks/${block}/comments`) 45 | return response.json() as Promise 46 | } 47 | 48 | /** 49 | * Ignores a block comment by its ID 50 | * 51 | * This adds an 'ignore' thread block targeted at the comment. 52 | * Ignored blocks are by default not returned when listing. 53 | * 54 | * @param id ID of the comment 55 | * @returns The ignored block 56 | */ 57 | async ignore(id: string) { 58 | const response = await this.sendDelete(`blocks/${id}`) 59 | return response.json() as Promise 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/modules/feed.ts: -------------------------------------------------------------------------------- 1 | import { API } from '../core/api' 2 | import { FeedItemList } from '../models' 3 | 4 | export type FeedModes = 'chrono' | 'annotated' | 'stacks' 5 | 6 | /** 7 | * Feed is an API module for paginating post and annotation block types 8 | * 9 | * @extends API 10 | */ 11 | export default class Feed extends API { 12 | /** 13 | * Retrieves post (join|leave|files|message) and annotation (comment|like) block type 14 | * 15 | * The mode option dictates how the feed is displayed: 16 | * * chrono: All feed block types are shown. Annotations always nest their target post, 17 | * i.e., the post a comment is about. 18 | * * annotated: Annotations are nested under post targets, but are not shown in the 19 | * top-level feed. 20 | * * stacks: Related blocks are chronologically grouped into 'stacks'. A new stack is 21 | * started if an unrelated block breaks continuity. This mode is used by Textile Photos. 22 | * Stacks may include: 23 | * * The initial post with some nested annotations. Newer annotations may have already been 24 | * listed. 25 | * * One or more annotations about a post. The newest annotation assumes the 'top' position in 26 | * the stack. Additional annotations are nested under the target. 27 | * * Newer annotations may have already been listed in the case as well. 28 | * 29 | * @param thread Thread ID (can also use ‘default’) 30 | * @param offset Offset ID to start listing from (omit for latest) 31 | * @param limit List page size (default: 5) 32 | * @param mode Feed mode (one of 'chrono’, 'annotated’, or ‘stacks’) 33 | */ 34 | async list(thread?: string, offset?: string, limit?: number, mode?: FeedModes) { 35 | const response = await this.sendGet( 36 | 'feed', 37 | undefined, 38 | { 39 | thread: thread || '', 40 | offset: offset || '', 41 | limit: limit || 5, 42 | mode: mode || 'chrono' 43 | } 44 | ) 45 | return response.json() as Promise 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/modules/messages.ts: -------------------------------------------------------------------------------- 1 | import { API } from '../core/api' 2 | import { Text, TextList, Block } from '../models' 3 | 4 | /** 5 | * Messages is an API module for managing thread/block messages 6 | * 7 | * Messages are added as blocks in a thread 8 | * 9 | * @extends API 10 | */ 11 | export default class Messages extends API { 12 | /** 13 | * Adds a message to a thread 14 | * 15 | * @param thread Thread ID 16 | * @param body Message body 17 | * @returns The generated message block 18 | */ 19 | async add(thread: string, body: string) { 20 | const response = await this.sendPost(`threads/${thread}/messages`, [body]) 21 | return response.json() as Promise 22 | } 23 | 24 | /** 25 | * Retrieves a message by block ID 26 | * 27 | * @param id ID of the target message 28 | * @returns The target message block 29 | */ 30 | async get(id: string) { 31 | const response = await this.sendGet(`messages/${id}`) 32 | return response.json() as Promise 33 | } 34 | 35 | /** 36 | * Retrieves thread messages 37 | * 38 | * @param thread Thread ID (can also use ‘default’) 39 | * @param offset Offset ID to start listing from (omit for latest) 40 | * @param limit List page size (default: 5) 41 | * @returns An array of message blocks 42 | */ 43 | async list(thread?: string, offset?: string, limit?: number) { 44 | const response = await this.sendGet('messages', undefined, { 45 | thread: thread || '', 46 | offset: offset || '', 47 | limit: limit || 5 48 | }) 49 | return response.json() as Promise 50 | } 51 | 52 | /** 53 | * Ignores a thread message by its ID 54 | * 55 | * This adds an 'ignore' thread block targeted at the comment. 56 | * Ignored blocks are by default not returned when listing. 57 | * 58 | * @param id ID of the message 59 | * @returns The added ignore block 60 | */ 61 | async ignore(id: string) { 62 | const response = await this.sendDelete(`blocks/${id}`) 63 | return response.json() as Promise 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/models/index.ts: -------------------------------------------------------------------------------- 1 | // Protobuf types created from go-textile using: 2 | // yarn add https://github.com/carsonfarmer/protoc-gen-jsonpb-ts 3 | // cd pb/protos 4 | // protoc --plugin=../../node_modules/protoc-gen-jsonpb-ts/bin/protoc-gen-jsonpb-ts --jsonpb-ts_out=. *.proto 5 | export * from './model_pb' 6 | export * from './cafe_service_pb' 7 | export * from './message_pb' 8 | export * from './query_pb' 9 | export * from './threads_service_pb' 10 | export * from './view_pb' 11 | export * from './mobile_pb' 12 | 13 | /** 14 | * The options object for the client object 15 | * @property url The base URL of the Textile node API (default 'http://127.0.0.1') 16 | * @property port The port of the Textile node API (default 40600) 17 | * @property version The Textile node API version (default 0) 18 | */ 19 | export interface TextileOptions { 20 | url?: string 21 | port?: number 22 | version?: number 23 | } 24 | /** 25 | * The options object for the client object 26 | * @property url The base URL of the Textile node API (default 'http://127.0.0.1') 27 | * @property port The port of the Textile node API (default 40600) 28 | * @property version The Textile node API version (default 0) 29 | */ 30 | export interface ApiOptions { 31 | url: string 32 | port: number 33 | version: number 34 | } 35 | 36 | /** 37 | * Additional options to control search queries 38 | * @typedef {Object} QueryOptions 39 | * @param local Whether to only search local contacts 40 | * @param remote Whether to only search remote contacts 41 | * @param limit Stops searching after 'limit' results are found 42 | * @param wait Stops searching after ‘wait’ seconds have elapsed 43 | */ 44 | export interface QueryOptions { 45 | local?: boolean 46 | remote?: boolean 47 | limit?: number 48 | wait?: number 49 | } 50 | 51 | export type KeyValue = Record 52 | 53 | /** 54 | * Version information for Textile nodes 55 | * @property cafe_version The API version of the Cafe 56 | * @property node_version The release version of the running node 57 | */ 58 | export interface Versions { 59 | cafe_version: string 60 | node_version: string 61 | } 62 | -------------------------------------------------------------------------------- /src/modules/invites.ts: -------------------------------------------------------------------------------- 1 | import { API } from '../core/api' 2 | import { Block, ExternalInvite, InviteViewList } from '../models' 3 | 4 | /** 5 | * Invites is an API module for managing thread invites 6 | * 7 | * @extends API 8 | */ 9 | export default class Invites extends API { 10 | /** 11 | * Accept an invite to a thread 12 | * 13 | * @param invite Invite ID 14 | * @param key Key for an external invite 15 | */ 16 | async accept(invite: string, key?: string) { 17 | const response = await this.sendPost( 18 | `invites/${invite}/accept`, 19 | undefined, 20 | { key: key || '' } 21 | ) 22 | return response.json() as Promise 23 | } 24 | 25 | /** 26 | * Adds a peer-to-peer invite to a thread 27 | * 28 | * @param thread Thread ID (can also use ‘default’) 29 | * @param address Account Address (omit to create an external invite) 30 | * @returns Whether the operation was successfull 31 | */ 32 | async add(thread: string, address: string) { 33 | const response = await this.sendPost(`invites`, undefined, { 34 | thread, 35 | address: address || '' 36 | }) 37 | return response.status === 201 38 | } 39 | 40 | /** 41 | * Adds a external invite to a thread 42 | * 43 | * @param thread Thread ID (can also use ‘default’) 44 | * @returns An external invite object 45 | */ 46 | async addExternal(thread: string) { 47 | const response = await this.sendPost(`invites`, undefined, { 48 | thread 49 | }) 50 | return response.json() as Promise 51 | } 52 | 53 | /** 54 | * Lists all pending thread invites 55 | * @returns An array of invite views 56 | */ 57 | async list() { 58 | const response = await this.sendGet('invites') 59 | return response.json() as Promise 60 | } 61 | 62 | /** 63 | * Ignore a direct invite to a thread 64 | * 65 | * @param id ID of the invite 66 | * @returns Whether the operation was successfull 67 | */ 68 | async ignore(id: string) { 69 | const response = await this.sendPost(`invites/${id}/ignore`) 70 | return response.status === 200 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/modules/profile.ts: -------------------------------------------------------------------------------- 1 | import { API, DEFAULT_API_OPTIONS } from '../core/api' 2 | import Config from './config' 3 | import Files from './files' 4 | import { Peer, ApiOptions } from '../models' 5 | 6 | /** 7 | * Profile is an API module for accessing public profile information 8 | */ 9 | export default class Profile extends API { 10 | private config: Config 11 | private files: Files 12 | constructor(opts: ApiOptions = DEFAULT_API_OPTIONS) { 13 | super(opts) 14 | this.config = new Config(opts) 15 | this.files = new Files(opts) 16 | } 17 | 18 | /** 19 | * Retrieve the local node's public profile peer information 20 | * @returns The local node's peer information 21 | */ 22 | async get() { 23 | const response = await this.sendGet('profile') 24 | return response.json() as Promise 25 | } 26 | 27 | /** 28 | * Get the local node's public profile display name 29 | * @returns Node's public profile display name 30 | */ 31 | async name() { 32 | const contact = await this.get() 33 | return contact.name 34 | } 35 | 36 | /** 37 | * Set the local node's public profile display name 38 | * 39 | * @param name Username string 40 | * @returns Whether the update was successful 41 | */ 42 | async setName(name: string) { 43 | const response = await this.sendPost('profile/name', [name]) 44 | return response.status === 201 45 | } 46 | 47 | /** 48 | * Get the local node's public profile avatar image hash 49 | * @returns Node's public profile avatar image hash 50 | */ 51 | async avatar() { 52 | const contact = await this.get() 53 | return contact.avatar 54 | } 55 | 56 | /** 57 | * Set the local node's public profile avatar image 58 | * 59 | * @param image Image to use as new avatar. Can be any input type accepted by [[Files.add]]. 60 | * @returns Whether the update was successful 61 | */ 62 | async setAvatar(image: any) { 63 | const thread = await this.config.get('Account.Thread') 64 | await this.files.add(image, 'avatar', thread as string) 65 | const response = await this.sendPost('profile/avatar') 66 | return response.status === 201 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/models/cafe_service_pb.ts: -------------------------------------------------------------------------------- 1 | /* GENERATED FROM cafe_service.proto. DO NOT EDIT MANUALLY. */ 2 | /* tslint:disabled */ 3 | /* eslint-disable */ 4 | 5 | import * as model from './model_pb' 6 | 7 | export interface CafeChallenge { 8 | address: string 9 | } 10 | 11 | export interface CafeNonce { 12 | value: string 13 | } 14 | 15 | export interface CafeRegistration { 16 | address: string 17 | value: string 18 | nonce: string 19 | sig: string 20 | token: string 21 | } 22 | 23 | export interface CafeDeregistration { 24 | token: string 25 | } 26 | 27 | export interface CafeDeregistrationAck { 28 | id: string 29 | } 30 | 31 | export interface CafeRefreshSession { 32 | access: string 33 | refresh: string 34 | } 35 | 36 | export interface CafePublishPeer { 37 | token: string 38 | peer: model.Peer 39 | } 40 | 41 | export interface CafePublishPeerAck { 42 | id: string 43 | } 44 | 45 | export interface CafeStore { 46 | token: string 47 | cids: string[] 48 | } 49 | 50 | export interface CafeStoreAck { 51 | id: string 52 | } 53 | 54 | export interface CafeUnstore { 55 | token: string 56 | cids: string[] 57 | } 58 | 59 | export interface CafeUnstoreAck { 60 | cids: string[] 61 | } 62 | 63 | export interface CafeObjectList { 64 | cids: string[] 65 | } 66 | 67 | export interface CafeObject { 68 | token: string 69 | cid: string 70 | data: string 71 | node: string 72 | } 73 | 74 | export interface CafeStoreThread { 75 | token: string 76 | id: string 77 | ciphertext: string 78 | } 79 | 80 | export interface CafeStoreThreadAck { 81 | id: string 82 | } 83 | 84 | export interface CafeUnstoreThread { 85 | token: string 86 | id: string 87 | } 88 | 89 | export interface CafeUnstoreThreadAck { 90 | id: string 91 | } 92 | 93 | export interface CafeDeliverMessage { 94 | id: string 95 | client: string 96 | } 97 | 98 | export interface CafeCheckMessages { 99 | token: string 100 | } 101 | 102 | export interface CafeMessages { 103 | messages: model.CafeMessage[] 104 | } 105 | 106 | export interface CafeDeleteMessages { 107 | token: string 108 | } 109 | 110 | export interface CafeDeleteMessagesAck { 111 | more: boolean 112 | } 113 | -------------------------------------------------------------------------------- /src/models/message_pb.ts: -------------------------------------------------------------------------------- 1 | /* GENERATED FROM message.proto. DO NOT EDIT MANUALLY. */ 2 | /* tslint:disabled */ 3 | /* eslint-disable */ 4 | 5 | export interface Message { 6 | type: Message.Type 7 | payload: any 8 | requestId: number 9 | isResponse: boolean 10 | } 11 | 12 | export namespace Message { 13 | export enum Type { 14 | PING = 'PING', 15 | PONG = 'PONG', 16 | THREAD_ENVELOPE = 'THREAD_ENVELOPE', 17 | CAFE_CHALLENGE = 'CAFE_CHALLENGE', 18 | CAFE_NONCE = 'CAFE_NONCE', 19 | CAFE_REGISTRATION = 'CAFE_REGISTRATION', 20 | CAFE_DEREGISTRATION = 'CAFE_DEREGISTRATION', 21 | CAFE_DEREGISTRATION_ACK = 'CAFE_DEREGISTRATION_ACK', 22 | CAFE_SESSION = 'CAFE_SESSION', 23 | CAFE_REFRESH_SESSION = 'CAFE_REFRESH_SESSION', 24 | CAFE_STORE = 'CAFE_STORE', 25 | CAFE_STORE_ACK = 'CAFE_STORE_ACK', 26 | CAFE_UNSTORE = 'CAFE_UNSTORE', 27 | CAFE_UNSTORE_ACK = 'CAFE_UNSTORE_ACK', 28 | CAFE_OBJECT = 'CAFE_OBJECT', 29 | CAFE_OBJECT_LIST = 'CAFE_OBJECT_LIST', 30 | CAFE_STORE_THREAD = 'CAFE_STORE_THREAD', 31 | CAFE_STORE_THREAD_ACK = 'CAFE_STORE_THREAD_ACK', 32 | CAFE_UNSTORE_THREAD = 'CAFE_UNSTORE_THREAD', 33 | CAFE_UNSTORE_THREAD_ACK = 'CAFE_UNSTORE_THREAD_ACK', 34 | CAFE_DELIVER_MESSAGE = 'CAFE_DELIVER_MESSAGE', 35 | CAFE_CHECK_MESSAGES = 'CAFE_CHECK_MESSAGES', 36 | CAFE_MESSAGES = 'CAFE_MESSAGES', 37 | CAFE_DELETE_MESSAGES = 'CAFE_DELETE_MESSAGES', 38 | CAFE_DELETE_MESSAGES_ACK = 'CAFE_DELETE_MESSAGES_ACK', 39 | CAFE_YOU_HAVE_MAIL = 'CAFE_YOU_HAVE_MAIL', 40 | CAFE_PUBLISH_PEER = 'CAFE_PUBLISH_PEER', 41 | CAFE_PUBLISH_PEER_ACK = 'CAFE_PUBLISH_PEER_ACK', 42 | CAFE_QUERY = 'CAFE_QUERY', 43 | CAFE_QUERY_RES = 'CAFE_QUERY_RES', 44 | CAFE_PUBSUB_QUERY = 'CAFE_PUBSUB_QUERY', 45 | CAFE_PUBSUB_QUERY_RES = 'CAFE_PUBSUB_QUERY_RES', 46 | ERROR = 'ERROR', 47 | CAFE_CONTACT_QUERY = 'CAFE_CONTACT_QUERY', 48 | CAFE_CONTACT_QUERY_RES = 'CAFE_CONTACT_QUERY_RES', 49 | CAFE_PUBSUB_CONTACT_QUERY = 'CAFE_PUBSUB_CONTACT_QUERY', 50 | CAFE_PUBSUB_CONTACT_QUERY_RES = 'CAFE_PUBSUB_CONTACT_QUERY_RES' 51 | } 52 | } 53 | 54 | export interface Envelope { 55 | message: Message 56 | sig: string 57 | } 58 | 59 | export interface Error { 60 | code: number 61 | message: string 62 | } 63 | -------------------------------------------------------------------------------- /src/modules/tokens.ts: -------------------------------------------------------------------------------- 1 | import { API } from '../core/api' 2 | 3 | /** 4 | * Tokens is an API module for managing Cafe access tokens 5 | * 6 | * Tokens allow other peers to register with a Cafe peer. Use this API to create, list, validate, 7 | * and remove tokens required for access to this Cafe. 8 | * 9 | * @extends API 10 | */ 11 | export default class Tokens extends API { 12 | /** 13 | * Creates an access token 14 | * 15 | * Generates an access token (44 random bytes) and saves a bcrypt hashed version for future 16 | * lookup. The response contains a base58 encoded version of the random bytes token. If the 17 | * ‘store’ option is set to false, the token is generated, but not stored in the local Cafe 18 | * db. Alternatively, an existing token can be added using by specifying the ‘token’ option. 19 | * 20 | * @param token Use an existing token, rather than creating a new one 21 | * @param store Whether to store the added/generated token to the local db (defaults to true) 22 | * @see Cafes#add 23 | * @returns New token as string 24 | */ 25 | async add(token?: string, store?: boolean) { 26 | const response = await this.sendPost(`tokens`, undefined, { 27 | token: token || '', 28 | store: store || false 29 | }) 30 | return response.text() as Promise 31 | } 32 | 33 | /** 34 | * Check validity of existing cafe access token 35 | * 36 | * @param token Access token 37 | * @returns Whether token is valid 38 | */ 39 | async validate(token: string) { 40 | const response = await this.sendGet(`tokens/${token}`) 41 | return response.status === 200 42 | } 43 | 44 | /** 45 | * Retrieves information about all stored cafe tokens 46 | * 47 | * Only really useful for debugging. These are hashed tokens, so are not valid. 48 | * @returns Array of bcrypt hashed tokens 49 | */ 50 | async list() { 51 | const response = await this.sendGet('tokens') 52 | return response.json() as Promise 53 | } 54 | 55 | /** 56 | * Removes an existing cafe token 57 | * 58 | * @param token Access token 59 | * @returns Whether remove was successful 60 | */ 61 | async remove(token: string) { 62 | const response = await this.sendDelete(`tokens/${token}`) 63 | return response.status === 204 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/modules/cafes.ts: -------------------------------------------------------------------------------- 1 | import { API } from '../core/api' 2 | import { CafeSessionList, CafeSession } from '../models' 3 | 4 | /** 5 | * Cafes is an API module for managing Cafe access, messages, and services 6 | * 7 | * Cafes are other peers on the network who offer pinning, backup, and inbox services. 8 | * 9 | * @extends API 10 | */ 11 | export default class Cafes extends API { 12 | /** 13 | * Registers with a cafe and saves an expiring service session token 14 | * 15 | * An access token is required to register, and should be obtained separately from the target 16 | * Cafe. 17 | * 18 | * @param cafe The host Cafe address 19 | * @param token An access token supplied by the target Cafe 20 | * @see Tokens#create 21 | * @returns A new Cafe session JWT 22 | */ 23 | async add(cafe: string, token: string) { 24 | const response = await this.sendPost(`cafes`, [cafe], { token }) 25 | return response.json() as Promise 26 | } 27 | 28 | /** 29 | * Retrieves information about a cafe session 30 | * 31 | * @param id ID of the target Cafe 32 | * @returns A Cafe session JWT 33 | */ 34 | async get(id: string) { 35 | const response = await this.sendGet(`cafes/${id}`) 36 | return response.json() as Promise 37 | } 38 | 39 | /** 40 | * Retrieves information about all active cafe sessions 41 | * @returns An array of Cafe session JWTs 42 | */ 43 | async list() { 44 | const response = await this.sendGet('cafes') 45 | return response.json() as Promise 46 | } 47 | 48 | /** 49 | * Checkes for messages at all cafes. 50 | * 51 | * New messages are downloaded and processed opportunistically. 52 | * @returns Whether the operation was successfull 53 | */ 54 | async messages() { 55 | const response = await this.sendPost('cafes/messages') 56 | return response.status === 200 57 | } 58 | 59 | /** 60 | * Deregisters with a cafe and removes local session data 61 | * 62 | * Note: pinned content will expire based on the Cafe’s service rules. 63 | * @param id ID of the target Cafe 64 | * @returns Whether the deregistration was successfull 65 | */ 66 | async remove(id: string) { 67 | const response = await this.sendDelete(`cafes/${id}`) 68 | return response.status === 204 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /config/DOCS_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Textile JS HTTP Client _(js-http-client)_ 2 | ===================== 3 | 4 | [Textile](https://www.textile.io) provides encrypted, recoverable, schema-based, and cross-application data storage built on [IPFS](https://github.com/ipfs) and [libp2p](https://github.com/libp2p). We like to think of it as a decentralized data wallet with built-in protocols for sharing and recovery, or more simply, **an open and programmable iCloud**. 5 | 6 | The reference implementation of Textile is [written in Go](https://github.com/textileio/go-textile), and can be compiled to various platforms, including mobile (Android/iOS) and desktop/server (OSX, Windows, Linux, etc). The `js-http-client` library is designed to help support things like browser-based Textile apps, Node.js apps, and other use-cases. 7 | 8 | This library provides access to an underlying `go-textile` node's REST API, adding various simplified APIs to support in-browser and programmatic desktop access. For the most part, the API would mimic the command-line and/or mobile APIs of `go-textile`, with some browser-specific enhancements. 9 | 10 | ## Install 11 | 12 | `js-http-client` is available on [`npmjs.com`](https://www.npmjs.com/package/@textile/js-http-client) under the `@textile` scope. Install it using your favorite package manager: 13 | 14 | ```sh 15 | yarn add @textile/js-http-client 16 | # npm i @textile/js-http-client 17 | ``` 18 | 19 | ## Usage 20 | 21 | ```javascript 22 | // Import the main Textile client 23 | const { Textile } = require("@textile/js-http-client"); 24 | 25 | // Create an instance of the client using the default options 26 | const textile = new Textile(); 27 | 28 | // Or, create an instance specifying your custom Textile node API connection 29 | const textile = new Textile({ 30 | url: "http://127.0.0.1", 31 | port: 40602, 32 | }); 33 | 34 | // Get your local Textile account's contact info 35 | const contact = await textile.account.contact(); 36 | console.log(`My display name is '${contact.name}'`); 37 | // > My display name is 'clyde'. 38 | 39 | // Get your Textile node's address 40 | const address = await textile.profile.address(); 41 | console.log(`My node's address is '${address}'`); 42 | // > My node's address is 'P69vwxHTh1p5...' 43 | 44 | // Get a paginated list of files 45 | const files = await textile.files.list() 46 | }); 47 | console.log("files", files.items); 48 | ``` 49 | 50 | For more detailed examples of usage, peruse the `examples` folder and see the [live `docs`](https://textileio.github.io/js-http-client/). -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | defaults: &defaults 4 | working_directory: ~/js-http-client 5 | docker: 6 | - image: node:10.0.0 7 | 8 | jobs: 9 | lint-test: 10 | <<: *defaults 11 | steps: 12 | - checkout 13 | - restore_cache: 14 | key: yarn-v1-{{ checksum "yarn.lock" }}-{{ arch }} 15 | - restore_cache: 16 | key: node-v1-{{ checksum "package.json" }}-{{ arch }} 17 | - run: yarn install 18 | - run: 19 | name: Lint 20 | command: | 21 | yarn lint 22 | - save_cache: 23 | key: yarn-v1-{{ checksum "yarn.lock" }}-{{ arch }} 24 | paths: 25 | - ~/.cache/yarn 26 | - save_cache: 27 | key: node-v1-{{ checksum "package.json" }}-{{ arch }} 28 | paths: 29 | - node_modules 30 | - run: 31 | name: Test 32 | command: | 33 | mkdir -p test-results/jest 34 | yarn test 35 | environment: 36 | JEST_JUNIT_OUTPUT: test-results/jest/junit.xml 37 | - store_test_results: 38 | path: test-results 39 | - store_artifacts: 40 | path: test-results 41 | deploy: 42 | <<: *defaults 43 | steps: 44 | - checkout 45 | - restore_cache: 46 | key: yarn-v1-{{ checksum "yarn.lock" }}-{{ arch }} 47 | - restore_cache: 48 | key: node-v1-{{ checksum "package.json" }}-{{ arch }} 49 | - run: yarn install 50 | - attach_workspace: 51 | at: ~/js-http-client 52 | - run: 53 | name: Authenticate 54 | command: echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > ~/js-http-client/.npmrc 55 | - run: 56 | name: Publish Package 57 | command: npm publish --access=public --unsafe-perm 58 | 59 | workflows: 60 | version: 2 61 | js-http-client: 62 | jobs: 63 | - lint-test: 64 | filters: 65 | tags: 66 | only: /.*/ 67 | - deploy: 68 | requires: 69 | - lint-test 70 | filters: 71 | tags: 72 | only: /^v.*/ 73 | branches: 74 | ignore: /.*/ 75 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@textile/js-http-client", 3 | "version": "1.1.0", 4 | "description": "Official Textile JS HTTP Wrapper Client", 5 | "main": "dist/index.js", 6 | "types": "dist/index.d.ts", 7 | "scripts": { 8 | "build": "tsc", 9 | "prepare": "yarn build", 10 | "credit": "node ./scripts/contributors.js", 11 | "test": "jest --no-watchman", 12 | "lint": "./node_modules/tslint/bin/tslint -c tslint.json --project .", 13 | "docs": "./node_modules/typedoc/bin/typedoc", 14 | "precommit": "yarn lint && yarn test", 15 | "preversion": "yarn credit; yarn docs; yarn precommit", 16 | "release": "yarn version --patch", 17 | "postversion": "git push --follow-tags", 18 | "browser": "webpack --mode production", 19 | "serve": "webpack-dev-server --mode development --progress --color" 20 | }, 21 | "engines": { 22 | "node": ">= 10.0.0", 23 | "npm": ">= 3.0.0", 24 | "yarn": "^1.0.0" 25 | }, 26 | "repository": { 27 | "type": "git", 28 | "url": "https://github.com/textileio/js-http-client.git" 29 | }, 30 | "bugs": { 31 | "url": "https://github.com/textileio/js-http-client/issues" 32 | }, 33 | "keywords": [ 34 | "textile", 35 | "photos", 36 | "client", 37 | "IPFS", 38 | "p2p" 39 | ], 40 | "author": "textile.io", 41 | "license": "MIT", 42 | "dependencies": { 43 | "@ef-carbon/fetch": "^2.1.3", 44 | "@ef-carbon/url": "^2.4.3", 45 | "@therebel/ksuid": "^1.1.2-alpha1", 46 | "isomorphic-form-data": "^2.0.0", 47 | "toposort": "^2.0.2", 48 | "url-parse": "^1.4.4", 49 | "url-toolkit": "^2.1.6", 50 | "web-streams-polyfill": "^2.0.3" 51 | }, 52 | "devDependencies": { 53 | "@textile/tslint-rules": "1.0.3", 54 | "@types/jest": "^24.0.11", 55 | "@types/nock": "^9.3.1", 56 | "@types/toposort": "^2.0.1", 57 | "@types/url-parse": "^1.4.3", 58 | "eslint": "^5.14.1", 59 | "eslint-config-airbnb-base": "^13.1.0", 60 | "eslint-config-prettier": "^4.0.0", 61 | "eslint-config-strict": "^14.0.1", 62 | "eslint-plugin-import": "^2.16.0", 63 | "eslint-plugin-node": "^8.0.1", 64 | "eslint-plugin-prettier": "^3.0.1", 65 | "jest": "^24.5.0", 66 | "jest-cli": "^24.5.0", 67 | "jsdoc": "^3.5.5", 68 | "markdown-magic": "^0.1.25", 69 | "markdown-magic-github-contributors": "^0.0.3", 70 | "nock": "^10.0.6", 71 | "nodemon": "^1.18.10", 72 | "prettier": "^1.16.4", 73 | "prettier-eslint": "^8.8.2", 74 | "ts-jest": "^24.0.0", 75 | "ts-loader": "^5.4.4", 76 | "tslint": "^5.14.0", 77 | "typedoc": "^0.14.2", 78 | "typedoc-plugin-markdown": "^1.1.27", 79 | "typescript": "^3.3.4000", 80 | "webpack": "^4.30.0", 81 | "webpack-cli": "^3.3.1", 82 | "webpack-dev-server": "^3.3.1" 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/modules/contacts.ts: -------------------------------------------------------------------------------- 1 | import { API } from '../core/api' 2 | import { streamHandler } from '../helpers/handlers' 3 | import { Contact, ContactList, QueryOptions, QueryResult } from '../models' 4 | import { ReadableStream } from 'web-streams-polyfill/ponyfill' 5 | 6 | /** 7 | * Contacts is an API module for managing local contacts and finding contacts on the network 8 | * 9 | * @extends API 10 | */ 11 | export default class Contacts extends API { 12 | /** 13 | * Adds or updates a contact directly, usually from a search 14 | * 15 | * @param contact JSON object representing a contact 16 | * @returns Whether the operation was sucessfull 17 | */ 18 | async add(address: string, contact: Contact) { 19 | const response = await this.sendPut( 20 | `contacts/${address}`, 21 | undefined, 22 | undefined, 23 | contact 24 | ) 25 | return response.status === 204 26 | } 27 | 28 | /** 29 | * Retrieve information about a known contact 30 | * 31 | * @param address Address of the contact 32 | * @returns The associated contact object 33 | */ 34 | async get(address: string) { 35 | const response = await this.sendGet(`contacts/${address}`) 36 | return response.json() as Promise 37 | } 38 | 39 | /** 40 | * Retrieves a list of known contacts 41 | * @returns An array of all known contacts 42 | */ 43 | async list() { 44 | const response = await this.sendGet('contacts') 45 | return response.json() as Promise 46 | } 47 | 48 | /** 49 | * Remove a known contact 50 | * 51 | * @param address Address of the contact 52 | * @returns Whether the operation was successfull 53 | */ 54 | async remove(contactId: string) { 55 | const response = await this.sendDelete(`contacts/${contactId}`) 56 | return response.status === 204 57 | } 58 | 59 | /** 60 | * Searches locally and on the network for contacts by display name, peer id, or address 61 | * 62 | * @param name Search by display name string 63 | * @param address Search by account address string 64 | * @param options Additional options to control the query 65 | * @returns A ReadableStream of QueryResult objects. 66 | * }) 67 | */ 68 | async search(name?: string, address?: string, options?: QueryOptions) { 69 | const opts = options || {} 70 | const cleanOpts = { 71 | name: name || '', 72 | address: address || '', 73 | local: opts.local || false, 74 | remote: opts.remote || false, 75 | limit: opts.limit || 5, 76 | wait: opts.wait || 2 77 | } 78 | 79 | const response = await this.sendPost('contacts/search', undefined, cleanOpts) 80 | if (!response.body) { 81 | throw Error('Empty response stream') 82 | } 83 | return streamHandler(response.body as ReadableStream) 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/modules/__test__/__static__/files.ts: -------------------------------------------------------------------------------- 1 | import { FilesList, Files, Keys, Directory } from '../../../models' 2 | 3 | export namespace files { 4 | export const get: Files = { 5 | block: 'QmeU7xSPq6Bewh6PdwHqGHhoErFH1UBQV5Aqb7VYP9ugey', 6 | comments: [], 7 | caption: 'caption', 8 | date: '2019-04-01T21:27:37.422127Z', 9 | files: [ 10 | { 11 | index: 0, 12 | file: { 13 | name: '', 14 | added: '', 15 | checksum: '', 16 | hash: '', 17 | key: '', 18 | media: '', 19 | meta: {}, 20 | mill: '', 21 | opts: '', 22 | size: '', 23 | source: '', 24 | targets: [] 25 | }, 26 | links: { 27 | large: { 28 | name: '', 29 | added: '2019-04-01T21:27:37.378108Z', 30 | checksum: 'B9RebNwLPTXQJymnjEJsYsFuS1dAuJBpBnHT1sB2LLVQ', 31 | hash: 'QmVhJPrbb6iD9BYkUcqyDepuAw1wGsudhZLKapfZGCuhKG', 32 | key: '', 33 | media: 'image/png', 34 | meta: { 35 | height: 128, 36 | width: 128 37 | }, 38 | mill: '/image/resize', 39 | opts: 'Atk8eCEgX98dPBCieRvUUBkH1nA7YiX6AxK1ZjbA6mY', 40 | size: '7270', 41 | source: 'B9RebNwLPTXQJymnjEJsYsFuS1dAuJBpBnHT1sB2LLVQ', 42 | targets: [ 43 | 'QmUMWopyRDwZYJznyyhp1TmbAJhLV3z2Xe9dSeqr6LNvV5' 44 | ] 45 | }, 46 | small: { 47 | name: '', 48 | added: '2019-04-01T21:27:37.387214Z', 49 | checksum: '5i7tM447RozmhG1zbYvWGb8UXRRa57DSKJAJSLiUsSYJ', 50 | hash: 'QmY2cqoiCNwNcf6Sf6tEUbA76V64zkcRbkdB8JRAWYafH2', 51 | key: '', 52 | media: 'image/png', 53 | meta: { 54 | height: 100, 55 | width: 100 56 | }, 57 | mill: '/image/resize', 58 | opts: 'Av7wdZtZ5P8rWQtuTkUPQ8iBmRK1PyfxPcVeDtAjb2uR', 59 | size: '8125', 60 | source: 'B9RebNwLPTXQJymnjEJsYsFuS1dAuJBpBnHT1sB2LLVQ', 61 | targets: [ 62 | 'QmUMWopyRDwZYJznyyhp1TmbAJhLV3z2Xe9dSeqr6LNvV5' 63 | ] 64 | } 65 | } 66 | } 67 | ], 68 | likes: [], 69 | target: 'QmUMWopyRDwZYJznyyhp1TmbAJhLV3z2Xe9dSeqr6LNvV5', 70 | threads: [ 71 | '12D3KooWRW4Ewc1tYCLA2xj6crywHoCxxBngenJNUsPyzHGm3meL' 72 | ], 73 | user: { 74 | address: 'P4RaCnWZYhKCztWK7mii1WQZmXVAtBVdRAyBU6Em51agxTfW', 75 | avatar: 'QmUMWopyRDwZYJznyyhp1TmbAJhLV3z2Xe9dSeqr6LNvV5', 76 | name: 'displayname' 77 | } 78 | } 79 | export const list: FilesList = { 80 | items: [files.get] 81 | } 82 | export const dir: Directory = { 83 | files: {...files.get.files[0].links} 84 | } 85 | export const keys: Keys = { 86 | files: { 87 | '/0/large/': 'blah', 88 | '/0/small/': 'blah' 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/modules/snapshots.ts: -------------------------------------------------------------------------------- 1 | import { API } from '../core/api' 2 | import { streamHandler } from '../helpers/handlers' 3 | import { QueryResult, Thread } from '../models' 4 | import { ReadableStream } from 'web-streams-polyfill/ponyfill' 5 | 6 | /** 7 | * Snapshots is an API module for managing thread snapshots 8 | * 9 | * @extends API 10 | */ 11 | export default class Snapshots extends API { 12 | 13 | /** 14 | * Snapshot all threads and push to registered cafes 15 | * 16 | * @returns Whether the snapshot process was successfull 17 | */ 18 | async create() { 19 | const response = await this.sendPost('snapshots') 20 | return response.status === 201 21 | } 22 | 23 | /** 24 | * Search the network for thread snapshots 25 | * 26 | * @param wait Stops searching after 'wait' seconds have elapsed (max 30 default 2) 27 | * @returns A ReadableStream of QueryResult objects. 28 | */ 29 | async search(wait?: number) { 30 | const response = await this.sendPost('snapshots/search', undefined, { wait: wait || 2 }) 31 | if (!response.body) { 32 | throw Error('Empty response stream') 33 | } 34 | return streamHandler(response.body as ReadableStream) 35 | } 36 | 37 | /** 38 | * Apply a single thread snapshot 39 | * @param id The snapshot id (omit to find and apply all snapshots) 40 | * @param wait Stops searching after 'wait' seconds have elapsed (max 30 default 2) 41 | * @returns A ReadableStream of QueryResult objects. 42 | */ 43 | async apply(id?: string, wait?: number) { 44 | const stream = await this.search(wait) 45 | // For cancellation 46 | let isReader: any 47 | let cancellationRequest = false 48 | const self = this 49 | return new ReadableStream({ 50 | start(controller) { 51 | const reader = stream.getReader() 52 | isReader = reader 53 | const processResult = (result: ReadableStreamReadResult) => { 54 | if (result.done) { 55 | if (cancellationRequest) { 56 | return // Immediately exit 57 | } 58 | controller.close() 59 | return 60 | } 61 | try { 62 | if (id === undefined || result.value.id === id) { 63 | self.applySnapshot(result.value).then((success) => { 64 | if (success) { 65 | controller.enqueue(result.value) 66 | } else { 67 | throw new Error('Unable to apply snapshot') 68 | } 69 | }) 70 | } 71 | } catch (e) { 72 | controller.error(e) 73 | cancellationRequest = true 74 | reader.cancel(undefined) 75 | return 76 | } 77 | reader.read().then(processResult) 78 | } 79 | reader.read().then(processResult) 80 | }, 81 | cancel(reason?: string) { 82 | cancellationRequest = true 83 | isReader.cancel(reason) 84 | } 85 | }) 86 | } 87 | 88 | async applySnapshot(snapshot: QueryResult) { 89 | const snap: Thread = snapshot.value 90 | const response = await this.sendPut(`threads/${snap.id}`, 91 | undefined, undefined, snap) 92 | return response.status === 204 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/modules/__test__/__static__/responses.ts: -------------------------------------------------------------------------------- 1 | import { Peer, Summary, Block, Text, Like, Comment, User, Contact, CafeSession, TextList } from '../../../models' 2 | 3 | export const user: User = { 4 | address: 'P4RaCnWZYhKCztWK7mii1WQZmXVAtBVdRAyBU6Em51agxTfW', 5 | name: 'displayname', 6 | avatar: 'QmUMWopyRDwZYJznyyhp1TmbAJhLV3z2Xe9dSeqr6LNvV5' 7 | } 8 | 9 | export const ignore: Block = { 10 | author: '12D3KooWJKPeAzZ6Sig3p1BwMdz5aMmnKhhgEdgJqn4wd1dsTMZB', 11 | date: '2019-04-01T20:54:46.359206Z', 12 | id: 'id', 13 | body: '', 14 | parents: [ 15 | 'id' 16 | ], 17 | target: 'ignore-target', 18 | thread: 'thread', 19 | type: Block.BlockType.IGNORE, 20 | user 21 | } 22 | 23 | export namespace account { 24 | export const contact: Contact = { 25 | address: 'P4RaCnWZYhKCztWK7mii1WQZmXVAtBVdRAyBU6Em51agxTfW', 26 | name: 'displayname', 27 | avatar: 'avatar', 28 | peers: [], 29 | threads: [] 30 | } 31 | export const address = 'P4RaCnWZYhKCztWK7mii1WQZmXVAtBVdRAyBU6Em51agxTfW' 32 | export const seed = 'SY4uXRezeDqXL6nhYLfJPBVTyhqJuFBGYYGqdumHT9bCNgEd' 33 | } 34 | 35 | export namespace cafes { 36 | export const add: CafeSession = { 37 | access: 'access', 38 | cafe: { 39 | address: 'address', 40 | api: 'v0', 41 | node: '0.1.10', 42 | peer: 'peer', 43 | protocol: '/textile/cafe/1.0.0', 44 | swarm: [ 45 | '/ip4/xx.xxx.xxx.xx/tcp/4001', 46 | '/ip4/xx.xxx.xxx.xx/tcp/8081/ws' 47 | ], 48 | url: 'http://xx.xxx.xxx.xx:40601' 49 | }, 50 | exp: '2019-04-25T16:53:32.525867505Z', 51 | id: 'id', 52 | refresh: 'refresh', 53 | rexp: '2019-05-23T16:53:32.525867505Z', 54 | subject: 'subject', 55 | type: 'JWT' 56 | } 57 | } 58 | 59 | export namespace comments { 60 | export const add: Comment = { 61 | body: 'comment', 62 | date: '2019-03-28T17:27:08.767878Z', 63 | id: 'id', 64 | user, 65 | target: { 66 | block: 'block', 67 | thread: 'thread', 68 | payload: {} 69 | } 70 | } 71 | } 72 | 73 | export namespace likes { 74 | export const add: Like = { 75 | date: '2019-03-28T17:35:14.718284Z', 76 | id: 'id', 77 | user, 78 | target: { 79 | block: 'block', 80 | thread: 'thread', 81 | payload: {} 82 | } 83 | } 84 | } 85 | 86 | export namespace messages { 87 | export const add: Text = { 88 | block: 'block', 89 | body: 'message', 90 | comments: [], 91 | date: '2019-03-28T17:24:25.796298Z', 92 | likes: [], 93 | user 94 | } 95 | export const list: TextList = { 96 | items: [ 97 | messages.add 98 | ] 99 | } 100 | } 101 | 102 | export namespace utils { 103 | export const summary: Summary = { 104 | id: '12D3KooWJKPeAzZ6Sig3p1BwMdz5aMmnKhhgEdgJqn4wd1dsTMZB', 105 | address: 'P4RaCnWZYhKCztWK7mii1WQZmXVAtBVdRAyBU6Em51agxTfW', 106 | thread_count: 0, 107 | account_peer_count: 0, 108 | files_count: 0, 109 | contact_count: 0 110 | } 111 | } 112 | 113 | export namespace profile { 114 | export const get: Peer = { 115 | ...user, 116 | id: 'id', 117 | inboxes: [], 118 | created: '2019-04-01T20:54:46.359206Z', 119 | updated: '2019-04-01T20:54:46.359206Z' 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at contact@textile.io. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at [http://contributor-covenant.org/version/1/4][version] 72 | 73 | [homepage]: http://contributor-covenant.org 74 | [version]: http://contributor-covenant.org/version/1/4/ 75 | -------------------------------------------------------------------------------- /src/helpers/schema-miller.ts: -------------------------------------------------------------------------------- 1 | import toposort from 'toposort' 2 | import { Link, Directory, Node, FileIndex } from '../models' 3 | 4 | export type MillOpts = Record 5 | 6 | export type MillFunction = (mill: string, opts: MillOpts, form: any, headers: { [k: string]: string }) => Promise 7 | 8 | export default class SchemaMiller { 9 | static sortLinksByDependency(links: { [k: string]: Link }) { 10 | // Create an array of [ name, dependency ] for sorting 11 | const linkAndDepends: ReadonlyArray<[string, string]> = Object.entries(links).map(([name, link]) => { 12 | const pair: [string, string] = [name, link.use] 13 | return pair 14 | }) 15 | // Sort the array into an execution order 16 | const sorted = [...toposort(linkAndDepends)].reverse() 17 | // Refill the items in the sorted array 18 | return ( 19 | sorted 20 | // File is the original form so we don't need a method 21 | .filter((name: string) => { 22 | return name !== ':file' 23 | }) 24 | // TODO: `Name` should eventually be converted to lowercase 25 | .map((name: string) => ({ name, link: links[name] })) 26 | ) 27 | } 28 | 29 | static normalizeOptions(info: Node | Link) { 30 | const opts: MillOpts = info.opts || {} 31 | // Check for top level opts 32 | opts.plaintext = (info.plaintext || false).toString() 33 | opts.pin = (info.pin || false).toString() 34 | return opts 35 | } 36 | 37 | static resolveDependency(method: MillOpts, payloadsByName: Directory) { 38 | let use 39 | 40 | // Convert 'use' to hash of payload 41 | if (method.use && method.use !== ':file') { 42 | // TODO: This is a hack, should use multihash JS lib in future 43 | use = (method.use.length === 46 && method.use.startsWith('Qm')) ? 44 | method.use : 45 | payloadsByName.files[method.use].hash 46 | } 47 | 48 | const resolvedMethod = { ...method } 49 | resolvedMethod.use = use || '' 50 | return resolvedMethod 51 | } 52 | 53 | static async mill(payload: any, node: Node, remoteMill: MillFunction): Promise { 54 | 55 | const dir: Directory = { files: {} } 56 | 57 | // Traverse the schema and collect generated files 58 | if (node.mill) { 59 | const normal = SchemaMiller.normalizeOptions(node) 60 | const resolved = SchemaMiller.resolveDependency(normal, dir) 61 | let form 62 | if (resolved.use) { 63 | form = undefined 64 | } else if (typeof payload === 'function') { 65 | form = payload() 66 | } else { 67 | form = payload 68 | } 69 | const file = await remoteMill(node.mill, resolved, form, {}) 70 | dir.files[':single'] = file 71 | } else if (node.links) { 72 | // Determine order 73 | const steps = SchemaMiller.sortLinksByDependency(node.links) 74 | // Send each link 75 | // eslint-disable-next-line no-restricted-syntax 76 | for (const step of steps) { 77 | let form 78 | const normal = SchemaMiller.normalizeOptions(step.link || {}) 79 | const resolved = SchemaMiller.resolveDependency(normal, dir) 80 | if (resolved.use) { 81 | // It's an existing 'file', hash will pass as payload, so don't send file again 82 | form = undefined 83 | } else if (typeof payload === 'function') { 84 | form = payload() 85 | } else { 86 | form = payload 87 | } 88 | // Must be synchronous for dependencies 89 | const file = await remoteMill(step.link.mill, resolved, form, {}) 90 | dir.files[step.name] = file 91 | } 92 | } 93 | return dir 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/modules/files.ts: -------------------------------------------------------------------------------- 1 | import { API, DEFAULT_API_OPTIONS } from '../core/api' 2 | import { ApiOptions, Node, FilesList, Files as FilesType, Keys, DirectoryList, Block } from '../models' 3 | import SchemaMiller, { MillOpts } from '../helpers/schema-miller' 4 | import Mills from './mills' 5 | import Threads from './threads' 6 | 7 | /** 8 | * Files is an API module for managing Textile files 9 | * 10 | * @extends {API} 11 | */ 12 | export default class Files extends API { 13 | mills: Mills 14 | threads: Threads 15 | constructor(opts: ApiOptions = DEFAULT_API_OPTIONS) { 16 | super(opts) 17 | this.mills = new Mills(opts) 18 | this.threads = new Threads(opts) 19 | } 20 | 21 | /** 22 | * Get a paginated array of files. 23 | * 24 | * @param thread Thread ID (can also use ‘default’). Omit for all 25 | * @param offset Offset ID to start listing from. Omit for latest 26 | * @param limit List page size (default 5) 27 | * @returns An array of Thread objects 28 | */ 29 | async list(thread?: string, offset?: string, limit?: number) { 30 | const response = await this.sendGet('files', undefined, { 31 | thread: thread || '', 32 | offset: offset || '', 33 | limit: limit || 5 34 | }) 35 | return response.json() as Promise 36 | } 37 | 38 | /** 39 | * Retrieves file encryption/decryption keys under the given target 40 | * 41 | * Note that the target id is _not_ the same as the block id. The target is the actual target 42 | * file object. 43 | * 44 | * @param target ID of the target file 45 | * @returns An array of file keys 46 | */ 47 | async keys(target: string) { 48 | const response = await this.sendGet(`keys/${target}`) 49 | return response.json() as Promise 50 | } 51 | 52 | /** 53 | * Ignores a thread file by its block ID 54 | * 55 | * This adds an 'ignore' thread block targeted at the file. 56 | * Ignored blocks are by default not returned when listing. 57 | * 58 | * @param id ID of the thread file 59 | * @returns The added ignore block 60 | */ 61 | async ignore(id: string) { 62 | const response = await this.sendDelete(`blocks/${id}`) 63 | return response.json() as Promise 64 | } 65 | 66 | /** 67 | * Add a file to a thread in your Textile node 68 | * 69 | * @param thread Id of the thread 70 | * @param file A FormData object or a function for creating a FormData object 71 | * @param caption Caption to associated with the added file object 72 | * @returns An array of created File objects 73 | */ 74 | async add(file: any, caption: string, thread?: string): Promise { 75 | if (!thread) { 76 | thread = 'default' 77 | } 78 | // Fetch schema (will throw if it doesn't have a schema node) 79 | const schemaNode: Node = (await this.threads.get(thread)).schema_node 80 | if (!schemaNode) { 81 | throw new Error('A thread schema is required before adding files to a thread.') 82 | } 83 | 84 | // Mill the file(s) before adding it 85 | const dir = await SchemaMiller.mill( 86 | file, 87 | schemaNode, 88 | async (mill: string, opts: MillOpts, form: any, headers: { [key: string]: string }) => { 89 | const file = await this.mills.run(mill, opts, form, headers) 90 | return file 91 | } 92 | ) 93 | // TODO: Do more than just wrap dirs in list 94 | const dirs: DirectoryList = { 95 | items: [dir] 96 | } 97 | const response = await this.sendPost( 98 | `threads/${thread}/files`, undefined, { caption }, dirs 99 | ) 100 | return response.json() 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/modules/blocks.ts: -------------------------------------------------------------------------------- 1 | import { API, DEFAULT_API_OPTIONS } from '../core/api' 2 | import { ApiOptions, Block, BlockList } from '../models' 3 | import Threads from './threads' 4 | 5 | const dot = '%2E' // needed as curl shortens /. to / 6 | 7 | /** 8 | * Blocks is an API module for managing Textile blocks 9 | * 10 | * Blocks are the raw components in a thread. Think of them as an append-only log of thread updates 11 | * where each update is hash-linked to its parent(s). New / recovering peers can sync history by 12 | * simply traversing the hash tree. 13 | * 14 | * There are several block types: 15 | * 16 | * - MERGE: 3-way merge added. 17 | * - IGNORE: A block was ignored. 18 | * - FLAG: A block was flagged. 19 | * - JOIN: Peer joined. 20 | * - ANNOUNCE: Peer set username / avatar / inbox addresses 21 | * - LEAVE: Peer left. 22 | * - TEXT: Text message added. 23 | * - FILES: File(s) added. 24 | * - COMMENT: Comment added to another block. 25 | * - LIKE: Like added to another block. 26 | * 27 | * @extends {API} 28 | */ 29 | export default class Blocks extends API { 30 | threads: Threads 31 | constructor(opts: ApiOptions = DEFAULT_API_OPTIONS) { 32 | super(opts) 33 | this.threads = new Threads(opts) 34 | } 35 | 36 | /** 37 | * Retrieves a block by ID 38 | * 39 | * @param id ID of the target block 40 | * @returns The block object 41 | */ 42 | async meta(id: string) { 43 | const response = await this.sendGet(`blocks/${id}/meta`) 44 | return response.json() as Promise 45 | } 46 | 47 | /** 48 | * Get a paginated array of files. 49 | * 50 | * @param thread Thread ID (can also use ‘default’). Omit for all 51 | * @param offset Offset ID to start listing from. Omit for latest 52 | * @param limit List page size (default 5) 53 | * @returns An array of Block objects 54 | */ 55 | async list(thread?: string, offset?: string, limit?: number) { 56 | const response = await this.sendGet('blocks', undefined, { 57 | thread: thread || '', 58 | offset: offset || '', 59 | limit: limit || 5 60 | }) 61 | return response.json() as Promise 62 | } 63 | 64 | /** 65 | * Ignores a block by its ID 66 | * 67 | * @param id ID of the block 68 | * @returns The added ignore block 69 | */ 70 | async ignore(id: string) { 71 | const response = await this.sendDelete(`blocks/${id}`) 72 | return response.json() as Promise 73 | } 74 | 75 | /** 76 | * Get the decrypted file content of a file within a files block 77 | * 78 | * @param id ID of the target block 79 | * @param index Index of the target file (defaults to '0') 80 | * @param path Path of the target file under the index (e.g., 'small') 81 | * @returns The file contents as an arrayBuffer (for a blob, use `file.content()`) 82 | */ 83 | async fileContent(id: string, index?: string, path?: string) { 84 | const response = await this.sendGet(`blocks/${id}/files/${index || '0'}/${path || dot}/content`) 85 | return response.arrayBuffer() 86 | } 87 | 88 | /** 89 | * Get the metadata of a file within a files block 90 | * 91 | * @param id ID of the target block 92 | * @param index Index of the target file (defaults to '0') 93 | * @param path Path of the target file under the index (e.g., 'small') 94 | * @returns The file contents as an arrayBuffer (for a blob, use `file.meta()`) 95 | */ 96 | async fileMeta(id: string, index?: string, path?: string) { 97 | const response = await this.sendGet(`blocks/${id}/files/${index || '0'}/${path || dot}/meta`) 98 | return response.arrayBuffer() 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/modules/ipfs.ts: -------------------------------------------------------------------------------- 1 | import { API } from '../core/api' 2 | import { readableNodeToWeb } from '../helpers/handlers' 3 | import { MobileQueryEvent } from '../models' 4 | import { ReadableStream } from 'web-streams-polyfill/ponyfill' 5 | import KSUID from '@therebel/ksuid' 6 | 7 | /** 8 | * IPFS is an API module for working with an underlying IPFS peer 9 | * 10 | * @extends API 11 | */ 12 | export default class IPFS extends API { 13 | /** 14 | * Retrieves underlying IPFS peer ID 15 | * @returns The underlying IPFS peer ID 16 | */ 17 | async id() { 18 | const response = await this.sendGet(`ipfs/id`) 19 | return response.text() as Promise 20 | } 21 | 22 | /** 23 | * Lists the set of peers to which this node is connected 24 | * 25 | * @param verbose Display all extra information 26 | * @param latency Also list information about latency to each peer 27 | * @param streams Also list information about open streams for each peer 28 | * @param direction Also list information about the direction of connection 29 | */ 30 | async peers(verbose?: boolean, latency?: boolean, streams?: boolean, direction?: boolean) { 31 | const response = await this.sendGet( 32 | 'ipfs/swarm/peers', 33 | undefined, 34 | { 35 | verbose: (!!verbose).toString(), 36 | latency: (!!latency).toString(), 37 | streams: (!!streams).toString(), 38 | direction: (!!direction).toString() 39 | } 40 | ) 41 | return response.json() 42 | } 43 | 44 | /** 45 | * Retrieves the data behind an IPFS CID path 46 | * 47 | * @param path IPFS/IPNS CID path 48 | * @param key Key to decrypt the underlying data on-the-fly 49 | * @returns The underlying data behind the given IPFS CID path 50 | */ 51 | async cat(path: string, key?: string) { 52 | const response = await this.sendGet(`ipfs/cat/${path}`, undefined, { key: key || '' }) 53 | return response.blob() 54 | } 55 | 56 | /** 57 | * Opens a new direct connection to a peer using an IPFS multiaddr 58 | * 59 | * @param addr Peer IPFS multiaddr 60 | * @returns Whether the peer swarm connect was successfull 61 | */ 62 | async connect(addr: string) { 63 | const response = await this.sendPost(`ipfs/swarm/connect`, [addr]) 64 | return response.status === 200 65 | } 66 | 67 | /** 68 | * Publishes a message to a given pubsub topic 69 | * 70 | * @param topic The topic to publish to 71 | * @param data The payload of message to publish 72 | * @returns Whether the publish was successfull 73 | */ 74 | async pubsubPub(topic: string, data: string | object) { 75 | const response = await this.sendPost(`ipfs/pubsub/pub/${topic}`, undefined, undefined, data, undefined, typeof data === 'string') 76 | return response.status === 200 77 | } 78 | 79 | /** 80 | * Subscribes to messages on a given topic with GET 81 | * 82 | * @param topic The ipfs pubsub sub topic 83 | * @returns A ReadableStream of ArrayBuffer 84 | */ 85 | async pubsubSubGet(topic: string, queryId: string) { 86 | const response = await this.sendGet(`ipfs/pubsub/sub/${topic}`, undefined, { queryId }) 87 | if (!response.body) { 88 | throw Error('Empty response stream') 89 | } 90 | return readableNodeToWeb(response.body as ReadableStream) 91 | } 92 | 93 | /** 94 | * Subscribes to messages on a given topic with EventSource or GET 95 | * 96 | * @param topic The ipfs pubsub sub topic 97 | * @param useEventSource Whether to use EventSource or GET 98 | * @returns An object with queryId and EventSource or a ReadableStream of ArrayBuffer 99 | */ 100 | async pubsubSub(topic: string, useEventSource?: boolean) { 101 | const ksuidFromAsync = await KSUID.random() 102 | const queryId = ksuidFromAsync.string 103 | const queryHandle = useEventSource ? this.sendEventSource(`ipfs/pubsub/sub/${topic}`, undefined, { events: true, queryId }) : await this.pubsubSubGet(topic, queryId) 104 | return { 105 | queryId, 106 | queryHandle 107 | } 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/helpers/handlers.ts: -------------------------------------------------------------------------------- 1 | import { Readable } from 'stream' 2 | import { ReadableStream } from 'web-streams-polyfill/ponyfill' 3 | 4 | // polyfill TextDecoder to be backward compatible with older 5 | // nodejs that doesn't expose TextDecoder as a global variable 6 | if (typeof TextDecoder === 'undefined' && typeof require !== 'undefined') { 7 | (global as any).TextDecoder = require('util').TextDecoder 8 | } 9 | 10 | // https://github.com/gwicke/node-web-streams 11 | export const readableNodeToWeb = (nodeStream: Readable | ReadableStream) => { 12 | if (!(nodeStream instanceof Readable)) { 13 | return nodeStream 14 | } 15 | return new ReadableStream({ 16 | start(controller) { 17 | nodeStream.pause() 18 | nodeStream.on('data', (chunk: T) => { 19 | controller.enqueue(chunk) 20 | nodeStream.pause() 21 | }) 22 | nodeStream.on('end', () => controller.close()) 23 | nodeStream.on('error', (e: Error) => controller.error(e)) 24 | }, 25 | pull(_controller) { 26 | nodeStream.resume() 27 | }, 28 | cancel(_reason) { 29 | nodeStream.pause() 30 | } 31 | }) 32 | } 33 | 34 | export class NodeReadable extends Readable { 35 | _webStream: ReadableStream 36 | _reader: ReadableStreamDefaultReader 37 | _reading: boolean 38 | constructor(webStream: ReadableStream, options?: {}) { 39 | super(options) 40 | this._webStream = webStream 41 | this._reader = webStream.getReader() 42 | this._reading = false 43 | } 44 | 45 | _read(size: number) { 46 | if (this._reading) { 47 | return 48 | } 49 | this._reading = true 50 | const doRead = (size: number) => { 51 | this._reader.read() 52 | .then((res) => { 53 | if (res.done) { 54 | // tslint:disable-next-line:no-null-keyword 55 | this.push(null) 56 | return 57 | } 58 | if (this.push(res.value)) { 59 | return doRead(size) 60 | } else { 61 | this._reading = false 62 | } 63 | }) 64 | } 65 | doRead(size) 66 | } 67 | } 68 | 69 | export const readableWebToNode = (webStream: ReadableStream) => { 70 | return new NodeReadable(webStream) 71 | } 72 | 73 | // https://github.com/canjs/can-ndjson-stream 74 | export const streamHandler = (response: ReadableStream) => { 75 | // For cancellation 76 | let isReader: any 77 | let cancellationRequest = false 78 | return new ReadableStream({ 79 | start(controller) { 80 | const reader = readableNodeToWeb(response).getReader() 81 | isReader = reader 82 | const decoder = new TextDecoder() 83 | let dataBuffer = '' 84 | 85 | const processResult = (result: ReadableStreamReadResult) => { 86 | if (result.done) { 87 | if (cancellationRequest) { 88 | return // Immediately exit 89 | } 90 | dataBuffer = dataBuffer.trim() 91 | if (dataBuffer.length !== 0) { 92 | try { 93 | const dataLine: T = JSON.parse(dataBuffer) 94 | controller.enqueue(dataLine) 95 | } catch (e) { 96 | controller.error(e) 97 | return 98 | } 99 | } 100 | controller.close() 101 | return 102 | } 103 | const data = decoder.decode(result.value, { stream: true }) 104 | dataBuffer += data 105 | const lines = dataBuffer.split('\n') 106 | for (const line of lines) { 107 | const l = line.trim() 108 | if (l.length > 0) { 109 | try { 110 | controller.enqueue(JSON.parse(l)) 111 | } catch (e) { 112 | controller.error(e) 113 | cancellationRequest = true 114 | reader.cancel(undefined) 115 | return 116 | } 117 | } 118 | } 119 | dataBuffer = lines.length > 1 ? lines[lines.length - 1] : '' 120 | reader.read().then(processResult) 121 | } 122 | reader.read().then(processResult) 123 | }, 124 | cancel(reason?: string) { 125 | cancellationRequest = true 126 | isReader.cancel(reason) 127 | } 128 | }) 129 | } 130 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import Account from './modules/account' 2 | import Blocks from './modules/blocks' 3 | import Cafes from './modules/cafes' 4 | import Config from './modules/config' 5 | import Comments from './modules/comments' 6 | import Contacts from './modules/contacts' 7 | import Events from './modules/events' 8 | import Feed from './modules/feed' 9 | import File from './modules/file' 10 | import Files from './modules/files' 11 | import Invites from './modules/invites' 12 | import IPFS from './modules/ipfs' 13 | import Likes from './modules/likes' 14 | import Logs from './modules/logs' 15 | import Messages from './modules/messages' 16 | import Notifications from './modules/notifications' 17 | import Profile from './modules/profile' 18 | import Schemas from './modules/schemas' 19 | import Observe from './modules/observe' 20 | import Threads from './modules/threads' 21 | import Tokens from './modules/tokens' 22 | import Utils from './modules/utils' 23 | import { TextileOptions } from './models' 24 | import { DEFAULT_API_OPTIONS as defaults } from './core/api' 25 | export * from './models' 26 | 27 | /** 28 | * Textile is the main client class 29 | */ 30 | export class Textile { 31 | /** 32 | * Returns a new instance of Textile 33 | * 34 | * @param [options] Textile TextileOptions object 35 | */ 36 | static create(options?: TextileOptions) { 37 | return new this(options) 38 | } 39 | /** @property {Account} account - Manage Textile node account */ 40 | account: Account 41 | /** @property {Blocks} blocks - Manage Textile Blocks */ 42 | blocks: Blocks 43 | /** @property {Cafes} account - Manage Textile Cafes */ 44 | cafes: Cafes 45 | /** @property {Config} config - Manage Textile Config settings */ 46 | config: Config 47 | /** @property {Comments} comments - Manage Textile block Comments */ 48 | comments: Comments 49 | /** @property {Contacts} contacts - Manage Textile peer Contacts */ 50 | contacts: Contacts 51 | /** @property {Events} events - Manage the Textile Events */ 52 | events: Events 53 | /** @property {Feed} feed - Manage the Textile Feed */ 54 | feed: Feed 55 | /** @property {File} file - Manage a Textile File */ 56 | file: File 57 | /** @property {Files} files - Manage Textile Files */ 58 | files: Files 59 | /** @property {Invites} invites - Manage Textile Invites */ 60 | invites: Invites 61 | /** @property {IPFS} ipfs - Manage the underlying IPFS peer */ 62 | ipfs: IPFS 63 | /** @property {Likes} likes - Manage Textile block Likes */ 64 | likes: Likes 65 | /** @property {Logs} logs - Manage Textile subsystem logs */ 66 | logs: Logs 67 | /** @property {Messages} messages - Manage Textile thread Messages */ 68 | messages: Messages 69 | /** @property {Notifications} notifications - Manage Textile Notifications */ 70 | notifications: Notifications 71 | /** @property {Profile} profile - Manage a Textile node's public profile */ 72 | profile: Profile 73 | /** @property {Schemas} schemas - Manage Textile Schemas */ 74 | schemas: Schemas 75 | /** @property {Observe} observe - Observe (real-time) thread updates */ 76 | observe: Observe 77 | /** @property {Threads} threads - Manage Textile Threads */ 78 | threads: Threads 79 | /** @property {Tokens} tokens - Manage Textile Threads */ 80 | tokens: Tokens 81 | /** @property {Utils} utils - Get information about the Textile node */ 82 | utils: Utils 83 | constructor(options?: TextileOptions) { 84 | const _options = defaults 85 | if (options && options.port !== undefined) { 86 | _options.port = options.port 87 | } 88 | if (options && options.url !== undefined) { 89 | _options.url = options.url 90 | } 91 | if (options && options.version !== undefined) { 92 | _options.version = options.version 93 | } 94 | this.account = new Account(_options) 95 | this.blocks = new Blocks(_options) 96 | this.cafes = new Cafes(_options) 97 | this.config = new Config(_options) 98 | this.comments = new Comments(_options) 99 | this.contacts = new Contacts(_options) 100 | this.events = new Events(_options) 101 | this.feed = new Feed(_options) 102 | this.file = new File(_options) 103 | this.files = new Files(_options) 104 | this.invites = new Invites(_options) 105 | this.ipfs = new IPFS(_options) 106 | this.likes = new Likes(_options) 107 | this.logs = new Logs(_options) 108 | this.messages = new Messages(_options) 109 | this.notifications = new Notifications(_options) 110 | this.profile = new Profile(_options) 111 | this.schemas = new Schemas(_options) 112 | this.observe = new Observe(_options) 113 | this.threads = new Threads(_options) 114 | this.tokens = new Tokens(_options) 115 | this.utils = new Utils(_options) 116 | } 117 | } 118 | 119 | export default new Textile() 120 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | The following is a set of guidelines for contributing to Textile-related projects, which are hosted in the [Textile organization](https://github.com/textileio). These are just guidelines, not rules. Use your best judgment, and 4 | feel free to propose changes to this document in a pull request. 5 | 6 | Note that Textile is an evolving project, so expect things to change over time as the team learns, listens and refines how we work with the community. 7 | 8 | #### Table Of Contents 9 | 10 | [What should I know before I get started?](#what-should-i-know-before-i-get-started) 11 | * [Code of Conduct](#code-of-conduct) 12 | 13 | [How Can I Contribute?](#how-can-i-contribute) 14 | * [Reporting Bugs](#reporting-bugs) 15 | * [Suggesting Enhancements](#suggesting-enhancements) 16 | 17 | [Additional Notes](#additional-notes) 18 | 19 | ## What should I know before I get started? 20 | 21 | ### Code of Conduct 22 | 23 | This project adheres to the Contributor Covenant [code of conduct](./CODE_OF_CONDUCT.md). 24 | By participating, you are expected to uphold this code. 25 | Please report unacceptable behavior to [contact@textile.io](mailto:contact@textile.io). 26 | 27 | ## How Can I Contribute? 28 | 29 | ### Developing locally 30 | 31 | #### How to Setup 32 | 33 | Generally, Textile projects can be initialized with something like: 34 | 35 | **Step 1:** git clone this repo: 36 | 37 | **Step 2:** cd to the cloned repo: 38 | 39 | **Step 3:** Install the Application with `yarn` or `npm i` 40 | 41 | See each indiviual project's `README` for details. 42 | 43 | ### Reporting Bugs 44 | 45 | This section guides you through submitting a bug report for any Textile repo. 46 | Following these guidelines helps maintainers and the community understand your report, reproduce the behavior, and find related reports. 47 | 48 | Before creating bug reports, please check [this list](../../labels/bug) as you might find out that you don't need to create one. When you are creating a bug report, please [include as many details as possible](#how-do-i-submit-a-good-bug-report). 49 | 50 | #### Before Submitting A Bug Report 51 | 52 | **Perform a [cursory search](../../labels/bug)** to see if the problem has already been reported. If it does exist, add a :thumbsup: to the issue to indicate this is also an issue for you, and add a comment to the existing issue if there is extra information you can contribute. 53 | 54 | #### How Do I Submit A (Good) Bug Report? 55 | 56 | Bugs are tracked as [GitHub issues](https://guides.github.com/features/issues/). 57 | 58 | Simply create an issue on the [Textile issue tracker](../../issues). 59 | 60 | The information we are interested in includes: 61 | 62 | - details about your environment - which build, which operating system 63 | - details about reproducing the issue - what steps to take, what happens, how 64 | often it happens 65 | - other relevant information - log files, screenshots, etc. 66 | 67 | ### Suggesting Enhancements 68 | 69 | This section guides you through submitting an enhancement suggestion for this project, including completely new features and minor improvements to existing functionality. Following these guidelines helps maintainers and the community understand your suggestion :pencil: and find related suggestions :mag_right:. 70 | 71 | Before creating enhancement suggestions, please check [this list](../..//labels/bug) 72 | as you might find out that you don't need to create one. When you are creating 73 | an enhancement suggestion, please [include as many details as possible](#how-do-i-submit-a-good-enhancement-suggestion). Include the steps 74 | that you imagine you would take if the feature you're requesting existed. 75 | 76 | #### Before Submitting An Enhancement Suggestion 77 | 78 | **Perform a [cursory search](../../labels/enhancement)** 79 | to see if the enhancement has already been suggested. If it has, add a 80 | :thumbsup: to indicate your interest in it, or comment if there is additional 81 | information you would like to add. 82 | 83 | #### How Do I Submit A (Good) Enhancement Suggestion? 84 | 85 | Enhancement suggestions are tracked as [GitHub issues](https://guides.github.com/features/issues/). 86 | 87 | Simply create an issue on the [Textile issue tracker](../..//issues) 88 | and provide the following information: 89 | 90 | * **Use a clear and descriptive title** for the issue to identify the 91 | suggestion. 92 | * **Provide a step-by-step description of the suggested enhancement** in as 93 | much detail as possible. This additional context helps the maintainers to 94 | understand the enhancement from your perspective 95 | * **Explain why this enhancement would be useful** to Textile users. 96 | * **Include screenshots and animated GIFs** if relevant to help you demonstrate 97 | the steps or point out the part of Textile which the suggestion is 98 | related to. 99 | * **List some other applications where this enhancement exists, if applicable.** 100 | 101 | ## Additional Notes 102 | 103 | More to be added 104 | -------------------------------------------------------------------------------- /src/models/view_pb.ts: -------------------------------------------------------------------------------- 1 | /* GENERATED FROM view.proto. DO NOT EDIT MANUALLY. */ 2 | /* tslint:disabled */ 3 | /* eslint-disable */ 4 | 5 | import * as model from './model_pb' 6 | 7 | export interface AddThreadConfig { 8 | key: string 9 | name: string 10 | schema: AddThreadConfig.Schema 11 | type: model.Thread.Type 12 | sharing: model.Thread.Sharing 13 | whitelist: string[] 14 | force: boolean 15 | } 16 | 17 | export namespace AddThreadConfig { 18 | export interface Schema { 19 | id: string 20 | json: string 21 | preset: AddThreadConfig.Schema.Preset 22 | } 23 | 24 | export namespace Schema { 25 | export enum Preset { 26 | NONE = 'NONE', 27 | BLOB = 'BLOB', 28 | CAMERA_ROLL = 'CAMERA_ROLL', 29 | MEDIA = 'MEDIA' 30 | } 31 | } 32 | } 33 | 34 | export interface BlockViz { 35 | dots: string 36 | count: number 37 | next: string 38 | } 39 | 40 | export interface Step { 41 | name: string 42 | link: model.Link 43 | } 44 | 45 | export interface Directory { 46 | files: Directory.Files 47 | } 48 | 49 | export namespace Directory { 50 | export interface Files { 51 | [k: string]: model.FileIndex 52 | } 53 | } 54 | 55 | export interface DirectoryList { 56 | items: Directory[] 57 | } 58 | 59 | export interface Keys { 60 | files: Keys.Files 61 | } 62 | 63 | export namespace Keys { 64 | export interface Files { 65 | [k: string]: string 66 | } 67 | } 68 | 69 | export interface InviteView { 70 | id: string 71 | name: string 72 | inviter: model.User 73 | date: string 74 | } 75 | 76 | export interface InviteViewList { 77 | items: InviteView[] 78 | } 79 | 80 | export interface ExternalInvite { 81 | id: string 82 | key: string 83 | inviter: string 84 | } 85 | 86 | export interface FeedRequest { 87 | thread: string 88 | offset: string 89 | limit: number 90 | mode: FeedRequest.Mode 91 | } 92 | 93 | export namespace FeedRequest { 94 | export enum Mode { 95 | CHRONO = 'CHRONO', 96 | ANNOTATED = 'ANNOTATED', 97 | STACKS = 'STACKS' 98 | } 99 | } 100 | 101 | export interface FeedItem { 102 | block: string 103 | thread: string 104 | payload: any 105 | } 106 | 107 | export interface FeedItemList { 108 | items: FeedItem[] 109 | count: number 110 | next: string 111 | } 112 | 113 | export interface Merge { 114 | block: string 115 | date: string 116 | user: model.User 117 | targets: FeedItem[] 118 | } 119 | 120 | export interface Ignore { 121 | block: string 122 | date: string 123 | user: model.User 124 | target: FeedItem 125 | } 126 | 127 | export interface Flag { 128 | block: string 129 | date: string 130 | user: model.User 131 | target: FeedItem 132 | } 133 | 134 | export interface Join { 135 | block: string 136 | date: string 137 | user: model.User 138 | likes: Like[] 139 | } 140 | 141 | export interface Announce { 142 | block: string 143 | date: string 144 | user: model.User 145 | } 146 | 147 | export interface Leave { 148 | block: string 149 | date: string 150 | user: model.User 151 | likes: Like[] 152 | } 153 | 154 | export interface Text { 155 | block: string 156 | date: string 157 | user: model.User 158 | body: string 159 | comments: Comment[] 160 | likes: Like[] 161 | } 162 | 163 | export interface TextList { 164 | items: Text[] 165 | } 166 | 167 | export interface File { 168 | index: number 169 | file: model.FileIndex 170 | links: File.Links 171 | } 172 | 173 | export namespace File { 174 | export interface Links { 175 | [k: string]: model.FileIndex 176 | } 177 | } 178 | 179 | export interface Files { 180 | block: string 181 | target: string 182 | date: string 183 | user: model.User 184 | caption: string 185 | files: File[] 186 | comments: Comment[] 187 | likes: Like[] 188 | threads: string[] 189 | } 190 | 191 | export interface FilesList { 192 | items: Files[] 193 | } 194 | 195 | export interface Comment { 196 | id: string 197 | date: string 198 | user: model.User 199 | body: string 200 | target: FeedItem 201 | } 202 | 203 | export interface CommentList { 204 | items: Comment[] 205 | } 206 | 207 | export interface Like { 208 | id: string 209 | date: string 210 | user: model.User 211 | target: FeedItem 212 | } 213 | 214 | export interface LikeList { 215 | items: Like[] 216 | } 217 | 218 | export interface WalletUpdate { 219 | id: string 220 | key: string 221 | type: WalletUpdate.Type 222 | } 223 | 224 | export namespace WalletUpdate { 225 | export enum Type { 226 | THREAD_ADDED = 'THREAD_ADDED', 227 | THREAD_REMOVED = 'THREAD_REMOVED', 228 | ACCOUNT_PEER_ADDED = 'ACCOUNT_PEER_ADDED', 229 | ACCOUNT_PEER_REMOVED = 'ACCOUNT_PEER_REMOVED' 230 | } 231 | } 232 | 233 | export interface Summary { 234 | id: string 235 | address: string 236 | account_peer_count: number 237 | thread_count: number 238 | files_count: number 239 | contact_count: number 240 | } 241 | 242 | export interface LogLevel { 243 | systems: LogLevel.Systems 244 | } 245 | 246 | export namespace LogLevel { 247 | export interface Systems { 248 | [k: string]: LogLevel.Level 249 | } 250 | 251 | export enum Level { 252 | CRITICAL = 'CRITICAL', 253 | ERROR = 'ERROR', 254 | WARNING = 'WARNING', 255 | NOTICE = 'NOTICE', 256 | INFO = 'INFO', 257 | DEBUG = 'DEBUG' 258 | } 259 | } 260 | -------------------------------------------------------------------------------- /src/modules/threads.ts: -------------------------------------------------------------------------------- 1 | import { API, DEFAULT_API_OPTIONS } from '../core/api' 2 | import { ApiOptions, Thread, ThreadList, ContactList } from '../models/index' 3 | import Snapshots from './snapshots' 4 | import Schemas from './schemas' 5 | 6 | export type ThreadType = 'private' | 'read_only' | 'public' | 'open' 7 | export type ThreadSharing = 'not_shared' | 'invite_only' | 'shared' 8 | 9 | /** 10 | * Threads is an API module for managing Textile threads 11 | * 12 | * Threads are distributed sets of encrypted files between peers governed by build-in or 13 | * custom Schemas. 14 | * 15 | * Thread type controls read (R), annotate (A), and write (W) access: 16 | * 17 | * private --> initiator: RAW, whitelist: 18 | * readonly --> initiator: RAW, whitelist: R 19 | * public --> initiator: RAW, whitelist: RA 20 | * open --> initiator: RAW, whitelist: RAW 21 | * 22 | * Thread sharing style controls if (Y/N) a thread can be shared: 23 | * 24 | * notshared --> initiator: N, whitelist: N 25 | * inviteonly --> initiator: Y, whitelist: N 26 | * shared --> initiator: Y, whitelist: Y 27 | * 28 | * @extends API 29 | */ 30 | export default class Threads extends API { 31 | snapshots: Snapshots 32 | schemas: Schemas 33 | constructor(opts: ApiOptions = DEFAULT_API_OPTIONS) { 34 | super(opts) 35 | this.snapshots = new Snapshots(opts) 36 | this.schemas = new Schemas(opts) 37 | } 38 | /** 39 | * Adds and joins a new thread 40 | * 41 | * @param name The name of the new thread 42 | * @param key A locally unique key used by an app to identify this thread on recovery 43 | * @param type The type of thread, must be one of 'private' (default), 'read_only', 'public', 44 | * or 'open' 45 | * @param sharing The sharing style of thread, must be one of 'notshared' 46 | * (default), 'invite_only', or 'shared' 47 | * @param whitelist An array of contact addresses. When supplied, the thread will not allow 48 | * additional peers, useful for 1-1 chat/file sharing or private threads. 49 | * @param schema Schema ID for the new thread 50 | * @returns The newly generated thread info 51 | */ 52 | async add(name: string, schema?: string | object, key?: string, type?: ThreadType, sharing?: ThreadSharing, whitelist?: string[]) { 53 | let targetSchema: string | undefined 54 | // Attempt to create the schema on the fly 55 | if (schema && typeof schema === 'object') { 56 | const fileIndex = await this.schemas.add(schema) 57 | targetSchema = fileIndex.hash 58 | } else if (schema && typeof schema === 'string') { 59 | // check if it is one of the default schemas 60 | const known = await this.schemas.defaultByName(schema) 61 | if (known) { 62 | // if one of the default, add it or get it to find the hash 63 | const added = await this.schemas.add(known) 64 | targetSchema = added.hash 65 | } else { 66 | // @todo - perhaps a specific warning if schema here doesn't match a hash len 67 | targetSchema = schema 68 | } 69 | } 70 | const response = await this.sendPost( 71 | 'threads', 72 | [name], 73 | { 74 | schema: targetSchema || '', 75 | key: key || '', 76 | type: type || 'private', 77 | sharing: sharing || 'not_shared', 78 | whitelist: (whitelist || []).join(',') 79 | } 80 | ) 81 | return response.json() as Promise 82 | } 83 | 84 | /** 85 | * Adds or updates a thread directly, usually from a backup 86 | * 87 | * @param thread ID of the thread 88 | * @param info Thread object 89 | */ 90 | async addOrUpdate(thread: string, info: Thread) { 91 | this.sendPut(`threads/${thread}`, undefined, undefined, info) 92 | } 93 | 94 | /** 95 | * Retrieve a thread by ID 96 | * 97 | * @param thread ID of the thread 98 | * @returns A thread object 99 | */ 100 | async get(thread: string) { 101 | const response = await this.sendGet(`threads/${thread}`) 102 | return response.json() as Promise 103 | } 104 | 105 | /** 106 | * Retrieve a thread by Key 107 | * 108 | * @param key Key of the thread 109 | * @returns A thread object 110 | */ 111 | async getByKey(key: string) { 112 | // @todo: update with https://github.com/textileio/go-textile/issues/712 113 | const response = await this.sendGet('threads') 114 | const threads: ThreadList = await response.json() 115 | return threads.items.find((thread) => thread.key === key) 116 | } 117 | /** 118 | * Retrieve threads by Name 119 | * 120 | * @param name Name of the thread 121 | * @returns An array thread objects 122 | */ 123 | async getByName(name: string) { 124 | // @todo: update with https://github.com/textileio/go-textile/issues/712 125 | const response = await this.sendGet('threads') 126 | const threads: ThreadList = await response.json() 127 | return threads.items.filter((thread) => thread.name === name) 128 | } 129 | 130 | /** 131 | * Lists all local threads 132 | * 133 | * @returns An array of threads 134 | */ 135 | async list() { 136 | const response = await this.sendGet('threads') 137 | return response.json() as Promise 138 | } 139 | 140 | /** 141 | * Leave and remove a thread by ID 142 | * 143 | * @param thread ID of the thread 144 | * @returns Whether the thread removal was successfull 145 | */ 146 | async remove(thread: string) { 147 | const response = await this.sendDelete(`threads/${thread}`) 148 | return response.status === 204 149 | } 150 | 151 | /** 152 | * Leave and remove a thread by Key 153 | * 154 | * @param key thread.key of the thread 155 | * @returns Whether the thread removal was successfull 156 | */ 157 | async removeByKey(key: string) { 158 | const thread = await this.getByKey(key) 159 | if (!thread) { 160 | return false 161 | } 162 | const response = await this.sendDelete(`threads/${thread.id}`) 163 | return response.status === 204 164 | } 165 | 166 | /** 167 | * Renames a thread 168 | * 169 | * Note: Only initiators can rename a thread. 170 | * @param thread ID of the thread 171 | * @param name New name for the thread 172 | * @returns Whether the rename was successfully 173 | */ 174 | async rename(thread: string, name: string) { 175 | const response = await this.sendPut(`threads/${thread}/name`) 176 | return response.status === 204 177 | } 178 | 179 | /** 180 | * List all peers in a thread 181 | * 182 | * @param thread ID of the thread (default is 'default'). 183 | * @returns An array of thread contacts 184 | */ 185 | async peers(thread: string) { 186 | const response = await this.sendGet(`threads/${thread || 'default'}/peers`) 187 | return response.json() as Promise 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /src/core/api.ts: -------------------------------------------------------------------------------- 1 | import '@ef-carbon/fetch/install' 2 | import URL from 'url-parse' 3 | import { buildAbsoluteURL } from 'url-toolkit' 4 | import { KeyValue, ApiOptions } from '../models' 5 | 6 | export const DEFAULT_API_OPTIONS = { 7 | url: 'http://127.0.0.1', 8 | port: 40600, 9 | version: 0 10 | } 11 | 12 | /** 13 | * Create 'args' like a CLI command would take 14 | * 15 | * @param {string[]} argsAr An array of arguments 16 | * @private 17 | */ 18 | export const getArgs = (argsAr?: string[]) => { 19 | if (!argsAr || !argsAr.length) { 20 | return '' 21 | } 22 | return argsAr.map((ar) => encodeValue(ar)).join(',') 23 | } 24 | 25 | /** 26 | * Create 'options' like a CLI command would take. 27 | * 28 | * @param {Object.} opts A map of option keys and values 29 | * @private 30 | */ 31 | export const getOpts = (opts?: KeyValue) => { 32 | if (!opts) { 33 | return '' 34 | } 35 | return Object.keys(opts) 36 | .map((key) => `${key}=${encodeValue(opts[key])}`) 37 | .join(',') 38 | } 39 | 40 | const encodeValue = (val: string | number | boolean) => { 41 | return encodeURIComponent(val.toString()) 42 | } 43 | 44 | export const createHeaders = (args?: string[], opts?: KeyValue, headers?: KeyValue): Record => { 45 | const h = headers || {} 46 | return { 47 | ...h, 48 | 'X-Textile-Args': getArgs(args), 49 | 'X-Textile-Opts': getOpts(opts) 50 | } 51 | } 52 | 53 | const handleErrors = (response: Response) => { 54 | if (!response.ok) { 55 | throw Error(response.statusText) 56 | } 57 | return response 58 | } 59 | 60 | /** 61 | * API is the base class for all SDK modules. 62 | * 63 | * @params {ApiOptions] opts API options object 64 | */ 65 | class API { 66 | private opts: ApiOptions 67 | private baseURL: string 68 | private gatewayURL: string 69 | constructor(opts: ApiOptions = DEFAULT_API_OPTIONS) { 70 | this.opts = opts 71 | const url = new URL(opts.url) 72 | if (opts.port) { 73 | url.set('port', opts.port) 74 | } 75 | url.set('pathname', `/api/v${opts.version || 0}/`) 76 | this.baseURL = url.toString() 77 | 78 | const gateway = new URL(this.opts.url) 79 | gateway.set('port', 5050) 80 | gateway.set('pathname', `/ipfs/`) 81 | this.gatewayURL = gateway.toString() 82 | } 83 | 84 | /** 85 | * Make a get request to the Textile node 86 | * 87 | * @param url The relative URL of the API endpoint 88 | * @param args An array of arguments to pass as Textile args headers 89 | * @param opts An object of options to pass as Textile options headers 90 | */ 91 | protected async sendGatewayGet(path: string, headers?: KeyValue) { 92 | return fetch(buildAbsoluteURL(this.gatewayURL, path), { 93 | method: 'GET', 94 | headers: createHeaders([], {}, headers) 95 | }) 96 | } 97 | 98 | /** 99 | * Make a post request to the Textile node 100 | * 101 | * @param url The relative URL of the API endpoint 102 | * @param args An array of arguments to pass as Textile args headers 103 | * @param opts An object of options to pass as Textile options headers 104 | * @param data An object of data to post 105 | */ 106 | protected async sendPost(url: string, args?: string[], opts?: KeyValue, data?: any, headers?: KeyValue, raw?: boolean) { 107 | const h = createHeaders(args, opts, headers) 108 | const response = await fetch(buildAbsoluteURL(this.baseURL, url), { 109 | method: 'POST', 110 | headers: new Headers(h), 111 | body: raw ? data : JSON.stringify(data) 112 | }) 113 | return handleErrors(response) 114 | } 115 | 116 | /** 117 | * Make a get request to the Textile node 118 | * 119 | * @param url The relative URL of the API endpoint 120 | * @param args An array of arguments to pass as Textile args headers 121 | * @param opts An object of options to pass as Textile options headers 122 | */ 123 | protected async sendGet(url: string, args?: string[], opts?: KeyValue, headers?: KeyValue) { 124 | const response = await fetch(buildAbsoluteURL(this.baseURL, url), { 125 | method: 'GET', 126 | headers: createHeaders(args, opts, headers) 127 | }) 128 | return handleErrors(response) 129 | } 130 | 131 | /** 132 | * Make a delete request to the Textile node 133 | * 134 | * @param url The relative URL of the API endpoint 135 | * @param args An array of arguments to pass as Textile args headers 136 | * @param opts An object of options to pass as Textile options headers 137 | */ 138 | protected async sendDelete(url: string, args?: string[], opts?: KeyValue, headers?: KeyValue) { 139 | const response = await fetch(buildAbsoluteURL(this.baseURL, url), { 140 | method: 'DELETE', 141 | headers: createHeaders(args, opts, headers) 142 | }) 143 | return handleErrors(response) 144 | } 145 | 146 | /** 147 | * Make a put request to the Textile node 148 | * 149 | * @param url The relative URL of the API endpoint 150 | * @param args An array of arguments to pass as Textile args headers 151 | * @param opts An object of options to pass as Textile options headers 152 | * @param data An object of data to put 153 | */ 154 | protected async sendPut(url: string, args?: string[], opts?: KeyValue, data?: any, headers?: KeyValue) { 155 | const response = await fetch(buildAbsoluteURL(this.baseURL, url), { 156 | method: 'PUT', 157 | headers: createHeaders(args, opts, headers), 158 | body: JSON.stringify(data) 159 | }) 160 | return handleErrors(response) 161 | } 162 | 163 | /** 164 | * Make a patch request to the Textile node 165 | * 166 | * @param url The relative URL of the API endpoint 167 | * @param args An array of arguments to pass as Textile args headers 168 | * @param opts An object of options to pass as Textile options headers 169 | * @param data An object of data to put 170 | */ 171 | protected async sendPatch(url: string, args?: string[], opts?: KeyValue, data?: any, headers?: KeyValue) { 172 | const response = await fetch(buildAbsoluteURL(this.baseURL, url), { 173 | method: 'patch', 174 | headers: createHeaders(args, opts, headers), 175 | body: JSON.stringify(data) 176 | }) 177 | return handleErrors(response) 178 | } 179 | 180 | /** 181 | * Make an EventSource request to the Textile node 182 | * 183 | * @param url The relative URL of the API endpoint 184 | * @param args An array of arguments to pass as query in native EventSource or Textile args headers in EventSourcePolyfill 185 | * @param opts An object of options to pass as Textile options headers 186 | */ 187 | protected sendEventSource(url: string, args?: string[], opts?: KeyValue, headers?: KeyValue) { 188 | // native EventSource can't set header, but can CORS 189 | return new EventSource(buildAbsoluteURL(this.baseURL, `${url}${opts ? `?${URL.qs.stringify(opts)}` : ''}`)) 190 | 191 | // EventSourcePolyfill of eventsource@1.0.7 can set header, but can't CORS 192 | // return new EventSourcePolyfill(buildAbsoluteURL(this.baseURL, url), { 193 | // headers: { 194 | // 'X-Textile-Opts': getOpts(opts), 195 | // } 196 | // }); 197 | } 198 | } 199 | 200 | export { API } 201 | -------------------------------------------------------------------------------- /src/modules/events.ts: -------------------------------------------------------------------------------- 1 | import { API, DEFAULT_API_OPTIONS } from '../core/api' 2 | import { ApiOptions, MobileQueryEvent } from '../models' 3 | import { ReadableStream } from 'web-streams-polyfill/ponyfill' 4 | 5 | class EventSubscription { 6 | cancel: () => void 7 | constructor(cancel: () => void) { 8 | this.cancel = cancel 9 | } 10 | } 11 | 12 | /** 13 | * Events is an API to stream updates from an ipfs pubsub 14 | */ 15 | export default class Events extends API { 16 | private pubsubQueryResultListeners: Array<{ 17 | listener: (queryId: string, message: string, messageId: string) => void, 18 | queryId: string 19 | }> 20 | private queryDoneListeners: Array<{ 21 | listener: (queryId: string) => void, 22 | queryId: string 23 | }> 24 | private queryErrorListeners: Array<{ 25 | listener: (queryId: string, error: string) => void, 26 | queryId: string 27 | }> 28 | constructor(opts: ApiOptions = DEFAULT_API_OPTIONS) { 29 | super(opts) 30 | this.pubsubQueryResultListeners = [] 31 | this.queryDoneListeners = [] 32 | this.queryErrorListeners = [] 33 | } 34 | 35 | addPubsubQueryResultListener( 36 | listener: (queryId: string, message: string, messageId: string) => void, 37 | queryId: string, 38 | queryHandle: EventSource | ReadableStream 39 | ) { 40 | this.pubsubQueryResultListeners.push({ 41 | listener, 42 | queryId 43 | }) 44 | // For cancellation 45 | const isEventSubscription = new EventSubscription( 46 | () => { 47 | this.pubsubQueryResultListeners = this.pubsubQueryResultListeners.filter( 48 | (item) => item.queryId !== queryId 49 | ) 50 | this.queryDoneListeners = this.queryDoneListeners.filter( 51 | (item) => item.queryId !== queryId 52 | ) 53 | this.queryErrorListeners = this.queryErrorListeners.filter( 54 | (item) => item.queryId !== queryId 55 | ) 56 | if (queryHandle instanceof EventSource) { 57 | queryHandle.close() 58 | } 59 | } 60 | ) 61 | 62 | const onPubsubQueryEvent = (queryEvent: MobileQueryEvent) => { 63 | const message = queryEvent.data.value.values[0] 64 | const messageId = queryEvent.data.id 65 | for (const item of this.pubsubQueryResultListeners) { 66 | if (item.queryId === queryEvent.id) { 67 | item.listener(queryEvent.id, message, messageId) 68 | } 69 | } 70 | } 71 | 72 | const onQueryError = (e: Error) => { 73 | for (const item of this.queryErrorListeners) { 74 | if (item.queryId === queryId) { 75 | item.listener(queryId, e.toString()) 76 | } 77 | } 78 | } 79 | 80 | const onQueryDone = () => { 81 | for (const item of this.queryDoneListeners) { 82 | if (item.queryId === queryId) { 83 | item.listener(queryId) 84 | } 85 | } 86 | 87 | this.pubsubQueryResultListeners = this.pubsubQueryResultListeners.filter( 88 | (item) => item.queryId !== queryId 89 | ) 90 | this.queryDoneListeners = this.queryDoneListeners.filter( 91 | (item) => item.queryId !== queryId 92 | ) 93 | this.queryErrorListeners = this.queryErrorListeners.filter( 94 | (item) => item.queryId !== queryId 95 | ) 96 | } 97 | 98 | if (queryHandle instanceof EventSource) { 99 | queryHandle.addEventListener('update', (e: any) => { 100 | try { 101 | const queryEvent: MobileQueryEvent = JSON.parse(e.data) 102 | onPubsubQueryEvent(queryEvent) 103 | } catch (e) { 104 | onQueryError(e) 105 | } 106 | }, false) 107 | queryHandle.addEventListener('open', (e: any) => { 108 | // Connection was opened. 109 | }, false) 110 | queryHandle.addEventListener('error', (e: any) => { 111 | if (e.readyState === 2 /* EventSource.CLOSED */) { 112 | onQueryDone() 113 | } else { 114 | onQueryError(e) 115 | } 116 | }, false) 117 | return isEventSubscription 118 | } else { 119 | // For cancellation 120 | let isReader: any 121 | let cancellationRequest = false 122 | return new ReadableStream({ 123 | start(controller) { 124 | const reader = queryHandle.getReader() 125 | isReader = reader 126 | const decoder = new TextDecoder() 127 | let dataBuffer = '' 128 | 129 | const processResult = (result: ReadableStreamReadResult) => { 130 | if (result.done) { 131 | if (cancellationRequest) { 132 | onQueryDone() 133 | return // Immediately exit 134 | } 135 | dataBuffer = dataBuffer.trim() 136 | if (dataBuffer.length !== 0) { 137 | try { 138 | const queryEvent: MobileQueryEvent = JSON.parse(dataBuffer) 139 | onPubsubQueryEvent(queryEvent) 140 | } catch (e) { 141 | controller.error(e) 142 | } 143 | } 144 | onQueryDone() 145 | controller.close() 146 | return 147 | } 148 | const data = decoder.decode(result.value, { stream: true }) 149 | dataBuffer += data 150 | const lines = dataBuffer.split('\n') 151 | for (const line of lines) { 152 | const l = line.trim() 153 | if (l.length > 0) { 154 | try { 155 | const queryEvent: MobileQueryEvent = JSON.parse(l) 156 | onPubsubQueryEvent(queryEvent) 157 | } catch (e) { 158 | onQueryError(e) 159 | controller.error(e) 160 | break 161 | } 162 | } 163 | } 164 | dataBuffer = lines.length > 1 ? lines[lines.length - 1] : '' 165 | reader.read().then(processResult) 166 | } 167 | reader.read().then(processResult) 168 | }, 169 | cancel(reason?: string) { 170 | isEventSubscription.cancel() 171 | cancellationRequest = true 172 | isReader.cancel(reason) 173 | } 174 | }) 175 | } 176 | } 177 | 178 | addQueryDoneListener( 179 | listener: (queryId: string) => void, 180 | queryId: string 181 | ) { 182 | this.queryDoneListeners.push({ 183 | listener, 184 | queryId 185 | }) 186 | // For cancellation 187 | const isEventSubscription = new EventSubscription( 188 | () => 189 | (this.queryDoneListeners = this.queryDoneListeners.filter( 190 | (item) => item.queryId !== queryId 191 | )) 192 | ) 193 | return isEventSubscription 194 | } 195 | 196 | addQueryErrorListener( 197 | listener: (queryId: string, error: string) => void, 198 | queryId: string 199 | ) { 200 | this.queryErrorListeners.push({ 201 | listener, 202 | queryId 203 | }) 204 | // For cancellation 205 | const isEventSubscription = new EventSubscription( 206 | () => 207 | (this.queryErrorListeners = this.queryErrorListeners.filter( 208 | (item) => item.queryId !== queryId 209 | )) 210 | ) 211 | return isEventSubscription 212 | } 213 | } 214 | -------------------------------------------------------------------------------- /src/models/model_pb.ts: -------------------------------------------------------------------------------- 1 | /* GENERATED FROM model.proto. DO NOT EDIT MANUALLY. */ 2 | /* tslint:disabled */ 3 | /* eslint-disable */ 4 | 5 | import * as message from './message_pb' 6 | 7 | export interface Peer { 8 | id: string 9 | address: string 10 | name: string 11 | avatar: string 12 | inboxes: Cafe[] 13 | created: string 14 | updated: string 15 | } 16 | 17 | export interface PeerList { 18 | items: Peer[] 19 | } 20 | 21 | export interface User { 22 | address: string 23 | name: string 24 | avatar: string 25 | } 26 | 27 | export interface Contact { 28 | address: string 29 | name: string 30 | avatar: string 31 | peers: Peer[] 32 | threads: string[] 33 | } 34 | 35 | export interface ContactList { 36 | items: Contact[] 37 | } 38 | 39 | export interface Thread { 40 | id: string 41 | key: string 42 | sk: string 43 | name: string 44 | schema: string 45 | initiator: string 46 | type: Thread.Type 47 | sharing: Thread.Sharing 48 | whitelist: string[] 49 | state: Thread.State 50 | head: string 51 | head_blocks: Block[] 52 | schema_node: Node 53 | block_count: number 54 | peer_count: number 55 | } 56 | 57 | export namespace Thread { 58 | export enum Type { 59 | PRIVATE = 'PRIVATE', 60 | READ_ONLY = 'READ_ONLY', 61 | PUBLIC = 'PUBLIC', 62 | OPEN = 'OPEN' 63 | } 64 | 65 | export enum Sharing { 66 | NOT_SHARED = 'NOT_SHARED', 67 | INVITE_ONLY = 'INVITE_ONLY', 68 | SHARED = 'SHARED' 69 | } 70 | 71 | export enum State { 72 | LOADING_TAIL = 'LOADING_TAIL', 73 | LOADED = 'LOADED', 74 | LOADING_HEAD = 'LOADING_HEAD' 75 | } 76 | } 77 | 78 | export interface ThreadList { 79 | items: Thread[] 80 | } 81 | 82 | export interface ThreadPeer { 83 | id: string 84 | thread: string 85 | welcomed: boolean 86 | } 87 | 88 | export interface Block { 89 | id: string 90 | thread: string 91 | author: string 92 | type: Block.BlockType 93 | date: string 94 | parents: string[] 95 | target: string 96 | body: string 97 | user: User 98 | } 99 | 100 | export namespace Block { 101 | export enum BlockType { 102 | MERGE = 'MERGE', 103 | IGNORE = 'IGNORE', 104 | FLAG = 'FLAG', 105 | JOIN = 'JOIN', 106 | ANNOUNCE = 'ANNOUNCE', 107 | LEAVE = 'LEAVE', 108 | TEXT = 'TEXT', 109 | FILES = 'FILES', 110 | COMMENT = 'COMMENT', 111 | LIKE = 'LIKE', 112 | ADD = 'ADD' 113 | } 114 | } 115 | 116 | export interface BlockList { 117 | items: Block[] 118 | } 119 | 120 | export interface BlockMessage { 121 | id: string 122 | peer: string 123 | env: message.Envelope 124 | date: string 125 | } 126 | 127 | export interface Invite { 128 | id: string 129 | block: string 130 | name: string 131 | inviter: Peer 132 | date: string 133 | } 134 | 135 | export interface InviteList { 136 | items: Invite[] 137 | } 138 | 139 | export interface FileIndex { 140 | mill: string 141 | checksum: string 142 | source: string 143 | opts: string 144 | hash: string 145 | key: string 146 | media: string 147 | name: string 148 | size: string 149 | added: string 150 | meta: Record 151 | targets: string[] 152 | } 153 | 154 | export interface Node { 155 | name: string 156 | pin: boolean 157 | plaintext: boolean 158 | mill: string 159 | opts: Node.Opts 160 | json_schema: Record 161 | links: Node.Links 162 | } 163 | 164 | export namespace Node { 165 | export interface Opts { 166 | [k: string]: string 167 | } 168 | 169 | export interface Links { 170 | [k: string]: Link 171 | } 172 | } 173 | 174 | export interface Link { 175 | use: string 176 | pin: boolean 177 | plaintext: boolean 178 | mill: string 179 | opts: Link.Opts 180 | json_schema: Record 181 | } 182 | 183 | export namespace Link { 184 | export interface Opts { 185 | [k: string]: string 186 | } 187 | } 188 | 189 | export interface Notification { 190 | id: string 191 | date: string 192 | actor: string 193 | subject: string 194 | subject_desc: string 195 | block: string 196 | target: string 197 | type: Notification.Type 198 | body: string 199 | read: boolean 200 | user: User 201 | } 202 | 203 | export namespace Notification { 204 | export enum Type { 205 | INVITE_RECEIVED = 'INVITE_RECEIVED', 206 | ACCOUNT_PEER_JOINED = 'ACCOUNT_PEER_JOINED', 207 | PEER_JOINED = 'PEER_JOINED', 208 | PEER_LEFT = 'PEER_LEFT', 209 | MESSAGE_ADDED = 'MESSAGE_ADDED', 210 | FILES_ADDED = 'FILES_ADDED', 211 | COMMENT_ADDED = 'COMMENT_ADDED', 212 | LIKE_ADDED = 'LIKE_ADDED' 213 | } 214 | } 215 | 216 | export interface NotificationList { 217 | items: Notification[] 218 | } 219 | 220 | export interface Cafe { 221 | peer: string 222 | address: string 223 | api: string 224 | protocol: string 225 | node: string 226 | url: string 227 | swarm: string[] 228 | } 229 | 230 | export interface CafeSession { 231 | id: string 232 | access: string 233 | exp: string 234 | refresh: string 235 | rexp: string 236 | subject: string 237 | type: string 238 | cafe: Cafe 239 | } 240 | 241 | export interface CafeSessionList { 242 | items: CafeSession[] 243 | } 244 | 245 | export interface CafeRequest { 246 | id: string 247 | peer: string 248 | target: string 249 | cafe: Cafe 250 | type: CafeRequest.Type 251 | size: string 252 | group: string 253 | date: string 254 | status: CafeRequest.Status 255 | } 256 | 257 | export namespace CafeRequest { 258 | export enum Type { 259 | STORE = 'STORE', 260 | UNSTORE = 'UNSTORE', 261 | STORE_THREAD = 'STORE_THREAD', 262 | UNSTORE_THREAD = 'UNSTORE_THREAD', 263 | INBOX = 'INBOX' 264 | } 265 | 266 | export enum Status { 267 | NEW = 'NEW', 268 | PENDING = 'PENDING', 269 | COMPLETE = 'COMPLETE' 270 | } 271 | } 272 | 273 | export interface CafeRequestList { 274 | items: CafeRequest[] 275 | } 276 | 277 | export interface CafeRequestGroupStatus { 278 | num_total: number 279 | num_pending: number 280 | num_complete: number 281 | size_total: string 282 | size_pending: string 283 | size_complete: string 284 | } 285 | 286 | export interface CafeHTTPRequest { 287 | type: CafeHTTPRequest.Type 288 | url: string 289 | headers: CafeHTTPRequest.Headers 290 | body: string 291 | } 292 | 293 | export namespace CafeHTTPRequest { 294 | export interface Headers { 295 | [k: string]: string 296 | } 297 | 298 | export enum Type { 299 | PUT = 'PUT', 300 | POST = 'POST', 301 | DELETE = 'DELETE' 302 | } 303 | } 304 | 305 | export interface CafeMessage { 306 | id: string 307 | peer: string 308 | date: string 309 | attempts: number 310 | } 311 | 312 | export interface CafeClientNonce { 313 | value: string 314 | address: string 315 | date: string 316 | } 317 | 318 | export interface CafeClient { 319 | id: string 320 | address: string 321 | created: string 322 | seen: string 323 | token: string 324 | } 325 | 326 | export interface CafeClientList { 327 | items: CafeClient[] 328 | } 329 | 330 | export interface CafeToken { 331 | id: string 332 | value: string 333 | date: string 334 | } 335 | 336 | export interface CafeClientThread { 337 | id: string 338 | client: string 339 | ciphertext: string 340 | } 341 | 342 | export interface CafeClientMessage { 343 | id: string 344 | peer: string 345 | client: string 346 | date: string 347 | } 348 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Textile JS HTTP Client _(js-http-client)_ 2 | 3 | [![Made by Textile](https://img.shields.io/badge/made%20by-Textile-informational.svg?style=popout-square)](https://textile.io) 4 | [![Chat on Slack](https://img.shields.io/badge/slack-slack.textile.io-informational.svg?style=popout-square)](https://slack.textile.io) 5 | [![Keywords](https://img.shields.io/github/package-json/keywords/textileio/js-http-client.svg?style=popout-square)](./package.json) 6 | 7 | [![GitHub package.json version](https://img.shields.io/github/package-json/v/textileio/js-http-client.svg?style=popout-square)](./package.json) 8 | [![npm (scoped)](https://img.shields.io/npm/v/@textile/js-http-client.svg?style=popout-square)](https://www.npmjs.com/package/@textile/js-http-client) 9 | [![node (scoped)](https://img.shields.io/node/v/@textile/js-http-client.svg?style=popout-square)](https://www.npmjs.com/package/@textile/js-http-client) 10 | [![GitHub license](https://img.shields.io/github/license/textileio/js-http-client.svg?style=popout-square)](./LICENSE) 11 | [![David](https://img.shields.io/david/dev/textileio/js-http-client.svg)](https://david-dm.org/textileio/js-http-client) 12 | [![CircleCI branch](https://img.shields.io/circleci/project/github/textileio/js-http-client/master.svg?style=popout-square)](https://circleci.com/gh/textileio/js-http-client) 13 | [![standard-readme compliant](https://img.shields.io/badge/readme%20style-standard-brightgreen.svg?style=popout-square)](https://github.com/RichardLitt/standard-readme) 14 | [![docs](https://img.shields.io/badge/docs-master-success.svg?style=popout-square)](https://textileio.github.io/js-http-client/) 15 | 16 | > Official Textile JS HTTP Wrapper Client 17 | 18 | Join us on our [public Slack channel](https://slack.textile.io/) for news, discussions, and status updates. For current status, and where you can help, please see [issue #1](https://github.com/textileio/js-http-client/issues/1). 19 | 20 | **Important**: With the move to Typescript and our 0.2.x release, `js-http-client` is now 21 | published under the `@textile` namespace, rather than `@textileio`. Previous 22 | releases will remain available under `@textileio`, however, all 23 | code should be updated to reflect this change. 24 | 25 | ## Table of Contents 26 | 27 | - [Textile JS HTTP Client _(js-http-client)_](#textile-js-http-client-js-http-client) 28 | - [Table of Contents](#table-of-contents) 29 | - [Background](#background) 30 | - [Development](#development) 31 | - [Documentation](#documentation) 32 | - [Maintainer](#maintainer) 33 | - [Contributing](#contributing) 34 | - [Contributors](#contributors) 35 | - [License](#license) 36 | 37 | ## Background 38 | 39 | [Textile](https://www.textile.io) provides encrypted, recoverable, schema-based, and cross-application data storage built on [IPFS](https://github.com/ipfs) and [libp2p](https://github.com/libp2p). We like to think of it as a decentralized data wallet with built-in protocols for sharing and recovery, or more simply, **an open and programmable iCloud**. 40 | 41 | The reference implementation of Textile is [written in Go](https://github.com/textileio/go-textile), and can be compiled to various platforms, including mobile (Android/iOS) and desktop/server (OSX, Windows, Linux, etc). The library in this repo is designed to help support things like browser-based Textile apps, Node.js apps, and other use-cases. 42 | 43 | This library provides access to an underlying `go-textile` node's REST API, adding various simplified APIs to support in-browser and programmatic desktop access. For the most part, the API would mimic the command-line and/or mobile APIs of `go-textile`, with some browser-specific enhancements. 44 | 45 | ## Development 46 | 47 | ```sh 48 | # Run all the unit tests 49 | yarn test 50 | 51 | # Lint everything 52 | # NOTE: Linting uses `prettier` to auto-fix styling issues when possible 53 | yarn lint 54 | ``` 55 | 56 | You can also compile the Typescript yourself with: 57 | 58 | ```sh 59 | yarn build 60 | ``` 61 | 62 | And even build a nice browser-compatible bundle with: 63 | 64 | ```sh 65 | yarn browser 66 | ``` 67 | 68 | These will both build and add the exported Javascript files to `dist`, ready to be used in your next NodeJS, browser, React, Vue, or whatever app! 69 | 70 | We also provide scripts to run a light-weight `webpack-dev-server` to test out the browser builds. Try something like `yarn serve`, and then browse to `http://127.0.0.1:8080/examples/browser` in your favorite browser for a simple example. 71 | 72 | ## Documentation 73 | 74 | The auto-generated documentation can be found at https://textileio.github.io/js-http-client/. 75 | 76 | ```sh 77 | # Re-build the documentation 78 | yarn docs 79 | ``` 80 | 81 | ## Maintainer 82 | 83 | [Carson Farmer](https://github.com/carsonfarmer) 84 | 85 | ## Contributing 86 | 87 | Textile's JS HTTP Client is a work in progress. As such, there's a few things you can do right now to help out: 88 | 89 | * Check out [issue 1](https://github.com/textileio/js-http-client/issues/1) for an up-to-date list (maintained by [carsonfarmer](https://github.com/carsonfarmer)) of tasks that could use your help. 90 | * Ask questions! We'll try to help. Be sure to drop a note (on the above issue) if there is anything you'd like to work on and we'll update the issue to let others know. Also [get in touch](https://slack.textile.io) on Slack. 91 | * Log bugs, [file issues](https://github.com/textileio/js-http-client/issues), submit pull requests! 92 | * **Perform code reviews**. More eyes will help a) speed the project along b) ensure quality and c) reduce possible future bugs. 93 | * Take a look at [go-textile](https://github.com/textileio/go-textile) (which we intend to follow to a point), and also at some of the client repositories: for instance, [`textile-mobile`](https://github.com/textileio/textile-mobile) and the Textile [`react-native-sdk`](https://github.com/textileio/react-native-sdk). Contributions here that would be most helpful are **top-level comments** about how it should look based on our understanding. Again, the more eyes the better. 94 | * **Add tests**. There can never be enough tests. 95 | * **Contribute to the [Textile docs](https://github.com/textileio/docs)** with any additions or questions you have about Textile and its various impmenentations. A good example would be asking, "What is a thread?". If you don't know a term, odds are someone else doesn't either. Eventually, we should have a good understanding of where we need to improve communications and teaching together to make Textile even better. 96 | 97 | Before you get started, be sure to read our [contributors guide](./CONTRIBUTING.md) and our [contributor covenant code of conduct](./CODE_OF_CONDUCT.md). 98 | 99 | ## Contributors 100 | 101 | 102 | | **Commits** | **Contributor** | 103 | | --- | --- | 104 | | 116 | [carsonfarmer](https://github.com/carsonfarmer) | 105 | | 26 | [andrewxhill](https://github.com/andrewxhill) | 106 | | 3 | [flyskywhy](https://github.com/flyskywhy) | 107 | | 3 | [dependabot[bot]](https://github.com/apps/dependabot) | 108 | | 2 | [robbynshaw](https://github.com/robbynshaw) | 109 | | 1 | [asutula](https://github.com/asutula) | 110 | | 1 | [balupton](https://github.com/balupton) | 111 | | 1 | [connectdotz](https://github.com/connectdotz) | 112 | 113 | 114 | 115 | ## License 116 | 117 | [MIT](./LICENSE) 118 | -------------------------------------------------------------------------------- /src/modules/__test__/__static__/feed.ts: -------------------------------------------------------------------------------- 1 | import { FeedItemList } from '../../../models' 2 | 3 | export const stacks: FeedItemList = { 4 | items: [ 5 | { 6 | block: 'QmRRYCEQMuSefa6ZtCLQ89QsqZaEBfsXikj2UzKRvcCkvY', 7 | thread: '12D3KooWGdz1WeHJPRv8fowwPWrqWPdUfwWUQyEp4NeRu4nyHXLH', 8 | payload: { 9 | '@type': '/Like', 10 | 'id': 'QmRRYCEQMuSefa6ZtCLQ89QsqZaEBfsXikj2UzKRvcCkvY', 11 | 'date': '2019-04-01T21:43:37.116893Z', 12 | 'user': { 13 | address: 'P4RaCnWZYhKCztWK7mii1WQZmXVAtBVdRAyBU6Em51agxTfW', 14 | name: 'displayname', 15 | avatar: 'QmUMWopyRDwZYJznyyhp1TmbAJhLV3z2Xe9dSeqr6LNvV5' 16 | }, 17 | 'target': { 18 | block: 'QmbU3JkMqnYHm9uv9VmBJLCgdB4gZeb6ZgUEW9G4RnmQxQ', 19 | thread: '12D3KooWGdz1WeHJPRv8fowwPWrqWPdUfwWUQyEp4NeRu4nyHXLH', 20 | payload: { 21 | '@type': '/Text', 22 | 'block': 'QmbU3JkMqnYHm9uv9VmBJLCgdB4gZeb6ZgUEW9G4RnmQxQ', 23 | 'date': '2019-03-28T17:24:25.796298Z', 24 | 'user': { 25 | address: 'P4RaCnWZYhKCztWK7mii1WQZmXVAtBVdRAyBU6Em51agxTfW', 26 | name: 'displayname', 27 | avatar: 'QmUMWopyRDwZYJznyyhp1TmbAJhLV3z2Xe9dSeqr6LNvV5' 28 | }, 29 | 'body': 'message' 30 | } 31 | } 32 | } 33 | }, 34 | { 35 | block: 'QmeU7xSPq6Bewh6PdwHqGHhoErFH1UBQV5Aqb7VYP9ugey', 36 | thread: '12D3KooWRW4Ewc1tYCLA2xj6crywHoCxxBngenJNUsPyzHGm3meL', 37 | payload: { 38 | '@type': '/Files', 39 | 'block': 'QmeU7xSPq6Bewh6PdwHqGHhoErFH1UBQV5Aqb7VYP9ugey', 40 | 'target': 'QmUMWopyRDwZYJznyyhp1TmbAJhLV3z2Xe9dSeqr6LNvV5', 41 | 'date': '2019-04-01T21:27:37.422127Z', 42 | 'user': { 43 | address: 'P4RaCnWZYhKCztWK7mii1WQZmXVAtBVdRAyBU6Em51agxTfW', 44 | name: 'displayname', 45 | avatar: 'QmUMWopyRDwZYJznyyhp1TmbAJhLV3z2Xe9dSeqr6LNvV5' 46 | }, 47 | 'files': [ 48 | { 49 | links: { 50 | large: { 51 | mill: '/image/resize', 52 | checksum: 'B9RebNwLPTXQJymnjEJsYsFuS1dAuJBpBnHT1sB2LLVQ', 53 | source: 'B9RebNwLPTXQJymnjEJsYsFuS1dAuJBpBnHT1sB2LLVQ', 54 | opts: 'Atk8eCEgX98dPBCieRvUUBkH1nA7YiX6AxK1ZjbA6mY', 55 | hash: 'QmVhJPrbb6iD9BYkUcqyDepuAw1wGsudhZLKapfZGCuhKG', 56 | media: 'image/png', 57 | size: '7270', 58 | added: '2019-04-01T21:27:37.378108Z', 59 | meta: { 60 | height: 128, 61 | width: 128 62 | }, 63 | targets: [ 64 | 'QmUMWopyRDwZYJznyyhp1TmbAJhLV3z2Xe9dSeqr6LNvV5' 65 | ] 66 | }, 67 | small: { 68 | mill: '/image/resize', 69 | checksum: '5i7tM447RozmhG1zbYvWGb8UXRRa57DSKJAJSLiUsSYJ', 70 | source: 'B9RebNwLPTXQJymnjEJsYsFuS1dAuJBpBnHT1sB2LLVQ', 71 | opts: 'Av7wdZtZ5P8rWQtuTkUPQ8iBmRK1PyfxPcVeDtAjb2uR', 72 | hash: 'QmY2cqoiCNwNcf6Sf6tEUbA76V64zkcRbkdB8JRAWYafH2', 73 | media: 'image/png', 74 | size: '8125', 75 | added: '2019-04-01T21:27:37.387214Z', 76 | meta: { 77 | height: 100, 78 | width: 100 79 | }, 80 | targets: [ 81 | 'QmUMWopyRDwZYJznyyhp1TmbAJhLV3z2Xe9dSeqr6LNvV5' 82 | ] 83 | } 84 | } 85 | } 86 | ], 87 | 'threads': [ 88 | '12D3KooWRW4Ewc1tYCLA2xj6crywHoCxxBngenJNUsPyzHGm3meL' 89 | ] 90 | } 91 | }, 92 | { 93 | block: 'QmU8GrBr3zNmqcHCP3UhMEueiZSEg1wypQ5E61t84t7TsV', 94 | thread: '12D3KooWRW4Ewc1tYCLA2xj6crywHoCxxBngenJNUsPyzHGm3meL', 95 | payload: { 96 | '@type': '/Join', 97 | 'block': 'QmU8GrBr3zNmqcHCP3UhMEueiZSEg1wypQ5E61t84t7TsV', 98 | 'date': '2019-04-01T21:27:37.363373Z', 99 | 'user': { 100 | address: 'P4RaCnWZYhKCztWK7mii1WQZmXVAtBVdRAyBU6Em51agxTfW', 101 | name: 'displayname', 102 | avatar: 'QmUMWopyRDwZYJznyyhp1TmbAJhLV3z2Xe9dSeqr6LNvV5' 103 | } 104 | } 105 | } 106 | ], 107 | count: 3, 108 | next: 'next' 109 | } 110 | 111 | export const chrono: FeedItemList = { 112 | items: [ 113 | { 114 | block: 'QmRRYCEQMuSefa6ZtCLQ89QsqZaEBfsXikj2UzKRvcCkvY', 115 | thread: 'thread', 116 | payload: { 117 | '@type': '/Like', 118 | 'id': 'like-id', 119 | 'date': '2019-04-01T21:43:37.116893Z', 120 | 'user': { 121 | address: 'P4RaCnWZYhKCztWK7mii1WQZmXVAtBVdRAyBU6Em51agxTfW', 122 | name: 'displayname', 123 | avatar: 'QmUMWopyRDwZYJznyyhp1TmbAJhLV3z2Xe9dSeqr6LNvV5' 124 | }, 125 | 'target': { 126 | block: 'QmbU3JkMqnYHm9uv9VmBJLCgdB4gZeb6ZgUEW9G4RnmQxQ', 127 | thread: 'thread', 128 | payload: { 129 | '@type': '/Text', 130 | 'block': 'QmbU3JkMqnYHm9uv9VmBJLCgdB4gZeb6ZgUEW9G4RnmQxQ', 131 | 'date': '2019-03-28T17:24:25.796298Z', 132 | 'user': { 133 | address: 'P4RaCnWZYhKCztWK7mii1WQZmXVAtBVdRAyBU6Em51agxTfW', 134 | name: 'displayname', 135 | avatar: 'QmUMWopyRDwZYJznyyhp1TmbAJhLV3z2Xe9dSeqr6LNvV5' 136 | }, 137 | 'body': 'message' 138 | } 139 | } 140 | } 141 | }, 142 | { 143 | block: 'QmeU7xSPq6Bewh6PdwHqGHhoErFH1UBQV5Aqb7VYP9ugey', 144 | thread: '12D3KooWRW4Ewc1tYCLA2xj6crywHoCxxBngenJNUsPyzHGm3meL', 145 | payload: { 146 | '@type': '/Files', 147 | 'block': 'QmeU7xSPq6Bewh6PdwHqGHhoErFH1UBQV5Aqb7VYP9ugey', 148 | 'target': 'QmUMWopyRDwZYJznyyhp1TmbAJhLV3z2Xe9dSeqr6LNvV5', 149 | 'date': '2019-04-01T21:27:37.422127Z', 150 | 'user': { 151 | address: 'P4RaCnWZYhKCztWK7mii1WQZmXVAtBVdRAyBU6Em51agxTfW', 152 | name: 'displayname', 153 | avatar: 'QmUMWopyRDwZYJznyyhp1TmbAJhLV3z2Xe9dSeqr6LNvV5' 154 | }, 155 | 'files': [ 156 | { 157 | links: { 158 | large: { 159 | mill: '/image/resize', 160 | checksum: 'B9RebNwLPTXQJymnjEJsYsFuS1dAuJBpBnHT1sB2LLVQ', 161 | source: 'B9RebNwLPTXQJymnjEJsYsFuS1dAuJBpBnHT1sB2LLVQ', 162 | opts: 'Atk8eCEgX98dPBCieRvUUBkH1nA7YiX6AxK1ZjbA6mY', 163 | hash: 'QmVhJPrbb6iD9BYkUcqyDepuAw1wGsudhZLKapfZGCuhKG', 164 | media: 'image/png', 165 | size: '7270', 166 | added: '2019-04-01T21:27:37.378108Z', 167 | meta: { 168 | height: 128, 169 | width: 128 170 | }, 171 | targets: [ 172 | 'QmUMWopyRDwZYJznyyhp1TmbAJhLV3z2Xe9dSeqr6LNvV5' 173 | ] 174 | }, 175 | small: { 176 | mill: '/image/resize', 177 | checksum: '5i7tM447RozmhG1zbYvWGb8UXRRa57DSKJAJSLiUsSYJ', 178 | source: 'B9RebNwLPTXQJymnjEJsYsFuS1dAuJBpBnHT1sB2LLVQ', 179 | opts: 'Av7wdZtZ5P8rWQtuTkUPQ8iBmRK1PyfxPcVeDtAjb2uR', 180 | hash: 'QmY2cqoiCNwNcf6Sf6tEUbA76V64zkcRbkdB8JRAWYafH2', 181 | media: 'image/png', 182 | size: '8125', 183 | added: '2019-04-01T21:27:37.387214Z', 184 | meta: { 185 | height: 100, 186 | width: 100 187 | }, 188 | targets: [ 189 | 'QmUMWopyRDwZYJznyyhp1TmbAJhLV3z2Xe9dSeqr6LNvV5' 190 | ] 191 | } 192 | } 193 | } 194 | ], 195 | 'threads': [ 196 | '12D3KooWRW4Ewc1tYCLA2xj6crywHoCxxBngenJNUsPyzHGm3meL' 197 | ] 198 | } 199 | }, 200 | { 201 | block: 'QmU8GrBr3zNmqcHCP3UhMEueiZSEg1wypQ5E61t84t7TsV', 202 | thread: '12D3KooWRW4Ewc1tYCLA2xj6crywHoCxxBngenJNUsPyzHGm3meL', 203 | payload: { 204 | '@type': '/Join', 205 | 'block': 'QmU8GrBr3zNmqcHCP3UhMEueiZSEg1wypQ5E61t84t7TsV', 206 | 'date': '2019-04-01T21:27:37.363373Z', 207 | 'user': { 208 | address: 'P4RaCnWZYhKCztWK7mii1WQZmXVAtBVdRAyBU6Em51agxTfW', 209 | name: 'displayname', 210 | avatar: 'QmUMWopyRDwZYJznyyhp1TmbAJhLV3z2Xe9dSeqr6LNvV5' 211 | } 212 | } 213 | } 214 | ], 215 | count: 3, 216 | next: 'next' 217 | } 218 | -------------------------------------------------------------------------------- /docs/interfaces/cafenonce.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | CafeNonce | @textile/js-http-client 7 | 8 | 9 | 10 | 11 | 12 |
13 |
14 |
15 |
16 | 27 |
28 |
29 | Options 30 |
31 |
32 | All 33 |
    34 |
  • Public
  • 35 |
  • Public/Protected
  • 36 |
  • All
  • 37 |
38 |
39 | 40 | 41 | 42 | 43 |
44 |
45 | Menu 46 |
47 |
48 |
49 |
50 |
51 |
52 | 60 |

Interface CafeNonce

61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |

Hierarchy

69 |
    70 |
  • 71 | CafeNonce 72 |
  • 73 |
74 |
75 |
76 |

Index

77 |
78 |
79 |
80 |

Properties

81 | 84 |
85 |
86 |
87 |
88 |
89 |

Properties

90 |
91 | 92 |

value

93 |
value: string
94 | 99 |
100 |
101 |
102 | 127 |
128 |
129 |
130 |
131 |

Legend

132 |
133 |
    134 |
  • Module
  • 135 |
  • Object literal
  • 136 |
  • Variable
  • 137 |
  • Function
  • 138 |
  • Function with type parameter
  • 139 |
  • Index signature
  • 140 |
  • Type alias
  • 141 |
142 |
    143 |
  • Enumeration
  • 144 |
  • Enumeration member
  • 145 |
  • Property
  • 146 |
  • Method
  • 147 |
148 |
    149 |
  • Interface
  • 150 |
  • Interface with type parameter
  • 151 |
  • Constructor
  • 152 |
  • Property
  • 153 |
  • Method
  • 154 |
  • Index signature
  • 155 |
156 |
    157 |
  • Class
  • 158 |
  • Class with type parameter
  • 159 |
  • Constructor
  • 160 |
  • Property
  • 161 |
  • Method
  • 162 |
  • Accessor
  • 163 |
  • Index signature
  • 164 |
165 |
    166 |
  • Inherited constructor
  • 167 |
  • Inherited property
  • 168 |
  • Inherited method
  • 169 |
  • Inherited accessor
  • 170 |
171 |
    172 |
  • Protected property
  • 173 |
  • Protected method
  • 174 |
  • Protected accessor
  • 175 |
176 |
    177 |
  • Private property
  • 178 |
  • Private method
  • 179 |
  • Private accessor
  • 180 |
181 |
    182 |
  • Static property
  • 183 |
  • Static method
  • 184 |
185 |
186 |
187 |
188 |
189 | 190 | 191 | 192 | -------------------------------------------------------------------------------- /docs/interfaces/cafestoreack.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | CafeStoreAck | @textile/js-http-client 7 | 8 | 9 | 10 | 11 | 12 |
13 |
14 |
15 |
16 | 27 |
28 |
29 | Options 30 |
31 |
32 | All 33 |
    34 |
  • Public
  • 35 |
  • Public/Protected
  • 36 |
  • All
  • 37 |
38 |
39 | 40 | 41 | 42 | 43 |
44 |
45 | Menu 46 |
47 |
48 |
49 |
50 |
51 |
52 | 60 |

Interface CafeStoreAck

61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |

Hierarchy

69 |
    70 |
  • 71 | CafeStoreAck 72 |
  • 73 |
74 |
75 |
76 |

Index

77 |
78 |
79 |
80 |

Properties

81 |
    82 |
  • id
  • 83 |
84 |
85 |
86 |
87 |
88 |
89 |

Properties

90 |
91 | 92 |

id

93 |
id: string
94 | 99 |
100 |
101 |
102 | 127 |
128 |
129 |
130 |
131 |

Legend

132 |
133 |
    134 |
  • Module
  • 135 |
  • Object literal
  • 136 |
  • Variable
  • 137 |
  • Function
  • 138 |
  • Function with type parameter
  • 139 |
  • Index signature
  • 140 |
  • Type alias
  • 141 |
142 |
    143 |
  • Enumeration
  • 144 |
  • Enumeration member
  • 145 |
  • Property
  • 146 |
  • Method
  • 147 |
148 |
    149 |
  • Interface
  • 150 |
  • Interface with type parameter
  • 151 |
  • Constructor
  • 152 |
  • Property
  • 153 |
  • Method
  • 154 |
  • Index signature
  • 155 |
156 |
    157 |
  • Class
  • 158 |
  • Class with type parameter
  • 159 |
  • Constructor
  • 160 |
  • Property
  • 161 |
  • Method
  • 162 |
  • Accessor
  • 163 |
  • Index signature
  • 164 |
165 |
    166 |
  • Inherited constructor
  • 167 |
  • Inherited property
  • 168 |
  • Inherited method
  • 169 |
  • Inherited accessor
  • 170 |
171 |
    172 |
  • Protected property
  • 173 |
  • Protected method
  • 174 |
  • Protected accessor
  • 175 |
176 |
    177 |
  • Private property
  • 178 |
  • Private method
  • 179 |
  • Private accessor
  • 180 |
181 |
    182 |
  • Static property
  • 183 |
  • Static method
  • 184 |
185 |
186 |
187 |
188 |
189 | 190 | 191 | 192 | -------------------------------------------------------------------------------- /docs/interfaces/threadflag.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | ThreadFlag | @textile/js-http-client 7 | 8 | 9 | 10 | 11 | 12 |
13 |
14 |
15 |
16 | 27 |
28 |
29 | Options 30 |
31 |
32 | All 33 |
    34 |
  • Public
  • 35 |
  • Public/Protected
  • 36 |
  • All
  • 37 |
38 |
39 | 40 | 41 | 42 | 43 |
44 |
45 | Menu 46 |
47 |
48 |
49 |
50 |
51 |
52 | 60 |

Interface ThreadFlag

61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |

Hierarchy

69 |
    70 |
  • 71 | ThreadFlag 72 |
  • 73 |
74 |
75 |
76 |

Index

77 |
78 |
79 |
80 |

Properties

81 | 84 |
85 |
86 |
87 |
88 |
89 |

Properties

90 |
91 | 92 |

target

93 |
target: string
94 | 99 |
100 |
101 |
102 | 127 |
128 |
129 |
130 |
131 |

Legend

132 |
133 |
    134 |
  • Module
  • 135 |
  • Object literal
  • 136 |
  • Variable
  • 137 |
  • Function
  • 138 |
  • Function with type parameter
  • 139 |
  • Index signature
  • 140 |
  • Type alias
  • 141 |
142 |
    143 |
  • Enumeration
  • 144 |
  • Enumeration member
  • 145 |
  • Property
  • 146 |
  • Method
  • 147 |
148 |
    149 |
  • Interface
  • 150 |
  • Interface with type parameter
  • 151 |
  • Constructor
  • 152 |
  • Property
  • 153 |
  • Method
  • 154 |
  • Index signature
  • 155 |
156 |
    157 |
  • Class
  • 158 |
  • Class with type parameter
  • 159 |
  • Constructor
  • 160 |
  • Property
  • 161 |
  • Method
  • 162 |
  • Accessor
  • 163 |
  • Index signature
  • 164 |
165 |
    166 |
  • Inherited constructor
  • 167 |
  • Inherited property
  • 168 |
  • Inherited method
  • 169 |
  • Inherited accessor
  • 170 |
171 |
    172 |
  • Protected property
  • 173 |
  • Protected method
  • 174 |
  • Protected accessor
  • 175 |
176 |
    177 |
  • Private property
  • 178 |
  • Private method
  • 179 |
  • Private accessor
  • 180 |
181 |
    182 |
  • Static property
  • 183 |
  • Static method
  • 184 |
185 |
186 |
187 |
188 |
189 | 190 | 191 | 192 | -------------------------------------------------------------------------------- /docs/interfaces/threadlike.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | ThreadLike | @textile/js-http-client 7 | 8 | 9 | 10 | 11 | 12 |
13 |
14 |
15 |
16 | 27 |
28 |
29 | Options 30 |
31 |
32 | All 33 |
    34 |
  • Public
  • 35 |
  • Public/Protected
  • 36 |
  • All
  • 37 |
38 |
39 | 40 | 41 | 42 | 43 |
44 |
45 | Menu 46 |
47 |
48 |
49 |
50 |
51 |
52 | 60 |

Interface ThreadLike

61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |

Hierarchy

69 |
    70 |
  • 71 | ThreadLike 72 |
  • 73 |
74 |
75 |
76 |

Index

77 |
78 |
79 |
80 |

Properties

81 | 84 |
85 |
86 |
87 |
88 |
89 |

Properties

90 |
91 | 92 |

target

93 |
target: string
94 | 99 |
100 |
101 |
102 | 127 |
128 |
129 |
130 |
131 |

Legend

132 |
133 |
    134 |
  • Module
  • 135 |
  • Object literal
  • 136 |
  • Variable
  • 137 |
  • Function
  • 138 |
  • Function with type parameter
  • 139 |
  • Index signature
  • 140 |
  • Type alias
  • 141 |
142 |
    143 |
  • Enumeration
  • 144 |
  • Enumeration member
  • 145 |
  • Property
  • 146 |
  • Method
  • 147 |
148 |
    149 |
  • Interface
  • 150 |
  • Interface with type parameter
  • 151 |
  • Constructor
  • 152 |
  • Property
  • 153 |
  • Method
  • 154 |
  • Index signature
  • 155 |
156 |
    157 |
  • Class
  • 158 |
  • Class with type parameter
  • 159 |
  • Constructor
  • 160 |
  • Property
  • 161 |
  • Method
  • 162 |
  • Accessor
  • 163 |
  • Index signature
  • 164 |
165 |
    166 |
  • Inherited constructor
  • 167 |
  • Inherited property
  • 168 |
  • Inherited method
  • 169 |
  • Inherited accessor
  • 170 |
171 |
    172 |
  • Protected property
  • 173 |
  • Protected method
  • 174 |
  • Protected accessor
  • 175 |
176 |
    177 |
  • Private property
  • 178 |
  • Private method
  • 179 |
  • Private accessor
  • 180 |
181 |
    182 |
  • Static property
  • 183 |
  • Static method
  • 184 |
185 |
186 |
187 |
188 |
189 | 190 | 191 | 192 | --------------------------------------------------------------------------------