hotkey support

This commit is contained in:
itsrubberduck
2026-06-20 03:17:53 +02:00
parent 33503e6a61
commit 7ee9392109
4 changed files with 241 additions and 0 deletions

View File

@@ -0,0 +1,46 @@
import { createError, readBody } from 'h3'
import { BridgeToken } from '../../models/BridgeToken'
import { getBridgeTokenFromHeader } from '../../utils/bridge'
import { logBridgeEvent } from '../../utils/bridgeLog'
import { pttBus, type PttState } from '../../utils/pttBus'
interface PttBody {
state?: PttState
}
export default defineEventHandler(async (event) => {
const token = getBridgeTokenFromHeader(event)
if (!token) {
throw createError({ statusCode: 401, statusMessage: 'x-bridge-token header fehlt oder ist ungültig.' })
}
const body = await readBody<PttBody>(event)
const state = body?.state
if (state !== 'down' && state !== 'up') {
throw createError({ statusCode: 400, statusMessage: "state muss 'down' oder 'up' sein." })
}
// Only relay for a linked token; an unknown/unlinked token is a no-op so the
// bus is never driven by an unauthenticated caller.
const exists = await BridgeToken.exists({ token })
if (!exists) {
throw createError({ statusCode: 404, statusMessage: 'Bridge-Token ist nicht verknüpft.' })
}
console.info(
`\x1b[33m[bridge:ptt]\x1b[0m token=\x1b[96m${token.slice(0, 6)}...\x1b[0m state=\x1b[92m${state}\x1b[0m`,
)
pttBus.publish(token, state)
logBridgeEvent(token, {
endpoint: '/api/bridge/ptt',
method: 'POST',
statusCode: 200,
color: '#eab308',
summary: `ptt=${state}`,
data: { state },
})
return { ok: true, state }
})

77
server/api/bridge/ws.ts Normal file
View File

@@ -0,0 +1,77 @@
// server/api/bridge/ws.ts
//
// Low-latency push channel for push-to-talk. A /pm tab opens this socket and
// sends { type: 'subscribe', token } using its `?token=` bridge link. The
// Bridge POSTs key edges to /api/bridge/ptt, which drives pttBus; we relay each
// edge to every peer subscribed to that token.
import { defineWebSocketHandler } from 'h3'
import { normalizeBridgeToken } from '../../utils/bridge'
import { pttBus } from '../../utils/pttBus'
// token → set of connected /pm peers
const subscribers = new Map<string, Set<any>>()
// peerId → token, so close() can clean up without scanning every set
const peerTokens = new Map<string, string>()
function peerId(peer: any): string {
return peer?.id ?? String(peer)
}
// Relay every PTT edge to the peers listening on that token.
pttBus.subscribe((token, state) => {
const peers = subscribers.get(token)
if (!peers) return
const payload = JSON.stringify({ type: 'ptt', state })
for (const peer of peers) {
try { peer.send(payload) } catch {}
}
})
function unsubscribe(peer: any) {
const id = peerId(peer)
const token = peerTokens.get(id)
if (!token) return
peerTokens.delete(id)
const peers = subscribers.get(token)
if (!peers) return
peers.delete(peer)
if (peers.size === 0) subscribers.delete(token)
}
export default defineWebSocketHandler({
message(peer, msg) {
let data: any
try {
data = JSON.parse(typeof msg === 'string' ? msg : msg.toString())
} catch {
peer.send(JSON.stringify({ type: 'error', message: 'Invalid JSON' }))
return
}
if (data.type === 'subscribe') {
const token = normalizeBridgeToken(data.token)
if (!token) {
peer.send(JSON.stringify({ type: 'error', message: 'Invalid token' }))
return
}
// A peer only ever listens to one token; drop any previous binding.
unsubscribe(peer)
let peers = subscribers.get(token)
if (!peers) {
peers = new Set()
subscribers.set(token, peers)
}
peers.add(peer)
peerTokens.set(peerId(peer), token)
peer.send(JSON.stringify({ type: 'subscribed' }))
}
},
close(peer) {
unsubscribe(peer)
},
error(peer) {
unsubscribe(peer)
},
})