mirror of
https://github.com/Luzifer/browserphone.git
synced 2024-11-08 13:50:05 +00:00
initial version
This commit is contained in:
commit
d734ea9f3a
11 changed files with 7336 additions and 0 deletions
1
.dockerignore
Symbolic link
1
.dockerignore
Symbolic link
|
@ -0,0 +1 @@
|
||||||
|
.gitignore
|
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
dist/app.js
|
||||||
|
node_modules
|
31
Dockerfile
Normal file
31
Dockerfile
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
FROM node:alpine as builder
|
||||||
|
|
||||||
|
COPY . /src
|
||||||
|
WORKDIR /src
|
||||||
|
|
||||||
|
RUN set -ex \
|
||||||
|
&& npm ci \
|
||||||
|
&& npm run build
|
||||||
|
|
||||||
|
RUN set -ex \
|
||||||
|
&& apk add curl \
|
||||||
|
&& curl -sSfLo /tmp/dumb-init "https://github.com/Yelp/dumb-init/releases/download/v1.2.2/dumb-init_1.2.2_amd64" \
|
||||||
|
&& chmod +x /tmp/dumb-init \
|
||||||
|
&& curl -sSfL "https://github.com/Luzifer/korvike/releases/download/v0.5.0/korvike_linux_amd64.tar.gz" | tar -xz -C /tmp
|
||||||
|
|
||||||
|
|
||||||
|
FROM python:3-alpine
|
||||||
|
|
||||||
|
LABEL maintainer Knut Ahlers <knut@ahlers.me>
|
||||||
|
|
||||||
|
COPY docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh
|
||||||
|
COPY --from=builder /tmp/dumb-init /usr/local/bin/dumb-init
|
||||||
|
COPY --from=builder /tmp/korvike_linux_amd64 /usr/local/bin/korvike
|
||||||
|
COPY --from=builder /src/dist /usr/local/share/browserphone/dist
|
||||||
|
|
||||||
|
RUN set -ex \
|
||||||
|
&& apk --no-cache add bash
|
||||||
|
|
||||||
|
EXPOSE 3000/tcp
|
||||||
|
WORKDIR "/usr/local/share/browserphone"
|
||||||
|
CMD ["/usr/local/bin/docker-entrypoint.sh"]
|
22
dist/index.html
vendored
Normal file
22
dist/index.html
vendored
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<!-- Required meta tags -->
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.8.2/css/all.css"
|
||||||
|
integrity="sha384-oS3vJWv+0UjzBfQzYUhtDYW+Pj2yciDJxpsK1OYPAYjqT085Qq/1cq5FLXAZQ7Ay" crossorigin="anonymous">
|
||||||
|
|
||||||
|
<title>BrowserPhone</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script>
|
||||||
|
window.capabilityTokenURL = '{{ env `CAP_TOKEN_URL` }}'
|
||||||
|
</script>
|
||||||
|
<script src="app.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
7
docker-entrypoint.sh
Executable file
7
docker-entrypoint.sh
Executable file
|
@ -0,0 +1,7 @@
|
||||||
|
#!/usr/local/bin/dumb-init /bin/bash
|
||||||
|
set -euxo pipefail
|
||||||
|
|
||||||
|
ln dist/app.js app.js
|
||||||
|
korvike -i dist/index.html -o index.html
|
||||||
|
|
||||||
|
exec python -m http.server 3000
|
7009
package-lock.json
generated
Normal file
7009
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
30
package.json
Normal file
30
package.json
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
{
|
||||||
|
"devDependencies": {
|
||||||
|
"axios": "^0.18.0",
|
||||||
|
"babel-core": "^6.26.3",
|
||||||
|
"babel-loader": "^7.1.5",
|
||||||
|
"babel-preset-env": "^1.7.0",
|
||||||
|
"bootstrap": "^4.3.1",
|
||||||
|
"bootstrap-vue": "^2.0.0-rc.19",
|
||||||
|
"bootswatch": "^4.3.1",
|
||||||
|
"css-loader": "^1.0.0",
|
||||||
|
"node-sass": "^4.9.2",
|
||||||
|
"popper.js": "^1.15.0",
|
||||||
|
"sass-loader": "^7.0.3",
|
||||||
|
"style-loader": "^0.21.0",
|
||||||
|
"twilio-client": "^1.7.3",
|
||||||
|
"vue": "^2.6.10",
|
||||||
|
"vue-i18n": "^8.11.2",
|
||||||
|
"vue-loader": "^15.7.0",
|
||||||
|
"vue-template-compiler": "^2.6.10",
|
||||||
|
"webpack": "^4.16.2",
|
||||||
|
"webpack-cli": "^3.1.0",
|
||||||
|
"webpack-dev-middleware": "^1.4.0",
|
||||||
|
"webpack-hot-middleware": "^2.9.1"
|
||||||
|
},
|
||||||
|
"name": "browserphone",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"build": "webpack -p"
|
||||||
|
}
|
||||||
|
}
|
154
src/app.vue
Normal file
154
src/app.vue
Normal file
|
@ -0,0 +1,154 @@
|
||||||
|
<template>
|
||||||
|
<div id="app">
|
||||||
|
<b-navbar toggleable="lg" type="dark" variant="primary">
|
||||||
|
<b-navbar-brand href="#">BrowserPhone</b-navbar-brand>
|
||||||
|
|
||||||
|
<b-navbar-toggle target="nav-collapse"></b-navbar-toggle>
|
||||||
|
|
||||||
|
<b-collapse id="nav-collapse" is-nav>
|
||||||
|
<b-navbar-nav class="ml-auto">
|
||||||
|
<b-nav-text v-if="identity">Signed in as <strong>{{ identity }}</strong></b-nav-text>
|
||||||
|
</b-navbar-nav>
|
||||||
|
</b-collapse>
|
||||||
|
</b-navbar>
|
||||||
|
|
||||||
|
<b-container>
|
||||||
|
<b-row class="justify-content-center mt-5">
|
||||||
|
<b-col cols="12" md="6">
|
||||||
|
|
||||||
|
<b-card>
|
||||||
|
<b-form-input size="lg" slot="header" type="tel" class="my-2 number border-info" autofocus v-model="number"></b-form-input>
|
||||||
|
|
||||||
|
<b-row>
|
||||||
|
<b-col v-for="key in keys" cols="4" class="text-center mb-3" :key="key">
|
||||||
|
<b-btn size="lg" @click="keyDown(key)">{{ key }}</b-btn>
|
||||||
|
</b-col>
|
||||||
|
|
||||||
|
<b-col cols="4" class="text-center mb-2">
|
||||||
|
<b-btn size="lg" :variant="ongoingCall ? 'danger' : 'success'" @click="toggleCall">
|
||||||
|
<i :class="{ 'fas': true, 'fa-phone': !ongoingCall, 'fa-phone-slash': ongoingCall }"></i>
|
||||||
|
</b-btn>
|
||||||
|
</b-col>
|
||||||
|
</b-row>
|
||||||
|
|
||||||
|
<div slot="footer" v-if="state">
|
||||||
|
{{ state }}
|
||||||
|
</div>
|
||||||
|
</b-card>
|
||||||
|
|
||||||
|
</b-col>
|
||||||
|
</b-row>
|
||||||
|
</b-container>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import axios from 'axios'
|
||||||
|
import Twilio from 'twilio-client'
|
||||||
|
|
||||||
|
import config from './config.js'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'app',
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
keys() {
|
||||||
|
return [1, 2, 3, 4, 5, 6, 7, 8, 9, '+', 0]
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
identity: null,
|
||||||
|
number: '',
|
||||||
|
ongoingCall: false,
|
||||||
|
phone: {
|
||||||
|
conn: null,
|
||||||
|
device: null,
|
||||||
|
},
|
||||||
|
state: '',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
announceStatus(state) {
|
||||||
|
this.state = state
|
||||||
|
window.setTimeout(() => {
|
||||||
|
this.state = ''
|
||||||
|
}, 2000)
|
||||||
|
},
|
||||||
|
|
||||||
|
keyDown(key) {
|
||||||
|
this.number = `${this.number}${key}`
|
||||||
|
},
|
||||||
|
|
||||||
|
setupPhone() {
|
||||||
|
if (!this.phone.device) {
|
||||||
|
this.phone.device = new Twilio.Device()
|
||||||
|
this.phone.device.on('ready', () => this.announceStatus('Phone ready...'))
|
||||||
|
this.phone.device.on('error', err => console.error(err))
|
||||||
|
this.phone.device.on('connect', conn => {
|
||||||
|
this.phone.conn = conn
|
||||||
|
this.ongoingCall = true
|
||||||
|
this.announeState('Call connected')
|
||||||
|
})
|
||||||
|
this.phone.device.on('disconnect', () => {
|
||||||
|
this.phone.conn = null
|
||||||
|
this.ongoingCall = false
|
||||||
|
this.announeState('Call disconnected')
|
||||||
|
})
|
||||||
|
this.phone.device.on('offline', () => {
|
||||||
|
this.announceStatus('Phone disconnected, reconnecting...')
|
||||||
|
this.setupPhone()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const opts = { codecPreferences: ['opus', 'pcmu'], fakeLocalDTMF: true }
|
||||||
|
|
||||||
|
axios.get(config.capabilityTokenURL)
|
||||||
|
.then(resp => {
|
||||||
|
this.identity = resp.data.identity
|
||||||
|
this.phone.device.setup(resp.data.token, opts)
|
||||||
|
})
|
||||||
|
.catch(err => console.error(err))
|
||||||
|
},
|
||||||
|
|
||||||
|
toggleCall() {
|
||||||
|
if (this.ongoingCall) {
|
||||||
|
this.phone.device.disconnectAll()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// handle incoming
|
||||||
|
|
||||||
|
if (this.number) {
|
||||||
|
this.announceStatus(`Dialing ${this.number}...`)
|
||||||
|
this.phone.device.connect({
|
||||||
|
To: this.number,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
this.announceStatus('Phone loaded...')
|
||||||
|
this.setupPhone()
|
||||||
|
},
|
||||||
|
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.btn-lg {
|
||||||
|
font-size: 2rem;
|
||||||
|
height: 2em;
|
||||||
|
width: 2em;
|
||||||
|
}
|
||||||
|
input.form-control-lg {
|
||||||
|
font-size: 2rem;
|
||||||
|
}
|
||||||
|
.number {
|
||||||
|
background-color: transparent;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
</style>
|
3
src/config.js
Normal file
3
src/config.js
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
export default {
|
||||||
|
capabilityTokenURL: window.capabilityTokenURL,
|
||||||
|
}
|
17
src/main.js
Normal file
17
src/main.js
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
import Vue from 'vue'
|
||||||
|
|
||||||
|
import BootstrapVue from 'bootstrap-vue'
|
||||||
|
|
||||||
|
import 'bootstrap/dist/css/bootstrap.css'
|
||||||
|
import 'bootstrap-vue/dist/bootstrap-vue.css'
|
||||||
|
import 'bootswatch/dist/darkly/bootstrap.css'
|
||||||
|
|
||||||
|
import app from './app.vue'
|
||||||
|
|
||||||
|
Vue.use(BootstrapVue)
|
||||||
|
|
||||||
|
new Vue({
|
||||||
|
components: { app },
|
||||||
|
el: '#app',
|
||||||
|
render: c => c('app'),
|
||||||
|
})
|
60
webpack.config.js
Normal file
60
webpack.config.js
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
const path = require('path')
|
||||||
|
const webpack = require('webpack');
|
||||||
|
|
||||||
|
const VueLoaderPlugin = require('vue-loader/lib/plugin')
|
||||||
|
|
||||||
|
function resolve(dir) {
|
||||||
|
return path.join(__dirname, dir)
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
entry: './src/main.js',
|
||||||
|
output: {
|
||||||
|
filename: 'app.js',
|
||||||
|
path: resolve('./dist'),
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
new webpack.optimize.OccurrenceOrderPlugin(),
|
||||||
|
new webpack.DefinePlugin({
|
||||||
|
'process.env': {
|
||||||
|
'NODE_ENV': JSON.stringify('production')
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
new VueLoaderPlugin(),
|
||||||
|
],
|
||||||
|
optimization: {
|
||||||
|
minimize: true
|
||||||
|
},
|
||||||
|
module: {
|
||||||
|
rules: [
|
||||||
|
|
||||||
|
{
|
||||||
|
test: /\.(s?)css$/,
|
||||||
|
use: [
|
||||||
|
'style-loader',
|
||||||
|
'css-loader',
|
||||||
|
'sass-loader',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
test: /\.js$/,
|
||||||
|
exclude: /(node_modules|bower_components)/,
|
||||||
|
use: {
|
||||||
|
loader: 'babel-loader',
|
||||||
|
options: {
|
||||||
|
presets: [
|
||||||
|
['env', { "targets": { "browsers": [">0.25%", "not ie 11", "not op_mini all"] } }]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
test: /\.vue$/,
|
||||||
|
loader: 'vue-loader',
|
||||||
|
},
|
||||||
|
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue