├── .eslintignore ├── .eslintrc.json ├── public ├── favicon.ico ├── index.html └── assets │ └── js │ └── uikit-icons.js ├── .editorconfig ├── .babelrc ├── app.json ├── .github └── workflows │ └── metrics.yml ├── app.js ├── components ├── end-call.vue ├── ot-subscriber.vue ├── home.vue ├── ot-publisher.vue ├── self-view.vue ├── caller.vue └── agent.vue ├── .gitignore ├── LICENSE ├── cert.pem ├── key.pem ├── webpack.config.js ├── CONTRIBUTING.md ├── package.json ├── CODE_OF_CONDUCT.md ├── README.md └── server.js /.eslintignore: -------------------------------------------------------------------------------- 1 | vendor/ 2 | dist/ 3 | uikit* 4 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "standard", 3 | "env": { 4 | "es6": true 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opentok/opentok-video-call-center/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-env", 5 | { 6 | "modules": false 7 | } 8 | ], 9 | ], 10 | "plugins": [ 11 | "@babel/plugin-syntax-dynamic-import", 12 | "@babel/plugin-syntax-import-meta", 13 | "@babel/plugin-proposal-class-properties", 14 | "@babel/plugin-proposal-json-strings" 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "addons": [], 4 | "buildpacks": [], 5 | "env": { 6 | "OPENTOK_API_KEY": { 7 | "description": "API key from OpenTok", 8 | "required": true 9 | }, 10 | "OPENTOK_API_SECRET": { 11 | "description": "API secret from OpeTok", 12 | "required": true 13 | } 14 | }, 15 | "formation": {}, 16 | "name": "opentok-call-center-demo", 17 | "scripts": {} 18 | } 19 | -------------------------------------------------------------------------------- /.github/workflows/metrics.yml: -------------------------------------------------------------------------------- 1 | name: Aggregit 2 | 3 | on: 4 | schedule: 5 | - cron: "0 0 * * *" 6 | 7 | jobs: 8 | recordMetrics: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: michaeljolley/aggregit@v1 12 | with: 13 | githubToken: ${{ secrets.GITHUB_TOKEN }} 14 | project_id: ${{ secrets.project_id }} 15 | private_key: ${{ secrets.private_key }} 16 | client_email: ${{ secrets.client_email }} 17 | firebaseDbUrl: ${{ secrets.firebaseDbUrl }} 18 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | OpenTok Call Center demo 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 | 15 |
16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import VueRouter from 'vue-router' 3 | 4 | // routes 5 | import Home from './components/home' 6 | import Agent from './components/agent' 7 | import Caller from './components/caller' 8 | import EndCall from './components/end-call' 9 | 10 | Vue.use(VueRouter) 11 | 12 | const router = new VueRouter({ 13 | routes: [ 14 | { path: '/', component: Home }, 15 | { path: '/agent', component: Agent }, 16 | { path: '/caller', component: Caller }, 17 | { path: '/end', component: EndCall } 18 | ] 19 | }) 20 | 21 | new Vue({ 22 | router 23 | // render: h => h(App) 24 | }).$mount('#app') 25 | -------------------------------------------------------------------------------- /components/end-call.vue: -------------------------------------------------------------------------------- 1 | 19 | -------------------------------------------------------------------------------- /components/ot-subscriber.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 34 | -------------------------------------------------------------------------------- /components/home.vue: -------------------------------------------------------------------------------- 1 | 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | \#*\# 3 | .\#* 4 | 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | 10 | # Runtime data 11 | pids 12 | *.pid 13 | *.seed 14 | *.pid.lock 15 | 16 | # Directory for instrumented libs generated by jscoverage/JSCover 17 | lib-cov 18 | 19 | # Coverage directory used by tools like istanbul 20 | coverage 21 | 22 | # nyc test coverage 23 | .nyc_output 24 | 25 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 26 | .grunt 27 | 28 | # node-waf configuration 29 | .lock-wscript 30 | 31 | # Compiled binary addons (http://nodejs.org/api/addons.html) 32 | build/Release 33 | 34 | # Dependency directories 35 | node_modules 36 | jspm_packages 37 | 38 | # Optional npm cache directory 39 | .npm 40 | 41 | # Optional eslint cache 42 | .eslintcache 43 | 44 | # Optional REPL history 45 | .node_repl_history 46 | 47 | # Output of 'npm pack' 48 | *.tgz 49 | 50 | # Yarn Integrity file 51 | .yarn-integrity 52 | 53 | # Ignore tern files 54 | .tern-port 55 | 56 | # Ignore config.js 57 | config.js 58 | 59 | # Ignore dist folder 60 | dist/ 61 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright Kaustav Das Modak 2017 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so, 8 | subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 15 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 16 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 17 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 18 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | -------------------------------------------------------------------------------- /components/ot-publisher.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 49 | -------------------------------------------------------------------------------- /cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDjDCCAnSgAwIBAgIJAOUIyEjII1knMA0GCSqGSIb3DQEBCwUAMFsxCzAJBgNV 3 | BAYTAklOMRUwEwYDVQQHDAxEZWZhdWx0IENpdHkxHDAaBgNVBAoME0RlZmF1bHQg 4 | Q29tcGFueSBMdGQxFzAVBgNVBAMMDmxvY2FsaG9zdDo4MDgwMB4XDTE3MTIyNzE4 5 | MTQwM1oXDTE4MTIyNzE4MTQwM1owWzELMAkGA1UEBhMCSU4xFTATBgNVBAcMDERl 6 | ZmF1bHQgQ2l0eTEcMBoGA1UECgwTRGVmYXVsdCBDb21wYW55IEx0ZDEXMBUGA1UE 7 | AwwObG9jYWxob3N0OjgwODAwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB 8 | AQCvSQcB03v3juWWoVU69PMI8PyP9Wn/lea/Dy8Bp63OE+e7XKWrNmmRKURgcDdA 9 | Ok/lnnhe6AdL/8uJ8DOAJ/Gl3QzuaGFVfGyEJki/7kD3CXYEi73iAslN8kLEQOcU 10 | HSNu/3SXGhwBlF130H9uU8WXOWBOJA2VTi7Jv+BuOaWYi7RtRQrCH7rxZPVOrFdz 11 | 7iuz9ToVhbRFFzLYqIn1qUSdRriV/LDjYd84ezGFpxked1DAFXh+BFXLxnFyudwO 12 | Nc7/0bq0bQCwtPAHPTcSu0mQwQPnCUEv7UqYi9TBS+XsgpExMpOvUOjbx/3f/KYm 13 | J7xyGIVLOI3ArdGUymsbEg9JAgMBAAGjUzBRMB0GA1UdDgQWBBS53fxH5Bep3sYQ 14 | t6GmYIgL2klUfjAfBgNVHSMEGDAWgBS53fxH5Bep3sYQt6GmYIgL2klUfjAPBgNV 15 | HRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQBndGsws/3TAJhPRB7bhryw 16 | Qydmu6YFg3ALALoDlGG3dy01sRbE5SAl0tkM3k7aTo1cAYLi+Z09u1dhNDw55gXD 17 | aUfloKZiiq55qTjdC5rmaE8x/EFaa4/UICwbYgCY+4PZtzmHsNrOsr41I7A8xuqI 18 | C22sqhO+l62E4xUiiakQ7qQPJSbLOiHDSY+4sCUoOF9xi/6k1/mpM71stHurOXAp 19 | onpTxAcfeTZgkBeYJIVcnlCGD/vp1oFeYAAnRVcc4bo2Q+PtXHdGr3RFWmkJHnv7 20 | E4kIWolNOKreimSUlTVymcRVMiSw6k0TjO4KZzdKSeRHGIkkbAnxpvPQkcBETRLd 21 | -----END CERTIFICATE----- 22 | -------------------------------------------------------------------------------- /key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCvSQcB03v3juWW 3 | oVU69PMI8PyP9Wn/lea/Dy8Bp63OE+e7XKWrNmmRKURgcDdAOk/lnnhe6AdL/8uJ 4 | 8DOAJ/Gl3QzuaGFVfGyEJki/7kD3CXYEi73iAslN8kLEQOcUHSNu/3SXGhwBlF13 5 | 0H9uU8WXOWBOJA2VTi7Jv+BuOaWYi7RtRQrCH7rxZPVOrFdz7iuz9ToVhbRFFzLY 6 | qIn1qUSdRriV/LDjYd84ezGFpxked1DAFXh+BFXLxnFyudwONc7/0bq0bQCwtPAH 7 | PTcSu0mQwQPnCUEv7UqYi9TBS+XsgpExMpOvUOjbx/3f/KYmJ7xyGIVLOI3ArdGU 8 | ymsbEg9JAgMBAAECggEAUNDSsjzXgyQXJoPrz+rvwseZKqZz4Ks0YBKYEaNFtyXm 9 | SbRFjdLgiVUFeZFDaBF6lNujk95WvuuI5Ggi1ouUFmz8cU8e0VM0lRkxoYwv17Y8 10 | +4uSWIoAVjfEIMfrwpJMq/iLwL8A+pl1HrC6kBt/lmGVzXFB8fzCBbK5vxhFCTdc 11 | 7MQezivslBccp0aH8cjEPSxF6jHlDDUyu4vG9Jo2dml2wIFLwdCqnaZVGh2ff89l 12 | 5D0JUOH7sf5x3y9XA1zPPtmqZvYdULL5xm4CH0U/3LqR4dprHLWNAPAobOsFQDnN 13 | Wf0ANqdYlxzjJRxB8w9sTBMeV9ZD5Ovz/Ybm+KGQgQKBgQDY7uaxYIaoZ63ffp5Q 14 | WlsXkOk3uWaM4zw8I8ITPZlWf2b9Y0iUB2ediq2jz1nB+gG8FUve65tGPDp4rj8d 15 | FSf3R0h+Hb+m26pKx3OfGDCC7U092jDvytSMeuUErevPBGpskzhH8DwPhIq+n6Ba 16 | GrtNMbKWTrMYMwCVNiJgX7K+TQKBgQDO2hBw8uRrQpWHSbDFWhgCZXc1elpDZBKv 17 | zcmi+al9mUys8qb+zpZy5oaQ5NfMa9buLAQ/yGbrvWovasxMiiLDoJo9DsbMqMeX 18 | aREHF0kbEu1ytnVjBlYitlAuHAWWKt1Dj1FOFnOtgERG5LqVyyhj5yN9bp8dnhSX 19 | Ljzp3Pxq7QKBgQC/Rf4gneR9blVPv3vli5XP9JS97noudVmyCTFg96pRKypq9vSS 20 | mOjSbullizkwILkQ1pI8Lu+NASPpLnI1uWaw0Khpkt9eR0cigjQ/LfvwJT468Dy/ 21 | 4c6BQwbVlmhZ8yHHNBOm0nqSkCIpq+OeLv3BNbWqdB/TkXzNE5tB8H9Q7QKBgEZ1 22 | CpKeeWV7mZkqZZbjWDhAvXkuwt4fkSnmy57CsZPBiteCE5XJYn+ivAVQnZzYwq3/ 23 | ujbLmRsFOs0J8KrFho/h/Yd8qASAHPQa6pzitpkNOmoPci6XsvFB7k+2ZcS/tvxT 24 | LduOVDqt7RPExzVMrOZSjckky/f3p4XTTxZNmoEFAoGABUJaxQu7CvnjFPSyLB6F 25 | dBO9tSz9A/00F0X/6QVg2YDDh9f7X+v42FUFsz+nWMTftXC8fy9f2o0LFHZ2o4Cc 26 | c2p8kgaDFWsOV4XcA4dPcQzSfmEJKW92Xs2HBhJk4T4rM+z7PYixYG64PThy3Kly 27 | vsedqkkMu+whBv1BnfEx/os= 28 | -----END PRIVATE KEY----- 29 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path') 2 | var webpack = require('webpack') 3 | const { VueLoaderPlugin } = require("vue-loader"); 4 | const TerserPlugin = require("terser-webpack-plugin"); 5 | 6 | 7 | module.exports = { 8 | entry: ['./app.js'], 9 | output: { 10 | path: path.resolve(__dirname, './public/dist'), 11 | publicPath: '/dist/', 12 | filename: 'build.js' 13 | }, 14 | plugins: [new VueLoaderPlugin()], 15 | module: { 16 | rules: [ 17 | { 18 | test: /\.css$/, 19 | use: [ 20 | 'vue-style-loader', 21 | 'css-loader' 22 | ] 23 | }, { 24 | test: /\.vue$/, 25 | loader: 'vue-loader', 26 | options: { 27 | loaders: { 28 | } 29 | // other vue-loader options go here 30 | } 31 | }, 32 | { 33 | test: /\.js$/, 34 | loader: 'babel-loader', 35 | exclude: /node_modules/ 36 | }, 37 | { 38 | test: /\.(png|jpg|gif|svg)$/, 39 | loader: 'file-loader', 40 | options: { 41 | name: '[name].[ext]?[hash]' 42 | } 43 | } 44 | ] 45 | }, 46 | resolve: { 47 | alias: { 48 | 'vue$': 'vue/dist/vue.esm.js' 49 | }, 50 | extensions: ['*', '.js', '.vue', '.json'] 51 | }, 52 | devServer: { 53 | historyApiFallback: true, 54 | noInfo: true, 55 | overlay: true 56 | }, 57 | performance: { 58 | hints: false 59 | }, 60 | devtool: 'eval-source-map', 61 | optimization: { 62 | minimize: true, 63 | minimizer: [new TerserPlugin()], 64 | }, 65 | } 66 | 67 | if (process.env.NODE_ENV === 'production') { 68 | module.exports.devtool = 'source-map' 69 | // http://vue-loader.vuejs.org/en/workflow/production.html 70 | module.exports.plugins = (module.exports.plugins || []).concat([ 71 | new webpack.DefinePlugin({ 72 | 'process.env': { 73 | NODE_ENV: '"production"' 74 | } 75 | }), 76 | new webpack.LoaderOptionsPlugin({ 77 | minimize: true 78 | }) 79 | ]) 80 | } 81 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | For anyone looking to get involved to this project, we are glad to hear from you. Here are a few types of contributions 4 | that we would be interested in hearing about. 5 | 6 | * Bug fixes 7 | - If you find a bug, please first report it using Github Issues. 8 | - Issues that have already been identified as a bug will be labelled `bug`. 9 | - If you'd like to submit a fix for a bug, send a Pull Request from your own fork and mention the Issue number. 10 | * New Features 11 | - If you'd like to accomplish something in the library that it doesn't already do, describe the problem in a new 12 | Github Issue. 13 | - Issues that have been identified as a feature request will be labelled `enhancement`. 14 | - If you'd like to implement the new feature, please wait for feedback from the project maintainers before spending 15 | too much time writing the code. In some cases, `enhancement`s may not align well with the project objectives at 16 | the time. 17 | * Documentation and Miscellaneous 18 | - If you think the documentation could be clearer, you've got an alternative 19 | implementation of something that may have more advantages, or any other change we would still be glad hear about 20 | it. 21 | - If its a trivial change, go ahead and send a Pull Request with the changes you have in mind 22 | - If not, open a Github Issue to discuss the idea first. 23 | 24 | ## Requirements 25 | 26 | For a contribution to be accepted: 27 | 28 | * Code must follow existing styling conventions 29 | * Commit messages must be descriptive. Related issues should be mentioned by number. 30 | 31 | If the contribution doesn't meet these criteria, a maintainer will discuss it with you on the Issue. You can still 32 | continue to add more commits to the branch you have sent the Pull Request from. 33 | 34 | ## How To 35 | 36 | 1. Fork this repository on GitHub. 37 | 1. Clone/fetch your fork to your local development machine. 38 | 1. Create a new branch (e.g. `issue-12`, `feat.add_foo`, etc) and check it out. 39 | 1. Make your changes and commit them. 40 | 1. Push your new branch to your fork. (e.g. `git push myname issue-12`) 41 | 1. Open a Pull Request from your new branch to the original fork's `master` branch. 42 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "opentok-call-center", 3 | "version": "0.0.1", 4 | "description": "OpenTok Call Center queueing demo", 5 | "main": "server.js", 6 | "private": true, 7 | "scripts": { 8 | "start": "npm run start-server", 9 | "dev": "npm run app-dev && npm run start-server", 10 | "start-server": "node server", 11 | "test": "eslint .", 12 | "certs": "openssl req -newkey rsa:2048 -nodes -keyout key.pem -x509 -days 365 -out cert.pem", 13 | "app-dev": "cross-env NODE_ENV=development webpack", 14 | "app-build": "cross-env NODE_ENV=production webpack", 15 | "heroku-postbuild": "npm run app-build" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "git+https://github.com/opentok/opentok-video-call-center.git" 20 | }, 21 | "keywords": [ 22 | "opentok", 23 | "demo" 24 | ], 25 | "author": "Kaustav Das Modak ", 26 | "license": "MIT", 27 | "bugs": { 28 | "url": "https://github.com/opentok/opentok-video-call-center/issues" 29 | }, 30 | "homepage": "https://github.com/opentok/opentok-video-call-center#readme", 31 | "devDependencies": { 32 | "eslint": "^8.0", 33 | "eslint-config-standard": "^11.0.0-beta.0", 34 | "eslint-plugin-import": "^2.8.0", 35 | "eslint-plugin-node": "^5.2.1", 36 | "eslint-plugin-promise": "^3.6.0", 37 | "eslint-plugin-standard": "^3.0.1", 38 | "terser-webpack-plugin": "^5.3.6", 39 | "webpack-cli": "^5.0.1" 40 | }, 41 | "dependencies": { 42 | "@babel/core": "^7.0", 43 | "@babel/plugin-syntax-dynamic-import": "^7.8.3", 44 | "@babel/plugin-syntax-import-meta": "^7.10.4", 45 | "@babel/preset-env": "^7.0.0", 46 | "@opentok/client": "^2.12.2", 47 | "axios": "^0.21.2", 48 | "babel-loader": "^8.0.0", 49 | "body-parser": "^1.20.1", 50 | "compression": "^1.7.1", 51 | "cross-env": "^5.0.5", 52 | "css-loader": "^5.0.0", 53 | "express": "^4.18.2", 54 | "file-loader": "^6.0", 55 | "opentok": "^2.15.2", 56 | "vue": "^2.5.11", 57 | "vue-loader": "^15.10.1", 58 | "vue-router": "^3.0.1", 59 | "vue-template-compiler": "^2.4.4", 60 | "webpack": "^5.0.0", 61 | "webpack-dev-server": "^4.0.0", 62 | "ws": "^3.3.3" 63 | }, 64 | "browserslist": [ 65 | "> 1%", 66 | "last 2 versions", 67 | "not ie <= 8" 68 | ] 69 | } 70 | -------------------------------------------------------------------------------- /components/self-view.vue: -------------------------------------------------------------------------------- 1 | 42 | 43 | 111 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | - Demonstrating empathy and kindness toward other people 21 | - Being respectful of differing opinions, viewpoints, and experiences 22 | - Giving and gracefully accepting constructive feedback 23 | - Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | - Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | - The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | - Trolling, insulting or derogatory comments, and personal or political attacks 33 | - Public or private harassment 34 | - Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | - Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | devrel@vonage.com. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /components/caller.vue: -------------------------------------------------------------------------------- 1 | 90 | 91 | 203 | -------------------------------------------------------------------------------- /components/agent.vue: -------------------------------------------------------------------------------- 1 | 57 | 58 | 63 | 64 | 65 | 301 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OpenTok Call Center Demo 2 | 3 | This demo showcases a simulation of call center where callers queue up to talk to available agents using OpenTok. 4 | 5 | [![Deploy to Heroku](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy?template=https://github.com/opentok/opentok-video-call-center) 6 | 7 | ## OpenTok features used 8 | 9 | These are the OpenTok features used in this demo: 10 | 11 | - Server-side SDK (NodeJS): Used to created dynamic session and token creation per call 12 | - Publish and subscribe streams: For connecting agent with caller 13 | - Signaling - To signal for agent join, call hold and call resume. 14 | - Session monitoring - To keep active caller list updated 15 | 16 | ## Install 17 | 18 | Install NodeJS v8.0+ 19 | 20 | Install dependencies with `npm` 21 | 22 | ```sh 23 | $ npm install 24 | ``` 25 | 26 | Get OpenTok API keys and set them as environment variables: 27 | 28 | ```sh 29 | $ export OPENTOK_API_KEY="opentok-api-key-here" 30 | $ export OPENTOK_API_SECRET="opentok-api-secret-here" 31 | ``` 32 | 33 | **Register session monitoring callback** in your [TokBox account](https://tokbox.com/account) for the path `/ot_callback`. For example, if this application is hosted at `https://example.com`, register this URL: `https://example.com/ot_callback`. 34 | 35 | Build assets for the app, run: 36 | 37 | ```sh 38 | $ npm run app-build 39 | ``` 40 | 41 | ### Start the server 42 | 43 | ```sh 44 | $ npm start 45 | ``` 46 | 47 | This will start the application on port `8080`. To change port, set the `PORT` environment variable. For example, to start the application on port `3000`, do this: 48 | 49 | ```sh 50 | $ PORT=3000 npm start 51 | ``` 52 | 53 | To start secure server, set the `SECURE` environment variable to some value. For example, this will start the application on HTTPS port 3000: 54 | 55 | ```sh 56 | $ SECURE=1 PORT=3000 npm start 57 | ``` 58 | 59 | ### Development use 60 | 61 | For development use, you can compile assets in development mode and launch server by running: 62 | 63 | ```sh 64 | $ npm run dev 65 | ``` 66 | 67 | ## Walkthrough 68 | 69 | This application builds a simple queue where callers can wait for agents to talk to them. It uses NodeJS as the backend and VueJS in the frontend. 70 | 71 | Note: The server stores all data in-memory for the purpose of this demo. A real-world application should use some database instead. 72 | 73 | ### Application flow 74 | 75 | #### Callers 76 | 77 | - Callers provide their name and reason for call when joining. They can also select whether they want to join as audio-only or using both audio and video. 78 | - Once callers have joined, they wait in queue till an agent joins that call. 79 | - Callers remain connected when they have been put on hold or if agent has disconnected. 80 | - Callers can decide to exit the call at any time. 81 | - Callers exit if agent triggers call end. 82 | 83 | #### Agents 84 | 85 | - Agents join and wait for callers to be available. 86 | - Each agent can be assigned multiple callers. Assignment is done using least connections strategy - when a new caller joins, the caller is assigned to the agent with least number of assigned callers. 87 | - When a caller is assigned to agent, agent's caller list is updated. 88 | - Agent can join one caller at a time. 89 | - Agent can put an existing caller on hold to join another caller. 90 | - Agent can resume a call. 91 | - Agent can end the call they are connected to. This will make the caller exit as well. 92 | - Agent can switch between callers that agent has been assigned. If agent switches to another caller, then the current caller is put on hold. 93 | 94 | ### Server 95 | 96 | Backend logic for this demo is present in [**server.js**](server.js). These are what the server does: 97 | 98 | - Call OpenTok REST API using the [OpenTok Node SDK](https://tokbox.com/developer/sdks/node/). 99 | - Create functions to generate OpenTok sessions and tokens. 100 | - Define `Caller` constructor used to represent a caller and provide methods to perform actions on each caller. 101 | - Define `Agent` constructor used to represent an agent and provide methods to perform actions on each agent. 102 | - Manage pending callers queue - A list of callers who are yet to be assigned to any agent. 103 | - Perform assignment of callers to available agents. 104 | - Send [signals through the OpenTok REST API](https://tokbox.com/developer/guides/signaling/rest/) to coordinate interactions between agent and caller. 105 | - Handle OpenTok session monitoring callbacks for connection created and connection destroyed events. This is used to keep track of when callers are ready to join agents and when they have left. 106 | - Provides HTTP interfaces for frontend to communicate, with endpoints for agents and callers. 107 | 108 | ### Frontend 109 | 110 | The frotend is a simple single-page application (SPA) built on VueJS with vue-router to route between agent and caller screens. It uses UIKit for drawing the UI. The demo intentionally keeps things simple without breaking down code into further components or customizing much of the UI elements. 111 | 112 | These are the relevant files: 113 | 114 | - [`app.js`](app.js): Bootstrapping script that loads vue and vue-router and mounts routes from the components. 115 | - [`components/`](components): Directory where all vue components are stored 116 | - [`components/home.vue`](components/home.vue): Template for the homepage of the demo 117 | - [`components/caller.vue`](components/caller.vue): Component used for the caller screen. This sets up the caller's initial form and then manages the whole lifecycle of the caller. 118 | - [`components/agent.vue`](components/agent.vue): Component used for the agent screen. This manages entire lifecycle of the agent. 119 | - [`components/ot-publisher.vue`](components/ot-publisher.vue) and [`components/ot-subscriber.vue`](components/ot-subscriber.vue) provide reusable components for OpenTok publisher and subscriber objects. 120 | 121 | #### Agent screen 122 | 123 | The agent screen has all the magic in this demo. Here is how agent screen handles callers in the frotnend: 124 | 125 | 1. Each caller uses a different OpenTok session. 126 | 2. When agent wants to join a caller, the frontend retrieves a token for the agent for the session of that caller. Then, it connects agent to that session, publishes agent stream and subscribes to existing caller stream. 127 | 3. When agent wants to put a caller on hold, agent unpublishes their stream and disconnects from the session. 128 | 4. When agent wants to resume talking to a caller they have put on hold, step 2 is repeated. 129 | 130 | So, the agent keeps on switching between OpenTok sessions - connecting to them and disconnecting as required. This whole process takes a reasonably short time. At each stage, the application sends out signals for each event so that the client UI can adjust accordingly. 131 | 132 | ### Call queue management 133 | 134 | A core part of this demo is managing caller queue and assigning callers to agents. All of this happens on the server side in `server.js`. It uses OpenTok's session monitoring to reliably determine when a caller has connected or disconnected. 135 | 136 | Call queue management is composed of six main pieces: 137 | 138 | - `callers[]`: List of current callers. This is a `Map` that uses caller ID as key and its corresponding `Caller` instance as the value. 139 | - `agents[]`: List of active agents. This is another `Map` that uses agent ID as key and its corresponding `Agent` instance as the value. 140 | - `pendingQueue[]`: An array that stores callers (instances of `Caller`) who are yet to be assigned to any agent. 141 | - `assignCaller(caller)`: A function that takes a `Caller` instance as argument and assigns it to an agent. If no agent is connected, this function adds the given caller to `pendingQueue[]`. 142 | - `agent.assignPending(limit = 1)`: A method on `Agent` that assigns a number of callers from `pendingQueue[]` to given agent in FIFO mode - callers who were in the queue earlier are assigned first. 143 | - `removeCaller(callerID)`: A function that removes a caller from list of active callers and also from `pendingQueue[]`. 144 | 145 | Here is a step-by-step description of how the call queue logic is handled: 146 | 147 | 1. When a caller connects to the application, the caller screen access the HTTP endpoint at `GET /dial`. 148 | - This creates a new instance of the `Caller` constructor by calling `new Caller()` based on the caller's supplied details - name, reason and whether the call is audio-only 149 | - The caller is initial marked as not ready. 150 | - Then, it tries to assign the caller to an agent by calling `assignCaller(caller)`. 151 | - Then, it creates a new OpenTok session and token for this caller and sends out a response with the caller status. 152 | 2. The caller screen on the frontend then uses this information to connect to the given OpenTok session and starts publishing the caller's stream. 153 | 3. The server listens to OpenTok session monitoring callbacks in the HTTP endpoint `POST /ot_callback`. 154 | - When a caller connects to the session, OpenTok posts data with caller's connection information to this endpoint. The endpoint calls the `handleConnectionCreated()` handler to match the connection data with the caller ID and marks the caller as ready. 155 | - Similarly, when a caller disconnects from OpenTok, OpenTok posts the caller's connection information. Then, the endpoint cleans up the caller info by calling `removeCaller(callerID)`. 156 | 4. When an agent joins, the agent interface calls the HTTP endpoint at `POST /agent`. 157 | - This creates a new instance of the `Agent` constructor. 158 | - Then it attempts to assign first 3 pending callers by calling `agent.assignPending(3)`. 159 | - Then it returns the agent ID. 160 | 5. The agent screen then keeps on polling the list of callers assigned to the current agent by hitting the HTTP endpoint at `/agent/:id/callers`. 161 | - This attempts to assign the first caller in the `pendingQueue[]` by calling `agent.assignPending(1)` if the agent has less than 3 callers assigned at that point. 162 | - Then it returns list of currently assigned callers who are marked as ready (see step 3 above). This ensures that the agent screen only sees list of callers who are currently connected to OpenTok. 163 | 6. When a caller needs to be removed, then frontend calls the HTTP endpoint at `GET /call/:id/delete`. This calls `removeCaller(callerID)` internally. The frontend can call this HTTP endpoint when any of these events happen: 164 | - Caller ends call by clicking "End call" button in the caller's screen 165 | - Agent ends current call by clicking "End call" button in the agent's screen 166 | - Caller closes their browser window when call is ongoing 167 | 7. When agent exits, either by closing their browser window or by clicking the "Exit" button, then existing callers assigned to the agent are moved to `pendingQueue[]`. That way, other agents can pick up those callers. 168 | 169 | ## Development and Contributing 170 | 171 | Interested in contributing? We :heart: pull requests! See the 172 | [Contribution](CONTRIBUTING.md) guidelines. 173 | 174 | ## Getting Help 175 | 176 | We love to hear from you so if you have questions, comments or find a bug in the project, let us know! You can either: 177 | 178 | - Open an issue on this repository 179 | - See for support options 180 | - Tweet at us! We're [@VonageDev](https://twitter.com/VonageDev) on Twitter 181 | - Or [join the Vonage Developer Community Slack](https://developer.nexmo.com/community/slack) 182 | 183 | ## Further Reading 184 | 185 | - Check out the Developer Documentation at 186 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | // Server script for the call center demo 2 | 3 | const express = require('express') 4 | const OpenTok = require('opentok') 5 | const compression = require('compression') 6 | const bodyParser = require('body-parser') 7 | 8 | // Get configurations 9 | const PORT = process.env.PORT || 8080 10 | 11 | // Set OPENTOK_API_KEY from environment variable 12 | // Exit with error if the environment variable is not specified. 13 | const OPENTOK_API_KEY = process.env.OPENTOK_API_KEY 14 | if (!OPENTOK_API_KEY) { 15 | throw new Error('Provide OPENTOK_API_KEY environment variable') 16 | } 17 | 18 | // Set OPENTOK_API_SECRET from environment variable 19 | // Exit with error if the environment variable is not specified. 20 | const OPENTOK_API_SECRET = process.env.OPENTOK_API_SECRET 21 | if (!OPENTOK_API_SECRET) { 22 | throw new Error('Provide OPENTOK_API_SECRET environment variable') 23 | } 24 | 25 | // --- In-memory data store --- 26 | // 27 | // An actual application will use databases to store these information. This demo does not go into those complexities by 28 | // using simple in-memory data structures. This means, you will lose caller and agent information on server restart. 29 | 30 | // List of current `Caller` indexed by caller ID 31 | let callers = new Map() 32 | // Sequence used to create incremental IDs for callers 33 | let callerSeq = 0 34 | // List of current `Agent` indexed by agent ID 35 | let agents = new Map() 36 | // Sequence used to create incremental IDs for agents 37 | let agentSeq = 0 38 | // List of callers pending to be assigned to any agent 39 | let pendingQueue = [] 40 | 41 | // --- OpenTok setup --- 42 | 43 | // Create instance of `OpenTok` that we will reuse for the rest of the application. 44 | const OT = new OpenTok(OPENTOK_API_KEY, OPENTOK_API_SECRET) 45 | 46 | /** 47 | * Create a new OpenTok session with the given `mediaMode`. 48 | * 49 | * @param {string} [mediaMode=routed] - The mediaMode for the session. 50 | * @returns {Promise} Returns a `Promise` that resolves to a new OpenTok session object. 51 | * @see https://tokbox.com/developer/guides/create-session/node/ 52 | */ 53 | function createSession (mediaMode = 'routed') { 54 | return new Promise(function (resolve, reject) { 55 | OT.createSession({ mediaMode: mediaMode }, (err, session) => { 56 | if (err) { 57 | console.log('Error creating OpenTok session', err) 58 | return reject(err) 59 | } 60 | return resolve(session) 61 | }) 62 | }) 63 | } 64 | 65 | /** 66 | * Create a new token to join an existing OpenTok session with the role set to `publisher`. 67 | * 68 | * @see https://tokbox.com/developer/guides/create-token/node/ 69 | * @param {string} sessionId - ID the OpenTok session to create token for 70 | * @param {string} userId - An internal user ID that is added to the serialized token data. 71 | * @param {string} [userType=caller] - An internal user type which is set to 'caller' by default. 72 | * @returns {Promise} Returns a `Promise` that resolves to a token. 73 | */ 74 | function createToken (sessionId, userId, userType = 'caller') { 75 | try { 76 | const token = OT.generateToken(sessionId, { 77 | role: 'publisher', 78 | data: JSON.stringify({ userId: userId, userType: userType }), 79 | expireTime: Math.round((Date.now() / 1000) + (60 * 60)) // 1 hour from now() 80 | }) 81 | return Promise.resolve(token) 82 | } catch (e) { 83 | return Promise.reject(e) 84 | } 85 | } 86 | 87 | /** 88 | * Caller holds metadata and provides actions for a single caller. Each new instance of this constructor is created for 89 | * every new caller. 90 | * 91 | * @constructor 92 | * @param {string} sessionId - OpenTok session ID for this caller 93 | * @param {string} token - OpenTok token used by the caller to join the session 94 | */ 95 | function Caller (sessionId, token) { 96 | // Session ID for this caller 97 | this.sessionId = sessionId 98 | // Token for the caller to join the session 99 | this.token = token 100 | // Auto-generated caller ID used to identify this caller 101 | this.callerId = (++callerSeq).toString() 102 | // Whether the caller is on hold 103 | this.onHold = false 104 | // Whether agent has connected to this caller 105 | this.agentConnected = false 106 | // Time when the caller joined 107 | this.connectedSince = new Date() 108 | // Time when the call started 109 | this.onCallSince = null 110 | // Whether caller is ready. This is set to true only after caller has successfully connected to OpenTok. 111 | this.ready = false 112 | // Name for this caller. 113 | this.callerName = null 114 | // Reason for call 115 | this.callerReason = null 116 | // Whether the caller has audio or video. Values can be either 'audioOnly` or `audioVideo`. 117 | this.audioVideo = null 118 | // `Agent` assigned to this caller. `null` means no agent assigned. 119 | this.assignedAgent = null 120 | } 121 | 122 | /** 123 | * Provides a subset of data used to send status of this caller. 124 | * 125 | * @returns {object} 126 | */ 127 | Caller.prototype.status = function () { 128 | return { 129 | callerId: this.callerId, 130 | onHold: this.onHold, 131 | connectedSince: this.connectedSince, 132 | onCallSince: this.onCallSince, 133 | agentConnected: this.agentConnected, 134 | callerName: this.callerName, 135 | callerReason: this.callerReason, 136 | audioVideo: this.audioVideo 137 | } 138 | } 139 | 140 | /** 141 | * Marks agent has started call and sends out a `agentConnected` signal with caller status. 142 | * 143 | * @returns {Promise} - Returns a `Promise` that fails if the signal failed to be sent. 144 | */ 145 | Caller.prototype.startCall = function () { 146 | return new Promise((resolve, reject) => { 147 | this.onCallSince = new Date() 148 | this.agentConnected = true 149 | OT.signal(this.sessionId, null, { type: 'agentConnected', data: JSON.stringify(this.status()) }, function (err) { 150 | if (err) { 151 | return reject(err) 152 | } 153 | return resolve() 154 | }) 155 | }) 156 | } 157 | 158 | /** 159 | * Marks the current call to be on hold and sends out a `hold` signal with caller status. 160 | * 161 | * @returns {Promise} - Returns a `Promise` that fails if the signal failed to be sent. 162 | */ 163 | Caller.prototype.hold = function () { 164 | return new Promise((resolve, reject) => { 165 | this.onHold = true 166 | this.agentConnected = false 167 | OT.signal(this.sessionId, null, { type: 'hold', data: JSON.stringify(this.status()) }, function (err) { 168 | if (err) { 169 | return reject(err) 170 | } 171 | return resolve() 172 | }) 173 | }) 174 | } 175 | 176 | /** 177 | * Marks the current call has been removed from hold and sends out an `unhold` signal with caller status. 178 | * 179 | * @returns {Promise} - Returns a `Promise` that fails if the signal failed to be sent. 180 | */ 181 | Caller.prototype.unhold = function () { 182 | return new Promise((resolve, reject) => { 183 | this.onHold = false 184 | this.agentConnected = true 185 | OT.signal(this.sessionId, null, { type: 'unhold', data: JSON.stringify(this.status()) }, function (err) { 186 | if (err) { 187 | return reject(err) 188 | } 189 | return resolve() 190 | }) 191 | }) 192 | } 193 | 194 | /** 195 | * Agent holds the metadata for a single agent. 196 | * 197 | * @constructor 198 | * @param {string} [name='N/a'] - Optional name for the agent 199 | */ 200 | function Agent (name = 'N/a') { 201 | // Auto-generated ID for the agent 202 | this.agentid = (++agentSeq).toString() 203 | // Agent's name 204 | this.name = name.trim() 205 | // List of callers assigned to this agent 206 | this.currentCallers = new Map() 207 | console.log('New Agent connected', this.agentid) 208 | } 209 | 210 | /** 211 | * Assigns given number of pending callers to this Agent. This looks up `pendingQueue` and assigns based on FIFO 212 | * (first-in-first-out). 213 | * 214 | * @param {number} [limit=1] - Number of callers to assign 215 | */ 216 | Agent.prototype.assignPending = function (limit = 1) { 217 | let c = pendingQueue.splice(0, limit) 218 | for (const i in c) { 219 | this.assignCaller(c[i]) 220 | } 221 | } 222 | 223 | /** 224 | * Assign given caller to current agent. 225 | * 226 | * @param {Caller} c - Instance of `Caller` to assign to. 227 | */ 228 | Agent.prototype.assignCaller = function (c) { 229 | c.assignedAgent = this.agentid 230 | this.currentCallers.set(c.callerId, c) 231 | } 232 | 233 | /** 234 | * Remove a caller from agent's list of current callers and assign one pending caller to this agent in its place. This 235 | * method is called when a caller has left or agent has ended a call. 236 | * 237 | * @param {string} callerid - ID of the caller 238 | */ 239 | Agent.prototype.removeCaller = function (callerid) { 240 | this.currentCallers.delete(callerid) 241 | this.assignPending(1) 242 | } 243 | 244 | /** 245 | * Mark the current agent as disconnected and move agent's current callers to pending callers' queue. Also, this removes 246 | * this agent from the list of available agents. 247 | */ 248 | Agent.prototype.disconnect = function () { 249 | for (const c of this.currentCallers.values()) { 250 | c.agentConnected = null 251 | pendingQueue.push(c) 252 | } 253 | console.log('Agent disconnected', this.agentid) 254 | agents.delete(this.agentid) 255 | } 256 | 257 | /** 258 | * Get status of callers who are marked as ready and currently assigned to this agent. 259 | * 260 | * @returns {Array} - `Caller.status()` for each active caller. 261 | */ 262 | Agent.prototype.activeCallers = function () { 263 | return Array.from(this.currentCallers.values()).filter(c => c.ready).map(c => c.status()) 264 | } 265 | 266 | /** 267 | * Get number of callers assigned to this agent 268 | * 269 | * @returns {number} 270 | */ 271 | Agent.prototype.callerCount = function () { 272 | return this.currentCallers.size 273 | } 274 | 275 | // --- Caller assignment functions --- 276 | 277 | /** 278 | * Sort current `agents` list in ascending order of the number of caller each `Agent` has. 279 | * 280 | * @returns {Agent[]} - Array of `Agent` 281 | */ 282 | function sortAgentsByCallerCount () { 283 | return Array.from(agents.values()).sort((a, b) => a.callerCount() - b.callerCount()) 284 | } 285 | 286 | /** 287 | * Assign a given caller to an available agent. It picks the agent with least number of callers. 288 | * 289 | * @param {Caller} caller - Caller to be assigned 290 | * @return {Promise} - Resolves to assigned agent's ID or `null` if no agent is available. 291 | */ 292 | async function assignCaller (caller) { 293 | if (agents.size) { 294 | let agent = sortAgentsByCallerCount()[0] 295 | agent.assignCaller(caller) 296 | caller.assignedAgent = agent.agentid 297 | return agent.agentid 298 | } 299 | pendingQueue.push(caller) 300 | return null 301 | } 302 | 303 | /** 304 | * Remove a given caller. This function cleans up a caller's presence from assigned agent's list as well as from pending 305 | * callers' queue if the caller is not currently assigned to any agent. 306 | * 307 | * @param {string} callerid - Caller's ID 308 | */ 309 | async function removeCaller (callerid) { 310 | let c = callers.get(callerid) 311 | if (c) { 312 | if (agents.has(c.assignedAgent)) { 313 | let a = agents.get(c.assignedAgent) 314 | a.removeCaller(callerid) 315 | } else { 316 | // Attempt removing caller from pending queue if no agent was assigned 317 | for (const c in pendingQueue) { 318 | if (pendingQueue[c].callerid === callerid) { 319 | pendingQueue.splice(c, 1) 320 | } 321 | } 322 | } 323 | callers.delete(callerid) 324 | } 325 | } 326 | 327 | // --- Server-side monitoring callbacks --- 328 | 329 | /** 330 | * Handle what happens when OpenTok sends callback that a new connection was created. If the connection is identified as 331 | * a caller, this function marks that caller as `ready`. 332 | * 333 | * @param {Object} data - Data posted from OpenTok 334 | */ 335 | async function handleConnectionCreated (data) { 336 | let conndata = JSON.parse(data.connection.data) 337 | if (!conndata.userId && !conndata.userType) { 338 | return 339 | } 340 | if (conndata.userType === 'caller') { 341 | let c = callers.get(conndata.userId) 342 | if (!c) { 343 | return 344 | } 345 | c.ready = true 346 | console.log('Caller connected', conndata.userId) 347 | } 348 | } 349 | 350 | /** 351 | * Handle what happens when OpenTok sends callback that a connection was destroyed. If the connection is identified as 352 | * caller, this function cleans up that caller from the queues. 353 | * 354 | * @param {Object} data - Data posted from OpenTok 355 | */ 356 | async function handleConnectionDestroyed (data) { 357 | let conndata = JSON.parse(data.connection.data) 358 | if (!conndata.userId && !conndata.userType) { 359 | return 360 | } 361 | if (conndata.userType === 'caller') { 362 | removeCaller(conndata.userId) 363 | console.log('Caller disconnected', conndata.userId) 364 | } 365 | } 366 | 367 | // --- Bootstrap Express Application --- 368 | 369 | // Create expressJS app instance 370 | const app = express() 371 | app.use(compression()) 372 | app.use(bodyParser.json()) 373 | app.use(bodyParser.urlencoded({ extended: true })) 374 | 375 | // Mount the `./public` dir to web-root as static. 376 | app.use('/', express.static('./public')) 377 | 378 | // --- REST endpoints --- 379 | 380 | /** 381 | * POST /dial 382 | * 383 | * Called by callers when they join. This creates a new caller and attempts to assign the caller to an available agent. 384 | */ 385 | app.post('/dial', (req, res, next) => { 386 | const c = new Caller() 387 | c.callerName = req.body.callerName 388 | c.callerReason = req.body.callerReason 389 | c.audioVideo = req.body.audioVideo 390 | assignCaller(c) 391 | .then(i => { 392 | c.assignedAgent = i 393 | return createSession() 394 | }) 395 | .then(session => { 396 | c.sessionId = session.sessionId 397 | return createToken(session.sessionId, c.callerId, 'caller') 398 | }) 399 | .then(token => { 400 | c.token = token 401 | callers.set(c.callerId, c) 402 | return res.status(200).json({ 403 | callerId: c.callerId, 404 | apiKey: OPENTOK_API_KEY, 405 | caller: c 406 | }) 407 | }) 408 | .catch(next) 409 | }) 410 | 411 | /** 412 | * GET /call/:id/join 413 | * 414 | * Used by agents to mark that they have joined a caller. 415 | */ 416 | app.get('/call/:id/join', (req, res, next) => { 417 | const c = callers.get(req.params.id) 418 | if (!c) { 419 | const e = new Error(`Caller ID ${req.params.id} not found`) 420 | e.status = 404 421 | return next(e) 422 | } 423 | createToken(c.sessionId, 'Agent', 'agent') 424 | .then(token => { 425 | c.startCall() 426 | return res.status(200).json({ 427 | apiKey: OPENTOK_API_KEY, 428 | sessionId: c.sessionId, 429 | token: token, 430 | caller: c.status() 431 | }) 432 | }) 433 | .catch(next) 434 | }) 435 | 436 | /** 437 | * GET /call/:id/hold 438 | * 439 | * Used by agents to mark given caller on hold. 440 | */ 441 | app.get('/call/:id/hold', (req, res, next) => { 442 | const c = callers.get(req.params.id) 443 | if (!c) { 444 | const e = new Error(`Caller ID ${req.params.id} not found`) 445 | e.status = 404 446 | return next(e) 447 | } 448 | c.hold() 449 | return res.status(200).json({ 450 | caller: c.status() 451 | }) 452 | }) 453 | 454 | /** 455 | * GET /call/:id/unhold 456 | * 457 | * Used by agents to mark given caller is removed from hold. 458 | */ 459 | app.get('/call/:id/unhold', (req, res, next) => { 460 | const c = callers.get(req.params.id) 461 | if (!c) { 462 | const e = new Error(`Caller ID ${req.params.id} not found`) 463 | e.status = 404 464 | return next(e) 465 | } 466 | createToken(c.sessionId, 'Agent', 'agent') 467 | .then(token => { 468 | c.unhold() 469 | return res.status(200).json({ 470 | apiKey: OPENTOK_API_KEY, 471 | sessionId: c.sessionId, 472 | token: token, 473 | caller: c.status() 474 | }) 475 | }) 476 | .catch(next) 477 | }) 478 | 479 | /** 480 | * GET /call/:id/delete 481 | * 482 | * Used by agents to remove a caller 483 | */ 484 | app.get('/call/:id/delete', (req, res, next) => { 485 | const c = callers.get(req.params.id) 486 | OT.signal(c.sessionId, null, { type: 'endCall', data: 'end' }, function (err) { 487 | if (err) { 488 | console.log(err) 489 | } 490 | removeCaller(req.params.id) 491 | res.status(200).json({ 492 | deleted: req.params.id 493 | }) 494 | }) 495 | }) 496 | 497 | /** 498 | * GET /call/:id 499 | * 500 | * Get status of given caller if found 501 | */ 502 | app.get('/call/:id', (req, res, next) => { 503 | const c = callers.get(req.params.id) 504 | if (!c) { 505 | const e = new Error(`Caller ID ${req.params.id} not found`) 506 | e.status = 404 507 | return next(e) 508 | } 509 | res.status(200).json({ 510 | caller: c.status() 511 | }) 512 | }) 513 | 514 | /** 515 | * GET /agent/:id/callers 516 | * 517 | * Get callers currently assigned to given agent. If agent has less than 3 callers, this also attempts to assign the 518 | * first caller in the pending queuee to this agent. 519 | */ 520 | app.get('/agent/:id/callers', (req, res, next) => { 521 | const a = agents.get(req.params.id) 522 | if (!a) { 523 | const e = new Error(`Agent ID ${req.params.id} not found`) 524 | e.status = 404 525 | return next(e) 526 | } 527 | if (a.callerCount() < 3) { 528 | a.assignPending(1) 529 | } 530 | res.status(200).json({ callers: a.activeCallers() }) 531 | }) 532 | 533 | /** 534 | * GET /agent/:id/disconnect 535 | * 536 | * Remove agent from list of active agents. 537 | */ 538 | app.post('/agent/:id/disconnect', (req, res, next) => { 539 | const a = agents.get(req.params.id) 540 | if (!a) { 541 | const e = new Error(`Agent ID ${req.params.id} not found`) 542 | e.status = 404 543 | return next(e) 544 | } 545 | a.disconnect() 546 | res.status(200).send() 547 | }) 548 | 549 | /** 550 | * POST /agent 551 | * 552 | * Creates a new agent. 553 | */ 554 | app.post('/agent', (req, res, next) => { 555 | let a = new Agent(req.body.name || 'N/A') 556 | a.assignPending(3) 557 | agents.set(a.agentid, a) 558 | res.status(200).json({ 559 | agentid: a.agentid, 560 | name: a.name 561 | }) 562 | }) 563 | 564 | /** 565 | * Callback handler for OpenTok session monitoring. 566 | */ 567 | app.post('/ot_callback', (req, res) => { 568 | switch (req.body.event) { 569 | case 'connectionCreated': 570 | handleConnectionCreated(req.body) 571 | break 572 | case 'connectionDestroyed': 573 | handleConnectionDestroyed(req.body) 574 | break 575 | } 576 | res.status(200).send() 577 | }) 578 | 579 | // error handler 580 | app.use(function (err, req, res, next) { 581 | err.status = err.status || 500 582 | if (err.status === 500) { 583 | console.log('Error', err) 584 | } 585 | res.status(err.status).json({ 586 | message: err.message || 'Unable to perform request', 587 | status: err.status 588 | }) 589 | }) 590 | 591 | if (!process.env.SECURE || process.env.SECURE === '0') { 592 | // Bootstrap and start HTTP server for app 593 | app.listen(PORT, () => { 594 | console.log(`Server started on port ${PORT}`) 595 | }) 596 | } else { 597 | const https = require('https') 598 | const fs = require('fs') 599 | const tlsOpts = { 600 | key: fs.readFileSync('key.pem'), 601 | cert: fs.readFileSync('cert.pem') 602 | } 603 | https.createServer(tlsOpts, app).listen(PORT, () => { 604 | console.log(`Listening on secure port ${PORT}...`) 605 | }) 606 | } 607 | -------------------------------------------------------------------------------- /public/assets/js/uikit-icons.js: -------------------------------------------------------------------------------- 1 | /*! UIkit 3.0.0-beta.35 | http://www.getuikit.com | (c) 2014 - 2017 YOOtheme | MIT License */ 2 | 3 | (function (global, factory) { 4 | typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : 5 | typeof define === 'function' && define.amd ? define('uikiticons', factory) : 6 | (global.UIkitIcons = factory()); 7 | }(this, (function () { 'use strict'; 8 | 9 | var album = " "; 10 | var ban = " "; 11 | var behance = " "; 12 | var bell = " "; 13 | var bold = " "; 14 | var bolt = " "; 15 | var bookmark = " "; 16 | var calendar = " "; 17 | var camera = " "; 18 | var cart = " "; 19 | var check = " "; 20 | var clock = " "; 21 | var close = " "; 22 | var code = " "; 23 | var cog = " "; 24 | var comment = " "; 25 | var commenting = " "; 26 | var comments = " "; 27 | var copy = " "; 28 | var database = " "; 29 | var desktop = " "; 30 | var download = " "; 31 | var dribbble = " "; 32 | var expand = " "; 33 | var facebook = " "; 34 | var file = " "; 35 | var flickr = " "; 36 | var folder = " "; 37 | var forward = " "; 38 | var foursquare = " "; 39 | var future = " "; 40 | var github = " "; 41 | var gitter = " "; 42 | var google = " "; 43 | var grid = " "; 44 | var happy = " "; 45 | var hashtag = " "; 46 | var heart = " "; 47 | var history = " "; 48 | var home = " "; 49 | var image = " "; 50 | var info = " "; 51 | var instagram = " "; 52 | var italic = " "; 53 | var joomla = " "; 54 | var laptop = " "; 55 | var lifesaver = " "; 56 | var link = " "; 57 | var linkedin = " "; 58 | var list = " "; 59 | var location = " "; 60 | var lock = " "; 61 | var mail = " "; 62 | var menu = " "; 63 | var minus = " "; 64 | var more = " "; 65 | var move = " "; 66 | var nut = " "; 67 | var pagekit = " "; 68 | var pencil = " "; 69 | var phone = " "; 70 | var pinterest = " "; 71 | var play = " "; 72 | var plus = " "; 73 | var pull = " "; 74 | var push = " "; 75 | var question = " "; 76 | var receiver = " "; 77 | var refresh = " "; 78 | var reply = " "; 79 | var rss = " "; 80 | var search = " "; 81 | var server = " "; 82 | var settings = " "; 83 | var shrink = " "; 84 | var social = " "; 85 | var soundcloud = " "; 86 | var star = " "; 87 | var strikethrough = " "; 88 | var table = " "; 89 | var tablet = " "; 90 | var tag = " "; 91 | var thumbnails = " "; 92 | var trash = " "; 93 | var tripadvisor = " "; 94 | var tumblr = " "; 95 | var tv = " "; 96 | var twitter = " "; 97 | var uikit = " "; 98 | var unlock = " "; 99 | var upload = " "; 100 | var user = " "; 101 | var users = " "; 102 | var vimeo = " "; 103 | var warning = " "; 104 | var whatsapp = " "; 105 | var wordpress = " "; 106 | var world = " "; 107 | var xing = " "; 108 | var yelp = " "; 109 | var youtube = " "; 110 | var Icons = { 111 | album: album, 112 | ban: ban, 113 | behance: behance, 114 | bell: bell, 115 | bold: bold, 116 | bolt: bolt, 117 | bookmark: bookmark, 118 | calendar: calendar, 119 | camera: camera, 120 | cart: cart, 121 | check: check, 122 | clock: clock, 123 | close: close, 124 | code: code, 125 | cog: cog, 126 | comment: comment, 127 | commenting: commenting, 128 | comments: comments, 129 | copy: copy, 130 | database: database, 131 | desktop: desktop, 132 | download: download, 133 | dribbble: dribbble, 134 | expand: expand, 135 | facebook: facebook, 136 | file: file, 137 | flickr: flickr, 138 | folder: folder, 139 | forward: forward, 140 | foursquare: foursquare, 141 | future: future, 142 | github: github, 143 | gitter: gitter, 144 | google: google, 145 | grid: grid, 146 | happy: happy, 147 | hashtag: hashtag, 148 | heart: heart, 149 | history: history, 150 | home: home, 151 | image: image, 152 | info: info, 153 | instagram: instagram, 154 | italic: italic, 155 | joomla: joomla, 156 | laptop: laptop, 157 | lifesaver: lifesaver, 158 | link: link, 159 | linkedin: linkedin, 160 | list: list, 161 | location: location, 162 | lock: lock, 163 | mail: mail, 164 | menu: menu, 165 | minus: minus, 166 | more: more, 167 | move: move, 168 | nut: nut, 169 | pagekit: pagekit, 170 | pencil: pencil, 171 | phone: phone, 172 | pinterest: pinterest, 173 | play: play, 174 | plus: plus, 175 | pull: pull, 176 | push: push, 177 | question: question, 178 | receiver: receiver, 179 | refresh: refresh, 180 | reply: reply, 181 | rss: rss, 182 | search: search, 183 | server: server, 184 | settings: settings, 185 | shrink: shrink, 186 | social: social, 187 | soundcloud: soundcloud, 188 | star: star, 189 | strikethrough: strikethrough, 190 | table: table, 191 | tablet: tablet, 192 | tag: tag, 193 | thumbnails: thumbnails, 194 | trash: trash, 195 | tripadvisor: tripadvisor, 196 | tumblr: tumblr, 197 | tv: tv, 198 | twitter: twitter, 199 | uikit: uikit, 200 | unlock: unlock, 201 | upload: upload, 202 | user: user, 203 | users: users, 204 | vimeo: vimeo, 205 | warning: warning, 206 | whatsapp: whatsapp, 207 | wordpress: wordpress, 208 | world: world, 209 | xing: xing, 210 | yelp: yelp, 211 | youtube: youtube, 212 | "500px": " ", 213 | "arrow-down": " ", 214 | "arrow-left": " ", 215 | "arrow-right": " ", 216 | "arrow-up": " ", 217 | "chevron-down": " ", 218 | "chevron-left": " ", 219 | "chevron-right": " ", 220 | "chevron-up": " ", 221 | "cloud-download": " ", 222 | "cloud-upload": " ", 223 | "credit-card": " ", 224 | "file-edit": " ", 225 | "git-branch": " ", 226 | "git-fork": " ", 227 | "github-alt": " ", 228 | "google-plus": " ", 229 | "minus-circle": " ", 230 | "more-vertical": " ", 231 | "paint-bucket": " ", 232 | "phone-landscape": " ", 233 | "play-circle": " ", 234 | "plus-circle": " ", 235 | "quote-right": " ", 236 | "sign-in": " ", 237 | "sign-out": " ", 238 | "tablet-landscape": " ", 239 | "triangle-down": " ", 240 | "triangle-left": " ", 241 | "triangle-right": " ", 242 | "triangle-up": " ", 243 | "video-camera": " " 244 | }; 245 | 246 | function plugin(UIkit) { 247 | 248 | if (plugin.installed) { 249 | return; 250 | } 251 | 252 | UIkit.icon.add(Icons); 253 | 254 | } 255 | 256 | if (typeof window !== 'undefined' && window.UIkit) { 257 | window.UIkit.use(plugin); 258 | } 259 | 260 | return plugin; 261 | 262 | }))); 263 | --------------------------------------------------------------------------------