├── .npmignore ├── .husky ├── pre-commit └── commit-msg ├── .gitignore ├── .github ├── ISSUE_TEMPLATE │ ├── config.yml │ ├── question.md │ ├── feature_request.md │ └── bug_report.yaml └── workflows │ └── test.yml ├── commitlint.config.js ├── .vscode └── settings.json ├── index.js ├── rollup.config.js ├── jsconfig.json ├── example-app ├── package.json └── index.js ├── .eslintrc.json ├── renovate.json ├── test ├── utils.js ├── example.test.js └── read.test.js ├── lib ├── errors.js ├── defaults.js ├── response-functions.js ├── yemot_router.js └── call.js ├── LICENSE ├── package.json ├── CHANGELOG.md ├── index.d.ts └── README.md /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | npm test 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | index.cjs -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | npx --no -- commitlint --edit $1 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | export default { extends: ['@commitlint/config-conventional'] }; 2 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.exclude": { 3 | "index.cjs": true 4 | } 5 | } -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | export { default as YemotRouter } from './lib/yemot_router.js'; 2 | export { SYSTEM_MESSAGE_CODES } from './lib/system-messages.js'; 3 | export * from './lib/errors.js'; 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/question.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Question 3 | about: Ask a question about library usage 4 | title: '' 5 | labels: question 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import { nodeResolve } from '@rollup/plugin-node-resolve'; 2 | 3 | export default { 4 | input: 'index.js', 5 | output: { 6 | file: 'index.cjs', 7 | format: 'cjs' 8 | }, 9 | external: [ 10 | 'express', 11 | 'colors', 12 | 'ms' 13 | ], 14 | plugins: [nodeResolve()] 15 | }; -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "exclude": [ 3 | "node_modules" 4 | ], 5 | "compilerOptions": { 6 | "moduleResolution": "node", 7 | "target": "ES6", 8 | "module": "commonjs", 9 | "baseUrl": ".", 10 | "paths": { 11 | "*": [ 12 | "*", 13 | "lib/*" 14 | ] 15 | } 16 | }, 17 | "include": [ 18 | "*" 19 | ] 20 | } -------------------------------------------------------------------------------- /example-app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "yemot-router2-example-app", 3 | "version": "0.0.1", 4 | "main": "index.js", 5 | "private": "true", 6 | "scripts": { 7 | "start": "node index.js", 8 | "dev": "node --watch index.js" 9 | }, 10 | "keywords": [], 11 | "author": "", 12 | "license": "ISC", 13 | "type": "module", 14 | "dependencies": { 15 | "express": "^4.22.1", 16 | "yemot-router2": "file:.." 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Lint and Test 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | eslint: 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | - uses: actions/checkout@v6 11 | - name: Install dependencies 12 | run: npm install --save-dev 13 | 14 | - name: Run ESLint 15 | run: npm run lint 16 | 17 | test: 18 | runs-on: ubuntu-latest 19 | 20 | steps: 21 | - uses: actions/checkout@v6 22 | - name: Install dependencies 23 | run: npm install --save-dev 24 | 25 | - name: Run Tests 26 | run: npm run test -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2021": true, 5 | "jest/globals": true 6 | }, 7 | "plugins": [ 8 | "jest" 9 | ], 10 | "extends": [ 11 | "standard" 12 | ], 13 | "overrides": [], 14 | "parserOptions": { 15 | "ecmaVersion": "latest", 16 | "sourceType": "module" 17 | }, 18 | "rules": { 19 | "indent": [ 20 | "error", 21 | 4 22 | ], 23 | "semi": [ 24 | "error", 25 | "always" 26 | ], 27 | "space-before-function-paren": [ 28 | "warn", 29 | "always" 30 | ], 31 | "eol-last": "off" 32 | } 33 | } -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:base" 5 | ], 6 | "rangeStrategy": "bump", 7 | "labels": ["dependencies"], 8 | "packageRules": [ 9 | { 10 | "depTypeList": ["dependencies"], 11 | "matchUpdateTypes": ["minor", "patch"], 12 | "matchCurrentVersion": "!/^0/", 13 | "automerge": true, 14 | "automergeType": "branch" 15 | }, 16 | { 17 | "depTypeList": ["dependencies"], 18 | "matchPackageNames": [ "@types/node", "@types/express" ], 19 | "dependencyDashboardApproval": true, 20 | "groupName": "devDependencies" 21 | }, 22 | { 23 | "depTypeList": ["devDependencies"], 24 | "dependencyDashboardApproval": true, 25 | "groupName": "devDependencies" 26 | } 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /test/utils.js: -------------------------------------------------------------------------------- 1 | import request from 'supertest'; 2 | import qs from 'qs'; 3 | import crypto from 'crypto'; 4 | 5 | export class CallSimulator { 6 | #port; 7 | constructor (port) { 8 | this.#port = port; 9 | 10 | this.values = { 11 | ApiCallId: crypto.randomBytes(20).toString('hex'), 12 | ApiYFCallId: crypto.randomBytes(20).toString('hex'), 13 | ApiDID: '0772222770', 14 | ApiRealDID: '07722225555', 15 | ApiPhone: '0527000000', 16 | ApiExtension: '', 17 | ApiTime: new Date().getTime().toString() 18 | }; 19 | } 20 | 21 | get (path = '/') { 22 | return request(`http://localhost:${this.#port}`).get(`${path}?${qs.stringify(this.values)}`); 23 | } 24 | 25 | post (path = '/') { 26 | return request(`http://localhost:${this.#port}`).post(`${path}?${qs.stringify(this.values)}`); 27 | } 28 | } -------------------------------------------------------------------------------- /lib/errors.js: -------------------------------------------------------------------------------- 1 | export class CallError extends Error { 2 | constructor ({ message, call }) { 3 | super(); 4 | 5 | this.name = 'CallError'; 6 | this.message = message; 7 | this.call = call; 8 | this.date = new Date(); 9 | } 10 | } 11 | 12 | export class ExitError extends CallError { 13 | constructor (call, context) { 14 | super({ call, message: 'the call was exited from the extension (by go_to_folder or id_list_message)' }); 15 | 16 | this.name = 'ExitError'; 17 | this.context = context; 18 | } 19 | } 20 | 21 | export class HangupError extends CallError { 22 | constructor (call) { 23 | super({ call, message: 'the call was hangup by the caller' }); 24 | 25 | this.name = 'HangupError'; 26 | } 27 | } 28 | 29 | export class TimeoutError extends CallError { 30 | constructor (call, timeout) { 31 | super({ call, message: 'timeout for receiving a response from the caller' }); 32 | 33 | this.name = 'TimeoutError'; 34 | this.timeout = timeout; 35 | } 36 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 ShlomoCode 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 | -------------------------------------------------------------------------------- /lib/defaults.js: -------------------------------------------------------------------------------- 1 | export default { 2 | printLog: false, 3 | removeInvalidChars: false, 4 | read: { 5 | // val_name is dynamic generated 6 | timeout: 0, 7 | re_enter_if_exists: false, 8 | removeInvalidChars: false, 9 | tap: { 10 | min_digits: 1, 11 | max_digits: '', 12 | sec_wait: 7, 13 | typing_playback_mode: 'No', 14 | block_asterisk_key: false, 15 | block_zero_key: false, 16 | replace_char: '', 17 | digits_allowed: '', 18 | amount_attempts: '', 19 | allow_empty: false, 20 | empty_val: 'None', 21 | block_change_keyboard: false 22 | }, 23 | stt: { 24 | lang: '', 25 | block_typing: false, 26 | max_digits: '', 27 | quiet_max: '', 28 | max_length: '', 29 | use_records_recognition_engine: false 30 | }, 31 | record: { 32 | path: '', 33 | file_name: '', 34 | no_confirm_menu: false, 35 | save_on_hangup: false, 36 | append_to_existing_file: false, 37 | min_length: '', 38 | max_length: '' 39 | } 40 | }, 41 | id_list_message: { 42 | removeInvalidChars: false 43 | // prependToNextAction not supported as default 44 | } 45 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "yemot-router2", 3 | "version": "6.2.0", 4 | "description": "", 5 | "exports": { 6 | "import": "./index.js", 7 | "require": "./index.cjs" 8 | }, 9 | "type": "module", 10 | "scripts": { 11 | "lint": "eslint . --fix --ext .js,.cjs,.mjs --ignore-path .gitignore", 12 | "test": "node --experimental-vm-modules node_modules/.bin/jest", 13 | "prepublishOnly": "rollup --config rollup.config.js", 14 | "prepare": "husky" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "git+https://github.com/ShlomoCode/yemot-router2.git" 19 | }, 20 | "author": "musicode1, ShlomoCode", 21 | "license": "MIT", 22 | "dependencies": { 23 | "@rollup/plugin-node-resolve": "^16.0.3", 24 | "@types/express": "^4.17.21", 25 | "@types/node": "^20.17.13", 26 | "colors": "^1.4.0", 27 | "express": "^4.22.1", 28 | "ms": "^2.1.3", 29 | "stack-trace": "^1.0.0-pre2" 30 | }, 31 | "devDependencies": { 32 | "@commitlint/cli": "^19.6.1", 33 | "@commitlint/config-conventional": "^19.6.0", 34 | "eslint": "^8.57.1", 35 | "eslint-config-standard": "^17.1.0", 36 | "eslint-plugin-import": "^2.31.0", 37 | "eslint-plugin-jest": "^27.9.0", 38 | "eslint-plugin-n": "^16.6.2", 39 | "eslint-plugin-promise": "^6.6.0", 40 | "husky": "^9.1.7", 41 | "jest": "^29.7.0", 42 | "qs": "^6.14.0", 43 | "rollup": "^4.30.1", 44 | "supertest": "^6.3.4" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yaml: -------------------------------------------------------------------------------- 1 | name: Bug report 2 | description: Create a report to help us improve 3 | labels: 4 | - bug 5 | body: 6 | - type: textarea 7 | id: bug_description 8 | attributes: 9 | label: Describe the bug 10 | description: A clear and concise description of what the bug is. 11 | validations: 12 | required: true 13 | - type: textarea 14 | id: version_information 15 | attributes: 16 | label: Version Information 17 | description: | 18 | Please provide the version of the package you are using. 19 | Use the following command to get this: 20 | ```bash 21 | node -v && npm list yemot-router2 express 22 | ``` 23 | render: 24 | rows: 4 25 | validations: 26 | required: true 27 | - type: textarea 28 | id: reproducible_example 29 | attributes: 30 | label: Reproducible Example 31 | description: A short, isolated, but runnable code example is required. **Otherwise, the issue will be closed**. 32 | value: | 33 | ``` 34 | // Add your code here... 35 | 36 | 37 | 38 | 39 | 40 | 41 | ``` 42 | render: 43 | rows: 30 44 | validations: 45 | required: true 46 | - type: textarea 47 | id: additional_context 48 | attributes: 49 | label: Additional Context 50 | description: Provide any other context about the problem, like error messages, stack traces, or screenshots. you can also drop a file here. 51 | validations: 52 | required: false 53 | -------------------------------------------------------------------------------- /test/example.test.js: -------------------------------------------------------------------------------- 1 | import request from 'supertest'; 2 | import { CallSimulator } from './utils'; 3 | import { app as exampleApp, router } from '../example-app/index.js'; 4 | import qs from 'qs'; 5 | 6 | describe('example-app file', () => { 7 | const randomPort = Math.floor(Math.random() * 10000) + 10000; 8 | let server; 9 | 10 | beforeAll(async () => { 11 | server = await exampleApp.listen(randomPort); 12 | console.log(`test server listening on port ${randomPort}`); 13 | }); 14 | 15 | afterAll(async () => { 16 | await server.close(); 17 | }); 18 | 19 | const callSimulator = new CallSimulator(randomPort); 20 | 21 | it('should return 200 and not valid yemot request message', async () => { 22 | const response = await request(`http://localhost:${randomPort}`).get('/'); 23 | expect(response.statusCode).toBe(200); 24 | expect(response.body.message).toBe('the request is not valid yemot request'); 25 | }); 26 | 27 | it('should return valid read response', async () => { 28 | const response = await callSimulator.get(); 29 | expect(response.text).toBe('read=t-היי, תקיש 10=val_1,no,2,2,7,No,no,no,,10,,,None,'); 30 | }); 31 | 32 | it('should call added to active calls', async () => { 33 | expect(Object.prototype.hasOwnProperty.call(router.activeCalls, callSimulator.values.ApiCallId)).toBe(true); 34 | }); 35 | it('should all query params added to call.values', async () => { 36 | const call = router.activeCalls[callSimulator.values.ApiCallId]; 37 | for (const [key, value] of Object.entries(callSimulator.values)) { 38 | expect(call[key]).toBe(value); 39 | } 40 | }); 41 | 42 | it('should return cannot POST / when use router.get', async () => { 43 | const response = await callSimulator.post(); 44 | expect(response.statusCode).toBe(404); 45 | expect(response.error.message).toBe(`cannot POST /?${qs.stringify(callSimulator.values)} (404)`); 46 | expect(response.text).toMatch('Cannot POST /'); 47 | }); 48 | 49 | it('should continue to next read', async () => { 50 | callSimulator.values.val_1 = '10'; 51 | const response = await callSimulator.get(); 52 | expect(response.text).toBe('read=t-שלום, אנא הקש את שמך המלא=val_2,no,,1,7,HebrewKeyboard,no,no,,,,,None,'); 53 | }); 54 | 55 | it('should return valid hangup response and remove call from active calls', async () => { 56 | callSimulator.values.ApiHangup = ''; 57 | callSimulator.values.hangup = 'yes'; 58 | 59 | const response = await callSimulator.get(); 60 | expect(response.body.message).toBe('hangup'); 61 | expect(router.activeCalls[callSimulator.values.ApiCallId]).toBeUndefined(); 62 | }); 63 | }); 64 | -------------------------------------------------------------------------------- /example-app/index.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import { YemotRouter } from 'yemot-router2'; 3 | import { fileURLToPath } from 'url'; 4 | import process from 'process'; 5 | export const app = express(); 6 | 7 | export const router = YemotRouter({ 8 | printLog: true, 9 | uncaughtErrorHandler: (error, call) => { 10 | console.log(`Uncaught error in ${call.req.path} from ${call.phone}. error stack: ${error.stack}`); 11 | // do something with the error - like send email to developer, print details log, etc. 12 | return call.id_list_message([{ type: 'text', data: 'אירעה שגיאה' }]); // play nice error message to the caller 13 | } 14 | }); 15 | 16 | router.events.on('call_hangup', (call) => { 17 | console.log(`[example.js] call ${call.callId} was hangup`); 18 | }); 19 | 20 | router.events.on('call_continue', (call) => { 21 | console.log(`[example.js] call ${call.callId} was continue`); 22 | }); 23 | 24 | router.events.on('new_call', (call) => { 25 | console.log(`[example.js] new call ${call.callId} from ${call.phone}`); 26 | }); 27 | 28 | /** @param {import('yemot-router2').Call} call */ 29 | async function callHandler (call) { 30 | // לא ניתן להתקדם ללא הקשת 10 וסולמית 31 | await call.read([{ type: 'text', data: 'היי, תקיש 10' }], 'tap', { 32 | max_digits: 2, 33 | min_digits: 2, 34 | digits_allowed: ['10'] 35 | }); 36 | 37 | const name = await call.read([{ type: 'text', data: 'שלום, אנא הקש את שמך המלא' }], 'tap', { typing_playback_mode: 'HebrewKeyboard' }); 38 | console.log('name:', name); 39 | 40 | const addressFilePath = await call.read( 41 | [ 42 | { type: 'text', data: 'שלום ' + name }, 43 | { type: 'text', data: 'אנא הקלט את הרחוב בו אתה גר' } 44 | ], 'record', 45 | { removeInvalidChars: true } 46 | ); 47 | console.log('address file path:', addressFilePath); 48 | 49 | // 💰 קטע זה משתמש בזיהוי דיבור ודורש יחידות במערכת 💰 50 | const text = await call.read([{ type: 'text', data: 'אנא אמור בקצרה את ההודעה שברצונך להשאיר' }], 'stt'); 51 | console.log('user message:', text); 52 | 53 | // לאחר השמעת ההודעה יוצא אוטומטית מהשלוחה 54 | // לשרשור פעולות לאחר השמעת ההודעה יש להגדיר prependToNextAction: true, ראה בREADME 55 | return call.id_list_message([{ 56 | type: 'system_message', 57 | data: 'M1399' // תגובתך התקבלה בהצלחה 58 | }]); 59 | }; 60 | 61 | router.get('/', callHandler); 62 | 63 | // this must if you want to use post requests (api_url_post=yes) 64 | app.use(express.urlencoded({ extended: true })); 65 | 66 | app.use('/', router); 67 | 68 | const port = 3000; 69 | const isMain = process.argv[1] === fileURLToPath(import.meta.url); 70 | if (isMain) { 71 | app.listen(port, () => { 72 | console.log(`example yemot-router2 running on port ${port}`); 73 | }); 74 | } 75 | -------------------------------------------------------------------------------- /test/read.test.js: -------------------------------------------------------------------------------- 1 | import { describe, expect } from '@jest/globals'; 2 | import { CallSimulator } from './utils.js'; 3 | import express from 'express'; 4 | import { YemotRouter } from '../index.js'; 5 | import request from 'supertest'; 6 | 7 | describe('test read defaults and options', () => { 8 | const randomPort = Math.floor(Math.random() * 10000) + 10000; 9 | const router = YemotRouter({ printLog: true }); 10 | let server; 11 | 12 | beforeAll(async () => { 13 | const app = express(); 14 | 15 | app.use(router); 16 | 17 | server = await app.listen(randomPort); 18 | console.log(`test server listening on port ${randomPort}`); 19 | }); 20 | 21 | afterAll(async () => { 22 | await server.close(); 23 | }); 24 | 25 | const callSimulator = new CallSimulator(randomPort); 26 | 27 | it('should return 404 before view is added', async () => { 28 | const response = await callSimulator.get(); 29 | expect(response.statusCode).toBe(404); 30 | expect(response.text).toMatch('Cannot GET /'); 31 | }); 32 | 33 | it('should view added', async () => { 34 | router.get('/', async (call) => { 35 | return await call.read([{ 36 | type: 'text', 37 | data: 'hello world' 38 | }]); 39 | }); 40 | const response = await request(`http://localhost:${randomPort}`).get('/'); 41 | expect(response.statusCode).toBe(200); 42 | }); 43 | 44 | it('should return not valid yemot request message', async () => { 45 | const response = await request(`http://localhost:${randomPort}`).get('/'); 46 | expect(response.body.message).toBe('the request is not valid yemot request'); 47 | }); 48 | 49 | it('should return valid read response', async () => { 50 | const response = await callSimulator.get(); 51 | expect(response.text).toBe('read=t-hello world=val_1,no,,1,7,No,no,no,,,,,None,'); 52 | }); 53 | 54 | it('should view added', async () => { 55 | router.get('/read_tap_1', async (call) => { 56 | return await call.read([{ 57 | type: 'text', 58 | data: 'hello world' 59 | }], 'tap', { 60 | max_digits: 1, 61 | typing_playback_mode: 'No', 62 | replace_char: '', 63 | digits_allowed: [1, 2, 3], 64 | amount_attempts: 3, 65 | allow_empty: true, 66 | empty_val: 'NULL', 67 | block_change_keyboard: true 68 | }); 69 | }); 70 | const response = await request(`http://localhost:${randomPort}`).get('/read_tap_1'); 71 | expect(response.statusCode).toBe(200); 72 | }); 73 | 74 | it('should return valid read tap response - 1', async () => { 75 | const response = await new CallSimulator(randomPort).get('/read_tap_1'); 76 | expect(response.text).toBe('read=t-hello world=val_1,no,1,1,7,No,no,no,,1.2.3,3,Ok,NULL,InsertLettersTypeChangeNo'); 77 | }); 78 | 79 | it('should view added', async () => { 80 | router.get('/read_tap_2', async (call) => { 81 | return await call.read([{ 82 | type: 'text', 83 | data: 'hello world' 84 | }], 'tap', { 85 | block_asterisk_key: true, 86 | block_zero_key: true, 87 | typing_playback_mode: 'Digits' 88 | }); 89 | }); 90 | const response = await request(`http://localhost:${randomPort}`).get('/read_tap_2'); 91 | expect(response.statusCode).toBe(200); 92 | }); 93 | 94 | it('should return valid read tap response - 2', async () => { 95 | const response = await new CallSimulator(randomPort).get('/read_tap_2'); 96 | expect(response.text).toBe('read=t-hello world=val_1,no,,1,7,Digits,yes,yes,,,,,None,'); 97 | }); 98 | 99 | it('should response is native null (in empty_val)', async () => { 100 | const callSimulator = new CallSimulator(randomPort); 101 | router.get('/read_tap_3', async (call) => { 102 | const resp = await call.read([{ 103 | type: 'text', 104 | data: 'hello world' 105 | }], 'tap', { 106 | allow_empty: true, 107 | empty_val: null 108 | }); 109 | expect(resp).toBe(null); 110 | return call.id_list_message([{ 111 | type: 'text', 112 | data: 'bye bye' 113 | }]); 114 | }); 115 | await callSimulator.get('/read_tap_3'); 116 | callSimulator.values.val_1 = 'null'; 117 | await callSimulator.get('/read_tap_3') 118 | .then((response) => { 119 | expect(response.text).toBe('id_list_message=t-bye bye&'); 120 | }); 121 | }); 122 | }); -------------------------------------------------------------------------------- /lib/response-functions.js: -------------------------------------------------------------------------------- 1 | import { CallError } from './errors.js'; 2 | import colors from 'colors'; 3 | 4 | const dataTypes = { 5 | file: 'f', 6 | text: 't', 7 | speech: 's', 8 | digits: 'd', 9 | number: 'n', 10 | alpha: 'a', 11 | zmanim: 'z', 12 | go_to_folder: 'g', 13 | system_message: 'm', 14 | music_on_hold: 'h', 15 | date: 'date', 16 | dateH: 'dateH' 17 | }; 18 | 19 | /** 20 | * @param {String} messagesCombined 21 | * @param {Object} options 22 | * @returns {String} - the read text 23 | */ 24 | export function makeTapModeRead (messagesCombined, options, call) { 25 | const PRESETS_MIN_MAX = { // from yemot docs https://f2.freeivr.co.il/post/77520 26 | Date: { min_digits: 8, max_digits: 8 }, 27 | HebrewDate: { min_digits: 8, max_digits: 8 }, 28 | Time: { min_digits: 4, max_digits: 4 }, 29 | TeudatZehut: { min_digits: 8, max_digits: 9 }, 30 | Phone: { min_digits: 9, max_digits: 10 } 31 | }; 32 | 33 | if (options.typing_playback_mode && PRESETS_MIN_MAX[options.typing_playback_mode]) { 34 | options.min_digits = PRESETS_MIN_MAX[options.typing_playback_mode].min_digits; 35 | options.max_digits = PRESETS_MIN_MAX[options.typing_playback_mode].max_digits; 36 | } 37 | 38 | if (options.digits_allowed && !Array.isArray(options.digits_allowed)) { 39 | throw new CallError({ message: `digits_allowed must be array, got ${typeof options.digits_allowed} (${options.digits_allowed})`, call }); 40 | } 41 | 42 | const tapOps = [ 43 | options.valName, 44 | options.re_enter_if_exists ? 'yes' : 'no', 45 | (options.max_digits || ''), 46 | (options.min_digits || 1), 47 | (options.sec_wait || 7), 48 | (options.typing_playback_mode || 'No'), 49 | options.block_asterisk_key ? 'yes' : 'no', 50 | options.block_zero_key ? 'yes' : 'no', 51 | options.replace_char, 52 | options.digits_allowed ? options.digits_allowed.join('.') : '', 53 | (options.amount_attempts || ''), 54 | (options.allow_empty ? 'Ok' : ''), 55 | (typeof options.empty_val === 'undefined' ? '' : String(options.empty_val)), 56 | (options.block_change_keyboard ? 'InsertLettersTypeChangeNo' : '') 57 | ]; 58 | return `read=${messagesCombined}=${tapOps.join(',')}`; 59 | }; 60 | 61 | /** 62 | * @param {String} messagesCombined 63 | * @param {Object} options 64 | * @returns {String} - the read text 65 | */ 66 | export function makeSttModeRead (messagesCombined, options, call) { 67 | if (options.use_records_recognition_engine) { 68 | if (typeof options.block_typing !== 'undefined') { 69 | throw new CallError({ message: 'block_typing setting option is not available when use_records_recognition_engine is true - typing is always blocked in records recognition engine', call }); 70 | } 71 | } else { 72 | if (options.quiet_max) { 73 | throw new CallError({ message: 'quiet_max option is only available when use_records_recognition_engine is true', call }); 74 | } else if (options.length_max) { 75 | throw new CallError({ message: 'length_max option are only available when use_records_recognition_engine is true', call }); 76 | } 77 | } 78 | 79 | const sttOps = [ 80 | options.valName, 81 | options.re_enter_if_exists ? 'yes' : 'no', 82 | 'voice', 83 | options.lang, 84 | (options.block_typing ? 'no' : ''), 85 | (options.max_digits || ''), 86 | (options.quiet_max || ''), 87 | (options.max_length || ''), 88 | (options.use_records_recognition_engine ? 'record' : '') 89 | ]; 90 | return `read=${messagesCombined}=${sttOps.join(',')}`; 91 | }; 92 | 93 | /** 94 | * @param {String} messagesCombined 95 | * @param {Object} options (record options) 96 | * @returns {String} - the read text 97 | */ 98 | export function makeRecordModeRead (messagesCombined, options, call) { 99 | const recordOps = [ 100 | options.valName, 101 | options.re_enter_if_exists ? 'yes' : 'no', 102 | 'record', 103 | options.path, 104 | options.file_name, 105 | (options.no_confirm_menu ? 'no' : ''), 106 | (options.save_on_hangup ? 'yes' : ''), 107 | (options.append_to_existing_file ? 'yes' : ''), 108 | (options.min_length || ''), 109 | (options.max_length || '') 110 | ]; 111 | return `read=${messagesCombined}=${recordOps.join(',')}`; 112 | }; 113 | 114 | function validateCharsForTTS (text, call) { 115 | const invalidCharsRgx = /[.\-"'&|]/g; 116 | const invalidCharsMatched = text.match(invalidCharsRgx); 117 | if (invalidCharsMatched) { 118 | throw new CallError({ message: `message '${text}' has invalid characters for yemot: ${colors.red(invalidCharsMatched.join(', '))}`, call }); 119 | } 120 | } 121 | 122 | function removeInvalidChars (text) { 123 | const invalidCharsRgx = /[.\-"'&|]/g; 124 | const invalidCharsMatched = text.match(invalidCharsRgx); 125 | if (invalidCharsMatched) { 126 | console.log(`invalid characters for yemot have been removed from the text: ${colors.red(invalidCharsMatched.join(''))} [original text: ${text}]`); 127 | } 128 | return text.replaceAll(invalidCharsRgx, ''); 129 | } 130 | 131 | export function makeMessagesData (messages, options, call) { 132 | for (const msg of messages) { 133 | if (typeof msg.data !== 'string') continue; 134 | if (msg.type === 'text' && (msg.removeInvalidChars ?? options.removeInvalidChars)) { 135 | msg.data = removeInvalidChars(msg.data); 136 | } else { 137 | validateCharsForTTS(msg.data, call); 138 | } 139 | } 140 | 141 | let res = ''; 142 | let i = 1; 143 | for (const value of messages) { 144 | if (i > 1) res += '.'; 145 | 146 | if (typeof value.type !== 'string') { 147 | throw new CallError({ message: `type must be a string, got ${typeof value.type}`, call }); 148 | } 149 | if (!dataTypes[value.type]) { 150 | throw new CallError({ message: `${value.type} is not a valid type!\nValid types are: ${Object.keys(dataTypes).join(', ')}`, call }); 151 | } 152 | 153 | res += dataTypes[value.type] + '-'; 154 | 155 | switch (value.type) { 156 | case 'zmanim': { 157 | if (typeof value.data !== 'object') throw new CallError({ message: 'in "zmanim" type, data should be an object', call }); 158 | let differenceSplitted; 159 | if (value.data.difference) { 160 | if (!/(-|\+)[YMDHmSs]\d+/.test(value.data.difference)) throw new CallError({ message: 'difference is invalid', call }); 161 | differenceSplitted = value.data.difference.match(/(-|\+)(.+)/); 162 | } 163 | res += [ 164 | value.data.time || 'T', 165 | value.data.zone || 'IL/Jerusalem', 166 | differenceSplitted ? differenceSplitted[1] : '', 167 | differenceSplitted ? differenceSplitted[2] : '' 168 | ].join(','); 169 | i++; 170 | break; 171 | } 172 | case 'music_on_hold': { 173 | if (typeof value.data !== 'object') throw new CallError({ message: 'in "music_on_hold" type, data should be an object', call }); 174 | if (typeof value.data.musicName !== 'string') throw new CallError({ message: 'in "music_on_hold" type, data.musicName should be a string', call }); 175 | if (typeof value.data.maxSec !== 'undefined' && !Number.isInteger(parseInt(value.data.maxSec))) throw new CallError({ message: 'in "music_on_hold" type, data.seconds should be a integer', call }); 176 | if (value.data.maxSec) { 177 | res += `${value.data.musicName},${value.data.maxSec}`; 178 | } else { 179 | res += value.data.musicName; 180 | } 181 | i++; 182 | break; 183 | } 184 | case 'system_message': { 185 | const sysMsgId = String(value.data).replace('M', '').trim(); 186 | if (!Number.isInteger(parseInt(sysMsgId))) { 187 | throw new CallError({ message: `'${value.data}' is not a valid system message id`, call }); 188 | } 189 | if (sysMsgId.length !== 4) { 190 | throw new CallError({ message: `'${value.data}' is not a valid system message id - it should be 4 digits, got ${sysMsgId.length}!`, call }); 191 | } 192 | res += sysMsgId; 193 | i++; 194 | break; 195 | } 196 | case 'date': 197 | case 'dateH': 198 | if (!/^\d{2}\/\d{2}\/\d{4}$/.test(value.data)) { 199 | throw new CallError({ message: `'${value.data}' is not a valid date format. should be DD/MM/YYYY format`, call }); 200 | } 201 | res += value.data; 202 | i++; 203 | break; 204 | default: 205 | res += value.data; 206 | i++; 207 | } 208 | if (value.type === 'go_to_folder') break; 209 | } 210 | return res; 211 | }; -------------------------------------------------------------------------------- /lib/yemot_router.js: -------------------------------------------------------------------------------- 1 | import { HangupError, TimeoutError, ExitError } from './errors.js'; 2 | import Call from './call.js'; 3 | import { Router } from 'express'; 4 | import EventEmitter from 'events'; 5 | import colors from 'colors'; 6 | import globalDefaults from './defaults.js'; 7 | import ms from 'ms'; 8 | import { parse as parseStack } from 'stack-trace'; 9 | 10 | function YemotRouter (options = {}) { 11 | const ops = { 12 | printLog: options.printLog, 13 | timeout: options.timeout, 14 | uncaughtErrorHandler: options.uncaughtErrorHandler || null, 15 | defaults: options.defaults || {} 16 | }; 17 | 18 | if (options.uncaughtErrorsHandler) { 19 | throw new Error('YemotRouter: uncaughtErrorsHandler is renamed to uncaughtErrorHandler'); 20 | } 21 | 22 | if (typeof ops.timeout !== 'undefined' && !ms(ops.timeout)) { 23 | throw new Error('YemotRouter: timeout must be a valid ms liberty string/milliseconds number'); 24 | } 25 | 26 | if (ops.defaults.id_list_message?.prependToNextAction) { 27 | throw new Error('YemotRouter: prependToNextAction is not supported as default'); 28 | } 29 | 30 | let mergedDefaults = { 31 | printLog: ops.printLog ?? globalDefaults.printLog, 32 | removeInvalidChars: ops.defaults?.removeInvalidChars ?? globalDefaults.removeInvalidChars, 33 | read: { 34 | timeout: ops.timeout ?? globalDefaults.read.timeout, 35 | tap: { 36 | ...globalDefaults.read.tap, 37 | ...ops.defaults.read?.tap 38 | }, 39 | stt: { 40 | ...globalDefaults.read.stt, 41 | ...ops.defaults.read?.stt 42 | }, 43 | record: { 44 | ...globalDefaults.read.record, 45 | ...ops.defaults.read?.record 46 | } 47 | }, 48 | id_list_message: { 49 | ...globalDefaults.id_list_message, 50 | ...ops.defaults.id_list_message 51 | } 52 | }; 53 | 54 | const activeCalls = {}; 55 | const eventsEmitter = new EventEmitter(); 56 | 57 | function logger (callId, msg, color = 'blue') { 58 | if (!(ops.printLog ?? mergedDefaults.printLog)) return; 59 | console.log(colors[color](`[${callId}]: ${msg}`)); 60 | } 61 | 62 | function deleteCall (callId) { 63 | const call = activeCalls[callId]; 64 | if (call && call._timeoutId) { 65 | clearTimeout(call._timeoutId); 66 | } 67 | delete activeCalls[callId]; 68 | return !!call; 69 | } 70 | 71 | async function makeNewCall (fn, callId, call) { 72 | try { 73 | await fn(call); 74 | deleteCall(callId); 75 | logger(callId, '🆗 the function is done'); 76 | } catch (error) { 77 | deleteCall(callId); 78 | 79 | const [trace] = parseStack(error); 80 | const errorPath = trace ? `(${trace.getFileName()}:${trace.getLineNumber()}:${trace.getColumnNumber()})` : ''; 81 | 82 | // טיפול בשגיאות מלאכותיות שנועדו לעצור את ריצת הפונקציה - בניתוק לדוגמה 83 | if (error instanceof HangupError) { 84 | logger(callId, '👋 the call was hangup by the caller'); 85 | } else if (error instanceof TimeoutError) { 86 | logger(callId, `💣 timeout for receiving a response from the caller (after ${ms(error.timeout, { long: true })}). the call has been deleted from active calls`); 87 | } else if (error instanceof ExitError) { 88 | logger(callId, `👋 the call was exited from the extension /${call.extension}` + (error.context ? ` (by ${error.context?.caller} to ${error.context?.target})` : '')); 89 | } else { 90 | if (ops.uncaughtErrorHandler) { 91 | logger(callId, `💥 Uncaught error. applying uncaughtErrorHandler ${errorPath}`, 'red'); 92 | try { 93 | await ops.uncaughtErrorHandler(error, call); 94 | } catch (err) { 95 | const [trace] = parseStack(err); 96 | const errorPath = `${trace.getFileName()}:${trace.getLineNumber()}:${trace.getColumnNumber()}`; 97 | if (err instanceof ExitError) { 98 | console.log(`👋 the call was exited from the extension /${call.extension} in uncaughtErrorHandler ${errorPath}` + (error.context ? ` (by ${error.context?.caller} to ${error.context?.target})` : '')); 99 | } else { 100 | console.error('💥 Error in uncaughtErrorHandler! process is crashing'); 101 | throw err; 102 | } 103 | } 104 | } else { 105 | logger(callId, `💥 Uncaught error. no uncaughtErrorHandler, throwing error ${errorPath}`, 'red'); 106 | throw error; 107 | } 108 | } 109 | } 110 | } 111 | 112 | const expressRouter = Router(options); 113 | 114 | const proxyHandler = { 115 | get (target, key) { 116 | if (key === 'add_fn') { 117 | throw new Error('YemotRouter: router.add_fn is deprecated, use get/post/all instead'); 118 | } else if (['get', 'post', 'all'].includes(key)) { 119 | return (path, fn) => { 120 | target[key](path, (req, res, next) => { 121 | if (req.method === 'POST' && !req.body) { 122 | throw new Error('YemotRouter: it look you use api_url_post=yes, but you didn\'t include express.urlencoded({ extended: true }) middleware! (https://expressjs.com/en/4x/api.html#express.urlencoded)'); 123 | } 124 | 125 | const values = req.method === 'POST' ? req.body : req.query; 126 | const requiredValues = ['ApiPhone', 'ApiDID', 'ApiExtension', 'ApiCallId']; 127 | if (requiredValues.some((key) => !Object.prototype.hasOwnProperty.call(values, key))) { 128 | return res.json({ message: 'the request is not valid yemot request' }); 129 | } 130 | 131 | const callId = values.ApiCallId; 132 | 133 | let isNewCall = false; 134 | let currentCall = activeCalls[callId]; 135 | if (!currentCall) { 136 | isNewCall = true; 137 | currentCall = new Call(callId, eventsEmitter, mergedDefaults); 138 | currentCall.setReqValues(req, res); 139 | if (values.hangup === 'yes') { 140 | logger(callId, '👋 call is hangup (outside the function)'); 141 | eventsEmitter.emit('call_hangup', currentCall); 142 | return res.json({ message: 'hangup' }); 143 | } 144 | activeCalls[callId] = currentCall; 145 | logger(callId, `📞 new call - from ${values.ApiPhone || 'AnonymousPhone'}`); 146 | eventsEmitter.emit('new_call', currentCall); 147 | } else { 148 | currentCall.setReqValues(req, res); 149 | } 150 | 151 | if (isNewCall) { 152 | makeNewCall(fn, callId, currentCall); 153 | } else { 154 | eventsEmitter.emit(callId, values.hangup === 'yes'); 155 | eventsEmitter.emit(values.hangup === 'yes' ? 'call_hangup' : 'call_continue', currentCall); 156 | } 157 | }); 158 | }; 159 | } else if (key === 'deleteCall') { 160 | return deleteCall; 161 | } else if (key === 'activeCalls') { 162 | return activeCalls; 163 | } else if (key === 'defaults') { 164 | return mergedDefaults; 165 | } else if (key === 'events') { 166 | return eventsEmitter; 167 | } else if (key === 'asExpressRouter') { 168 | return new Proxy(expressRouter, proxyHandler); 169 | } else if (['use', 'handle', 'set', 'name', 'length', 'caseSensitive', 'stack'].includes(key)) { 170 | return target[key]; 171 | } else { 172 | throw new Error(`YemotRouter: ${key.toString()} is not supported yet [get]`); 173 | } 174 | }, 175 | set (target, key, value) { 176 | if (key === 'defaults') { 177 | if (value.id_list_message?.prependToNextAction) { 178 | throw new Error('prependToNextAction is not supported as default'); 179 | } 180 | 181 | if (typeof value.read?.timeout !== 'undefined' && !ms(value.read.timeout)) { 182 | throw new Error('timeout must be a valid ms liberty string/milliseconds number'); 183 | } 184 | 185 | mergedDefaults = { 186 | ...mergedDefaults, 187 | ...value 188 | }; 189 | } else { 190 | throw new Error(`YemotRouter: ${key.toString()} is not supported yet [set]`); 191 | } 192 | } 193 | }; 194 | 195 | return new Proxy(expressRouter, proxyHandler); 196 | } 197 | 198 | export default YemotRouter; 199 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | # יומן שינויים 3 | 4 | הספריה עוקבת אחרי [Semantic Versioning](https://semver.org) ([עברית](https://semver.org/lang/he/)). 5 | 6 | ## ‏6.2.0 7 | 8 | - נוסף טייפ `SYSTEM_MESSAGE_CODES` עבור הודעות מערכת, המספק גם בטיחות מפני טעות הקלדה וגם הצגת תיאור ההודעה בריחוף על שמה 9 | 10 | דוגמה לשימוש: 11 | 12 | ```typescript 13 | import { SYSTEM_MESSAGE_CODES, type Call } from 'yemot-router2'; 14 | 15 | async function callHandler (call: Call) { 16 | return call.id_list_message([{ 17 | type: 'system_message', 18 | data: SYSTEM_MESSAGE_CODES.M1000 19 | }]); 20 | } 21 | ``` 22 | 23 | ## ‏6.1.8 24 | 25 | - תיקון לטייפ של `asExpressRouter` 26 | 27 | ## ‏6.1.7 28 | 29 | - הוספת ולידציה למבנה ההודעה ב‫`id_list_message` (כמו ב‫`read`) 30 | 31 | ## ‏6.1.6 32 | 33 | - תיקון טייפ עבור `typing_playback_mode` מסוג `Number` 34 | - עדכון הדוגמה (`example.js`) עם הדגמה לJSDoc שגורם ל-IDE להציג IntelliSense כראוי 35 | 36 | ## ‏6.1.5 37 | 38 | תיקון טייפ עבור הודעה מסוג `music_on_hold` 39 | 40 | ## ‏6.1.4 41 | 42 | - נוסף ‫JSDoc לקובץ הטייפים עבור כל השיטות והאפשרויות, כולל הטמעת התיעוד של ימות המשיח 43 | - תיקון הגדרת `removeInvalidChars` כברירת מחדל ברמת ראוטר/שיחה 44 | - שיפור התיעוד 45 | 46 | ## ‏6.1.3 47 | 48 | - ברירת המחדל של ההגדרה ‫`save_on_hangup` ‫(`record`) היתה `true`, למרות שעל פי התיעוד היא אמורה להיות `false`. **כעת ברירת המחדל תוקנה והיא `false`**. 49 | - נוספה ההגדרה ‫`use_records_recognition_engine` ‫(`stt`) שהופיע בתיעוד אך היתה חסרה בפועל 50 | - תיקון: ‫`router.asExpressRouter` ‫(TypeScript בלבד) 51 | 52 | ## ‏6.1.2 53 | 54 | - תיקון באג עבור פרויקטים שכתובים ב-CommonJS 55 | 56 | ## ‏6.1.1 57 | 58 | - מעבר לESM; עדיין נשלח לnpm קובץ cjs, כך שקוד CommonJS קיים לא אמור להישבר 59 | - - עדכון: בגרסה 6.1.2 תוקן באג עבור פרויקטים שכתובים ב-CommonJS 60 | - שימוש בספריית [stack-trace](https://www.npmjs.com/package/stack-trace) לצורך חילוץ מקור השגיאה, מה שמתקן מקרים מסוימים שהמקור לא זוהה כראוי 61 | 62 | ## ‏6.1.0 63 | 64 | - נוספה החזרת בוליאני מ`deleteCall` האם השיחה היתה קיימת או לא 65 | - תוקנו הטייפים (DTS) עבור שימוש בספרייה עם טייפסקריפט - **ראה [README.md#TypeScript](./README.md#typescript) לבעיות ידועות** 66 | 67 | ## ‏6.0.3 68 | 69 | תיקון שינוי ברירות מחדל ברמת ראוטר (לאחר אתחול הראוטר). 70 | 71 | ## ‏6.0.2 72 | 73 | - תיקון טעות בlogger שגרמה להדפסה מיותרת של לוגים מסוימים כאשר מוגדר timeout, גם כאשר `printLog` הוגדר ל`false` 74 | 75 | תיקונים במנגנון הטיפול בשגיאות: 76 | 77 | - מניעת קריסה מתחת Node.js v18 78 | - מניעת קריסה בשגיאות ללא stack 79 | 80 | ## ‏6.0.1 81 | 82 | תוקנה בעיה בשרשור פעולות (`prependToNextAction`) שבה רק הפעולה האחרונה בוצעה. 83 | 84 | ## ‏6.0.0 85 | 86 | ⚠️ גרסה 6 כוללת **שינויים שוברים** בAPI של הספריה ⚠️ 87 | 88 | לפני שדרוג פרויקט קיים לגרסה זו יש לוודא שעדכנתם את הקוד הקיים לשינויים השוברים המפורטים להלן: 89 | 90 | ### שינויים שוברים 91 | 92 | - ‫הוסרו המשתנים `call.query` ו-`call.body`, במקומם נוסף המשתנה `call.values` שמכיל את הquery/body בהתאמה, לפי שיטת הפניה מימות - GET או POST. 93 | - הוסרה האפשרות להוסיף פונקציית שיחה ע"י `add_fn` שסומנה כמיושנת בגרסה 5.0.0 ניתן להשתמש במקום ב`get`/`post`/`all`, כמו באקספרס רגיל. 94 | - הוסר המשתנה `call.params`, ניתן להשתמש ב`call.req.params` במקום. 95 | - האפשרות `uncaughtErrorsHandler` שונתה לכתיב הנכון - `uncaughtErrorHandler` (בלי ה-s). 96 | - הפרמטר הבוליאני של `id_list_message` שהגדיר האם לחבר את ההודעות לפעולה הבאה לצורך שרשור פעולות **הוסר**, ובמקומו יש להעביר אובייקט, עם `prependToNextAction: true`, פרטים נוספים [בתיעוד](#id_list_messagemessages-options). 97 | - ההגדרות בכתיב השגוי `lenght_max` ו-`lenght_max` שהוצאו משימוש בגרסה 5.0.0, הוסרו סופית ולא יעבדו יותר. 98 | - הסרת סוג השגיאה `InputValidationError` (בעת הצורך תיזרק שגיאה חדשה - `CallError`). 99 | - שמות של אופציות רבות בread שלא היו מספיק ברורות שונו, וב2 מהן אף שונתה ברירת המחדל כדי להתאים לברירת המחדל של ימות המשיח. 100 | 101 | **להלן פירוט השינויים בשמות האופציות:** 102 | 103 |
104 | 105 | | שם ההגדרה הישן | שם ההגדרה החדש | סוג | הערות | 106 | | ------------------------ | -------------------------------- | ------------- | --------------------------------------------------------------------------------------------------------------- | 107 | | `play_ok_mode` | `typing_playback_mode` | *️⃣ הקשות | | 108 | | `read_none` | `allow_empty` | *️⃣ הקשות | | 109 | | `read_none_var` | `empty_val` | *️⃣ הקשות | | 110 | | `block_change_type_lang` | `block_change_keyboard` | *️⃣ הקשות | | 111 | | `min` | `min_digits` | *️⃣ הקשות | | 112 | | `max` | `max_digits` | *️⃣ הקשות | | 113 | | `block_zero` | `block_zero_key` | *️⃣ הקשות | | 114 | | `block_asterisk` | `block_asterisk_key` | *️⃣ הקשות | | 115 | | `record_ok` | `no_confirm_menu` | 🎙️ הקלטה | ברירת מחדל **משמיע** תפריט אישור, הפוך מההגדרה הישנה, כלומר false בהגדרה הישנה שקול לברירת מחדל של החדשה - true | 116 | | `record_hangup` | `save_on_hangup` | 🎙️ הקלטה | | 117 | | `record_attach` | `append_to_existing_file` | 🎙️ הקלטה | | 118 | | `length_min` | `length_min` | 🎙️ הקלטה | | 119 | | `length_max` | `max_length` | 🎙️ הקלטה | | 120 | | `allow_typing` | `block_typing` | 🗣️ זיהוי דיבור | ברירת מחדל **מאפשר** הקשות, הפוך מההגדרה הישנה | 121 | | `use_records_engine` | `use_records_recognition_engine` | 🗣️ זיהוי דיבור | | 122 | | `length_max` | `max_length` | 🗣️ זיהוי דיבור | | 123 | 124 | טיפ - ניתן להשתמש בביטוי הרגולרי הבא כדי לבצע חיפוש בכל הפרויקט בתוכנת VSCode, וכך למצוא בקלות הגדרות בשמות הישנים הדורשים טיפול: 125 | 126 | ```text 127 | play_ok_mode:| read_none:| read_none_var:| block_change_type_lang:| min:| max:| block_zero:| block_asterisk:| length_min:| length_max: 128 | ``` 129 | 130 | ### תכונות חדשות 131 | 132 | - מנגנון ברירות מחדל, ראה בפירוט ב[תיעוד](./README.md#ברירות-מחדל). 133 | - נוסף דגל להסרה שקטה של תווים לא חוקיים מהקראת טקסט - `removeInvalidChars`. ראו פירוט [בתיעוד](#תווים-לא-חוקיים-בהקראת-טקסט). 134 | - נוספה תמיכה להגדרות חסרות באפשרות זיהוי דיבור (stt) 135 | - נוספה מתודת `call.hangup()` (קיצור ל `call.go_to_folder('hangup')`) 136 | - ניתן להעביר לאתחול הראוטר אופציות עבור אקספרס ראוטר עצמו - ראה [פירוט בתיעוד express.js](https://expressjs.com/en/api.html#express.router) על האופציות הזמינות. 137 | - תמיכה בכל הפורמטים הקבילים של [ספריית ms](https://npmjs.com/ms) בהגדרת timeout 138 | - ‏ניתן להעביר בערך `empty_val` גם ערכים פרימיטיביים אחרים - לדוגמה `null`, `false` או `""` והערך שיתקבל מהread במקרה של חוסר תגובה יהיה הערך הפרימיטיבי שהוגדר. 139 | - [מערכת אוונטים](./README.md#Events) - כרגע האוונטים: `new_call`, `call_hangup`, `call_continue`. מקבל כארגומנט את מופע השיחה, ראה [דוגמה](./example.js#L14-L24). 140 | 141 | ### תיקוני באגים ושיפורים 142 | 143 | - שיפורים פנימיים משמעותיים המונעים באגים והתנהגויות לא צפויות 144 | - התיעוד עודכן ושופר משמעותית 145 | 146 | ## ‏5.1.2 147 | 148 | תוקנה התמיכה בבקשות POST (ההגדרה `api_url_post=yes` בשלוחה), שבהן הפרמטרים נשלחים ב`body` ולא ב`query` 149 | 150 | נוסף פרוקסי שמיירט נסיון גישה לreq.query בבקשות POST או לreq.body בבקשות GET, ומציג הסבר מפורט לתיקון. 151 | 152 | ## ‏5.1.4 153 | 154 | תוקן באג בid_list_message ללא שרשור לפעולה. 155 | 156 | ## ‏5.1.3 157 | 158 | תוקנה האופציה timeout באתחול הראוטר. 159 | 160 | ## ‏5.1.1 161 | 162 | תוקן באג שבו ניתוק מחוץ לפונקציה (לדוגמה השמעת id_list_message, יציאה מהשלוחה ואז ניתוק) היה מפעיל את הפונקציה. 163 | 164 | ## ‏5.0.1 165 | 166 | כל הפרמטרים בURL (query params) שמתחילים במילה `Api` (פרמטרים אוטומטיים של ימות), לדוגמה `ApiExtension`, `ApiPhone`, כן מוזרקים אוטומטית לאובייקט הCall. 167 | 168 | ## ‏5.0.0 169 | 170 | גרסה 5 כוללת שינויים רבים, כולל שינויים שוברים, ושכתוב משמעותי של הAPI הפנימי. 171 | 172 | **שינויים שוברים עיקריים:** 173 | 174 | - ‫שם המחלקה `Yemot_router` הוחלף ל `YemotRouter` 175 | 176 | - הפרמטרים מהurl לא מוזרקים אוטומטית לאובייקט הcall, אלא זמינים תחת call.req - `call.req.params`/`call.req.query`, בהתאמה, או בקיצור - `call.params`/`call.query`. 177 | 178 | - סוג שגיאה חדש: `InputValidationError` - נפלט כאשר הועבר קלט לא חוקי, למשל השמעת הודעת טקסט המכילה תו נקודה (הוסר בגרסה 6). 179 | 180 | - ניתן להשתמש במתודות get/post/all כמו באקספרס רגיל. כרגע מתודת `add_fn` נשמרת לצורך תאימות, אבל מומלץ לעדכן (עדכון: הוסר בגרסה 6). 181 | 182 | - ‫`lenght_min` בread מסוג הקלטה תוקן ל`length_min`, כנ"ל `lenght_max` תוקן ל`length_max`. כרגע הכתיב השגוי עדיין נתמך, אבל יוסר בהמשך (שונה ל`min_length`/`max_length` בגרסה 6). 183 | 184 | - שליטה באתחול הראוטר האם יודפסו לוגים פנימיים של הספריה (ברירת מחדל לא - בשונה מבגרסאות הקודמות) 185 | 186 | - ‫שמות משתנים הומרו לCamelCase כמקובל, לדוגמה `call_id` הומר ל`callId` וכן הלאה. 187 | 188 | בנוסף שיפורים ושינויים רבים לא שוברים, לדוגמה: 189 | 190 | - לוג מפורט בהעברת תווים לא חוקיים 191 | 192 | - אפשרות העברת מטפל לשגיאות כלליות שלא נתפסו - לא שגיאות פנימיות של הספריה כמו `HangupError`, אלא שגיאה לא צפויה. מאפשר לדוגמה לשלוח מייל למפתח עם לוג מפורט, ולהשמיע למשתמש הודעת שגיאה כללית במקום שהתהליך יקרוס. 193 | 194 | - שינויים ושיפורים רבים נוספים מאחורי הקלעים. 195 | 196 | ## ‏4.0.0 197 | 198 | בגרסה 4.0.0 שינוי משמעותי: 199 | 200 | במקום לבדוק בכל פעם את הפרמטר `hangup` כעת בעת ניתוק תיזרק שגיאה פנימית - `HangupError`, 201 | 202 | ‫ניתן לתפוס אותה להתנהגות מותאמת אישית, כלומר שאם מבקשים נתון על ידי read והמשתמש מנתק, תתבצע פעולה, לדוגמה למחוק רשומות שכבר נוצרו בשיחה. 203 | 204 | אם לא תופסים את השגיאת ניתוק, היא תגרום לעצירה של ריצת הפונקציה (**ללא עצירה של התהליך כולו**, כיוון שהשגיאה נתפסת ברמה יותר גבוהה - ע"י הספריה). 205 | -------------------------------------------------------------------------------- /lib/call.js: -------------------------------------------------------------------------------- 1 | import { makeMessagesData, makeTapModeRead, makeSttModeRead, makeRecordModeRead } from './response-functions.js'; 2 | import { HangupError, TimeoutError, ExitError, CallError } from './errors.js'; 3 | import colors from 'colors'; 4 | import ms from 'ms'; 5 | 6 | function shiftDuplicatedValues (values) { 7 | /** אם יש ערך מסויים כמה פעמים, שייקבע רק האחרון **/ 8 | for (const key of Object.keys(values)) { 9 | const value = values[key]; 10 | if (Array.isArray(value)) { 11 | values[key] = value[value.length - 1]; 12 | } 13 | } 14 | return values; 15 | } 16 | 17 | class Call { 18 | #eventsEmitter; 19 | #defaults; 20 | #valNameIndex; 21 | #timeoutId; 22 | #_responsesTextQueue; 23 | #responsesTextQueue; 24 | #values; 25 | 26 | constructor (callId, eventsEmitter, defaults) { 27 | this.#eventsEmitter = eventsEmitter; 28 | this.#defaults = defaults; 29 | 30 | this.callId = callId; 31 | this.did = ''; 32 | this.phone = ''; 33 | this.real_did = ''; 34 | this.extension = ''; 35 | 36 | this.#valNameIndex = 0; 37 | this.#_responsesTextQueue = ''; 38 | this.#responsesTextQueue = { 39 | pull: () => { 40 | const queueText = this.#_responsesTextQueue; 41 | this.#_responsesTextQueue = ''; 42 | return queueText; 43 | }, 44 | push: (newResText) => { 45 | this.#_responsesTextQueue += `${newResText}&`; 46 | } 47 | }; 48 | } 49 | 50 | get defaults () { 51 | return this.#defaults; 52 | } 53 | 54 | set defaults (newDefaults) { 55 | if (newDefaults.id_list_message?.prependToNextAction) { 56 | throw new CallError({ message: 'prependToNextAction is not supported as default', call: this }); 57 | } 58 | 59 | if (typeof newDefaults.read?.timeout !== 'undefined' && !ms(newDefaults.read.timeout)) { 60 | throw new CallError({ message: 'timeout must be a valid ms liberty string/milliseconds number', call: this }); 61 | } 62 | 63 | this.#defaults = { 64 | ...this.#defaults, 65 | ...newDefaults 66 | }; 67 | } 68 | 69 | get values () { 70 | return this.#values; 71 | } 72 | 73 | set values (newValues) { 74 | throw new CallError({ message: 'call.values is read-only', call: this }); 75 | } 76 | 77 | #logger (msg, color = 'blue') { 78 | if (!this.#defaults.printLog) return; 79 | console.log(colors[color](`[${this.callId}]: ${msg}`)); 80 | } 81 | 82 | async read (messages, mode = 'tap', options = {}) { 83 | if (!Array.isArray(messages)) { 84 | throw new CallError({ message: `messages must be array, got ${typeof messages}`, call: this }); 85 | } 86 | 87 | if (!messages.length) { 88 | throw new CallError({ message: 'messages must not be empty array', call: this }); 89 | } 90 | 91 | if (messages.some((message) => typeof message !== 'object')) { 92 | throw new CallError({ message: `message must be object, one or more of the messages got type ${typeof messages}`, call: this }); 93 | } 94 | 95 | if (messages.some((message) => typeof message.type !== 'string')) { 96 | throw new CallError({ message: `message type must be string, one or more of the messages got type ${typeof messages.type}`, call: this }); 97 | } 98 | 99 | messages.forEach((message) => { 100 | if (typeof message.data === 'undefined') { 101 | throw new CallError({ message: 'message data is required, got undefined', call: this }); 102 | } 103 | if (message.type !== 'zmanim' && message.type !== 'music_on_hold') { 104 | if (['number', 'digits'].includes(message.type) && Number.isInteger(message.data)) return; 105 | if (typeof message.data !== 'string') throw new CallError({ message: `message (in type ${message.type}) data must be string, got ${typeof message.data}`, call: this }); 106 | } 107 | }); 108 | 109 | if (!['tap', 'stt', 'record'].includes(mode)) { 110 | throw new CallError({ message: `mode '${mode}' is invalid. valid modes are: tap, stt, record`, call: this }); 111 | } 112 | 113 | let valName; 114 | let responseText; 115 | const sendResp = async () => { 116 | if (options.val_name) { 117 | valName = options.val_name; 118 | } else { 119 | this.#valNameIndex++; 120 | valName = `val_${this.#valNameIndex}`; 121 | } 122 | 123 | const messagesCombined = makeMessagesData(messages, { 124 | removeInvalidChars: options.removeInvalidChars ?? this.#defaults.read.removeInvalidChars ?? this.#defaults.removeInvalidChars 125 | }, this); 126 | 127 | const readOptions = { 128 | ...this.#defaults.read[mode], 129 | timeout: this.#defaults.read.timeout, 130 | re_enter_if_exists: this.#defaults.read.re_enter_if_exists, 131 | removeInvalidChars: this.#defaults.read.removeInvalidChars ?? this.#defaults.removeInvalidChars, 132 | ...options, 133 | valName 134 | }; 135 | 136 | const readOptionsDeprecates = [ 137 | ['play_ok_mode', 'typing_playback_mode'], 138 | ['read_none', 'allow_empty'], 139 | ['read_none_var', 'empty_val'], 140 | ['block_change_type_lang', 'block_change_keyboard'], 141 | ['min', 'min_digits'], 142 | ['max', 'max_digits'], 143 | ['block_zero', 'block_zero_key'], 144 | ['block_asterisk', 'block_asterisk_key'], 145 | ['record_ok', 'no_confirm_menu'], 146 | ['record_hangup', 'save_on_hangup'], 147 | ['record_attach', 'append_to_existing_file'], 148 | ['allow_typing', 'block_typing'], 149 | ['use_records_engine', 'use_records_recognition_engine'], 150 | ['lenght_min', 'min_length'], 151 | ['length_min', 'min_length'], 152 | ['lenght_max', 'min_length'], 153 | ['length_max', 'max_length'] 154 | ]; 155 | 156 | for (const [oldName, newName] of readOptionsDeprecates) { 157 | if (typeof readOptions[oldName] !== 'undefined') { 158 | throw new CallError({ message: `read option '${oldName}' is deprecated, use '${newName}' instead`, call: this }); 159 | } 160 | } 161 | 162 | if (mode === 'tap') { 163 | responseText = makeTapModeRead(messagesCombined, readOptions, this); 164 | } else if (mode === 'stt') { 165 | responseText = makeSttModeRead(messagesCombined, readOptions, this); 166 | } else if (mode === 'record') { 167 | responseText = makeRecordModeRead(messagesCombined, readOptions, this); 168 | } 169 | 170 | this.send(this.#responsesTextQueue.pull() + responseText); 171 | 172 | await this.blockRunningUntilNextRequest(options.timeout ?? this.#defaults.read.timeout); 173 | 174 | if (!this.#values[valName]) { 175 | await sendResp(); 176 | } 177 | }; 178 | 179 | await sendResp(); 180 | 181 | const value = this.#values[valName]; 182 | if (options.allow_empty && String(value) === String(options.empty_val)) { 183 | return options.empty_val; 184 | } 185 | return value; 186 | } 187 | 188 | go_to_folder (target) { 189 | const responseTxt = `go_to_folder=${target}`; 190 | this.send(this.#responsesTextQueue.pull() + responseTxt); 191 | throw new ExitError(this, { target, caller: 'go_to_folder' }); 192 | } 193 | 194 | restart_ext () { 195 | const currentFolder = this.extension; 196 | return this.go_to_folder(`/${currentFolder}`); 197 | } 198 | 199 | hangup () { 200 | return this.go_to_folder('hangup'); 201 | } 202 | 203 | /** 204 | * @param {Msg[]} messages 205 | * @param {Object} options 206 | * @param {Boolean} options.prependToNextAction 207 | * @param {Boolean} options.removeInvalidChars 208 | */ 209 | id_list_message (messages, options = {}) { 210 | if (!Array.isArray(messages)) { 211 | throw new CallError({ message: `messages must be array, got ${typeof messages}.\nmessages got: ${JSON.stringify(messages)}`, call: this }); 212 | } 213 | 214 | messages.forEach((message) => { 215 | if (typeof message.data === 'undefined') { 216 | throw new CallError({ message: 'message data is required, got undefined', call: this }); 217 | } 218 | if (message.type !== 'zmanim' && message.type !== 'music_on_hold') { 219 | if (['number', 'digits'].includes(message.type) && Number.isInteger(message.data)) return; 220 | if (typeof message.data !== 'string') throw new CallError({ message: `message (in type ${message.type}) data must be string, got ${typeof message.data}`, call: this }); 221 | } 222 | }); 223 | 224 | if (!['object', 'undefined'].includes(typeof options)) { 225 | throw new CallError({ message: `if you pass options argument to id_list_message it must be object, but got ${typeof options} ('${options}')`, call: this }); 226 | } 227 | 228 | const { prependToNextAction = false } = options; 229 | 230 | if (prependToNextAction && messages.some((message) => message?.type === 'go_to_folder')) { 231 | throw new CallError({ message: 'if you use go_to_folder message type in id_list_message you can\'t use prependToNextAction=true!', call: this }); 232 | } 233 | 234 | const goToFolderMessageIndex = messages.findIndex((message) => message?.type === 'go_to_folder'); 235 | if (goToFolderMessageIndex !== -1 && goToFolderMessageIndex !== messages.length - 1) { 236 | throw new CallError({ message: 'go_to_folder message type must be the last message in id_list_message. No further messages can be threaded after this message type', call: this }); 237 | } 238 | 239 | const messagesCombined = makeMessagesData(messages, { removeInvalidChars: options.removeInvalidChars ?? this.#defaults.id_list_message.removeInvalidChars ?? this.#defaults.removeInvalidChars }, this); 240 | const responseTxt = `id_list_message=${messagesCombined}`; 241 | 242 | if (prependToNextAction) { 243 | this.#responsesTextQueue.push(responseTxt); 244 | } else { 245 | if (this.res._headerSent) { 246 | this.#logger('Cannot send id_list_message after sending response (probably done from uncaughtErrorHandler due to error in asynchronous code after returning response)', 'red'); 247 | return; 248 | } 249 | this.send(this.#responsesTextQueue.pull() + responseTxt + '&'); 250 | throw new ExitError(this, { 251 | target: goToFolderMessageIndex !== -1 ? messages[goToFolderMessageIndex].data : `parent of /${this.extension}`, 252 | caller: goToFolderMessageIndex !== -1 ? 'go_to_folder' : 'id_list_message' 253 | }); 254 | } 255 | } 256 | 257 | routing_yemot (number) { 258 | const responseTxt = `routing_yemot=${number}`; 259 | this.send(this.#responsesTextQueue.pull() + responseTxt); 260 | throw new ExitError(this, { target: `other system - ${number}`, caller: 'routing_yemot' }); 261 | } 262 | 263 | send (data) { 264 | this.res.send(data); 265 | } 266 | 267 | setReqValues (req, res) { 268 | this.req = req; 269 | this.res = res; 270 | 271 | this.#values = shiftDuplicatedValues(req.method === 'POST' ? req.body : req.query); 272 | 273 | this.phone = this.#values.ApiPhone; 274 | this.callId = this.#values.ApiCallId; 275 | this.did = this.#values.ApiDID; 276 | this.real_did = this.#values.ApiRealDID; 277 | this.extension = this.#values.ApiExtension; 278 | 279 | const valuesToInject = Object.keys(this.#values).filter((key) => key.startsWith('Api')); 280 | for (const key of valuesToInject) { 281 | this[key] = this.#values[key]; 282 | } 283 | } 284 | 285 | async blockRunningUntilNextRequest (timeout) { 286 | this.#logger(`🔒 fn running blocked - waiting to next request from yemot ${timeout ? `(timeout ${timeout === 'number' ? ms(timeout, { long: true }) : ms(ms(timeout), { long: true })} is defined)` : ''}`); 287 | return new Promise((resolve, reject) => { 288 | clearTimeout(this.#timeoutId); 289 | if (timeout) { 290 | const timeoutMilliseconds = typeof timeout === 'number' ? timeout : ms(timeout); 291 | this.#timeoutId = setTimeout(() => { 292 | if (!this.res._headerSent) this.res.json({ message: 'timeout' }); 293 | reject(new TimeoutError(this, timeoutMilliseconds)); 294 | }, timeoutMilliseconds); 295 | } 296 | this.#eventsEmitter.once(this.callId, (isHangup) => { 297 | clearTimeout(this.#timeoutId); 298 | this.#logger('🔓 fn running unblocked'); 299 | if (isHangup) { 300 | if (!this.res._headerSent) this.res.json({ message: 'hangup' }); 301 | reject(new HangupError(this)); 302 | } else { 303 | resolve(); 304 | } 305 | }); 306 | }); 307 | } 308 | 309 | get query () { 310 | throw new CallError({ message: 'call.query is deprecated, use call.values instead', call: this }); 311 | } 312 | 313 | get body () { 314 | throw new CallError({ message: 'call.body is deprecated, use call.values instead', call: this }); 315 | } 316 | 317 | get params () { 318 | throw new CallError({ message: 'call.params is deprecated, use call.req.params instead', call: this }); 319 | } 320 | } 321 | 322 | export default Call; 323 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | import { type EventEmitter } from 'events'; 2 | import { type Router, type Request, type Response } from 'express'; 3 | export { SYSTEM_MESSAGE_CODES, SystemMessageCode } from './system-messages' 4 | import { SystemMessageCode } from './system-messages'; 5 | 6 | /** 7 | * הגדרות למיקום וזמן עבור סוג הודעה 'zmanim' 8 | */ 9 | export interface ZmanimData { 10 | time?: string; 11 | zone?: string; 12 | difference?: string; 13 | } 14 | 15 | /** 16 | * הגדרות למוזיקה בהמתנה עבור סוג הודעה 'music_on_hold' 17 | */ 18 | export interface MusicOnHoldData { 19 | musicName: string; 20 | maxSec?: number; 21 | } 22 | 23 | /** 24 | * מייצג "הודעה" הניתנת להשמעה ב‫`read`/`id_list_message` 25 | */ 26 | export type Msg = 27 | | ({ type: 'file'; data: string; removeInvalidChars?: boolean }) 28 | | ({ type: 'text'; data: string; removeInvalidChars?: boolean }) 29 | | ({ type: 'speech'; data: string; removeInvalidChars?: boolean }) 30 | | ({ type: 'digits'; data: number | string; removeInvalidChars?: boolean }) 31 | | ({ type: 'number'; data: number | string; removeInvalidChars?: boolean }) 32 | | ({ type: 'alpha'; data: string; removeInvalidChars?: boolean }) 33 | | ({ type: 'zmanim'; data: ZmanimData; removeInvalidChars?: boolean }) 34 | | ({ type: 'go_to_folder'; data: string; removeInvalidChars?: boolean }) 35 | | ({ type: 'system_message'; data: SystemMessageCode; removeInvalidChars?: boolean }) 36 | | ({ type: 'music_on_hold'; data: MusicOnHoldData; removeInvalidChars?: boolean }) 37 | | ({ type: 'date'; data: string; removeInvalidChars?: boolean }) 38 | | ({ type: 'dateH'; data: string; removeInvalidChars?: boolean }); 39 | 40 | interface Defaults { 41 | /** 42 | * האם להדפיס לוג מפורט על כל קריאה לשרת, שיחה חדשה, ניתוק ועוד. שימושי לפיתוח ודיבוג
43 | * @see [router.events](https://github.com/ShlomoCode/yemot-router2#events) - קבלת קאלבק על שיחה חדשה/קבלת נתון ממאזין/ניתוק שיחה, למשתמשים מתקדמים. לא תלוי בהגדרה זו 44 | * @default false 45 | */ 46 | printLog?: boolean 47 | /** 48 | * האם להסיר אוטומטית תווים לא חוקיים (`.`,`-`,`'`,`"`,`&`) מתשובות הקראת טקסט
49 | * ,באם לא מוגדרת הסרה (ברירת מחדל), תיזרק שגיאה
50 | * באם מוגדרת הסרה ‫(`true`) התווים יוסרו מהתשובה שתוחזר לימות ולא תיזרק שגיאה
51 | * ניתן להגדיר ערך זה ב3 רמות, ראו [תיעוד מלא](https://github.com/ShlomoCode/yemot-router2#%D7%AA%D7%95%D7%95%D7%99%D7%9D-%D7%9C%D7%90-%D7%97%D7%95%D7%A7%D7%99%D7%99%D7%9D-%D7%91%D7%94%D7%A7%D7%A8%D7%90%D7%AA-%D7%98%D7%A7%D7%A1%D7%98) 52 | * @default false 53 | */ 54 | removeInvalidChars?: boolean 55 | read?: { 56 | /** 57 | * ‫ זמן המתנה לקבלת נתון מהמשתמש (במילישניות). במידה ולא התקבל הנתון בזמן הנ"ל, השיחה תימחק מה`activeCalls` וגם אם המשתמש יקיש השרת יקבל אותו כשיחה חדשה
58 | * ‫מקבל מספר מילישיניות, או מחרוזת הקבילה ע"י ספריית [ספריית ms](https://npmjs.com/ms).
59 | * ‫מכסה גם מקרי קצה שלא התקבלה מימות אות ניתוק (קריאה עם `hangup=yes` בפרטרים)
60 | * 61 | * ‫יש לשים לב לא להגדיר ערך נמוך שמחייג לגיטימי שלא הקיש מיד תשובה עלול להיתקל ב-timeout
62 | * 63 | * ברירת מחדל: אין ‫timeout 64 | */ 65 | timeout?: number 66 | tap?: TapOptions 67 | stt?: SstOptions 68 | record?: RecordOptions 69 | } 70 | id_list_message?: IdListMessageOptions 71 | } 72 | 73 | // from @types/express - https://github.com/DefinitelyTyped/DefinitelyTyped/blob/f800de4ffd291820a9e444e6b6cd3ac9b4a16e53/types/express/index.d.ts#L73-#L92 74 | interface ExpressRouterOptions { 75 | /** 76 | * Enable case sensitivity. 77 | */ 78 | caseSensitive?: boolean | undefined 79 | 80 | /** 81 | * Preserve the req.params values from the parent router. 82 | * If the parent and the child have conflicting param names, the child’s value take precedence. 83 | * 84 | * @default false 85 | * @since 4.5.0 86 | */ 87 | mergeParams?: boolean | undefined 88 | 89 | /** 90 | * Enable strict routing. 91 | */ 92 | strict?: boolean | undefined 93 | } 94 | 95 | type YemotRouterOptions = ExpressRouterOptions & { 96 | /** 97 | * ‫ זמן המתנה לקבלת נתון מהמשתמש (במילישניות). במידה ולא התקבל הנתון בזמן הנ"ל, השיחה תימחק מה`activeCalls` וגם אם המשתמש יקיש השרת יקבל אותו כשיחה חדשה
98 | * ‫מקבל מספר מילישיניות, או מחרוזת הקבילה ע"י ספריית [ספריית ms](https://npmjs.com/ms).
99 | * ‫מכסה גם מקרי קצה שלא התקבלה מימות אות ניתוק (קריאה עם `hangup=yes` בפרטרים)
100 | * 101 | * ‫יש לשים לב לא להגדיר ערך נמוך שמחייג לגיטימי שלא הקיש מיד תשובה עלול להיתקל ב-timeout
102 | * 103 | * ברירת מחדל: אין ‫timeout 104 | */ 105 | timeout?: number 106 | /** 107 | * האם להדפיס לוג מפורט על כל קריאה לשרת, שיחה חדשה, ניתוק ועוד. שימושי לפיתוח ודיבוג
108 | * @see [router.events](https://github.com/ShlomoCode/yemot-router2#events) - קבלת קאלבק על שיחה חדשה/קבלת נתון ממאזין/ניתוק שיחה, למשתמשים מתקדמים. לא תלוי בהגדרה זו 109 | * @default false 110 | */ 111 | printLog?: boolean 112 | /** 113 | * הגדרת קאלבק לטיפול בשגיאות בתוך שיחה, שימושי לדוגמה לשמירת לוג + השמעת הודעה למשתמש, במקום קריסה של השרת (והמשתמש ישמע "אין מענה משרת ‫API")
114 | * ראה דוגמה ב‫[example.js](https://github.com/ShlomoCode/yemot-router2/blob/master/example.js) 115 | */ 116 | uncaughtErrorHandler?: (error: Error, call: Call) => void | Promise 117 | defaults?: Defaults 118 | }; 119 | 120 | type CallHandler = (call: Call) => Promise; 121 | 122 | interface RouterEventEmitter extends EventEmitter { 123 | on: (eventName: 'call_hangup' | 'call_continue' | 'new_call', listener: (call: Call) => void) => this 124 | once: (eventName: 'call_hangup' | 'call_continue' | 'new_call', listener: (call: Call) => void) => this 125 | } 126 | 127 | export function YemotRouter (options?: YemotRouterOptions): { 128 | /** 129 | * הוספת הנלדר לשיחות (במתודת ‫`GET` בלבד) 130 | */ 131 | get: (path: string, handler: CallHandler) => void 132 | /** 133 | * הוספת הנלדר לשיחות (במתודת ‫`POST` בלבד - ‫`api_url_post=yes`) 134 | */ 135 | post: (path: string, handler: CallHandler) => void 136 | /** 137 | * הוספת הנלדר לשיחות במתודת ‫`GET` וגם במתודת ‫`POST` 138 | */ 139 | all: (path: string, handler: CallHandler) => void 140 | use: Router['use'] 141 | /** 142 | * delete call from active calls by callId 143 | * @returns true if the call was deleted, false if the call was not found 144 | */ 145 | deleteCall: (callId: string) => boolean 146 | events: RouterEventEmitter 147 | defaults: Defaults 148 | /** 149 | * מתודה לשימוש בטייפסקריפט בלבד 150 | * --------------- 151 | * מחזיר את הראוטר ‫(`YemotRouter`) כפי שהוא - עם טייפ של ‫`Express.Router`, למניעת שגיאת טייפ 152 | * @example ```ts 153 | * import express from 'express'; 154 | * import { YemotRouter } from 'yemot-router'; 155 | * 156 | * const app = express(); 157 | * const router = YemotRouter(); 158 | * 159 | * app.use(router.asExpressRouter); // 👈👈👈 160 | * ``` 161 | */ 162 | asExpressRouter: Router 163 | }; 164 | 165 | // based of https://tchumim.com/post/157692, https://tchumim.com/post/157706 166 | interface ReadModes { 167 | tap: TapOptions 168 | stt: SstOptions 169 | record: RecordOptions 170 | } 171 | 172 | /** 173 | * מייצג "שיחה", עליה ניתן להפעיל מתודות שונות כמו ‫`read`, `id_list_message`, `go_to_folder` וכו' 174 | */ 175 | export interface Call { 176 | /** 177 | * מתודה לקבלת נתון מהמחייג, מחזירה ‫`Promise` שנפתר עם סטרינג עם התשובה (במקרה של בקשת הקלטה, יחזור נתיב הקובץ)
178 | * [פירוט נוסף על ‫read בתיעוד ימות המשיח](https://f2.freeivr.co.il/post/78283) 179 | * @param messages מערך ההודעות שיושמעו למשתמש לפני קבלת הנתון 180 | * @param mode סוג הנתון שמבקשים מהמחייג
181 | * `tap` = הקשות
182 | * `stt` = זיהוי דיבור
183 | * `record` = הקלטה 184 | * @param options אפשרויות נוספות לפי סוג הנתון - לדוגמה ספרות מותרות להקשה, מקסימום ספרות, וכולי 185 | */ 186 | read: (messages: Msg[], mode: T, options?: ReadModes[T]) => Promise 187 | /** 188 | * מתודה להעברת השיחה לשלוחה מסוימת במערכת הנוכחית 189 | * @param target נתיב למעבר, יחסי לשלוחה הנוכחית, יחסי לשלוחה הראשית (מתחיל ב`/`). [פירוט של האופציות הזמינות](https://f2.freeivr.co.il/post/58) 190 | * @see {@link hangup|`call.hangup()`} - קיצור לנוחות של ‫`go_to_folder('hangup')` 191 | */ 192 | go_to_folder: (target: string) => void 193 | /** 194 | * השמעת הודעה אחת או יותר
195 | *
196 | * 197 | * ⚠️ שים לב! ⚠️
198 | * לאחר השמעת ההודעות, השיחה תצא אוטומטית מהשלוחה! 199 | * באם מעוניינים לשרשר פעולה נוספת לאחר ההשמעה, לדוגמה להשמיע הודעה ואז לבצע ‫`read` (קבלת נתונים נוספים), יש להגדיר בארגומנט ה‫`options` את ‫`prependToNextAction` ל‫`true` 200 | * @param messages 201 | * @param options 202 | * @example השמעת הודעה ויציאה מהשלוחה 203 | * ```js 204 | * call.id_list_message([{ type: 'text', data: 'הודעה לדוגמה' }]); 205 | * ``` 206 | * @example השמעת הודעה והמשך לפעולה הבאה - לדוגמה ‫`read` 207 | * ```js 208 | * call.id_list_message([{ type: 'text', data: 'הודעה לדוגמה' }], { prependToNextAction: true }); 209 | * const res = await call.read([{ type: 'text', data: 'הקש משהו' }], 'tap'); 210 | * ``` 211 | */ 212 | id_list_message: (messages: Msg[], options?: IdListMessageOptions) => void 213 | /** 214 | * מתודה להעברת השיחה למערכת אחרת בימות המשיח ללא עלות יחידות, באמצעות ‫"ראוטינג ימות"
215 | * הפונקציה מקבלת ארגומנט יחיד - סטרינג של מספר מערכת בימות להעברת השיחה אליה
216 | */ 217 | routing_yemot: (number: string) => void 218 | /** 219 | * הפעלה מחדש של השלוחה הנוכחית
220 | *
221 | * 222 | * ‫קיצור לתחביר הבא: 223 | * ```js 224 | * call.go_to_folder(`/${call.ApiExtension}`); 225 | * ``` 226 | */ 227 | restart_ext: () => void 228 | /** 229 | * ניתוק השיחה. קיצור לתחביר הבא‫: 230 | * ```js 231 | * call.go_to_folder('hangup'); 232 | * ``` 233 | */ 234 | hangup: () => void 235 | blockRunningUntilNextRequest: () => Promise 236 | /** 237 | * ניתן להשתמש במתודה זו כדי לשלוח סטרינג חופשי לחלוטין, לדוגמה עבור פונקציונליות שעדיין לא נתמכת בספרייה
238 | * במתודה זו יש להעביר את הסטרינג בדיוק כפי שמעוניינים שהשרת של ימות יקבל אותו, והוא לא עובר אף ולידציה או עיבוד
239 | * 240 | * :כדי להשתמש לבקשת מידע - לדוגמה מעבר לסליקת אשראי, יש לשלב עם קריאות ל 241 | * ``` 242 | * await call.blockRunningUntilNextRequest(); 243 | * ``` 244 | */ 245 | send: (resp: string) => Response 246 | /** 247 | * מכיל את כל הפרמטרים שנשלחו מימות
248 | * אם הבקשה נשלחה ב-‫`HTTP GET`, יכיל את ה‫`query string`
249 | * אם הבקשה נשלחה ב-‫`HTTP POST` (‫`api_url_post=yes`), יכיל את ה‫`body` 250 | */ 251 | values: Readonly> 252 | /** 253 | * ברירות מחדל - ברמת שיחה (דורס את הברירות מחדל ברמת ראוטר) 254 | */ 255 | defaults: Defaults 256 | req: Request 257 | /** 258 | * מספר הטלפון **הראשי** של המערכת
259 | * קיצור של ‫{@link ApiDID|`call.ApiDID`} 260 | */ 261 | did: string 262 | /** 263 | * מספר הטלפון של המחייג
264 | * קיצור של ‫{@link ApiPhone|`call.ApiPhone`} 265 | */ 266 | phone: string 267 | /** 268 | * המספר אליו חייג המשתמש
269 | * במידה ויש כמה מספרים למערכת שלכם, והלקוח צלצל למספר משנה, הערך הזה יהיה שונה מהערך הקודם
270 | * קיצור של ‫{@link ApiRealDID|`call.ApiRealDID`} 271 | */ 272 | real_did: string 273 | /** 274 | * מזהה ייחודי לאורך השיחה
275 | * קיצור של ‫{@link ApiCallId|`call.ApiCallId`} 276 | */ 277 | callId: string 278 | /** 279 | * שם התיקייה/שלוחה בה נמצא המשתמש
280 | * קיצור של ‫{@link ApiExtension|`call.ApiExtension`} 281 | * @example "9" - שלוחה 9 בתפריט הראשי 282 | * @example "" - שלוחה ראשית 283 | */ 284 | extension: string 285 | /** 286 | * מזהה ייחודי לאורך השיחה 287 | *
288 | * @see {@link callId|`call.callId`} (קיצור לנוחות) 289 | */ 290 | ApiCallId: string 291 | /** 292 | * מספר הטלפון של המחייג 293 | *
294 | * @see {@link phone|`call.phone`} (קיצור לנוחות) 295 | */ 296 | ApiPhone: string 297 | /** 298 | * מספר הטלפון **הראשי** של המערכת 299 | *
300 | * @see {@link did|`call.did`} (קיצור לנוחות) 301 | */ 302 | ApiDID: string 303 | /** 304 | * המספר אליו חייג המשתמש
305 | * במידה ויש כמה מספרים למערכת והלקוח חייג למספר משנה, הערך הזה יהיה שונה מ ‫{@link ApiDID|`call.ApiDID`} 306 | *
307 | * @see {@link real_did|`call.real_did`} (קיצור לנוחות) 308 | */ 309 | ApiRealDID: string 310 | /** 311 | * שם התיקייה/שלוחה בה נמצא המשתמש 312 | *
313 | * @see {@link extension|`call.extension`} (קיצור לנוחות) 314 | * @example "9" - שלוחה 9 בתפריט הראשי 315 | * @example "" - שלוחה ראשית 316 | */ 317 | ApiExtension: string 318 | /** 319 | * במידה ובוצעה התחברות לפי זיהוי אישי, יצורף ערך זה המכיל את סוג ההתחברות וה-ID של המשתמש (מידע נוסף [כאן](https://f2.freeivr.co.il/post/1250)) 320 | */ 321 | ApiEnterID: string 322 | /** 323 | * שם משויך לזיהוי האישי (כפי שמוסבר בערך של `login_add_val_name=yes` [כאן](https://f2.freeivr.co.il/post/2015)) 324 | */ 325 | ApiEnterIDName: string 326 | /** 327 | * זמן בשניות מ1970, Epoch, i.e., since 1970-01-01 00:00:00 UTC 328 | * @example "1683594518" 329 | */ 330 | ApiTime: string 331 | /** 332 | * זהה ל`callId` של הcalls בAPI `GetCallsStatus` 333 | */ 334 | ApiYFCallId: string 335 | } 336 | 337 | /** 338 | * מייצג "הודעה" הניתנת להשמעה ב‫`read`/`id_list_message` 339 | */ 340 | export interface Msg { 341 | type: 'file' | 'text' | 'speech' | 'digits' | 'number' | 'alpha' | 'zmanim' | 'go_to_folder' | 'system_message' | 'music_on_hold' | 'date' | 'dateH' 342 | data: string | number | SystemMessageCode | { time?: string, zone?: string, difference?: string } | { musicName: string, maxSec?: number } 343 | /** 344 | * האם להסיר אוטומטית תווים לא חוקיים (`.`,`-`,`'`,`"`,`&`) מתשובות הקראת טקסט
345 | * ,באם לא מוגדרת הסרה (ברירת מחדל), תיזרק שגיאה
346 | * באם מוגדרת הסרה ‫(`true`) התווים יוסרו מהתשובה שתוחזר לימות ולא תיזרק שגיאה
347 | * ניתן להגדיר ערך זה ב3 רמות, ראו [תיעוד מלא](https://github.com/ShlomoCode/yemot-router2#%D7%AA%D7%95%D7%95%D7%99%D7%9D-%D7%9C%D7%90-%D7%97%D7%95%D7%A7%D7%99%D7%99%D7%9D-%D7%91%D7%94%D7%A7%D7%A8%D7%90%D7%AA-%D7%98%D7%A7%D7%A1%D7%98) 348 | * @default false 349 | */ 350 | removeInvalidChars?: boolean 351 | } 352 | 353 | interface GeneralReadOptions { 354 | /** 355 | * שם‫ הפרמטר בURL שבו יצורף הערך שהמשתמש הקיש
356 | * ברירת מחדל - נקבע אוטומטית: ‫`val_1`, `val_2`, `val_3` וכו' בסדר עולה 357 | */ 358 | val_name?: string 359 | /** 360 | * האם לבקש את הערך שוב אם הפרמטר בשם שנבחר ‫({@link GeneralReadOptions.val_name|`val_name`}) כבר קיים ‫בURL
361 | * ברירת מחדל - המערכת תבקש מחדש, במידה ומוגדר ‫`true` בערך זה, המערכת תשתמש בערך הקודם שהוקש ותשלח אותו בתור תשובה 362 | */ 363 | re_enter_if_exists?: boolean 364 | /** 365 | * האם להסיר אוטומטית תווים לא חוקיים (`.`,`-`,`'`,`"`,`&`) מתשובות הקראת טקסט
366 | * ,באם לא מוגדרת הסרה (ברירת מחדל), תיזרק שגיאה
367 | * באם מוגדרת הסרה ‫(`true`) התווים יוסרו מהתשובה שתוחזר לימות ולא תיזרק שגיאה
368 | * ניתן להגדיר ערך זה ב3 רמות, ראו [תיעוד מלא](https://github.com/ShlomoCode/yemot-router2#%D7%AA%D7%95%D7%95%D7%99%D7%9D-%D7%9C%D7%90-%D7%97%D7%95%D7%A7%D7%99%D7%99%D7%9D-%D7%91%D7%94%D7%A7%D7%A8%D7%90%D7%AA-%D7%98%D7%A7%D7%A1%D7%98) 369 | * @default false 370 | */ 371 | removeInvalidChars?: boolean 372 | } 373 | 374 | export interface TapOptions extends GeneralReadOptions { 375 | /** 376 | * כמות הספרות המקסימלית שהמשתמש יוכל להקיש
377 | * ברירת מחדל ללא הגבלה 378 | */ 379 | max_digits?: number 380 | /** 381 | * כמות ספרות מינימלית
382 | * ברירת מחדל: 1 383 | * @default 1 384 | */ 385 | min_digits?: number 386 | /** 387 | * זמן המתנה להקשה בשניות
388 | * ברירת מחדל: 7 שניות 389 | * @default 7 390 | */ 391 | sec_wait?: number 392 | /** 393 | * צורת ההשמעה למשתמש את הקשותיו
394 | * באם מעוניינים במקלדת שונה ממקלדת ספרות, כגון ‫`EmailKeyboard` או ‫`HebrewKeyboard`, יש להכניס כאן את סוג המקלדת [ראו example.js]
395 | * פירוט על כל אופציה ניתן למצוא בתיעוד מודול API של ימות המשיח, תחת"הערך השישי (הקשה)". 396 | * @default "No" 397 | */ 398 | typing_playback_mode?: 'Number' | 'Digits' | 'File' | 'TTS' | 'Alpha' | 'No' | 'HebrewKeyboard' | 'EmailKeyboard' | 'EnglishKeyboard' | 'DigitsKeyboard' | 'TeudatZehut' | 'Price' | 'Time' | 'Phone' | 'No' 399 | /** 400 | * האם לחסום מקש כוכבית 401 | * @default false 402 | */ 403 | block_asterisk_key?: boolean 404 | /** 405 | * האם לחסום מקש אפס 406 | * @default false 407 | */ 408 | block_zero_key?: boolean 409 | /** 410 | * החלפת תווים 411 | * החלפת מקש בכל סימן אחר
412 | * לדוגמה במידה ואתם רוצים שישלח כתובת ספריה כאשר המפריד בין התיקיות הוא סלש ‫(/)
413 | * בטלפון לא ניתן להקיש סלש, אבל ניתן לבקש מהלקוח להקיש כוכבית בין תיקייה לתיקייה, ולסמן להחליף את הכוכבית בסלש
414 | * ערך זה יכול להכיל 2 סימנים: הסימן הראשון את איזה ערך להחליף, הסימן השני זה מה לשים במקום מה שהוחלף
415 | * @example "*@"" 416 | * כלומר להחליף את מקש כוכבית בסלש 417 | */ 418 | replace_char?: string 419 | /** 420 | * איזה מקשים המשתמש יוכל להקיש
421 | * באם המשתמש יקיש מקש שלא הוגדר המערכת תודיע ‫‫`M1224` "בחירה לא חוקית"
422 | * 423 | * ברירת מחדל: המשתמש יכול להקיש על כל המקשים
424 | * @example [1, 2, '3', '*'] 425 | * @example [10, 20, 30, 40] 426 | */ 427 | digits_allowed?: Array 428 | /** 429 | * ברירת מחדל במידה והמשתמש לא הקיש כלום ועבר הזמן שהוגדר להקשה ‫({@link TapOptions.sec_wait}) הנתון שהמערכת קולטת הוא ריק
430 | * כמות הפעמים שהמערכת משמיעה את השאלה לפני שהיא מגדירה את הנתון כ"ריק" היא פעם אחת 431 | * בערך זה ניתן להגדיר כמות פעמים שונה 432 | * @see {@link TapOptions.allow_empty} 433 | * @default 1 434 | */ 435 | amount_attempts?: number 436 | /** 437 | * ברירת מחדל, במידה והנתון שהתקבל הוא "ריק" (ראה {@link TapOptions.amount_attempts}) המערכת משמיעה `M1002` "לא הוקשה בחירה" והמשתמש עובר להקשה מחודשת של הנתון 438 | * ניתן להגדיר שאם הנתון ריק המערכת תתקדם הלאה 439 | * @default false 440 | * @see {@link TapOptions.empty_val} 441 | */ 442 | allow_empty?: boolean 443 | /** 444 | * הערך שיישלח כשלא הוקשה תשובה. ברירת מחדל: ‫`"None"`
445 | * ניתן להעביר גם ערכים שאינם מחרוזת, לדוגמה ‫`null` והערך שיתקבל מה‫read יהיה ‫`null` ולא ‫`"null"` 446 | *
447 | * 448 | * :למשתמשי טייפסקריפט בלבד 449 | * --------------- 450 | * כאשר מגדירים ‫`empty_val` שאינו מסוג ‫`string`, כרגע ‫ה‫DTS לא מוגדר להסיק את הטייפ אוטומטית, ויש להגדיר אותו ידנית עם ‫`as`, דוגמה: 451 | * ```ts 452 | * const res = await call.read([{ type: 'text', data: 'please type one' }], 'tap', { 453 | * allow_empty: true, 454 | * empty_val: null, 455 | * }) as string | null; 456 | * ``` 457 | *
458 | * @default "None" 459 | */ 460 | empty_val?: any 461 | /** 462 | * האם לחסום שינוי מצב הקלדה
463 | * באם הוגדר ב‫{@link typing_playback_mode} מצב מקלדת, 464 | * ברירת מחדל המשתמש יכול בתפריט סיום או ביטול (במקש כוכבית) לשנות את סוג המקלדת.
465 | * באם הגדרה זו מופעלת, אם המשתמש מנסה לשנות שפה האופציה תיחסם, והמערכת תשמיע ‫`M4186` "שינוי שפת הקלדה חסום בכתיבה זו" 466 | * @default false 467 | */ 468 | block_change_keyboard?: boolean 469 | } 470 | 471 | export interface SstOptions extends GeneralReadOptions { 472 | /** 473 | * שפה לזיהוי הדיבור
474 | * ברירת מחדל: עברית (או מה שהוגדר בערך ‫`lang` בשלוחה)
475 | * [רשימת השפות הנתמכות](https://drive.google.com/file/d/1UC_KOjhZgPWZff8BcUfBLwMbSmKewy8A/view) 476 | */ 477 | lang?: string 478 | /** 479 | * שלא יהיה ניתן להקיש במקום לדבר
480 | * (ברירת המחדל היא שהמשתמש יכול או לדבר או להקיש) 481 | * @default false 482 | */ 483 | block_typing?: boolean 484 | /** 485 | * כמות הספרות המקסימלית שהמשתמש יוכל להקיש
486 | * ברירת מחדל: אין הגבלה 487 | */ 488 | max_digits?: number 489 | /** 490 | * האם להשתמש במנוע זיהוי דיבור של הקלטות (תומך בזיהוי ארוך, אך לא מאפשר או הקשה או דיבור)
491 | * @default false 492 | */ 493 | use_records_recognition_engine?: boolean 494 | /** 495 | * אחרי כמה שניות של שקט לסיים את ההקלטה (ברירת מחדל - לא מפסיק)
496 | * רלוונטי רק אם משתמשים במנוע זיהוי טקסטים ארוכים ‫({@link use_records_recognition_engine}) 497 | */ 498 | quiet_max?: number 499 | /** 500 | * מספר שניות מרבי להקלטה (ברירת מחדל: ללא הגבלה) 501 | */ 502 | length_max?: number 503 | } 504 | 505 | export interface RecordOptions extends GeneralReadOptions { 506 | /** 507 | * (היכן תישמר ההקלטה במערכת (נתיב לתקיה, שם הקובץ מוגדר בנפרד
508 | * 509 | * ברירת מחדל - נשמרת בתיקייה שמוגדרת ב ‫`api_dir` בשלוחה (כלומר תקייה נוכחית בברירת מחדל)
510 | * ניתן להגדיר מיקום שונה, לדוגמה ‫`8/` = שלוחה 8 בתפריט הראשי
511 | * הערה: חובה לשים `/` בהתחלה, אסור לשים `/` בסוף 512 | */ 513 | path?: string 514 | /** 515 | * שם הקובץ שיישמר (**ללא סיומת**)
516 | * ברירת מחדל - מיספור אוטומטי כקובץ הגבוה בשלוחה
517 | * לדוגמה אם הקובץ הכי גבוה בשלוחה היה 100, הקובץ החדש יהיה 101 518 | */ 519 | file_name?: string 520 | /** 521 | * האם לשמור את הקובץ בסיום ישירות, ללא תפריט שמיעת ההקלטה/אישור/הקלטה מחדש/המשך הקלטה 522 | * @default false 523 | */ 524 | no_confirm_menu?: boolean 525 | /** 526 | * @default true 527 | * האם לשמור את ההקלטה לקובץ אם המשתמש ניתק באמצע ההקלטה 528 | */ 529 | save_on_hangup?: boolean 530 | /** 531 | * במידה והוגדר שם קובץ לשמירה (file_name) וכבר קיים קובץ בשם שנבחר
532 | * האם לשנות את שם הקובץ הישן ולשמור את החדש בשם שנבחר (ברירת מחדל)
533 | * או לצרף את ההקלטה החדשה לסוף הקובץ הישן 534 | * @default false 535 | */ 536 | append_to_existing_file?: boolean 537 | /** 538 | * (כמות שניות מינימלית להקלטה (ברירת מחדל: אין מינימום 539 | */ 540 | min_length?: number 541 | /** 542 | * (כמות שניות מקסימלית להקלטה (ברירת מחדל: אין מקסימום 543 | */ 544 | max_length?: number 545 | } 546 | 547 | interface IdListMessageOptions { 548 | /** 549 | * האם להסיר אוטומטית תווים לא חוקיים (`.`,`-`,`'`,`"`,`&`) מתשובות הקראת טקסט
550 | * ,באם לא מוגדרת הסרה (ברירת מחדל), תיזרק שגיאה
551 | * באם מוגדרת הסרה ‫(`true`) התווים יוסרו מהתשובה שתוחזר לימות ולא תיזרק שגיאה
552 | * ניתן להגדיר ערך זה ב3 רמות, ראו [תיעוד מלא](https://github.com/ShlomoCode/yemot-router2#%D7%AA%D7%95%D7%95%D7%99%D7%9D-%D7%9C%D7%90-%D7%97%D7%95%D7%A7%D7%99%D7%99%D7%9D-%D7%91%D7%94%D7%A7%D7%A8%D7%90%D7%AA-%D7%98%D7%A7%D7%A1%D7%98) 553 | * @default false 554 | */ 555 | removeInvalidChars?: boolean 556 | /** 557 | * יש להגדיר במידה ומעוניינים לשרשר פעולות נוספות (לדוגמה read) 558 | * @default: false 559 | */ 560 | prependToNextAction?: boolean 561 | } 562 | 563 | export class CallError extends Error { 564 | readonly date: Date; 565 | readonly call: Call; 566 | 567 | constructor (options: { message: string, call: Call }); 568 | } 569 | 570 | export class ExitError extends CallError { 571 | readonly context: { 572 | caller: 'go_to_folder' | 'id_list_message' | 'routing_yemot' 573 | target: string 574 | }; 575 | 576 | constructor (call: Call, context: ExitError['context']); 577 | } 578 | 579 | export class HangupError extends CallError { 580 | constructor (call: Call); 581 | } 582 | 583 | export class TimeoutError extends CallError { 584 | readonly timeout: number; 585 | 586 | constructor (call: Call, timeout: number); 587 | } 588 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # yemot-router2 2 | 3 | ספריה שמאפשרת לתכנת מערכות טלפוניות בקלות באמצעות [מודול API](https://f2.freeivr.co.il/post/76) של חברת 'ימות המשיח'. 4 | 5 | מטרת הספריה לאפשר תקשורת מול המערכת הטלפונית בצורה נקיה וקריאה: 6 | 7 | - הרצה רציפה של הקוד מתחילה ועד סוף, תוך שמירת הstate של השיחה בין הקריאות, בצורה שקופה לחלוטין 8 | - יצירת התשובות על ידי קריאה למתודות של השיחה במקום יצירה ידנית של הסטרינגים 9 | - אפשרויות נוחות נוספות כגון מטפל בשגיאות, הסרה של תווים לא חוקיים, לוג אוטומטי מפורט (אופציונלי) 10 | - תיעוד מפורט מוטמע ומוצג תוך כדי עריכה ‫(JSDoc) 11 | - [תמיכה בTypeScript](#typescript) 12 | - ועוד אפשרויות רבות! פירוט בתיעוד 👇 13 | 14 | # התקנה 15 | 16 | ```bash 17 | npm install yemot-router2 18 | ``` 19 | 20 | # יומן שינויים/הוראות שדרוג 21 | 22 | ⚙️ משדרגים מגרסה קודמת? ראו [changelog](./CHANGELOG.md)! 23 | 24 | # תיעוד 25 | 26 | מומלץ להשתמש בכפתור "תוכן העניינים" האוטומטי כדי לנווט בתיעוד: 27 | ![CleanShot 2023-05-02 at 04 14 20@2x](https://user-images.githubusercontent.com/78599753/235558893-e8b9ebe0-e2a1-4740-8c96-eb370f9a3952.png) 28 | 29 | # אתחול הראוטר ושימוש בסיסי 30 | 31 | הספריה עובדת על ידי חיקוי של Express Router, כך שאופן השימוש הוא די דומה. דוגמה: 32 | 33 | ```js 34 | import { YemotRouter } from 'yemot-router2'; 35 | const router = YemotRouter(); 36 | 37 | router.get('/', async (call) => { 38 | return call.id_list_message([{ 39 | type: 'text', 40 | data: 'שלום עולם' 41 | }]) 42 | }); 43 | ``` 44 | 45 | את הראוטר יש לחבר לאפליקציית express על ידי `app.use`, כרגיל. 46 | 47 | הראוטר מקבל פונקצייה אסינכרונית, שהארגומנט שהיא מקבלת מהראוטר הוא אובייקט `Call` (יבואר בהמשך), המייצג את השיחה, ובאמצעות המתודות שלו ניתן לקבל מידע אודות השיחה, להשמיע/לבקש נתונים מהמחייג, להפנות אותו לשלוחה אחרת, ועוד. 48 | 49 | **👨‍💻 דוגמה בסיסית: [example-app/index.js](example-app/index.js)** 50 | 51 | **טיפ**: מומלץ מאוד לא להגדיר בשלוחה `api_hangup_send=no`, לביצועי זיכרון טובים יותר. 52 | 53 | ## אפשרויות הראוטר 54 | 55 | הראוטר מקבל את הפונקציות הבאות, כולן אופציונליות: 56 | 57 | - **timeout**: ‫ זמן המתנה לקבלת נתון מהמשתמש (במילישניות). במידה ולא התקבל הנתון בזמן הנ"ל, השיחה תימחק מהactiveCalls וגם אם המשתמש יקיש השרת יקבל אותו כשיחה חדשה.
58 | ‫מקבל מספר מילישיניות, או מחרוזת הקבילה ע"י ספריית [ספריית ms](https://npmjs.com/ms).
59 | ‫מכסה גם מקרי קצה שלא התקבלה מימות הקריאה עם hangup=yes.
60 | ‫יש לשים לב לא להגדיר ערך נמוך שמחייג לגיטימי שלא הקיש מיד תשובה עלול להיתקל ב-timeout. 61 | - **printLog** (בוליאני): האם להדפיס לוג מפורט על כל קריאה לשרת, ניתוק שיחה וכולי. שימושי לפיתוח. 62 | - **uncaughtErrorHandler**: (פונקציה) ‫ פונקציה לטיפול בשגיאות לא מטופלות בתוך שיחה, שימושי לדוגמה לשמירת לוג + השמעת הודעה למשתמש, במקום קריסה של השרת, והמשתמש ישמע "אין מענה משרת API". 63 | ראה דוגמה ב[example.js](example.js). 64 | 65 | בנוסף ניתן להעביר אופציות של אקספרס ראוטר עצמו - ראה [פירוט בתיעוד express.js](https://expressjs.com/en/api.html#express.router). 66 | 67 | # מתודות אובייקט ה`Call` 68 | 69 | ## `read(messages, mode, options?)` 70 | 71 | מתודה לקבלת נתון מהמחייג, מחזירה `Promise` עם התשובה (במקרה של בקשת הקלטה, יחזור נתיב הקובץ).
72 | פירוט נוסף על `read`: [https://f2.freeivr.co.il/post/78283](https://f2.freeivr.co.il/post/78283) 73 | 74 | ### הארגומנט `messages` 75 | 76 | ההודעות שיושמעו למחייג לפני קבלת הנתון.
77 | מערך של אובייקטי "הודעה" (ראה [פירוט בהמשך](#אובייקט-message)), שיושמעו למשתמש ברצף. 78 | 79 | ### הארגומנט `mode` 80 | 81 | מגדיר את סוג הנתון שמבקשים מהמחייג: 82 | 83 | `tap` = הקשות 84 | 85 | `stt` = זיהוי דיבור 86 | 87 | `record` = הקלטה 88 | 89 | ### הארגומנט `options` 90 | 91 | בפרמטר הזה, ניתן להעביר אפשרויות נוספות, כגון סך הקשות מינימלי, מקסימלי, וכו'. 92 | 93 | #### הקשות 94 | 95 | ```js 96 | let options = { 97 | /* שם הערך בימות 98 | ברירת מחדל, נקבע אוטומטית, 99 | val_1, val_2, val_3 ... 100 | */ 101 | val_name: "val_x", 102 | 103 | /* האם לבקש את הערך שוב אם קיים. */ 104 | re_enter_if_exists: false, 105 | 106 | /* כמות הספרות המקסימלית שהמשתמש יוכל להקיש */ 107 | max_digits: "*", 108 | 109 | /* כמות ספרות מינימלית */ 110 | min_digits: 1, 111 | 112 | /* שניות להמתנה */ 113 | sec_wait: 7, 114 | 115 | /* צורת ההשמעה למשתמש את הקשותיו 116 | באם מעוניינים במקלדת שונה ממקלדת ספרות, כגון EmailKeyboard או HebrewKeyboard, יש להכניס כאן את סוג המקלדת 117 | [ראו example.js] 118 | האופציות הקיימות: 119 | "Number" | "Digits" | "File" | "TTS" | "Alpha" | "No" | "HebrewKeyboard" | 120 | "EmailKeyboard" | "EnglishKeyboard" | "DigitsKeyboard" | "TeudatZehut" | 121 | "Price" | "Time" | "Phone" | "No" 122 | פירוט על כל אופציה ניתן למצוא בתיעוד מודול API של ימות המשיח, תחת"הערך השישי (הקשה)". 123 | */ 124 | 125 | typing_playback_mode: "No", 126 | 127 | /* האם לחסום הקשה על כוכבית */ 128 | block_asterisk_key: false, 129 | 130 | /* האם לחסום הקשה על אפס */ 131 | block_zero_key: false, 132 | 133 | /* החלפת תווים*/ 134 | replace_char: "", 135 | 136 | /* ספרות מותרות להקשה - מערך 137 | [1, 2, 3 ...] 138 | */ 139 | digits_allowed: [], 140 | 141 | /* כמה פעמים להשמיע את השאלה לפני שליחת תשובת "None" (כלומר תשובה ריקה). ברירת מחדל פעם אחת */ 142 | amount_attempts: "", 143 | 144 | /* 145 | האם לאפשר תשובה ריקה - או שלאחר זמן ההמתנה יושמע "לא הוקשה בחירה" וידרוש להקיש 146 | ברירת מחדל לא מאפשר תשובה ריקה 147 | */ 148 | allow_empty: false, 149 | 150 | /* 151 | הערך שיישלח כשלא הוקשה תשובה. ברירת מחדל "None" 152 | ניתן להעביר גם ערכים שאינם מחרוזת, לדוגמה null והערך שיתקבל מהread יהיה null ולא 'null' 153 | */ 154 | empty_val: "None", 155 | 156 | /* האם לחסום שינוי שפת מקלדת */ 157 | block_change_keyboard: false, 158 | } 159 | ``` 160 | 161 | #### הקלטה 162 | 163 | ערכי ברירת מחדל - הקלטות: 164 | 165 | ```js 166 | const options = { 167 | /* נתיב לשמירת ההקלטה - שלוחה בלבד, ברירת מחדל שלוחה נוכחית, או api_dir אם מוגדר */ 168 | path: '', 169 | 170 | /* שם קובץ (ללא סיומת) לשמירת ההקלטה, ברירת מחדל - ממוספר אוטומטית כקובץ הגבוה בשלוחה */ 171 | file_name: '', 172 | 173 | /* 174 | ברירת מחדל משמיע תפריט לאישור ההקלטה/הקלטה מחדש, ניתן להגדיר שמיד בהקשה על סולמית ההקלטה תאושר 175 | */ 176 | no_confirm_menu: false, 177 | 178 | /* האם לשמור את ההקלטה באם המשתמש ניתק באמצע הקלטה */ 179 | save_on_hangup: false, 180 | 181 | /* 182 | במידה והוגדר שם קובץ לשמירה (file_name) וכבר קיים קובץ כזה, 183 | האם לשנות את שם הקובץ הישן ולשמור את החדש בשם שנבחר (ברירת מחדל), או לצרף את ההקלטה החדשה לסוף הקובץ הישן 184 | */ 185 | append_to_existing_file: false, 186 | 187 | /* כמות שניות מינימלית להקלטה, ברירת מחדל אין מינימום */ 188 | min_length: '', 189 | 190 | /* כמות שניות מקסימלית להקלטה, ברירת מחדל ללא הגבלה */ 191 | max_length: '' 192 | }; 193 | ``` 194 | 195 | #### זיהוי דיבור 196 | 197 | ערכי ברירת מחדל - זיהוי דיבור: 198 | 199 | ```js 200 | const options = { 201 | /* 202 | שפת הדיבור 203 | ברירת מחדל עברית או מה שהוגדר בlang בשלוחה, 204 | רשימת השפות הזמינות להגדרה: https://did.li/m1lrl 205 | */ 206 | lang: '', 207 | 208 | /* 209 | האם לחסום הקשה במצב זיהוי דיבור 210 | ברירת מחדל מאפשר להקיש תוך כדי הדיבור, כלומר המחייג בוחר אם להקיש או לדבר 211 | */ 212 | block_typing: false, 213 | 214 | /* 215 | מקסימום ספרות שאפשר להקיש, באם לא נחסמה ההקשה תוך כדי דיבור 216 | ברירת מחדל לא מוגבל 217 | */ 218 | max_digits: '', 219 | 220 | /* 221 | האם להשתמש במנוע הדיבור של הקלטות - נצרך עבור זיהוי טקסט ארוך 222 | באם מפעילים הגדרה זו, לא ניתן לקלוט הקשות תוך כדי דיבור 223 | */ 224 | use_records_recognition_engine: false, 225 | 226 | /* 227 | אחרי כמה שניות של שקט לסיים את ההקלטה, 228 | רלוונטי רק אם משתמשים במנוע זיהוי טקסטים ארוכים (use_records_recognition_engine) 229 | */ 230 | quiet_max: '', 231 | 232 | /* 233 | * מספר שניות מרבי להקלטה, ברירת מחדל: ללא הגבלה 234 | */ 235 | length_max: '' 236 | }; 237 | ``` 238 | 239 | ## `go_to_folder(target)` 240 | 241 | מתודה להעברת השיחה לשלוחה מסוימת במערכת הנוכחית. 242 | 243 | ניתן לכתוב נתיב יחסי לשלוחה הנוכחית או לשלוחה הראשית, פירוט על האופציות הזמינות ניתן לקרוא [כאן](https://f2.freeivr.co.il/post/58). 244 | 245 | ניתן גם להעביר בפרמטר folder את הסטרינג `hangup`, וכך לנתק את השיחה, או להשתמש בקיצור [`call.hangup()`](#hangup). 246 | 247 | ## `restart_ext()` 248 | 249 | הפעלה מחדש של השלוחה הנוכחית. 250 | 251 | קיצור לתחביר הבא: 252 | 253 | ```js 254 | call.go_to_folder(`/${call.ApiExtension}`); 255 | ``` 256 | 257 | ## `hangup()` 258 | 259 | ניתוק השיחה. קיצור לתחביר הבא: 260 | 261 | ```js 262 | call.go_to_folder('hangup'); 263 | ``` 264 | 265 | ## `id_list_message(messages, options?)` 266 | 267 | במתודה זו ניתן להשמיע למשתמש הודעה אחת, או מספר הודעות ברצף.
268 | המתודה מקבלת מערך של הודעות ([אובייקט message](#אובייקט-message)) ומשמיעה אותן למשתמש. 269 | 270 | ⚠️ שים לב! ⚠️ 271 | 272 | לאחר השמעת ההודעות, השיחה תצא אוטומטית מהשלוחה! 273 | 274 | באם מעוניינים לשרשר פעולה נוספת לאחר ההשמעה, לדוגמה להשמיע הודעה ואז לבצע `read` (קבלת נתונים נוספים), יש להגדיר בארגומנט ה`options` את `prependToNextAction` ל`true`. 275 | 276 |
277 | 278 | ## `routing_yemot(number)` 279 | 280 | מתודה להעברת השיחה למערכת אחרת בימות המשיח ללא עלות יחידות, באמצעות "ראוטינג ימות". 281 | 282 | הפונקציה מקבלת ארגומנט יחיד - סטרינג של מספר מערכת בימות להעברת השיחה. 283 | 284 | ניתן גם לנתב את השיחה ממערכת בשרת הפריווט למערכת בשרת הרגיל ולהיפך. 285 | 286 | ## `send(data)` 287 | 288 | ניתן להשתמש במתודה זו כדי לשלוח סטרינג חופשי לחלוטין, לדוגמה עבור פונקציונליות שעדיין לא נתמכת בספרייה. 289 | 290 | במתודה זו יש להעביר את הסטרינג בדיוק כפי שמעוניינים שהשרת של ימות יקבל אותו, והוא לא עובר ולידציה או עיבוד. 291 | 292 | כדי להשתמש לבקשת מידע - לדוגמה מעבר לסליקת אשראי, יש לשלב עם קריאות ל 293 | 294 | ```js 295 | await call.blockRunningUntilNextRequest(); 296 | ``` 297 | 298 | # `values` 299 | 300 | מכיל את כל הפרמטרים שנשלחו מימות - 301 | 302 | אם הבקשה נשלחה ב-‫`HTTP GET`, יכיל את ה‫`query string`, 303 | 304 | אם הבקשה נשלחה ב-‫`HTTP POST` ‫(`api_url_post=yes`), יכיל את ה‫`body` 305 | 306 | # אובייקט `message` 307 | 308 | כל אובייקט הודעה צריך להיות במבנה הבא: 309 | 310 | ```js 311 | { type: string, data: string } 312 | ``` 313 | 314 | כאשר `type` הוא סוג הודעה מתוך הטבלה שלהלן, ו`data` הוא המידע עצמו - `string`,
315 | מלבד כאשר ה`type` הוא `zmanim`/`music_on_hold`, שאז ה`data` יהיה אובייקט - מפורט מתחת לטבלה: 316 | 317 | - [zmanim](#מבנה-הdata-בzmanim) 318 | - [music_on_hold](#מבנה-הdata-ב-music_on_hold) 319 | 320 | **סוגי הודעות נתמכים:** 321 | 322 |
323 | 324 | | סוג | תיאור מקוצר | דוגמה | הערות | 325 | | ---------------- | -------------------------------------------------------------------------------------- | ---------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 326 | | `text` | הקראת טקסט | `דוגמה לטקסט דוגמה` | שים לב ל[אזהרה מתחת לטבלה](#תווים-לא-חוקיים-בהקראת-טקסט) לגבי תווים שלא ניתן להקריא | 327 | | `file` | השמעת קובץ אודיו | `/1/002`, ניתן לכתוב רק את שם הקובץ `002` אם הקובץ נמצא בתקייה הנוכחית | אין לכתוב סיומת קובץ. [ניתן להשמיע ממאגר גלובלי](https://f2.freeivr.co.il/topic/56/%D7%9E%D7%95%D7%93%D7%95%D7%9C-api-%D7%AA%D7%A7%D7%A9%D7%95%D7%A8-%D7%A2%D7%9D-%D7%9E%D7%97%D7%A9%D7%91%D7%99%D7%9D-%D7%95%D7%9E%D7%9E%D7%A9%D7%A7%D7%99-%D7%A0%D7%AA%D7%95%D7%A0%D7%99%D7%9D-%D7%97%D7%99%D7%A6%D7%95%D7%A0%D7%99%D7%99%D7%9D/4?_=1682984503080#:~:text=%D7%94%D7%A9%D7%9E%D7%A2%D7%AA%20%D7%A7%D7%95%D7%91%D7%A5%20%D7%9E%D7%AA%D7%95%D7%9A%20%D7%9E%D7%90%D7%92%D7%A8%20%D7%92%D7%9C%D7%95%D7%91%D7%9C%D7%99). | 328 | | `speech` | הקראה אוטומטית של קובץ TTS | נתיב לקובץ TTS או שם קובץ TTS בתקיה הנוכחית | ללא הסיומת | 329 | | `digits` | השמעת ספרות | `105` - ישמיע "אחת אפס חמש" | שימושי בעיקר להקראת מספר טלפון | 330 | | `number` | השמעת מספר | `105` - ישמיע "מאה וחמש" | - | 331 | | `alpha` | השמעת אותיות באנגלית | `abc`, ישמיע "איי, בי, סי" | לא תומך בעברית | 332 | | `zmanim` | השמעת שעה | אובייקט. פירוט בנפרד 👇 | - | 333 | | `system_message` | השמעת הודעת מערכת | `M1005` או `1005` | [רשימת הודעות המערכת](https://f2.freeivr.co.il/post/3) | 334 | | `music_on_hold` | השמעת מוזיקה בהמתנה | `{ musicName: 'ztomao', maxSec: 10 }` | הפרמור maxSec רשות. ראה [כאן](https://f2.freeivr.co.il/topic/44/%D7%9E%D7%95%D7%96%D7%99%D7%A7%D7%94-%D7%91%D7%94%D7%9E%D7%AA%D7%A0%D7%94) סוגי מוזיקה זמינים והוראות ליצירת חדש. | 335 | | `dateH` | השמעת תאריך עברי | פורמט `DD/MM/YYYY` - 28/07/2022 | - | 336 | | `date` | השמעת תאריך לועזי | פורמט תאריך לועזי `DD/MM/YYYY`, ישמיע את התאריך העברי המתאים | - | 337 | | `go_to_folder` | נתיב יחסי לשלוחה הנוכחית או לשלוחה הראשית, ראה [כאן](https://f2.freeivr.co.il/post/58) | העברת השיחה לשלוחה אחרת | לא מומלץ, עדיף להשתמש ב [`call.go_to_folder`](#go_to_folderpath). לא ניתן לשרשר הודעות נוספות לאחר סוג זה. | 338 |
339 | 340 | ### תווים לא חוקיים בהקראת טקסט 341 | 342 | ⚠️ שימו לב! ⚠️ 343 | 344 | לא ניתן להחזיר לימות את התוים: 345 | 346 | נקודה,מקף,גרש,גרשיים,& 347 | 348 | העברת אחד מהתוים הנ"ל יגרום לזריקת שגיאה, אלא אם כן נאפשר הסרת תווים לא חוקיים שקטה: 349 | 350 | **כאשר מעבירים טקסט להקראה (`'type: 'text`) ניתן להגדיר הסרה של תווים לא חוקיים**, כלומר שבמקום לזרוק שגיאה הם פשוט יוסרו מהתשובה שתוחזר לימות. 351 | 352 | ההגדרה היא `removeInvalidChars`, אותה ניתן להגדיר בשתי רמות, ברמת הודעה בודדת, או ברמת כל ה`read`/`id_list_message`. 353 | 354 | דוגמאות: 355 | 356 | - ברמת ההודעה המסוימת - העברת הפרמטר `removeInvalidChars` באובייקט ההודעה: 357 | 358 | ```js 359 | { 360 | type: "text", 361 | data: "טקסט. בעייתי.", 362 | removeInvalidChars: true 363 | } 364 | ``` 365 | 366 | - ברמת כל ה`read`/`id_list_message` - העברת הפרמטר `removeInvalidChars` באובייקט האפשרויות. 367 | 368 | דוגמה ל`read`: 369 | 370 | ```js 371 | const resp = await call.read(messagesWidthInvalidChars, 'tap', { removeInvalidChars: true }); 372 | ``` 373 | 374 | דוגמה ל`id_list_message`: 375 | 376 | ```js 377 | call.id_list_message(messagesWidthInvalidChars, { removeInvalidChars: true }); 378 | ``` 379 | 380 | ### מבנה הdata ב`zmanim` 381 | 382 | ```js 383 | { 384 | time: string, // optional, default: "T" (current time) 385 | zone: string, // optional, default: "IL/Jerusalem", 386 | difference: string // optional, default: 0 387 | }; 388 | ``` 389 | 390 | #### הערך `time` 391 | 392 | סוג הזמן שרוצים להשמיע. 393 | 394 | ברירת מחדל: "`T`" = השעה הנוכחית. 395 | השמעת שעה - `THH:MM`, 396 | או זמן הלכתי - ניתן לראות [כאן](https://f2.freeivr.co.il/post/82875) את רשימת הזמנים שניתן לחשב מהם זמן. 397 | 398 | #### הערך `zone` 399 | 400 | אזור הזמן שעבורו יש לחשב את הזמנים. 401 | 402 | ברירת מחדל: `IL/Jerusalem`. 403 | 404 | ניתן לראות [כאן](https://f2.freeivr.co.il/post/82868) את רשימת אזורי הזמן הקיימים במערכת. 405 | 406 | #### הערך `difference` 407 | 408 | ערך זה משמש להוספה/הסרה מלאכותית של זמן על הזמן שמשמיעים. 409 | 410 | באם לא יועבר פרמטר זה, יושמע הזמן ללא שינוי. 411 | 412 | הערך **`difference`** מכיל קודם את סוג הפעולה - פלוס (+) להוספת זמן, או מינוס (-) להפחתת זמן, ואז את הזמן על פי הצורה הבאה: Y - שנה M - חודש D - יום H - שעה m - דקה S - שניה s - אלפית שניה למשל, עבור 20 דקות אחורה יש להגדיר `m20-`, עבור 3 שעות קדימה יש לרשום `H3+`. עבור יומיים אחורה יש לרשום `D1-`. 413 | 414 | לדוגמה, עבור השמעת זמן שקיעת החמה מחר בעיר בני ברק: 415 | 416 | ```js 417 | const messages = [{ 418 | type: 'zmanim', 419 | data: { 420 | time: 'sunset', 421 | zone: 'IL/Bney_Brak', 422 | difference: '+1D' 423 | } 424 | }]; 425 | ``` 426 | 427 | ### מבנה הdata ב-**music_on_hold** 428 | 429 | ```js 430 | { 431 | musicName: string, 432 | maxSec: number // optional 433 | }; 434 | ``` 435 | 436 | [כאן](https://f2.freeivr.co.il/topic/44/%D7%9E%D7%95%D7%96%D7%99%D7%A7%D7%94-%D7%91%D7%94%D7%9E%D7%AA%D7%A0%D7%94) סוגי מוזיקה זמינים והוראות ליצירת חדשה. 437 | 438 | # ברירות מחדל 439 | 440 | ------------- 441 | **שימו לב:** 442 | 443 | אפשרות זו זמינה בגרסה 6.0 ומעלה 444 | 445 | אפשרות זו הינה אופציונלית לחלוטין, ניתן להמשיך להעביר אובייקט אפשרויות בכל `read`/`id_list_message` כמו קודם 446 | 447 | ------------- 448 | 449 | ניתן להגדיר ברירות מחדל בדרכים הבאות: 450 | 451 | - ברירות מחדל של המערכת - שהן כמו ברירות המחדל של ימות ([defaults.js](lib/defaults.js)) 452 | - רמת ראוטר 453 | - רמת שיחה 454 | - ספציפית לקריאת `read`/`id_list_message` מסויימת 455 | 456 | האפשרויות ימוזגו עם סדר קדימויות. הסדר הוא: 457 | ברירות המחדל של הספרייה שנמצאת ב- [lib/defaults.js](lib/defaults.js), 458 | ברמת מופע ראוטר, 459 | ברמת מופע שיחה, 460 | ברמת קריאה ספציפית. 461 | 462 | כל אפשרות מקבלת קדימות ודורסת את זו שלפניה. 463 | 464 | דוגמה: 465 | 466 | ```js 467 | const router = YemotRouter({ 468 | printLog: true, 469 | defaults: { 470 | removeInvalidChars: true, 471 | read: { 472 | timeout: 30000 473 | } 474 | } 475 | }); 476 | 477 | // אפשר גם כך: 478 | // router.defaults.read.timeout = 30000; 479 | 480 | router.get('/', async (call) => { 481 | // הtimeout יהיה 30 שניות 482 | await call.read([{ type: 'text', data: 'היי, תקיש 1' }], 'tap', { 483 | max_digits: 1, 484 | digits_allowed: [1] 485 | }); 486 | 487 | // הtimeout יהיה 40 שניות 488 | call.defaults.read.timeout = 40000; 489 | await call.read([{ type: 'text', data: 'היי, תקיש 1' }], 'tap', { 490 | max_digits: 1, 491 | digits_allowed: [1] 492 | }); 493 | 494 | // הtimeout יהיה 60 שניות 495 | await call.read([{ type: 'text', data: 'היי, תקיש 1' }], 'tap', { 496 | max_digits: 1, 497 | digits_allowed: [1], 498 | timeout: 60000 499 | }); 500 | }); 501 | ``` 502 | 503 | בדוגמה מאתחלים את הראוטר עם הגדרה של timeout של 1000 שניות, 504 | לאחר מכן בתוך השיחה משנים אותו ל2000, 505 | ולאחר מכן מבצעים קריאה בודדת עם אובייקט אופציות עם timeout של 3000, והוא גובר על ההגדרות הקודמות שהיו ברמה יותר גבוהה. 506 | 507 | **שימו לב!** ניתן להגדיר את האופציות הבאות ברמת קריאה בודדת בלבד ולא ברמת שיחה/ראוטר: 508 | 509 | - `val_name` (`read`) 510 | - `prependToNextAction` (`id_list_message`) 511 | 512 | # Events 513 | 514 | ניתן להאזין לאירועים ברמת ראוטר על ידי האזנה ל`router.events`: 515 | 516 | - `new_call` - כאשר שיחה חדשה נכנסת למערכת 517 | - `call_continue` - התקבלה תשובה לקריאת read 518 | - `call_hangup` - ‫כאשר שיחה מנותקת על ידי המחייג (התקבלה בקשה עם `hangup=yes`) 519 | 520 | שימושי לדוגמה כדי לטפל בקריאות עם `hangup=yes` גם מחוץ לשלוחה. 521 | 522 | # TypeScript 523 | 524 | הערות לשימוש בספריה עם טייפסקריפט: 525 | 526 | ## ‏`app.use(router)` 527 | 528 | בשימוש בראוטר עם אקספרס - 529 | ```ts 530 | const router = YemotRouter() 531 | app.use(router) 532 | ``` 533 | תוצג שגיאת טייפ. הפיתרון כרגע: 534 | ```ts 535 | const { Router } = require('express') 536 | const router = YemotRouter() 537 | app.use(router.asExpressRouter) 538 | ``` 539 | 540 | ## הגדרת ‏`empty_val` שאינו מסוג string 541 | 542 | כרגע הDTS לא מוגדר להסיק את הטייפ אוטומטית, ויש להגדיר אותו ידנית עם `as`, דוגמה: 543 | ```ts 544 | const res = await call.read([{ type: 'text', data: 'please type one' }], 'tap', { 545 | allow_empty: true, 546 | empty_val: null, 547 | }) as string | null; 548 | ``` 549 | 550 | # נספח: מקרי קצה - למנוסים בשימוש בספריה 551 | 552 | ## הרצת קוד לאחר החזרת תשובה למחייג 553 | 554 | **⚠️ שימו לב - קטע זה מורכב מעט להבנה, ונצרך רק אם מעוניינים להחזיר תשובה למחייג ולתת לו לצאת מהשלוחה (`id_list_message`), ולאחר מכן להריץ קוד "כבד", ולא נצרכת אם נותנים למאזין להמתין לאישור ביצוע הפעולה (באמצעות `read`).** 555 | 556 | בעת החזרת תשובה שאינה `read` ולא גורמת לשרת של ימות לחזור לראוטר - `id_list_message` או `go_to_folder`, 557 | נזרקת שגיאה על ידי הספריה - שתופסת אותה בחזרה ברמה יותר גבוהה, וכך ריצת הפונקציה נהרגת כדי לחסוך בזיכרון 558 | וכן לנקות את השיחה מה`activeCalls` - כדי שבכניסה חוזרת לשלוחה הפונקציה תרוץ מההתחלה ולא תמשיך. 559 | 560 | לכן אם מנסים להחזיר תשובה למשתמש ולאחר מכן להריץ את הקוד ה"כבד" כמו שעושים בשרת nodejs + express רגיל, לדוגמה: 561 | 562 | ```js 563 | function runBigJob(req, res) { 564 | res.status(202).send('ok, we got your request, we will send you an email when the job is done') 565 | doBigJob() 566 | } 567 | ``` 568 | 569 | או במקרה של הראוטר - קוד (**שגוי**) כזה: 570 | 571 | ```js 572 | async function runBigJob(call) { 573 | call.id_list_message([{ 574 | type: 'text', 575 | data: 'בסדר, בקשתך תטופל בהקדם' 576 | }]) 577 | await doBigJob() 578 | } 579 | ``` 580 | 581 | קוד כזה לא יעבוד כיוון שהקריאה ל`call.id_list_messsage` זורקת שגיאה שהורגת את ריצת הפונקציה, ולכן הקוד שלאחריה לא ירוץ. 582 | 583 | כדי להריץ קוד לאחר החזרת התשובה, יש לתפוס את השגיאה שנזרקת (במקרה שהיא שגיאת `ExitError` פנימית כנ"ל): 584 | 585 | ```js 586 | import { ExitError } from 'yemot-router2'; 587 | async function runBigJob (call) { 588 | try { 589 | call.id_list_message([{ 590 | type: 'text', 591 | data: 'בסדר, בקשתך תטופל בהקדם' 592 | }]); 593 | } catch (error) { 594 | if (error.isExitError) return; 595 | throw error; 596 | }; 597 | await doBigJob(); 598 | } 599 | ``` 600 | 601 | ### מחיקה ידנית של השיחה מה`activeCalls` 602 | 603 | אין צורך לזרוק ידנית את `ExitError` כדי למחוק את השיחה מהactiveCalls, כיוון שהיא נמחקת אוטומטית על ידי הספריה בסיום הריצה של הפונקציה.
604 | עם זאת, באם הקוד הנוסף המורץ אמור לקחת זמן, מומלץ לנקות ידנית באופן מיידי (מייד לאחר החזרת התשובה ב`id_list_message`) את השיחה מהactiveCalls - אחרת לא יהיה ניתן להיכנס לשלוחה עד לסיום ביצוע הפעולה הכבדה: 605 | 606 | ```js 607 | const router = YemotRouter({ printLog: true }); 608 | router.get('/', async (call) => { 609 | try { 610 | call.id_list_message([{ 611 | type: 'text', 612 | data: 'בסדר, בקשתך תטופל בהקדם' 613 | }]); 614 | } catch (error) { 615 | if (error.isExitError) return; 616 | throw error; 617 | }; 618 | router.deleteCall(call.callId); 619 | await doBigJob(); 620 | }); 621 | ``` 622 | 623 | ## דרך נוספת להרצת קוד כבד 624 | 625 | אופציה נוספת היא להריץ את הקוד בצורה מנותקת מהפונקציה, כלומר קריאה לקוד ה"כבד" בתוך פונקצייה אנונימית **לפני** החזרת התשובה: 626 | 627 | ```js 628 | (async () => { 629 | await doBigJob(); 630 | })(); 631 | call.id_list_message(...); 632 | ``` 633 | --------------------------------------------------------------------------------