├── .env ├── .eslintignore ├── .eslintrc ├── .gitattributes ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── develop-pr.yml │ ├── develop.yml │ ├── main-pr.yml │ ├── main.yml │ └── publish.yml ├── .gitignore ├── .husky ├── .gitignore ├── pre-commit └── prepare-commit-msg ├── .prettierrc ├── CHANGELOG.md ├── LICENSE ├── README.md ├── jest.config.json ├── package-lock.json ├── package.json ├── src ├── index.ts ├── modules │ ├── chat.ts │ ├── instances.ts │ └── sarufi.ts └── shared │ ├── helpers │ ├── error.helper.ts │ └── id.helper.ts │ └── interfaces │ ├── auth.interface.ts │ ├── bot.interface.ts │ ├── conversation.interface.ts │ └── shared.interface.ts ├── test ├── index.d.ts ├── mocks │ └── mocks.constants.ts ├── modules │ └── index.spec.ts └── shared │ ├── error.helper.spec.ts │ └── id.helper.spec.ts └── tsconfig.json /.env: -------------------------------------------------------------------------------- 1 | # Local env vars for debugging 2 | TS_NODE_IGNORE="false" 3 | TS_NODE_FILES="true" -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | src/types/global.d.ts -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "plugins": ["@typescript-eslint", "node", "prettier"], 5 | "parserOptions": { 6 | "sourceType": "module", 7 | "tsconfigRootDir": "./", 8 | "project": ["./tsconfig.json"] 9 | }, 10 | "extends": [ 11 | "eslint:recommended", 12 | "plugin:node/recommended", 13 | "plugin:@typescript-eslint/eslint-recommended", 14 | "plugin:@typescript-eslint/recommended", 15 | "plugin:@typescript-eslint/recommended-requiring-type-checking", 16 | "plugin:prettier/recommended" 17 | ], 18 | "rules": { 19 | "prettier/prettier": "warn", 20 | "node/no-missing-import": "off", 21 | "node/no-empty-function": "off", 22 | "node/no-unsupported-features/es-syntax": "off", 23 | "node/no-missing-require": "off", 24 | "node/shebang": "off", 25 | "@typescript-eslint/no-use-before-define": "off", 26 | "quotes": ["warn", "single", { "avoidEscape": true }], 27 | "node/no-unpublished-import": "off", 28 | "@typescript-eslint/no-unsafe-assignment": "off", 29 | "@typescript-eslint/no-var-requires": "off", 30 | "@typescript-eslint/ban-ts-comment": "off", 31 | "@typescript-eslint/no-explicit-any": "off" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Set the repository to show as TypeScript rather than JS in GitHub 2 | *.js linguist-detectable=false -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "🐛 Bug Report" 3 | about: Report a reproducible bug or regression. 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## Current Behavior 11 | 12 | 13 | 14 | ## Expected Behavior 15 | 16 | 17 | 18 | ## Steps to Reproduce the Problem 19 | 20 | 1. 21 | 1. 22 | 1. 23 | 24 | ## Environment 25 | 26 | - Version: 27 | - Platform: 28 | - Node.js Version: 29 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: ✨ Feature request 3 | about: Suggest an amazing new idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | --- 8 | 9 | ## Feature Request 10 | 11 | **Is your feature request related to a problem? Please describe.** 12 | 13 | 14 | 15 | **Describe the solution you'd like** 16 | 17 | 18 | 19 | **Describe alternatives you've considered** 20 | 21 | 22 | 23 | ## Are you willing to resolve this issue by submitting a Pull Request? 24 | 25 | 28 | 29 | - [ ] Yes, I have the time, and I know how to start. 30 | - [ ] Yes, I have the time, but I don't know how to start. I would need guidance. 31 | - [ ] No, I don't have the time, although I believe I could do it if I had the time... 32 | - [ ] No, I don't have the time and I wouldn't even know how to start. 33 | 34 | 37 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 7 | 8 | ### Description of change 9 | 10 | 23 | 24 | ### Pull-Request Checklist 25 | 26 | 31 | 32 | - [ ] Code is up-to-date with the `main` branch 33 | - [ ] `npm run lint` passes with this change 34 | - [ ] `npm run test` passes with this change 35 | - [ ] This pull request links relevant issues as `Fixes #0000` 36 | - [ ] There are new or updated unit tests validating the change 37 | - [ ] Documentation has been updated to reflect this change 38 | - [ ] The new commits follow conventions outlined in the [conventional commit spec](https://www.conventionalcommits.org/en/v1.0.0/) 39 | 40 | 43 | -------------------------------------------------------------------------------- /.github/workflows/develop-pr.yml: -------------------------------------------------------------------------------- 1 | name: CREATE DEVELOP PR 2 | 3 | on: 4 | push: 5 | 6 | jobs: 7 | build: 8 | runs-on: ubuntu-latest 9 | 10 | strategy: 11 | matrix: 12 | node-version: [15.x] 13 | steps: 14 | - uses: actions/checkout@v2 15 | - name: 🔀 16 | uses: BaharaJr/create-pr@0.0.1 17 | with: 18 | GITHUB_TOKEN: ${{secrets.TOKEN}} 19 | DESTINATION_BRANCH: develop 20 | KEYWORD: release 21 | -------------------------------------------------------------------------------- /.github/workflows/develop.yml: -------------------------------------------------------------------------------- 1 | name: RELEASE 2 | on: 3 | pull_request: 4 | branches: [develop] 5 | 6 | jobs: 7 | release: 8 | name: Release 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - name: 🚚 13 | uses: actions/checkout@v2 14 | with: 15 | fetch-depth: 0 16 | 17 | - name: 🧱 18 | uses: actions/setup-node@v2 19 | with: 20 | node-version: lts/* 21 | 22 | - name: ➕ 23 | run: npm ci 24 | 25 | - name: 💚 26 | run: npm run package 27 | -------------------------------------------------------------------------------- /.github/workflows/main-pr.yml: -------------------------------------------------------------------------------- 1 | name: CREATE RELEASE PR 2 | 3 | on: 4 | push: 5 | branches: [develop] 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | 11 | strategy: 12 | matrix: 13 | node-version: [15.x] 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: 🔀 17 | uses: BaharaJr/create-pr@0.0.1 18 | with: 19 | GITHUB_TOKEN: ${{secrets.TOKEN}} 20 | DESTINATION_BRANCH: main 21 | KEYWORD: build 22 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: RELEASE 2 | on: 3 | pull_request: 4 | branches: [main] 5 | 6 | jobs: 7 | release: 8 | name: Release 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - name: 🚚 13 | uses: actions/checkout@v2 14 | with: 15 | fetch-depth: 0 16 | 17 | - name: 🧱 18 | uses: actions/setup-node@v2 19 | with: 20 | node-version: lts/* 21 | 22 | - name: ➕ 23 | run: npm i 24 | 25 | - name: 💚 26 | run: npm run package 27 | 28 | - name: 🚀 29 | env: 30 | GITHUB_TOKEN: ${{ secrets.TOKEN }} 31 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 32 | run: npx semantic-release 33 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish to NPM 2 | 3 | on: 4 | push: 5 | branches: 6 | - 'main' 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: 🚚 13 | uses: actions/checkout@v2 14 | - name: set global attributes 15 | run: | 16 | git config --global user.email "neicoreadams@gmail.com" 17 | git config --global user.name "neicore" 18 | git remote set-url origin https://x-access-token:${{ secrets.TOKEN }}@github.com/${{ github.repository }} 19 | 20 | - name: Setup Node 21 | uses: actions/setup-node@v2 22 | with: 23 | node-version: '14.x' 24 | registry-url: 'https://registry.npmjs.org' 25 | - name: Install dependencies and build 🔧 26 | run: npm install && npm run build 27 | - name: Publish package on NPM 📦 28 | run: npm publish --access public 29 | env: 30 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 31 | # update version 32 | - name: install jq 33 | run: | 34 | sudo apt-get update 35 | sudo apt-get -y install jq 36 | - uses: actions/checkout@v2 37 | with: 38 | ref: develop 39 | token: ${{ secrets.TOKEN }} 40 | fetch-depth: 0 41 | - name: update develop branch 42 | run: | 43 | git checkout develop 44 | git config pull.rebase false 45 | git pull origin develop --allow-unrelated-histories 46 | 47 | - name: get version 48 | run: | 49 | PACKAGE_VERSION=$(cat ./package.json | jq '.version' | tr -d '"') 50 | echo "::set-output name=PACKAGE_VERSION::$(cat ./package.json | jq '.version' | tr -d '"')" 51 | id: version 52 | - name: bump-version 53 | id: bump_version 54 | uses: flexcodelabs/bump-version@0.0.2 55 | with: 56 | GITHUB_TOKEN: ${{secrets.TOKEN}} 57 | PACKAGE_VERSION: ${{ steps.version.outputs.PACKAGE_VERSION }} 58 | DELETE_BRANCH: false 59 | CHANGELOG_PATH: ./CHANGELOG.md 60 | PACKAGE_JSON_PATH: ./package.json 61 | # Commit and push the latest version 62 | - name: update develop branch 63 | run: | 64 | git checkout develop 65 | git config pull.rebase false 66 | git add ./package.json 67 | git add ./CHANGELOG.md 68 | git commit -m "Skip CI: update package version and changelog" 69 | git pull origin develop --allow-unrelated-histories 70 | git push origin develop 71 | 72 | 73 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # Snowpack dependency directory (https://snowpack.dev/) 45 | web_modules/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env.test 73 | .env.local 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | .parcel-cache 78 | 79 | # Next.js build output 80 | .next 81 | out 82 | 83 | # Nuxt.js build / generate output 84 | .nuxt 85 | dist 86 | 87 | # Gatsby files 88 | .cache/ 89 | # Comment in the public line in if your project uses Gatsby and not Next.js 90 | # https://nextjs.org/blog/next-9-1#public-directory-support 91 | # public 92 | 93 | # vuepress build output 94 | .vuepress/dist 95 | 96 | # Serverless directories 97 | .serverless/ 98 | 99 | # FuseBox cache 100 | .fusebox/ 101 | 102 | # DynamoDB Local files 103 | .dynamodb/ 104 | 105 | # TernJS port file 106 | .tern-port 107 | 108 | # Stores VSCode versions used for testing VSCode extensions 109 | .vscode-test 110 | 111 | # yarn v2 112 | .yarn/cache 113 | .yarn/unplugged 114 | .yarn/build-state.yml 115 | .yarn/install-state.gz 116 | .pnp.* 117 | 118 | # Compiled code 119 | lib/ 120 | -------------------------------------------------------------------------------- /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx lint-staged 5 | -------------------------------------------------------------------------------- /.husky/prepare-commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | exec suply supply typo in README. @[`Alfeo`](https://github.com/Pheogrammer) 72 | - Improve README JSON code blocks. @[`Alpha`](https://github.com/alphaolomi) 73 | - Fix wether whether typo in README. @[`Shanas`](https://github.com/mrshanas) 74 | 75 | ## [0.0.3-Beta] - 2022-09-30 76 | 77 | ### Added 78 | 79 | - Added axios xhr adaptor custom typings for mocking tests. 80 | - Add optional token to every request for persisted tokens. 81 | 82 | ### Changed 83 | 84 | - Fixed error with failed typings for chat method on bot response. 85 | - Fix typo in README file and note for optional url. 86 | - Update README file for changed getBots request parameters and new added features. 87 | 88 | ## [0.0.2-Beta] - 2022-09-30 89 | 90 | ### Added 91 | 92 | - Added all Sarufi features. 93 | - Added tests for all methods and helpers. 94 | 95 | ## [0.0.5] - 2023-05-24 96 | 97 | ### Changed 98 | 99 | - Change auth URL 100 | - Bump package version 101 | 102 | ### Added 103 | 104 | - Build to CJS 105 | - Remove external dependencies 106 | 107 | ## [0.0.6] - 2023-06-28 108 | 109 | ### Changed 110 | 111 | - Change authentication approach to api_key 112 | - Update README file explaining new authentication approach (api_key) 113 | - Bump package version 114 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Flexcode Labs 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NODEJS SARUFI SDK 2 | 3 | ![TESTS](https://github.com/flexcodelabs/sarufi/actions/workflows/develop.yml/badge.svg) 4 | ![RELEASES](https://github.com/flexcodelabs/sarufi/actions/workflows/main.yml/badge.svg) 5 | ![DEVELOP PR](https://github.com/flexcodelabs/sarufi/actions/workflows/develop-pr.yml/badge.svg) 6 | ![RELEASE PR](https://github.com/flexcodelabs/sarufi/actions/workflows/main-pr.yml/badge.svg) 7 | 8 | Sarufi NodeJS SDK to help you interact with SARUFI platform inspired by [Python Sarufi SDK](https://github.com/Neurotech-HQ/sarufi-python-sdk) 9 | 10 | ## Table of Contents 11 | 12 | 1. [Installation](#installation-and-use) 13 | 1.1 [Installation](#installation) 14 | 2. [Use](#use) 15 | 2.1. [Get your api key](#get-your-api-key) 16 | 2.1. [Create an empty chatbot](#create-an-empty-chatbot) 17 | 2.3. [Updating your bot](#updating-your-bot) 18 | 2.4. [Get bot by id](#get-bot-by-id) 19 | 2.5. [Get bots](#get-bots) 20 | 2.6. [Delete bot](#delete-bot) 21 | 2.7. [Start conversation](#start-conversation) 22 | 2.8. [All responses details](#all-responses-have) 23 | 24 | # 25 | 26 | ## Installation and Use 27 | 28 | ### Installation 29 | 30 | From terminal in the root directory of your project, run 31 | 32 | ``` 33 | npm i sarufi 34 | ``` 35 | 36 | ### Use 37 | 38 | ```js 39 | import sarufi from sarufi 40 | ``` 41 | 42 | ### Get your api key 43 | 44 | Head to [Sarufi](https://www.sarufi.io/profile?tab=authorization), copy your api_key and login: 45 | 46 | ```js 47 | sarufi.login({ api_key: YOUR_API_KEY }) 48 | ``` 49 | 50 | ### Create an Empty chatbot 51 | 52 | We supply chatbot details 53 | 54 | > Request payload 55 | 56 | ```JSON 57 | { 58 | "name": "YOUR AWESOME BOT NAME", 59 | "description": "PUT DESCRIPTION HERE", 60 | "industry": "YOUR BOT INDUSTRY", 61 | "intents": {}, 62 | "flows": {}, 63 | } 64 | ``` 65 | 66 | Then we call 67 | 68 | ```JS 69 | // call this first if you haven't logged in. 70 | sarufi.login({ api_key: YOUR_API_KEY }) 71 | 72 | sarufi.createBot({bot: REQUEST PAYLOAD}) 73 | ``` 74 | 75 | `NB:` For detailed description of intents and flows to be used in conversation, refer to [Python Sarufi SDK Docs](https://docs.sarufi.io/docs/Getting%20started%20/create-a-simple-chatbot#help-me-order-a-pizza-intent) 76 | 77 | > Response for successful bot creation 78 | 79 | ```JSONC 80 | { 81 | "success": true, 82 | "bot": { "name": "YOUR AWESOME BOT NAME", 83 | "description": "PUT DESCRIPTION HERE", 84 | "user_id": "YOUR ID", 85 | "industry": "YOUR BOT INDUSTRY", 86 | "intents": {}, //intents if they were supplied 87 | "flows": {}, //flows if they were supplied 88 | "updated_at": "DATE THE BOT WAS LAST UPDATED", 89 | "id": "BOT ID", 90 | "model_name": "BOT MODEL NAME", 91 | "created_at": "DATE THE BOT WAS CREATED" 92 | }, 93 | "chat": "({message: string, chat_id?: uknown}) => RETURNS CHAT RESPONSE" //A callable method that starts a chat with your bot, it takes in a string message and optional chat ID 94 | } 95 | ``` 96 | 97 | ### Updating your bot 98 | 99 | Updating a bot, takes in the same arguments as creating a bot with addition of bot id 100 | 101 | > Request 102 | 103 | ```JS 104 | 105 | // api_key is optional 106 | sarufi.updateBot({bot: REQUEST PAYLOAD, id: YOUR BOT ID, api_key: YOUR_API_KEY}) 107 | 108 | ``` 109 | 110 | > Response on success, is the same as the response for creating a bot 111 | 112 | ### Get bot by id 113 | 114 | We call the following method on `sarufi` and pass the bot id 115 | 116 | > Request 117 | 118 | ```JS 119 | // api_key is optional 120 | sarufi.getBot({id: BOT ID, api_key: YOUR_API_KEY}) 121 | 122 | ``` 123 | 124 | > Response on success, is the same as the response for creating and updating a bot 125 | 126 | ### Get bots 127 | 128 | We call the following method on `sarufi` and pass the bot id 129 | 130 | > Request 131 | 132 | ```JS 133 | //For version 0.0.1-Beta 134 | sarufi.getBots() 135 | 136 | //For versions 0.0.2-Beta and above, 137 | sarufi.getBots({api_key: YOUR_API_KEY}) //This accepts optional paramemters url and api key for persisted authorization tokens. 138 | 139 | ``` 140 | 141 | > Response on success 142 | 143 | ```JSONC 144 | { 145 | "success": true, 146 | "bots": [] // Array of all bots you created 147 | } 148 | 149 | ``` 150 | 151 | ### Delete bot 152 | 153 | We call the following method on `sarufi` and pass the bot id 154 | 155 | ```JS 156 | // api_key is optional 157 | sarufi.deleteBot({id: BOT ID, api_key: YOUR_API_KEY}) 158 | 159 | ``` 160 | 161 | > Response on success 162 | 163 | ```JSON 164 | { 165 | "success": true, 166 | "message": "A MESSAGE STATING THAT YOUR BOT HAS BEEN DELETED" 167 | } 168 | ``` 169 | 170 | ### Start conversation 171 | 172 | There are two methods for this, i.e 173 | 174 | 1. bot.chat() this requires us to get a bot we want to have a conversation with and calling a chat method 175 | 2. sarufi.chat() this requires a required message, required bot_id and an optional chat_id as request arguments 176 | 177 | ```JS 178 | // api_key is optional 179 | 180 | // bot.chat() 181 | const bot = await sarufi.getBot({id: 'OUR BOT ID', api_key: YOUR_API_KEY}) 182 | await bot.chat({message: 'Yooh', channel: 'whatsapp'}) //channel is optional (Default is general) 183 | 184 | //sarufi.chat() 185 | await sarufi.chat({message: 'Hey', bot_id: bot.id, chat_id: 'HEYOO', api_key: YOUR_API_KEY, channel: 'whatsapp'}) 186 | 187 | ``` 188 | 189 | > Response on success 190 | 191 | ```JSONC 192 | // General Channel 193 | { 194 | "message": string| number | unknown | [string] | Record | [Record], 195 | "success": true, 196 | "memory": { [key: string]: string | unknown}, 197 | [key: string]: string | unknown 198 | } 199 | 200 | // Whatsapp Channel 201 | { 202 | "actions": Array<{ 203 | "send_message"?: string[]; 204 | "send_button"?: any; 205 | "send_reply_button"?: { 206 | "type": string; 207 | "body": { "text": string }; 208 | "action": any 209 | }; 210 | "send_image"?: Array<{ 211 | "link": string; 212 | "caption": string 213 | }>; 214 | "send_audio"?: any; 215 | "send_videos"?: any; 216 | }>;, 217 | "success": true, 218 | "memory": { [key: string]: string | unknown}, 219 | } 220 | ``` 221 | 222 | ### All Responses have 223 | 224 | 1. `Success` property that shows whether or not the request was successful 225 | 2. For failed requests, the response's success property will be false and additional properties for tracing will be added to the response object 226 | 227 | > Example of failed request 228 | 229 | ```JSONC 230 | { 231 | "success": false, 232 | "message": "MESSAGE", //an error message explaining the error 233 | "code": "string", 234 | "status": "NUMBER", 235 | } 236 | ``` 237 | 238 | Although an error response can have more than those properties, when the status is 500, that will denote an error occured within the sarufi otherwise it will be from an error originating from the remote sarufi server. 239 | 240 | ### `All requests have an optional url property that can be passed in case the url changes in the future` 241 | 242 | `NB`: For detailed documentation, please visit the [Python Sarufi SDK Docs](https://docs.sarufi.io/) 243 | 244 | ### Developed and Maintained with ❤️ at [Flexcode Labs](https://flexcodelabs.com) 245 | -------------------------------------------------------------------------------- /jest.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "preset": "ts-jest", 3 | "testEnvironment": "node", 4 | "testMatch": ["**/test/**/*.spec.ts"], 5 | "collectCoverageFrom": [ 6 | "/src/**/*.ts", 7 | "!/src/types/**/*.ts" 8 | ], 9 | "globals": { 10 | "ts-jest": { 11 | "diagnostics": false, 12 | "isolatedModules": true 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sarufi", 3 | "version": "0.1.5", 4 | "description": "Sarufi NodeJs SDK to help you interact with Sarufi Conversational API", 5 | "main": "lib/index.js", 6 | "types": "lib/index.d.ts", 7 | "files": [ 8 | "lib/**/*" 9 | ], 10 | "scripts": { 11 | "prebuild": "npm run clean", 12 | "build": "tsc && rm -rf ./lib/test && cp -r ./lib/src/* ./lib/ && rm -rf ./lib/src", 13 | "package": "npm run build -m && ncc build && cd ./lib && find . -name '*.js' -type f -delete && cd ../ && cp -r ./lib/* ./dist/ && rm -rf ./lib && mv dist lib", 14 | "clean": "rm -rf ./lib/", 15 | "cm": "cz", 16 | "release": "npm run package && npm publish", 17 | "lint": "eslint ./src/ --fix", 18 | "prepare": "husky install", 19 | "semantic-release": "semantic-release", 20 | "test:watch": "jest --watch", 21 | "test": "jest --coverage", 22 | "typecheck": "tsc --noEmit" 23 | }, 24 | "repository": { 25 | "type": "git", 26 | "url": "git+https://github.com/flexcodelabs/sarufi.git" 27 | }, 28 | "license": "MIT", 29 | "author": { 30 | "name": "Flexcode Labs", 31 | "email": "hi@flexcodelabs.com", 32 | "url": "https://github.com/flexcodelabs" 33 | }, 34 | "engines": { 35 | "node": ">=12.0" 36 | }, 37 | "keywords": [ 38 | "sarufi", 39 | "sdk", 40 | "nodejs", 41 | "flexcodelabs", 42 | "flexcode labs" 43 | ], 44 | "bugs": { 45 | "url": "https://github.com/flexcodelabs/sarufi/issues" 46 | }, 47 | "homepage": "https://github.com/flexcodelabs/sarufi#readme", 48 | "devDependencies": { 49 | "@ryansonshine/commitizen": "^4.2.8", 50 | "@ryansonshine/cz-conventional-changelog": "^3.3.4", 51 | "@types/jest": "^27.5.2", 52 | "@types/node": "^12.20.11", 53 | "@typescript-eslint/eslint-plugin": "^4.22.0", 54 | "@typescript-eslint/parser": "^4.22.0", 55 | "@vercel/ncc": "^0.36.1", 56 | "axios": "^1.4.0", 57 | "conventional-changelog-conventionalcommits": "^5.0.0", 58 | "eslint": "^7.25.0", 59 | "eslint-config-prettier": "^8.3.0", 60 | "eslint-plugin-node": "^11.1.0", 61 | "eslint-plugin-prettier": "^3.4.0", 62 | "http-request-mock": "^1.8.2", 63 | "husky": "^6.0.0", 64 | "jest": "^27.2.0", 65 | "lint-staged": "^10.5.4", 66 | "prettier": "^2.2.1", 67 | "semantic-release": "^19.0.2", 68 | "ts-jest": "^27.0.5", 69 | "ts-node": "^10.2.1", 70 | "typescript": "^4.2.4" 71 | }, 72 | "config": { 73 | "commitizen": { 74 | "path": "./node_modules/@ryansonshine/cz-conventional-changelog" 75 | } 76 | }, 77 | "lint-staged": { 78 | "*.ts": "eslint --cache --cache-location .eslintcache --fix" 79 | }, 80 | "release": { 81 | "branches": [ 82 | "main" 83 | ], 84 | "plugins": [ 85 | [ 86 | "@semantic-release/commit-analyzer", 87 | { 88 | "preset": "conventionalcommits", 89 | "releaseRules": [ 90 | { 91 | "type": "build", 92 | "scope": "deps", 93 | "release": "patch" 94 | } 95 | ] 96 | } 97 | ], 98 | [ 99 | "@semantic-release/release-notes-generator", 100 | { 101 | "preset": "conventionalcommits", 102 | "presetConfig": { 103 | "types": [ 104 | { 105 | "type": "feat", 106 | "section": "Features" 107 | }, 108 | { 109 | "type": "fix", 110 | "section": "Bug Fixes" 111 | }, 112 | { 113 | "type": "build", 114 | "section": "Dependencies and Other Build Updates", 115 | "hidden": false 116 | } 117 | ] 118 | } 119 | } 120 | ], 121 | "@semantic-release/npm", 122 | "@semantic-release/github" 123 | ] 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import * as sarufi from './modules/instances'; 2 | export default sarufi; 3 | -------------------------------------------------------------------------------- /src/modules/chat.ts: -------------------------------------------------------------------------------- 1 | import { Bot } from '../shared/interfaces/bot.interface'; 2 | import { 3 | ConversationInput, 4 | ConversationResponse, 5 | WhatsappConversationResponse, 6 | } from '../shared/interfaces/conversation.interface'; 7 | import axios, { AxiosError } from 'axios'; 8 | import { ErrorResponse } from '../shared/interfaces/shared.interface'; 9 | import { sanitizeErrorResponse } from '../shared/helpers/error.helper'; 10 | import { id } from '../shared/helpers/id.helper'; 11 | 12 | export class ChatConversation { 13 | constructor( 14 | private bot: Bot, 15 | private url: string, 16 | private token: string | undefined 17 | ) {} 18 | 19 | /** 20 | * A method to start chat with your bot 21 | * 22 | * @param data with message and optional chat_id, if chat id is not supplied, a unique string will be used 23 | */ 24 | chat = async ( 25 | data: ConversationInput 26 | ): Promise< 27 | ConversationResponse | WhatsappConversationResponse | ErrorResponse 28 | > => { 29 | if (this.token) { 30 | return this.startChat(this.token, { 31 | ...data, 32 | channel: data.channel ?? 'general', 33 | }); 34 | } 35 | return { 36 | success: false, 37 | bot: undefined, 38 | message: 'Unauthorized', 39 | }; 40 | }; 41 | 42 | private startChat = async ( 43 | token: string, 44 | data: ConversationInput 45 | ): Promise< 46 | ConversationResponse | WhatsappConversationResponse | ErrorResponse 47 | > => { 48 | let url = `${this.url}/conversation`; 49 | console.log(data); 50 | if (data.channel?.toLowerCase() === 'whatsapp') { 51 | url = `${url}/whatsapp`; 52 | } 53 | 54 | try { 55 | const response: ConversationResponse | WhatsappConversationResponse = ( 56 | await axios.post( 57 | url, 58 | { 59 | ...data, 60 | bot_id: this.bot.id, 61 | chat_id: data.chat_id || id, 62 | }, 63 | { headers: { Authorization: `Bearer ${token}` } } 64 | ) 65 | ).data; 66 | return { ...response, success: true }; 67 | } catch (e) { 68 | return sanitizeErrorResponse(e as AxiosError); 69 | } 70 | }; 71 | } 72 | -------------------------------------------------------------------------------- /src/modules/instances.ts: -------------------------------------------------------------------------------- 1 | import { Login, LoginResponse } from '../shared/interfaces/auth.interface'; 2 | import { 3 | BotResponse, 4 | BotsResponse, 5 | CreateBot, 6 | DeleteBot, 7 | GetBot, 8 | GetBots, 9 | UpdateBot, 10 | } from '../shared/interfaces/bot.interface'; 11 | import { 12 | ChatInput, 13 | ConversationResponse, 14 | WhatsappConversationResponse, 15 | } from '../shared/interfaces/conversation.interface'; 16 | import { ErrorResponse } from '../shared/interfaces/shared.interface'; 17 | import { Sarufi } from './sarufi'; 18 | 19 | /** 20 | * 21 | * @param data payload to login you in and interact with your bot, an api key from sarufi dashboard and an optional url 22 | */ 23 | export const login = async ( 24 | data: Login 25 | ): Promise => { 26 | const sarufi = new Sarufi(data.url); 27 | return await sarufi.login({ 28 | api_key: data.api_key, 29 | }); 30 | }; 31 | 32 | /** 33 | * A method to create a new bot 34 | * 35 | * @param data payload to create a bot with bot property as the DTO, an optional url and an optional token 36 | */ 37 | 38 | export const createBot = async ( 39 | data: CreateBot 40 | ): Promise => { 41 | const sarufi = new Sarufi(data?.url, data?.api_key); 42 | return await sarufi.createBot(data.bot); 43 | }; 44 | 45 | export const getBots = async ( 46 | data?: GetBots 47 | ): Promise => { 48 | const sarufi = new Sarufi(data?.url, data?.api_key); 49 | return await sarufi.getBots(); 50 | }; 51 | 52 | export const getBot = async ( 53 | data: GetBot 54 | ): Promise => { 55 | const sarufi = new Sarufi(data?.url, data?.api_key); 56 | return await sarufi.getBot(data.id); 57 | }; 58 | 59 | export const updateBot = async ( 60 | data: UpdateBot 61 | ): Promise => { 62 | const sarufi = new Sarufi(data?.url, data?.api_key); 63 | return await sarufi.updateBot(data.id, data.bot); 64 | }; 65 | 66 | export const deleteBot = async ( 67 | data: GetBot 68 | ): Promise => { 69 | const sarufi = new Sarufi(data?.url, data?.api_key); 70 | return await sarufi.deleteBot(data.id); 71 | }; 72 | 73 | export const chat = async ( 74 | data: ChatInput 75 | ): Promise< 76 | ErrorResponse | ConversationResponse | WhatsappConversationResponse 77 | > => { 78 | const sarufi = new Sarufi(data?.url, data?.api_key); 79 | return await sarufi.chat({ 80 | message: data.message, 81 | bot_id: data.bot_id, 82 | chat_id: data.chat_id, 83 | channel: data.channel ?? 'general', 84 | }); 85 | }; 86 | -------------------------------------------------------------------------------- /src/modules/sarufi.ts: -------------------------------------------------------------------------------- 1 | import axios, { AxiosError } from 'axios'; 2 | import { sanitizeErrorResponse } from '../shared/helpers/error.helper'; 3 | import { id } from '../shared/helpers/id.helper'; 4 | import { Login, LoginResponse } from '../shared/interfaces/auth.interface'; 5 | import { 6 | BotRequest, 7 | BotResponse, 8 | BotsResponse, 9 | DeleteBot, 10 | } from '../shared/interfaces/bot.interface'; 11 | import { 12 | ChatInput, 13 | ConversationInput, 14 | ConversationResponse, 15 | WhatsappConversationResponse, 16 | } from '../shared/interfaces/conversation.interface'; 17 | import { ErrorResponse } from '../shared/interfaces/shared.interface'; 18 | import { ChatConversation } from './chat'; 19 | 20 | declare let global: { 21 | api_key: string | undefined; 22 | url: string | undefined; 23 | }; 24 | 25 | export class Sarufi { 26 | constructor(private url?: string, private api_key?: string) { 27 | global.url = this.url; 28 | if (this.api_key) { 29 | global.api_key = this.api_key; 30 | } 31 | } 32 | private BASE_DOMAIN = global.url || 'https://developers.sarufi.io'; 33 | 34 | login = async (data: Login): Promise => { 35 | if (!global?.api_key) { 36 | global.api_key = data.api_key; 37 | } 38 | return { api_key: data.api_key }; 39 | // throw new Error( 40 | // 'Method deprecated. Get a token from the website dashboard' 41 | // ); 42 | // try { 43 | // const loggedInUser: LoginResponse = ( 44 | // await axios.post(`${this.BASE_DOMAIN}/api/api_key`, data) 45 | // ).data; 46 | // if (global) { 47 | // global.api_key = loggedInUser.api_key; 48 | // } 49 | // return { ...loggedInUser, success: true }; 50 | // } catch (error) { 51 | // return sanitizeErrorResponse(error as AxiosError); 52 | // } 53 | }; 54 | 55 | createBot = async (bot: BotRequest): Promise => { 56 | if (global?.api_key) { 57 | return await this.createUserBot(bot, global?.api_key); 58 | } 59 | return { success: false, bot: undefined, message: 'Unauthorized' }; 60 | }; 61 | 62 | getBots = async (): Promise => { 63 | if (global?.api_key) { 64 | return await this.getUserBots(global?.api_key); 65 | } 66 | return { success: false, bots: [], message: 'Unauthorized' }; 67 | }; 68 | 69 | getBot = async (id: number): Promise => { 70 | if (global?.api_key && id) { 71 | return await this.getUserBot(global?.api_key, id); 72 | } 73 | return { 74 | success: false, 75 | bots: undefined, 76 | message: global?.api_key ? 'Unauthorized' : 'Bot ID not supplied', 77 | }; 78 | }; 79 | 80 | updateBot = async ( 81 | id: number, 82 | bot: BotRequest 83 | ): Promise => { 84 | if (global?.api_key && id) { 85 | return await this.updateUserBot(global?.api_key, id, bot); 86 | } 87 | return { 88 | success: false, 89 | bot: undefined, 90 | message: global?.api_key ? 'Unauthorized' : 'Bot ID not supplied', 91 | }; 92 | }; 93 | 94 | deleteBot = async (id: number): Promise => { 95 | if (global?.api_key && id) { 96 | return await this.deleteUserBot(global?.api_key, id); 97 | } 98 | return { 99 | success: false, 100 | bot: undefined, 101 | message: global?.api_key ? 'Unauthorized' : 'Bot ID not supplied', 102 | }; 103 | }; 104 | 105 | chat = async ( 106 | data: ChatInput 107 | ): Promise< 108 | ConversationResponse | WhatsappConversationResponse | ErrorResponse 109 | > => { 110 | if (global.api_key) { 111 | return this.startChat(data, global.api_key); 112 | } 113 | return { 114 | success: false, 115 | bot: undefined, 116 | message: global?.api_key ? 'Unauthorized' : 'Bot ID not supplied', 117 | }; 118 | }; 119 | 120 | private startChat = async ( 121 | data: ChatInput, 122 | api_key: string 123 | ): Promise< 124 | ConversationResponse | WhatsappConversationResponse | ErrorResponse 125 | > => { 126 | let url = `${this.BASE_DOMAIN}/conversation`; 127 | if (data.channel?.toLowerCase() === 'whatsapp') { 128 | url = `${url}/whatsapp`; 129 | } 130 | 131 | try { 132 | const response: ConversationResponse | WhatsappConversationResponse = ( 133 | await axios.post( 134 | url, 135 | { 136 | ...data, 137 | chat_id: data.chat_id || id, 138 | }, 139 | { headers: { Authorization: `Bearer ${api_key}` } } 140 | ) 141 | ).data; 142 | return { ...response, success: true }; 143 | } catch (e) { 144 | return sanitizeErrorResponse(e as AxiosError); 145 | } 146 | }; 147 | 148 | private getUserBots = async ( 149 | api_key: string 150 | ): Promise => { 151 | try { 152 | const bots = ( 153 | await axios.get(`${this.BASE_DOMAIN}/chatbots`, { 154 | headers: { Authorization: `Bearer ${api_key}` }, 155 | }) 156 | ).data; 157 | return { success: true, bots }; 158 | } catch (e) { 159 | return sanitizeErrorResponse(e as AxiosError); 160 | } 161 | }; 162 | 163 | private getUserBot = async ( 164 | api_key: string, 165 | id: number 166 | ): Promise => { 167 | try { 168 | const bot = ( 169 | await axios.get(`${this.BASE_DOMAIN}/chatbot/${id}`, { 170 | headers: { Authorization: `Bearer ${api_key}` }, 171 | }) 172 | ).data; 173 | const chatBot = new ChatConversation( 174 | bot, 175 | this.BASE_DOMAIN, 176 | global.api_key 177 | ); 178 | return { 179 | success: true, 180 | bot, 181 | chat: async function ( 182 | data: ConversationInput 183 | ): Promise< 184 | ConversationResponse | WhatsappConversationResponse | ErrorResponse 185 | > { 186 | return await chatBot.chat(data); 187 | }, 188 | }; 189 | } catch (e) { 190 | return sanitizeErrorResponse(e as AxiosError); 191 | } 192 | }; 193 | 194 | private updateUserBot = async ( 195 | api_key: string, 196 | id: number, 197 | bot: BotRequest 198 | ): Promise => { 199 | try { 200 | const updatedBot = ( 201 | await axios.put(`${this.BASE_DOMAIN}/chatbot/${id}`, bot, { 202 | headers: { Authorization: `Bearer ${api_key}` }, 203 | }) 204 | ).data; 205 | const chatBot = new ChatConversation( 206 | updatedBot, 207 | this.BASE_DOMAIN, 208 | global.api_key 209 | ); 210 | return { 211 | success: true, 212 | bot: updatedBot, 213 | chat: async function ( 214 | data: ConversationInput 215 | ): Promise< 216 | ConversationResponse | WhatsappConversationResponse | ErrorResponse 217 | > { 218 | return await chatBot.chat(data); 219 | }, 220 | }; 221 | } catch (e) { 222 | return sanitizeErrorResponse(e as AxiosError); 223 | } 224 | }; 225 | 226 | private deleteUserBot = async ( 227 | api_key: string, 228 | id: number 229 | ): Promise => { 230 | try { 231 | const deleteBot: DeleteBot = ( 232 | await axios.delete(`${this.BASE_DOMAIN}/chatbot/${id}`, { 233 | headers: { Authorization: `Bearer ${api_key}` }, 234 | }) 235 | ).data; 236 | return { ...deleteBot, success: true }; 237 | } catch (e) { 238 | return sanitizeErrorResponse(e as AxiosError); 239 | } 240 | }; 241 | 242 | private createUserBot = async ( 243 | bot: BotRequest, 244 | api_key: string 245 | ): Promise => { 246 | try { 247 | const createdBot = ( 248 | await axios.post(`${this.BASE_DOMAIN}/chatbot`, bot, { 249 | headers: { Authorization: `Bearer ${api_key}` }, 250 | }) 251 | ).data; 252 | const chatBot = new ChatConversation( 253 | createdBot, 254 | this.BASE_DOMAIN, 255 | global.api_key 256 | ); 257 | 258 | return { 259 | success: true, 260 | bot: createdBot, 261 | chat: async function ( 262 | data: ConversationInput 263 | ): Promise< 264 | ConversationResponse | WhatsappConversationResponse | ErrorResponse 265 | > { 266 | return await chatBot.chat(data); 267 | }, 268 | }; 269 | } catch (e) { 270 | return sanitizeErrorResponse(e as AxiosError); 271 | } 272 | }; 273 | } 274 | -------------------------------------------------------------------------------- /src/shared/helpers/error.helper.ts: -------------------------------------------------------------------------------- 1 | import axios, { AxiosError } from 'axios'; 2 | import { ErrorResponse } from '../interfaces/shared.interface'; 3 | 4 | const sanitizedAxiosError = ( 5 | error: AxiosError, 6 | serverError: AxiosError 7 | ) => { 8 | if (serverError?.response) { 9 | return { 10 | ...serverError?.response?.data, 11 | message: error.message, 12 | error: error.message, 13 | code: serverError?.code || 'FAILED', 14 | status: serverError?.response?.status || 400, 15 | success: false, 16 | }; 17 | } 18 | return { 19 | ...error, 20 | success: false, 21 | message: error?.message, 22 | token: '', 23 | status: 400, 24 | }; 25 | }; 26 | export const sanitizeErrorResponse = (error: AxiosError): ErrorResponse => { 27 | const serverError = error as AxiosError; 28 | if (axios.isAxiosError(error)) { 29 | return sanitizedAxiosError(error, serverError); 30 | } 31 | return { 32 | success: false, 33 | message: serverError?.message || 'Internal server error', 34 | token: '', 35 | status: 400, 36 | code: 'FAILED', 37 | }; 38 | }; 39 | -------------------------------------------------------------------------------- /src/shared/helpers/id.helper.ts: -------------------------------------------------------------------------------- 1 | const sample = (id = [], fn = Math.random): string | undefined => { 2 | if (id.length === 0) return; 3 | return id[Math.round(fn() * (id.length - 1))]; 4 | }; 5 | 6 | const geratedId = (limit = 13, fn = Math.random): string => { 7 | const allowedLetters: any = [ 8 | 'abcdefghijklmnopqrstuvwxyz', 9 | 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 10 | ].join(''); 11 | const allowedCharacters: any = ['0123456789', allowedLetters].join(''); 12 | 13 | const generatedId = [sample(allowedLetters, fn)]; 14 | 15 | for (let i = 0; i < limit - 1; i++) { 16 | generatedId.push(sample(allowedCharacters, fn)); 17 | } 18 | 19 | return generatedId.join(''); 20 | }; 21 | 22 | export const id = geratedId(); 23 | -------------------------------------------------------------------------------- /src/shared/interfaces/auth.interface.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * An interface for login response 3 | */ 4 | export interface LoginResponse { 5 | api_key: string; 6 | } 7 | 8 | /** 9 | * An interface for login request 10 | */ 11 | export interface Login { 12 | api_key: string; 13 | url?: string; 14 | } 15 | -------------------------------------------------------------------------------- /src/shared/interfaces/bot.interface.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ConversationInput, 3 | ConversationResponse, 4 | WhatsappConversationResponse, 5 | } from './conversation.interface'; 6 | import { ErrorResponse } from './shared.interface'; 7 | 8 | /** 9 | * Get bots response 10 | */ 11 | export interface BotsResponse { 12 | success: boolean; 13 | bots: Bot[]; 14 | } 15 | 16 | /** 17 | * An interface for bot intents used for training bots 18 | */ 19 | export interface Intents { 20 | [key: string]: string | string[] | unknown | number | Record; 21 | } 22 | 23 | /** 24 | * An interface for bot flows used for training bots 25 | */ 26 | export interface Flows { 27 | [key: string]: string | string[] | unknown | number | Record; 28 | } 29 | 30 | /** 31 | * An interface for both get bot and create bot responses 32 | */ 33 | export interface BotResponse { 34 | success: boolean; 35 | bot: Bot | undefined; 36 | /** 37 | * @method Chat method for initiating chat with a particular bot 38 | */ 39 | chat: ( 40 | data: ConversationInput 41 | ) => Promise< 42 | ConversationResponse | WhatsappConversationResponse | ErrorResponse 43 | >; 44 | } 45 | /** 46 | * A sarufi single bot interface with all the properties 47 | */ 48 | export interface Bot { 49 | id: number; 50 | user_id: number; 51 | name: string; 52 | description: string; 53 | visible_on_community: boolean; 54 | intents: Intents; 55 | flows: Flows; 56 | model_name: string; 57 | industry: string; 58 | created_at: string; 59 | updated_at: string; 60 | language?: string; 61 | } 62 | 63 | export interface BotRequest { 64 | name: string; 65 | description?: string; 66 | intents?: Intents; 67 | flows?: Flows; 68 | model_name?: string; 69 | industry?: string; 70 | visible_on_community: boolean; 71 | language?: string; 72 | } 73 | 74 | export interface DeleteBot { 75 | success: string; 76 | message: string; 77 | } 78 | 79 | export interface GetBot { 80 | id: number; 81 | url?: string; 82 | api_key?: string; 83 | } 84 | export interface GetBots { 85 | api_key?: string; 86 | url?: string; 87 | } 88 | export interface UpdateBot { 89 | id: number; 90 | bot: BotRequest; 91 | url?: string; 92 | api_key?: string; 93 | } 94 | 95 | /** 96 | * An interface for create bot request 97 | */ 98 | export interface CreateBot { 99 | bot: BotRequest; 100 | url?: string; 101 | api_key?: string; 102 | } 103 | -------------------------------------------------------------------------------- /src/shared/interfaces/conversation.interface.ts: -------------------------------------------------------------------------------- 1 | export interface Conversation { 2 | chat_id: string | number | unknown; 3 | bot_id: string; 4 | message: string; 5 | message_type?: string; 6 | current_state?: string; 7 | next_state?: string; 8 | language?: string; 9 | } 10 | 11 | export interface ConversationResponse { 12 | message: 13 | | string 14 | | number 15 | | unknown 16 | | [string] 17 | | Record 18 | | [Record]; 19 | success: boolean; 20 | memory?: { 21 | [key: string]: string | unknown; 22 | }; 23 | [key: string]: string | unknown; 24 | } 25 | 26 | export interface WhatsappConversationResponse extends ConversationResponse { 27 | actions: Array<{ 28 | send_message?: any[]; 29 | send_button?: any; 30 | send_reply_button?: { type: string; body: { text: string }; action: any }; 31 | send_image?: Array<{ link: string; caption: string }>; 32 | send_audio?: any; 33 | send_videos?: any; 34 | }>; 35 | } 36 | 37 | export interface ConversationInput { 38 | message: string; 39 | chat_id?: string | number | unknown; 40 | channel?: 'general' | 'whatsapp'; 41 | } 42 | 43 | export interface ChatInput { 44 | message: string; 45 | bot_id: string | number; 46 | chat_id?: string | number | unknown; 47 | url?: string; 48 | api_key?: string; 49 | channel?: 'general' | 'whatsapp'; 50 | } 51 | -------------------------------------------------------------------------------- /src/shared/interfaces/shared.interface.ts: -------------------------------------------------------------------------------- 1 | import { Bot } from './bot.interface'; 2 | import { 3 | ConversationInput, 4 | ConversationResponse, 5 | WhatsappConversationResponse, 6 | } from './conversation.interface'; 7 | 8 | export interface ErrorResponse { 9 | success: boolean; 10 | message: string; 11 | code?: string; 12 | status?: number; 13 | token?: string | undefined; 14 | bot?: Bot | undefined; 15 | bots?: Bot[] | undefined; 16 | chat?: ( 17 | data: ConversationInput 18 | ) => Promise; 19 | [key: string]: unknown; 20 | } 21 | -------------------------------------------------------------------------------- /test/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'axios/lib/adapters/xhr'; 2 | -------------------------------------------------------------------------------- /test/mocks/mocks.constants.ts: -------------------------------------------------------------------------------- 1 | export const BotData = { 2 | name: 'Bennett', 3 | user_id: 16, 4 | description: 'PUT DESCRIPTION HERE', 5 | industry: 'general', 6 | intents: {}, 7 | flows: {}, 8 | visible_on_community: false, 9 | updated_at: '2022-09-26T23=21=50.496431', 10 | id: 27, 11 | model_name: 'models/079d4fa7f3159d44b540c5dd3f146591.pkl', 12 | created_at: '2022-09-26T23=21=50.496419', 13 | }; 14 | 15 | export const BotsData = [ 16 | { 17 | intents: {}, 18 | user_id: 16, 19 | description: 'PUT DESCRIPTION HERE', 20 | industry: 'general', 21 | updated_at: '2022-09-26T23:21:50.496431', 22 | name: 'Bennett', 23 | visible_on_community: false, 24 | flows: {}, 25 | id: 26, 26 | model_name: 'models/079d4fa7f3159d44b540c5dd3f146591.pkl', 27 | created_at: '2022-09-26T23:21:50.496419', 28 | }, 29 | { 30 | intents: {}, 31 | user_id: 16, 32 | description: 'PUT DESCRIPTION HERE', 33 | industry: 'general', 34 | visible_on_community: false, 35 | updated_at: '2022-09-26T23:21:50.496431', 36 | name: 'Bennett', 37 | flows: {}, 38 | id: 27, 39 | model_name: 'models/7051f2f6512bd89d29082c5c683289cb.pkl', 40 | created_at: '2022-09-26T23:21:50.496419', 41 | }, 42 | ]; 43 | 44 | export const BotUpdate = { 45 | name: "Bennett's Bot", 46 | user_id: 16, 47 | description: 'PUT DESCRIPTION HERE', 48 | industry: 'general', 49 | visible_on_community: false, 50 | intents: {}, 51 | flows: { 52 | greets: [ 53 | 'hey', 54 | 'hello', 55 | 'hi', 56 | 'howdy', 57 | 'hola', 58 | 'greetings', 59 | 'good morning', 60 | 'good afternoon', 61 | 'good evening', 62 | ], 63 | }, 64 | id: 26, 65 | model_name: 'models/079d4fa7f3159d44b540c5dd3f146591.pkl', 66 | }; 67 | -------------------------------------------------------------------------------- /test/modules/index.spec.ts: -------------------------------------------------------------------------------- 1 | import sarufi from '../../src'; 2 | import { LoginResponse } from '../../src/shared/interfaces/auth.interface'; 3 | import axios from 'axios'; 4 | import xhrAdapter from 'axios/lib/adapters/xhr'; 5 | import HttpRequestMock from 'http-request-mock'; 6 | import { ErrorResponse } from '../../src/shared/interfaces/shared.interface'; 7 | import { Bot, BotRequest } from '../../src/shared/interfaces/bot.interface'; 8 | import { BotData, BotsData, BotUpdate } from '../mocks/mocks.constants'; 9 | import { 10 | ConversationResponse, 11 | WhatsappConversationResponse, 12 | } from '../../src/shared/interfaces/conversation.interface'; 13 | 14 | jest.setTimeout(300000); 15 | 16 | axios.defaults.adapter = xhrAdapter; 17 | const mocker = HttpRequestMock.setupForUnitTest('xhr'); 18 | const BASE_DOMAIN = 'https://developers.sarufi.io'; 19 | 20 | // describe('Login User', () => { 21 | // const loginDTO = { 22 | // token_type: 'Bearer', 23 | // scope: 'read:data write:data', 24 | // expires_in: 86400, 25 | // api_key: 'basic token', 26 | // }; 27 | 28 | // mocker.post(`${BASE_DOMAIN}/api/access_token`, (): 29 | // | LoginResponse 30 | // | ErrorResponse => { 31 | // return loginDTO; 32 | // }); 33 | // it('Should return message and token', async () => { 34 | // const result: LoginResponse | ErrorResponse = await sarufi.login({ 35 | // client_id: '', 36 | // client_secret: '', 37 | // }); 38 | // expect(result.api_key).toBeDefined(); 39 | // expect(result).toMatchObject(loginDTO); 40 | // }); 41 | // }); 42 | 43 | describe('Create Bot', () => { 44 | mocker.post(`${BASE_DOMAIN}/chatbot`, (): BotRequest => { 45 | return BotData; 46 | }); 47 | it('Should return a created bot given a valid token', async () => { 48 | const createdBot = await sarufi.createBot({ 49 | bot: { name: '', visible_on_community: false }, 50 | }); 51 | expect(createdBot.success).toBe(true); 52 | expect(createdBot.bot).toMatchObject(BotData); 53 | }); 54 | }); 55 | 56 | describe('Get Bots', () => { 57 | mocker.get(`${BASE_DOMAIN}/chatbots`, (): Bot[] => { 58 | return BotsData; 59 | }); 60 | it('Should return user bots', async () => { 61 | const userBots = await sarufi.getBots(); 62 | expect(userBots.success).toBe(true); 63 | expect(userBots.bots?.length).toBe(2); 64 | }); 65 | }); 66 | 67 | describe('Get Bot', () => { 68 | mocker.get(`${BASE_DOMAIN}/chatbot/27`, (): Bot => { 69 | return BotData; 70 | }); 71 | it('Should return a single bot', async () => { 72 | const userBot = await sarufi.getBot({ id: BotData.id }); 73 | expect(userBot.success).toBe(true); 74 | expect(userBot.bot?.id).toBe(27); 75 | }); 76 | }); 77 | 78 | describe('Update Bot', () => { 79 | mocker.put(`${BASE_DOMAIN}/chatbot/27`, (): BotRequest => { 80 | return BotUpdate; 81 | }); 82 | it('Should return an updated bot', async () => { 83 | const bot: BotRequest = { 84 | name: "Bennett's Bot", 85 | description: 'PUT DESCRIPTION HERE', 86 | industry: 'general', 87 | visible_on_community: false, 88 | intents: { 89 | greets: [ 90 | 'hey', 91 | 'hello', 92 | 'hi', 93 | 'howdy', 94 | 'hola', 95 | 'greetings', 96 | 'good morning', 97 | 'good afternoon', 98 | 'good evening', 99 | ], 100 | goodbye: [ 101 | 'bye', 102 | 'goodbye', 103 | 'see you later', 104 | 'see you soon', 105 | 'see you', 106 | 'talk to you later', 107 | 'talk to you soon', 108 | 'talk to you', 109 | ], 110 | order_pizza: [ 111 | 'I need a pizza', 112 | 'I want a pizza', 113 | 'order a pizza', 114 | 'I want to order a pizza', 115 | ], 116 | }, 117 | model_name: 'models/079d4fa7f3159d44b540c5dd3f146591.pkl', 118 | }; 119 | const userBot = await sarufi.updateBot({ id: BotData.id, bot }); 120 | expect(userBot.success).toBe(true); 121 | expect(userBot.bot?.id).toBe(26); 122 | }); 123 | }); 124 | 125 | describe('Start chat with bot Bot', () => { 126 | mocker.post(`${BASE_DOMAIN}/conversation`, (): ConversationResponse => { 127 | return { 128 | success: true, 129 | message: ['Hi, How are you?'], 130 | memory: { 131 | greets: 'oya', 132 | }, 133 | next_state: 'end', 134 | }; 135 | }); 136 | it('Should start a chat with a bot', async () => { 137 | const chat = await sarufi.chat({ 138 | message: 'Hey', 139 | bot_id: 27, 140 | chat_id: 'HEYOO', 141 | }); 142 | expect(chat.success).toBe(true); 143 | expect(chat?.message).toBeDefined(); 144 | }); 145 | }); 146 | describe('Delete Bot', () => { 147 | mocker.delete(`${BASE_DOMAIN}/chatbot/27`, (): { message: string } => { 148 | return { message: 'Bot deleted' }; 149 | }); 150 | it('Should delete a bot', async () => { 151 | const deleteBot = await sarufi.deleteBot({ id: BotData.id }); 152 | expect(deleteBot.success).toBe(true); 153 | expect(deleteBot?.message).toBe('Bot deleted'); 154 | }); 155 | }); 156 | describe('Start conversation', () => { 157 | mocker.get(`${BASE_DOMAIN}/chatbot/27`, (): Bot => { 158 | return BotData; 159 | }); 160 | it('Should start convo given bot', async () => { 161 | const userBot = await sarufi.getBot({ id: BotData.id }); 162 | expect(userBot.success).toBe(true); 163 | expect(userBot.bot?.id).toBe(27); 164 | if (userBot.chat) { 165 | const convoResponse = { 166 | success: true, 167 | message: ['Hi, How are you?'], 168 | memory: { 169 | greets: 'oya', 170 | }, 171 | next_state: 'end', 172 | }; 173 | mocker.post(`${BASE_DOMAIN}/conversation`, (): ConversationResponse => { 174 | return convoResponse; 175 | }); 176 | const convo: 177 | | ConversationResponse 178 | | WhatsappConversationResponse 179 | | ErrorResponse = await userBot.chat({ 180 | message: 'Yooh', 181 | chat_id: 'Start', 182 | }); 183 | expect(convo).toMatchObject(convoResponse); 184 | expect(convo?.memory).toMatchObject(convoResponse.memory); 185 | } 186 | }); 187 | }); 188 | -------------------------------------------------------------------------------- /test/shared/error.helper.spec.ts: -------------------------------------------------------------------------------- 1 | import { sanitizeErrorResponse } from '../../src/shared/helpers/error.helper'; 2 | import { AxiosError } from 'axios'; 3 | describe('Login', () => { 4 | it('Should return a sanitized Error', () => { 5 | const error = { 6 | message: 'An error occured', 7 | response: { data: {} }, 8 | code: 'FAILED', 9 | status: 404, 10 | }; 11 | const errorResponse = sanitizeErrorResponse(error as unknown as AxiosError); 12 | expect(errorResponse.message).toBeDefined(); 13 | expect(errorResponse.success).toBe(false); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /test/shared/id.helper.spec.ts: -------------------------------------------------------------------------------- 1 | import { id } from '../../src/shared/helpers/id.helper'; 2 | 3 | describe('ID', () => { 4 | it('Should return a random id', () => { 5 | const chart_id = id; 6 | expect(chart_id).toBeDefined(); 7 | expect(chart_id.length).toBe(13); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 4 | 5 | /* Basic Options */ 6 | // "incremental": true, /* Enable incremental compilation */ 7 | "target": "es6", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */ 8 | "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ 9 | // "lib": [], /* Specify library files to be included in the compilation. */ 10 | // "allowJs": true, /* Allow javascript files to be compiled. */ 11 | // "checkJs": true, /* Report errors in .js files. */ 12 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', 'react', 'react-jsx' or 'react-jsxdev'. */ 13 | "declaration": true, /* Generates corresponding '.d.ts' file. */ 14 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 15 | // "sourceMap": true, /* Generates corresponding '.map' file. */ 16 | // "outFile": "./", /* Concatenate and emit output to single file. */ 17 | "outDir": "./lib/", /* Redirect output structure to the directory. */ 18 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 19 | // "composite": true, /* Enable project compilation */ 20 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 21 | // "removeComments": true, /* Do not emit comments to output. */ 22 | // "noEmit": true, /* Do not emit outputs. */ 23 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 24 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 25 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 26 | 27 | /* Strict Type-Checking Options */ 28 | "strict": true, /* Enable all strict type-checking options. */ 29 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 30 | // "strictNullChecks": true, /* Enable strict null checks. */ 31 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 32 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 33 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 34 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 35 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 36 | 37 | /* Additional Checks */ 38 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 39 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 40 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 41 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 42 | // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ 43 | // "noPropertyAccessFromIndexSignature": true, /* Require undeclared properties from index signatures to use element accesses. */ 44 | 45 | /* Module Resolution Options */ 46 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 47 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 48 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 49 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 50 | // "typeRoots": [], /* List of folders to include type definitions from. */ 51 | // "types": [], /* Type declaration files to be included in compilation. */ 52 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 53 | "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 54 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 55 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 56 | 57 | /* Source Map Options */ 58 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 59 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 60 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 61 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 62 | 63 | /* Experimental Options */ 64 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 65 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 66 | 67 | /* Advanced Options */ 68 | "skipLibCheck": true, /* Skip type checking of declaration files. */ 69 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ 70 | }, 71 | "include": ["src/**/*.ts", "test/**/*.ts"], 72 | "exclude": ["*.spec.ts"] 73 | } 74 | --------------------------------------------------------------------------------