2023-03-06 07:57:05 +13:00
|
|
|
import { Server } from "socket.io"
|
|
|
|
import http from "http"
|
|
|
|
import Koa from "koa"
|
|
|
|
import Cookies from "cookies"
|
|
|
|
import { userAgent } from "koa-useragent"
|
|
|
|
import { auth } from "@budibase/backend-core"
|
2023-03-07 04:09:42 +13:00
|
|
|
import currentApp from "../middleware/currentapp"
|
2023-05-25 19:48:56 +12:00
|
|
|
import { createAdapter } from "@socket.io/redis-adapter"
|
|
|
|
import { getSocketPubSubClients } from "../utilities/redis"
|
2023-03-06 07:57:05 +13:00
|
|
|
|
|
|
|
export default class Socket {
|
|
|
|
io: Server
|
|
|
|
|
|
|
|
constructor(
|
|
|
|
app: Koa,
|
|
|
|
server: http.Server,
|
2023-05-25 19:48:56 +12:00
|
|
|
path: string = "/",
|
2023-03-06 07:57:05 +13:00
|
|
|
additionalMiddlewares?: any[]
|
|
|
|
) {
|
|
|
|
this.io = new Server(server, {
|
|
|
|
path,
|
|
|
|
})
|
|
|
|
|
|
|
|
// Attach default middlewares
|
|
|
|
const authenticate = auth.buildAuthMiddleware([], {
|
|
|
|
publicAllowed: true,
|
|
|
|
})
|
|
|
|
const middlewares = [
|
|
|
|
userAgent,
|
|
|
|
authenticate,
|
2023-04-20 19:02:49 +12:00
|
|
|
currentApp,
|
2023-03-06 07:57:05 +13:00
|
|
|
...(additionalMiddlewares || []),
|
|
|
|
]
|
|
|
|
|
|
|
|
// Apply middlewares
|
|
|
|
this.io.use(async (socket, next) => {
|
|
|
|
// Build fake koa context
|
|
|
|
const res = new http.ServerResponse(socket.request)
|
|
|
|
const ctx: any = {
|
|
|
|
...app.createContext(socket.request, res),
|
|
|
|
|
|
|
|
// Additional overrides needed to make our middlewares work with this
|
|
|
|
// fake koa context
|
|
|
|
cookies: new Cookies(socket.request, res),
|
|
|
|
get: (field: string) => socket.request.headers[field],
|
|
|
|
throw: (code: number, message: string) => {
|
|
|
|
throw new Error(message)
|
|
|
|
},
|
|
|
|
|
|
|
|
// Needed for koa-useragent middleware
|
2023-04-20 19:02:49 +12:00
|
|
|
headers: socket.request.headers,
|
2023-03-06 07:57:05 +13:00
|
|
|
header: socket.request.headers,
|
2023-04-21 02:23:57 +12:00
|
|
|
|
|
|
|
// We don't really care about the path since it will never contain
|
|
|
|
// an app ID
|
|
|
|
path: "/socket",
|
2023-03-06 07:57:05 +13:00
|
|
|
}
|
|
|
|
|
|
|
|
// Run all koa middlewares
|
|
|
|
try {
|
|
|
|
for (let [idx, middleware] of middlewares.entries()) {
|
|
|
|
await middleware(ctx, () => {
|
|
|
|
if (idx === middlewares.length - 1) {
|
2023-05-17 01:18:31 +12:00
|
|
|
// Middlewares are finished
|
2023-03-06 07:57:05 +13:00
|
|
|
// Extract some data from our enriched koa context to persist
|
|
|
|
// as metadata for the socket
|
2023-05-17 01:18:31 +12:00
|
|
|
|
|
|
|
// Add user info, including a deterministic color and friendly
|
|
|
|
// label
|
|
|
|
const { _id, email, firstName, lastName } = ctx.user
|
|
|
|
let hue = 1
|
|
|
|
for (let i = 0; i < email.length && i < 5; i++) {
|
|
|
|
hue *= email.charCodeAt(i + 1)
|
|
|
|
hue /= 17
|
|
|
|
}
|
|
|
|
hue = hue % 360
|
|
|
|
const color = `hsl(${hue}, 50%, 40%)`
|
|
|
|
let label = email
|
|
|
|
if (firstName) {
|
|
|
|
label = firstName
|
|
|
|
if (lastName) {
|
|
|
|
label += ` ${lastName}`
|
|
|
|
}
|
|
|
|
}
|
2023-03-06 20:43:45 +13:00
|
|
|
socket.data.user = {
|
2023-05-17 01:18:31 +12:00
|
|
|
id: _id,
|
|
|
|
email,
|
|
|
|
color,
|
|
|
|
label,
|
2023-03-06 20:43:45 +13:00
|
|
|
}
|
2023-05-17 01:18:31 +12:00
|
|
|
|
|
|
|
// Add app ID to help split sockets into rooms
|
|
|
|
socket.data.appId = ctx.appId
|
2023-03-06 07:57:05 +13:00
|
|
|
next()
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
} catch (error: any) {
|
|
|
|
next(error)
|
|
|
|
}
|
|
|
|
})
|
2023-05-25 19:48:56 +12:00
|
|
|
|
|
|
|
// Instantiate redis adapter
|
|
|
|
const { pub, sub } = getSocketPubSubClients()
|
|
|
|
const opts = { key: `socket.io-${path}` }
|
|
|
|
this.io.adapter(createAdapter(pub, sub, opts))
|
2023-03-06 07:57:05 +13:00
|
|
|
}
|
|
|
|
|
|
|
|
// Emit an event to all sockets
|
|
|
|
emit(event: string, payload: any) {
|
|
|
|
this.io.sockets.emit(event, payload)
|
|
|
|
}
|
|
|
|
}
|