Compare commits

...

12 Commits

Author SHA1 Message Date
48a3e60623 Change env variables
RC_* to RINGCENTRAL_*
APP_KEY and APP_SECRET repalced with CLIENT_ID and CLIENT_SECRET to
reflect RingCentral's descriptions

Change RC_SANDBOX to RINGCENTRAL_SERVER and changed its behaviour.
2021-03-15 11:57:10 -06:00
9a61396db6 Use winston for logging 2021-03-15 09:46:40 -06:00
0fb6c9bcfb Add DEBUG RingCentral request logging 2021-03-14 15:47:29 -06:00
4cbfd1a4f8 Allow fromName to be null 2021-03-14 15:47:29 -06:00
bc7500ce3e Attempt to re-authenticate on auth refresh error 2021-03-14 15:47:12 -06:00
2668f4d814 Fix getMisisngTranscriptionVoicemails
Meant whereNull, not whereNotNull.
2021-03-14 15:46:33 -06:00
30d95657cf Add RingCentral token refresh error logging. 2021-03-11 15:11:11 -07:00
1c1682fe4f Move duration column to recordings table 2021-03-11 12:52:08 -07:00
0946158005 Add FIRST_RUN_AGE variable
Allows setting the maximum age of voicemails to fetch on the first run.

Other changes:
- Refactor `from` to `age` in function parameters
- Improve README
2021-03-11 11:55:50 -07:00
39cd664b3c Modify template to indicate when a transcription is partial 2021-03-11 11:15:43 -07:00
1052dfc1b1 Adjust intervals
We still fetch voicemails from RingCentral every 60 seconds, but now we
check for missing transcriptions every 15 seconds, and poll the database
for voicemails that are ready to be ticketized every second.
2021-03-11 11:04:32 -07:00
3452cd143a Add "recordings" table, save raw audio blobs in it
Other changes:
- Move Stored(Voicemail|Recording) interfaces into knex/types/tables
module for reduced boilerplate.
- Change updateStoredVoicemail to take Partial<StoredVoicemail>,
allowing to only update only some columns
2021-03-11 10:59:08 -07:00
11 changed files with 705 additions and 128 deletions

View File

@ -5,27 +5,38 @@
## Configuration
```shell
# Required API/authentication variables:
# Required API endpoint/authentication variables
SONAR_URL=https://instance.sonar.software/api/graphql
SONAR_TOKEN=
RC_APP_KEY=
RC_APP_SECRET=
RC_LOGIN_USERNAME=
RC_LOGIN_EXT=
RC_LOGIN_PASSWORD=
RINGCENTRAL_CLIENT_ID=
RINGCENTRAL_CLIENT_SECRET=
RINGCENTRAL_USERNAME=
RINGCENTRAL_EXTENSION=
RINGCENTRAL_PASSWORD=
# Set to any value to enable use of RingCentral's sandbox API
RC_SANDBOX=
# Set to 'sandbox' to use RingCentral's sandbox API
# Any other value will result in using RingCentral's production API
RINGCENTRAL_SERVER=production
# The database to use
# valid options: pg, sqlite
# default: sqlite
DB_ENGINE=sqlite # can be pg
# only used when DB_ENGINE=pg
# Only used when DB_ENGINE=pg
DB_URL=
# only used when DB_ENGINE=sqlite
# Only used when DB_ENGINE=sqlite
# default: voicemails.db
DB_FILE=voicemails.db
# A mapping of extension number to Sonar Ticket Group
# Only the voicemail boxes of these extensions will be checked
EXTENSION_TICKET_GROUPS=1:1,2:2,2:3
# Upon first run, query RingCentral voicemails up to FIRST_RUN_AGE seconds old.
# Useful when the application is restarted after not running for some time.
# default: 86400 (1 day)
FIRST_RUN_AGE=86400
```
## Deployment
@ -45,6 +56,8 @@ services:
environment:
# ... see Configuration above
DB_FILE: /data/voicemails.db
# so the created tickets show the correct 'Received' date & time
TZ: America/Creston
volumes:
- data:/data
```

410
package-lock.json generated
View File

@ -18,7 +18,8 @@
"react": "^17.0.1",
"react-dom": "^17.0.1",
"sqlite3": "^5.0.2",
"ts-node": "^9.1.1"
"ts-node": "^9.1.1",
"winston": "^3.3.3"
},
"devDependencies": {
"@types/luxon": "^1.26.2",
@ -116,6 +117,16 @@
"node": ">=4"
}
},
"node_modules/@dabh/diagnostics": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.2.tgz",
"integrity": "sha512-+A1YivoVDNNVCdfozHSR8v/jyuuLTMXwjWuxPFlFlUapXoGc+Gj9mDlTDDfrwl7rXCl2tNZ0kE8sIBO6YOn96Q==",
"dependencies": {
"colorspace": "1.1.x",
"enabled": "2.0.x",
"kuler": "^2.0.0"
}
},
"node_modules/@eslint/eslintrc": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.4.0.tgz",
@ -455,6 +466,11 @@
"node": ">=8"
}
},
"node_modules/async": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/async/-/async-3.2.0.tgz",
"integrity": "sha512-TR2mEZFVOj2pLStYxLht7TyfuRzaydfpxr3k9RpHIzMgw7A64dzsdqCxH1WJyQdoe8T10nDXd9wnEigmiuHIZw=="
},
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
@ -757,6 +773,15 @@
"node": ">=0.10.0"
}
},
"node_modules/color": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/color/-/color-3.0.0.tgz",
"integrity": "sha512-jCpd5+s0s0t7p3pHQKpnJ0TpQKKdleP71LWcA0aqiljpiuAkOSUFN/dyH8ZwF0hRmFlrIuRhufds1QyEP9EB+w==",
"dependencies": {
"color-convert": "^1.9.1",
"color-string": "^1.5.2"
}
},
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
@ -772,14 +797,52 @@
"node_modules/color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"dev": true
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
},
"node_modules/color-string": {
"version": "1.5.5",
"resolved": "https://registry.npmjs.org/color-string/-/color-string-1.5.5.tgz",
"integrity": "sha512-jgIoum0OfQfq9Whcfc2z/VhCNcmQjWbey6qBX0vqt7YICflUmBCh9E9CiQD5GSJ+Uehixm3NUwHVhqUAWRivZg==",
"dependencies": {
"color-name": "^1.0.0",
"simple-swizzle": "^0.2.2"
}
},
"node_modules/color/node_modules/color-convert": {
"version": "1.9.3",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
"integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
"dependencies": {
"color-name": "1.1.3"
}
},
"node_modules/color/node_modules/color-name": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
"integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU="
},
"node_modules/colorette": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/colorette/-/colorette-1.2.1.tgz",
"integrity": "sha512-puCDz0CzydiSYOrnXpz/PKd69zRrribezjtE9yd4zvytoRc8+RY/KJPvtPFKZS3E3wP6neGyMe0vOTlHO5L3Pw=="
},
"node_modules/colors": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz",
"integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==",
"engines": {
"node": ">=0.1.90"
}
},
"node_modules/colorspace": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.1.2.tgz",
"integrity": "sha512-vt+OoIP2d76xLhjwbBaucYlNSpPsrJWPlBTtwCpQKIu6/CSMutyzX93O/Do0qzpH3YoHEes8YEFXyZ797rEhzQ==",
"dependencies": {
"color": "3.0.x",
"text-hex": "1.0.x"
}
},
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
@ -1021,6 +1084,11 @@
"integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==",
"dev": true
},
"node_modules/enabled": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz",
"integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ=="
},
"node_modules/encoding": {
"version": "0.1.13",
"resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz",
@ -1313,6 +1381,16 @@
"integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=",
"dev": true
},
"node_modules/fast-safe-stringify": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.0.7.tgz",
"integrity": "sha512-Utm6CdzT+6xsDk2m8S6uL8VHxNwI6Jub+e9NYTcAms28T84pTa25GJQV9j0CY0N1rM8hK4x6grpF2BQf+2qwVA=="
},
"node_modules/fecha": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.0.tgz",
"integrity": "sha512-aN3pcx/DSmtyoovUudctc8+6Hl4T+hI9GBBHLjA76jdZl7+b1sgh5g4k+u/GL3dTy1/pnYzKp69FpJ0OicE3Wg=="
},
"node_modules/file-entry-cache": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
@ -1356,6 +1434,11 @@
"integrity": "sha512-zAoAQiudy+r5SvnSw3KJy5os/oRJYHzrzja/tBDqrZtNhUw8bt6y8OBzMWcjWr+8liV8Eb6yOhw8WZ7VFZ5ZzA==",
"dev": true
},
"node_modules/fn.name": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz",
"integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw=="
},
"node_modules/forever-agent": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz",
@ -1772,6 +1855,11 @@
"node": ">= 0.10"
}
},
"node_modules/is-arrayish": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz",
"integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ=="
},
"node_modules/is-binary-path": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
@ -2083,6 +2171,11 @@
"node": ">=8"
}
},
"node_modules/kuler": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz",
"integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A=="
},
"node_modules/latest-version": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/latest-version/-/latest-version-5.1.0.tgz",
@ -2113,6 +2206,18 @@
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
},
"node_modules/logform": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/logform/-/logform-2.2.0.tgz",
"integrity": "sha512-N0qPlqfypFx7UHNn4B3lzS/b0uLqt2hmuoa+PpuXNYgozdJYAyauF5Ky0BWVjrxDlMWiT3qN4zPq3vVAfZy7Yg==",
"dependencies": {
"colors": "^1.2.1",
"fast-safe-stringify": "^2.0.4",
"fecha": "^4.2.0",
"ms": "^2.1.1",
"triple-beam": "^1.3.0"
}
},
"node_modules/loose-envify": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
@ -2619,6 +2724,14 @@
"wrappy": "1"
}
},
"node_modules/one-time": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz",
"integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==",
"dependencies": {
"fn.name": "1.x.x"
}
},
"node_modules/optionator": {
"version": "0.9.1",
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz",
@ -3268,6 +3381,14 @@
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz",
"integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA=="
},
"node_modules/simple-swizzle": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz",
"integrity": "sha1-pNprY1/8zMoz9w0Xy5JZLeleVXo=",
"dependencies": {
"is-arrayish": "^0.3.1"
}
},
"node_modules/slice-ansi": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz",
@ -3366,6 +3487,14 @@
"node": ">=0.10.0"
}
},
"node_modules/stack-trace": {
"version": "0.0.10",
"resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz",
"integrity": "sha1-VHxws0fo0ytOEI6hoqFZ5f3eGcA=",
"engines": {
"node": "*"
}
},
"node_modules/string_decoder": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
@ -3539,6 +3668,11 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/text-hex": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz",
"integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg=="
},
"node_modules/text-table": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
@ -3611,6 +3745,11 @@
"node": ">=0.8"
}
},
"node_modules/triple-beam": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.3.0.tgz",
"integrity": "sha512-XrHUvV5HpdLmIj4uVMxHggLbFSZYIn7HEWsqePZcI50pco+MPqJ50wMGY794X7AOOhxOBAjbkqfAbEe/QMp2Lw=="
},
"node_modules/ts-node": {
"version": "9.1.1",
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-9.1.1.tgz",
@ -3892,6 +4031,72 @@
"node": ">=8"
}
},
"node_modules/winston": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/winston/-/winston-3.3.3.tgz",
"integrity": "sha512-oEXTISQnC8VlSAKf1KYSSd7J6IWuRPQqDdo8eoRNaYKLvwSb5+79Z3Yi1lrl6KDpU6/VWaxpakDAtb1oQ4n9aw==",
"dependencies": {
"@dabh/diagnostics": "^2.0.2",
"async": "^3.1.0",
"is-stream": "^2.0.0",
"logform": "^2.2.0",
"one-time": "^1.0.0",
"readable-stream": "^3.4.0",
"stack-trace": "0.0.x",
"triple-beam": "^1.3.0",
"winston-transport": "^4.4.0"
},
"engines": {
"node": ">= 6.4.0"
}
},
"node_modules/winston-transport": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.4.0.tgz",
"integrity": "sha512-Lc7/p3GtqtqPBYYtS6KCN3c77/2QCev51DvcJKbkFPQNoj1sinkGwLGFDxkXY9J6p9+EPnYs+D90uwbnaiURTw==",
"dependencies": {
"readable-stream": "^2.3.7",
"triple-beam": "^1.2.0"
},
"engines": {
"node": ">= 6.4.0"
}
},
"node_modules/winston-transport/node_modules/readable-stream": {
"version": "2.3.7",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz",
"integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==",
"dependencies": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.3",
"isarray": "~1.0.0",
"process-nextick-args": "~2.0.0",
"safe-buffer": "~5.1.1",
"string_decoder": "~1.1.1",
"util-deprecate": "~1.0.1"
}
},
"node_modules/winston-transport/node_modules/safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
},
"node_modules/winston-transport/node_modules/string_decoder": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"dependencies": {
"safe-buffer": "~5.1.0"
}
},
"node_modules/winston/node_modules/is-stream": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz",
"integrity": "sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==",
"engines": {
"node": ">=8"
}
},
"node_modules/word-wrap": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz",
@ -4029,6 +4234,16 @@
}
}
},
"@dabh/diagnostics": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.2.tgz",
"integrity": "sha512-+A1YivoVDNNVCdfozHSR8v/jyuuLTMXwjWuxPFlFlUapXoGc+Gj9mDlTDDfrwl7rXCl2tNZ0kE8sIBO6YOn96Q==",
"requires": {
"colorspace": "1.1.x",
"enabled": "2.0.x",
"kuler": "^2.0.0"
}
},
"@eslint/eslintrc": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.4.0.tgz",
@ -4314,6 +4529,11 @@
"integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==",
"dev": true
},
"async": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/async/-/async-3.2.0.tgz",
"integrity": "sha512-TR2mEZFVOj2pLStYxLht7TyfuRzaydfpxr3k9RpHIzMgw7A64dzsdqCxH1WJyQdoe8T10nDXd9wnEigmiuHIZw=="
},
"asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
@ -4552,6 +4772,30 @@
"resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz",
"integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c="
},
"color": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/color/-/color-3.0.0.tgz",
"integrity": "sha512-jCpd5+s0s0t7p3pHQKpnJ0TpQKKdleP71LWcA0aqiljpiuAkOSUFN/dyH8ZwF0hRmFlrIuRhufds1QyEP9EB+w==",
"requires": {
"color-convert": "^1.9.1",
"color-string": "^1.5.2"
},
"dependencies": {
"color-convert": {
"version": "1.9.3",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
"integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
"requires": {
"color-name": "1.1.3"
}
},
"color-name": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
"integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU="
}
}
},
"color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
@ -4564,14 +4808,36 @@
"color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"dev": true
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
},
"color-string": {
"version": "1.5.5",
"resolved": "https://registry.npmjs.org/color-string/-/color-string-1.5.5.tgz",
"integrity": "sha512-jgIoum0OfQfq9Whcfc2z/VhCNcmQjWbey6qBX0vqt7YICflUmBCh9E9CiQD5GSJ+Uehixm3NUwHVhqUAWRivZg==",
"requires": {
"color-name": "^1.0.0",
"simple-swizzle": "^0.2.2"
}
},
"colorette": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/colorette/-/colorette-1.2.1.tgz",
"integrity": "sha512-puCDz0CzydiSYOrnXpz/PKd69zRrribezjtE9yd4zvytoRc8+RY/KJPvtPFKZS3E3wP6neGyMe0vOTlHO5L3Pw=="
},
"colors": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz",
"integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA=="
},
"colorspace": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.1.2.tgz",
"integrity": "sha512-vt+OoIP2d76xLhjwbBaucYlNSpPsrJWPlBTtwCpQKIu6/CSMutyzX93O/Do0qzpH3YoHEes8YEFXyZ797rEhzQ==",
"requires": {
"color": "3.0.x",
"text-hex": "1.0.x"
}
},
"combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
@ -4757,6 +5023,11 @@
"integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==",
"dev": true
},
"enabled": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz",
"integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ=="
},
"encoding": {
"version": "0.1.13",
"resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz",
@ -4984,6 +5255,16 @@
"integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=",
"dev": true
},
"fast-safe-stringify": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.0.7.tgz",
"integrity": "sha512-Utm6CdzT+6xsDk2m8S6uL8VHxNwI6Jub+e9NYTcAms28T84pTa25GJQV9j0CY0N1rM8hK4x6grpF2BQf+2qwVA=="
},
"fecha": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.0.tgz",
"integrity": "sha512-aN3pcx/DSmtyoovUudctc8+6Hl4T+hI9GBBHLjA76jdZl7+b1sgh5g4k+u/GL3dTy1/pnYzKp69FpJ0OicE3Wg=="
},
"file-entry-cache": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
@ -5018,6 +5299,11 @@
"integrity": "sha512-zAoAQiudy+r5SvnSw3KJy5os/oRJYHzrzja/tBDqrZtNhUw8bt6y8OBzMWcjWr+8liV8Eb6yOhw8WZ7VFZ5ZzA==",
"dev": true
},
"fn.name": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz",
"integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw=="
},
"forever-agent": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz",
@ -5347,6 +5633,11 @@
"resolved": "https://registry.npmjs.org/interpret/-/interpret-2.2.0.tgz",
"integrity": "sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw=="
},
"is-arrayish": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz",
"integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ=="
},
"is-binary-path": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
@ -5576,6 +5867,11 @@
}
}
},
"kuler": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz",
"integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A=="
},
"latest-version": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/latest-version/-/latest-version-5.1.0.tgz",
@ -5600,6 +5896,18 @@
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
},
"logform": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/logform/-/logform-2.2.0.tgz",
"integrity": "sha512-N0qPlqfypFx7UHNn4B3lzS/b0uLqt2hmuoa+PpuXNYgozdJYAyauF5Ky0BWVjrxDlMWiT3qN4zPq3vVAfZy7Yg==",
"requires": {
"colors": "^1.2.1",
"fast-safe-stringify": "^2.0.4",
"fecha": "^4.2.0",
"ms": "^2.1.1",
"triple-beam": "^1.3.0"
}
},
"loose-envify": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
@ -6004,6 +6312,14 @@
"wrappy": "1"
}
},
"one-time": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz",
"integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==",
"requires": {
"fn.name": "1.x.x"
}
},
"optionator": {
"version": "0.9.1",
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz",
@ -6492,6 +6808,14 @@
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz",
"integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA=="
},
"simple-swizzle": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz",
"integrity": "sha1-pNprY1/8zMoz9w0Xy5JZLeleVXo=",
"requires": {
"is-arrayish": "^0.3.1"
}
},
"slice-ansi": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz",
@ -6566,6 +6890,11 @@
"tweetnacl": "~0.14.0"
}
},
"stack-trace": {
"version": "0.0.10",
"resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz",
"integrity": "sha1-VHxws0fo0ytOEI6hoqFZ5f3eGcA="
},
"string_decoder": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
@ -6700,6 +7029,11 @@
"integrity": "sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==",
"dev": true
},
"text-hex": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz",
"integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg=="
},
"text-table": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
@ -6756,6 +7090,11 @@
"punycode": "^2.1.1"
}
},
"triple-beam": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.3.0.tgz",
"integrity": "sha512-XrHUvV5HpdLmIj4uVMxHggLbFSZYIn7HEWsqePZcI50pco+MPqJ50wMGY794X7AOOhxOBAjbkqfAbEe/QMp2Lw=="
},
"ts-node": {
"version": "9.1.1",
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-9.1.1.tgz",
@ -6979,6 +7318,67 @@
}
}
},
"winston": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/winston/-/winston-3.3.3.tgz",
"integrity": "sha512-oEXTISQnC8VlSAKf1KYSSd7J6IWuRPQqDdo8eoRNaYKLvwSb5+79Z3Yi1lrl6KDpU6/VWaxpakDAtb1oQ4n9aw==",
"requires": {
"@dabh/diagnostics": "^2.0.2",
"async": "^3.1.0",
"is-stream": "^2.0.0",
"logform": "^2.2.0",
"one-time": "^1.0.0",
"readable-stream": "^3.4.0",
"stack-trace": "0.0.x",
"triple-beam": "^1.3.0",
"winston-transport": "^4.4.0"
},
"dependencies": {
"is-stream": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz",
"integrity": "sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw=="
}
}
},
"winston-transport": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.4.0.tgz",
"integrity": "sha512-Lc7/p3GtqtqPBYYtS6KCN3c77/2QCev51DvcJKbkFPQNoj1sinkGwLGFDxkXY9J6p9+EPnYs+D90uwbnaiURTw==",
"requires": {
"readable-stream": "^2.3.7",
"triple-beam": "^1.2.0"
},
"dependencies": {
"readable-stream": {
"version": "2.3.7",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz",
"integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==",
"requires": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.3",
"isarray": "~1.0.0",
"process-nextick-args": "~2.0.0",
"safe-buffer": "~5.1.1",
"string_decoder": "~1.1.1",
"util-deprecate": "~1.0.1"
}
},
"safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
},
"string_decoder": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"requires": {
"safe-buffer": "~5.1.0"
}
}
}
},
"word-wrap": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz",

View File

@ -25,7 +25,8 @@
"react": "^17.0.1",
"react-dom": "^17.0.1",
"sqlite3": "^5.0.2",
"ts-node": "^9.1.1"
"ts-node": "^9.1.1",
"winston": "^3.3.3"
},
"devDependencies": {
"@types/luxon": "^1.26.2",

View File

@ -0,0 +1,13 @@
import { Knex } from "knex";
export async function up(knex: Knex) {
await knex.schema.createTable("recordings", (table) => {
table.bigInteger("messageId").primary().references("voicemails.messageId");
table.string("mimeType", 32);
table.binary("audio");
});
}
export async function down(knex: Knex) {
await knex.schema.dropTable("recordings");
}

View File

@ -0,0 +1,47 @@
import { Knex } from "knex";
export async function up(knex: Knex) {
return knex.transaction(async (trx) => {
await trx.schema.alterTable("recordings", (table) => {
table.integer("duration");
});
// transfer recording durations from voicemails table
await trx("recordings").update({
duration: knex("voicemails")
.select("duration")
.where("messageId", knex.raw("??", "recordings.messageId")),
});
// now we can make duration column not-nullable
await trx.schema.alterTable("recordings", (table) => {
table.integer("duration").notNullable().alter();
});
await trx.schema.alterTable("voicemails", (table) => {
table.dropColumn("duration");
});
});
}
export async function down(knex: Knex) {
return knex.transaction(async (trx) => {
await trx.schema.alterTable("voicemails", (table) => {
table.integer("duration");
});
await trx("voicemails").update({
duration: knex("recordings")
.select("duration")
.where("messageId", knex.raw("??", "voicemails.messageId")),
});
await trx.schema.alterTable("voicemails", (table) => {
table.integer("duration").notNullable().alter();
});
await trx.schema.alterTable("recordings", (table) => {
table.dropColumn("duration");
});
});
}

View File

@ -0,0 +1,17 @@
import { Knex } from "knex";
export async function up(knex: Knex) {
return knex.transaction((trx) =>
trx.schema.alterTable("voicemails", (table) => {
table.string("fromName", 64).alter();
})
);
}
export async function down(knex: Knex) {
return knex.transaction((trx) =>
trx.schema.alterTable("voicemails", (table) => {
table.string("fromName", 64).notNullable().alter();
})
);
}

View File

@ -3,16 +3,17 @@ import knexConfig from "./db";
import { Sonar, gql } from "./sonar";
import { SDK } from "@ringcentral/sdk";
import { ticketize } from "./ticketize";
import { logger, DEBUG } from "./util";
function checkEnv() {
[
"SONAR_URL",
"SONAR_TOKEN",
"RC_APP_KEY",
"RC_APP_SECRET",
"RC_LOGIN_USERNAME",
"RC_LOGIN_EXT",
"RC_LOGIN_PASSWORD",
"RINGCENTRAL_CLIENT_ID",
"RINGCENTRAL_CLIENT_SECRET",
"RINGCENTRAL_USERNAME",
"RINGCENTRAL_EXTENSION",
"RINGCENTRAL_PASSWORD",
"EXTENSION_TICKET_GROUPS",
].forEach((env) => {
if (process.env[env] === undefined) {
@ -30,6 +31,22 @@ function getExtensionToTicketGroupMapping() {
return mapping;
}
const DEFAULT_FIRST_RUN_AGE = 86400;
function getTicketizeConfig() {
const firstRunAge = process.env.FIRST_RUN_AGE
? parseInt(process.env.FIRST_RUN_AGE)
: DEFAULT_FIRST_RUN_AGE;
if (isNaN(firstRunAge) || firstRunAge <= 0) {
throw new Error("FIRST_RUN_AGE must be a valid positive integer");
}
return {
firstRunAge,
extensionToTicketGroup: getExtensionToTicketGroupMapping(),
};
}
async function initSonar() {
const sonar = new Sonar(process.env.SONAR_URL!, process.env.SONAR_TOKEN!);
// simple query to test API cedentials
@ -42,22 +59,44 @@ async function initSonar() {
}
`
);
console.log(`Authenticated to Sonar as '${user.me.name}'.`);
logger.info(`Authenticated to Sonar as '${user.me.name}'`);
return sonar;
}
async function initRingCentralSDK() {
const sdk = new SDK({
server: SDK.server[process.env.RC_SANDBOX ? "sandbox" : "production"],
clientId: process.env.RC_APP_KEY,
clientSecret: process.env.RC_APP_SECRET,
server:
SDK.server[
process.env.RINGCENTRAL_SERVER === "sandbox" ? "sandbox" : "production"
],
clientId: process.env.RINGCENTRAL_CLIENT_ID,
clientSecret: process.env.RINGCENTRAL_CLIENT_SECRET,
});
await sdk.login({
username: process.env.RC_LOGIN_USERNAME,
extension: process.env.RC_LOGIN_EXT,
password: process.env.RC_LOGIN_PASSWORD,
const login = () =>
sdk.login({
username: process.env.RINGCENTRAL_USERNAME,
extension: process.env.RINGCENTRAL_EXTENSION,
password: process.env.RINGCENTRAL_PASSWORD,
});
if (DEBUG) {
const client = sdk.client();
client.on(client.events.beforeRequest, (req) => {
logger.debug(req.url);
});
}
const platform = sdk.platform();
platform.on(platform.events.refreshError, async (err) => {
logger.error("Refresh token error:", err);
await login();
logger.info("RingCentral re-authentication successful");
});
console.log("Authenticated to RingCentral.");
await login();
logger.info("Authenticated to RingCentral");
return sdk;
}
@ -65,7 +104,7 @@ async function initDB() {
const db = knex(knexConfig);
if (!process.env.DB_SKIP_MIGRATIONS) {
await db.migrate.latest();
console.log("Database migrations run successfully.");
logger.info("Database migrations run successfully");
}
return db;
}
@ -78,14 +117,13 @@ async function main() {
const rcsdk = await initRingCentralSDK();
const db = await initDB();
console.log("Starting ticketizer...");
const intervals = ticketize(sonar, rcsdk, db, {
extensionToTicketGroup: getExtensionToTicketGroupMapping(),
});
logger.info("Starting");
const intervals = ticketize(sonar, rcsdk, db, getTicketizeConfig());
["SIGINT", "SIGTERM", "SIGQUIT"].forEach((sig) => {
process.on(sig, async () => {
console.log(`\nCaught ${sig}, shutting down...`);
logger.info(`Caught ${sig}, shutting down...`);
const results = await Promise.allSettled(
intervals.map((interval) => interval.clear())
);
@ -93,15 +131,15 @@ async function main() {
results.forEach((result) => {
if (result.status === "rejected") {
errors = true;
console.error(result.reason);
logger.error(result.reason);
}
});
console.log("exiting now");
await rcsdk.logout();
process.exit(errors ? 1 : 0);
});
});
} catch (err) {
console.error(err);
logger.error(err);
}
}

View File

@ -1,19 +1,25 @@
import React from "react";
import ReactDOMServer from "react-dom/server";
import type { Contact, StoredVoicemail } from "./types";
import { getNationalNumber, formatSeconds } from "./util";
import { DateTime } from "luxon";
import type { Contact } from "./types";
import type { StoredVoicemail, StoredRecording } from "knex/types/tables";
export function getTicketSubject(
voicemail: StoredVoicemail,
contact?: Contact
) {
return `New Voicemail from ${getNationalNumber(voicemail.fromNumber)} (${
contact ? contact.name : voicemail.fromName
})`;
function fromName(vm: StoredVoicemail, contact?: Contact) {
return contact?.name ?? vm.fromName ?? "unknown";
}
export function getTicketBody(vm: StoredVoicemail, contact?: Contact) {
export function getTicketSubject(vm: StoredVoicemail, contact?: Contact) {
const name = fromName(vm, contact);
return `New Voicemail from ${getNationalNumber(vm.fromNumber)} (${name})`;
}
export function getTicketBody(
vm: StoredVoicemail & StoredRecording,
contact?: Contact
) {
const name = fromName(vm, contact);
return ReactDOMServer.renderToStaticMarkup(
<div>
<div>
@ -21,8 +27,7 @@ export function getTicketBody(vm: StoredVoicemail, contact?: Contact) {
{DateTime.fromISO(vm.received).toLocaleString(DateTime.DATETIME_MED)}
</div>
<div>
<b>From:</b> {getNationalNumber(vm.fromNumber)} (
{contact?.name ?? vm.fromName})
<b>From:</b> {getNationalNumber(vm.fromNumber)} ({name})
</div>
<div>
<b>To:</b> {getNationalNumber(vm.toNumber)}x{vm.extensionNumber} (
@ -33,7 +38,12 @@ export function getTicketBody(vm: StoredVoicemail, contact?: Contact) {
</div>
<br />
<div>
<strong>Transcription: </strong>
<span>
<b>Transcription:</b>{" "}
{vm.transcriptionStatus === "CompletedPartially" ? (
<i>(partial)</i>
) : undefined}
</span>
<p>
<i>
{vm.transcription

View File

@ -1,7 +1,7 @@
import SDK from "@ringcentral/sdk";
import path from "path";
import { Knex } from "knex";
import { getNationalNumber, setAsyncInterval } from "./util";
import { logger, getNationalNumber, setAsyncInterval } from "./util";
import { getTicketSubject, getTicketBody } from "./template";
import { Sonar, gql } from "./sonar";
import type {
@ -11,8 +11,8 @@ import type {
RCAudioAttachment,
Recording,
Transcription,
StoredVoicemail,
} from "./types";
import type { StoredVoicemail, StoredRecording } from "knex/types/tables";
const SEARCH_CONTACT_BY_PHONE_NUMBER_QUERY = gql`
query getContactByPhoneNumber($phoneNumber: String!) {
@ -49,6 +49,7 @@ function rcapi(short: string, version = "v1.0") {
}
interface TicketizeConfig {
firstRunAge: number;
extensionToTicketGroup: { [key: string]: number };
}
@ -62,7 +63,7 @@ export function ticketize(
sonar: Sonar,
rcsdk: SDK,
db: Knex,
{ extensionToTicketGroup }: TicketizeConfig
{ firstRunAge, extensionToTicketGroup }: TicketizeConfig
) {
/**
* Uploads a file to Sonar, returning its ID.
@ -92,17 +93,17 @@ export function ticketize(
}
/**
* Returns `extensionId`s messages that are up to `from` seconds old.
* Returns `extensionId`s messages that are up to `age` seconds old.
*
* @param extensionId
* @param from how many seconds ago to retrieve messages from
* @param age the maximum age (in seconds) of voicemails to fetch
*/
async function getExtensionVoicemails(extensionId: number, from = 86000) {
async function getExtensionVoicemails(extensionId: number, age = 86000) {
const result = await rcsdk.get(
rcapi(`/account/~/extension/${extensionId}/message-store`),
{
messageType: "VoiceMail",
dateFrom: new Date(Date.now() - from * 1000).toISOString(),
dateFrom: new Date(Date.now() - age * 1000).toISOString(),
}
);
return (await result.json()).records as RCMessage[];
@ -168,8 +169,8 @@ export function ticketize(
const response = await rcsdk.get(audio.uri);
const result = {
duration: audio.vmDuration,
mimetype: audio.contentType,
audio: await response.blob(),
mimeType: audio.contentType,
audio: await response.arrayBuffer(),
};
return result;
}
@ -193,7 +194,10 @@ export function ticketize(
* @param voicemail
* @param contact
*/
async function createTicket(voicemail: StoredVoicemail, contact?: Contact) {
async function createTicket(
voicemail: StoredVoicemail & StoredRecording,
contact?: Contact
) {
const input: any = {
subject: getTicketSubject(voicemail, contact),
description: getTicketBody(voicemail, contact),
@ -229,29 +233,38 @@ export function ticketize(
recording: Recording,
transcription: Transcription
) {
return db<StoredVoicemail>("voicemails").insert({
messageId: message.id,
extensionId: message.extensionId,
received: message.creationTime,
toNumber: message.to[0].phoneNumber,
extensionNumber: extension.extensionNumber,
extensionName: extension.name,
fromNumber: message.from.phoneNumber,
fromName: message.from.name,
duration: recording.duration,
transcriptionStatus: transcription.status,
transcription: transcription.text,
await db.transaction(async (trx) => {
await trx("voicemails").insert({
messageId: message.id,
extensionId: message.extensionId,
received: message.creationTime,
toNumber: message.to[0].phoneNumber,
extensionNumber: extension.extensionNumber,
extensionName: extension.name,
fromNumber: message.from.phoneNumber,
fromName: message.from.name,
transcriptionStatus: transcription.status,
transcription: transcription.text,
});
await trx("recordings").insert({
messageId: message.id,
mimeType: recording.mimeType,
audio: new Uint8Array(recording.audio),
duration: recording.duration,
});
});
}
/**
* Updates a stored voicemail using its current properties
* Updates a stored voicemail
* @param voicemail the voicemail to update
*/
async function updateStoredVoicemail(voicemail: StoredVoicemail) {
await db<StoredVoicemail>("voicemails")
.update({ ...voicemail })
.where({ messageId: voicemail.messageId });
async function updateStoredVoicemail(voicemail: Partial<StoredVoicemail>) {
const messageId = voicemail.messageId;
if (!messageId) {
throw new Error("Missing required messageId property");
}
await db("voicemails").update(voicemail).where({ messageId });
}
/**
@ -259,9 +272,7 @@ export function ticketize(
* @returns whether the message by the given ID has been stored
*/
async function isMessageStored(messageId: number) {
const result = await db<StoredVoicemail>("voicemails")
.where({ messageId })
.first();
const result = await db("voicemails").where({ messageId }).first();
return result !== undefined;
}
@ -269,7 +280,8 @@ export function ticketize(
* @returns stored voicemails that haven't had tickets created for them yet
*/
async function getUnprocessedVoicemails() {
return await db<StoredVoicemail>("voicemails")
return await db("voicemails")
.join("recordings", "voicemails.messageId", "recordings.messageId")
.whereNull("ticketId")
.whereIn("transcriptionStatus", [
"Completed",
@ -283,8 +295,8 @@ export function ticketize(
* @returns stored voicemails whose trranscriptions may still be in progress
*/
async function getMissingTranscriptionVoicemails() {
return await db<StoredVoicemail>("voicemails")
.whereNotNull("transcription")
return await db("voicemails")
.whereNull("transcription")
.whereNotIn("transcriptionStatus", [
// Don't include those whose transcriptions have failed or will not
// be completed.
@ -294,16 +306,13 @@ export function ticketize(
}
/**
* Retrieves and stores the voicemails for `extension` that are up to `from`
* Retrieves and stores the voicemails for `extension` that are up to `age`
* seconds old.
* @param extension
* @param from
* @param age
*/
async function storeExtensionVoicemails(
extension: RCExtension,
from: number
) {
const messages = await getExtensionVoicemails(extension.id, from);
async function storeExtensionVoicemails(extension: RCExtension, age: number) {
const messages = await getExtensionVoicemails(extension.id, age);
const isStored = await Promise.all(
messages.map((message) => isMessageStored(message.id))
);
@ -311,7 +320,9 @@ export function ticketize(
messages
.filter((_, i) => !isStored[i])
.map(async (message) => {
console.log("Saving voicemail", message.id);
logger.info(
`New voicemail ${message.id} from ${message.from.phoneNumber} at ${message.creationTime}`
);
return storeVoicemail(
extension,
message,
@ -324,15 +335,16 @@ export function ticketize(
/**
* Fetch and store new voicemails. If this is the first run, we get the last
* day's worth of voicemails. Otherwise, we fetch only the last 15 minutes.
* day's worth of voicemails. Otherwise, we fetch only the last 5 minutes.
*
* @param firstRun whether this is the first run or not
* @param firstRun whether this is the first run
*/
async function fetchAndStoreNewVoicemails(firstRun = false) {
logger.verbose("Checking for new voicemails");
const extensions = await getValidRCExtensions();
return Promise.all(
extensions.map((extension) =>
storeExtensionVoicemails(extension, firstRun ? 86400 : 900)
storeExtensionVoicemails(extension, firstRun ? firstRunAge : 300)
)
);
}
@ -366,7 +378,11 @@ export function ticketize(
// else we do nothing
return;
}
return updateStoredVoicemail(message);
return updateStoredVoicemail({
messageId: message.messageId,
transcriptionStatus: message.transcriptionStatus,
transcription: message.transcription,
});
})
);
}
@ -381,11 +397,11 @@ export function ticketize(
voicemails.map(async (voicemail) => {
const contact = await searchContactByPhoneNumber(voicemail.fromNumber);
const ticketId = await createTicket(voicemail, contact);
console.log(
logger.info(
`Created ticket ${ticketId} from voicemail ${voicemail.messageId}`
);
return updateStoredVoicemail({
...voicemail,
messageId: voicemail.messageId,
ticketId,
contactId: contact?.id,
contactableType: contact?.contactable.__typename,
@ -395,30 +411,26 @@ export function ticketize(
);
}
function catchHandler(reason: any) {
console.error(reason);
}
const catchHandler = logger.error;
let firstRun = true;
return [
setAsyncInterval(
() => {
const promise = fetchAndStoreNewVoicemails(firstRun);
const promise = fetchAndStoreNewVoicemails(firstRun).catch(
catchHandler
);
firstRun = false;
return promise.catch(catchHandler);
return promise;
},
60 * 1000,
true // immediate
),
setAsyncInterval(
() => fetchMissingTranscriptions().catch(catchHandler),
60 * 1000,
true
),
setAsyncInterval(
() => createTickets().catch(catchHandler),
60 * 1000,
15 * 1000,
true
),
setAsyncInterval(() => createTickets().catch(catchHandler), 1000, true),
];
}

View File

@ -97,8 +97,8 @@ export interface RCMessage {
export interface Recording {
duration: number;
mimetype: string;
audio: Blob;
mimeType: string;
audio: ArrayBuffer;
}
export interface Transcription {
@ -106,21 +106,33 @@ export interface Transcription {
text: string | null;
}
export interface StoredVoicemail {
messageId: number;
extensionId: number;
processed: boolean;
received: string;
toNumber: string;
extensionNumber: string;
extensionName: string;
fromNumber: string;
fromName?: string;
duration: number;
transcriptionStatus: TranscriptionStatus;
transcription: string | null;
ticketId?: number;
contactId?: number;
contactableType?: string;
contactableId?: number;
declare module "knex/types/tables" {
interface StoredVoicemail {
messageId: number;
extensionId: number;
received: string;
toNumber: string;
extensionNumber: string;
extensionName: string;
fromNumber: string;
fromName: string;
transcriptionStatus: TranscriptionStatus;
transcription: string | null;
ticketId?: number;
contactId?: number;
contactableType?: string;
contactableId?: number;
}
interface StoredRecording {
messageId: number;
mimeType: string;
audio: ArrayBuffer;
duration: number;
}
interface Tables {
voicemails: StoredVoicemail;
recordings: StoredRecording;
}
}

View File

@ -1,4 +1,18 @@
import PhoneNumber from "awesome-phonenumber";
import winston, { format } from "winston";
export const DEBUG = !!process.env.DEBUG;
export const logger = winston.createLogger({
level: DEBUG ? "debug" : process.env.LOG_LEVEL ?? "info",
transports: [new winston.transports.Console()],
format: format.combine(
format.errors({ stack: true }),
format.printf(
({ level, message, stack }) =>
`${level}: ${message}${stack ? "\n" + stack : ""}`
)
),
});
export function getNationalNumber(input: string) {
const number = new PhoneNumber(input);