├── .prettierrc.json ├── .coveralls.yml ├── .gitignore ├── src ├── index.ts ├── arena_service.ts └── arena_api_types.ts ├── docs ├── .nojekyll ├── assets │ ├── navigation.js │ ├── highlight.css │ └── search.js └── types │ ├── GetUserApiResponse.html │ ├── CreateBlockCommentApiResponse.html │ ├── GetUserFollowingApiResponse.html │ ├── ChannelStatus.html │ ├── CreateBlockApiResponse.html │ ├── CreateChannelApiResponse.html │ ├── ChannelConnectBlockApiResponse.html │ └── ChannelConnectChannelApiResponse.html ├── .travis.yml ├── .yarnrc.yml ├── tsconfig.build.json ├── jest.config.js ├── .eslintrc.json ├── package.json ├── README.md ├── tsconfig.json └── tests └── arena_service.test.ts /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true 3 | } 4 | -------------------------------------------------------------------------------- /.coveralls.yml: -------------------------------------------------------------------------------- 1 | repo_token: sawcdrsMiNznfZBuUyQ6k2LulePXX7LcM 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .yarn/ 3 | node_modules 4 | coverage 5 | dist 6 | *.log 7 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './arena_api_types'; 2 | export * from './arena_service'; 3 | -------------------------------------------------------------------------------- /docs/.nojekyll: -------------------------------------------------------------------------------- 1 | TypeDoc added this file to prevent GitHub Pages from using Jekyll. You can turn off this behavior by setting the `githubPages` option to false. -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "10" 4 | - "12" 5 | - "14" 6 | before_script: "yarn lint" 7 | script: "yarn test" 8 | after_script: "yarn coverage" 9 | -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: node-modules 2 | 3 | plugins: 4 | - path: .yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs 5 | spec: "@yarnpkg/plugin-interactive-tools" 6 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 5 | "rootDir": "./src" 6 | }, 7 | "typedocOptions": { 8 | "inputFiles": ["./src"], 9 | "mode": "modules", 10 | "out": "docs", 11 | "theme": "default" 12 | }, 13 | "include": ["./src/"], 14 | "exclude": ["./dist/", "./test/"] 15 | } 16 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | testEnvironment: 'node', 4 | collectCoverage: true, 5 | collectCoverageFrom: ['src/**/*.ts'], 6 | transform: { 7 | '^.+\\.tsx?$': 'ts-jest' 8 | }, 9 | testRegex: 'test.ts', 10 | moduleFileExtensions: ['ts', 'js'], 11 | coverageThreshold: { 12 | global: { 13 | branches: 50, 14 | functions: 40, 15 | lines: 50, 16 | statements: 50 17 | } 18 | }, 19 | } 20 | -------------------------------------------------------------------------------- /docs/assets/navigation.js: -------------------------------------------------------------------------------- 1 | window.navigationData = "data:application/octet-stream;base64,H4sIAAAAAAAAE5WXXW+bMBSG/wvX1bZG6j56l6ZpV2nVprXTLqZdOOQ0WAEb2Sfqqmn/fYSCCfjjHO4i3sfP6xgD8q+/GcIfzC6zpQElVqUEhdlZVgssmot5KawF+/YkfFNgVTbEXqptdnm++PjvzDk+I9ZrY7TxDS5KjW9rlrUchkuFYJ5E3s+hCceCxcX7qeCq1Pk+aekJUrUqhFJQJmUDQ+pujT7USVlPkKoHECYvki6HkLIfFkxS1QGkaIko8qIa7SF8qd3dc/HE9O7Th/OLRdzW3jBC2TIs75WwEDe6lOeKe+Y5VrqKrtspwDJ2ezIo67I5npVu9oNCm/L1zBzvT4nFNaCQZVJ9gvHsryu1VijxJSw+JVjOdbWBbdDVJixH+3AHHW3CctxVYgdBR5vwHfGNO8Qs2xep9nGZS1mue9jKxCM1xCzb12cF5k496aDMpSzXY/M7PjGXslzHt2lQcwzYBurhmTCUd3iMFeTYfyG/g621st6OS9PzuoYPKK/N55l9Dyjw4C3WKCRNr1OQWl0LFJ5qlJIuAwKBXOkgNcPdvet4FT7Ma2LcwwhH+ZuV9FzHa9S4G8C8mA5sL1Ijb4Hc/gGEa+1WwDLsAZTdQt70OMnomHzz0zVxmN/0WByqDatmSvI7eH9j7vzdK4HSB0FGQ39kSLinCNfKW5gYymjpzhUJ+YRgOnkTj5DMjhtdlvoZDKMkhM5qkWrHbRmjVMs9JLyjkDJ9EzupxHH/NkcyIzcHBO9TG2IorzvFxmbpAWHj7//jpYkT6xAAAA==" -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "plugins": ["prettier", "@typescript-eslint"], 4 | "extends": [ 5 | "plugin:@typescript-eslint/recommended", 6 | "prettier", 7 | "plugin:prettier/recommended" 8 | ], 9 | "ignorePatterns": ["dist/**"], 10 | "parserOptions": { 11 | "ecmaVersion": 2018, 12 | "sourceType": "module" 13 | }, 14 | "rules": { 15 | "array-callback-return": "off", 16 | "@typescript-eslint/no-use-before-define": "off", 17 | "@typescript-eslint/explicit-function-return-type": "off", 18 | "@typescript-eslint/no-explicit-any": "off", 19 | "@typescript-eslint/explicit-module-boundary-types": "off", 20 | "prettier/prettier": "warn" 21 | }, 22 | "overrides": [{ 23 | "files": ["**/*.test.ts"], 24 | "extends": ["plugin:jest/recommended"], 25 | "plugins": ["jest"] 26 | }] 27 | } 28 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "arena-ts", 3 | "version": "1.0.2", 4 | "description": "A typescript API client for Are.na", 5 | "main": "dist/index.js", 6 | "typings": "dist/index.d.ts", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/e-e-e/arena-ts.git" 10 | }, 11 | "scripts": { 12 | "build": "tsc -b tsconfig.build.json", 13 | "dev": "tsc -b tsconfig.build.json -w", 14 | "docs": "typedoc", 15 | "lint": "eslint ./src --fix", 16 | "test": "jest", 17 | "coverage": "cat ./coverage/lcov.info | coveralls", 18 | "test:watch": "jest --watch", 19 | "prepublish": "yarn lint && yarn build && yarn docs" 20 | }, 21 | "files": [ 22 | "dist/" 23 | ], 24 | "author": "Benjamin Forster", 25 | "license": "MIT", 26 | "devDependencies": { 27 | "@types/jest": "^29.5.8", 28 | "@types/node-fetch": "^2.6.1", 29 | "@typescript-eslint/eslint-plugin": "^6.10.0", 30 | "@typescript-eslint/parser": "^6.10.0", 31 | "coveralls": "^3.1.1", 32 | "eslint": "^8.14.0", 33 | "eslint-config-prettier": "^9.0.0", 34 | "eslint-plugin-jest": "^27.6.0", 35 | "eslint-plugin-prettier": "^5.0.1", 36 | "jest": "^29.7.0", 37 | "jest-fetch-mock": "^3.0.3", 38 | "node-fetch": "^2.6.7", 39 | "prettier": "^3.1.0", 40 | "ts-jest": "^29.1.1", 41 | "ts-node": "^10.7.0", 42 | "typedoc": "^0.25.3", 43 | "typescript": "^5.2.2", 44 | "typescript-eslint": "^0.0.1-alpha.0" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # arena-ts 2 | 3 | [![Build Status](https://travis-ci.org/e-e-e/arena-ts.svg?branch=master)](https://travis-ci.org/e-e-e/arena-ts) 4 | [![Coverage Status](https://coveralls.io/repos/github/e-e-e/arena-ts/badge.svg?branch=master)](https://coveralls.io/github/e-e-e/arena-ts?branch=master) 5 | 6 | A typescript client for [are.na](are.na). Compatible in node and browser environments. 7 | 8 | Minimum node version 18. 9 | 10 | Prior art: [arena-js](https://github.com/ivangreene/arena-js). 11 | 12 | **Note:** This is an unofficial client and typing information has been derived by comparing the official documentation 13 | with response types from use. These may change over time or depending on context, if you notice any discrepancies please 14 | let us know. Contributions are welcome. 15 | 16 | ### Installation 17 | 18 | ```bash 19 | // using npm 20 | npm install arena-ts 21 | // using yarn 22 | yarn add arena-ts 23 | 24 | ``` 25 | 26 | ### Usage 27 | 28 | ##### Simple Example: 29 | 30 | ```ts 31 | import {ArenaClient} from 'arena-ts'; 32 | 33 | const client = new ArenaClient(); 34 | 35 | client.channels().then(console.log); 36 | ``` 37 | 38 | ### API 39 | 40 | Check out the complete [API Documentation](https://e-e-e.github.io/arena-ts/). 41 | 42 | This is based on [Arena's Restful API](dev.are.na). 43 | 44 | Note: some undocumented endpoints have been added: 45 | 46 | - `client.me()` - gets authenticated users details 47 | - `client.group('groupname').get()` - get group details 48 | - `client.group('groupname').channels()` - group channels 49 | - `client.block(123).comments()` - fetch block comments 50 | - `client.block(123).addComment('comment')` - add new comment to block 51 | - `client.block(123).deleteComment(123)` - delete comment by id 52 | - `client.block(123).updateComment(123, 'new comment')` - update comment by id 53 | -------------------------------------------------------------------------------- /docs/assets/highlight.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --light-hl-0: #795E26; 3 | --dark-hl-0: #DCDCAA; 4 | --light-hl-1: #000000; 5 | --dark-hl-1: #D4D4D4; 6 | --light-hl-2: #A31515; 7 | --dark-hl-2: #CE9178; 8 | --light-hl-3: #AF00DB; 9 | --dark-hl-3: #C586C0; 10 | --light-hl-4: #001080; 11 | --dark-hl-4: #9CDCFE; 12 | --light-hl-5: #0000FF; 13 | --dark-hl-5: #569CD6; 14 | --light-hl-6: #0070C1; 15 | --dark-hl-6: #4FC1FF; 16 | --light-code-background: #FFFFFF; 17 | --dark-code-background: #1E1E1E; 18 | } 19 | 20 | @media (prefers-color-scheme: light) { :root { 21 | --hl-0: var(--light-hl-0); 22 | --hl-1: var(--light-hl-1); 23 | --hl-2: var(--light-hl-2); 24 | --hl-3: var(--light-hl-3); 25 | --hl-4: var(--light-hl-4); 26 | --hl-5: var(--light-hl-5); 27 | --hl-6: var(--light-hl-6); 28 | --code-background: var(--light-code-background); 29 | } } 30 | 31 | @media (prefers-color-scheme: dark) { :root { 32 | --hl-0: var(--dark-hl-0); 33 | --hl-1: var(--dark-hl-1); 34 | --hl-2: var(--dark-hl-2); 35 | --hl-3: var(--dark-hl-3); 36 | --hl-4: var(--dark-hl-4); 37 | --hl-5: var(--dark-hl-5); 38 | --hl-6: var(--dark-hl-6); 39 | --code-background: var(--dark-code-background); 40 | } } 41 | 42 | :root[data-theme='light'] { 43 | --hl-0: var(--light-hl-0); 44 | --hl-1: var(--light-hl-1); 45 | --hl-2: var(--light-hl-2); 46 | --hl-3: var(--light-hl-3); 47 | --hl-4: var(--light-hl-4); 48 | --hl-5: var(--light-hl-5); 49 | --hl-6: var(--light-hl-6); 50 | --code-background: var(--light-code-background); 51 | } 52 | 53 | :root[data-theme='dark'] { 54 | --hl-0: var(--dark-hl-0); 55 | --hl-1: var(--dark-hl-1); 56 | --hl-2: var(--dark-hl-2); 57 | --hl-3: var(--dark-hl-3); 58 | --hl-4: var(--dark-hl-4); 59 | --hl-5: var(--dark-hl-5); 60 | --hl-6: var(--dark-hl-6); 61 | --code-background: var(--dark-code-background); 62 | } 63 | 64 | .hl-0 { color: var(--hl-0); } 65 | .hl-1 { color: var(--hl-1); } 66 | .hl-2 { color: var(--hl-2); } 67 | .hl-3 { color: var(--hl-3); } 68 | .hl-4 { color: var(--hl-4); } 69 | .hl-5 { color: var(--hl-5); } 70 | .hl-6 { color: var(--hl-6); } 71 | pre, code { background: var(--code-background); } 72 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */ 4 | "target": "es5", 5 | /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ 6 | "module": "commonjs", 7 | "lib": [ 8 | "es2017", 9 | "es7", 10 | "es6", 11 | "dom" 12 | ], 13 | /* Generates corresponding '.d.ts' file. */ 14 | "declaration": true, 15 | /* Generates a sourcemap for each corresponding '.d.ts' file. */ 16 | "declarationMap": true, 17 | /* Generates corresponding '.map' file. */ 18 | "sourceMap": true, 19 | /* Redirect output structure to the directory. */ 20 | "outDir": "./dist", 21 | /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 22 | "downlevelIteration": true, 23 | /* Enable all strict type-checking options. */ 24 | "strict": true, 25 | /* Raise error on expressions and declarations with an implied 'any' type. */ 26 | "noImplicitAny": true, 27 | /* Parse in strict mode and emit "use strict" for each source file. */ 28 | "alwaysStrict": true, 29 | /* Report errors on unused locals. */ 30 | "noUnusedLocals": true, 31 | /* Report error when not all code paths in function return a value. */ 32 | "noImplicitReturns": false, 33 | /* Report errors for fallthrough cases in switch statement. */ 34 | "noFallthroughCasesInSwitch": true, 35 | /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 36 | "moduleResolution": "node", 37 | /* Base directory to resolve non-absolute module names. */ 38 | "baseUrl": "./src", 39 | /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 40 | "esModuleInterop": true, 41 | /* Skip type checking of declaration files. */ 42 | "skipLibCheck": true, 43 | /* Disallow inconsistently-cased references to the same file. */ 44 | "forceConsistentCasingInFileNames": true 45 | }, 46 | "typedocOptions": { 47 | "out": "docs", 48 | "excludeExternals": true, 49 | "excludePrivate": true, 50 | "excludeProtected": true, 51 | "entryPoints": [ 52 | "./src/index.ts" 53 | ], 54 | "theme": "default", 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /docs/assets/search.js: -------------------------------------------------------------------------------- 1 | window.searchData = "data:application/octet-stream;base64,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"; -------------------------------------------------------------------------------- /tests/arena_service.test.ts: -------------------------------------------------------------------------------- 1 | import { ArenaClient } from '../src'; 2 | import fetchMock from 'jest-fetch-mock'; 3 | 4 | function expectHttpError(message: string, status: number) { 5 | return expect.objectContaining({ 6 | name: 'HttpError', 7 | message, 8 | status, 9 | }); 10 | } 11 | 12 | describe('ArenaClient', () => { 13 | const fakeResponseBody = { some: 'data' }; 14 | const fakeDate = { now: () => 12345 }; 15 | 16 | function createFetchMockWithSimpleResponse() { 17 | return fetchMock.mockResponse(JSON.stringify(fakeResponseBody), { 18 | status: 200, 19 | }); 20 | } 21 | 22 | function createFetchMockWithErrorResponse(status: number) { 23 | return fetchMock.mockResponse(JSON.stringify(''), { 24 | status, 25 | }); 26 | } 27 | 28 | beforeEach(() => { 29 | fetchMock.mockReset(); 30 | }); 31 | 32 | describe('me', () => { 33 | it('throws 401 HttpError when not authenticated', async () => { 34 | const fetch = createFetchMockWithErrorResponse(401); 35 | const client = new ArenaClient({ fetch }); 36 | await expect(client.me()).rejects.toThrowError( 37 | expectHttpError('Unauthorized', 401) 38 | ); 39 | expect(fetch).toBeCalledTimes(1); 40 | expect(fetch).toBeCalledWith('https://api.are.na/v2/me', { 41 | headers: expect.objectContaining({ 42 | Authorization: '', 43 | 'Content-Type': 'application/json', 44 | }), 45 | method: 'GET', 46 | }); 47 | }); 48 | 49 | it('calls GET /me with authentication and returns body as JSON', async () => { 50 | const fetch = createFetchMockWithSimpleResponse(); 51 | const client = new ArenaClient({ token: 'MY_API_TOKEN', fetch }); 52 | await expect(client.me()).resolves.toMatchObject(fakeResponseBody); 53 | expect(fetch).toBeCalledTimes(1); 54 | expect(fetch).toBeCalledWith('https://api.are.na/v2/me', { 55 | headers: expect.objectContaining({ 56 | Authorization: 'Bearer MY_API_TOKEN', 57 | 'Content-Type': 'application/json', 58 | }), 59 | method: 'GET', 60 | }); 61 | }); 62 | }); 63 | 64 | describe('channels', () => { 65 | describe('with user id', () => { 66 | it('calls GET /users/:user/channel', async () => { 67 | const fetch = createFetchMockWithSimpleResponse(); 68 | const client = new ArenaClient({ 69 | token: 'MY_API_TOKEN', 70 | fetch, 71 | date: fakeDate, 72 | }); 73 | await expect( 74 | client.user(123456).channels() 75 | ).resolves.toMatchObject(fakeResponseBody); 76 | expect(fetch).toBeCalledTimes(1); 77 | expect(fetch).toBeCalledWith( 78 | expect.stringMatching( 79 | 'https://api.are.na/v2/users/123456/channels?' 80 | ), 81 | { 82 | headers: { 83 | Authorization: 'Bearer MY_API_TOKEN', 84 | 'Content-Type': 'application/json', 85 | }, 86 | method: 'GET', 87 | } 88 | ); 89 | }); 90 | }); 91 | 92 | describe('without user id', () => { 93 | it('calls GET /channels', async () => { 94 | const fetch = createFetchMockWithSimpleResponse(); 95 | const client = new ArenaClient({ 96 | token: 'MY_API_TOKEN', 97 | fetch, 98 | date: fakeDate, 99 | }); 100 | await expect(client.channels()).resolves.toMatchObject( 101 | fakeResponseBody 102 | ); 103 | expect(fetch).toBeCalledTimes(1); 104 | expect(fetch).toBeCalledWith( 105 | expect.stringMatching('https://api.are.na/v2/channels?'), 106 | { 107 | headers: { 108 | Authorization: 'Bearer MY_API_TOKEN', 109 | 'Content-Type': 'application/json', 110 | }, 111 | method: 'GET', 112 | } 113 | ); 114 | }); 115 | }); 116 | describe('pagination options', () => { 117 | it('appends date as query string if forceRefresh option is true', async () => { 118 | const fetch = createFetchMockWithSimpleResponse(); 119 | const client = new ArenaClient({ 120 | token: 'MY_API_TOKEN', 121 | fetch, 122 | date: fakeDate, 123 | }); 124 | await expect( 125 | client.channels({ forceRefresh: true }) 126 | ).resolves.toMatchObject(fakeResponseBody); 127 | expect(fetch).toBeCalledTimes(1); 128 | expect(fetch).toBeCalledWith(expect.stringContaining('date=12345'), { 129 | headers: { 130 | Authorization: 'Bearer MY_API_TOKEN', 131 | 'Content-Type': 'application/json', 132 | }, 133 | method: 'GET', 134 | }); 135 | }); 136 | }); 137 | }); 138 | 139 | describe('channel', () => { 140 | it('calls GET /channel/:channel', async () => { 141 | const fetch = createFetchMockWithSimpleResponse(); 142 | const client = new ArenaClient({ 143 | token: 'MY_API_TOKEN', 144 | fetch, 145 | date: fakeDate, 146 | }); 147 | await expect( 148 | client.channel('fake-channel-id').get() 149 | ).resolves.toMatchObject(fakeResponseBody); 150 | expect(fetch).toBeCalledTimes(1); 151 | expect(fetch).toBeCalledWith( 152 | expect.stringMatching('https://api.are.na/v2/channels/fake-channel-id'), 153 | { 154 | headers: { 155 | Authorization: 'Bearer MY_API_TOKEN', 156 | 'Content-Type': 'application/json', 157 | }, 158 | method: 'GET', 159 | } 160 | ); 161 | }); 162 | it('accepts pagination parameters', async () => { 163 | const fetch = createFetchMockWithSimpleResponse(); 164 | const client = new ArenaClient({ 165 | fetch, 166 | date: fakeDate, 167 | }); 168 | await expect( 169 | client 170 | .channel('fake-channel-id') 171 | .get({ per: 1, page: 1, direction: 'asc' }) 172 | ).resolves.toMatchObject(fakeResponseBody); 173 | expect(fetch).toBeCalledTimes(1); 174 | expect(fetch).toBeCalledWith( 175 | 'https://api.are.na/v2/channels/fake-channel-id?page=1&per=1&sort=position&direction=asc', 176 | { 177 | headers: { 178 | Authorization: '', 179 | 'Content-Type': 'application/json', 180 | }, 181 | method: 'GET', 182 | } 183 | ); 184 | }); 185 | 186 | describe('pagination options', () => { 187 | it('appends date as query string if forceRefresh option is true', async () => { 188 | const fetch = createFetchMockWithSimpleResponse(); 189 | const client = new ArenaClient({ 190 | token: 'MY_API_TOKEN', 191 | fetch, 192 | date: fakeDate, 193 | }); 194 | await expect( 195 | client.channel('test').get({ forceRefresh: true }) 196 | ).resolves.toMatchObject(fakeResponseBody); 197 | expect(fetch).toBeCalledTimes(1); 198 | expect(fetch).toBeCalledWith(expect.stringContaining('date=12345'), { 199 | headers: { 200 | Authorization: 'Bearer MY_API_TOKEN', 201 | 'Content-Type': 'application/json', 202 | }, 203 | method: 'GET', 204 | }); 205 | }); 206 | }); 207 | }); 208 | 209 | describe('block', () => { 210 | it('calls GET /block/:block', async () => { 211 | const fetch = createFetchMockWithSimpleResponse(); 212 | const client = new ArenaClient({ 213 | token: 'MY_API_TOKEN', 214 | fetch, 215 | date: fakeDate, 216 | }); 217 | await expect(client.block(3).get()).resolves.toMatchObject( 218 | fakeResponseBody 219 | ); 220 | expect(fetch).toBeCalledTimes(1); 221 | expect(fetch).toBeCalledWith( 222 | expect.stringMatching('https://api.are.na/v2/blocks/3'), 223 | { 224 | headers: { 225 | Authorization: 'Bearer MY_API_TOKEN', 226 | 'Content-Type': 'application/json', 227 | }, 228 | method: 'GET', 229 | } 230 | ); 231 | }); 232 | }); 233 | 234 | describe('connect', () => { 235 | it('throws 401 HttpError when not authenticated', async () => { 236 | const fetch = createFetchMockWithErrorResponse(401); 237 | const client = new ArenaClient({ fetch }); 238 | await expect( 239 | client.channel('fake-channel-id').connect.block(4) 240 | ).rejects.toThrowError(expectHttpError('Unauthorized', 401)); 241 | expect(fetch).toBeCalledTimes(1); 242 | expect(fetch).toBeCalledWith( 243 | 'https://api.are.na/v2/channels/fake-channel-id/connections', 244 | { 245 | body: '{"connectable_type":"Block","connectable_id":4}', 246 | headers: { 247 | Authorization: '', 248 | 'Content-Type': 'application/json', 249 | }, 250 | method: 'POST', 251 | } 252 | ); 253 | }); 254 | 255 | it('calls POST /channels/:channel/connections', async () => { 256 | const fetch = createFetchMockWithSimpleResponse(); 257 | const client = new ArenaClient({ 258 | token: 'MY_API_TOKEN', 259 | fetch, 260 | date: fakeDate, 261 | }); 262 | await expect( 263 | client.channel('fake-channel-id').connect.channel(3) 264 | ).resolves.toMatchObject(fakeResponseBody); 265 | expect(fetch).toBeCalledTimes(1); 266 | expect(fetch).toBeCalledWith( 267 | 'https://api.are.na/v2/channels/fake-channel-id/connections', 268 | { 269 | body: '{"connectable_type":"Channel","connectable_id":3}', 270 | headers: { 271 | Authorization: 'Bearer MY_API_TOKEN', 272 | 'Content-Type': 'application/json', 273 | }, 274 | method: 'POST', 275 | } 276 | ); 277 | }); 278 | describe('remove', () => { 279 | it('throws 401 HttpError when not authenticated', async () => { 280 | const fetch = createFetchMockWithErrorResponse(401); 281 | const client = new ArenaClient({ fetch }); 282 | await expect( 283 | client.channel('fake-channel-id').delete() 284 | ).rejects.toThrowError(expectHttpError('Unauthorized', 401)); 285 | expect(fetch).toBeCalledTimes(1); 286 | expect(fetch).toBeCalledWith( 287 | 'https://api.are.na/v2/channels/fake-channel-id', 288 | { 289 | headers: { 290 | Authorization: '', 291 | 'Content-Type': 'application/json', 292 | }, 293 | method: 'DELETE', 294 | } 295 | ); 296 | }); 297 | 298 | describe('with block id', () => { 299 | it('calls DELETE /channels/:channel/', async () => { 300 | const fetch = createFetchMockWithSimpleResponse(); 301 | const client = new ArenaClient({ 302 | token: 'MY_API_TOKEN', 303 | fetch, 304 | date: fakeDate, 305 | }); 306 | await expect( 307 | client.channel('fake-channel-id').disconnect.block(3) 308 | ).resolves.toBeUndefined(); 309 | expect(fetch).toBeCalledTimes(1); 310 | expect(fetch).toBeCalledWith( 311 | 'https://api.are.na/v2/channels/fake-channel-id/blocks/3', 312 | { 313 | headers: { 314 | Authorization: 'Bearer MY_API_TOKEN', 315 | 'Content-Type': 'application/json', 316 | }, 317 | method: 'DELETE', 318 | } 319 | ); 320 | }); 321 | }); 322 | 323 | describe('without block id', () => { 324 | it('calls DELETE /channels/:channel/', async () => { 325 | const fetch = createFetchMockWithSimpleResponse(); 326 | const client = new ArenaClient({ 327 | token: 'MY_API_TOKEN', 328 | fetch, 329 | date: fakeDate, 330 | }); 331 | await expect( 332 | client.channel('fake-channel-id').delete() 333 | ).resolves.toBeUndefined(); 334 | expect(fetch).toBeCalledTimes(1); 335 | expect(fetch).toBeCalledWith( 336 | 'https://api.are.na/v2/channels/fake-channel-id', 337 | { 338 | headers: { 339 | Authorization: 'Bearer MY_API_TOKEN', 340 | 'Content-Type': 'application/json', 341 | }, 342 | method: 'DELETE', 343 | } 344 | ); 345 | }); 346 | }); 347 | }); 348 | }); 349 | }); 350 | -------------------------------------------------------------------------------- /src/arena_service.ts: -------------------------------------------------------------------------------- 1 | import { 2 | GetChannelsApiResponse, 3 | GetConnectionsApiResponse, 4 | MeApiResponse, 5 | PaginationAttributes, 6 | GetGroupApiResponse, 7 | GetGroupChannelsApiResponse, 8 | SearchApiResponse, 9 | GetBlockApiResponse, 10 | CreateBlockApiResponse, 11 | GetBlockChannelsApiResponse, 12 | CreateChannelApiResponse, 13 | GetChannelThumbApiResponse, 14 | GetChannelContentsApiResponse, 15 | ChannelConnectBlockApiResponse, 16 | ChannelConnectChannelApiResponse, 17 | GetUserChannelsApiResponse, 18 | GetUserApiResponse, 19 | GetUserFollowersApiResponse, 20 | GetUserFollowingApiResponse, 21 | GetBlockCommentApiResponse, 22 | CreateBlockCommentApiResponse, 23 | } from './arena_api_types'; 24 | 25 | export class HttpError extends Error { 26 | readonly name = 'HttpError'; 27 | 28 | constructor( 29 | message?: string, 30 | readonly status: number = 500, 31 | ) { 32 | super(message); 33 | } 34 | } 35 | 36 | export interface ArenaBlockApi { 37 | get(): Promise; 38 | 39 | channels( 40 | options?: PaginationAttributes, 41 | ): Promise; 42 | 43 | update(data: { 44 | title?: string; 45 | description?: string; 46 | content?: string; 47 | }): Promise; 48 | 49 | comments(options?: PaginationAttributes): Promise; 50 | 51 | addComment(comment: string): Promise; 52 | 53 | deleteComment(commentId: number): Promise; 54 | 55 | updateComment(commentId: number, comment: string): Promise; 56 | } 57 | 58 | export interface ArenaUserApi { 59 | get(): Promise; 60 | 61 | channels(options?: PaginationAttributes): Promise; 62 | 63 | following(): Promise; 64 | 65 | followers(): Promise; 66 | } 67 | 68 | export type ChannelStatus = 'public' | 'closed' | 'private'; 69 | 70 | export interface ArenaGroupApi { 71 | get(): Promise; 72 | 73 | channels( 74 | options?: PaginationAttributes, 75 | ): Promise; 76 | } 77 | 78 | export interface ArenaChannelApi { 79 | create(status?: ChannelStatus): Promise; 80 | 81 | get(options?: PaginationAttributes): Promise; 82 | 83 | delete(): Promise; 84 | 85 | update(data: { title: string; status?: ChannelStatus }): Promise; 86 | 87 | thumb(): Promise; 88 | 89 | sort: { 90 | block(id: number, position: number): Promise; 91 | channel(id: number, position: number): Promise; 92 | }; 93 | 94 | contents( 95 | options?: PaginationAttributes, 96 | ): Promise; 97 | 98 | createBlock(data: { 99 | source?: string; 100 | content?: string; 101 | description?: string; 102 | }): Promise; 103 | 104 | connect: { 105 | block(id: number): Promise; 106 | channel(id: number): Promise; 107 | }; 108 | disconnect: { 109 | block(id: number): Promise; 110 | connection(id: number): Promise; 111 | }; 112 | 113 | connections( 114 | options?: PaginationAttributes, 115 | ): Promise; 116 | } 117 | 118 | export interface ArenaSearchApi { 119 | everything( 120 | query: string, 121 | options?: PaginationAttributes, 122 | ): Promise; 123 | 124 | users( 125 | query: string, 126 | options?: PaginationAttributes, 127 | ): Promise; 128 | 129 | channels( 130 | query: string, 131 | options?: PaginationAttributes, 132 | ): Promise; 133 | 134 | blocks( 135 | query: string, 136 | options?: PaginationAttributes, 137 | ): Promise; 138 | } 139 | 140 | export interface ArenaApi { 141 | /** 142 | * Fetch information about current authenticated user. 143 | */ 144 | me(): Promise; 145 | 146 | channels(options?: PaginationAttributes): Promise; 147 | 148 | user(id: number | string): ArenaUserApi; 149 | 150 | group(slug: string): ArenaGroupApi; 151 | 152 | channel(slug: string): ArenaChannelApi; 153 | 154 | block(id: number): ArenaBlockApi; 155 | 156 | readonly search: ArenaSearchApi; 157 | } 158 | 159 | export type Fetch = (url: RequestInfo, init?: RequestInit) => Promise; 160 | export type Date = { now(): number }; 161 | 162 | export class ArenaClient implements ArenaApi { 163 | private readonly domain = 'https://api.are.na/v2/'; 164 | private readonly headers: HeadersInit; 165 | private readonly fetch: Fetch; 166 | private readonly date: Date; 167 | 168 | private static defaultPaginationOptions: PaginationAttributes = { 169 | sort: 'position', 170 | direction: 'desc', 171 | per: 50, 172 | }; 173 | 174 | constructor(config?: { token?: string | null; fetch?: Fetch; date?: Date }) { 175 | this.headers = { 176 | 'Content-Type': 'application/json', 177 | Authorization: config?.token ? `Bearer ${config.token}` : '', 178 | }; 179 | this.fetch = config?.fetch || fetch; 180 | this.date = config?.date || Date; 181 | } 182 | 183 | me() { 184 | return this.getJson('me'); 185 | } 186 | 187 | channels(options?: PaginationAttributes): Promise { 188 | return this.getJsonWithPaginationQuery('channels', options); 189 | } 190 | 191 | user(id: number | string): ArenaUserApi { 192 | return { 193 | get: (): Promise => this.getJson(`users/${id}`), 194 | channels: (options?: PaginationAttributes) => 195 | this.getJsonWithPaginationQuery(`users/${id}/channels`, options), 196 | following: (): Promise => 197 | this.getJson(`users/${id}/following`), 198 | followers: (): Promise => 199 | this.getJson(`users/${id}/followers`), 200 | }; 201 | } 202 | 203 | group(slug: string): ArenaGroupApi { 204 | return { 205 | get: () => this.getJson(`groups/${slug}`), 206 | channels: (options?: PaginationAttributes): Promise => { 207 | return this.getJsonWithPaginationQuery( 208 | `groups/${slug}/channels`, 209 | options, 210 | ); 211 | }, 212 | }; 213 | } 214 | 215 | channel(slug: string): ArenaChannelApi { 216 | return { 217 | sort: { 218 | block: (id: number, position: number): Promise => { 219 | return this.sortConnection(slug, id, position, 'Block'); 220 | }, 221 | channel: (id: number, position: number): Promise => { 222 | return this.sortConnection(slug, id, position, 'Channel'); 223 | }, 224 | }, 225 | connect: { 226 | block: (blockId: number) => 227 | this.createConnection(slug, blockId, 'Block'), 228 | channel: (channelId: number) => 229 | this.createConnection(slug, channelId, 'Channel'), 230 | }, 231 | disconnect: { 232 | block: (blockId: number) => 233 | // 204 on success 234 | this.del(`channels/${slug}/blocks/${blockId}`), 235 | connection: async (connectionId) => 236 | this.del(`connections/${connectionId}`), 237 | }, 238 | contents: ( 239 | options?: PaginationAttributes, 240 | ): Promise => { 241 | return this.getJsonWithPaginationQuery( 242 | `channels/${slug}/contents`, 243 | options, 244 | ); 245 | }, 246 | connections: ( 247 | options?: PaginationAttributes, 248 | ): Promise => { 249 | return this.getJsonWithPaginationQuery( 250 | `channels/${slug}/connections`, 251 | options, 252 | ); 253 | }, 254 | create: (status?: ChannelStatus): Promise => { 255 | return this.postJson('channels', { 256 | title: slug, 257 | status, 258 | }); 259 | }, 260 | update: (data: { 261 | title: string; 262 | status?: ChannelStatus; 263 | }): Promise => { 264 | return this.putJson(`channels/${slug}`, data); 265 | }, 266 | createBlock: (data: { 267 | source?: string; 268 | content?: string; 269 | description?: string; 270 | }): Promise => { 271 | return this.postJson(`channels/${slug}/blocks`, data); 272 | }, 273 | get: (options?: PaginationAttributes): Promise => 274 | this.getJsonWithPaginationQuery(`channels/${slug}`, options), 275 | delete: (): Promise => { 276 | return this.del(`channels/${slug}`); 277 | }, 278 | thumb: (): Promise => 279 | this.getJson(`channels/${slug}/thumb`), 280 | }; 281 | } 282 | 283 | block(id: number): ArenaBlockApi { 284 | return { 285 | channels: ( 286 | options?: PaginationAttributes, 287 | ): Promise => 288 | this.getJsonWithPaginationQuery(`blocks/${id}/channels`, options), 289 | get: (): Promise => { 290 | return this.getJson(`blocks/${id}`); 291 | }, 292 | update: (data: { 293 | title?: string; 294 | description?: string; 295 | content?: string; 296 | }): Promise => { 297 | return this.putJson(`blocks/${id}`, data); 298 | }, 299 | comments: ( 300 | options?: PaginationAttributes, 301 | ): Promise => { 302 | return this.getJsonWithPaginationQuery( 303 | `blocks/${id}/comments`, 304 | options, 305 | ); 306 | }, 307 | addComment: (body: string): Promise => { 308 | return this.postJson(`blocks/${id}/comments`, { body }); 309 | }, 310 | deleteComment: (commentId: number): Promise => { 311 | return this.del(`blocks/${id}/comments/${commentId}`); 312 | }, 313 | updateComment: (commentId: number, body: string): Promise => { 314 | return this.putJson(`blocks/${id}/comments/${commentId}`, { body }); 315 | }, 316 | }; 317 | } 318 | 319 | get search(): ArenaSearchApi { 320 | return { 321 | everything: ( 322 | query: string, 323 | options?: PaginationAttributes, 324 | ): Promise => 325 | this.getJsonWithSearchAndPaginationQuery(`/search`, { 326 | q: query, 327 | ...options, 328 | }), 329 | blocks: (query: string, options?: PaginationAttributes): Promise => 330 | this.getJsonWithSearchAndPaginationQuery(`/search/blocks`, { 331 | q: query, 332 | ...options, 333 | }), 334 | channels: (query: string, options?: PaginationAttributes): Promise => 335 | this.getJsonWithSearchAndPaginationQuery(`/search/channels`, { 336 | q: query, 337 | ...options, 338 | }), 339 | users: (query: string, options?: PaginationAttributes): Promise => 340 | this.getJsonWithSearchAndPaginationQuery(`/search/users`, { 341 | q: query, 342 | ...options, 343 | }), 344 | }; 345 | } 346 | 347 | private createConnection( 348 | channelSlug: string, 349 | id: number, 350 | type: 'Block' | 'Channel', 351 | ) { 352 | return this.postJson(`channels/${channelSlug}/connections`, { 353 | connectable_type: type, 354 | connectable_id: id, 355 | }); 356 | } 357 | 358 | private sortConnection( 359 | channelSlug: string, 360 | id: number, 361 | position: number, 362 | type: 'Block' | 'Channel', 363 | ) { 364 | return this.putJson(`channels/${channelSlug}/sort`, { 365 | connectable_type: type, 366 | connectable_id: id, 367 | new_position: position, 368 | }); 369 | } 370 | 371 | private getJsonWithSearchAndPaginationQuery( 372 | url: string, 373 | options?: PaginationAttributes & { q?: string }, 374 | ) { 375 | const qs = this.paginationQueryString(options); 376 | const searchQuery = 377 | options && options.q ? `q=${options.q}${qs ? '&' : ''}` : ''; 378 | return this.getJson(`${url}?${searchQuery}${qs}`); 379 | } 380 | 381 | private getJsonWithPaginationQuery( 382 | url: string, 383 | options?: PaginationAttributes, 384 | ) { 385 | const qs = this.paginationQueryString(options); 386 | return this.getJson(`${url}?${qs}`); 387 | } 388 | 389 | private async getJson(endpoint: string) { 390 | return this.fetch(`${this.domain}${endpoint}`, { 391 | method: 'GET', 392 | headers: this.headers, 393 | }).then((res: Response) => { 394 | if (res.ok) { 395 | return res.json(); 396 | } 397 | throw new HttpError(res.statusText, res.status); 398 | }); 399 | } 400 | 401 | private async putJson(endpoint: string, data?: unknown) { 402 | return this.fetch(`${this.domain}${endpoint}`, { 403 | method: 'PUT', 404 | headers: this.headers, 405 | body: data ? JSON.stringify(data) : undefined, 406 | }).then((res) => { 407 | if (res.ok) { 408 | return undefined; 409 | } 410 | throw new HttpError(res.statusText, res.status); 411 | }); 412 | } 413 | 414 | private async postJson(endpoint: string, data?: unknown) { 415 | return this.fetch(`${this.domain}${endpoint}`, { 416 | method: 'POST', 417 | headers: this.headers, 418 | body: data ? JSON.stringify(data) : undefined, 419 | }).then((res) => { 420 | if (res.ok) { 421 | return res.json(); 422 | } 423 | throw new HttpError(res.statusText, res.status); 424 | }); 425 | } 426 | 427 | private async del(endpoint: string) { 428 | return this.fetch(`${this.domain}${endpoint}`, { 429 | method: 'DELETE', 430 | headers: this.headers, 431 | }).then((res) => { 432 | if (res.ok) { 433 | return undefined; 434 | } 435 | throw new HttpError(res.statusText, res.status); 436 | }); 437 | } 438 | 439 | private paginationQueryString(options?: PaginationAttributes) { 440 | const { page, per, sort, direction, forceRefresh } = { 441 | ...ArenaClient.defaultPaginationOptions, 442 | ...options, 443 | }; 444 | const attrs = []; 445 | if (page) attrs.push(`page=${page}`); 446 | if (per) attrs.push(`per=${per}`); 447 | if (sort) attrs.push(`sort=${sort}`); 448 | if (direction) attrs.push(`direction=${direction}`); 449 | if (forceRefresh) attrs.push(`date=${this.date.now()}`); 450 | return attrs.join('&'); 451 | } 452 | } 453 | -------------------------------------------------------------------------------- /src/arena_api_types.ts: -------------------------------------------------------------------------------- 1 | export type ArenaUser = { 2 | /** The internal ID of the user */ 3 | id: number; 4 | /** The slug of the user. This is used for the user's default profile channel */ 5 | slug: string; 6 | /** Currently this will be equivalent to "full_name" */ 7 | username: string; 8 | /** The first name of the user */ 9 | first_name: string; 10 | /** The last name of the user */ 11 | last_name: string; 12 | /** The gravatar URL to the user's avatar */ 13 | avatar: string; 14 | avatar_image: ArenaImage[] | null; 15 | /** The number of channels the user owns or is a collaborator on */ 16 | channel_count: number; 17 | /** The number of channels and users a user is following */ 18 | following_count: number; 19 | /** The internal ID of the user's profile channel */ 20 | profile_id: number; 21 | /** The number of users following the user */ 22 | follower_count: number; 23 | /** Will always be "User" */ 24 | class: 'User'; 25 | /** The initials of a user. Derived from the user's first and last name */ 26 | initials: string; 27 | }; 28 | 29 | export type ArenaUserWithDetails = ArenaUser & { 30 | avatar_image: { 31 | display: string; 32 | thumb: string; 33 | }; 34 | can_index: boolean; 35 | badge: null | string; 36 | created_at: string; 37 | is_confirmed: boolean; 38 | is_exceeding_private_connections_limit?: boolean; 39 | is_lifetime_premium: boolean; 40 | is_pending_confirmation: boolean; 41 | is_pending_reconfirmation: boolean; 42 | is_premium: boolean; 43 | is_supporter: boolean; 44 | metadata: { description: null | string }; 45 | }; 46 | 47 | export type MeApiResponse = ArenaUserWithDetails & { 48 | channels: GetChannelsApiResponse[]; 49 | }; 50 | 51 | export type GetUserApiResponse = ArenaUserWithDetails; 52 | export type GetUserFollowersApiResponse = { 53 | length: number; 54 | total_pages: number; 55 | current_page: number; 56 | per: number; 57 | base_class: 'User'; 58 | class: 'User'; 59 | users: ArenaUserWithDetails[]; 60 | }; 61 | export type GetUserFollowingApiResponse = GetUserFollowersApiResponse; 62 | 63 | export type GetUserChannelsApiResponse = { 64 | length: number; 65 | total_pages: number; 66 | current_page: number; 67 | per: number; 68 | base_class: 'User'; 69 | class: 'User'; 70 | channels: ArenaChannelWithDetails[]; 71 | }; 72 | 73 | export type ArenaAttachment = { 74 | content_type: string; 75 | extension: string; 76 | file_name: string; 77 | file_size: number; 78 | file_size_display: string; 79 | url: string; 80 | }; 81 | 82 | export type ArenaEmbed = { 83 | author_name: string | null; 84 | author_url: string | null; // https://soundcloud.com/dkmntl" 85 | height: number; 86 | html: string | null; 87 | source_url: string | null; 88 | thumbnail_url: string | null; 89 | title: string | null; 90 | type: 'rich' | null; 91 | url: string | null; 92 | width: number; 93 | }; 94 | 95 | export type ArenaImage = { 96 | /** (String) Name of the file as it appears on the Arena filesystem */ 97 | filename: string; 98 | /** (String) MIME type of the image (e.g. 'image/png') */ 99 | content_type: string; 100 | /**(Timestamp) Timestamp of the last time the file was updated */ 101 | updated_at: string; 102 | /** (Hash) Only contains url which is a URL of the thumbnail sized image (200x200) */ 103 | thumb: { url: string }; 104 | /**(Hash) Only contains url which is a URL of the display sized image (same aspect ratio as original image but with a maximim width of 600px or a maximum height of 600px, whichever comes first) */ 105 | display: { url: string }; 106 | large: { url: string }; 107 | square: { url: string }; 108 | /** (Hash) Contains url which is a URL of the original image as well file_size (an integer representation in bytes) and file_size_display (a nicer string representation of the file_size) */ 109 | original: { 110 | file_size: number; 111 | file_size_display: string; 112 | url: string; 113 | }; 114 | }; 115 | 116 | // https://dev.are.na/documentation/blocks 117 | export type ArenaBaseBlock = { 118 | /** (Integer) The internal ID of the block */ 119 | id: number; 120 | /** (String, can be null) The title of the block */ 121 | title: string | null; 122 | /** (Timestamp) Timestamp when the block was last updated */ 123 | updated_at: string; 124 | /** (Timestamp) Timestamp when the block was created */ 125 | created_at: string; 126 | /** (String) Represents the state of the blocks processing lifecycle (this will most often "Available" but can also be "Failure", "Processed", "Processing") */ 127 | state: 128 | | 'available' 129 | | 'failure' 130 | | 'processed' 131 | | 'processing' 132 | | 'remote_processing'; 133 | visibility?: 'private' | 'public'; // from create Block 134 | /** (Integer) The number of comments on a block */ 135 | comment_count: number; 136 | /** (String) If the title is present on the block, this will be identical to the title. Otherwise it will be a truncated string of the *description* or *content*. If neither of those are present, it will be "Untitled" */ 137 | generated_title: string; 138 | /** (String) The type of block. Can be "Image", "Text", "Link", "Media", or "Attachment" */ 139 | class: 'Image' | 'Text' | 'Link' | 'Media' | 'Attachment'; 140 | /** (String) This will always be "Block" */ 141 | base_class: 'Block'; 142 | /**(String, can be null) If the block is of class "Text", this will be the text content as markdown */ 143 | content: string | null; 144 | /** (String, can be null) If the block is of class "Text", this will be the text content as HTML */ 145 | content_html: string | null; 146 | /** (String, can be null) This is used for captioning any type of block. Returns markdown. */ 147 | description: string | null; 148 | /** (String, can be null) This is used for captioning any type of block. Returns HTML */ 149 | description_html: string | null; 150 | source: { 151 | title?: string; 152 | /** (String) The url of the source */ 153 | url: string; 154 | /** (Hash) A hash of more info about the provider name: (String) The name of the source provider url: (String) The hostname of the source provider */ 155 | provider: { 156 | url: string; 157 | name: string; 158 | } | null; 159 | } | null; 160 | image: ArenaImage | null; 161 | /** (Hash) Representation of the author of the block */ 162 | user: ArenaUserWithDetails; // can be user when returning 163 | group?: ArenaGroup | null; 164 | attachment?: ArenaAttachment | null; 165 | embed?: ArenaEmbed | null; 166 | connections?: ArenaChannel[]; // connection type 167 | }; 168 | 169 | export type ArenaImageBlock = ArenaBaseBlock & { 170 | /** (String) The type of block. Will always be "Image" */ 171 | class: 'Image'; 172 | image: ArenaImage; 173 | source: { 174 | title?: string; 175 | /** (String) The url of the source */ 176 | url: string; 177 | /** (Hash) A hash of more info about the provider name: (String) The name of the source provider url: (String) The hostname of the source provider */ 178 | provider: { 179 | url: string; 180 | name: string; 181 | } | null; 182 | }; 183 | }; 184 | 185 | export type ArenaTextBlock = ArenaBaseBlock & { 186 | /** (String) The type of block. Will always be "Text" */ 187 | class: 'Text'; 188 | /** (String) The text content of the block as markdown */ 189 | content: string; 190 | /** (String) The text content of the block as HTML */ 191 | content_html: string; 192 | }; 193 | 194 | export type ArenaLinkBlock = ArenaBaseBlock & { 195 | /** (String) The type of block. Will always be "Link" */ 196 | class: 'Link'; 197 | image: ArenaImage; 198 | source: { 199 | title?: string; 200 | /** (String) The url of the source */ 201 | url: string; 202 | /** (Hash) A hash of more info about the provider name: (String) The name of the source provider url: (String) The hostname of the source provider */ 203 | provider: { 204 | url: string; 205 | name: string; 206 | } | null; 207 | }; 208 | }; 209 | 210 | export type ArenaMediaBlock = ArenaBaseBlock & { 211 | /** (String) The type of block. Will always be "Media" */ 212 | class: 'Media'; 213 | }; 214 | 215 | export type ArenaAttachmentBlock = ArenaBaseBlock & { 216 | /** (String) The type of block. Will always be "Attachment" */ 217 | class: 'Attachment'; 218 | }; 219 | 220 | export type ArenaBlock = 221 | | ArenaImageBlock 222 | | ArenaTextBlock 223 | | ArenaLinkBlock 224 | | ArenaMediaBlock 225 | | ArenaAttachmentBlock; 226 | 227 | export type ArenaCommentEntity = { 228 | type: 'user'; 229 | user_id: number; 230 | user_slug: string; 231 | user_name: string; 232 | start: number; 233 | end: number; 234 | }; 235 | 236 | export type ArenaBlockComment = { 237 | id: number; 238 | created_at: string; 239 | updated_at: string; 240 | commentable_id: number; 241 | commentable_type: 'Block'; 242 | body: string; 243 | user_id: string; 244 | deleted: boolean | null; 245 | entities: ArenaCommentEntity[]; 246 | base_class: 'Comment'; 247 | user: ArenaUserWithDetails; 248 | }; 249 | 250 | export type GetBlockCommentApiResponse = { 251 | length: number; 252 | total_pages: null | number; 253 | current_page: number; 254 | per: number; 255 | channel_title: null | string; 256 | comments: ArenaBlockComment[]; 257 | }; 258 | 259 | export type CreateBlockCommentApiResponse = ArenaBlockComment; 260 | 261 | export type GetBlockApiResponse = ArenaBlock & { 262 | /** (Array) An array of hash representations of each of the channels the block appears in */ 263 | connections: ArenaChannel[]; 264 | }; 265 | 266 | export type CreateBlockApiResponse = ArenaBlock & ConnectionData; 267 | 268 | export type GetBlockChannelsApiResponse = { 269 | length: number; 270 | total_pages: number; 271 | current_page: number; 272 | per: number; 273 | channels: ArenaChannelWithDetails[]; 274 | }; 275 | 276 | export type ConnectionData = { 277 | /** (Integer) The position of the block inside the channel (as determined by the channel's author and collaborators) */ 278 | position: number; 279 | /** (Boolean) Block is marked as selected inside the channel (this is an initial attempt to allow users to "feature" some content over others, can be used for moderation, introduction text, etc) */ 280 | selected: boolean; 281 | /** (Timestamp) Time when block was connected to the channel (if the block was created at the same time as the channel this will be identical to created_at) */ 282 | connected_at: string; 283 | /** (Integer) ID of the user who connected the block to the channel (if the block was not reused by another user, this will be identical to user_id) */ 284 | connected_by_user_id: number; 285 | connection_id?: number; 286 | connected_by_username?: string; 287 | connected_by_user_slug?: string; 288 | }; 289 | 290 | export type ArenaGroup = { 291 | id: number; 292 | class: 'Group'; 293 | base_class: 'Group'; 294 | created_at: string; 295 | updated_at: string; 296 | name: string; 297 | description?: string | null; 298 | visibility?: number; 299 | slug: string; 300 | }; 301 | 302 | export type GetGroupApiResponse = ArenaGroup & { 303 | title: string; 304 | user: ArenaUserWithDetails; 305 | users: ArenaUserWithDetails[]; 306 | member_ids: number[]; 307 | accessible_by_ids: number[]; 308 | published: boolean; 309 | }; 310 | 311 | export type ArenaChannel = { 312 | /** (Integer) The internal ID of the channel */ 313 | id: number; 314 | /** (String) The title of the channel */ 315 | title: string; 316 | /** (Timestamp) Timestamp when the channel was created */ 317 | created_at: string; 318 | /** (Timestamp) Timestamp when the channel was last updated */ 319 | updated_at: string; 320 | /** (Timestamp) Timestamp when the channel was last added to */ 321 | added_to_at?: string; 322 | /** (Boolean) If channel is visible to all members of arena or not */ 323 | published: boolean; 324 | /** (Boolean) If channel is open to other members of arena for adding blocks */ 325 | open: boolean; 326 | /** (Boolean) If the channel has collaborators or not */ 327 | collaboration: boolean; 328 | /** (String) The slug of the channel used in the url (e.g. http://are.na/arena-influences) */ 329 | slug: string; 330 | /** (Integer) The number of items in a channel (blocks and other channels) */ 331 | length: number; 332 | /** (String) Can be either "default" (a standard channel) or "profile" the default channel of a user */ 333 | kind: 'default' | 'profile'; 334 | /** (String) Can be "private" (only open for reading and adding to the channel by channel author and collaborators), "closed" (open for reading by everyone, only channel author and collaborators can add) or "public" (everyone can read and add to the channel) */ 335 | status: 'private' | 'public' | 'closed'; 336 | state: 'available'; 337 | 'nsfw?': boolean; 338 | metadata: { description: null | string } | null; 339 | /** (Integer) Internal ID of the channel author */ 340 | user_id: number; 341 | /** (String) Will always be "Channel" */ 342 | class: 'Channel'; 343 | /** (String) Will always be "Channel" */ 344 | base_class: 'Channel'; 345 | }; 346 | 347 | export type ArenaOwnerInfo = { 348 | owner_type: 'Group' | 'User'; 349 | owner_id: string; 350 | owner_slug?: string; 351 | }; 352 | 353 | export type CreateChannelApiResponse = ArenaChannel & ArenaOwnerInfo; 354 | 355 | export type GetChannelThumbApiResponse = ArenaChannel & 356 | ArenaOwnerInfo & { 357 | contents: ReadonlyArray< 358 | (ArenaBlock | Omit) & ConnectionData 359 | > | null; 360 | }; 361 | 362 | export type ArenaChannelContents = (ArenaBlock | ArenaChannelWithDetails) & 363 | ConnectionData; 364 | 365 | export type GetChannelContentsApiResponse = { 366 | contents: ReadonlyArray; 367 | }; 368 | 369 | export type ChannelConnectBlockApiResponse = ArenaBlock & ConnectionData; 370 | export type ChannelConnectChannelApiResponse = ArenaChannelWithDetails & 371 | ConnectionData; 372 | 373 | export type ArenaChannelWithDetails = ArenaOwnerInfo & 374 | ArenaChannel & { 375 | id: number; 376 | /** (Object) More information on the channel author. Contains id, slug, first_name, last_name, full_name, avatar, email, channel_count, following_count, follower_count, and profile_id */ 377 | user?: ArenaUserWithDetails; 378 | group?: ArenaGroup; 379 | // /** (Integer) If pagination is used, how many total pages there are in your request */ 380 | // total_pages: number; 381 | // /** (Integer) If pagination is used, page requested */ 382 | // current_page: number; 383 | // /** (Integer) If pagination is used, items per page requested */ 384 | // per: number; 385 | // /** (Integer) Number of followers the channel has */ 386 | follower_count: number; 387 | can_index: boolean; 388 | /** (Array, can be null) Array of blocks and other channels in the channel. Note: If the request is authenticated, this will include any private channels included in the requested channel that you have access to. If not, only public channels included in the requested channel will be shown. */ 389 | contents: ReadonlyArray | null; 390 | }; 391 | 392 | export type GetGroupChannelsApiResponse = { 393 | length: number; 394 | total_pages: null | number; 395 | current_page: number; 396 | per: number; 397 | channel_title: null | string; 398 | channels: ArenaChannelWithDetails[]; 399 | }; 400 | 401 | export type GetChannelsApiResponse = ArenaChannelWithDetails & { 402 | per: number; 403 | page: number; 404 | owner: ArenaUserWithDetails | null; 405 | collaborators: ReadonlyArray[] | null; 406 | }; 407 | 408 | export type GetConnectionsApiResponse = (ArenaBlock | GetChannelsApiResponse) & 409 | ConnectionData; 410 | 411 | /** 412 | * Options available for paginating requests to channel endpoints. 413 | */ 414 | export type PaginationAttributes = { 415 | /** Number of items returned per page. */ 416 | per?: number; 417 | /** The page to fetch. */ 418 | page?: number; 419 | /** The field to sort results by. */ 420 | sort?: string; 421 | /** The direction of returned results. */ 422 | direction?: 'asc' | 'desc'; 423 | /** Force refresh of the server cache. */ 424 | forceRefresh?: boolean; 425 | }; 426 | 427 | export type SearchApiResponse = { 428 | term: string; 429 | per: number; 430 | current_page: number; 431 | total_pages: number; 432 | length: number; 433 | authenticated: boolean; 434 | channels: ArenaChannel[]; 435 | blocks: ArenaBlock[]; 436 | users: ArenaUserWithDetails[]; 437 | }; 438 | -------------------------------------------------------------------------------- /docs/types/GetUserApiResponse.html: -------------------------------------------------------------------------------- 1 | GetUserApiResponse | arena-ts

Generated using TypeDoc

-------------------------------------------------------------------------------- /docs/types/CreateBlockCommentApiResponse.html: -------------------------------------------------------------------------------- 1 | CreateBlockCommentApiResponse | arena-ts

Generated using TypeDoc

-------------------------------------------------------------------------------- /docs/types/GetUserFollowingApiResponse.html: -------------------------------------------------------------------------------- 1 | GetUserFollowingApiResponse | arena-ts

Generated using TypeDoc

-------------------------------------------------------------------------------- /docs/types/ChannelStatus.html: -------------------------------------------------------------------------------- 1 | ChannelStatus | arena-ts

Generated using TypeDoc

-------------------------------------------------------------------------------- /docs/types/CreateBlockApiResponse.html: -------------------------------------------------------------------------------- 1 | CreateBlockApiResponse | arena-ts

Generated using TypeDoc

-------------------------------------------------------------------------------- /docs/types/CreateChannelApiResponse.html: -------------------------------------------------------------------------------- 1 | CreateChannelApiResponse | arena-ts

Generated using TypeDoc

-------------------------------------------------------------------------------- /docs/types/ChannelConnectBlockApiResponse.html: -------------------------------------------------------------------------------- 1 | ChannelConnectBlockApiResponse | arena-ts

Generated using TypeDoc

-------------------------------------------------------------------------------- /docs/types/ChannelConnectChannelApiResponse.html: -------------------------------------------------------------------------------- 1 | ChannelConnectChannelApiResponse | arena-ts

Generated using TypeDoc

--------------------------------------------------------------------------------