Install typescript, upgrade server to TypeScript

This commit is contained in:
Jerko Steiner 2019-11-13 00:54:35 -03:00
parent 085fae4b22
commit 1eaca46a16
23 changed files with 2174 additions and 451 deletions

View File

@ -1,21 +0,0 @@
{
"parser": "babel-eslint",
"extends": ["standard", "standard-react"],
"rules": {
"max-len": [2, 80, 4],
"jsx-quotes": ["error", "prefer-double"],
"padded-blocks": 0,
"import/first": 0,
"no-return-assign": 0,
"indent": ["error", 2, { "MemberExpression": 0, "SwitchCase": 1, "flatTernaryExpressions": true }],
},
"globals": {
"expect": true,
"jest": true,
"jasmine": true,
"it": true,
"beforeEach": true,
"afterEach": true,
"describe": true
}
}

66
.eslintrc.yaml Normal file
View File

@ -0,0 +1,66 @@
extends:
- eslint:recommended
- plugin:react/recommended
- plugin:@typescript-eslint/eslint-recommended
- plugin:@typescript-eslint/recommended
plugins:
- import
settings:
react:
version: 'detect'
rules:
max-len:
- warn
- code: 80
ignorePattern: '^import .* from '
comma-dangle:
- warn
- arrays: always-multiline
objects: always-multiline
imports: always-multiline
exports: always-multiline
functions: always-multiline
semi:
- warn
- never
quotes:
- warn
- single
- allowTemplateLiterals: true
# interface-name-prefix:
'import/no-extraneous-dependencies': off
'@typescript-eslint/member-delimiter-style':
- warn
- multiline:
delimiter: none
singleline:
delimiter: comma
'@typescript-eslint/no-unused-vars':
- warn
- vars: all
args: none
ignoreRestSiblings: true
'@typescript-eslint/explicit-function-return-type': off
'@typescript-eslint/no-non-null-assertion': off
'@typescript-eslint/no-use-before-define': off
'@typescript-eslint/no-empty-interface': off
'@typescript-eslint/no-explicit-any':
- warn
- ignoreRestArgs: true
'@typescript-eslint/triple-slash-reference':
- warn
- path: always
overrides:
- files:
- '*.test.ts'
- '*.test.tsx'
rules:
'@typescript-eslint/no-explicit-any': off
- files:
- '*.js'
rules:
'@typescript-eslint/no-var-requires': off
env:
node: true
es6: true

16
jest.config.js Normal file
View File

@ -0,0 +1,16 @@
module.exports = {
roots: [
'<rootDir>/src'
],
transform: {
'^.+\\.tsx?$': 'ts-jest'
},
testRegex: '(/__tests__/.*|\\.(test|spec))\\.tsx?$',
moduleFileExtensions: [
'ts',
'tsx',
'js',
'jsx'
],
setupFiles: ['<rootDir>/jest.setup.js']
}

View File

@ -1 +0,0 @@
import '@babel/polyfill'

2074
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -3,27 +3,29 @@
"version": "2.0.12",
"description": "Group peer to peer video calls for anybody.",
"repository": "https://github.com/jeremija/peer-calls",
"main": "src/index.js",
"main": "lib/index.js",
"bin": {
"peercalls": "./src/index.js"
"peercalls": "./lib/index.js"
},
"scripts": {
"start": "node src/index.js",
"start:server": "nodemon src/index.js --ignore build/ --ignore src/client",
"start:watch": "chastifol [ npm run js:watch ] [ npm run css:watch ] [ npm run start:server ]",
"start": "node lib/index.js",
"start:server": "nodemon -ignore build/ --ignore lib/client lib/index.js",
"start:watch": "chastifol [ npm run ts:watch ] [ npm run js:watch ] [ npm run css:watch ] [ npm run start:server ]",
"watch": "",
"prepublishOnly": "npm run build",
"build": "npm run css && npm run js",
"test": "jest",
"test:coverage": "jest --coverage",
"test:watch": "jest --watch",
"js": "browserify -t babelify ./src/client/index.js -o ./build/index.js",
"js:watch": "watchify -d -v -t babelify ./src/client/index.js -o ./build/index.js",
"js": "browserify -t babelify ./lib/client/index.js -o ./build/index.js",
"js:watch": "watchify -d -v -t babelify ./lib/client/index.js -o ./build/index.js",
"css": "node-sass ./src/scss/style.scss -o ./build/",
"css:watch": "npm run css && node-sass --watch ./src/scss/style.scss -o ./build/",
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"ci": "npm run lint && npm run test:coverage && npm run build"
"ci": "npm run lint && npm run test:coverage && npm run build",
"ts:watch": "tsc --build . --watch --preserveWatchOutput",
"ts": "tsc --build ."
},
"babel": {
"presets": [
@ -78,36 +80,41 @@
"@babel/polyfill": "^7.4.4",
"@babel/preset-env": "^7.5.0",
"@babel/preset-react": "^7.0.0",
"@types/config": "0.0.36",
"@types/debug": "^4.1.5",
"@types/express": "^4.17.2",
"@types/jest": "^24.0.23",
"@types/node": "^12.12.7",
"@types/socket.io": "^2.1.4",
"@types/socket.io-client": "^1.4.32",
"@types/supertest": "^2.0.8",
"@types/underscore": "^1.9.3",
"@types/uuid": "^3.4.6",
"@typescript-eslint/eslint-plugin": "^2.7.0",
"@typescript-eslint/parser": "^2.7.0",
"babel-core": "^7.0.0-bridge.0",
"babel-eslint": "^10.0.1",
"babel-jest": "^24.8.0",
"babelify": "^10.0.0",
"chastifol": "^4.1.0",
"eslint": "^5.10.0",
"eslint": "^6.6.0",
"eslint-config-standard": "^12.0.0",
"eslint-config-standard-react": "^7.0.2",
"eslint-plugin-import": "^2.3.0",
"eslint-plugin-import": "^2.18.2",
"eslint-plugin-node": "^8.0.0",
"eslint-plugin-promise": "^4.0.1",
"eslint-plugin-react": "^7.0.1",
"eslint-plugin-react": "^7.16.0",
"eslint-plugin-standard": "^4.0.0",
"jest": "^24.9.0",
"jest-cli": "^24.8.0",
"node-sass": "^4.13.0",
"nodemon": "^1.18.8",
"redux-mock-store": "^1.2.3",
"supertest": "^3.0.0",
"ts-jest": "^24.1.0",
"ts-node": "^8.5.0",
"typescript": "^3.6.4",
"uglify-js": "^3.4.9",
"watchify": "^3.11.1"
},
"jest": {
"transform": {
"^.+\\.[t|j]sx?$": "<rootDir>/node_modules/babel-jest"
},
"setupFiles": [
"<rootDir>/jest.setup.js"
],
"modulePathIgnorePatterns": [
"<rootDir>/node_modules/"
]
}
}

View File

@ -4,8 +4,10 @@ if (!process.env.DEBUG) {
process.env.DEBUG = 'peercalls'
}
const app = require('./server/app.js')
const debug = require('debug')('peercalls')
import app from './server/app'
import _debug from 'debug'
const debug = _debug('peercalls')
const port = process.env.PORT || 3000
const server = app.listen(port, () => debug('Listening on: %s', port))

View File

@ -1,16 +1,19 @@
jest.mock('socket.io', () => {
// eslint-disable-next-line
const { EventEmitter } = require('events')
return jest.fn().mockReturnValue(new EventEmitter())
})
jest.mock('./socket.js')
jest.mock('./socket')
const app = require('./app.js')
const config = require('config')
const handleSocket = require('./socket.js')
const io = require('socket.io')()
const request = require('supertest')
import app from './app'
import { config } from './config'
import handleSocket from './socket'
import SocketIO from 'socket.io'
import request from 'supertest'
const BASE_URL = config.get('baseUrl')
const io = SocketIO()
const BASE_URL: string = config.get('baseUrl')
describe('server/app', () => {
@ -50,7 +53,7 @@ describe('server/app', () => {
it('calls handleSocket with socket', () => {
const socket = { hi: 'me socket' }
io.emit('connection', socket)
expect(handleSocket.mock.calls).toEqual([[ socket, io ]])
expect((handleSocket as jest.Mock).mock.calls).toEqual([[ socket, io ]])
})
})

View File

@ -1,20 +1,23 @@
#!/usr/bin/env node
'use strict'
const config = require('config')
const debug = require('debug')('peercalls')
const express = require('express')
const handleSocket = require('./socket.js')
const path = require('path')
const { createServer } = require('./server.js')
import { config } from './config'
import _debug from 'debug'
import express from 'express'
import handleSocket from './socket'
import path from 'path'
import { createServer } from './server'
import SocketIO from 'socket.io'
import call from './routes/call'
import index from './routes/index'
const BASE_URL = config.get('baseUrl')
const debug = _debug('peercalls')
const BASE_URL: string = config.get('baseUrl')
const SOCKET_URL = `${BASE_URL}/ws`
debug(`WebSocket URL: ${SOCKET_URL}`)
const app = express()
const server = createServer(config, app)
const io = require('socket.io')(server, { path: SOCKET_URL })
const io = SocketIO(server, { path: SOCKET_URL })
app.locals.version = require('../../package.json').version
app.locals.baseUrl = BASE_URL
@ -25,10 +28,10 @@ app.set('views', path.join(__dirname, '../views'))
const router = express.Router()
router.use('/res', express.static(path.join(__dirname, '../res')))
router.use('/static', express.static(path.join(__dirname, '../../build')))
router.use('/call', require('./routes/call.js'))
router.use('/', require('./routes/index.js'))
router.use('/call', call)
router.use('/', index)
app.use(BASE_URL, router)
io.on('connection', socket => handleSocket(socket, io))
module.exports = server
export default server

26
src/server/config.ts Normal file
View File

@ -0,0 +1,26 @@
import cfg, { IConfig } from 'config'
export type ICEServer = {
url: string
urls: string[] | string
auth: 'secret'
username: string
secret: string
} | {
url: string
urls: string[] | string
auth: undefined
username: string
credential: string
}
export interface Config {
baseUrl: string
iceServers: ICEServer[]
ssl: {
cert: string
key: string
}
}
export const config = cfg as IConfig & Config

View File

@ -1,23 +0,0 @@
#!/usr/bin/env node
'use strict'
const config = require('config')
const turn = require('../turn.js')
const router = require('express').Router()
const uuid = require('uuid')
const BASE_URL = config.get('baseUrl')
const cfgIceServers = config.get('iceServers')
router.get('/', (req, res) => {
res.redirect(`${BASE_URL}/call/${uuid.v4()}`)
})
router.get('/:callId', (req, res) => {
const iceServers = turn.processServers(cfgIceServers)
res.render('call', {
callId: encodeURIComponent(req.params.callId),
iceServers
})
})
module.exports = router

23
src/server/routes/call.ts Normal file
View File

@ -0,0 +1,23 @@
import { config, ICEServer } from '../config'
import * as turn from '../turn'
import { Router } from 'express'
import { v4 } from 'uuid'
const router = Router()
const BASE_URL: string = config.get('baseUrl')
const cfgIceServers = config.get('iceServers') as ICEServer[]
router.get('/', (req, res) => {
res.redirect(`${BASE_URL}/call/${v4()}`)
})
router.get('/:callId', (req, res) => {
const iceServers = turn.processServers(cfgIceServers)
res.render('call', {
callId: encodeURIComponent(req.params.callId),
iceServers,
})
})
export default router

View File

@ -1,9 +0,0 @@
#!/usr/bin/env node
'use strict'
const router = require('express').Router()
router.get('/', (req, res) => {
res.render('index')
})
module.exports = router

View File

@ -0,0 +1,9 @@
import { Router } from 'express'
const router = Router()
router.get('/', (req, res) => {
res.render('index')
})
export default router

View File

@ -1,17 +0,0 @@
const fs = require('fs')
const path = require('path')
const projectRoot = path.resolve(path.join(__dirname, '../..'))
const readFile = file => fs.readFileSync(path.resolve(projectRoot, file))
function createServer (config, app) {
if (config.ssl) {
const key = readFile(config.ssl.key)
const cert = readFile(config.ssl.cert)
return require('https').createServer({ key, cert }, app)
}
return require('http').createServer(app)
}
module.exports = { createServer }

View File

@ -1,11 +1,11 @@
const express = require('express')
const http = require('http')
const https = require('https')
const { createServer } = require('./server.js')
import express from 'express'
import http from 'http'
import https from 'https'
import { createServer } from './server'
describe('server', () => {
let app, config
let app: Express.Application, config: any
beforeEach(() => {
config = {}
app = express()
@ -15,7 +15,7 @@ describe('server', () => {
it('creates https server when config.ssl', () => {
config.ssl = {
cert: 'config/cert.example.pem',
key: 'config/cert.example.key'
key: 'config/cert.example.key',
}
const s = createServer(config, app)
expect(s).toEqual(jasmine.any(https.Server))

16
src/server/server.ts Normal file
View File

@ -0,0 +1,16 @@
import { readFileSync } from 'fs'
import { resolve, join } from 'path'
import { Config } from './config'
const projectRoot = resolve(join(__dirname, '../..'))
const readFile = (file: string) => readFileSync(resolve(projectRoot, file))
export function createServer (config: Config, app: Express.Application) {
if (config.ssl) {
const key = readFile(config.ssl.key)
const cert = readFile(config.ssl.cert)
return require('https').createServer({ key, cert }, app)
}
return require('http').createServer(app)
}

View File

@ -1,21 +1,33 @@
'use strict'
const EventEmitter = require('events').EventEmitter
const handleSocket = require('./socket.js')
import { EventEmitter } from 'events'
import handleSocket from './socket'
import { Socket, Server } from 'socket.io'
describe('server/socket', () => {
let socket, io, rooms
type SocketMock = Socket & {
id: string
room?: string
join: jest.Mock
leave: jest.Mock
emit: jest.Mock
}
let socket: SocketMock
let io: Server & {
in: jest.Mock<(room: string) => SocketMock>
to: jest.Mock<(room: string) => SocketMock>
}
let rooms: Record<string, {emit: any}>
beforeEach(() => {
socket = new EventEmitter()
socket = new EventEmitter() as SocketMock
socket.id = 'socket0'
socket.join = jest.fn()
socket.leave = jest.fn()
rooms = {}
io = {}
io = {} as any
io.in = io.to = jest.fn().mockImplementation(room => {
return (rooms[room] = rooms[room] || {
emit: jest.fn()
emit: jest.fn(),
})
})
@ -23,21 +35,21 @@ describe('server/socket', () => {
adapter: {
rooms: {
room1: {
'socket0': true
},
socket0: true,
} as any,
room2: {
'socket0': true
},
socket0: true,
} as any,
room3: {
sockets: {
'socket0': true,
'socket1': true,
'socket2': true
}
}
}
}
}
'socket2': true,
},
} as any,
},
} as any,
} as any
socket.leave = jest.fn()
socket.join = jest.fn()
@ -52,16 +64,16 @@ describe('server/socket', () => {
describe('signal', () => {
it('should broadcast signal to specific user', () => {
let signal = { type: 'signal' }
const signal = { type: 'signal' }
socket.emit('signal', { userId: 'a', signal })
expect(io.to.mock.calls).toEqual([[ 'a' ]])
expect(io.to('a').emit.mock.calls).toEqual([[
expect((io.to('a').emit as jest.Mock).mock.calls).toEqual([[
'signal', {
userId: 'socket0',
signal
}
signal,
},
]])
})
})
@ -84,19 +96,19 @@ describe('server/socket', () => {
socket.emit('ready', 'room3')
expect(io.to.mock.calls).toEqual([[ 'room3' ]])
expect(io.to('room3').emit.mock.calls).toEqual([
expect((io.to('room3').emit as jest.Mock).mock.calls).toEqual([
[
'users', {
initiator: 'socket0',
users: [{
id: 'socket0'
id: 'socket0',
}, {
id: 'socket1'
id: 'socket1',
}, {
id: 'socket2'
}]
}
]
id: 'socket2',
}],
},
],
])
})
})

View File

@ -1,13 +1,18 @@
'use strict'
const debug = require('debug')('peer-calls:socket')
const _ = require('underscore')
import _debug from 'debug'
import _ from 'underscore'
import { Socket, Server } from 'socket.io'
module.exports = function (socket, io) {
const debug = _debug('peercalls:socket')
type SocketWithRoom = Socket & { room?: string }
export default function handleSocket(socket: SocketWithRoom, io: Server) {
socket.on('signal', payload => {
// debug('signal: %s, payload: %o', socket.id, payload)
io.to(payload.userId).emit('signal', {
userId: socket.id,
signal: payload.signal
signal: payload.signal,
})
})
@ -18,17 +23,17 @@ module.exports = function (socket, io) {
socket.join(roomName)
socket.room = roomName
let users = getUsers(roomName)
const users = getUsers(roomName)
debug('ready: %s, room: %s, users: %o', socket.id, roomName, users)
io.to(roomName).emit('users', {
initiator: socket.id,
users
users,
})
})
function getUsers (roomName) {
function getUsers (roomName: string) {
return _.map(io.sockets.adapter.rooms[roomName].sockets, (_, id) => {
return { id }
})

View File

@ -1,35 +0,0 @@
'use strict'
const crypto = require('crypto')
function getCredentials (name, secret) {
// this credential would be valid for the next 24 hours
const timestamp = parseInt(Date.now() / 1000, 10) + 24 * 3600
const username = [timestamp, name].join(':')
const hmac = crypto.createHmac('sha1', secret)
hmac.setEncoding('base64')
hmac.write(username)
hmac.end()
const credential = hmac.read()
return { username, credential }
}
function processServers (iceServers) {
return iceServers.map(server => {
switch (server.auth) {
case undefined:
return server
case 'secret':
const cred = getCredentials(server.username, server.secret)
return {
url: server.url,
urls: server.urls,
username: cred.username,
credential: cred.credential
}
default:
throw new Error('Authentication type not implemented: ' + server.auth)
}
})
}
module.exports = { getCredentials, processServers }

View File

@ -1,4 +1,5 @@
const turn = require('./turn.js')
import * as turn from './turn'
import { ICEServer } from './config'
describe('server/turn', () => {
describe('getCredentials', () => {
@ -11,17 +12,18 @@ describe('server/turn', () => {
})
describe('processServers', () => {
const servers = [{
const servers: ICEServer[] = [{
url: 'server1',
urls: 'server1',
auth: undefined,
username: 'a',
credential: 'b'
credential: 'b',
}, {
url: 'server2',
urls: 'server2',
username: 'c',
secret: 'd',
auth: 'secret'
auth: 'secret',
}]
it('does not expose secret', () => {
@ -32,13 +34,13 @@ describe('server/turn', () => {
url: 'server2',
urls: 'server2',
username: jasmine.any(String),
credential: jasmine.any(String)
credential: jasmine.any(String),
})
expect(s[1].username).toMatch(/^[0-9]+:c$/)
})
it('throws error when unknown auth type', () => {
expect(() => turn.processServers([{ auth: 'bla' }]))
expect(() => turn.processServers([{ auth: 'bla' } as any]))
.toThrowError(/not implemented/)
})
})

45
src/server/turn.ts Normal file
View File

@ -0,0 +1,45 @@
import crypto from 'crypto'
import { ICEServer } from './config'
export interface Credentials {
username: string
credential: string
}
export function getCredentials (name: string, secret: string): Credentials {
// this credential would be valid for the next 24 hours
const timestamp = Math.floor(Date.now() / 1000) + 24 * 3600
const username = [timestamp, name].join(':')
const hmac = crypto.createHmac('sha1', secret)
hmac.setEncoding('base64')
hmac.write(username)
hmac.end()
const credential = hmac.read()
return { username, credential }
}
function getServerConfig(server: ICEServer, cred: Credentials) {
return {
url: server.url,
urls: server.urls,
username: cred.username,
credential: cred.credential,
}
}
export function processServers (iceServers: ICEServer[]) {
return iceServers.map(server => {
switch (server.auth) {
case undefined:
return server
case 'secret':
return getServerConfig(
server,
getCredentials(server.username, server.secret),
)
default:
throw new Error('Authentication type not implemented: ' +
(server as {auth: string}).auth)
}
})
}

24
tsconfig.json Normal file
View File

@ -0,0 +1,24 @@
{
"compilerOptions": {
"composite": true,
"declaration": true,
"declarationMap": true,
"inlineSourceMap": true,
"inlineSources": true,
"lib": ["es2015", "dom"],
"target": "es3",
"moduleResolution": "node",
"jsx": "react",
"noImplicitAny": true,
"strict": true,
"skipLibCheck": true,
"noUnusedLocals": false,
"esModuleInterop": true,
"emitDecoratorMetadata": false,
"experimentalDecorators": false,
"allowJs": false,
"checkJs": false,
"outDir": "lib",
"rootDir": "src"
}
}