├── .editorconfig ├── .env-example ├── .eslintignore ├── .eslintrc.js ├── .github └── workflows │ ├── cleanup.yml │ ├── pr-title-check.yml │ ├── release-pr.yml │ ├── release.yml │ └── test.yml ├── .gitignore ├── .husky └── pre-commit ├── .prettierignore ├── .readme-assets └── Github-Graphic-JS.jpg ├── .yarn └── releases │ └── yarn-1.22.22.cjs ├── .yarnrc ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── __tests__ ├── agent.test.ts ├── assets │ ├── test-file.pdf │ └── test-image.jpg ├── block-lists.test.ts ├── call-members.test.ts ├── call-types.test.ts ├── call.test.ts ├── channel-types.test.ts ├── channel.test.ts ├── command.test.ts ├── create-test-client.ts ├── create-token.test.ts ├── date-transform.test.ts ├── devices-push.test.ts ├── external-storage.test.ts ├── messages.test.ts ├── permissions-app-settings.test.ts ├── rate-limit.test.ts ├── teams.test.ts ├── user-compat.test.ts ├── users.test.ts └── webhook.test.ts ├── generate-openapi.sh ├── index.ts ├── openapitools.json ├── package.json ├── rollup.config.mjs ├── src ├── BaseApi.ts ├── StreamCall.ts ├── StreamChannel.ts ├── StreamChatClient.ts ├── StreamClient.ts ├── StreamModerationClient.ts ├── StreamVideoClient.ts ├── gen │ ├── chat │ │ ├── ChannelApi.ts │ │ └── ChatApi.ts │ ├── common │ │ └── CommonApi.ts │ ├── model-decoders │ │ └── index.ts │ ├── models │ │ └── index.ts │ ├── moderation │ │ └── ModerationApi.ts │ └── video │ │ ├── CallApi.ts │ │ └── VideoApi.ts ├── types.ts └── utils │ ├── create-token.ts │ └── rate-limit.ts ├── test-cleanup.js ├── tsconfig.eslint.json ├── tsconfig.json ├── vite.config.ts └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.ts] 12 | quote_type = single -------------------------------------------------------------------------------- /.env-example: -------------------------------------------------------------------------------- 1 | STREAM_API_KEY= 2 | STREAM_SECRET= 3 | OPENAI_API_KEY= 4 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | /dist 2 | /node_modules 3 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es2021: true, 5 | }, 6 | extends: ["standard-with-typescript", "prettier"], 7 | plugins: ["unused-imports"], 8 | overrides: [ 9 | { 10 | env: { 11 | node: true, 12 | }, 13 | files: [".eslintrc.{js,cjs}"], 14 | parserOptions: { 15 | sourceType: "script", 16 | sourceType: "module", 17 | }, 18 | }, 19 | ], 20 | parserOptions: { 21 | ecmaVersion: "latest", 22 | sourceType: "module", 23 | project: ["tsconfig.eslint.json"], 24 | }, 25 | rules: { 26 | semi: "off", 27 | "@typescript-eslint/ban-ts-comment": "off", 28 | "@typescript-eslint/semi": "off", 29 | "@typescript-eslint/promise-function-async": "off", 30 | "@typescript-eslint/explicit-function-return-type": "off", 31 | "@typescript-eslint/comma-dangle": "off", 32 | "@typescript-eslint/consistent-type-imports": "off", 33 | "@typescript-eslint/naming-convention": "off", 34 | "@typescript-eslint/strict-boolean-expressions": "off", 35 | "@typescript-eslint/no-non-null-assertion": "off", 36 | "@typescript-eslint/no-confusing-void-expression": "off", 37 | "no-unused-vars": "off", 38 | "@typescript-eslint/no-unused-vars": "off", 39 | "unused-imports/no-unused-imports": "error", 40 | "unused-imports/no-unused-vars": [ 41 | "error", 42 | { 43 | vars: "all", 44 | varsIgnorePattern: "^_", 45 | args: "after-used", 46 | argsIgnorePattern: "^_", 47 | }, 48 | ], 49 | "@typescript-eslint/no-empty-interface": "off", 50 | eqeqeq: ["error", "smart"], 51 | }, 52 | }; 53 | -------------------------------------------------------------------------------- /.github/workflows/cleanup.yml: -------------------------------------------------------------------------------- 1 | name: Cleanup after tests 2 | env: 3 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 4 | STREAM_API_KEY: ${{ vars.TEST_API_KEY }} 5 | STREAM_SECRET: ${{ secrets.TEST_SECRET }} 6 | 7 | # Runs midnight at every Sunday https://crontab.guru/weekly 8 | on: 9 | schedule: 10 | - cron: "0 0 * * 0" 11 | workflow_dispatch: 12 | 13 | jobs: 14 | cleanup: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v3 18 | with: 19 | fetch-depth: 0 20 | - name: Setup Node 21 | uses: actions/setup-node@v3 22 | with: 23 | node-version: 22 24 | cache: "yarn" 25 | 26 | - name: Install Dependencies 27 | run: yarn install --immutable 28 | - name: Build 29 | run: yarn build 30 | - name: Cleanup 31 | run: node test-cleanup.js 32 | -------------------------------------------------------------------------------- /.github/workflows/pr-title-check.yml: -------------------------------------------------------------------------------- 1 | name: Check PR title 2 | 3 | on: 4 | pull_request: 5 | types: [opened, edited, synchronize, reopened] 6 | 7 | jobs: 8 | lint: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: aslafy-z/conventional-pr-title-action@v3 12 | env: 13 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 14 | -------------------------------------------------------------------------------- /.github/workflows/release-pr.yml: -------------------------------------------------------------------------------- 1 | name: Release PR 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | permissions: 9 | contents: write 10 | pull-requests: write 11 | 12 | jobs: 13 | release-please: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: google-github-actions/release-please-action@v3 17 | with: 18 | release-type: node 19 | package-name: "@stream-io/node-sdk" 20 | bump-minor-pre-major: true 21 | bump-patch-for-minor-pre-major: true 22 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | pull_request: 5 | types: [closed] 6 | branches: 7 | - main 8 | workflow_dispatch: 9 | 10 | jobs: 11 | Release: 12 | name: 🚀 Release 13 | if: github.event_name == 'workflow_dispatch' || github.event.pull_request.merged && startsWith(github.head_ref, 'release-please--branches--main') 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v3 17 | with: 18 | fetch-depth: 0 19 | - name: Setup Node 20 | uses: actions/setup-node@v3 21 | with: 22 | node-version: 18 23 | cache: "yarn" 24 | 25 | - name: Install Dependencies 26 | run: yarn install --immutable 27 | 28 | - name: Publish package 29 | run: | 30 | npm config set //registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }} 31 | npm publish 32 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | env: 3 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 4 | STREAM_API_KEY: ${{ vars.TEST_API_KEY }} 5 | STREAM_SECRET: ${{ secrets.TEST_SECRET }} 6 | OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} 7 | 8 | on: 9 | push: 10 | branches: 11 | - main 12 | pull_request: 13 | types: [opened, synchronize, reopened] 14 | 15 | jobs: 16 | lint: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: actions/checkout@v3 20 | with: 21 | fetch-depth: 0 22 | - name: Setup Node 23 | uses: actions/setup-node@v3 24 | with: 25 | node-version: ${{ matrix.version }} 26 | cache: "yarn" 27 | 28 | - name: Install Dependencies 29 | run: yarn install --immutable 30 | - name: Lint 31 | run: yarn lint 32 | test-node: 33 | strategy: 34 | matrix: 35 | version: [18, 20, 22] 36 | runs-on: ubuntu-latest 37 | needs: [lint] 38 | steps: 39 | - uses: actions/checkout@v3 40 | with: 41 | fetch-depth: 0 42 | - name: Setup Node 43 | uses: actions/setup-node@v3 44 | with: 45 | node-version: ${{ matrix.version }} 46 | cache: "yarn" 47 | 48 | - name: Install Dependencies 49 | run: yarn install --immutable 50 | 51 | - name: Build 52 | run: yarn build 53 | 54 | - name: Test 55 | run: yarn test 56 | test-bun: 57 | runs-on: ubuntu-latest 58 | needs: [lint] 59 | steps: 60 | - uses: actions/checkout@v3 61 | with: 62 | fetch-depth: 0 63 | - name: Setup Bun@1 64 | uses: oven-sh/setup-bun@v1 65 | - name: Setup Node 66 | uses: actions/setup-node@v3 67 | with: 68 | node-version: 20 69 | cache: "yarn" 70 | 71 | - name: Install Dependencies 72 | run: yarn install --immutable 73 | 74 | - name: Build 75 | run: yarn build 76 | 77 | - name: Test 78 | run: yarn test 79 | 80 | - name: Test 81 | run: bun run vitest 82 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .yarn/* 2 | !.yarn/patches 3 | !.yarn/plugins 4 | !.yarn/releases 5 | !.yarn/sdks 6 | !.yarn/versions 7 | 8 | # Swap the comments on the following lines if you don't wish to use zero-installs 9 | # Documentation here: https://yarnpkg.com/features/zero-installs 10 | #!.yarn/cache 11 | #.pnp.* 12 | 13 | .DS_Store 14 | node_modules 15 | dist 16 | .vercel 17 | .vscode 18 | .Vscode 19 | .idea 20 | .angular 21 | .env 22 | 23 | .eslintcache 24 | coverage 25 | 26 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npx lint-staged 5 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | dist 2 | -------------------------------------------------------------------------------- /.readme-assets/Github-Graphic-JS.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GetStream/stream-node/3123e5ca556f44cef085a90d087f93f1ab855b13/.readme-assets/Github-Graphic-JS.jpg -------------------------------------------------------------------------------- /.yarnrc: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | yarn-path ".yarn/releases/yarn-1.22.22.cjs" 6 | 7 | ignore-engines: true 8 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [0.4.24](https://github.com/GetStream/stream-node/compare/v0.4.23...v0.4.24) (2025-05-06) 4 | 5 | 6 | ### Features 7 | 8 | * empty commit to trigger release ([c0890a8](https://github.com/GetStream/stream-node/commit/c0890a8d8b7adc96d4d81717220159321e924480)) 9 | 10 | ## [0.4.23](https://github.com/GetStream/stream-node/compare/v0.4.22...v0.4.23) (2025-04-30) 11 | 12 | 13 | ### Features 14 | 15 | * update to v171.1.7 ([#107](https://github.com/GetStream/stream-node/issues/107)) ([dd442f0](https://github.com/GetStream/stream-node/commit/dd442f0e6b9acc465781cb748e5c5a03dd51624c)) 16 | 17 | ## [0.4.22](https://github.com/GetStream/stream-node/compare/v0.4.21...v0.4.22) (2025-04-09) 18 | 19 | 20 | ### Bug Fixes 21 | 22 | * handle missing optional dependency in declarations ([#103](https://github.com/GetStream/stream-node/issues/103)) ([eb271c5](https://github.com/GetStream/stream-node/commit/eb271c5f3bfb6002d341323af25beabd15516ef2)) 23 | 24 | ## [0.4.21](https://github.com/GetStream/stream-node/compare/v0.4.20...v0.4.21) (2025-04-08) 25 | 26 | 27 | ### Bug Fixes 28 | 29 | * remove undici dependency ([#101](https://github.com/GetStream/stream-node/issues/101)) ([c943be1](https://github.com/GetStream/stream-node/commit/c943be1b2871b2b010dd4b29d690c46ecacda23b)) 30 | 31 | ## [0.4.20](https://github.com/GetStream/stream-node/compare/v0.4.19...v0.4.20) (2025-04-07) 32 | 33 | 34 | ### Features 35 | 36 | * set max connections to 100, allow integrators to configure Fetch API ([#98](https://github.com/GetStream/stream-node/issues/98)) ([b044b59](https://github.com/GetStream/stream-node/commit/b044b599867a69b33b3aa9d989d1c1e3277dc92f)) 37 | 38 | ## [0.4.19](https://github.com/GetStream/stream-node/compare/v0.4.18...v0.4.19) (2025-03-17) 39 | 40 | 41 | ### Features 42 | 43 | * update to API spec v163.0.0 ([#96](https://github.com/GetStream/stream-node/issues/96)) ([dcdfea8](https://github.com/GetStream/stream-node/commit/dcdfea8cfbf83b0ef3426426909c844424f682ab)) 44 | 45 | ## [0.4.18](https://github.com/GetStream/stream-node/compare/v0.4.17...v0.4.18) (2025-03-10) 46 | 47 | 48 | ### Bug Fixes 49 | 50 | * support realtime AI model overrides ([#94](https://github.com/GetStream/stream-node/issues/94)) ([1071f75](https://github.com/GetStream/stream-node/commit/1071f75aaf7ffb029f8c3c4c06465e81abf341d6)) 51 | 52 | ## [0.4.17](https://github.com/GetStream/stream-node/compare/v0.4.16...v0.4.17) (2025-03-06) 53 | 54 | 55 | ### Bug Fixes 56 | 57 | * use an exact version of @stream-io/openai-realtime-api ([#92](https://github.com/GetStream/stream-node/issues/92)) ([d6f0419](https://github.com/GetStream/stream-node/commit/d6f0419f924bfdadbb44d2b12b44a664bb4b39a6)) 58 | 59 | ## [0.4.16](https://github.com/GetStream/stream-node/compare/v0.4.15...v0.4.16) (2025-02-25) 60 | 61 | 62 | ### Bug Fixes 63 | 64 | * update changelog ([#89](https://github.com/GetStream/stream-node/issues/89)) ([1b46c91](https://github.com/GetStream/stream-node/commit/1b46c919ccc5a98414ca441b833020743217e95d)) 65 | 66 | ## [0.4.15](https://github.com/GetStream/stream-node/compare/v0.4.14...v0.4.15) (2025-02-25) 67 | 68 | ### Features 69 | 70 | - update to API spec 157.6.2 ([#87](https://github.com/GetStream/stream-node/issues/87)) ([92cdde4](https://github.com/GetStream/stream-node/commit/92cdde47ca016f70816eb363d8d6123f254b5411)) 71 | 72 | `start_rtmp_broadcasts` option is removed from `goLive` call (it didn't do anything, so it's safe to remove) 73 | 74 | `client.chat.getExportChannelsStatus` is removed, you can use the `client.getTask` instead, or the new `export.channels.success` and `export.channels.error` Webhook events 75 | 76 | ## [0.4.14](https://github.com/GetStream/stream-node/compare/v0.4.13...v0.4.14) (2025-02-18) 77 | 78 | ### Features 79 | 80 | - add a method to connect OpenAI agent to call ([#82](https://github.com/GetStream/stream-node/issues/82)) ([b9fc11f](https://github.com/GetStream/stream-node/commit/b9fc11f7a485d41917314dd4dd9a309475fb4b58)) 81 | 82 | ## [0.4.13](https://github.com/GetStream/stream-node/compare/v0.4.12...v0.4.13) (2025-02-10) 83 | 84 | ### Features 85 | 86 | - update to API version 155.0.0 ([#83](https://github.com/GetStream/stream-node/issues/83)) ([04a176a](https://github.com/GetStream/stream-node/commit/04a176a68e8239f89a03aff4cfe209af66902c7c)) 87 | 88 | ## [0.4.12](https://github.com/GetStream/stream-node/compare/v0.4.11...v0.4.12) (2025-02-03) 89 | 90 | ### Features 91 | 92 | - update to api spec v151.0.1 ([#80](https://github.com/GetStream/stream-node/issues/80)) ([73efff2](https://github.com/GetStream/stream-node/commit/73efff2debf78dc95dde4e91b8de3ed0b264dadd)) 93 | 94 | ## [0.4.11](https://github.com/GetStream/stream-node/compare/v0.4.10...v0.4.11) (2025-01-27) 95 | 96 | ### Features 97 | 98 | - update to api spec v147.3.0 ([#78](https://github.com/GetStream/stream-node/issues/78)) ([147bddb](https://github.com/GetStream/stream-node/commit/147bddb7aaee2981cf808c79b702c7682115eecb)) 99 | 100 | ## [0.4.10](https://github.com/GetStream/stream-node/compare/v0.4.9...v0.4.10) (2025-01-22) 101 | 102 | ### Features 103 | 104 | - [VID-300] Query user feedback API endpoint ([#75](https://github.com/GetStream/stream-node/issues/75)) ([71815d6](https://github.com/GetStream/stream-node/commit/71815d6acb7099d8bce91ece4db860003d20d836)) 105 | 106 | ## [0.4.9](https://github.com/GetStream/stream-node/compare/v0.4.8...v0.4.9) (2025-01-14) 107 | 108 | ### Features 109 | 110 | - update to API version 142.7.0 ([#73](https://github.com/GetStream/stream-node/issues/73)) ([d38b43d](https://github.com/GetStream/stream-node/commit/d38b43dac51fe61087d542a62c43fc6bd6864f92)) 111 | 112 | ## [0.4.8](https://github.com/GetStream/stream-node/compare/v0.4.7...v0.4.8) (2025-01-10) 113 | 114 | ### Features 115 | 116 | - Stable video-stats API: Get call report ([#71](https://github.com/GetStream/stream-node/issues/71)) ([16d5e4f](https://github.com/GetStream/stream-node/commit/16d5e4f62081312a8f60a374a52cfa25241ef4f6)) 117 | 118 | ## [0.4.7](https://github.com/GetStream/stream-node/compare/v0.4.6...v0.4.7) (2025-01-07) 119 | 120 | ### Features 121 | 122 | - update to API version 142.4.0 ([#69](https://github.com/GetStream/stream-node/issues/69)) ([c1a0d02](https://github.com/GetStream/stream-node/commit/c1a0d02b13581379875a7f8c5428ba9edad09d88)) 123 | 124 | ## [0.4.6](https://github.com/GetStream/stream-node/compare/v0.4.5...v0.4.6) (2024-12-30) 125 | 126 | ### Features 127 | 128 | - add closed caption endpoints ([#67](https://github.com/GetStream/stream-node/issues/67)) ([c1820f2](https://github.com/GetStream/stream-node/commit/c1820f20be4ab5969bb17a04fbf5ae417b40e846)) 129 | 130 | ## [0.4.5](https://github.com/GetStream/stream-node/compare/v0.4.4...v0.4.5) (2024-12-06) 131 | 132 | ### Features 133 | 134 | - update open api to latest version ([#65](https://github.com/GetStream/stream-node/issues/65)) ([8393378](https://github.com/GetStream/stream-node/commit/8393378dbd47985a88dcf1247a05d975696a39ed)) 135 | 136 | ## [0.4.4](https://github.com/GetStream/stream-node/compare/v0.4.3...v0.4.4) (2024-10-02) 137 | 138 | ### Bug Fixes 139 | 140 | - node js esm module import ([#61](https://github.com/GetStream/stream-node/issues/61)) ([3a0d9a6](https://github.com/GetStream/stream-node/commit/3a0d9a61679e249f765912a92613db65963d40da)) 141 | 142 | ## [0.4.3](https://github.com/GetStream/stream-node/compare/v0.4.2...v0.4.3) (2024-09-18) 143 | 144 | ### Features 145 | 146 | - update to api version 131.1.1 ([#58](https://github.com/GetStream/stream-node/issues/58)) ([6e84e26](https://github.com/GetStream/stream-node/commit/6e84e26ab9ac6c5d0134b80add189976a1914168)) 147 | 148 | ## [0.4.2](https://github.com/GetStream/stream-node/compare/v0.4.1...v0.4.2) (2024-09-17) 149 | 150 | ### Bug Fixes 151 | 152 | - parse non-JSON response objects correctly ([#56](https://github.com/GetStream/stream-node/issues/56)) ([232f240](https://github.com/GetStream/stream-node/commit/232f2403899cd95621a47ef021b198f560b5d036)) 153 | 154 | ## [0.4.1](https://github.com/GetStream/stream-node/compare/v0.4.0...v0.4.1) (2024-09-09) 155 | 156 | ### Features 157 | 158 | - RTMP broadcasts list endpoint ([#53](https://github.com/GetStream/stream-node/issues/53)) ([8851dab](https://github.com/GetStream/stream-node/commit/8851dab3f5afd6f6739518b4d38c1baeeb5c79a9)) 159 | 160 | ## [0.4.0](https://github.com/GetStream/stream-node/compare/v0.3.1...v0.4.0) (2024-08-30) 161 | 162 | ### ⚠ BREAKING CHANGES 163 | 164 | - use unified API spec, and go generator ([#39](https://github.com/GetStream/stream-node/issues/39)) 165 | 166 | ### Features 167 | 168 | - use unified API spec, and go generator ([#39](https://github.com/GetStream/stream-node/issues/39)) ([1bba83d](https://github.com/GetStream/stream-node/commit/1bba83dd3f5536c4989c4d566c1969bce3b29423)) 169 | 170 | ## [0.3.1](https://github.com/GetStream/stream-node/compare/v0.3.0...v0.3.1) (2024-08-28) 171 | 172 | ### Bug Fixes 173 | 174 | - support modern esm and types resolution ([#48](https://github.com/GetStream/stream-node/issues/48)) ([78e3c9f](https://github.com/GetStream/stream-node/commit/78e3c9f01e907d680e9db4dc802d8fbc0ac4bde6)) 175 | 176 | ## [0.3.0](https://github.com/GetStream/stream-node/compare/v0.2.6...v0.3.0) (2024-06-28) 177 | 178 | ### ⚠ BREAKING CHANGES 179 | 180 | - update to latest open api ([#42](https://github.com/GetStream/stream-node/issues/42)) 181 | 182 | ### Features 183 | 184 | - update to latest open api ([#42](https://github.com/GetStream/stream-node/issues/42)) ([73df541](https://github.com/GetStream/stream-node/commit/73df541301e383cf2fc38538f2d588a07537688a)) 185 | 186 | ## [0.2.6](https://github.com/GetStream/stream-node/compare/v0.2.5...v0.2.6) (2024-06-20) 187 | 188 | ### Features 189 | 190 | - implement delete call endpoint ([#45](https://github.com/GetStream/stream-node/issues/45)) ([96884a3](https://github.com/GetStream/stream-node/commit/96884a3ec4da1901dd9f135d6c7ab0111851d5d1)) 191 | 192 | ## [0.2.5](https://github.com/GetStream/stream-node/compare/v0.2.4...v0.2.5) (2024-06-04) 193 | 194 | ### Features 195 | 196 | - session timers ([#43](https://github.com/GetStream/stream-node/issues/43)) ([68d8c0d](https://github.com/GetStream/stream-node/commit/68d8c0d1348fc12ccca07446ac69606b01f859d9)) 197 | 198 | ## [0.2.4](https://github.com/GetStream/stream-node/compare/v0.2.3...v0.2.4) (2024-05-30) 199 | 200 | ### Bug Fixes 201 | 202 | - iat can be in the future ([#40](https://github.com/GetStream/stream-node/issues/40)) ([483d4c4](https://github.com/GetStream/stream-node/commit/483d4c4b01e313520a872fb91d906b2d8144b581)) 203 | 204 | ## [0.2.3](https://github.com/GetStream/stream-node/compare/v0.2.2...v0.2.3) (2024-05-23) 205 | 206 | ### Features 207 | 208 | - add call stats query endpoints ([#37](https://github.com/GetStream/stream-node/issues/37)) ([f2c3ec9](https://github.com/GetStream/stream-node/commit/f2c3ec981bc42c4741cf23cffe571eef68b345af)) 209 | 210 | ## [0.2.2](https://github.com/GetStream/stream-node/compare/v0.2.1...v0.2.2) (2024-05-21) 211 | 212 | ### Features 213 | 214 | - add option to provide role for call token ([#35](https://github.com/GetStream/stream-node/issues/35)) ([befe685](https://github.com/GetStream/stream-node/commit/befe68549f9e5fc4c43d63377098de57b39ca1c4)) 215 | 216 | ## [0.2.1](https://github.com/GetStream/stream-node/compare/v0.2.0...v0.2.1) (2024-05-15) 217 | 218 | ### Bug Fixes 219 | 220 | - custom event signature ([#32](https://github.com/GetStream/stream-node/issues/32)) ([5e02b9b](https://github.com/GetStream/stream-node/commit/5e02b9bb333c5f7fa2a171e60a02ce997091c2ad)) 221 | 222 | ## [0.2.0](https://github.com/GetStream/stream-node/compare/v0.1.13...v0.2.0) (2024-05-14) 223 | 224 | ### ⚠ BREAKING CHANGES 225 | 226 | - upgrade to API v113.0.0 ([#28](https://github.com/GetStream/stream-node/issues/28)) 227 | 228 | ### Features 229 | 230 | - delete recording and transcription methods ([#30](https://github.com/GetStream/stream-node/issues/30)) ([beb89ac](https://github.com/GetStream/stream-node/commit/beb89ac2f4470e0255b4a8c99cc5e1f5e9bb27d8)) 231 | - upgrade to API v113.0.0 ([#28](https://github.com/GetStream/stream-node/issues/28)) ([cc0d00d](https://github.com/GetStream/stream-node/commit/cc0d00ddfa18eaa01907310382cbce125655f586)) 232 | 233 | ## [0.1.13](https://github.com/GetStream/stream-node/compare/v0.1.12...v0.1.13) (2024-03-26) 234 | 235 | ### Features 236 | 237 | - listTranscriptions API ([#24](https://github.com/GetStream/stream-node/issues/24)) ([a170fd6](https://github.com/GetStream/stream-node/commit/a170fd6133b1fbeb01ba27987ffb8b50dd2c95fc)) 238 | 239 | ## [0.1.12](https://github.com/GetStream/stream-node/compare/v0.1.11...v0.1.12) (2024-03-08) 240 | 241 | ### Features 242 | 243 | - update video to api v100.4.3 ([#22](https://github.com/GetStream/stream-node/issues/22)) ([2424b2d](https://github.com/GetStream/stream-node/commit/2424b2db10353dc46cd9c78c543666732228d5c7)) 244 | 245 | ### Bug Fixes 246 | 247 | - basePath not stored properly ([#20](https://github.com/GetStream/stream-node/issues/20)) ([3221ba0](https://github.com/GetStream/stream-node/commit/3221ba0ec1fe0bf002c6cf21794f23f1d6a818fd)) 248 | 249 | ## [0.1.11](https://github.com/GetStream/stream-node/compare/v0.1.10...v0.1.11) (2024-02-29) 250 | 251 | ### Bug Fixes 252 | 253 | - add missing dependencies ([#18](https://github.com/GetStream/stream-node/issues/18)) ([5a1a75b](https://github.com/GetStream/stream-node/commit/5a1a75b8125be825788f3f520f29ac1d6024b2b7)) 254 | - persist [@ts-expect-error](https://github.com/ts-expect-error) comments in typing files ([#16](https://github.com/GetStream/stream-node/issues/16)) ([b1d3051](https://github.com/GetStream/stream-node/commit/b1d3051d8146031f7cb6b5f88bc07ee5a916d921)) 255 | 256 | ## [0.1.10](https://github.com/GetStream/stream-node/compare/v0.1.9...v0.1.10) (2024-02-12) 257 | 258 | ### Features 259 | 260 | - add verifyWebhook method ([#12](https://github.com/GetStream/stream-node/issues/12)) ([e79f53c](https://github.com/GetStream/stream-node/commit/e79f53cdbed202428a99b5a0cf7df6baf200333c)) 261 | - update open api and implement external storage endpoints ([#15](https://github.com/GetStream/stream-node/issues/15)) ([fcd75e0](https://github.com/GetStream/stream-node/commit/fcd75e06d7a1c55021dc2ee47e12415e5b18a1b5)) 262 | 263 | ## [0.1.9](https://github.com/GetStream/stream-node/compare/v0.1.8...v0.1.9) (2023-12-01) 264 | 265 | ### Features 266 | 267 | - update open api ([#10](https://github.com/GetStream/stream-node/issues/10)) ([05fbbf6](https://github.com/GetStream/stream-node/commit/05fbbf667039079b97f04ffef850bf624256fb09)) 268 | 269 | ## [0.1.8](https://github.com/GetStream/stream-node/compare/v0.1.7...v0.1.8) (2023-12-01) 270 | 271 | ### Features 272 | 273 | - Update open api ([4c461e7](https://github.com/GetStream/stream-node/commit/4c461e7e0d46a5c3c214d31934ac4ebc8b2a4ede)) 274 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # :recycle: Contributing 2 | 3 | We welcome code changes that improve this library or fix a problem, please make sure to follow all best practices and add tests if applicable before submitting a Pull Request on Github. We are very happy to merge your code in the official repository. Make sure to sign our [Contributor License Agreement (CLA)](https://docs.google.com/forms/d/e/1FAIpQLScFKsKkAJI7mhCr7K9rEIOpqIDThrWxuvxnwUq2XkHyG154vQ/viewform) first. See our license file for more details. 4 | 5 | ## Getting started 6 | 7 | ### Install dependencies 8 | 9 | ```shell 10 | $ yarn install 11 | ``` 12 | 13 | ### Run tests 14 | 15 | ```shell 16 | $ yarn test 17 | ``` 18 | 19 | ## Linting and code formatting 20 | 21 | We use [ESLint](https://eslint.org/) for linting and [Prettier](https://prettier.io/) for code formatting. We enforce formatting with pre-commit hook, linting with CI. If your IDE has integration with these tools, it's recommended to set them up. 22 | 23 | ## Commit message convention 24 | 25 | Since we're autogenerating our [CHANGELOG](./CHANGELOG.md) and version bump, we need to follow a specific commit message convention on the `main` branch. 26 | 27 | The recommended approach is to use "Squash and merge" and make sure the **PR title** follows the [conventional commits format](https://www.conventionalcommits.org/). Here's how a usual PR title looks like for a new feature: `feat: allow provided config object to extend other configs`. A bugfix: `fix: prevent racing of requests`. We have a CI action to make sure the PR title follows the necessary format. 28 | 29 | If you don't want to squash your commits, make sure that your commits follow the above format. 30 | 31 | ## Generating code from Open API 32 | 33 | ### Commands 34 | 35 | Checkout the [protocol](https://github.com/GetStream/protocol) or [chat](https://github.com/GetStream/chat) reporisitories and run one of the following commands: 36 | 37 | ```shell 38 | # if you have protocol repo: 39 | $ yarn generate:open-api 40 | # if you have chat repo 41 | $ yarn generate:open-api:dev 42 | ``` 43 | 44 | ## Release (for Stream developers) 45 | 46 | Releasing this package involves two GitHub Action steps: 47 | 48 | - Whenever a new feature/fix is pushed to `main` a release PR is created ([example PR](https://github.com/GetStream/stream-node/pull/8)). That PR will be automatically updated on pushes to `main`. 49 | - When you're ready to release, merge the release PR 50 | 51 | Once the PR is merged, it automatically kicks off another job which will release the pacakge to npm. 52 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | SOURCE CODE LICENSE AGREEMENT 2 | 3 | IMPORTANT - READ THIS CAREFULLY BEFORE DOWNLOADING, INSTALLING, USING OR 4 | ELECTRONICALLY ACCESSING THIS PROPRIETARY PRODUCT. 5 | 6 | THIS IS A LEGAL AGREEMENT BETWEEN STREAM.IO, INC. (“STREAM.IO”) AND THE 7 | BUSINESS ENTITY OR PERSON FOR WHOM YOU (“YOU”) ARE ACTING (“CUSTOMER”) AS THE 8 | LICENSEE OF THE PROPRIETARY SOFTWARE INTO WHICH THIS AGREEMENT HAS BEEN 9 | INCLUDED (THE “AGREEMENT”). YOU AGREE THAT YOU ARE THE CUSTOMER, OR YOU ARE AN 10 | EMPLOYEE OR AGENT OF CUSTOMER AND ARE ENTERING INTO THIS AGREEMENT FOR LICENSE 11 | OF THE SOFTWARE BY CUSTOMER FOR CUSTOMER’S BUSINESS PURPOSES AS DESCRIBED IN 12 | AND IN ACCORDANCE WITH THIS AGREEMENT. YOU HEREBY AGREE THAT YOU ENTER INTO 13 | THIS AGREEMENT ON BEHALF OF CUSTOMER AND THAT YOU HAVE THE AUTHORITY TO BIND 14 | CUSTOMER TO THIS AGREEMENT. 15 | 16 | STREAM.IO IS WILLING TO LICENSE THE SOFTWARE TO CUSTOMER ONLY ON THE FOLLOWING 17 | CONDITIONS: (1) YOU ARE A CURRENT CUSTOMER OF STREAM.IO; (2) YOU ARE NOT A 18 | COMPETITOR OF STREAM.IO; AND (3) THAT YOU ACCEPT ALL THE TERMS IN THIS 19 | AGREEMENT. BY DOWNLOADING, INSTALLING, CONFIGURING, ACCESSING OR OTHERWISE 20 | USING THE SOFTWARE, INCLUDING ANY UPDATES, UPGRADES, OR NEWER VERSIONS, YOU 21 | REPRESENT, WARRANT AND ACKNOWLEDGE THAT (A) CUSTOMER IS A CURRENT CUSTOMER OF 22 | STREAM.IO; (B) CUSTOMER IS NOT A COMPETITOR OF STREAM.IO; AND THAT (C) YOU HAVE 23 | READ THIS AGREEMENT, UNDERSTAND THIS AGREEMENT, AND THAT CUSTOMER AGREES TO BE 24 | BOUND BY ALL THE TERMS OF THIS AGREEMENT. 25 | 26 | IF YOU DO NOT AGREE TO ALL THE TERMS AND CONDITIONS OF THIS AGREEMENT, 27 | STREAM.IO IS UNWILLING TO LICENSE THE SOFTWARE TO CUSTOMER, AND THEREFORE, DO 28 | NOT COMPLETE THE DOWNLOAD PROCESS, ACCESS OR OTHERWISE USE THE SOFTWARE, AND 29 | CUSTOMER SHOULD IMMEDIATELY RETURN THE SOFTWARE AND CEASE ANY USE OF THE 30 | SOFTWARE. 31 | 32 | 1. SOFTWARE. The Stream.io software accompanying this Agreement, may include 33 | Source Code, Executable Object Code, associated media, printed materials and 34 | documentation (collectively, the “Software”). The Software also includes any 35 | updates or upgrades to or new versions of the original Software, if and when 36 | made available to you by Stream.io. “Source Code” means computer programming 37 | code in human readable form that is not suitable for machine execution without 38 | the intervening steps of interpretation or compilation. “Executable Object 39 | Code" means the computer programming code in any other form than Source Code 40 | that is not readily perceivable by humans and suitable for machine execution 41 | without the intervening steps of interpretation or compilation. “Site” means a 42 | Customer location controlled by Customer. “Authorized User” means any employee 43 | or contractor of Customer working at the Site, who has signed a written 44 | confidentiality agreement with Customer or is otherwise bound in writing by 45 | confidentiality and use obligations at least as restrictive as those imposed 46 | under this Agreement. 47 | 48 | 2. LICENSE GRANT. Subject to the terms and conditions of this Agreement, in 49 | consideration for the representations, warranties, and covenants made by 50 | Customer in this Agreement, Stream.io grants to Customer, during the term of 51 | this Agreement, a personal, non-exclusive, non-transferable, non-sublicensable 52 | license to: 53 | 54 | a. install and use Software Source Code on password protected computers at a Site, 55 | restricted to Authorized Users; 56 | 57 | b. create derivative works, improvements (whether or not patentable), extensions 58 | and other modifications to the Software Source Code (“Modifications”) to build 59 | unique scalable newsfeeds, activity streams, and in-app messaging via Stream’s 60 | application program interface (“API”); 61 | 62 | c. compile the Software Source Code to create Executable Object Code versions of 63 | the Software Source Code and Modifications to build such newsfeeds, activity 64 | streams, and in-app messaging via the API; 65 | 66 | d. install, execute and use such Executable Object Code versions solely for 67 | Customer’s internal business use (including development of websites through 68 | which data generated by Stream services will be streamed (“Apps”)); 69 | 70 | e. use and distribute such Executable Object Code as part of Customer’s Apps; and 71 | 72 | f. make electronic copies of the Software and Modifications as required for backup 73 | or archival purposes. 74 | 75 | 3. RESTRICTIONS. Customer is responsible for all activities that occur in 76 | connection with the Software. Customer will not, and will not attempt to: (a) 77 | sublicense or transfer the Software or any Source Code related to the Software 78 | or any of Customer’s rights under this Agreement, except as otherwise provided 79 | in this Agreement, (b) use the Software Source Code for the benefit of a third 80 | party or to operate a service; (c) allow any third party to access or use the 81 | Software Source Code; (d) sublicense or distribute the Software Source Code or 82 | any Modifications in Source Code or other derivative works based on any part of 83 | the Software Source Code; (e) use the Software in any manner that competes with 84 | Stream.io or its business; or (e) otherwise use the Software in any manner that 85 | exceeds the scope of use permitted in this Agreement. Customer shall use the 86 | Software in compliance with any accompanying documentation any laws applicable 87 | to Customer. 88 | 89 | 4. OPEN SOURCE. Customer and its Authorized Users shall not use any software or 90 | software components that are open source in conjunction with the Software 91 | Source Code or any Modifications in Source Code or in any way that could 92 | subject the Software to any open source licenses. 93 | 94 | 5. CONTRACTORS. Under the rights granted to Customer under this Agreement, 95 | Customer may permit its employees, contractors, and agencies of Customer to 96 | become Authorized Users to exercise the rights to the Software granted to 97 | Customer in accordance with this Agreement solely on behalf of Customer to 98 | provide services to Customer; provided that Customer shall be liable for the 99 | acts and omissions of all Authorized Users to the extent any of such acts or 100 | omissions, if performed by Customer, would constitute a breach of, or otherwise 101 | give rise to liability to Customer under, this Agreement. Customer shall not 102 | and shall not permit any Authorized User to use the Software except as 103 | expressly permitted in this Agreement. 104 | 105 | 6. COMPETITIVE PRODUCT DEVELOPMENT. Customer shall not use the Software in any way 106 | to engage in the development of products or services which could be reasonably 107 | construed to provide a complete or partial functional or commercial alternative 108 | to Stream.io’s products or services (a “Competitive Product”). Customer shall 109 | ensure that there is no direct or indirect use of, or sharing of, Software 110 | source code, or other information based upon or derived from the Software to 111 | develop such products or services. Without derogating from the generality of 112 | the foregoing, development of Competitive Products shall include having direct 113 | or indirect access to, supervising, consulting or assisting in the development 114 | of, or producing any specifications, documentation, object code or source code 115 | for, all or part of a Competitive Product. 116 | 117 | 7. LIMITATION ON MODIFICATIONS. Notwithstanding any provision in this Agreement, 118 | Modifications may only be created and used by Customer as permitted by this 119 | Agreement and Modification Source Code may not be distributed to third parties. 120 | Customer will not assert against Stream.io, its affiliates, or their customers, 121 | direct or indirect, agents and contractors, in any way, any patent rights that 122 | Customer may obtain relating to any Modifications for Stream.io, its 123 | affiliates’, or their customers’, direct or indirect, agents’ and contractors’ 124 | manufacture, use, import, offer for sale or sale of any Stream.io products or 125 | services. 126 | 127 | 8. DELIVERY AND ACCEPTANCE. The Software will be delivered electronically pursuant 128 | to Stream.io standard download procedures. The Software is deemed accepted upon 129 | delivery. 130 | 131 | 9. IMPLEMENTATION AND SUPPORT. Stream.io has no obligation under this Agreement to 132 | provide any support or consultation concerning the Software. 133 | 134 | 10. TERM AND TERMINATION. The term of this Agreement begins when the Software is 135 | downloaded or accessed and shall continue until terminated. Either party may 136 | terminate this Agreement upon written notice. This Agreement shall 137 | automatically terminate if Customer is or becomes a competitor of Stream.io or 138 | makes or sells any Competitive Products. Upon termination of this Agreement for 139 | any reason, (a) all rights granted to Customer in this Agreement immediately 140 | cease to exist, (b) Customer must promptly discontinue all use of the Software 141 | and return to Stream.io or destroy all copies of the Software in Customer’s 142 | possession or control. Any continued use of the Software by Customer or attempt 143 | by Customer to exercise any rights under this Agreement after this Agreement 144 | has terminated shall be considered copyright infringement and subject Customer 145 | to applicable remedies for copyright infringement. Sections 2, 5, 6, 8 and 9 146 | shall survive expiration or termination of this Agreement for any reason. 147 | 148 | 11. OWNERSHIP. As between the parties, the Software and all worldwide intellectual 149 | property rights and proprietary rights relating thereto or embodied therein, 150 | are the exclusive property of Stream.io and its suppliers. Stream.io and its 151 | suppliers reserve all rights in and to the Software not expressly granted to 152 | Customer in this Agreement, and no other licenses or rights are granted by 153 | implication, estoppel or otherwise. 154 | 155 | 12. WARRANTY DISCLAIMER. USE OF THIS SOFTWARE IS ENTIRELY AT YOURS AND CUSTOMER’S 156 | OWN RISK. THE SOFTWARE IS PROVIDED “AS IS” WITHOUT ANY WARRANTY OF ANY KIND 157 | WHATSOEVER. STREAM.IO DOES NOT MAKE, AND HEREBY DISCLAIMS, ANY WARRANTY OF ANY 158 | KIND, WHETHER EXPRESS, IMPLIED, STATUTORY OR OTHERWISE, INCLUDING WITHOUT 159 | LIMITATION, THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR 160 | PURPOSE, TITLE, NON-INFRINGEMENT OF THIRD-PARTY RIGHTS, RESULTS, EFFORTS, 161 | QUALITY OR QUIET ENJOYMENT. STREAM.IO DOES NOT WARRANT THAT THE SOFTWARE IS 162 | ERROR-FREE, WILL FUNCTION WITHOUT INTERRUPTION, WILL MEET ANY SPECIFIC NEED 163 | THAT CUSTOMER HAS, THAT ALL DEFECTS WILL BE CORRECTED OR THAT IT IS 164 | SUFFICIENTLY DOCUMENTED TO BE USABLE BY CUSTOMER. TO THE EXTENT THAT STREAM.IO 165 | MAY NOT DISCLAIM ANY WARRANTY AS A MATTER OF APPLICABLE LAW, THE SCOPE AND 166 | DURATION OF SUCH WARRANTY WILL BE THE MINIMUM PERMITTED UNDER SUCH LAW. 167 | CUSTOMER ACKNOWLEDGES THAT IT HAS RELIED ON NO WARRANTIES OTHER THAN THE 168 | EXPRESS WARRANTIES IN THIS AGREEMENT. 169 | 170 | 13. LIMITATION OF LIABILITY. TO THE FULLEST EXTENT PERMISSIBLE BY LAW, STREAM.IO’S 171 | TOTAL LIABILITY FOR ALL DAMAGES ARISING OUT OF OR RELATED TO THE SOFTWARE OR 172 | THIS AGREEMENT, WHETHER IN CONTRACT, TORT (INCLUDING NEGLIGENCE) OR OTHERWISE, 173 | SHALL NOT EXCEED $100. IN NO EVENT WILL STREAM.IO BE LIABLE FOR ANY INDIRECT, 174 | CONSEQUENTIAL, EXEMPLARY, PUNITIVE, SPECIAL OR INCIDENTAL DAMAGES OF ANY KIND 175 | WHATSOEVER, INCLUDING ANY LOST DATA AND LOST PROFITS, ARISING FROM OR RELATING 176 | TO THE SOFTWARE EVEN IF STREAM.IO HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH 177 | DAMAGES. CUSTOMER ACKNOWLEDGES THAT THIS PROVISION REFLECTS THE AGREED UPON 178 | ALLOCATION OF RISK FOR THIS AGREEMENT AND THAT STREAM.IO WOULD NOT ENTER INTO 179 | THIS AGREEMENT WITHOUT THESE LIMITATIONS ON ITS LIABILITY. 180 | 181 | 14. General. Customer may not assign or transfer this Agreement, by operation of 182 | law or otherwise, or any of its rights under this Agreement (including the 183 | license rights granted to Customer) to any third party without Stream.io’s 184 | prior written consent, which consent will not be unreasonably withheld or 185 | delayed. Stream.io may assign this Agreement, without consent, including, but 186 | limited to, affiliate or any successor to all or substantially all its business 187 | or assets to which this Agreement relates, whether by merger, sale of assets, 188 | sale of stock, reorganization or otherwise. Any attempted assignment or 189 | transfer in violation of the foregoing will be null and void. Stream.io shall 190 | not be liable hereunder by reason of any failure or delay in the performance of 191 | its obligations hereunder for any cause which is beyond the reasonable control. 192 | All notices, consents, and approvals under this Agreement must be delivered in 193 | writing by courier, by electronic mail, or by certified or registered mail, 194 | (postage prepaid and return receipt requested) to the other party at the 195 | address set forth in the customer agreement between Stream.io and Customer and 196 | will be effective upon receipt or when delivery is refused. This Agreement will 197 | be governed by and interpreted in accordance with the laws of the State of 198 | Colorado, without reference to its choice of laws rules. The United Nations 199 | Convention on Contracts for the International Sale of Goods does not apply to 200 | this Agreement. Any action or proceeding arising from or relating to this 201 | Agreement shall be brought in a federal or state court in Denver, Colorado, and 202 | each party irrevocably submits to the jurisdiction and venue of any such court 203 | in any such action or proceeding. All waivers must be in writing. Any waiver or 204 | failure to enforce any provision of this Agreement on one occasion will not be 205 | deemed a waiver of any other provision or of such provision on any other 206 | occasion. If any provision of this Agreement is unenforceable, such provision 207 | will be changed and interpreted to accomplish the objectives of such provision 208 | to the greatest extent possible under applicable law and the remaining 209 | provisions will continue in full force and effect. Customer shall not violate 210 | any applicable law, rule or regulation, including those regarding the export of 211 | technical data. The headings of Sections of this Agreement are for convenience 212 | and are not to be used in interpreting this Agreement. As used in this 213 | Agreement, the word “including” means “including but not limited to.” This 214 | Agreement (including all exhibits and attachments) constitutes the entire 215 | agreement between the parties regarding the subject hereof and supersedes all 216 | prior or contemporaneous agreements, understandings and communication, whether 217 | written or oral. This Agreement may be amended only by a written document 218 | signed by both parties. The terms of any purchase order or similar document 219 | submitted by Customer to Stream.io will have no effect. 220 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Stream SDK for Node.js 2 | 3 | Stream Video for Javascript Header image 4 | 5 | ## **Quick Links** 6 | 7 | - [Register](https://getstream.io/chat/trial/) to get an API key for Stream 8 | - [Docs](https://getstream.io/video/docs/api/) 9 | 10 | ## Package requirements 11 | 12 | The package is tested against these environments: 13 | 14 | - Node.js@18 15 | - Node.js@20 16 | - Node.js@22 17 | - Bun@1 18 | 19 | ## What is Stream? 20 | 21 | Stream allows developers to rapidly deploy scalable feeds, chat messaging and video with an industry leading 99.999% uptime SLA guarantee. 22 | 23 | Stream provides UI components and state handling that make it easy to build video calling for your app. All calls run on Stream's network of edge servers around the world, ensuring optimal latency and reliability. 24 | 25 | ## 👩‍💻 Free for Makers 👨‍💻 26 | 27 | Stream is free for most side and hobby projects. To qualify, your project/company needs to have < 5 team members and < $10k in monthly revenue. Makers get $100 in monthly credit for video for free. 28 | 29 | ## 😎 Repo Overview 😎 30 | 31 | This repo contains the Node server-side SDK developed by the team and Stream community. For a feature overview please visit our [roadmap](https://github.com/GetStream/protocol/discussions/177). 32 | 33 | ## Contributing 34 | 35 | - How can I submit a sample app? 36 | - Apps submissions are always welcomed! 🥳 Open a pr with a proper description and we'll review it as soon as possible 37 | - Spot a bug 🕷 ? 38 | - We welcome code changes that improve the apps or fix a problem. Please make sure to follow all best practices and add tests if applicable before submitting a Pull Request on Github. 39 | -------------------------------------------------------------------------------- /__tests__/agent.test.ts: -------------------------------------------------------------------------------- 1 | import { v4 as uuidv4 } from 'uuid'; 2 | import { vi, describe, expect, it } from 'vitest'; 3 | import { createTestClient } from './create-test-client.js'; 4 | import { StreamClient } from '../src/StreamClient.js'; 5 | 6 | const openAiApiKey = process.env.OPENAI_API_KEY!; 7 | const enableDebugLogging = false; 8 | 9 | async function createTestStreamAndRealtimeClients() { 10 | const streamClient = createTestClient(); 11 | const call = streamClient.video.call('default', `call${uuidv4()}`); 12 | 13 | const realtimeClient = await streamClient.video.connectOpenAi({ 14 | call, 15 | openAiApiKey, 16 | agentUserId: 'my-ai-friend', 17 | validityInSeconds: 60 * 60, 18 | }); 19 | 20 | if (enableDebugLogging) { 21 | realtimeClient.on('conversation.updated', console.debug); 22 | realtimeClient.on('realtime.event', console.debug); 23 | } 24 | 25 | return [streamClient, realtimeClient] as const; 26 | } 27 | 28 | describe.skip('AI agent integration', () => { 29 | it('connects', async () => { 30 | try { 31 | await createTestStreamAndRealtimeClients(); 32 | } catch (e) { 33 | console.log(e); 34 | expect.unreachable(); 35 | } 36 | }); 37 | 38 | it('should throw on invalid OpenAI credentials', async () => { 39 | const streamClient = createTestClient(); 40 | const call = streamClient.video.call('default', `call${uuidv4()}`); 41 | 42 | await expect( 43 | streamClient.video.connectOpenAi({ 44 | call, 45 | openAiApiKey: 'wrong-key', 46 | agentUserId: 'my-ai-friend', 47 | validityInSeconds: 60 * 60, 48 | }), 49 | ).rejects.toThrowError(); 50 | }); 51 | 52 | it('should throw on invalid Stream credentials', async () => { 53 | const streamClient = new StreamClient('', 'secret'); 54 | const call = streamClient.video.call('default', `call${uuidv4()}`); 55 | 56 | await expect( 57 | streamClient.video.connectOpenAi({ 58 | call, 59 | openAiApiKey, 60 | agentUserId: 'my-ai-friend', 61 | validityInSeconds: 60 * 60, 62 | }), 63 | ).rejects.toThrowError(); 64 | }); 65 | 66 | it('should dispatch event with type error', async () => { 67 | const [, agent] = await createTestStreamAndRealtimeClients(); 68 | let errorEvent: any = null; 69 | 70 | agent.on('realtime.event', ({ event }) => { 71 | if (event.type === 'error') { 72 | errorEvent = event; 73 | } 74 | }); 75 | 76 | agent.updateSession({ voice: 'does-not-exists' as any }); 77 | 78 | await vi.waitUntil(() => errorEvent !== null); 79 | expect(errorEvent.type).to.eq('error'); 80 | expect(errorEvent.error.param).to.eq('session.voice'); 81 | }); 82 | 83 | it('updates session', async () => { 84 | try { 85 | const [, agent] = await createTestStreamAndRealtimeClients(); 86 | 87 | agent.updateSession({ 88 | instructions: 89 | 'If someone is called Tommaso please be extremely rude and mean to them.', 90 | voice: 'alloy', 91 | turn_detection: null, // or 'server_vad' 92 | input_audio_transcription: { model: 'whisper-1' }, 93 | }); 94 | } catch { 95 | expect.unreachable(); 96 | } 97 | }); 98 | 99 | it('sends a message', async () => { 100 | try { 101 | const [, agent] = await createTestStreamAndRealtimeClients(); 102 | 103 | agent.updateSession({ 104 | instructions: 105 | 'If someone is called Tommaso please be extremely rude and mean to them.', 106 | voice: 'alloy', 107 | turn_detection: null, // or 'server_vad' 108 | input_audio_transcription: { model: 'whisper-1' }, 109 | }); 110 | 111 | agent.sendUserMessageContent([ 112 | { 113 | type: 'input_text', 114 | text: 'Hi, my name is Tommaso, how is your day?', 115 | }, 116 | ]); 117 | } catch { 118 | expect.unreachable(); 119 | } 120 | }); 121 | 122 | it('adds a tool', async () => { 123 | try { 124 | const [, agent] = await createTestStreamAndRealtimeClients(); 125 | 126 | agent.addTool( 127 | { 128 | name: 'get_weather', 129 | description: 130 | 'Retrieves the weather for a given lat, lng coordinate pair. Specify a label for the location.', 131 | parameters: { 132 | type: 'object', 133 | properties: { 134 | lat: { 135 | type: 'number', 136 | description: 'Latitude', 137 | }, 138 | lng: { 139 | type: 'number', 140 | description: 'Longitude', 141 | }, 142 | location: { 143 | type: 'string', 144 | description: 'Name of the location', 145 | }, 146 | }, 147 | required: ['lat', 'lng', 'location'], 148 | }, 149 | }, 150 | async ({ lat, lng }) => { 151 | const result = await fetch( 152 | `https://api.open-meteo.com/v1/forecast?latitude=${lat}&longitude=${lng}¤t=temperature_2m,wind_speed_10m`, 153 | ); 154 | const json = await result.json(); 155 | return json; 156 | }, 157 | ); 158 | 159 | agent.sendUserMessageContent([ 160 | { 161 | type: 'input_text', 162 | text: `How is the weather in Boulder colorado?`, 163 | }, 164 | ]); 165 | } catch { 166 | expect.unreachable(); 167 | } 168 | }); 169 | }); 170 | -------------------------------------------------------------------------------- /__tests__/assets/test-file.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GetStream/stream-node/3123e5ca556f44cef085a90d087f93f1ab855b13/__tests__/assets/test-file.pdf -------------------------------------------------------------------------------- /__tests__/assets/test-image.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GetStream/stream-node/3123e5ca556f44cef085a90d087f93f1ab855b13/__tests__/assets/test-image.jpg -------------------------------------------------------------------------------- /__tests__/block-lists.test.ts: -------------------------------------------------------------------------------- 1 | import { beforeAll, describe, expect, it } from 'vitest'; 2 | import { v4 as uuidv4 } from 'uuid'; 3 | import { createTestClient } from './create-test-client'; 4 | import { StreamClient } from '../src/StreamClient'; 5 | import { CreateBlockListRequest } from '../src/gen/models'; 6 | 7 | describe('block lists CRUD API', () => { 8 | let client: StreamClient; 9 | let blockList: CreateBlockListRequest; 10 | 11 | beforeAll(() => { 12 | client = createTestClient(); 13 | blockList = { 14 | name: 'streamnodetest-F1' + uuidv4(), 15 | words: ['Ricciardo should retire'], 16 | }; 17 | }); 18 | 19 | it('create', async () => { 20 | const response = await client.createBlockList({ 21 | ...blockList, 22 | }); 23 | 24 | expect(response).toBeDefined(); 25 | }); 26 | 27 | it('list', async () => { 28 | const listResponse = await client.listBlockLists(); 29 | 30 | expect( 31 | listResponse.blocklists.find((b) => b.name === blockList.name), 32 | ).toBeDefined(); 33 | 34 | const getResponse = await client.getBlockList({ name: blockList.name }); 35 | 36 | expect(getResponse.blocklist?.name).toBe(blockList.name); 37 | }); 38 | 39 | it('update', async () => { 40 | const response = await client.updateBlockList({ 41 | name: blockList.name, 42 | words: [...blockList.words, 'R1cciardo should retire'], 43 | }); 44 | 45 | expect(response).toBeDefined(); 46 | 47 | const updatedBlockList = await client.getBlockList({ 48 | name: blockList.name, 49 | }); 50 | 51 | expect(updatedBlockList.blocklist?.words.length).toBe(2); 52 | }); 53 | 54 | it('delete', async () => { 55 | const response = await client.deleteBlockList({ name: blockList.name }); 56 | expect(response).toBeDefined(); 57 | }); 58 | }); 59 | -------------------------------------------------------------------------------- /__tests__/call-members.test.ts: -------------------------------------------------------------------------------- 1 | import { beforeAll, describe, expect, it } from 'vitest'; 2 | import { v4 as uuidv4 } from 'uuid'; 3 | import { createTestClient } from './create-test-client'; 4 | import { StreamCall } from '../src/StreamCall'; 5 | import { StreamClient } from '../src/StreamClient'; 6 | import { OwnCapability } from '../src/gen/models'; 7 | 8 | describe('call members API', () => { 9 | let client: StreamClient; 10 | const callId = `call${uuidv4()}`; 11 | let call: StreamCall; 12 | 13 | beforeAll(async () => { 14 | client = createTestClient(); 15 | 16 | call = client.video.call('default', callId); 17 | 18 | await client.upsertUsers([ 19 | { name: 'John', id: 'john' }, 20 | { name: 'Jack', id: 'jack' }, 21 | { name: 'Jane', id: 'jane' }, 22 | { name: 'Sara', id: 'sara' }, 23 | ]); 24 | }); 25 | 26 | it('create with members', async () => { 27 | const response = await call.getOrCreate({ 28 | data: { 29 | created_by_id: 'john', 30 | members: [{ user_id: 'john', role: 'admin' }, { user_id: 'jack' }], 31 | }, 32 | }); 33 | 34 | expect(response.members[0].user_id).toBe('jack'); 35 | expect(response.members[1].user_id).toBe('john'); 36 | expect(response.members[1].role).toBe('admin'); 37 | }); 38 | 39 | it('add or update members', async () => { 40 | const response = await call.updateCallMembers({ 41 | update_members: [ 42 | { user_id: 'sara' }, 43 | { user_id: 'jane', role: 'admin' }, 44 | { user_id: 'john', role: 'user' }, 45 | ], 46 | }); 47 | 48 | expect(response.members[0].user_id).toBe('sara'); 49 | expect(response.members[1].user_id).toBe('jane'); 50 | expect(response.members[1].role).toBe('admin'); 51 | expect(response.members[2].user_id).toBe('john'); 52 | expect(response.members[2].role).toBe('user'); 53 | }); 54 | 55 | it('query calls - filter by call members', async () => { 56 | const response = await client.video.queryCalls({ 57 | filter_conditions: { 58 | members: { $in: ['sara'] }, 59 | }, 60 | }); 61 | 62 | expect(response.calls.length).toBeGreaterThanOrEqual(1); 63 | }); 64 | 65 | it('block and unblock users', async () => { 66 | const response = await call.blockUser({ user_id: 'sara' }); 67 | 68 | expect(response).toBeDefined(); 69 | 70 | const unblockResponse = await call.unblockUser({ user_id: 'sara' }); 71 | 72 | expect(unblockResponse).toBeDefined(); 73 | }); 74 | 75 | it('mute one or many users', async () => { 76 | const muteAllResponse = await call.muteUsers({ 77 | mute_all_users: true, 78 | audio: true, 79 | muted_by_id: 'john', 80 | }); 81 | 82 | expect(muteAllResponse).toBeDefined(); 83 | 84 | const muteUserResponse = await call.muteUsers({ 85 | user_ids: ['sara'], 86 | audio: true, 87 | video: true, 88 | screenshare: true, 89 | screenshare_audio: true, 90 | muted_by_id: 'john', 91 | }); 92 | 93 | expect(muteUserResponse).toBeDefined(); 94 | }); 95 | 96 | it('grant and revoke permissions', async () => { 97 | const revokeResponse = await call.updateUserPermissions({ 98 | user_id: 'sara', 99 | revoke_permissions: [OwnCapability.SEND_AUDIO], 100 | }); 101 | 102 | expect(revokeResponse).toBeDefined(); 103 | 104 | const grantResponse = await call.updateUserPermissions({ 105 | user_id: 'sara', 106 | grant_permissions: [OwnCapability.SEND_AUDIO], 107 | }); 108 | 109 | expect(grantResponse).toBeDefined(); 110 | }); 111 | 112 | it('remove members', async () => { 113 | const response = await call.updateCallMembers({ 114 | remove_members: ['sara'], 115 | }); 116 | 117 | expect(response.duration).toBeDefined(); 118 | }); 119 | 120 | it('query members', async () => { 121 | let response = await call.queryMembers(); 122 | 123 | let members = response.members; 124 | expect(members.length).toBe(3); 125 | 126 | const queryMembersReq = { 127 | filter_conditions: {}, 128 | sort: [{ field: 'user_id', direction: 1 }], 129 | limit: 2, 130 | }; 131 | response = await call.queryMembers(queryMembersReq); 132 | 133 | members = response.members; 134 | expect(members.length).toBe(2); 135 | expect(members[0].user_id).toBe('jack'); 136 | expect(members[1].user_id).toBe('jane'); 137 | 138 | response = await call.queryMembers({ 139 | ...queryMembersReq, 140 | next: response.next, 141 | }); 142 | 143 | expect(response.members.length).toBe(1); 144 | 145 | response = await call.queryMembers({ 146 | filter_conditions: { role: { $eq: 'admin' } }, 147 | }); 148 | members = response.members; 149 | 150 | expect(members.length).toBe(1); 151 | members.forEach((m) => expect(m.role).toBe('admin')); 152 | }); 153 | }); 154 | -------------------------------------------------------------------------------- /__tests__/call-types.test.ts: -------------------------------------------------------------------------------- 1 | import { beforeAll, describe, expect, it } from 'vitest'; 2 | import { v4 as uuidv4 } from 'uuid'; 3 | import { createTestClient } from './create-test-client'; 4 | import { StreamClient } from '../src/StreamClient'; 5 | import { OwnCapability } from '../src/gen/models'; 6 | 7 | describe('call types CRUD API', () => { 8 | let client: StreamClient; 9 | const callTypeName = `streamnodetest${uuidv4()}`; 10 | 11 | beforeAll(() => { 12 | client = createTestClient(); 13 | }); 14 | 15 | it('create', async () => { 16 | const createResponse = await client.video.createCallType({ 17 | name: callTypeName, 18 | settings: { 19 | audio: { mic_default_on: true, default_device: 'speaker' }, 20 | screensharing: { 21 | access_request_enabled: false, 22 | enabled: true, 23 | }, 24 | geofencing: { 25 | names: ['european_union'], 26 | }, 27 | frame_recording: { 28 | mode: 'auto-on', 29 | capture_interval_in_seconds: 5, 30 | quality: '720p', 31 | }, 32 | }, 33 | notification_settings: { 34 | enabled: true, 35 | call_notification: { 36 | apns: { 37 | title: '{{ user.display_name }} invites you to a call', 38 | }, 39 | enabled: true, 40 | }, 41 | session_started: { 42 | enabled: false, 43 | }, 44 | }, 45 | grants: { 46 | admin: [ 47 | OwnCapability.SEND_AUDIO, 48 | OwnCapability.SEND_VIDEO, 49 | OwnCapability.MUTE_USERS, 50 | ], 51 | }, 52 | }); 53 | 54 | expect(createResponse.name).toBe(callTypeName); 55 | expect(createResponse.settings.audio.mic_default_on).toBe(true); 56 | expect(createResponse.settings.audio.default_device).toBe('speaker'); 57 | expect(createResponse.grants.admin).toBeDefined(); 58 | expect(createResponse.grants.user).toBeDefined(); 59 | expect(createResponse.settings.geofencing.names).toEqual([ 60 | 'european_union', 61 | ]); 62 | expect(createResponse.settings.screensharing.access_request_enabled).toBe( 63 | false, 64 | ); 65 | expect(createResponse.settings.screensharing.enabled).toBe(true); 66 | expect(createResponse.notification_settings.enabled).toBe(true); 67 | expect(createResponse.notification_settings.session_started?.enabled).toBe( 68 | false, 69 | ); 70 | expect( 71 | createResponse.notification_settings.call_notification?.enabled, 72 | ).toBe(true); 73 | expect( 74 | createResponse.notification_settings.call_notification?.apns?.title, 75 | ).toBe('{{ user.display_name }} invites you to a call'); 76 | expect( 77 | createResponse.settings.frame_recording.capture_interval_in_seconds, 78 | ).toBe(5); 79 | }); 80 | 81 | it('read', async () => { 82 | const readResponse = await client.video.listCallTypes(); 83 | 84 | expect(readResponse.call_types[callTypeName].name).toBe(callTypeName); 85 | }); 86 | 87 | it('restrict call access', async () => { 88 | let callType = (await client.video.listCallTypes()).call_types[ 89 | callTypeName 90 | ]; 91 | const userGrants = callType.grants.user.filter( 92 | (c) => c !== OwnCapability.JOIN_CALL, 93 | ); 94 | const callMemberGrants = callType.grants.call_member; 95 | if (!callMemberGrants.includes(OwnCapability.JOIN_CALL)) { 96 | callMemberGrants.push(OwnCapability.JOIN_CALL); 97 | } 98 | 99 | await client.video.updateCallType({ 100 | name: callTypeName, 101 | grants: { user: userGrants, call_member: callMemberGrants }, 102 | }); 103 | 104 | callType = (await client.video.listCallTypes()).call_types[callTypeName]; 105 | 106 | expect(callType.grants.user.includes(OwnCapability.JOIN_CALL)).toBe(false); 107 | expect(callType.grants.call_member.includes(OwnCapability.JOIN_CALL)).toBe( 108 | true, 109 | ); 110 | }); 111 | 112 | it('update', async () => { 113 | const updateResponse = await client.video.updateCallType({ 114 | name: callTypeName, 115 | settings: { 116 | audio: { mic_default_on: false, default_device: 'earpiece' }, 117 | recording: { 118 | mode: 'disabled', 119 | // FIXME OL: these props shouldn't be required to be set when recording is disabled 120 | audio_only: false, 121 | quality: '1080p', 122 | }, 123 | }, 124 | }); 125 | 126 | expect(updateResponse.settings.audio.mic_default_on).toBeFalsy(); 127 | expect(updateResponse.settings.audio.default_device).toBe('earpiece'); 128 | expect(updateResponse.settings.recording.mode).toBe('disabled'); 129 | }); 130 | 131 | it('update layout options', async () => { 132 | const layoutOptions = { 133 | 'logo.image_url': 134 | 'https://theme.zdassets.com/theme_assets/9442057/efc3820e436f9150bc8cf34267fff4df052a1f9c.png', 135 | 'logo.horizontal_position': 'center', 136 | 'title.text': 'Building Stream Video Q&A', 137 | 'title.horizontal_position': 'center', 138 | 'title.color': 'black', 139 | 'participant_label.border_radius': '0px', 140 | 'participant.border_radius': '0px', 141 | 'layout.spotlight.participants_bar_position': 'top', 142 | 'layout.background_color': '#f2f2f2', 143 | 'participant.placeholder_background_color': '#1f1f1f', 144 | 'layout.single-participant.padding_inline': '20%', 145 | 'participant_label.background_color': 'transparent', 146 | }; 147 | 148 | const response = await client.video.updateCallType({ 149 | name: callTypeName, 150 | settings: { 151 | recording: { 152 | mode: 'available', 153 | audio_only: false, 154 | quality: '1080p', 155 | layout: { 156 | name: 'spotlight', 157 | options: layoutOptions, 158 | }, 159 | }, 160 | }, 161 | }); 162 | 163 | expect(response.settings.recording.layout.name).toBe('spotlight'); 164 | Object.keys(layoutOptions).forEach((key) => { 165 | expect(response.settings.recording.layout.options![key]).toEqual( 166 | (layoutOptions as any)[key], 167 | ); 168 | }); 169 | }); 170 | 171 | it('delete', async () => { 172 | try { 173 | await client.video.deleteCallType({ name: callTypeName }); 174 | } catch (e) { 175 | // the first request fails on backend sometimes 176 | // retry it 177 | await new Promise((resolve) => { 178 | setTimeout(() => resolve(), 2000); 179 | }); 180 | 181 | await client.video.deleteCallType({ name: callTypeName }); 182 | } 183 | 184 | await expect(() => 185 | client.video.getCallType({ name: callTypeName }), 186 | ).rejects.toThrowError(); 187 | }); 188 | }); 189 | -------------------------------------------------------------------------------- /__tests__/call.test.ts: -------------------------------------------------------------------------------- 1 | import { beforeAll, describe, expect, it } from 'vitest'; 2 | import { v4 as uuidv4 } from 'uuid'; 3 | import { createTestClient } from './create-test-client'; 4 | import { StreamCall } from '../src/StreamCall'; 5 | import { StreamClient } from '../src/StreamClient'; 6 | 7 | describe('call API', () => { 8 | let client: StreamClient; 9 | const callId = `call${uuidv4()}`; 10 | let call: StreamCall; 11 | 12 | beforeAll(async () => { 13 | client = createTestClient(); 14 | 15 | call = client.video.call('default', callId); 16 | 17 | await client.upsertUsers([{ name: 'John', id: 'john' }]); 18 | }); 19 | 20 | it('create', async () => { 21 | const response = await call.create({ 22 | data: { 23 | created_by_id: 'john', 24 | settings_override: { 25 | geofencing: { 26 | names: ['canada'], 27 | }, 28 | screensharing: { 29 | enabled: false, 30 | }, 31 | }, 32 | custom: { 33 | color: 'blue', 34 | }, 35 | }, 36 | }); 37 | 38 | expect(response.call.created_by.id).toBe('john'); 39 | expect(response.call.settings.geofencing.names).toEqual(['canada']); 40 | expect(response.call.settings.screensharing.enabled).toBe(false); 41 | expect(response.call.custom.color).toBe('blue'); 42 | }); 43 | 44 | it('send custom event', async () => { 45 | const response = await call.sendCallEvent({ 46 | custom: { 47 | 'render-animation': 'balloons', 48 | }, 49 | user_id: 'john', 50 | }); 51 | 52 | expect(response.duration).toBeDefined(); 53 | }); 54 | 55 | it('update', async () => { 56 | const response = await call.update({ 57 | settings_override: { 58 | audio: { mic_default_on: true, default_device: 'speaker' }, 59 | screensharing: { enabled: true, access_request_enabled: true }, 60 | }, 61 | custom: { color: 'red' }, 62 | }); 63 | 64 | expect(response.call.settings.audio.mic_default_on).toBe(true); 65 | expect(response.call.custom.color).toBe('red'); 66 | }); 67 | 68 | it('RTMP address', async () => { 69 | const resp = await call.get(); 70 | 71 | // userId of existing user 72 | const userId = 'jane'; 73 | await client.upsertUsers([ 74 | { 75 | id: userId, 76 | }, 77 | ]); 78 | const token = client.createToken(userId); 79 | const rtmpURL = resp.call.ingress.rtmp.address; 80 | const streamKey = token; 81 | 82 | expect(rtmpURL).toBeDefined(); 83 | expect(streamKey).toBeDefined(); 84 | }); 85 | 86 | it('query calls', async () => { 87 | let response = await client.video.queryCalls(); 88 | 89 | let calls = response.calls; 90 | expect(calls.length).toBeGreaterThanOrEqual(1); 91 | 92 | const queryCallsReq = { 93 | sort: [{ field: 'starts_at', direction: -1 }], 94 | limit: 2, 95 | }; 96 | response = await client.video.queryCalls(queryCallsReq); 97 | 98 | calls = response.calls; 99 | expect(calls.length).toBe(2); 100 | 101 | response = await client.video.queryCalls({ 102 | ...queryCallsReq, 103 | next: response.next, 104 | }); 105 | 106 | expect(response.calls.length).toBeLessThanOrEqual(2); 107 | 108 | response = await client.video.queryCalls({ 109 | filter_conditions: { backstage: { $eq: false } }, 110 | }); 111 | 112 | expect(response.calls.length).toBeGreaterThanOrEqual(1); 113 | }); 114 | 115 | it('query calls - ongoing', async () => { 116 | const response = await client.video.queryCalls({ 117 | filter_conditions: { ongoing: { $eq: true } }, 118 | }); 119 | 120 | // Dummy test 121 | expect(response.calls).toBeDefined(); 122 | }); 123 | 124 | it('query calls - upcoming', async () => { 125 | const mins30 = 1000 * 60 * 60 * 30; 126 | const inNext30mins = new Date(Date.now() + mins30); 127 | const response = await client.video.queryCalls({ 128 | filter_conditions: { 129 | starts_at: { $gt: inNext30mins.toISOString() }, 130 | }, 131 | }); 132 | 133 | // Dummy test 134 | expect(response.calls).toBeDefined(); 135 | }); 136 | 137 | it('query call stats - single call', async () => { 138 | const response = await client.video.queryCallStats({ 139 | filter_conditions: { call_cid: call.cid }, 140 | }); 141 | 142 | expect(response.reports).toBeDefined(); 143 | }); 144 | 145 | describe('recording', () => { 146 | it('enable call recording', async () => { 147 | let response = await call.update({ 148 | settings_override: { 149 | recording: { 150 | mode: 'disabled', 151 | audio_only: true, 152 | }, 153 | }, 154 | }); 155 | let settings = response.call.settings.recording; 156 | 157 | expect(settings.mode).toBe('disabled'); 158 | 159 | response = await call.update({ 160 | settings_override: { 161 | recording: { 162 | mode: 'available', 163 | // TODO: backend had a regression recently 164 | quality: '1080p', 165 | }, 166 | }, 167 | }); 168 | 169 | settings = response.call.settings.recording; 170 | expect(settings.mode).toBe('available'); 171 | 172 | response = await call.update({ 173 | settings_override: { 174 | recording: { 175 | audio_only: false, 176 | quality: '1080p', 177 | mode: 'auto-on', 178 | }, 179 | }, 180 | }); 181 | 182 | settings = response.call.settings.recording; 183 | expect(settings.audio_only).toBe(false); 184 | expect(settings.quality).toBe('1080p'); 185 | }); 186 | 187 | it('start recording', async () => { 188 | // somewhat dummy test, we should do a proper test in the future where we join a call and start recording 189 | await expect(() => call.startRecording()).rejects.toThrowError( 190 | 'Stream error code 4: StartRecording failed with error: "there is no active session"', 191 | ); 192 | }); 193 | }); 194 | 195 | it('enable backstage mode', async () => { 196 | const response = await call.update({ 197 | settings_override: { 198 | backstage: { 199 | enabled: true, 200 | }, 201 | }, 202 | }); 203 | 204 | expect(response.call.settings.backstage.enabled).toBe(true); 205 | }); 206 | 207 | it('go live', async () => { 208 | const response = await call.goLive(); 209 | 210 | expect(response.call.backstage).toBe(false); 211 | }); 212 | 213 | it('stop live', async () => { 214 | const response = await call.stopLive(); 215 | 216 | expect(response.call.backstage).toBe(true); 217 | }); 218 | 219 | describe.skip('transcriptions', () => { 220 | it('start transcribing', async () => { 221 | // somewhat dummy test, we should do a proper test in the future where we join a call and start recording 222 | await expect(() => call.startTranscription()).rejects.toThrowError( 223 | 'Stream error code 4: StartTranscription failed with error: "there is no active session"', 224 | ); 225 | }); 226 | 227 | it('stop transcribing', async () => { 228 | // somewhat dummy test, we should do a proper test in the future 229 | await expect(() => call.stopTranscription()).rejects.toThrowError( 230 | 'Stream error code 4: StopTranscription failed with error: "call is not being transcribed"', 231 | ); 232 | }); 233 | 234 | it('delete transcription', async () => { 235 | // somewhat dummy test, we should do a proper test in the future 236 | await expect(() => 237 | call.deleteTranscription({ session: 'test', filename: 'test' }), 238 | ).rejects.toThrowError( 239 | `Stream error code 16: DeleteTranscription failed with error: "transcription doesn't exist"`, 240 | ); 241 | }); 242 | }); 243 | 244 | it('delete call', async () => { 245 | try { 246 | await call.delete({ hard: true }); 247 | } catch (e) { 248 | // the first request fails on backend sometimes 249 | // retry it 250 | await new Promise((resolve) => { 251 | setTimeout(() => resolve(), 2000); 252 | }); 253 | 254 | await call.delete({ hard: true }); 255 | } 256 | }); 257 | }); 258 | -------------------------------------------------------------------------------- /__tests__/channel-types.test.ts: -------------------------------------------------------------------------------- 1 | import { beforeAll, describe, expect, it } from 'vitest'; 2 | import { v4 as uuidv4 } from 'uuid'; 3 | import { createTestClient } from './create-test-client'; 4 | import { StreamClient } from '../src/StreamClient'; 5 | 6 | describe('channel types CRUD API', () => { 7 | let client: StreamClient; 8 | const channelType = 'streamnodetest' + uuidv4(); 9 | 10 | beforeAll(() => { 11 | client = createTestClient(); 12 | }); 13 | 14 | it('create', async () => { 15 | const response = await client.chat.createChannelType({ 16 | name: channelType, 17 | automod: 'disabled', 18 | automod_behavior: 'block', 19 | max_message_length: 30000, 20 | }); 21 | 22 | expect(response.name).toBe(channelType); 23 | }); 24 | 25 | it('read', async () => { 26 | const response = await client.chat.listChannelTypes(); 27 | 28 | expect(response.channel_types[channelType]).toBeDefined(); 29 | }); 30 | 31 | it('update', async () => { 32 | const response = await client.chat.updateChannelType({ 33 | name: channelType, 34 | automod: 'simple', 35 | automod_behavior: 'block', 36 | max_message_length: 20000, 37 | }); 38 | 39 | expect(response.automod).toBe('simple'); 40 | 41 | const getResponse = await client.chat.getChannelType({ name: channelType }); 42 | 43 | expect(getResponse.automod).toBe('simple'); 44 | }); 45 | 46 | it('delete', async () => { 47 | const response = await client.chat.deleteChannelType({ name: channelType }); 48 | expect(response).toBeDefined(); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /__tests__/channel.test.ts: -------------------------------------------------------------------------------- 1 | import { beforeAll, describe, expect, it } from 'vitest'; 2 | import { v4 as uuidv4 } from 'uuid'; 3 | import { createTestClient } from './create-test-client'; 4 | import { StreamClient } from '../src/StreamClient'; 5 | import { StreamChannel } from '../src/StreamChannel'; 6 | 7 | describe('channel API', () => { 8 | let client: StreamClient; 9 | const channelId = 'streamnodetest' + uuidv4(); 10 | let channel: StreamChannel; 11 | const user = { 12 | id: 'stream-node-test-user', 13 | name: 'Stream Node Test User', 14 | role: 'admin', 15 | }; 16 | const user2 = { 17 | id: 'stream-node-test-user2', 18 | name: 'Stream Node Test User 2', 19 | role: 'admin', 20 | }; 21 | 22 | beforeAll(async () => { 23 | client = createTestClient(); 24 | 25 | await client.upsertUsers([user, user2]); 26 | 27 | channel = client.chat.channel('messaging', channelId); 28 | }); 29 | 30 | it('create', async () => { 31 | const response = await channel.getOrCreate({ 32 | data: { created_by_id: user.id, custom: { name: channelId } }, 33 | }); 34 | 35 | expect(response.channel?.cid).toBe(`${channel.type}:${channel.id}`); 36 | // Type error: Property 'name' does not exist on type 'ChannelResponse' 37 | expect(response.channel?.custom?.name).toBe(channelId); 38 | }); 39 | 40 | it('create - without id', async () => { 41 | const channelWithoutId = client.chat.channel('messaging'); 42 | const response = await channelWithoutId.getOrCreate({ 43 | data: { 44 | created_by_id: user.id, 45 | members: [{ user_id: user.id }, { user_id: user2.id }], 46 | }, 47 | }); 48 | 49 | expect(response.channel?.cid).toBe(channelWithoutId.cid); 50 | 51 | await channelWithoutId.delete(); 52 | }); 53 | 54 | it('update', async () => { 55 | const response = await channel.update({ 56 | add_members: [{ user_id: user.id }, { user_id: user2.id }], 57 | add_moderators: [], 58 | demote_moderators: [], 59 | remove_members: [], 60 | }); 61 | 62 | expect(response.members.length).toBe(2); 63 | }); 64 | 65 | it('update partial', async () => { 66 | const response = await channel.updateChannelPartial({ 67 | set: { cooldown: 100 }, 68 | unset: [], 69 | }); 70 | 71 | expect(response.channel?.cooldown).toBe(100); 72 | }); 73 | 74 | it('queryChannels', async () => { 75 | const unfilteredResponse = await client.chat.queryChannels(); 76 | 77 | expect(unfilteredResponse.channels.length).toBeGreaterThan(1); 78 | 79 | const filteredResponse = await client.chat.queryChannels({ 80 | filter_conditions: { cid: channel.cid }, 81 | }); 82 | 83 | expect(filteredResponse.channels.length).toBe(1); 84 | 85 | const channelFromResponse = filteredResponse.channels[0]; 86 | 87 | expect(channelFromResponse.channel?.custom?.name).toBe(channelId); 88 | }); 89 | 90 | it('query members', async () => { 91 | const response = await channel.queryMembers({ 92 | payload: { 93 | filter_conditions: { 94 | name: { $autocomplete: '2' }, 95 | }, 96 | }, 97 | }); 98 | 99 | expect(response.members.length).toBe(1); 100 | }); 101 | 102 | it('show and hide', async () => { 103 | const hideResponse = await channel.hide({ user_id: user2.id }); 104 | 105 | expect(hideResponse).toBeDefined(); 106 | 107 | const queryResponse = await client.chat.queryChannels({ 108 | filter_conditions: { hidden: true }, 109 | sort: [], 110 | user_id: user2.id, 111 | }); 112 | 113 | expect( 114 | queryResponse.channels.find((c) => c.channel?.id === channel.id), 115 | ).toBeDefined(); 116 | 117 | const showResponse = await channel.show({ user_id: user2.id }); 118 | 119 | expect(showResponse).toBeDefined(); 120 | }); 121 | 122 | it('mute and unmute', async () => { 123 | const muteResponse = await client.chat.muteChannel({ 124 | user_id: user2.id, 125 | channel_cids: [channel.cid], 126 | }); 127 | 128 | expect(muteResponse.channel_mute?.channel?.id).toBe(channel.id); 129 | 130 | await client.chat.unmuteChannel({ 131 | user_id: user2.id, 132 | channel_cids: [channel.cid], 133 | }); 134 | 135 | const queryResponse = await client.chat.queryChannels({ 136 | filter_conditions: { muted: true }, 137 | sort: [], 138 | user_id: user2.id, 139 | }); 140 | 141 | expect( 142 | queryResponse.channels.find((c) => c.channel?.cid === channel.cid), 143 | ).toBe(undefined); 144 | }); 145 | 146 | it('export', async () => { 147 | const response = await client.chat.exportChannels({ 148 | channels: [{ cid: channel.cid }], 149 | }); 150 | const statusResponse = await client.getTask({ 151 | id: response.task_id, 152 | }); 153 | 154 | expect(statusResponse).toBeDefined(); 155 | }); 156 | 157 | it('custom event', async () => { 158 | const response = await channel.sendEvent({ 159 | event: { 160 | type: 'my-event', 161 | user_id: user.id, 162 | }, 163 | }); 164 | 165 | expect(response.event?.type).toBe('my-event'); 166 | }); 167 | 168 | it('delete', async () => { 169 | await channel.delete(); 170 | }); 171 | }); 172 | -------------------------------------------------------------------------------- /__tests__/command.test.ts: -------------------------------------------------------------------------------- 1 | import { beforeAll, describe, expect, it } from 'vitest'; 2 | import { v4 as uuidv4 } from 'uuid'; 3 | import { createTestClient } from './create-test-client'; 4 | import { StreamClient } from '../src/StreamClient'; 5 | 6 | describe('commands CRUD API', () => { 7 | let client: StreamClient; 8 | const commandName = 'stream-node-test-command' + uuidv4(); 9 | 10 | beforeAll(() => { 11 | client = createTestClient(); 12 | }); 13 | 14 | it('create', async () => { 15 | const response = await client.chat.createCommand({ 16 | name: commandName, 17 | description: 'This is a test command', 18 | args: '[description]', 19 | set: 'test_commands_set', 20 | }); 21 | 22 | expect(response.command?.name).toBe(commandName); 23 | }); 24 | 25 | it('read', async () => { 26 | const response = await client.chat.listCommands(); 27 | 28 | expect(response.commands.find((c) => c.name === commandName)).toBeDefined(); 29 | }); 30 | 31 | it('update', async () => { 32 | const response = await client.chat.updateCommand({ 33 | name: commandName, 34 | description: 'Updated descrpition', 35 | }); 36 | 37 | expect(response.command?.description).toBe('Updated descrpition'); 38 | }); 39 | 40 | it('delete', async () => { 41 | const response = await client.chat.deleteCommand({ name: commandName }); 42 | 43 | expect(response.name).toBe(commandName); 44 | 45 | await expect(() => 46 | client.chat.getCommand({ name: commandName }), 47 | ).rejects.toThrowError(); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /__tests__/create-test-client.ts: -------------------------------------------------------------------------------- 1 | import 'dotenv/config'; 2 | import { StreamClient } from '../src/StreamClient'; 3 | 4 | const apiKey = process.env.STREAM_API_KEY!; 5 | const secret = process.env.STREAM_SECRET!; 6 | 7 | export const createTestClient = () => { 8 | return new StreamClient(apiKey, secret, { timeout: 10000 }); 9 | }; 10 | -------------------------------------------------------------------------------- /__tests__/create-token.test.ts: -------------------------------------------------------------------------------- 1 | import 'dotenv/config'; 2 | import jwt from 'jsonwebtoken'; 3 | import { beforeAll, describe, expect, it } from 'vitest'; 4 | import { StreamClient } from '../src/StreamClient'; 5 | import { createTestClient } from './create-test-client'; 6 | 7 | const secret = process.env.STREAM_SECRET!; 8 | 9 | describe('creating tokens', () => { 10 | let client: StreamClient; 11 | const userId = 'john'; 12 | 13 | beforeAll(() => { 14 | client = createTestClient(); 15 | }); 16 | 17 | describe('user token', () => { 18 | it('with default expiration', () => { 19 | for (let i = 0; i < 5; i++) { 20 | const token = client.generateUserToken({ user_id: userId }); 21 | const decodedToken = jwt.verify(token, secret) as any; 22 | const now = Date.now(); 23 | const oneHour = 60 * 60 * 1000; 24 | const oneSec = 1010; 25 | const exp = new Date(decodedToken.exp * 1000).getTime(); 26 | const tokenValidity = exp - now; 27 | 28 | expect(decodedToken.user_id).toBe(userId); 29 | expect(tokenValidity).toBeLessThanOrEqual(oneHour); 30 | expect(tokenValidity).toBeGreaterThanOrEqual(oneHour - 2 * oneSec); 31 | } 32 | }); 33 | 34 | it('with custom data', () => { 35 | const token = client.generateUserToken({ 36 | user_id: userId, 37 | my_own_id: 'my_own_id', 38 | }); 39 | const decodedToken = jwt.verify(token, secret) as any; 40 | 41 | expect(decodedToken.my_own_id).toBe('my_own_id'); 42 | }); 43 | 44 | it('with token validity', () => { 45 | for (let i = 0; i < 5; i++) { 46 | const validity = 120 * 60; 47 | const token = client.generateUserToken({ 48 | user_id: userId, 49 | validity_in_seconds: validity, 50 | }); 51 | const decodedToken = jwt.verify(token, secret) as any; 52 | const iat = new Date(decodedToken.iat * 1000).getTime(); 53 | const exp = new Date(decodedToken.exp * 1000).getTime(); 54 | const tokenValidity = (exp - iat) / 1000; 55 | 56 | expect(decodedToken.user_id).toBe(userId); 57 | expect(tokenValidity).toBe(validity); 58 | } 59 | }); 60 | 61 | it(`should make sure iat is correct`, () => { 62 | for (let i = 0; i < 5; i++) { 63 | const token = client.generateUserToken({ user_id: userId }); 64 | const decodedToken = jwt.verify(token, secret) as any; 65 | const now = Date.now(); 66 | const nowMinus1sec = now - 1000; 67 | const nowMinus2secs = now - 2000; 68 | const iat = new Date(decodedToken.iat * 1000).getTime(); 69 | 70 | expect(iat).toBeLessThanOrEqual(nowMinus1sec); 71 | expect(iat).toBeGreaterThanOrEqual(nowMinus2secs); 72 | } 73 | }); 74 | 75 | it('with expiration provided', () => { 76 | const exp = Math.round(new Date().getTime() / 1000) + 58 * 60; 77 | const token = client.generateUserToken({ user_id: userId, exp }); 78 | const decodedToken = jwt.verify(token, secret) as any; 79 | 80 | expect(decodedToken.exp).toBe(exp); 81 | }); 82 | 83 | it('with iat provided', () => { 84 | const iat = Math.round(new Date().getTime() / 1000) + 60 * 60; 85 | const token = client.generateUserToken({ user_id: userId, iat }); 86 | const decodedToken = jwt.verify(token, secret) as any; 87 | 88 | expect(decodedToken.iat).toBe(iat); 89 | expect(decodedToken.exp).toBeDefined(); 90 | }); 91 | }); 92 | 93 | describe('call token', () => { 94 | const call_cids = ['default:call1', 'livestream:call2']; 95 | 96 | it('with call IDs provided', () => { 97 | const token = client.generateCallToken({ user_id: userId, call_cids }); 98 | const decodedToken = jwt.verify(token, secret) as any; 99 | 100 | expect(decodedToken.user_id).toEqual(userId); 101 | expect(decodedToken.call_cids).toEqual(call_cids); 102 | expect(decodedToken.iat).toBeDefined(); 103 | expect(decodedToken.exp).toBeDefined(); 104 | }); 105 | 106 | it('with call IDs and role provided', () => { 107 | const token = client.generateCallToken({ 108 | user_id: userId, 109 | call_cids, 110 | role: 'admin', 111 | }); 112 | const decodedToken = jwt.verify(token, secret) as any; 113 | 114 | expect(decodedToken.call_cids).toEqual(call_cids); 115 | expect(decodedToken.role).toEqual('admin'); 116 | expect(decodedToken.user_id).toEqual(userId); 117 | expect(decodedToken.iat).toBeDefined(); 118 | expect(decodedToken.exp).toBeDefined(); 119 | }); 120 | 121 | it('with default expiration', () => { 122 | for (let i = 0; i < 5; i++) { 123 | const token = client.generateCallToken({ user_id: userId, call_cids }); 124 | const decodedToken = jwt.verify(token, secret) as any; 125 | const now = Date.now(); 126 | const oneHour = 60 * 60 * 1000; 127 | const oneSec = 1000; 128 | const exp = new Date(decodedToken.exp * 1000).getTime(); 129 | const tokenValidity = exp - now; 130 | 131 | expect(decodedToken.user_id).toBe(userId); 132 | expect(tokenValidity).toBeLessThanOrEqual(oneHour); 133 | expect(tokenValidity).toBeGreaterThanOrEqual(oneHour - 2 * oneSec); 134 | } 135 | }); 136 | 137 | it(`should make sure iat is correct`, () => { 138 | for (let i = 0; i < 5; i++) { 139 | const token = client.generateCallToken({ user_id: userId, call_cids }); 140 | const decodedToken = jwt.verify(token, secret) as any; 141 | const now = Date.now(); 142 | const nowMinus1sec = now - 1000; 143 | const nowMinus2secs = now - 2000; 144 | const iat = new Date(decodedToken.iat * 1000).getTime(); 145 | 146 | expect(iat).toBeLessThanOrEqual(nowMinus1sec); 147 | expect(iat).toBeGreaterThanOrEqual(nowMinus2secs); 148 | } 149 | }); 150 | 151 | it('with expiration provided', () => { 152 | const exp = Math.round(new Date().getTime() / 1000) + 58 * 60; 153 | const token = client.generateCallToken({ 154 | user_id: userId, 155 | call_cids, 156 | exp, 157 | }); 158 | const decodedToken = jwt.verify(token, secret) as any; 159 | 160 | expect(decodedToken.exp).toBe(exp); 161 | }); 162 | 163 | it('with iat provided', () => { 164 | const iat = Math.round(new Date().getTime() / 1000) + 60 * 60; 165 | const token = client.generateCallToken({ 166 | user_id: userId, 167 | call_cids, 168 | iat, 169 | }); 170 | const decodedToken = jwt.verify(token, secret) as any; 171 | 172 | expect(decodedToken.iat).toBe(iat); 173 | expect(decodedToken.exp).toBeDefined(); 174 | }); 175 | }); 176 | }); 177 | -------------------------------------------------------------------------------- /__tests__/date-transform.test.ts: -------------------------------------------------------------------------------- 1 | import { beforeAll, describe, expect, it } from 'vitest'; 2 | import { v4 as uuidv4 } from 'uuid'; 3 | import { createTestClient } from './create-test-client'; 4 | import { StreamClient } from '../src/StreamClient'; 5 | import { UserRequest } from '../src/gen/models'; 6 | 7 | describe('Date conversion', () => { 8 | let client: StreamClient; 9 | const user = { 10 | id: 'stream-node-test-user', 11 | role: 'admin', 12 | name: 'Test User for user API compatibily', 13 | custom: { 14 | note: 'compatibilty test', 15 | }, 16 | }; 17 | 18 | beforeAll(async () => { 19 | client = createTestClient(); 20 | await client.upsertUsers([user]); 21 | }); 22 | 23 | it('call + members', async () => { 24 | const id = uuidv4(); 25 | const call = client.video.call('default', id); 26 | const startsAt = new Date(); 27 | const response = await call.create({ 28 | data: { 29 | created_by: user, 30 | starts_at: startsAt, 31 | members: [{ user_id: 'jack' }], 32 | }, 33 | }); 34 | const now = Date.now(); 35 | const oneMin = 60 * 1000; 36 | 37 | const createdAt = response.call.created_at; 38 | 39 | expect(createdAt instanceof Date).toBe(true); 40 | expect(now - createdAt.getTime()).toBeLessThan(oneMin); 41 | 42 | expect(response.call.starts_at instanceof Date).toBe(true); 43 | expect(startsAt.getTime()).toEqual(response.call.starts_at?.getTime()); 44 | 45 | expect(response.call.ended_at).toBeNull(); 46 | 47 | expect(response.members.length).toBeGreaterThan(0); 48 | 49 | response.members.forEach((m) => { 50 | expect(m.created_at instanceof Date).toBe(true); 51 | }); 52 | 53 | const queryResult = await client.video.queryCalls({ 54 | filter_conditions: { 55 | starts_at: { $lte: startsAt.toISOString() }, 56 | }, 57 | }); 58 | 59 | expect(queryResult.calls.find((c) => c.call.id === id)).toBeDefined(); 60 | 61 | await call.delete(); 62 | }); 63 | 64 | it('channel + members', async () => { 65 | const id = uuidv4(); 66 | const channel = client.chat.channel('messaging', id); 67 | const response = await channel.getOrCreate({ 68 | data: { 69 | created_by_id: user.id, 70 | members: [{ user_id: 'jack' }], 71 | }, 72 | }); 73 | const now = Date.now(); 74 | const oneMin = 60 * 1000; 75 | 76 | const createdAt = response.channel!.created_at; 77 | 78 | expect(createdAt instanceof Date).toBe(true); 79 | expect(now - createdAt.getTime()).toBeLessThan(oneMin); 80 | 81 | expect(response.channel!.deleted_at).toBeUndefined(); 82 | 83 | expect(response.members.length).toBeGreaterThan(0); 84 | 85 | response.members.forEach((m) => { 86 | expect(m.created_at instanceof Date).toBe(true); 87 | }); 88 | 89 | const queryResult = await client.chat.queryChannels({ 90 | filter_conditions: { 91 | created_at: { $lte: createdAt.toISOString() }, 92 | }, 93 | limit: 10, 94 | }); 95 | 96 | queryResult.channels.forEach((c) => { 97 | expect(c.channel!.created_at.getTime()).toBeLessThanOrEqual( 98 | createdAt.getTime(), 99 | ); 100 | }); 101 | 102 | await channel.delete(); 103 | }); 104 | 105 | it('users', async () => { 106 | const newUser: UserRequest = { 107 | id: uuidv4(), 108 | custom: { 109 | created_at: new Date(), 110 | }, 111 | }; 112 | 113 | const response = await client.upsertUsers([newUser]); 114 | 115 | expect(response.users[newUser.id].created_at instanceof Date).toBe(true); 116 | 117 | expect( 118 | typeof response.users[newUser.id].custom.created_at === 'string', 119 | ).toBe(true); 120 | 121 | await client.deleteUsers({ user_ids: [newUser.id] }); 122 | }); 123 | }); 124 | -------------------------------------------------------------------------------- /__tests__/devices-push.test.ts: -------------------------------------------------------------------------------- 1 | import { beforeAll, describe, expect, it } from 'vitest'; 2 | import { v4 as uuidv4 } from 'uuid'; 3 | import { createTestClient } from './create-test-client'; 4 | import { StreamClient } from '../src/StreamClient'; 5 | import { CreateDeviceRequest, PushProvider } from '../src/gen/models'; 6 | 7 | describe('devices and push', () => { 8 | let client: StreamClient; 9 | const user = { 10 | id: 'stream-node-test-user', 11 | role: 'admin', 12 | }; 13 | const device: CreateDeviceRequest = { 14 | id: uuidv4(), 15 | push_provider: 'firebase', 16 | push_provider_name: 'firebase', 17 | user_id: user.id, 18 | }; 19 | const pushProvider: PushProvider = { 20 | name: 'test-push-provider', 21 | type: 'xiaomi', 22 | xiaomi_app_secret: '', 23 | xiaomi_package_name: '', 24 | }; 25 | 26 | beforeAll(async () => { 27 | client = createTestClient(); 28 | await client.upsertUsers([user]); 29 | }); 30 | 31 | it('create device', async () => { 32 | expect(async () => await client.createDevice(device)).not.toThrowError(); 33 | }); 34 | 35 | it('list devices', async () => { 36 | const response = await client.listDevices({ user_id: user.id }); 37 | 38 | expect(response.devices.find((d) => d.id === device.id)).toBeDefined(); 39 | }); 40 | 41 | it('delete device', async () => { 42 | const response = await client.deleteDevice({ 43 | id: device.id, 44 | user_id: user.id, 45 | }); 46 | 47 | expect(response).toBeDefined(); 48 | }); 49 | 50 | it('create push provider', async () => { 51 | // Can't properly test upsert without valid credentials 52 | await expect(() => 53 | client.upsertPushProvider({ push_provider: pushProvider }), 54 | ).rejects.toThrowError( 55 | 'Stream error code 4: UpsertPushProvider failed with error: "xiaomi credentials are invalid"', 56 | ); 57 | }); 58 | 59 | it('list push provider', async () => { 60 | const response = await client.listPushProviders(); 61 | 62 | expect(response.push_providers).toBeDefined(); 63 | }); 64 | 65 | it('test push provider', async () => { 66 | const response = await client.checkPush({ user_id: user.id }); 67 | 68 | expect(response).toBeDefined(); 69 | }); 70 | 71 | // TODO: can't test delete because we can't upsert 72 | // it('delete push provider', async () => { 73 | // const response = await client.deletePushProvider({name: pushProvider.push_provider!.name, type: DeletePushProviderTypeEnum.FIREBASE}); 74 | // }); 75 | }); 76 | -------------------------------------------------------------------------------- /__tests__/external-storage.test.ts: -------------------------------------------------------------------------------- 1 | import { beforeAll, describe, expect, it } from 'vitest'; 2 | import { v4 as uuidv4 } from 'uuid'; 3 | import { createTestClient } from './create-test-client'; 4 | import { StreamClient } from '../src/StreamClient'; 5 | 6 | describe('external storage CRUD API', () => { 7 | let client: StreamClient; 8 | const storageName = `streamnodetest${uuidv4()}`; 9 | 10 | beforeAll(() => { 11 | client = createTestClient(); 12 | }); 13 | 14 | it('create', async () => { 15 | const response = await client.createExternalStorage({ 16 | name: storageName, 17 | bucket: 'test', 18 | storage_type: 's3', 19 | }); 20 | 21 | expect(response).toBeDefined(); 22 | }); 23 | 24 | it('read', async () => { 25 | const readResponse = await client.listExternalStorage(); 26 | 27 | expect(readResponse.external_storages).toBeDefined(); 28 | expect(readResponse.external_storages[storageName]).toBeDefined(); 29 | }); 30 | 31 | it('update', async () => { 32 | const newBucket = 'new bucket'; 33 | const response = await client.updateExternalStorage({ 34 | name: storageName, 35 | bucket: newBucket, 36 | storage_type: 'abs', 37 | }); 38 | 39 | expect(response.bucket).toBe('new bucket'); 40 | }); 41 | 42 | it('delete', async () => { 43 | const response = await client.deleteExternalStorage({ name: storageName }); 44 | 45 | expect(response).toBeDefined(); 46 | }); 47 | 48 | it('docs snippets', async () => { 49 | const s3name = `streamnodetest-${uuidv4()}`; 50 | await client.createExternalStorage({ 51 | name: s3name, 52 | storage_type: 's3', 53 | bucket: 'my-bucket', 54 | path: 'directory_name/', 55 | aws_s3: { 56 | s3_api_key: 'us-east-1', 57 | s3_region: 'my-access-key', 58 | s3_secret: 'my-secret', 59 | }, 60 | }); 61 | 62 | await client.deleteExternalStorage({ name: s3name }); 63 | 64 | const gcsName = `streamnodetest-${uuidv4()}`; 65 | await client.createExternalStorage({ 66 | bucket: 'my-bucket', 67 | name: gcsName, 68 | storage_type: 'gcs', 69 | path: 'directory_name/', 70 | gcs_credentials: '{"type": "service_account"}', 71 | }); 72 | 73 | await client.deleteExternalStorage({ name: gcsName }); 74 | 75 | const azureName = `streamnodetest-${uuidv4()}`; 76 | await client.createExternalStorage({ 77 | name: azureName, 78 | storage_type: 'abs', 79 | bucket: 'my-bucket', 80 | path: 'directory_name/', 81 | azure_blob: { 82 | abs_account_name: '...', 83 | abs_client_id: '...', 84 | abs_client_secret: '...', 85 | abs_tenant_id: '...', 86 | }, 87 | }); 88 | 89 | await client.deleteExternalStorage({ name: azureName }); 90 | }); 91 | }); 92 | -------------------------------------------------------------------------------- /__tests__/messages.test.ts: -------------------------------------------------------------------------------- 1 | import 'dotenv/config'; 2 | import { afterAll, beforeAll, describe, expect, it } from 'vitest'; 3 | import { createTestClient } from './create-test-client'; 4 | import { StreamChannel } from '../src/StreamChannel'; 5 | import { StreamClient } from '../src/StreamClient'; 6 | import { v4 as uuidv4 } from 'uuid'; 7 | 8 | describe('messages API', () => { 9 | let client: StreamClient; 10 | const channelId = 'streamnodetest' + uuidv4(); 11 | let channel: StreamChannel; 12 | const user = { 13 | id: 'stream-node-test-user', 14 | name: 'Stream Node Test User', 15 | role: 'admin', 16 | }; 17 | const user2 = { 18 | id: 'stream-node-test-user2', 19 | name: 'Stream Node Test User 2', 20 | role: 'admin', 21 | }; 22 | let messageId: string | undefined; 23 | 24 | beforeAll(async () => { 25 | client = createTestClient(); 26 | 27 | await client.upsertUsers([user, user2]); 28 | 29 | channel = client.chat.channel('messaging', channelId); 30 | await channel.getOrCreate({ 31 | data: { 32 | // Type '{ id: string; }' is missing the following properties from type 'UserObject': banned, custom, online, role 33 | created_by: { id: user.id }, 34 | members: [{ user }, { user: user2 }], 35 | }, 36 | }); 37 | }); 38 | 39 | it('send message', async () => { 40 | const response = await channel.sendMessage({ 41 | message: { 42 | text: 'Hello from Stream Node SDK', 43 | attachments: [], 44 | user_id: user.id, 45 | }, 46 | }); 47 | 48 | expect(response.message?.text).toBe('Hello from Stream Node SDK'); 49 | 50 | messageId = response.message.id; 51 | 52 | const getResponse = await channel.getManyMessages({ 53 | ids: [messageId, messageId], 54 | }); 55 | 56 | expect(getResponse.messages.length).toBe(1); 57 | }); 58 | 59 | it('thread replies', async () => { 60 | const now = new Date(); 61 | const response = await channel.sendMessage({ 62 | message: { 63 | text: 'Hello from Stream Node SDK', 64 | attachments: [], 65 | user_id: user.id, 66 | }, 67 | }); 68 | 69 | const threadResponse = await channel.sendMessage({ 70 | message: { 71 | parent_id: response.message.id, 72 | text: 'Hello from a thread', 73 | attachments: [], 74 | user_id: user.id, 75 | }, 76 | }); 77 | 78 | const getResponse = await client.chat.getReplies({ 79 | parent_id: response.message.id, 80 | created_at_after: now, 81 | }); 82 | 83 | expect( 84 | getResponse.messages.find((m) => m.id === threadResponse.message.id), 85 | ).toBeDefined(); 86 | }); 87 | 88 | it('update message', async () => { 89 | const urlAttachment = await client.getOG({ url: 'https://getstream.io/' }); 90 | 91 | const response = await client.chat.updateMessage({ 92 | id: messageId!, 93 | message: { 94 | text: 'https://getstream.io/', 95 | attachments: [{ title_link: urlAttachment.title_link }], 96 | user_id: user.id, 97 | }, 98 | }); 99 | 100 | expect(response.message?.text).toBe('https://getstream.io/'); 101 | expect(response.message?.attachments[0].title_link).toBe( 102 | 'https://getstream.io/', 103 | ); 104 | }); 105 | 106 | it('update partial', async () => { 107 | const response = await client.chat.updateMessagePartial({ 108 | id: messageId!, 109 | set: { 110 | text: 'check this out: https://getstream.io/', 111 | }, 112 | unset: [], 113 | user_id: user.id, 114 | }); 115 | 116 | expect(response.message?.text).toBe( 117 | 'check this out: https://getstream.io/', 118 | ); 119 | expect(response.message?.attachments[0].title_link).toBe( 120 | 'https://getstream.io/', 121 | ); 122 | }); 123 | 124 | it('translate', async () => { 125 | const response = await client.chat.translateMessage({ 126 | id: messageId!, 127 | language: 'hu', 128 | }); 129 | 130 | // @ts-expect-error 131 | expect(response.message?.i18n?.hu_text).toBeDefined(); 132 | }); 133 | 134 | it('read and unread', async () => { 135 | const readResponse = await channel.markRead({ user_id: user2.id }); 136 | 137 | expect(readResponse.event?.channel_id).toBe(channel.id); 138 | 139 | const unreadResponse = await channel.markUnread({ 140 | user_id: user2.id, 141 | message_id: messageId!, 142 | }); 143 | 144 | expect(unreadResponse).toBeDefined(); 145 | }); 146 | 147 | it('send reaction', async () => { 148 | const response = await client.chat.sendReaction({ 149 | id: messageId!, 150 | reaction: { type: 'like', user_id: user.id }, 151 | }); 152 | 153 | expect(response.message?.id).toBe(messageId); 154 | expect(response.reaction?.message_id).toBe(messageId); 155 | expect(response.reaction?.type).toBe('like'); 156 | }); 157 | 158 | it('get reactions', async () => { 159 | const response = await client.chat.getReactions({ id: messageId! }); 160 | 161 | expect(response.reactions.length).toBe(1); 162 | }); 163 | 164 | it('delete reaction', async () => { 165 | const response = await client.chat.deleteReaction({ 166 | id: messageId!, 167 | type: 'like', 168 | user_id: user.id, 169 | }); 170 | 171 | expect(response.message?.id).toBe(messageId); 172 | expect(response.reaction?.type).toBe('like'); 173 | }); 174 | 175 | it('search', async () => { 176 | const response = await client.chat.search({ 177 | payload: { 178 | filter_conditions: { members: { $in: [user2.id] } }, 179 | message_filter_conditions: { text: { $autocomplete: 'check' } }, 180 | }, 181 | }); 182 | 183 | expect(response.results).toBeDefined(); 184 | }); 185 | 186 | it('flag', async () => { 187 | const response = await client.moderation.flag({ 188 | entity_type: 'stream:chat:v1:message', 189 | entity_id: messageId!, 190 | user_id: user.id, 191 | reason: 'hate', 192 | }); 193 | 194 | expect(response.item_id).toBeDefined(); 195 | }); 196 | 197 | it('truncate', async () => { 198 | await channel.truncate({ user_id: user.id }); 199 | 200 | const response = await channel.getOrCreate(); 201 | 202 | expect(response.messages.length).toBe(0); 203 | }); 204 | 205 | it('delete message', async () => { 206 | const response = await client.chat.deleteMessage({ 207 | id: messageId!, 208 | hard: true, 209 | }); 210 | 211 | expect(response.message?.id).toBe(messageId); 212 | 213 | await expect(() => 214 | client.chat.getMessage({ id: messageId! }), 215 | ).rejects.toThrowError( 216 | `Stream error code 4: GetMessage failed with error: "Message with id ${messageId} doesn't exist"`, 217 | ); 218 | }); 219 | 220 | afterAll(async () => { 221 | await channel.delete({ hard_delete: true }); 222 | }); 223 | }); 224 | -------------------------------------------------------------------------------- /__tests__/permissions-app-settings.test.ts: -------------------------------------------------------------------------------- 1 | import { beforeAll, describe, expect, it } from 'vitest'; 2 | import { v4 as uuidv4 } from 'uuid'; 3 | import { createTestClient } from './create-test-client'; 4 | import { StreamClient } from '../src/StreamClient'; 5 | import { CreateRoleRequest, OwnCapability } from '../src/gen/models'; 6 | 7 | describe('permissions and app settings API', () => { 8 | let client: StreamClient; 9 | let role: CreateRoleRequest; 10 | 11 | beforeAll(() => { 12 | role = { 13 | name: 'streamnodetest' + uuidv4(), 14 | }; 15 | client = createTestClient(); 16 | }); 17 | 18 | it('list permissions', async () => { 19 | const response = await client.listPermissions(); 20 | 21 | expect(response.permissions).toBeDefined(); 22 | 23 | const permission = response.permissions[0]; 24 | 25 | const getResponse = await client.getPermission({ id: permission.id }); 26 | 27 | expect(getResponse.permission.id).toBe(permission.id); 28 | }); 29 | 30 | it('create role', async () => { 31 | const response = await client.createRole(role); 32 | 33 | expect(response.role.name).toBe(role.name); 34 | }); 35 | 36 | it('list roles', async () => { 37 | const response = await client.listRoles(); 38 | 39 | expect(response.roles.find((r) => r.name === role.name)).toBeDefined(); 40 | }); 41 | 42 | it('update role', async () => { 43 | const response = await client.updateApp({ 44 | grants: { 45 | [role.name]: [OwnCapability.CREATE_CALL], 46 | }, 47 | }); 48 | 49 | expect(response).toBeDefined(); 50 | 51 | const appSettings = await client.getApp(); 52 | 53 | expect( 54 | appSettings.app.grants[role.name].includes(OwnCapability.CREATE_CALL), 55 | ).toBe(true); 56 | }); 57 | 58 | it('delete role', async () => { 59 | await client.updateApp({ 60 | grants: { 61 | [role.name]: [], 62 | }, 63 | }); 64 | 65 | let response; 66 | 67 | try { 68 | response = await client.deleteRole({ name: role.name }); 69 | } catch (e) { 70 | // the first request fails on backend sometimes 71 | // retry it 72 | await new Promise((resolve) => { 73 | setTimeout(() => resolve(), 3000); 74 | }); 75 | 76 | response = await client.deleteRole({ name: role.name }); 77 | } 78 | 79 | expect(response).toBeDefined(); 80 | }); 81 | 82 | it('get rate limits', async () => { 83 | const response = await client.getRateLimits(); 84 | 85 | expect(response.web).toBeDefined(); 86 | }); 87 | }); 88 | -------------------------------------------------------------------------------- /__tests__/rate-limit.test.ts: -------------------------------------------------------------------------------- 1 | import { beforeAll, describe, expect, it } from 'vitest'; 2 | import { createTestClient } from './create-test-client'; 3 | import { StreamClient } from '../src/StreamClient'; 4 | 5 | describe('rate limit', () => { 6 | let client: StreamClient; 7 | 8 | beforeAll(async () => { 9 | client = createTestClient(); 10 | }); 11 | 12 | it('should inspect rate limit', async () => { 13 | const response = await client.getRateLimits({ 14 | server_side: true, 15 | endpoints: 'QueryChannels,GetOrCreateChannel', 16 | }); 17 | 18 | expect(response.server_side).toBeDefined(); 19 | expect(response.server_side!.QueryChannels).toBeDefined(); 20 | expect(response.server_side!.GetOrCreateChannel).toBeDefined(); 21 | }); 22 | 23 | it('should get rate limit from response header', async () => { 24 | let response = await client.getApp(); 25 | const rateLimit = response.metadata.rateLimit; 26 | const now = Date.now(); 27 | 28 | expect(typeof rateLimit.rateLimit).toBe('number'); 29 | expect(typeof rateLimit.rateLimitRemaining).toBe('number'); 30 | expect(rateLimit.rateLimitReset instanceof Date).toBe(true); 31 | 32 | expect(rateLimit.rateLimit).toBe( 33 | +response.metadata.responseHeaders.get('x-ratelimit-limit')!, 34 | ); 35 | expect(rateLimit.rateLimitRemaining).toBe( 36 | +response.metadata.responseHeaders.get('x-ratelimit-remaining')!, 37 | ); 38 | expect(rateLimit.rateLimitReset!.getTime()).toBe( 39 | new Date( 40 | +response.metadata.responseHeaders.get('x-ratelimit-reset')! * 1000, 41 | ).getTime(), 42 | ); 43 | 44 | // Some basic tests for date parsing 45 | expect(rateLimit.rateLimitReset!.getTime()).toBeGreaterThanOrEqual(now); 46 | expect(rateLimit.rateLimitReset!.getTime()).toBeLessThan( 47 | now + 1000 * 60 * 60 * 24, 48 | ); 49 | 50 | response = await client.getApp(); 51 | const rateLimit2 = response.metadata.rateLimit; 52 | 53 | // It might fail in some rare cases, but since we retry every test 3x, this shouldn't be a problem 54 | expect(rateLimit2.rateLimitRemaining).toBeLessThan( 55 | rateLimit.rateLimitRemaining!, 56 | ); 57 | }); 58 | 59 | it('should include rate limit in errors as well', async () => { 60 | const call = client.video.call('default', 'test'); 61 | try { 62 | await call.startTranscription(); 63 | throw new Error(`Method didn't throw`); 64 | } catch (error) { 65 | expect(error.code).toBeDefined(); 66 | expect(error.metadata).toBeDefined(); 67 | expect(error.metadata.responseCode).toBe(404); 68 | 69 | const rateLimit = error.metadata.rateLimit; 70 | 71 | expect(rateLimit.rateLimit).toBeDefined(); 72 | } 73 | }); 74 | }); 75 | -------------------------------------------------------------------------------- /__tests__/teams.test.ts: -------------------------------------------------------------------------------- 1 | import { beforeAll, describe, expect, it } from 'vitest'; 2 | import { v4 as uuidv4 } from 'uuid'; 3 | import { createTestClient } from './create-test-client'; 4 | import { StreamClient } from '../src/StreamClient'; 5 | import { StreamCall } from '../src/StreamCall'; 6 | import { UserRequest } from '../src/gen/models'; 7 | 8 | describe('teams', () => { 9 | let client: StreamClient; 10 | const userId = 'streamnodetest' + uuidv4(); 11 | const newUser: UserRequest = { 12 | id: userId, 13 | role: 'user', 14 | custom: { 15 | color: 'red', 16 | }, 17 | name: userId, 18 | image: ':)', 19 | teams: ['red', 'blue'], 20 | }; 21 | const user = { 22 | id: 'stream-node-test-user', 23 | role: 'admin', 24 | }; 25 | const callId = `call${uuidv4()}`; 26 | let call: StreamCall; 27 | 28 | beforeAll(async () => { 29 | client = createTestClient(); 30 | await client.upsertUsers([user]); 31 | await client.updateApp({ multi_tenant_enabled: true }); 32 | call = client.video.call('default', callId); 33 | }); 34 | 35 | it('create user with team', async () => { 36 | const response = await client.upsertUsers([newUser]); 37 | 38 | const createdUser = response.users[newUser.id]; 39 | 40 | expect(createdUser.teams).toEqual(['red', 'blue']); 41 | 42 | const queryResponse = await client.queryUsers({ 43 | payload: { 44 | sort: [], 45 | filter_conditions: { 46 | id: { $eq: newUser.id }, 47 | }, 48 | }, 49 | }); 50 | 51 | expect(queryResponse.users.find((u) => u.id === newUser.id)?.teams).toEqual( 52 | ['red', 'blue'], 53 | ); 54 | }); 55 | 56 | it('search users with teams', async () => { 57 | let response = await client.queryUsers({ 58 | payload: { 59 | filter_conditions: { 60 | name: userId, 61 | teams: { $in: ['blue', 'green'] }, 62 | }, 63 | }, 64 | }); 65 | 66 | expect(response.users[0].id).toBe(userId); 67 | 68 | response = await client.queryUsers({ 69 | payload: { 70 | filter_conditions: { 71 | name: userId, 72 | teams: null, 73 | }, 74 | }, 75 | }); 76 | 77 | expect(response.users.length).toBe(0); 78 | }); 79 | 80 | it('create call with teams', async () => { 81 | const response = await call.create({ 82 | data: { team: 'red', created_by_id: user.id }, 83 | }); 84 | 85 | expect(response.call.team).toBe('red'); 86 | }); 87 | 88 | it('query calls', async () => { 89 | let response = await client.video.queryCalls(); 90 | 91 | expect(response.calls.length).toBeGreaterThan(0); 92 | 93 | response = await client.video.queryCalls({ 94 | filter_conditions: { 95 | team: null, 96 | }, 97 | }); 98 | 99 | response = await client.video.queryCalls({ 100 | filter_conditions: { 101 | team: 'red', 102 | }, 103 | }); 104 | 105 | expect(response.calls.find((c) => c.call.id === callId)).toBeDefined(); 106 | }); 107 | }); 108 | -------------------------------------------------------------------------------- /__tests__/user-compat.test.ts: -------------------------------------------------------------------------------- 1 | import { beforeAll, describe, expect, it } from 'vitest'; 2 | import { v4 as uuidv4 } from 'uuid'; 3 | import { createTestClient } from './create-test-client'; 4 | import { StreamClient } from '../src/StreamClient'; 5 | 6 | describe('user-video compatibility API', () => { 7 | let client: StreamClient; 8 | const user = { 9 | id: uuidv4(), 10 | role: 'admin', 11 | name: 'Test User for user API compatibily', 12 | custom: { 13 | note: 'compatibilty test', 14 | }, 15 | }; 16 | 17 | beforeAll(async () => { 18 | client = createTestClient(); 19 | await client.upsertUsers([user]); 20 | }); 21 | 22 | it('create call', async () => { 23 | const call = client.video.call('default', uuidv4()); 24 | const response = await call.create({ data: { created_by: user } }); 25 | 26 | // Backend returns: {custom: { custom: { note: 'compatibilty test' } }} 27 | expect(response.call.created_by.custom.note).toBe('compatibilty test'); 28 | expect(response.call.created_by.name).toBe( 29 | 'Test User for user API compatibily', 30 | ); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /__tests__/users.test.ts: -------------------------------------------------------------------------------- 1 | import { beforeAll, describe, expect, it } from 'vitest'; 2 | import { v4 as uuidv4 } from 'uuid'; 3 | import { createTestClient } from './create-test-client'; 4 | import { StreamClient } from '../src/StreamClient'; 5 | import { UserRequest } from '../src/gen/models'; 6 | 7 | describe('user API', () => { 8 | let client: StreamClient; 9 | const userId = 'streamnodetest' + uuidv4(); 10 | const newUser: UserRequest = { 11 | id: userId, 12 | role: 'user', 13 | custom: { 14 | color: 'red', 15 | }, 16 | name: userId, 17 | image: ':)', 18 | }; 19 | const user = { 20 | id: 'stream-node-test-user', 21 | role: 'admin', 22 | }; 23 | 24 | beforeAll(async () => { 25 | client = createTestClient(); 26 | await client.upsertUsers([user]); 27 | }); 28 | 29 | it('query users', async () => { 30 | let response = await client.queryUsers({ 31 | payload: { 32 | sort: [{ field: 'name', direction: 1 }], 33 | filter_conditions: {}, 34 | }, 35 | }); 36 | 37 | expect(response.users).toBeDefined(); 38 | 39 | response = await client.queryUsers({ 40 | payload: { 41 | sort: [{ field: 'name', direction: 1 }], 42 | filter_conditions: { 43 | id: { $eq: 'zitaszuperagetstreamio' }, 44 | }, 45 | }, 46 | }); 47 | 48 | expect(response.users.length).toBe(1); 49 | }); 50 | 51 | it('create', async () => { 52 | const response = await client.upsertUsers([newUser]); 53 | 54 | const createdUser = response.users[newUser.id]; 55 | 56 | expect(createdUser.id).toBe(newUser.id); 57 | expect(createdUser.role).toBe(newUser.role); 58 | expect(createdUser.name).toBe(newUser.name); 59 | expect(createdUser.image).toBe(newUser.image); 60 | expect(createdUser.custom.color).toBe('red'); 61 | 62 | const queryResponse = await client.queryUsers({ 63 | payload: { 64 | sort: [], 65 | filter_conditions: { 66 | id: { $eq: newUser.id }, 67 | }, 68 | }, 69 | }); 70 | 71 | expect(queryResponse.users.length).toBe(1); 72 | expect(queryResponse.users[0].custom.color).toBe('red'); 73 | expect(queryResponse.users[0].id).toBe(newUser.id); 74 | expect(queryResponse.users[0].name).toBe(newUser.name); 75 | expect(queryResponse.users[0].image).toBe(newUser.image); 76 | }); 77 | 78 | it('create guest', async () => { 79 | await client.updateApp({ multi_tenant_enabled: false }); 80 | 81 | const guest: UserRequest = { 82 | id: uuidv4(), 83 | name: 'Guest 3', 84 | image: ': )', 85 | custom: { 86 | color: 'red', 87 | }, 88 | }; 89 | 90 | const response = await client.createGuest({ user: guest }); 91 | 92 | expect(response.user?.role).toBe('guest'); 93 | expect(response.user?.custom.color).toBe('red'); 94 | expect(response.user?.name).toBe('Guest 3'); 95 | expect(response.user?.image).toBe(': )'); 96 | 97 | await client.updateApp({ multi_tenant_enabled: true }); 98 | }); 99 | 100 | it('ban and unban', async () => { 101 | await client.moderation.ban({ 102 | target_user_id: newUser.id, 103 | banned_by_id: user.id, 104 | }); 105 | 106 | let queryResponse = await client.queryBannedUsers({ 107 | payload: { filter_conditions: {} }, 108 | }); 109 | 110 | expect( 111 | queryResponse.bans.find((b) => b.user?.id === newUser.id), 112 | ).toBeDefined(); 113 | 114 | await client.moderation.unban({ 115 | target_user_id: newUser.id, 116 | created_by: user.id, 117 | }); 118 | 119 | queryResponse = await client.queryBannedUsers({ 120 | payload: { filter_conditions: {} }, 121 | }); 122 | 123 | expect( 124 | queryResponse.bans.find((b) => b.user?.id === newUser.id), 125 | ).toBeUndefined(); 126 | }); 127 | 128 | it('mute and unmute', async () => { 129 | const muteResponse = await client.moderation.mute({ 130 | target_ids: [newUser.id], 131 | user_id: user.id, 132 | timeout: 5, 133 | }); 134 | 135 | expect(muteResponse.mutes?.[0]?.target?.id).toBe(newUser.id); 136 | 137 | const unmuteResponse = await client.moderation.unmute({ 138 | target_ids: [newUser.id], 139 | user_id: user.id, 140 | }); 141 | 142 | expect(unmuteResponse).toBeDefined(); 143 | }); 144 | 145 | it('block and unblock', async () => { 146 | const badUser: UserRequest = { 147 | id: 'bad-alice', 148 | name: 'Alice', 149 | }; 150 | await client.upsertUsers([badUser]); 151 | 152 | const blockResponse = await client.blockUsers({ 153 | blocked_user_id: badUser.id, 154 | user_id: user.id, 155 | }); 156 | 157 | expect(blockResponse.blocked_user_id).toBe(badUser.id); 158 | 159 | const blockedUsers = await client.getBlockedUsers({ user_id: user.id }); 160 | 161 | expect( 162 | blockedUsers.blocks.find((b) => b.blocked_user_id === badUser.id), 163 | ).toBeDefined(); 164 | 165 | const unblockResponse = await client.unblockUsers({ 166 | blocked_user_id: badUser.id, 167 | user_id: user.id, 168 | }); 169 | 170 | expect(unblockResponse.duration).toBeDefined(); 171 | }); 172 | 173 | it('send custom event', async () => { 174 | const response = await client.chat.sendUserCustomEvent({ 175 | user_id: newUser.id, 176 | event: { type: 'my-custom-event' }, 177 | }); 178 | 179 | expect(response).toBeDefined(); 180 | }); 181 | 182 | it('update', async () => { 183 | const response = await client.updateUsersPartial({ 184 | users: [ 185 | { 186 | id: newUser.id, 187 | set: { 188 | role: 'admin', 189 | custom: { 190 | color: 'blue', 191 | }, 192 | }, 193 | unset: ['name'], 194 | }, 195 | ], 196 | }); 197 | 198 | const userResponse = response.users[newUser.id]; 199 | 200 | expect(userResponse.name).toBeFalsy(); 201 | expect(userResponse.role).toBe('admin'); 202 | // TODO: backend issue 203 | // expect(userResponse.custom.color).toBe('blue'); 204 | }); 205 | 206 | it('deactivate and reactivate', async () => { 207 | const deactivateResponse = await client.deactivateUser({ 208 | user_id: newUser.id, 209 | }); 210 | 211 | expect(deactivateResponse.user?.id).toBe(newUser.id); 212 | 213 | const reactivateResponse = await client.reactivateUsers({ 214 | user_ids: [newUser.id], 215 | }); 216 | 217 | expect(reactivateResponse.task_id).toBeDefined(); 218 | 219 | const response = await client.getTask({ id: reactivateResponse.task_id }); 220 | 221 | expect(response.status).toBeDefined(); 222 | }); 223 | 224 | it('restore', async () => { 225 | await expect(() => 226 | client.restoreUsers({ user_ids: [newUser.id] }), 227 | ).rejects.toThrowError( 228 | `Stream error code 4: RestoreUsers failed with error: "user "${newUser.id}" is not deleted, it can't be restored"`, 229 | ); 230 | }); 231 | 232 | it('delete', async () => { 233 | const response = await client.deleteUsers({ user_ids: [newUser.id] }); 234 | 235 | expect(response).toBeDefined(); 236 | }); 237 | }); 238 | -------------------------------------------------------------------------------- /__tests__/webhook.test.ts: -------------------------------------------------------------------------------- 1 | import { beforeAll, describe, expect, it } from 'vitest'; 2 | import { StreamClient } from '../src/StreamClient'; 3 | import { createTestClient } from './create-test-client'; 4 | 5 | describe('webhooks', () => { 6 | let client: StreamClient; 7 | 8 | beforeAll(async () => { 9 | client = createTestClient(); 10 | }); 11 | 12 | it('verify webhook - call.session_participant_joined', async () => { 13 | const body = `{"type":"call.session_participant_joined","created_at":"2023-11-14T14:49:14.142187951Z","call_cid":"default:esJPeexho8md","session_id":"25a5b332-36f4-42c5-9fcb-a654bcaa2f3f","participant":{"user":{"id":"zita_szupera","name":"Zita Szupera","image":"https://lh3.googleusercontent.com/a/AAcHTtd__ATa58lPX-VAwJ46QU5arELhguPwTxoh9yzTaM_adw=s96-c","custom":{"imageUrl":"https://lh3.googleusercontent.com/a/AAcHTtd__ATa58lPX-VAwJ46QU5arELhguPwTxoh9yzTaM_adw=s120"},"role":"user","teams":[],"created_at":"2023-05-22T12:44:57.422509Z","updated_at":"2023-11-14T14:49:12.471772Z"},"user_session_id":"6245918f-461b-4d4f-b3f8-ed083e3cf867","role":"user","joined_at":"2023-11-14T14:49:14.142179098Z"}}`; 14 | const validSignature = 15 | '72b2a8f814b840e457882fd9968ac7f4210b23273461ab95365e9bf49fca12d8'; 16 | const invalidSignature = 17 | '5d7fd77f8c3f92fc017a09775527716d22357f73d9d362435a7ed2a72d8c1a66'; 18 | 19 | let isValid = client.verifyWebhook(body, validSignature); 20 | 21 | expect(isValid).toBe(true); 22 | 23 | isValid = client.verifyWebhook(body, invalidSignature); 24 | 25 | expect(isValid).toBe(false); 26 | }); 27 | 28 | it('verify webhook - call.session_participant_left', async () => { 29 | const body = `{"type":"call.session_participant_left","created_at":"2023-11-14T14:49:17.231445173Z","call_cid":"default:esJPeexho8md","session_id":"25a5b332-36f4-42c5-9fcb-a654bcaa2f3f","participant":{"user":{"id":"zita_szupera","name":"Zita Szupera","image":"https://lh3.googleusercontent.com/a/AAcHTtd__ATa58lPX-VAwJ46QU5arELhguPwTxoh9yzTaM_adw=s96-c","custom":{"imageUrl":"https://lh3.googleusercontent.com/a/AAcHTtd__ATa58lPX-VAwJ46QU5arELhguPwTxoh9yzTaM_adw=s120"},"role":"user","teams":[],"created_at":"2023-05-22T12:44:57.422509Z","updated_at":"2023-11-14T14:49:12.471772Z"},"user_session_id":"6245918f-461b-4d4f-b3f8-ed083e3cf867","role":"user","joined_at":"2023-11-14T14:49:17.231436648Z"}}`; 30 | const validSignature = 31 | '676dfafc03d08eeb9928af04169fd76a4a2efe8267f9f375bdd340c562fed37a'; 32 | const invalidSignature = 33 | '5d7fd77f8c3f92fc017a09775527716d22357f73d9d362435a7ed2a72d8c1a66'; 34 | 35 | let isValid = client.verifyWebhook(body, validSignature); 36 | 37 | expect(isValid).toBe(true); 38 | 39 | isValid = client.verifyWebhook(body, invalidSignature); 40 | 41 | expect(isValid).toBe(false); 42 | }); 43 | 44 | it('verify webhook - user.updated', async () => { 45 | const body = `{"type":"user.updated","user":{"id":"federico_guerinoni","role":"user","created_at":"2023-05-26T07:53:03.611031Z","updated_at":"2023-11-13T09:50:24.444759Z","last_active":"2023-11-10T15:27:36.208436Z","banned":false,"online":true,"name":"Federico Guerinoni","image":"https://lh3.googleusercontent.com/a/AGNmyxbj_VkTg2cbpxA0oODYVSvU4xLQihvT5ZBM7pdw=s96-c"},"created_at":"2023-11-13T09:50:24.447224815Z","members":[]}`; 46 | const validSignature = 47 | '35be5bf8e58170a042da724bba7d6b933d3f29ec85e6696ef1cf001e7c097fb8'; 48 | const invalidSignature = 49 | '5d7fd77f8c3f92fc017a09775527716d22357f73d9d362435a7ed2a72d8c1a66'; 50 | 51 | let isValid = client.verifyWebhook(body, validSignature); 52 | 53 | expect(isValid).toBe(true); 54 | 55 | isValid = client.verifyWebhook(body, invalidSignature); 56 | 57 | expect(isValid).toBe(false); 58 | }); 59 | 60 | it('verify webhook - channel.created', async () => { 61 | const body = `{"type":"channel.created","cid":"videocall:esJPeexho8md","channel_id":"esJPeexho8md","channel_type":"videocall","channel":{"id":"esJPeexho8md","type":"videocall","cid":"videocall:esJPeexho8md","created_at":"2023-11-14T14:49:12.81875Z","updated_at":"2023-11-14T14:49:12.81875Z","created_by":{"id":"zita_szupera","role":"user","created_at":"2023-05-22T12:44:57.422509Z","updated_at":"2023-11-14T14:49:12.471772Z","last_active":"2023-11-13T09:49:44.789879Z","banned":false,"online":false,"name":"Zita Szupera","image":"https://lh3.googleusercontent.com/a/AAcHTtd__ATa58lPX-VAwJ46QU5arELhguPwTxoh9yzTaM_adw=s96-c","imageUrl":"https://lh3.googleusercontent.com/a/AAcHTtd__ATa58lPX-VAwJ46QU5arELhguPwTxoh9yzTaM_adw=s120"},"frozen":false,"disabled":false,"config":{"created_at":"2023-02-16T15:06:02.355424Z","updated_at":"2023-04-03T16:20:57.360607Z","name":"videocall","typing_events":true,"read_events":true,"connect_events":true,"search":true,"reactions":true,"replies":false,"quotes":true,"mutes":true,"uploads":true,"url_enrichment":true,"custom_events":true,"push_notifications":true,"reminders":false,"mark_messages_pending":false,"message_retention":"infinite","max_message_length":5000,"automod":"disabled","automod_behavior":"flag","blocklist_behavior":"flag","commands":[{"name":"giphy","description":"Post a random gif to the channel","args":"[text]","set":"fun_set"},{"name":"ban","description":"Ban a user","args":"[@username] [text]","set":"moderation_set"},{"name":"unban","description":"Unban a user","args":"[@username]","set":"moderation_set"},{"name":"mute","description":"Mute a user","args":"[@username]","set":"moderation_set"},{"name":"unmute","description":"Unmute a user","args":"[@username]","set":"moderation_set"}]}},"user":{"id":"zita_szupera","role":"user","created_at":"2023-05-22T12:44:57.422509Z","updated_at":"2023-11-14T14:49:12.471772Z","last_active":"2023-11-13T09:49:44.789879Z","banned":false,"online":false,"name":"Zita Szupera","image":"https://lh3.googleusercontent.com/a/AAcHTtd__ATa58lPX-VAwJ46QU5arELhguPwTxoh9yzTaM_adw=s96-c","imageUrl":"https://lh3.googleusercontent.com/a/AAcHTtd__ATa58lPX-VAwJ46QU5arELhguPwTxoh9yzTaM_adw=s120"},"created_at":"2023-11-14T14:49:12.828267423Z","request_info":{"type":"client","ip":"89.134.25.76","user_agent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36","sdk":"stream-chat-javascript-client-browser-8.13.0"}}`; 62 | const validSignature = 63 | '6de7226ece892e191326906fea40473ae7f65aaa7426a13dddab517ab9338e41'; 64 | const invalidSignature = 65 | '5d7fd77f8c3f92fc017a09775527716d22357f73d9d362435a7ed2a72d8c1a66'; 66 | 67 | let isValid = client.verifyWebhook(body, validSignature); 68 | 69 | expect(isValid).toBe(true); 70 | 71 | isValid = client.verifyWebhook(body, invalidSignature); 72 | 73 | expect(isValid).toBe(false); 74 | }); 75 | }); 76 | -------------------------------------------------------------------------------- /generate-openapi.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail 3 | 4 | OUTPUT_DIR="../stream-node/src/gen" 5 | CHAT_DIR="../chat" 6 | 7 | rm -rf $OUTPUT_DIR 8 | 9 | ( cd $CHAT_DIR ; make openapi ; go run ./cmd/chat-manager openapi generate-client --language ts --spec ./releases/v2/serverside-api.yaml --output $OUTPUT_DIR ) 10 | 11 | yarn lint:gen -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- 1 | export * from './src/gen/models'; 2 | export * from './src/StreamClient'; 3 | export * from './src/StreamCall'; 4 | export * from './src/StreamChatClient'; 5 | export * from './src/StreamChannel'; 6 | export * from './src/StreamVideoClient'; 7 | -------------------------------------------------------------------------------- /openapitools.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@openapitools/openapi-generator-cli/config.schema.json", 3 | "spaces": 2, 4 | "generator-cli": { 5 | "version": "6.6.0" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@stream-io/node-sdk", 3 | "version": "0.4.24", 4 | "description": "", 5 | "exports": { 6 | ".": { 7 | "import": "./dist/index.es.mjs", 8 | "require": "./dist/index.cjs.js", 9 | "types": "./dist/index.d.ts" 10 | } 11 | }, 12 | "main": "dist/index.cjs.js", 13 | "module": "dist/index.es.mjs", 14 | "types": "dist/index.d.ts", 15 | "author": "https://getstream.io", 16 | "license": "See license in LICENSE", 17 | "repository": { 18 | "type": "git", 19 | "url": "https://github.com/GetStream/stream-node.git" 20 | }, 21 | "scripts": { 22 | "prepare": "husky install", 23 | "prepublish": "yarn build", 24 | "test": "vitest", 25 | "test:bun": "bun run vitest", 26 | "start": "rollup -w -c", 27 | "build": "rm -rf dist && rollup -c", 28 | "generate:open-api": "./generate-openapi.sh", 29 | "lint": "eslint **/*.ts", 30 | "lint:fix": "eslint --fix **/*.ts", 31 | "prettier:fix": "prettier . --write", 32 | "lint:gen": "eslint --fix src/gen/**/*ts && prettier src/gen/**/*.ts --write" 33 | }, 34 | "lint-staged": { 35 | "**/*": [ 36 | "prettier --write --ignore-unknown" 37 | ] 38 | }, 39 | "files": [ 40 | "dist", 41 | "src", 42 | "index.ts", 43 | "package.json", 44 | "README.md", 45 | "LICENSE", 46 | ".readme-assets" 47 | ], 48 | "devDependencies": { 49 | "@openapitools/openapi-generator-cli": "^2.7.0", 50 | "@rollup/plugin-replace": "^5.0.2", 51 | "@rollup/plugin-typescript": "^11.1.4", 52 | "@stream-io/openai-realtime-api": "~0.1.3", 53 | "@types/uuid": "^9.0.4", 54 | "@typescript-eslint/eslint-plugin": "^6.4.0", 55 | "dotenv": "^16.3.1", 56 | "eslint": "^8.0.1", 57 | "eslint-config-prettier": "^9.0.0", 58 | "eslint-config-standard-with-typescript": "^40.0.0", 59 | "eslint-plugin-import": "^2.25.2", 60 | "eslint-plugin-n": "^15.0.0 || ^16.0.0 ", 61 | "eslint-plugin-promise": "^6.0.0", 62 | "eslint-plugin-unused-imports": "^4.0.1", 63 | "husky": "^8.0.3", 64 | "lint-staged": "^15.1.0", 65 | "prettier": "^3.1.0", 66 | "rollup": "^3.29.3", 67 | "typescript": "^5.2.2", 68 | "vite": "^4.4.9", 69 | "vitest": "^1.0.0-beta.5", 70 | "vitest-mock-extended": "^1.2.1" 71 | }, 72 | "dependencies": { 73 | "@types/jsonwebtoken": "^9.0.3", 74 | "@types/node": "^18.3.0", 75 | "jsonwebtoken": "^9.0.2", 76 | "uuid": "^9.0.1" 77 | }, 78 | "peerDependencies": { 79 | "@stream-io/openai-realtime-api": "~0.1.3" 80 | }, 81 | "peerDependenciesMeta": { 82 | "@stream-io/openai-realtime-api": { 83 | "optional": true 84 | } 85 | }, 86 | "engines": { 87 | "node": ">=18.0.0" 88 | }, 89 | "packageManager": "yarn@1.22.22+sha1.ac34549e6aa8e7ead463a7407e1c7390f61a6610" 90 | } 91 | -------------------------------------------------------------------------------- /rollup.config.mjs: -------------------------------------------------------------------------------- 1 | import typescript from "@rollup/plugin-typescript"; 2 | import replace from "@rollup/plugin-replace"; 3 | 4 | import { createRequire } from "module"; 5 | const require = createRequire(import.meta.url); 6 | const pkg = require("./package.json"); 7 | 8 | const nodeConfig = { 9 | input: "index.ts", 10 | output: [ 11 | { 12 | file: "dist/index.cjs.js", 13 | format: "cjs", 14 | dynamicImportInCjs: false, 15 | sourcemap: true, 16 | }, 17 | { 18 | file: "dist/index.es.mjs", 19 | format: "es", 20 | sourcemap: true, 21 | }, 22 | ], 23 | plugins: [ 24 | replace({ 25 | preventAssignment: true, 26 | "process.env.PKG_VERSION": JSON.stringify(pkg.version), 27 | }), 28 | typescript(), 29 | ], 30 | }; 31 | 32 | export default nodeConfig; 33 | -------------------------------------------------------------------------------- /src/BaseApi.ts: -------------------------------------------------------------------------------- 1 | import { v4 as uuidv4 } from 'uuid'; 2 | import { ApiConfig, RequestMetadata, StreamError } from './types'; 3 | import { APIError } from './gen/models'; 4 | import { getRateLimitFromResponseHeader } from './utils/rate-limit'; 5 | 6 | export class BaseApi { 7 | private readonly dispatcher?: RequestInit['dispatcher']; 8 | 9 | constructor(protected readonly apiConfig: ApiConfig) { 10 | this.dispatcher = this.apiConfig.agent; 11 | } 12 | 13 | protected sendRequest = async ( 14 | method: string, 15 | url: string, 16 | pathParams?: Record, 17 | queryParams?: Record, 18 | body?: any, 19 | ) => { 20 | queryParams = queryParams ?? {}; 21 | queryParams.api_key = this.apiConfig.apiKey; 22 | const encodedParams = this.queryParamsStringify(queryParams); 23 | if (pathParams) { 24 | Object.keys(pathParams).forEach((paramName) => { 25 | url = url.replace(`{${paramName}}`, pathParams[paramName]); 26 | }); 27 | } 28 | 29 | url += `?${encodedParams}`; 30 | const clientRequestId = uuidv4(); 31 | const headers = { 32 | Authorization: this.apiConfig.token, 33 | 'stream-auth-type': 'jwt', 34 | 'Content-Type': 'application/json', 35 | 'X-Stream-Client': 'stream-node-' + process.env.PKG_VERSION, 36 | 'Accept-Encoding': 'gzip', 37 | 'x-client-request-id': clientRequestId, 38 | }; 39 | 40 | const signal = AbortSignal.timeout(this.apiConfig.timeout); 41 | 42 | try { 43 | const response = await fetch(`${this.apiConfig.baseUrl}${url}`, { 44 | signal, 45 | method, 46 | body: JSON.stringify(body), 47 | headers, 48 | dispatcher: this.dispatcher, 49 | }); 50 | 51 | const responseHeaders = response.headers; 52 | 53 | const metadata: RequestMetadata = { 54 | clientRequestId, 55 | responseHeaders, 56 | responseCode: response.status, 57 | rateLimit: getRateLimitFromResponseHeader(responseHeaders), 58 | }; 59 | 60 | if (response.status < 200 || response.status >= 300) { 61 | let error: APIError; 62 | try { 63 | error = (await response.json()) as APIError; 64 | } catch (_) { 65 | throw new StreamError( 66 | `Stream error: ${response.status} - ${response.statusText}`, 67 | metadata, 68 | response.status, 69 | ); 70 | } 71 | throw new StreamError( 72 | `Stream error code ${error!.code}: ${error!.message}`, 73 | metadata, 74 | error!.code, 75 | undefined, 76 | ); 77 | } 78 | 79 | const responseBody = (await response.json()) as T; 80 | 81 | return { body: responseBody, metadata }; 82 | } catch (error: any) { 83 | if (error instanceof StreamError) { 84 | throw error; 85 | } 86 | const metadata: Partial = { 87 | clientRequestId, 88 | responseCode: error.status, 89 | }; 90 | if (error.name === 'AbortError' || error.name === 'TimeoutError') { 91 | throw new StreamError( 92 | `The request was aborted due to to the ${this.apiConfig.timeout}ms timeout, you can set the timeout in the StreamClient constructor`, 93 | metadata, 94 | undefined, 95 | error, 96 | ); 97 | } else { 98 | throw new StreamError( 99 | `The request failed due to an unexpected error`, 100 | metadata, 101 | error, 102 | ); 103 | } 104 | } 105 | }; 106 | 107 | protected queryParamsStringify = (params: Record) => { 108 | const newParams = []; 109 | for (const k in params) { 110 | const param = params[k]; 111 | if (Array.isArray(param)) { 112 | newParams.push(`${k}=${encodeURIComponent(param.join(','))}`); 113 | } else if (param instanceof Date) { 114 | newParams.push(param.toISOString()); 115 | } else if (typeof param === 'object') { 116 | newParams.push(`${k}=${encodeURIComponent(JSON.stringify(param))}`); 117 | } else { 118 | if ( 119 | typeof param === 'string' || 120 | typeof param === 'number' || 121 | typeof param === 'boolean' 122 | ) { 123 | newParams.push(`${k}=${encodeURIComponent(param)}`); 124 | } 125 | } 126 | } 127 | 128 | return newParams.join('&'); 129 | }; 130 | } 131 | -------------------------------------------------------------------------------- /src/StreamCall.ts: -------------------------------------------------------------------------------- 1 | import { QueryCallMembersRequest } from './gen/models'; 2 | import { CallApi } from './gen/video/CallApi'; 3 | import { OmitTypeId } from './types'; 4 | 5 | export class StreamCall extends CallApi { 6 | get cid() { 7 | return `${this.type}:${this.id}`; 8 | } 9 | 10 | create = this.getOrCreate; 11 | 12 | queryMembers = (request?: OmitTypeId) => { 13 | return this.videoApi.queryCallMembers({ 14 | id: this.id, 15 | type: this.type, 16 | ...(request ?? {}), 17 | }); 18 | }; 19 | } 20 | -------------------------------------------------------------------------------- /src/StreamChannel.ts: -------------------------------------------------------------------------------- 1 | import { ChannelApi } from './gen/chat/ChannelApi'; 2 | import { ChannelGetOrCreateRequest, QueryMembersPayload } from './gen/models'; 3 | import { OmitTypeId } from './types'; 4 | 5 | export class StreamChannel extends ChannelApi { 6 | get cid() { 7 | return `${this.type}:${this.id}`; 8 | } 9 | 10 | getOrCreate = (channel_get_or_create_request?: ChannelGetOrCreateRequest) => { 11 | if (!this.id) { 12 | return this.chatApi 13 | .getOrCreateDistinctChannel({ 14 | type: this.type, 15 | ...channel_get_or_create_request, 16 | }) 17 | .then((response) => { 18 | this.id = response.channel?.id; 19 | return response; 20 | }); 21 | } else { 22 | return this.chatApi.getOrCreateChannel({ 23 | id: this.id, 24 | type: this.type, 25 | ...channel_get_or_create_request, 26 | }); 27 | } 28 | }; 29 | 30 | queryMembers(request?: { payload?: OmitTypeId }) { 31 | return this.chatApi.queryMembers({ 32 | payload: { 33 | id: this.id, 34 | type: this.type, 35 | ...(request?.payload ?? { filter_conditions: {} }), 36 | }, 37 | }); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/StreamChatClient.ts: -------------------------------------------------------------------------------- 1 | import { ChatApi } from './gen/chat/ChatApi'; 2 | import { StreamChannel } from './StreamChannel'; 3 | 4 | export class StreamChatClient extends ChatApi { 5 | channel = (type: string, id?: string) => { 6 | return new StreamChannel(this, type, id); 7 | }; 8 | } 9 | -------------------------------------------------------------------------------- /src/StreamClient.ts: -------------------------------------------------------------------------------- 1 | import { JWTServerToken, JWTUserToken } from './utils/create-token'; 2 | import { CommonApi } from './gen/common/CommonApi'; 3 | import { StreamVideoClient } from './StreamVideoClient'; 4 | import crypto from 'crypto'; 5 | import { StreamChatClient } from './StreamChatClient'; 6 | import { CallTokenPayload, UserTokenPayload } from './types'; 7 | import { QueryBannedUsersPayload, UserRequest } from './gen/models'; 8 | import { StreamModerationClient } from './StreamModerationClient'; 9 | 10 | export interface StreamClientOptions { 11 | timeout?: number; 12 | basePath?: string; 13 | // We use unknown here because RequestInit['dispatcher'] is different between Node versions 14 | /** The [HTTP Agent](https://undici.nodejs.org/#/docs/api/Agent.md) to use. */ 15 | agent?: unknown; 16 | } 17 | 18 | export class StreamClient extends CommonApi { 19 | public readonly video: StreamVideoClient; 20 | public readonly chat: StreamChatClient; 21 | public readonly moderation: StreamModerationClient; 22 | public readonly options: StreamClientOptions = {}; 23 | 24 | private static readonly DEFAULT_TIMEOUT = 3000; 25 | 26 | /** 27 | * 28 | * @param apiKey 29 | * @param secret 30 | * @param config config object 31 | */ 32 | constructor( 33 | readonly apiKey: string, 34 | private readonly secret: string, 35 | readonly config?: StreamClientOptions, 36 | ) { 37 | const token = JWTServerToken(secret); 38 | const timeout = config?.timeout ?? StreamClient.DEFAULT_TIMEOUT; 39 | const chatBaseUrl = config?.basePath ?? 'https://chat.stream-io-api.com'; 40 | const videoBaseUrl = config?.basePath ?? 'https://video.stream-io-api.com'; 41 | super({ 42 | apiKey, 43 | token, 44 | timeout, 45 | baseUrl: chatBaseUrl, 46 | agent: config?.agent as RequestInit['dispatcher'], 47 | }); 48 | 49 | this.video = new StreamVideoClient({ 50 | streamClient: this, 51 | apiKey, 52 | token, 53 | timeout, 54 | baseUrl: videoBaseUrl, 55 | agent: config?.agent as RequestInit['dispatcher'], 56 | }); 57 | this.chat = new StreamChatClient({ 58 | apiKey, 59 | token, 60 | timeout, 61 | baseUrl: chatBaseUrl, 62 | agent: config?.agent as RequestInit['dispatcher'], 63 | }); 64 | this.moderation = new StreamModerationClient({ 65 | apiKey, 66 | token, 67 | timeout, 68 | baseUrl: chatBaseUrl, 69 | agent: config?.agent as RequestInit['dispatcher'], 70 | }); 71 | } 72 | 73 | upsertUsers = (users: UserRequest[]) => { 74 | const payload: Record = {}; 75 | 76 | users.forEach((u) => { 77 | payload[u.id] = u; 78 | }); 79 | 80 | return this.updateUsers({ users: payload }); 81 | }; 82 | 83 | queryBannedUsers = (request?: { payload?: QueryBannedUsersPayload }) => { 84 | return this.chat.queryBannedUsers(request); 85 | }; 86 | 87 | /** 88 | * 89 | * @param payload 90 | * - user_id - the id of the user the token is for 91 | * - validity_in_seconds - how many seconds is the token valid for (starting from issued at), by default it's 1 hour, dicarded if exp is provided 92 | * - exp - when the token expires, unix timestamp in seconds 93 | * - iat - issued at date of the token, unix timestamp in seconds, by default it's now 94 | */ 95 | generateUserToken = ( 96 | payload: { 97 | user_id: string; 98 | validity_in_seconds?: number; 99 | exp?: number; 100 | iat?: number; 101 | } & Record, 102 | ) => { 103 | const defaultIat = Math.floor((Date.now() - 1000) / 1000); 104 | payload.iat = payload.iat ?? defaultIat; 105 | const validityInSeconds = payload.validity_in_seconds ?? 60 * 60; 106 | payload.exp = payload.exp ?? payload.iat + validityInSeconds; 107 | 108 | return JWTUserToken(this.secret, payload as UserTokenPayload); 109 | }; 110 | 111 | /** 112 | * 113 | * @param payload 114 | * - user_id - the id of the user the token is for 115 | * - validity_in_seconds - how many seconds is the token valid for (starting from issued at), by default it's 1 hour, dicarded if exp is provided 116 | * - exp - when the token expires, unix timestamp in seconds 117 | * - iat - issued at date of the token, unix timestamp in seconds, by default it's now 118 | */ 119 | generateCallToken = ( 120 | payload: { 121 | user_id: string; 122 | role?: string; 123 | call_cids: string[]; 124 | validity_in_seconds?: number; 125 | exp?: number; 126 | iat?: number; 127 | } & Record, 128 | ) => { 129 | return this.generateUserToken(payload); 130 | }; 131 | 132 | /** 133 | * 134 | * @param userID 135 | * @param exp 136 | * @param iat deprecated, the default date will be set internally 137 | * @returns 138 | * 139 | * @deprecated use generateUserToken instead 140 | */ 141 | createToken = ( 142 | userID: string, 143 | exp = Math.round(Date.now() / 1000) + 60 * 60, 144 | iat = Math.floor((Date.now() - 1000) / 1000), 145 | ) => { 146 | const payload: UserTokenPayload = { 147 | user_id: userID, 148 | exp, 149 | iat, 150 | }; 151 | 152 | return JWTUserToken(this.secret, payload); 153 | }; 154 | 155 | /** 156 | * 157 | * @param userID 158 | * @param call_cids 159 | * @param exp 160 | * @param iat this is deprecated, the current date will be set internally 161 | * @returns 162 | * 163 | * @deprecated use generateCallToken instead 164 | */ 165 | createCallToken = ( 166 | userIdOrObject: string | { user_id: string; role?: string }, 167 | call_cids: string[], 168 | exp = Math.round(Date.now() / 1000) + 60 * 60, 169 | iat = Math.floor((Date.now() - 1000) / 1000), 170 | ) => { 171 | const payload: CallTokenPayload = { 172 | exp, 173 | iat, 174 | call_cids, 175 | user_id: 176 | typeof userIdOrObject === 'string' 177 | ? userIdOrObject 178 | : userIdOrObject.user_id, 179 | }; 180 | 181 | if (typeof userIdOrObject === 'object' && userIdOrObject.role) { 182 | payload.role = userIdOrObject.role; 183 | } 184 | 185 | return JWTUserToken(this.secret, payload); 186 | }; 187 | 188 | verifyWebhook = (requestBody: string | Buffer, xSignature: string) => { 189 | const key = Buffer.from(this.secret, 'utf8'); 190 | const hash = crypto 191 | .createHmac('sha256', key) 192 | .update(requestBody) 193 | .digest('hex'); 194 | 195 | try { 196 | return crypto.timingSafeEqual(Buffer.from(hash), Buffer.from(xSignature)); 197 | } catch (err) { 198 | return false; 199 | } 200 | }; 201 | } 202 | -------------------------------------------------------------------------------- /src/StreamModerationClient.ts: -------------------------------------------------------------------------------- 1 | import { ModerationApi } from './gen/moderation/ModerationApi'; 2 | 3 | export class StreamModerationClient extends ModerationApi {} 4 | -------------------------------------------------------------------------------- /src/StreamVideoClient.ts: -------------------------------------------------------------------------------- 1 | import { VideoApi } from './gen/video/VideoApi'; 2 | import { StreamCall } from './StreamCall'; 3 | import type { StreamClient } from './StreamClient'; 4 | import type { ApiConfig } from './types'; 5 | // eslint-disable-next-line @typescript-eslint/prefer-ts-expect-error 6 | /** @ts-ignore Optional dependency */ 7 | import type { 8 | createRealtimeClient, 9 | RealtimeAPIModel, 10 | RealtimeClient, 11 | } from '@stream-io/openai-realtime-api'; 12 | 13 | export class StreamVideoClient extends VideoApi { 14 | private readonly streamClient: StreamClient; 15 | 16 | constructor({ 17 | streamClient, 18 | ...apiConfig 19 | }: ApiConfig & { streamClient: StreamClient }) { 20 | super(apiConfig); 21 | this.streamClient = streamClient; 22 | } 23 | 24 | call = (type: string, id: string) => { 25 | return new StreamCall(this, type, id); 26 | }; 27 | 28 | connectOpenAi = async (options: { 29 | call: StreamCall; 30 | agentUserId: string; 31 | openAiApiKey: string; 32 | model?: RealtimeAPIModel; 33 | validityInSeconds?: number; 34 | }): Promise => { 35 | let doCreateRealtimeClient: typeof createRealtimeClient; 36 | 37 | try { 38 | doCreateRealtimeClient = (await import('@stream-io/openai-realtime-api')) 39 | .createRealtimeClient; 40 | } catch { 41 | throw new Error( 42 | 'Cannot create Realtime API client. Is @stream-io/openai-realtime-api installed?', 43 | ); 44 | } 45 | 46 | if (!options.agentUserId) { 47 | throw new Error('"agentUserId" must by specified in options'); 48 | } 49 | 50 | const token = this.streamClient.generateCallToken({ 51 | user_id: options.agentUserId, 52 | call_cids: [options.call.cid], 53 | validity_in_seconds: options.validityInSeconds, 54 | }); 55 | 56 | const realtimeClient = doCreateRealtimeClient({ 57 | baseUrl: this.apiConfig.baseUrl, 58 | call: options.call, 59 | streamApiKey: this.apiConfig.apiKey, 60 | streamUserToken: token, 61 | openAiApiKey: options.openAiApiKey, 62 | model: options.model, 63 | }); 64 | 65 | await realtimeClient.connect(); 66 | return realtimeClient; 67 | }; 68 | } 69 | -------------------------------------------------------------------------------- /src/gen/chat/ChannelApi.ts: -------------------------------------------------------------------------------- 1 | import { ChatApi } from './ChatApi'; 2 | import { StreamResponse } from '../../types'; 3 | import { 4 | ChannelGetOrCreateRequest, 5 | ChannelStateResponse, 6 | DeleteChannelResponse, 7 | EventResponse, 8 | FileUploadRequest, 9 | FileUploadResponse, 10 | GetDraftResponse, 11 | GetManyMessagesResponse, 12 | HideChannelRequest, 13 | HideChannelResponse, 14 | ImageUploadRequest, 15 | ImageUploadResponse, 16 | MarkReadRequest, 17 | MarkReadResponse, 18 | MarkUnreadRequest, 19 | Response, 20 | SendEventRequest, 21 | SendMessageRequest, 22 | SendMessageResponse, 23 | ShowChannelRequest, 24 | ShowChannelResponse, 25 | TruncateChannelRequest, 26 | TruncateChannelResponse, 27 | UpdateChannelPartialRequest, 28 | UpdateChannelPartialResponse, 29 | UpdateChannelRequest, 30 | UpdateChannelResponse, 31 | UpdateMemberPartialRequest, 32 | UpdateMemberPartialResponse, 33 | } from '../models'; 34 | 35 | export class ChannelApi { 36 | constructor( 37 | protected chatApi: ChatApi, 38 | public readonly type: string, 39 | public id?: string, 40 | ) {} 41 | 42 | delete = (request?: { 43 | hard_delete?: boolean; 44 | }): Promise> => { 45 | if (!this.id) { 46 | throw new Error( 47 | `Channel isn't yet created, call getOrCreateDistinctChannel() before this operation`, 48 | ); 49 | } 50 | return this.chatApi.deleteChannel({ 51 | id: this.id, 52 | type: this.type, 53 | ...request, 54 | }); 55 | }; 56 | 57 | updateChannelPartial = ( 58 | request?: UpdateChannelPartialRequest, 59 | ): Promise> => { 60 | if (!this.id) { 61 | throw new Error( 62 | `Channel isn't yet created, call getOrCreateDistinctChannel() before this operation`, 63 | ); 64 | } 65 | return this.chatApi.updateChannelPartial({ 66 | id: this.id, 67 | type: this.type, 68 | ...request, 69 | }); 70 | }; 71 | 72 | update = ( 73 | request?: UpdateChannelRequest, 74 | ): Promise> => { 75 | if (!this.id) { 76 | throw new Error( 77 | `Channel isn't yet created, call getOrCreateDistinctChannel() before this operation`, 78 | ); 79 | } 80 | return this.chatApi.updateChannel({ 81 | id: this.id, 82 | type: this.type, 83 | ...request, 84 | }); 85 | }; 86 | 87 | deleteDraft = (request?: { 88 | parent_id?: string; 89 | user_id?: string; 90 | }): Promise> => { 91 | if (!this.id) { 92 | throw new Error( 93 | `Channel isn't yet created, call getOrCreateDistinctChannel() before this operation`, 94 | ); 95 | } 96 | return this.chatApi.deleteDraft({ 97 | id: this.id, 98 | type: this.type, 99 | ...request, 100 | }); 101 | }; 102 | 103 | getDraft = (request?: { 104 | parent_id?: string; 105 | user_id?: string; 106 | }): Promise> => { 107 | if (!this.id) { 108 | throw new Error( 109 | `Channel isn't yet created, call getOrCreateDistinctChannel() before this operation`, 110 | ); 111 | } 112 | return this.chatApi.getDraft({ id: this.id, type: this.type, ...request }); 113 | }; 114 | 115 | sendEvent = ( 116 | request: SendEventRequest, 117 | ): Promise> => { 118 | if (!this.id) { 119 | throw new Error( 120 | `Channel isn't yet created, call getOrCreateDistinctChannel() before this operation`, 121 | ); 122 | } 123 | return this.chatApi.sendEvent({ id: this.id, type: this.type, ...request }); 124 | }; 125 | 126 | deleteFile = (request?: { 127 | url?: string; 128 | }): Promise> => { 129 | if (!this.id) { 130 | throw new Error( 131 | `Channel isn't yet created, call getOrCreateDistinctChannel() before this operation`, 132 | ); 133 | } 134 | return this.chatApi.deleteFile({ 135 | id: this.id, 136 | type: this.type, 137 | ...request, 138 | }); 139 | }; 140 | 141 | uploadFile = ( 142 | request?: FileUploadRequest, 143 | ): Promise> => { 144 | if (!this.id) { 145 | throw new Error( 146 | `Channel isn't yet created, call getOrCreateDistinctChannel() before this operation`, 147 | ); 148 | } 149 | return this.chatApi.uploadFile({ 150 | id: this.id, 151 | type: this.type, 152 | ...request, 153 | }); 154 | }; 155 | 156 | hide = ( 157 | request?: HideChannelRequest, 158 | ): Promise> => { 159 | if (!this.id) { 160 | throw new Error( 161 | `Channel isn't yet created, call getOrCreateDistinctChannel() before this operation`, 162 | ); 163 | } 164 | return this.chatApi.hideChannel({ 165 | id: this.id, 166 | type: this.type, 167 | ...request, 168 | }); 169 | }; 170 | 171 | deleteImage = (request?: { 172 | url?: string; 173 | }): Promise> => { 174 | if (!this.id) { 175 | throw new Error( 176 | `Channel isn't yet created, call getOrCreateDistinctChannel() before this operation`, 177 | ); 178 | } 179 | return this.chatApi.deleteImage({ 180 | id: this.id, 181 | type: this.type, 182 | ...request, 183 | }); 184 | }; 185 | 186 | uploadImage = ( 187 | request?: ImageUploadRequest, 188 | ): Promise> => { 189 | if (!this.id) { 190 | throw new Error( 191 | `Channel isn't yet created, call getOrCreateDistinctChannel() before this operation`, 192 | ); 193 | } 194 | return this.chatApi.uploadImage({ 195 | id: this.id, 196 | type: this.type, 197 | ...request, 198 | }); 199 | }; 200 | 201 | updateMemberPartial = ( 202 | request?: UpdateMemberPartialRequest & { user_id?: string }, 203 | ): Promise> => { 204 | if (!this.id) { 205 | throw new Error( 206 | `Channel isn't yet created, call getOrCreateDistinctChannel() before this operation`, 207 | ); 208 | } 209 | return this.chatApi.updateMemberPartial({ 210 | id: this.id, 211 | type: this.type, 212 | ...request, 213 | }); 214 | }; 215 | 216 | sendMessage = ( 217 | request: SendMessageRequest, 218 | ): Promise> => { 219 | if (!this.id) { 220 | throw new Error( 221 | `Channel isn't yet created, call getOrCreateDistinctChannel() before this operation`, 222 | ); 223 | } 224 | return this.chatApi.sendMessage({ 225 | id: this.id, 226 | type: this.type, 227 | ...request, 228 | }); 229 | }; 230 | 231 | getManyMessages = (request: { 232 | ids: string[]; 233 | }): Promise> => { 234 | if (!this.id) { 235 | throw new Error( 236 | `Channel isn't yet created, call getOrCreateDistinctChannel() before this operation`, 237 | ); 238 | } 239 | return this.chatApi.getManyMessages({ 240 | id: this.id, 241 | type: this.type, 242 | ...request, 243 | }); 244 | }; 245 | 246 | getOrCreate = ( 247 | request?: ChannelGetOrCreateRequest, 248 | ): Promise> => { 249 | if (!this.id) { 250 | throw new Error( 251 | `Channel isn't yet created, call getOrCreateDistinctChannel() before this operation`, 252 | ); 253 | } 254 | return this.chatApi.getOrCreateChannel({ 255 | id: this.id, 256 | type: this.type, 257 | ...request, 258 | }); 259 | }; 260 | 261 | markRead = ( 262 | request?: MarkReadRequest, 263 | ): Promise> => { 264 | if (!this.id) { 265 | throw new Error( 266 | `Channel isn't yet created, call getOrCreateDistinctChannel() before this operation`, 267 | ); 268 | } 269 | return this.chatApi.markRead({ id: this.id, type: this.type, ...request }); 270 | }; 271 | 272 | show = ( 273 | request?: ShowChannelRequest, 274 | ): Promise> => { 275 | if (!this.id) { 276 | throw new Error( 277 | `Channel isn't yet created, call getOrCreateDistinctChannel() before this operation`, 278 | ); 279 | } 280 | return this.chatApi.showChannel({ 281 | id: this.id, 282 | type: this.type, 283 | ...request, 284 | }); 285 | }; 286 | 287 | truncate = ( 288 | request?: TruncateChannelRequest, 289 | ): Promise> => { 290 | if (!this.id) { 291 | throw new Error( 292 | `Channel isn't yet created, call getOrCreateDistinctChannel() before this operation`, 293 | ); 294 | } 295 | return this.chatApi.truncateChannel({ 296 | id: this.id, 297 | type: this.type, 298 | ...request, 299 | }); 300 | }; 301 | 302 | markUnread = ( 303 | request?: MarkUnreadRequest, 304 | ): Promise> => { 305 | if (!this.id) { 306 | throw new Error( 307 | `Channel isn't yet created, call getOrCreateDistinctChannel() before this operation`, 308 | ); 309 | } 310 | return this.chatApi.markUnread({ 311 | id: this.id, 312 | type: this.type, 313 | ...request, 314 | }); 315 | }; 316 | } 317 | -------------------------------------------------------------------------------- /src/gen/common/CommonApi.ts: -------------------------------------------------------------------------------- 1 | import { BaseApi } from '../../BaseApi'; 2 | import { StreamResponse } from '../../types'; 3 | import { 4 | BlockUsersRequest, 5 | BlockUsersResponse, 6 | CheckExternalStorageResponse, 7 | CheckPushRequest, 8 | CheckPushResponse, 9 | CheckSNSRequest, 10 | CheckSNSResponse, 11 | CheckSQSRequest, 12 | CheckSQSResponse, 13 | CreateBlockListRequest, 14 | CreateBlockListResponse, 15 | CreateDeviceRequest, 16 | CreateExternalStorageRequest, 17 | CreateExternalStorageResponse, 18 | CreateGuestRequest, 19 | CreateGuestResponse, 20 | CreateImportRequest, 21 | CreateImportResponse, 22 | CreateImportURLRequest, 23 | CreateImportURLResponse, 24 | CreateRoleRequest, 25 | CreateRoleResponse, 26 | DeactivateUserRequest, 27 | DeactivateUserResponse, 28 | DeactivateUsersRequest, 29 | DeactivateUsersResponse, 30 | DeleteExternalStorageResponse, 31 | DeleteUsersRequest, 32 | DeleteUsersResponse, 33 | ExportUserResponse, 34 | ExportUsersRequest, 35 | ExportUsersResponse, 36 | GetApplicationResponse, 37 | GetBlockListResponse, 38 | GetBlockedUsersResponse, 39 | GetCustomPermissionResponse, 40 | GetImportResponse, 41 | GetOGResponse, 42 | GetRateLimitsResponse, 43 | GetTaskResponse, 44 | ListBlockListResponse, 45 | ListDevicesResponse, 46 | ListExternalStorageResponse, 47 | ListImportsResponse, 48 | ListPermissionsResponse, 49 | ListPushProvidersResponse, 50 | ListRolesResponse, 51 | QueryUsersPayload, 52 | QueryUsersResponse, 53 | ReactivateUserRequest, 54 | ReactivateUserResponse, 55 | ReactivateUsersRequest, 56 | ReactivateUsersResponse, 57 | Response, 58 | RestoreUsersRequest, 59 | UnblockUsersRequest, 60 | UnblockUsersResponse, 61 | UpdateAppRequest, 62 | UpdateBlockListRequest, 63 | UpdateBlockListResponse, 64 | UpdateExternalStorageRequest, 65 | UpdateExternalStorageResponse, 66 | UpdateUsersPartialRequest, 67 | UpdateUsersRequest, 68 | UpdateUsersResponse, 69 | UpsertPushProviderRequest, 70 | UpsertPushProviderResponse, 71 | } from '../models'; 72 | import { decoders } from '../model-decoders'; 73 | 74 | export class CommonApi extends BaseApi { 75 | getApp = async (): Promise> => { 76 | const response = await this.sendRequest< 77 | StreamResponse 78 | >('GET', '/api/v2/app', undefined, undefined); 79 | 80 | decoders.GetApplicationResponse?.(response.body); 81 | 82 | return { ...response.body, metadata: response.metadata }; 83 | }; 84 | 85 | updateApp = async ( 86 | request?: UpdateAppRequest, 87 | ): Promise> => { 88 | const body = { 89 | async_url_enrich_enabled: request?.async_url_enrich_enabled, 90 | auto_translation_enabled: request?.auto_translation_enabled, 91 | before_message_send_hook_url: request?.before_message_send_hook_url, 92 | cdn_expiration_seconds: request?.cdn_expiration_seconds, 93 | channel_hide_members_only: request?.channel_hide_members_only, 94 | custom_action_handler_url: request?.custom_action_handler_url, 95 | disable_auth_checks: request?.disable_auth_checks, 96 | disable_permissions_checks: request?.disable_permissions_checks, 97 | enforce_unique_usernames: request?.enforce_unique_usernames, 98 | feeds_moderation_enabled: request?.feeds_moderation_enabled, 99 | feeds_v2_region: request?.feeds_v2_region, 100 | guest_user_creation_disabled: request?.guest_user_creation_disabled, 101 | image_moderation_enabled: request?.image_moderation_enabled, 102 | migrate_permissions_to_v2: request?.migrate_permissions_to_v2, 103 | moderation_enabled: request?.moderation_enabled, 104 | moderation_webhook_url: request?.moderation_webhook_url, 105 | multi_tenant_enabled: request?.multi_tenant_enabled, 106 | permission_version: request?.permission_version, 107 | reminders_interval: request?.reminders_interval, 108 | reminders_max_members: request?.reminders_max_members, 109 | revoke_tokens_issued_before: request?.revoke_tokens_issued_before, 110 | sns_key: request?.sns_key, 111 | sns_secret: request?.sns_secret, 112 | sns_topic_arn: request?.sns_topic_arn, 113 | sqs_key: request?.sqs_key, 114 | sqs_secret: request?.sqs_secret, 115 | sqs_url: request?.sqs_url, 116 | webhook_url: request?.webhook_url, 117 | allowed_flag_reasons: request?.allowed_flag_reasons, 118 | image_moderation_block_labels: request?.image_moderation_block_labels, 119 | image_moderation_labels: request?.image_moderation_labels, 120 | user_search_disallowed_roles: request?.user_search_disallowed_roles, 121 | webhook_events: request?.webhook_events, 122 | apn_config: request?.apn_config, 123 | async_moderation_config: request?.async_moderation_config, 124 | datadog_info: request?.datadog_info, 125 | file_upload_config: request?.file_upload_config, 126 | firebase_config: request?.firebase_config, 127 | grants: request?.grants, 128 | huawei_config: request?.huawei_config, 129 | image_upload_config: request?.image_upload_config, 130 | push_config: request?.push_config, 131 | xiaomi_config: request?.xiaomi_config, 132 | }; 133 | 134 | const response = await this.sendRequest>( 135 | 'PATCH', 136 | '/api/v2/app', 137 | undefined, 138 | undefined, 139 | body, 140 | ); 141 | 142 | decoders.Response?.(response.body); 143 | 144 | return { ...response.body, metadata: response.metadata }; 145 | }; 146 | 147 | listBlockLists = async (request?: { 148 | team?: string; 149 | }): Promise> => { 150 | const queryParams = { 151 | team: request?.team, 152 | }; 153 | 154 | const response = await this.sendRequest< 155 | StreamResponse 156 | >('GET', '/api/v2/blocklists', undefined, queryParams); 157 | 158 | decoders.ListBlockListResponse?.(response.body); 159 | 160 | return { ...response.body, metadata: response.metadata }; 161 | }; 162 | 163 | createBlockList = async ( 164 | request: CreateBlockListRequest, 165 | ): Promise> => { 166 | const body = { 167 | name: request?.name, 168 | words: request?.words, 169 | team: request?.team, 170 | type: request?.type, 171 | }; 172 | 173 | const response = await this.sendRequest< 174 | StreamResponse 175 | >('POST', '/api/v2/blocklists', undefined, undefined, body); 176 | 177 | decoders.CreateBlockListResponse?.(response.body); 178 | 179 | return { ...response.body, metadata: response.metadata }; 180 | }; 181 | 182 | deleteBlockList = async (request: { 183 | name: string; 184 | team?: string; 185 | }): Promise> => { 186 | const queryParams = { 187 | team: request?.team, 188 | }; 189 | const pathParams = { 190 | name: request?.name, 191 | }; 192 | 193 | const response = await this.sendRequest>( 194 | 'DELETE', 195 | '/api/v2/blocklists/{name}', 196 | pathParams, 197 | queryParams, 198 | ); 199 | 200 | decoders.Response?.(response.body); 201 | 202 | return { ...response.body, metadata: response.metadata }; 203 | }; 204 | 205 | getBlockList = async (request: { 206 | name: string; 207 | team?: string; 208 | }): Promise> => { 209 | const queryParams = { 210 | team: request?.team, 211 | }; 212 | const pathParams = { 213 | name: request?.name, 214 | }; 215 | 216 | const response = await this.sendRequest< 217 | StreamResponse 218 | >('GET', '/api/v2/blocklists/{name}', pathParams, queryParams); 219 | 220 | decoders.GetBlockListResponse?.(response.body); 221 | 222 | return { ...response.body, metadata: response.metadata }; 223 | }; 224 | 225 | updateBlockList = async ( 226 | request: UpdateBlockListRequest & { name: string }, 227 | ): Promise> => { 228 | const pathParams = { 229 | name: request?.name, 230 | }; 231 | const body = { 232 | team: request?.team, 233 | words: request?.words, 234 | }; 235 | 236 | const response = await this.sendRequest< 237 | StreamResponse 238 | >('PUT', '/api/v2/blocklists/{name}', pathParams, undefined, body); 239 | 240 | decoders.UpdateBlockListResponse?.(response.body); 241 | 242 | return { ...response.body, metadata: response.metadata }; 243 | }; 244 | 245 | checkPush = async ( 246 | request?: CheckPushRequest, 247 | ): Promise> => { 248 | const body = { 249 | apn_template: request?.apn_template, 250 | firebase_data_template: request?.firebase_data_template, 251 | firebase_template: request?.firebase_template, 252 | message_id: request?.message_id, 253 | push_provider_name: request?.push_provider_name, 254 | push_provider_type: request?.push_provider_type, 255 | skip_devices: request?.skip_devices, 256 | user_id: request?.user_id, 257 | user: request?.user, 258 | }; 259 | 260 | const response = await this.sendRequest>( 261 | 'POST', 262 | '/api/v2/check_push', 263 | undefined, 264 | undefined, 265 | body, 266 | ); 267 | 268 | decoders.CheckPushResponse?.(response.body); 269 | 270 | return { ...response.body, metadata: response.metadata }; 271 | }; 272 | 273 | checkSNS = async ( 274 | request?: CheckSNSRequest, 275 | ): Promise> => { 276 | const body = { 277 | sns_key: request?.sns_key, 278 | sns_secret: request?.sns_secret, 279 | sns_topic_arn: request?.sns_topic_arn, 280 | }; 281 | 282 | const response = await this.sendRequest>( 283 | 'POST', 284 | '/api/v2/check_sns', 285 | undefined, 286 | undefined, 287 | body, 288 | ); 289 | 290 | decoders.CheckSNSResponse?.(response.body); 291 | 292 | return { ...response.body, metadata: response.metadata }; 293 | }; 294 | 295 | checkSQS = async ( 296 | request?: CheckSQSRequest, 297 | ): Promise> => { 298 | const body = { 299 | sqs_key: request?.sqs_key, 300 | sqs_secret: request?.sqs_secret, 301 | sqs_url: request?.sqs_url, 302 | }; 303 | 304 | const response = await this.sendRequest>( 305 | 'POST', 306 | '/api/v2/check_sqs', 307 | undefined, 308 | undefined, 309 | body, 310 | ); 311 | 312 | decoders.CheckSQSResponse?.(response.body); 313 | 314 | return { ...response.body, metadata: response.metadata }; 315 | }; 316 | 317 | deleteDevice = async (request: { 318 | id: string; 319 | user_id?: string; 320 | }): Promise> => { 321 | const queryParams = { 322 | id: request?.id, 323 | user_id: request?.user_id, 324 | }; 325 | 326 | const response = await this.sendRequest>( 327 | 'DELETE', 328 | '/api/v2/devices', 329 | undefined, 330 | queryParams, 331 | ); 332 | 333 | decoders.Response?.(response.body); 334 | 335 | return { ...response.body, metadata: response.metadata }; 336 | }; 337 | 338 | listDevices = async (request?: { 339 | user_id?: string; 340 | }): Promise> => { 341 | const queryParams = { 342 | user_id: request?.user_id, 343 | }; 344 | 345 | const response = await this.sendRequest< 346 | StreamResponse 347 | >('GET', '/api/v2/devices', undefined, queryParams); 348 | 349 | decoders.ListDevicesResponse?.(response.body); 350 | 351 | return { ...response.body, metadata: response.metadata }; 352 | }; 353 | 354 | createDevice = async ( 355 | request: CreateDeviceRequest, 356 | ): Promise> => { 357 | const body = { 358 | id: request?.id, 359 | push_provider: request?.push_provider, 360 | push_provider_name: request?.push_provider_name, 361 | user_id: request?.user_id, 362 | voip_token: request?.voip_token, 363 | user: request?.user, 364 | }; 365 | 366 | const response = await this.sendRequest>( 367 | 'POST', 368 | '/api/v2/devices', 369 | undefined, 370 | undefined, 371 | body, 372 | ); 373 | 374 | decoders.Response?.(response.body); 375 | 376 | return { ...response.body, metadata: response.metadata }; 377 | }; 378 | 379 | exportUsers = async ( 380 | request: ExportUsersRequest, 381 | ): Promise> => { 382 | const body = { 383 | user_ids: request?.user_ids, 384 | }; 385 | 386 | const response = await this.sendRequest< 387 | StreamResponse 388 | >('POST', '/api/v2/export/users', undefined, undefined, body); 389 | 390 | decoders.ExportUsersResponse?.(response.body); 391 | 392 | return { ...response.body, metadata: response.metadata }; 393 | }; 394 | 395 | listExternalStorage = async (): Promise< 396 | StreamResponse 397 | > => { 398 | const response = await this.sendRequest< 399 | StreamResponse 400 | >('GET', '/api/v2/external_storage', undefined, undefined); 401 | 402 | decoders.ListExternalStorageResponse?.(response.body); 403 | 404 | return { ...response.body, metadata: response.metadata }; 405 | }; 406 | 407 | createExternalStorage = async ( 408 | request: CreateExternalStorageRequest, 409 | ): Promise> => { 410 | const body = { 411 | bucket: request?.bucket, 412 | name: request?.name, 413 | storage_type: request?.storage_type, 414 | gcs_credentials: request?.gcs_credentials, 415 | path: request?.path, 416 | aws_s3: request?.aws_s3, 417 | azure_blob: request?.azure_blob, 418 | }; 419 | 420 | const response = await this.sendRequest< 421 | StreamResponse 422 | >('POST', '/api/v2/external_storage', undefined, undefined, body); 423 | 424 | decoders.CreateExternalStorageResponse?.(response.body); 425 | 426 | return { ...response.body, metadata: response.metadata }; 427 | }; 428 | 429 | deleteExternalStorage = async (request: { 430 | name: string; 431 | }): Promise> => { 432 | const pathParams = { 433 | name: request?.name, 434 | }; 435 | 436 | const response = await this.sendRequest< 437 | StreamResponse 438 | >('DELETE', '/api/v2/external_storage/{name}', pathParams, undefined); 439 | 440 | decoders.DeleteExternalStorageResponse?.(response.body); 441 | 442 | return { ...response.body, metadata: response.metadata }; 443 | }; 444 | 445 | updateExternalStorage = async ( 446 | request: UpdateExternalStorageRequest & { name: string }, 447 | ): Promise> => { 448 | const pathParams = { 449 | name: request?.name, 450 | }; 451 | const body = { 452 | bucket: request?.bucket, 453 | storage_type: request?.storage_type, 454 | gcs_credentials: request?.gcs_credentials, 455 | path: request?.path, 456 | aws_s3: request?.aws_s3, 457 | azure_blob: request?.azure_blob, 458 | }; 459 | 460 | const response = await this.sendRequest< 461 | StreamResponse 462 | >('PUT', '/api/v2/external_storage/{name}', pathParams, undefined, body); 463 | 464 | decoders.UpdateExternalStorageResponse?.(response.body); 465 | 466 | return { ...response.body, metadata: response.metadata }; 467 | }; 468 | 469 | checkExternalStorage = async (request: { 470 | name: string; 471 | }): Promise> => { 472 | const pathParams = { 473 | name: request?.name, 474 | }; 475 | 476 | const response = await this.sendRequest< 477 | StreamResponse 478 | >('GET', '/api/v2/external_storage/{name}/check', pathParams, undefined); 479 | 480 | decoders.CheckExternalStorageResponse?.(response.body); 481 | 482 | return { ...response.body, metadata: response.metadata }; 483 | }; 484 | 485 | createGuest = async ( 486 | request: CreateGuestRequest, 487 | ): Promise> => { 488 | const body = { 489 | user: request?.user, 490 | }; 491 | 492 | const response = await this.sendRequest< 493 | StreamResponse 494 | >('POST', '/api/v2/guest', undefined, undefined, body); 495 | 496 | decoders.CreateGuestResponse?.(response.body); 497 | 498 | return { ...response.body, metadata: response.metadata }; 499 | }; 500 | 501 | createImportURL = async ( 502 | request?: CreateImportURLRequest, 503 | ): Promise> => { 504 | const body = { 505 | filename: request?.filename, 506 | }; 507 | 508 | const response = await this.sendRequest< 509 | StreamResponse 510 | >('POST', '/api/v2/import_urls', undefined, undefined, body); 511 | 512 | decoders.CreateImportURLResponse?.(response.body); 513 | 514 | return { ...response.body, metadata: response.metadata }; 515 | }; 516 | 517 | listImports = async (): Promise> => { 518 | const response = await this.sendRequest< 519 | StreamResponse 520 | >('GET', '/api/v2/imports', undefined, undefined); 521 | 522 | decoders.ListImportsResponse?.(response.body); 523 | 524 | return { ...response.body, metadata: response.metadata }; 525 | }; 526 | 527 | createImport = async ( 528 | request: CreateImportRequest, 529 | ): Promise> => { 530 | const body = { 531 | mode: request?.mode, 532 | path: request?.path, 533 | }; 534 | 535 | const response = await this.sendRequest< 536 | StreamResponse 537 | >('POST', '/api/v2/imports', undefined, undefined, body); 538 | 539 | decoders.CreateImportResponse?.(response.body); 540 | 541 | return { ...response.body, metadata: response.metadata }; 542 | }; 543 | 544 | getImport = async (request: { 545 | id: string; 546 | }): Promise> => { 547 | const pathParams = { 548 | id: request?.id, 549 | }; 550 | 551 | const response = await this.sendRequest>( 552 | 'GET', 553 | '/api/v2/imports/{id}', 554 | pathParams, 555 | undefined, 556 | ); 557 | 558 | decoders.GetImportResponse?.(response.body); 559 | 560 | return { ...response.body, metadata: response.metadata }; 561 | }; 562 | 563 | getOG = async (request: { 564 | url: string; 565 | }): Promise> => { 566 | const queryParams = { 567 | url: request?.url, 568 | }; 569 | 570 | const response = await this.sendRequest>( 571 | 'GET', 572 | '/api/v2/og', 573 | undefined, 574 | queryParams, 575 | ); 576 | 577 | decoders.GetOGResponse?.(response.body); 578 | 579 | return { ...response.body, metadata: response.metadata }; 580 | }; 581 | 582 | listPermissions = async (): Promise< 583 | StreamResponse 584 | > => { 585 | const response = await this.sendRequest< 586 | StreamResponse 587 | >('GET', '/api/v2/permissions', undefined, undefined); 588 | 589 | decoders.ListPermissionsResponse?.(response.body); 590 | 591 | return { ...response.body, metadata: response.metadata }; 592 | }; 593 | 594 | getPermission = async (request: { 595 | id: string; 596 | }): Promise> => { 597 | const pathParams = { 598 | id: request?.id, 599 | }; 600 | 601 | const response = await this.sendRequest< 602 | StreamResponse 603 | >('GET', '/api/v2/permissions/{id}', pathParams, undefined); 604 | 605 | decoders.GetCustomPermissionResponse?.(response.body); 606 | 607 | return { ...response.body, metadata: response.metadata }; 608 | }; 609 | 610 | listPushProviders = async (): Promise< 611 | StreamResponse 612 | > => { 613 | const response = await this.sendRequest< 614 | StreamResponse 615 | >('GET', '/api/v2/push_providers', undefined, undefined); 616 | 617 | decoders.ListPushProvidersResponse?.(response.body); 618 | 619 | return { ...response.body, metadata: response.metadata }; 620 | }; 621 | 622 | upsertPushProvider = async ( 623 | request?: UpsertPushProviderRequest, 624 | ): Promise> => { 625 | const body = { 626 | push_provider: request?.push_provider, 627 | }; 628 | 629 | const response = await this.sendRequest< 630 | StreamResponse 631 | >('POST', '/api/v2/push_providers', undefined, undefined, body); 632 | 633 | decoders.UpsertPushProviderResponse?.(response.body); 634 | 635 | return { ...response.body, metadata: response.metadata }; 636 | }; 637 | 638 | deletePushProvider = async (request: { 639 | type: string; 640 | name: string; 641 | }): Promise> => { 642 | const pathParams = { 643 | type: request?.type, 644 | name: request?.name, 645 | }; 646 | 647 | const response = await this.sendRequest>( 648 | 'DELETE', 649 | '/api/v2/push_providers/{type}/{name}', 650 | pathParams, 651 | undefined, 652 | ); 653 | 654 | decoders.Response?.(response.body); 655 | 656 | return { ...response.body, metadata: response.metadata }; 657 | }; 658 | 659 | getRateLimits = async (request?: { 660 | server_side?: boolean; 661 | android?: boolean; 662 | ios?: boolean; 663 | web?: boolean; 664 | endpoints?: string; 665 | }): Promise> => { 666 | const queryParams = { 667 | server_side: request?.server_side, 668 | android: request?.android, 669 | ios: request?.ios, 670 | web: request?.web, 671 | endpoints: request?.endpoints, 672 | }; 673 | 674 | const response = await this.sendRequest< 675 | StreamResponse 676 | >('GET', '/api/v2/rate_limits', undefined, queryParams); 677 | 678 | decoders.GetRateLimitsResponse?.(response.body); 679 | 680 | return { ...response.body, metadata: response.metadata }; 681 | }; 682 | 683 | listRoles = async (): Promise> => { 684 | const response = await this.sendRequest>( 685 | 'GET', 686 | '/api/v2/roles', 687 | undefined, 688 | undefined, 689 | ); 690 | 691 | decoders.ListRolesResponse?.(response.body); 692 | 693 | return { ...response.body, metadata: response.metadata }; 694 | }; 695 | 696 | createRole = async ( 697 | request: CreateRoleRequest, 698 | ): Promise> => { 699 | const body = { 700 | name: request?.name, 701 | }; 702 | 703 | const response = await this.sendRequest>( 704 | 'POST', 705 | '/api/v2/roles', 706 | undefined, 707 | undefined, 708 | body, 709 | ); 710 | 711 | decoders.CreateRoleResponse?.(response.body); 712 | 713 | return { ...response.body, metadata: response.metadata }; 714 | }; 715 | 716 | deleteRole = async (request: { 717 | name: string; 718 | }): Promise> => { 719 | const pathParams = { 720 | name: request?.name, 721 | }; 722 | 723 | const response = await this.sendRequest>( 724 | 'DELETE', 725 | '/api/v2/roles/{name}', 726 | pathParams, 727 | undefined, 728 | ); 729 | 730 | decoders.Response?.(response.body); 731 | 732 | return { ...response.body, metadata: response.metadata }; 733 | }; 734 | 735 | getTask = async (request: { 736 | id: string; 737 | }): Promise> => { 738 | const pathParams = { 739 | id: request?.id, 740 | }; 741 | 742 | const response = await this.sendRequest>( 743 | 'GET', 744 | '/api/v2/tasks/{id}', 745 | pathParams, 746 | undefined, 747 | ); 748 | 749 | decoders.GetTaskResponse?.(response.body); 750 | 751 | return { ...response.body, metadata: response.metadata }; 752 | }; 753 | 754 | queryUsers = async (request?: { 755 | payload?: QueryUsersPayload; 756 | }): Promise> => { 757 | const queryParams = { 758 | payload: request?.payload, 759 | }; 760 | 761 | const response = await this.sendRequest>( 762 | 'GET', 763 | '/api/v2/users', 764 | undefined, 765 | queryParams, 766 | ); 767 | 768 | decoders.QueryUsersResponse?.(response.body); 769 | 770 | return { ...response.body, metadata: response.metadata }; 771 | }; 772 | 773 | updateUsersPartial = async ( 774 | request: UpdateUsersPartialRequest, 775 | ): Promise> => { 776 | const body = { 777 | users: request?.users, 778 | }; 779 | 780 | const response = await this.sendRequest< 781 | StreamResponse 782 | >('PATCH', '/api/v2/users', undefined, undefined, body); 783 | 784 | decoders.UpdateUsersResponse?.(response.body); 785 | 786 | return { ...response.body, metadata: response.metadata }; 787 | }; 788 | 789 | updateUsers = async ( 790 | request: UpdateUsersRequest, 791 | ): Promise> => { 792 | const body = { 793 | users: request?.users, 794 | }; 795 | 796 | const response = await this.sendRequest< 797 | StreamResponse 798 | >('POST', '/api/v2/users', undefined, undefined, body); 799 | 800 | decoders.UpdateUsersResponse?.(response.body); 801 | 802 | return { ...response.body, metadata: response.metadata }; 803 | }; 804 | 805 | getBlockedUsers = async (request?: { 806 | user_id?: string; 807 | }): Promise> => { 808 | const queryParams = { 809 | user_id: request?.user_id, 810 | }; 811 | 812 | const response = await this.sendRequest< 813 | StreamResponse 814 | >('GET', '/api/v2/users/block', undefined, queryParams); 815 | 816 | decoders.GetBlockedUsersResponse?.(response.body); 817 | 818 | return { ...response.body, metadata: response.metadata }; 819 | }; 820 | 821 | blockUsers = async ( 822 | request: BlockUsersRequest, 823 | ): Promise> => { 824 | const body = { 825 | blocked_user_id: request?.blocked_user_id, 826 | user_id: request?.user_id, 827 | user: request?.user, 828 | }; 829 | 830 | const response = await this.sendRequest>( 831 | 'POST', 832 | '/api/v2/users/block', 833 | undefined, 834 | undefined, 835 | body, 836 | ); 837 | 838 | decoders.BlockUsersResponse?.(response.body); 839 | 840 | return { ...response.body, metadata: response.metadata }; 841 | }; 842 | 843 | deactivateUsers = async ( 844 | request: DeactivateUsersRequest, 845 | ): Promise> => { 846 | const body = { 847 | user_ids: request?.user_ids, 848 | created_by_id: request?.created_by_id, 849 | mark_channels_deleted: request?.mark_channels_deleted, 850 | mark_messages_deleted: request?.mark_messages_deleted, 851 | }; 852 | 853 | const response = await this.sendRequest< 854 | StreamResponse 855 | >('POST', '/api/v2/users/deactivate', undefined, undefined, body); 856 | 857 | decoders.DeactivateUsersResponse?.(response.body); 858 | 859 | return { ...response.body, metadata: response.metadata }; 860 | }; 861 | 862 | deleteUsers = async ( 863 | request: DeleteUsersRequest, 864 | ): Promise> => { 865 | const body = { 866 | user_ids: request?.user_ids, 867 | calls: request?.calls, 868 | conversations: request?.conversations, 869 | messages: request?.messages, 870 | new_call_owner_id: request?.new_call_owner_id, 871 | new_channel_owner_id: request?.new_channel_owner_id, 872 | user: request?.user, 873 | }; 874 | 875 | const response = await this.sendRequest< 876 | StreamResponse 877 | >('POST', '/api/v2/users/delete', undefined, undefined, body); 878 | 879 | decoders.DeleteUsersResponse?.(response.body); 880 | 881 | return { ...response.body, metadata: response.metadata }; 882 | }; 883 | 884 | reactivateUsers = async ( 885 | request: ReactivateUsersRequest, 886 | ): Promise> => { 887 | const body = { 888 | user_ids: request?.user_ids, 889 | created_by_id: request?.created_by_id, 890 | restore_channels: request?.restore_channels, 891 | restore_messages: request?.restore_messages, 892 | }; 893 | 894 | const response = await this.sendRequest< 895 | StreamResponse 896 | >('POST', '/api/v2/users/reactivate', undefined, undefined, body); 897 | 898 | decoders.ReactivateUsersResponse?.(response.body); 899 | 900 | return { ...response.body, metadata: response.metadata }; 901 | }; 902 | 903 | restoreUsers = async ( 904 | request: RestoreUsersRequest, 905 | ): Promise> => { 906 | const body = { 907 | user_ids: request?.user_ids, 908 | }; 909 | 910 | const response = await this.sendRequest>( 911 | 'POST', 912 | '/api/v2/users/restore', 913 | undefined, 914 | undefined, 915 | body, 916 | ); 917 | 918 | decoders.Response?.(response.body); 919 | 920 | return { ...response.body, metadata: response.metadata }; 921 | }; 922 | 923 | unblockUsers = async ( 924 | request: UnblockUsersRequest, 925 | ): Promise> => { 926 | const body = { 927 | blocked_user_id: request?.blocked_user_id, 928 | user_id: request?.user_id, 929 | user: request?.user, 930 | }; 931 | 932 | const response = await this.sendRequest< 933 | StreamResponse 934 | >('POST', '/api/v2/users/unblock', undefined, undefined, body); 935 | 936 | decoders.UnblockUsersResponse?.(response.body); 937 | 938 | return { ...response.body, metadata: response.metadata }; 939 | }; 940 | 941 | deactivateUser = async ( 942 | request: DeactivateUserRequest & { user_id: string }, 943 | ): Promise> => { 944 | const pathParams = { 945 | user_id: request?.user_id, 946 | }; 947 | const body = { 948 | created_by_id: request?.created_by_id, 949 | mark_messages_deleted: request?.mark_messages_deleted, 950 | }; 951 | 952 | const response = await this.sendRequest< 953 | StreamResponse 954 | >( 955 | 'POST', 956 | '/api/v2/users/{user_id}/deactivate', 957 | pathParams, 958 | undefined, 959 | body, 960 | ); 961 | 962 | decoders.DeactivateUserResponse?.(response.body); 963 | 964 | return { ...response.body, metadata: response.metadata }; 965 | }; 966 | 967 | exportUser = async (request: { 968 | user_id: string; 969 | }): Promise> => { 970 | const pathParams = { 971 | user_id: request?.user_id, 972 | }; 973 | 974 | const response = await this.sendRequest>( 975 | 'GET', 976 | '/api/v2/users/{user_id}/export', 977 | pathParams, 978 | undefined, 979 | ); 980 | 981 | decoders.ExportUserResponse?.(response.body); 982 | 983 | return { ...response.body, metadata: response.metadata }; 984 | }; 985 | 986 | reactivateUser = async ( 987 | request: ReactivateUserRequest & { user_id: string }, 988 | ): Promise> => { 989 | const pathParams = { 990 | user_id: request?.user_id, 991 | }; 992 | const body = { 993 | created_by_id: request?.created_by_id, 994 | name: request?.name, 995 | restore_messages: request?.restore_messages, 996 | }; 997 | 998 | const response = await this.sendRequest< 999 | StreamResponse 1000 | >( 1001 | 'POST', 1002 | '/api/v2/users/{user_id}/reactivate', 1003 | pathParams, 1004 | undefined, 1005 | body, 1006 | ); 1007 | 1008 | decoders.ReactivateUserResponse?.(response.body); 1009 | 1010 | return { ...response.body, metadata: response.metadata }; 1011 | }; 1012 | } 1013 | -------------------------------------------------------------------------------- /src/gen/moderation/ModerationApi.ts: -------------------------------------------------------------------------------- 1 | import { BaseApi } from '../../BaseApi'; 2 | import { StreamResponse } from '../../types'; 3 | import { 4 | BanRequest, 5 | BanResponse, 6 | CheckRequest, 7 | CheckResponse, 8 | CustomCheckRequest, 9 | CustomCheckResponse, 10 | DeleteModerationConfigResponse, 11 | DeleteModerationTemplateResponse, 12 | FlagRequest, 13 | FlagResponse, 14 | GetConfigResponse, 15 | GetReviewQueueItemResponse, 16 | MuteRequest, 17 | MuteResponse, 18 | QueryFeedModerationTemplatesResponse, 19 | QueryModerationConfigsRequest, 20 | QueryModerationConfigsResponse, 21 | QueryModerationLogsRequest, 22 | QueryModerationLogsResponse, 23 | QueryReviewQueueRequest, 24 | QueryReviewQueueResponse, 25 | SubmitActionRequest, 26 | SubmitActionResponse, 27 | UnbanRequest, 28 | UnbanResponse, 29 | UnmuteRequest, 30 | UnmuteResponse, 31 | UpsertConfigRequest, 32 | UpsertConfigResponse, 33 | UpsertModerationTemplateRequest, 34 | UpsertModerationTemplateResponse, 35 | } from '../models'; 36 | import { decoders } from '../model-decoders'; 37 | 38 | export class ModerationApi extends BaseApi { 39 | ban = async (request: BanRequest): Promise> => { 40 | const body = { 41 | target_user_id: request?.target_user_id, 42 | banned_by_id: request?.banned_by_id, 43 | channel_cid: request?.channel_cid, 44 | ip_ban: request?.ip_ban, 45 | reason: request?.reason, 46 | shadow: request?.shadow, 47 | timeout: request?.timeout, 48 | banned_by: request?.banned_by, 49 | }; 50 | 51 | const response = await this.sendRequest>( 52 | 'POST', 53 | '/api/v2/moderation/ban', 54 | undefined, 55 | undefined, 56 | body, 57 | ); 58 | 59 | decoders.BanResponse?.(response.body); 60 | 61 | return { ...response.body, metadata: response.metadata }; 62 | }; 63 | 64 | check = async ( 65 | request: CheckRequest, 66 | ): Promise> => { 67 | const body = { 68 | config_key: request?.config_key, 69 | entity_creator_id: request?.entity_creator_id, 70 | entity_id: request?.entity_id, 71 | entity_type: request?.entity_type, 72 | config_team: request?.config_team, 73 | test_mode: request?.test_mode, 74 | user_id: request?.user_id, 75 | moderation_payload: request?.moderation_payload, 76 | options: request?.options, 77 | user: request?.user, 78 | }; 79 | 80 | const response = await this.sendRequest>( 81 | 'POST', 82 | '/api/v2/moderation/check', 83 | undefined, 84 | undefined, 85 | body, 86 | ); 87 | 88 | decoders.CheckResponse?.(response.body); 89 | 90 | return { ...response.body, metadata: response.metadata }; 91 | }; 92 | 93 | upsertConfig = async ( 94 | request: UpsertConfigRequest, 95 | ): Promise> => { 96 | const body = { 97 | key: request?.key, 98 | async: request?.async, 99 | team: request?.team, 100 | user_id: request?.user_id, 101 | ai_image_config: request?.ai_image_config, 102 | ai_text_config: request?.ai_text_config, 103 | ai_video_config: request?.ai_video_config, 104 | automod_platform_circumvention_config: 105 | request?.automod_platform_circumvention_config, 106 | automod_semantic_filters_config: request?.automod_semantic_filters_config, 107 | automod_toxicity_config: request?.automod_toxicity_config, 108 | aws_rekognition_config: request?.aws_rekognition_config, 109 | block_list_config: request?.block_list_config, 110 | bodyguard_config: request?.bodyguard_config, 111 | google_vision_config: request?.google_vision_config, 112 | rule_builder_config: request?.rule_builder_config, 113 | user: request?.user, 114 | velocity_filter_config: request?.velocity_filter_config, 115 | }; 116 | 117 | const response = await this.sendRequest< 118 | StreamResponse 119 | >('POST', '/api/v2/moderation/config', undefined, undefined, body); 120 | 121 | decoders.UpsertConfigResponse?.(response.body); 122 | 123 | return { ...response.body, metadata: response.metadata }; 124 | }; 125 | 126 | deleteConfig = async (request: { 127 | key: string; 128 | team?: string; 129 | }): Promise> => { 130 | const queryParams = { 131 | team: request?.team, 132 | }; 133 | const pathParams = { 134 | key: request?.key, 135 | }; 136 | 137 | const response = await this.sendRequest< 138 | StreamResponse 139 | >('DELETE', '/api/v2/moderation/config/{key}', pathParams, queryParams); 140 | 141 | decoders.DeleteModerationConfigResponse?.(response.body); 142 | 143 | return { ...response.body, metadata: response.metadata }; 144 | }; 145 | 146 | getConfig = async (request: { 147 | key: string; 148 | team?: string; 149 | }): Promise> => { 150 | const queryParams = { 151 | team: request?.team, 152 | }; 153 | const pathParams = { 154 | key: request?.key, 155 | }; 156 | 157 | const response = await this.sendRequest>( 158 | 'GET', 159 | '/api/v2/moderation/config/{key}', 160 | pathParams, 161 | queryParams, 162 | ); 163 | 164 | decoders.GetConfigResponse?.(response.body); 165 | 166 | return { ...response.body, metadata: response.metadata }; 167 | }; 168 | 169 | queryModerationConfigs = async ( 170 | request?: QueryModerationConfigsRequest, 171 | ): Promise> => { 172 | const body = { 173 | limit: request?.limit, 174 | next: request?.next, 175 | prev: request?.prev, 176 | user_id: request?.user_id, 177 | sort: request?.sort, 178 | filter: request?.filter, 179 | user: request?.user, 180 | }; 181 | 182 | const response = await this.sendRequest< 183 | StreamResponse 184 | >('POST', '/api/v2/moderation/configs', undefined, undefined, body); 185 | 186 | decoders.QueryModerationConfigsResponse?.(response.body); 187 | 188 | return { ...response.body, metadata: response.metadata }; 189 | }; 190 | 191 | customCheck = async ( 192 | request: CustomCheckRequest, 193 | ): Promise> => { 194 | const body = { 195 | entity_id: request?.entity_id, 196 | entity_type: request?.entity_type, 197 | flags: request?.flags, 198 | entity_creator_id: request?.entity_creator_id, 199 | user_id: request?.user_id, 200 | moderation_payload: request?.moderation_payload, 201 | user: request?.user, 202 | }; 203 | 204 | const response = await this.sendRequest< 205 | StreamResponse 206 | >('POST', '/api/v2/moderation/custom_check', undefined, undefined, body); 207 | 208 | decoders.CustomCheckResponse?.(response.body); 209 | 210 | return { ...response.body, metadata: response.metadata }; 211 | }; 212 | 213 | v2DeleteTemplate = async (): Promise< 214 | StreamResponse 215 | > => { 216 | const response = await this.sendRequest< 217 | StreamResponse 218 | >( 219 | 'DELETE', 220 | '/api/v2/moderation/feeds_moderation_template', 221 | undefined, 222 | undefined, 223 | ); 224 | 225 | decoders.DeleteModerationTemplateResponse?.(response.body); 226 | 227 | return { ...response.body, metadata: response.metadata }; 228 | }; 229 | 230 | v2QueryTemplates = async (): Promise< 231 | StreamResponse 232 | > => { 233 | const response = await this.sendRequest< 234 | StreamResponse 235 | >( 236 | 'GET', 237 | '/api/v2/moderation/feeds_moderation_template', 238 | undefined, 239 | undefined, 240 | ); 241 | 242 | decoders.QueryFeedModerationTemplatesResponse?.(response.body); 243 | 244 | return { ...response.body, metadata: response.metadata }; 245 | }; 246 | 247 | v2UpsertTemplate = async ( 248 | request: UpsertModerationTemplateRequest, 249 | ): Promise> => { 250 | const body = { 251 | name: request?.name, 252 | config: request?.config, 253 | }; 254 | 255 | const response = await this.sendRequest< 256 | StreamResponse 257 | >( 258 | 'POST', 259 | '/api/v2/moderation/feeds_moderation_template', 260 | undefined, 261 | undefined, 262 | body, 263 | ); 264 | 265 | decoders.UpsertModerationTemplateResponse?.(response.body); 266 | 267 | return { ...response.body, metadata: response.metadata }; 268 | }; 269 | 270 | flag = async ( 271 | request: FlagRequest, 272 | ): Promise> => { 273 | const body = { 274 | entity_id: request?.entity_id, 275 | entity_type: request?.entity_type, 276 | entity_creator_id: request?.entity_creator_id, 277 | reason: request?.reason, 278 | user_id: request?.user_id, 279 | custom: request?.custom, 280 | moderation_payload: request?.moderation_payload, 281 | user: request?.user, 282 | }; 283 | 284 | const response = await this.sendRequest>( 285 | 'POST', 286 | '/api/v2/moderation/flag', 287 | undefined, 288 | undefined, 289 | body, 290 | ); 291 | 292 | decoders.FlagResponse?.(response.body); 293 | 294 | return { ...response.body, metadata: response.metadata }; 295 | }; 296 | 297 | queryModerationLogs = async ( 298 | request?: QueryModerationLogsRequest, 299 | ): Promise> => { 300 | const body = { 301 | limit: request?.limit, 302 | next: request?.next, 303 | prev: request?.prev, 304 | user_id: request?.user_id, 305 | sort: request?.sort, 306 | filter: request?.filter, 307 | user: request?.user, 308 | }; 309 | 310 | const response = await this.sendRequest< 311 | StreamResponse 312 | >('POST', '/api/v2/moderation/logs', undefined, undefined, body); 313 | 314 | decoders.QueryModerationLogsResponse?.(response.body); 315 | 316 | return { ...response.body, metadata: response.metadata }; 317 | }; 318 | 319 | mute = async ( 320 | request: MuteRequest, 321 | ): Promise> => { 322 | const body = { 323 | target_ids: request?.target_ids, 324 | timeout: request?.timeout, 325 | user_id: request?.user_id, 326 | user: request?.user, 327 | }; 328 | 329 | const response = await this.sendRequest>( 330 | 'POST', 331 | '/api/v2/moderation/mute', 332 | undefined, 333 | undefined, 334 | body, 335 | ); 336 | 337 | decoders.MuteResponse?.(response.body); 338 | 339 | return { ...response.body, metadata: response.metadata }; 340 | }; 341 | 342 | queryReviewQueue = async ( 343 | request?: QueryReviewQueueRequest, 344 | ): Promise> => { 345 | const body = { 346 | limit: request?.limit, 347 | lock_count: request?.lock_count, 348 | lock_duration: request?.lock_duration, 349 | lock_items: request?.lock_items, 350 | next: request?.next, 351 | prev: request?.prev, 352 | stats_only: request?.stats_only, 353 | user_id: request?.user_id, 354 | sort: request?.sort, 355 | filter: request?.filter, 356 | user: request?.user, 357 | }; 358 | 359 | const response = await this.sendRequest< 360 | StreamResponse 361 | >('POST', '/api/v2/moderation/review_queue', undefined, undefined, body); 362 | 363 | decoders.QueryReviewQueueResponse?.(response.body); 364 | 365 | return { ...response.body, metadata: response.metadata }; 366 | }; 367 | 368 | getReviewQueueItem = async (request: { 369 | id: string; 370 | }): Promise> => { 371 | const pathParams = { 372 | id: request?.id, 373 | }; 374 | 375 | const response = await this.sendRequest< 376 | StreamResponse 377 | >('GET', '/api/v2/moderation/review_queue/{id}', pathParams, undefined); 378 | 379 | decoders.GetReviewQueueItemResponse?.(response.body); 380 | 381 | return { ...response.body, metadata: response.metadata }; 382 | }; 383 | 384 | submitAction = async ( 385 | request: SubmitActionRequest, 386 | ): Promise> => { 387 | const body = { 388 | action_type: request?.action_type, 389 | item_id: request?.item_id, 390 | user_id: request?.user_id, 391 | ban: request?.ban, 392 | custom: request?.custom, 393 | delete_activity: request?.delete_activity, 394 | delete_message: request?.delete_message, 395 | delete_reaction: request?.delete_reaction, 396 | delete_user: request?.delete_user, 397 | mark_reviewed: request?.mark_reviewed, 398 | unban: request?.unban, 399 | user: request?.user, 400 | }; 401 | 402 | const response = await this.sendRequest< 403 | StreamResponse 404 | >('POST', '/api/v2/moderation/submit_action', undefined, undefined, body); 405 | 406 | decoders.SubmitActionResponse?.(response.body); 407 | 408 | return { ...response.body, metadata: response.metadata }; 409 | }; 410 | 411 | unban = async ( 412 | request: UnbanRequest & { 413 | target_user_id: string; 414 | channel_cid?: string; 415 | created_by?: string; 416 | }, 417 | ): Promise> => { 418 | const queryParams = { 419 | target_user_id: request?.target_user_id, 420 | channel_cid: request?.channel_cid, 421 | created_by: request?.created_by, 422 | }; 423 | const body = { 424 | unbanned_by_id: request?.unbanned_by_id, 425 | unbanned_by: request?.unbanned_by, 426 | }; 427 | 428 | const response = await this.sendRequest>( 429 | 'POST', 430 | '/api/v2/moderation/unban', 431 | undefined, 432 | queryParams, 433 | body, 434 | ); 435 | 436 | decoders.UnbanResponse?.(response.body); 437 | 438 | return { ...response.body, metadata: response.metadata }; 439 | }; 440 | 441 | unmute = async ( 442 | request: UnmuteRequest, 443 | ): Promise> => { 444 | const body = { 445 | target_ids: request?.target_ids, 446 | user_id: request?.user_id, 447 | user: request?.user, 448 | }; 449 | 450 | const response = await this.sendRequest>( 451 | 'POST', 452 | '/api/v2/moderation/unmute', 453 | undefined, 454 | undefined, 455 | body, 456 | ); 457 | 458 | decoders.UnmuteResponse?.(response.body); 459 | 460 | return { ...response.body, metadata: response.metadata }; 461 | }; 462 | } 463 | -------------------------------------------------------------------------------- /src/gen/video/CallApi.ts: -------------------------------------------------------------------------------- 1 | import { VideoApi } from './VideoApi'; 2 | import { StreamResponse } from '../../types'; 3 | import { 4 | BlockUserRequest, 5 | BlockUserResponse, 6 | CollectUserFeedbackRequest, 7 | CollectUserFeedbackResponse, 8 | DeleteCallRequest, 9 | DeleteCallResponse, 10 | DeleteRecordingResponse, 11 | DeleteTranscriptionResponse, 12 | EndCallResponse, 13 | GetCallReportResponse, 14 | GetCallResponse, 15 | GetOrCreateCallRequest, 16 | GetOrCreateCallResponse, 17 | GoLiveRequest, 18 | GoLiveResponse, 19 | ListRecordingsResponse, 20 | ListTranscriptionsResponse, 21 | MuteUsersRequest, 22 | MuteUsersResponse, 23 | PinRequest, 24 | PinResponse, 25 | QueryCallParticipantsRequest, 26 | QueryCallParticipantsResponse, 27 | SendCallEventRequest, 28 | SendCallEventResponse, 29 | StartClosedCaptionsRequest, 30 | StartClosedCaptionsResponse, 31 | StartFrameRecordingRequest, 32 | StartFrameRecordingResponse, 33 | StartHLSBroadcastingResponse, 34 | StartRTMPBroadcastsRequest, 35 | StartRTMPBroadcastsResponse, 36 | StartRecordingRequest, 37 | StartRecordingResponse, 38 | StartTranscriptionRequest, 39 | StartTranscriptionResponse, 40 | StopAllRTMPBroadcastsResponse, 41 | StopClosedCaptionsRequest, 42 | StopClosedCaptionsResponse, 43 | StopFrameRecordingResponse, 44 | StopHLSBroadcastingResponse, 45 | StopLiveRequest, 46 | StopLiveResponse, 47 | StopRTMPBroadcastsRequest, 48 | StopRTMPBroadcastsResponse, 49 | StopRecordingResponse, 50 | StopTranscriptionRequest, 51 | StopTranscriptionResponse, 52 | UnblockUserRequest, 53 | UnblockUserResponse, 54 | UnpinRequest, 55 | UnpinResponse, 56 | UpdateCallMembersRequest, 57 | UpdateCallMembersResponse, 58 | UpdateCallRequest, 59 | UpdateCallResponse, 60 | UpdateUserPermissionsRequest, 61 | UpdateUserPermissionsResponse, 62 | } from '../models'; 63 | 64 | export class CallApi { 65 | constructor( 66 | protected videoApi: VideoApi, 67 | public readonly type: string, 68 | public readonly id: string, 69 | ) {} 70 | 71 | get = (request?: { 72 | members_limit?: number; 73 | ring?: boolean; 74 | notify?: boolean; 75 | video?: boolean; 76 | }): Promise> => { 77 | return this.videoApi.getCall({ id: this.id, type: this.type, ...request }); 78 | }; 79 | 80 | update = ( 81 | request?: UpdateCallRequest, 82 | ): Promise> => { 83 | return this.videoApi.updateCall({ 84 | id: this.id, 85 | type: this.type, 86 | ...request, 87 | }); 88 | }; 89 | 90 | getOrCreate = ( 91 | request?: GetOrCreateCallRequest, 92 | ): Promise> => { 93 | return this.videoApi.getOrCreateCall({ 94 | id: this.id, 95 | type: this.type, 96 | ...request, 97 | }); 98 | }; 99 | 100 | blockUser = ( 101 | request: BlockUserRequest, 102 | ): Promise> => { 103 | return this.videoApi.blockUser({ 104 | id: this.id, 105 | type: this.type, 106 | ...request, 107 | }); 108 | }; 109 | 110 | delete = ( 111 | request?: DeleteCallRequest, 112 | ): Promise> => { 113 | return this.videoApi.deleteCall({ 114 | id: this.id, 115 | type: this.type, 116 | ...request, 117 | }); 118 | }; 119 | 120 | sendCallEvent = ( 121 | request?: SendCallEventRequest, 122 | ): Promise> => { 123 | return this.videoApi.sendCallEvent({ 124 | id: this.id, 125 | type: this.type, 126 | ...request, 127 | }); 128 | }; 129 | 130 | collectUserFeedback = ( 131 | request: CollectUserFeedbackRequest, 132 | ): Promise> => { 133 | return this.videoApi.collectUserFeedback({ 134 | id: this.id, 135 | type: this.type, 136 | ...request, 137 | }); 138 | }; 139 | 140 | goLive = ( 141 | request?: GoLiveRequest, 142 | ): Promise> => { 143 | return this.videoApi.goLive({ id: this.id, type: this.type, ...request }); 144 | }; 145 | 146 | end = (): Promise> => { 147 | return this.videoApi.endCall({ id: this.id, type: this.type }); 148 | }; 149 | 150 | updateCallMembers = ( 151 | request?: UpdateCallMembersRequest, 152 | ): Promise> => { 153 | return this.videoApi.updateCallMembers({ 154 | id: this.id, 155 | type: this.type, 156 | ...request, 157 | }); 158 | }; 159 | 160 | muteUsers = ( 161 | request?: MuteUsersRequest, 162 | ): Promise> => { 163 | return this.videoApi.muteUsers({ 164 | id: this.id, 165 | type: this.type, 166 | ...request, 167 | }); 168 | }; 169 | 170 | queryCallParticipants = ( 171 | request?: QueryCallParticipantsRequest & { limit?: number }, 172 | ): Promise> => { 173 | return this.videoApi.queryCallParticipants({ 174 | id: this.id, 175 | type: this.type, 176 | ...request, 177 | }); 178 | }; 179 | 180 | videoPin = (request: PinRequest): Promise> => { 181 | return this.videoApi.videoPin({ id: this.id, type: this.type, ...request }); 182 | }; 183 | 184 | listRecordings = (): Promise> => { 185 | return this.videoApi.listRecordings({ id: this.id, type: this.type }); 186 | }; 187 | 188 | getCallReport = (request?: { 189 | session_id?: string; 190 | }): Promise> => { 191 | return this.videoApi.getCallReport({ 192 | id: this.id, 193 | type: this.type, 194 | ...request, 195 | }); 196 | }; 197 | 198 | startRTMPBroadcasts = ( 199 | request: StartRTMPBroadcastsRequest, 200 | ): Promise> => { 201 | return this.videoApi.startRTMPBroadcasts({ 202 | id: this.id, 203 | type: this.type, 204 | ...request, 205 | }); 206 | }; 207 | 208 | stopAllRTMPBroadcasts = (): Promise< 209 | StreamResponse 210 | > => { 211 | return this.videoApi.stopAllRTMPBroadcasts({ 212 | id: this.id, 213 | type: this.type, 214 | }); 215 | }; 216 | 217 | stopRTMPBroadcast = ( 218 | request: StopRTMPBroadcastsRequest & { name: string }, 219 | ): Promise> => { 220 | return this.videoApi.stopRTMPBroadcast({ 221 | id: this.id, 222 | type: this.type, 223 | ...request, 224 | }); 225 | }; 226 | 227 | startHLSBroadcasting = (): Promise< 228 | StreamResponse 229 | > => { 230 | return this.videoApi.startHLSBroadcasting({ id: this.id, type: this.type }); 231 | }; 232 | 233 | startClosedCaptions = ( 234 | request?: StartClosedCaptionsRequest, 235 | ): Promise> => { 236 | return this.videoApi.startClosedCaptions({ 237 | id: this.id, 238 | type: this.type, 239 | ...request, 240 | }); 241 | }; 242 | 243 | startFrameRecording = ( 244 | request?: StartFrameRecordingRequest, 245 | ): Promise> => { 246 | return this.videoApi.startFrameRecording({ 247 | id: this.id, 248 | type: this.type, 249 | ...request, 250 | }); 251 | }; 252 | 253 | startRecording = ( 254 | request?: StartRecordingRequest, 255 | ): Promise> => { 256 | return this.videoApi.startRecording({ 257 | id: this.id, 258 | type: this.type, 259 | ...request, 260 | }); 261 | }; 262 | 263 | startTranscription = ( 264 | request?: StartTranscriptionRequest, 265 | ): Promise> => { 266 | return this.videoApi.startTranscription({ 267 | id: this.id, 268 | type: this.type, 269 | ...request, 270 | }); 271 | }; 272 | 273 | stopHLSBroadcasting = (): Promise< 274 | StreamResponse 275 | > => { 276 | return this.videoApi.stopHLSBroadcasting({ id: this.id, type: this.type }); 277 | }; 278 | 279 | stopClosedCaptions = ( 280 | request?: StopClosedCaptionsRequest, 281 | ): Promise> => { 282 | return this.videoApi.stopClosedCaptions({ 283 | id: this.id, 284 | type: this.type, 285 | ...request, 286 | }); 287 | }; 288 | 289 | stopFrameRecording = (): Promise< 290 | StreamResponse 291 | > => { 292 | return this.videoApi.stopFrameRecording({ id: this.id, type: this.type }); 293 | }; 294 | 295 | stopLive = ( 296 | request?: StopLiveRequest, 297 | ): Promise> => { 298 | return this.videoApi.stopLive({ id: this.id, type: this.type, ...request }); 299 | }; 300 | 301 | stopRecording = (): Promise> => { 302 | return this.videoApi.stopRecording({ id: this.id, type: this.type }); 303 | }; 304 | 305 | stopTranscription = ( 306 | request?: StopTranscriptionRequest, 307 | ): Promise> => { 308 | return this.videoApi.stopTranscription({ 309 | id: this.id, 310 | type: this.type, 311 | ...request, 312 | }); 313 | }; 314 | 315 | listTranscriptions = (): Promise< 316 | StreamResponse 317 | > => { 318 | return this.videoApi.listTranscriptions({ id: this.id, type: this.type }); 319 | }; 320 | 321 | unblockUser = ( 322 | request: UnblockUserRequest, 323 | ): Promise> => { 324 | return this.videoApi.unblockUser({ 325 | id: this.id, 326 | type: this.type, 327 | ...request, 328 | }); 329 | }; 330 | 331 | videoUnpin = ( 332 | request: UnpinRequest, 333 | ): Promise> => { 334 | return this.videoApi.videoUnpin({ 335 | id: this.id, 336 | type: this.type, 337 | ...request, 338 | }); 339 | }; 340 | 341 | updateUserPermissions = ( 342 | request: UpdateUserPermissionsRequest, 343 | ): Promise> => { 344 | return this.videoApi.updateUserPermissions({ 345 | id: this.id, 346 | type: this.type, 347 | ...request, 348 | }); 349 | }; 350 | 351 | deleteRecording = (request: { 352 | session: string; 353 | filename: string; 354 | }): Promise> => { 355 | return this.videoApi.deleteRecording({ 356 | id: this.id, 357 | type: this.type, 358 | ...request, 359 | }); 360 | }; 361 | 362 | deleteTranscription = (request: { 363 | session: string; 364 | filename: string; 365 | }): Promise> => { 366 | return this.videoApi.deleteTranscription({ 367 | id: this.id, 368 | type: this.type, 369 | ...request, 370 | }); 371 | }; 372 | } 373 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | export type OmitTypeId = Omit; 2 | 3 | export interface ApiConfig { 4 | apiKey: string; 5 | token: string; 6 | baseUrl: string; 7 | /** The timeout for requests in milliseconds. The default is 3000. */ 8 | timeout: number; 9 | agent?: RequestInit['dispatcher']; 10 | } 11 | 12 | export interface RequestMetadata { 13 | responseHeaders: Headers; 14 | rateLimit: RateLimit; 15 | responseCode: number; 16 | clientRequestId: string; 17 | } 18 | 19 | export type StreamResponse = T & { 20 | metadata: RequestMetadata; 21 | }; 22 | 23 | export class StreamError extends Error { 24 | constructor( 25 | message: string, 26 | public metadata: Partial, 27 | public code?: number, 28 | errorOptions?: ErrorOptions, 29 | ) { 30 | super(message, errorOptions); 31 | } 32 | } 33 | 34 | export interface RateLimit { 35 | rateLimit?: number; 36 | rateLimitRemaining?: number; 37 | rateLimitReset?: Date; 38 | } 39 | 40 | interface BaseTokenPayload { 41 | user_id: string; 42 | exp: number; 43 | iat: number; 44 | call_cids?: string[]; 45 | } 46 | 47 | export type UserTokenPayload = BaseTokenPayload; 48 | 49 | export type CallTokenPayload = BaseTokenPayload & { 50 | call_cids: string[]; 51 | role?: string; 52 | }; 53 | -------------------------------------------------------------------------------- /src/utils/create-token.ts: -------------------------------------------------------------------------------- 1 | import jwt, { Secret, SignOptions } from 'jsonwebtoken'; 2 | 3 | export function JWTUserToken( 4 | apiSecret: Secret, 5 | payload: { 6 | user_id: string; 7 | exp: number; 8 | iat: number; 9 | call_cids?: string[]; 10 | } & { [key: string]: any }, 11 | ) { 12 | // make sure we return a clear error when jwt is shimmed (ie. browser build) 13 | if (jwt == null || jwt.sign == null) { 14 | throw Error( 15 | `Unable to find jwt crypto, if you are getting this error is probably because you are trying to generate tokens on browser or React Native (or other environment where crypto functions are not available). Please Note: token should only be generated server-side.`, 16 | ); 17 | } 18 | 19 | const opts: SignOptions = Object.assign({ 20 | algorithm: 'HS256', 21 | noTimestamp: true, 22 | }); 23 | 24 | if (payload.iat) { 25 | opts.noTimestamp = false; 26 | } 27 | return jwt.sign(payload, apiSecret, opts); 28 | } 29 | 30 | export function JWTServerToken( 31 | apiSecret: Secret, 32 | jwtOptions: SignOptions = {}, 33 | ) { 34 | const payload = { 35 | server: true, 36 | }; 37 | 38 | const opts: SignOptions = Object.assign( 39 | { algorithm: 'HS256', noTimestamp: true }, 40 | jwtOptions, 41 | ); 42 | return jwt.sign(payload, apiSecret, opts); 43 | } 44 | -------------------------------------------------------------------------------- /src/utils/rate-limit.ts: -------------------------------------------------------------------------------- 1 | import { RateLimit } from '../types'; 2 | 3 | export const getRateLimitFromResponseHeader = (responseHeaders: Headers) => { 4 | const rateLimit = responseHeaders.has('x-ratelimit-limit') 5 | ? +responseHeaders.get('x-ratelimit-limit')! 6 | : undefined; 7 | const rateLimitRemaining = responseHeaders.has('x-ratelimit-remaining') 8 | ? +responseHeaders.get('x-ratelimit-remaining')! 9 | : undefined; 10 | const rateLimitReset = responseHeaders.has('x-ratelimit-reset') 11 | ? new Date(+responseHeaders.get('x-ratelimit-reset')! * 1000) 12 | : undefined; 13 | 14 | const result: RateLimit = { 15 | rateLimit, 16 | rateLimitRemaining, 17 | rateLimitReset, 18 | }; 19 | 20 | return result; 21 | }; 22 | -------------------------------------------------------------------------------- /test-cleanup.js: -------------------------------------------------------------------------------- 1 | require("dotenv/config"); 2 | const { StreamClient } = require("./dist/index.cjs.js"); 3 | 4 | const apiKey = process.env.STREAM_API_KEY; 5 | const secret = process.env.STREAM_SECRET; 6 | 7 | const createTestClient = () => { 8 | return new StreamClient(apiKey, secret, { timeout: 10000 }); 9 | }; 10 | 11 | const client = createTestClient(); 12 | 13 | const cleanupBlockLists = async () => { 14 | const blockLists = (await client.listBlockLists()).blocklists; 15 | const customBlockLists = blockLists.filter((b) => 16 | b.name.startsWith("streamnodetest-F1"), 17 | ); 18 | 19 | await Promise.all( 20 | customBlockLists.map((b) => client.deleteBlockList({ name: b.name })), 21 | ); 22 | }; 23 | 24 | const cleanupCalls = async () => { 25 | const calls = (await client.video.queryCalls()).calls; 26 | const testCalls = Object.keys(calls).filter((t) => 27 | t.startsWith("callnodetest"), 28 | ); 29 | 30 | await Promise.all( 31 | testCalls.map((t) => 32 | client.video.call(t.call.type, t.call.id).delete({ hard: true }), 33 | ), 34 | ); 35 | }; 36 | 37 | const cleanupCallTypes = async () => { 38 | const callTypes = (await client.video.listCallTypes()).call_types; 39 | const customCallTypes = Object.keys(callTypes).filter( 40 | (t) => 41 | t.startsWith("streamnodetest") || 42 | t.startsWith("calltype") || 43 | t.startsWith("long_inactivity_timeout") || 44 | t.startsWith("frame_recording"), 45 | ); 46 | 47 | await Promise.all( 48 | customCallTypes.map((t) => client.video.deleteCallType({ name: t })), 49 | ); 50 | }; 51 | 52 | const cleanupExternalStorage = async () => { 53 | const storage = (await client.listExternalStorage()).external_storages; 54 | const customStorage = Object.keys(storage).filter((k) => 55 | k.startsWith("streamnodetest"), 56 | ); 57 | 58 | await Promise.all( 59 | customStorage.map((s) => client.deleteExternalStorage({ name: s })), 60 | ); 61 | }; 62 | 63 | const cleanUpChannelTypes = async () => { 64 | const channelTypes = (await client.chat.listChannelTypes()).channel_types; 65 | const customChannelTypes = Object.keys(channelTypes).filter((type) => 66 | type.startsWith("streamnodetest"), 67 | ); 68 | 69 | await Promise.all( 70 | customChannelTypes.map((channelType) => 71 | client.chat.deleteChannelType({ name: channelType }), 72 | ), 73 | ); 74 | }; 75 | 76 | const cleanUpChannels = async () => { 77 | const channels = ( 78 | await client.chat.queryChannels({ 79 | payload: { 80 | filter_conditions: { name: { $autocomplete: "streamnodetest" } }, 81 | }, 82 | }) 83 | ).channels; 84 | 85 | await Promise.all( 86 | channels.map((c) => 87 | client.chat 88 | .channel(c.channel.type, c.channel.id) 89 | .delete({ hardDelete: true }), 90 | ), 91 | ); 92 | }; 93 | 94 | const cleanUpCommands = async () => { 95 | const commands = (await client.chat.listCommands()).commands; 96 | const customCommands = commands.filter((c) => 97 | c.name.startsWith("stream-node-test-command"), 98 | ); 99 | 100 | await Promise.all( 101 | customCommands.map((c) => client.chat.deleteCommand({ name: c.name })), 102 | ); 103 | }; 104 | 105 | const cleanUpRoles = async () => { 106 | const roles = (await client.listRoles()).roles; 107 | const customRoles = roles.filter((r) => r.name.startsWith("streamnodetest")); 108 | 109 | let grants = {}; 110 | customRoles.forEach((r) => { 111 | grants[r.name] = []; 112 | }); 113 | await client.updateApp({ 114 | grants, 115 | }); 116 | 117 | await Promise.all( 118 | customRoles.map((r) => client.deleteRole({ name: r.name })), 119 | ); 120 | }; 121 | 122 | const cleanUpUsers = async () => { 123 | const users = ( 124 | await client.queryUsers({ 125 | payload: { 126 | filter_conditions: { name: { $autocomplete: "streamnodetest" } }, 127 | }, 128 | }) 129 | ).users; 130 | 131 | if (users.length > 0) { 132 | await client.deleteUsers({ 133 | user_ids: users.map((u) => u.id), 134 | user: "hard", 135 | }); 136 | } 137 | }; 138 | 139 | const cleanup = async () => { 140 | await cleanupBlockLists(); 141 | await cleanupCallTypes(); 142 | await cleanUpChannelTypes(); 143 | await cleanUpChannels(); 144 | await cleanUpCommands(); 145 | await cleanUpRoles(); 146 | await cleanUpUsers(); 147 | await cleanupExternalStorage(); 148 | await cleanupCalls(); 149 | }; 150 | 151 | cleanup().then(() => { 152 | console.log("cleanup done"); 153 | process.exit(); 154 | }); 155 | -------------------------------------------------------------------------------- /tsconfig.eslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": ["./src", "index.ts", "./__tests__"] 4 | } 5 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./dist", 4 | "module": "ES2020", 5 | "target": "ES2020", 6 | "lib": ["esnext"], 7 | "noEmitOnError": true, 8 | "noImplicitAny": true, 9 | "declaration": true, 10 | "moduleResolution": "node", 11 | "allowSyntheticDefaultImports": true, 12 | "esModuleInterop": true, 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "skipLibCheck": true, 16 | "strict": true, 17 | "strictNullChecks": true, 18 | "sourceMap": true, 19 | "inlineSources": true 20 | }, 21 | "include": ["./src", "index.ts"] 22 | } 23 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config'; 2 | 3 | export default defineConfig({ 4 | build: {}, 5 | test: { 6 | coverage: { 7 | reporter: ['lcov'], 8 | }, 9 | testTimeout: 35000, 10 | include: ['__tests__/**/*.test.ts'], 11 | includeSource: ['src/**/*.ts'], 12 | retry: 3, 13 | }, 14 | }); 15 | --------------------------------------------------------------------------------