├── .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 | 
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 |
--------------------------------------------------------------------------------