import http from 'http'
import net from 'net'
class ActionRunner {
constructor (opts = {}) {
this.keetHost = opts.keetHost || 'localhost'
this.keetPort = opts.keetPort || 3117
this._log = opts.onLog || console.log
}
async run (action, data) {
switch (action.type) {
case 'send-message':
return this._sendMessage(action, data)
case 'http-call':
return this._httpCall(action, data)
case 'log':
return this._log(action, data)
default:
throw new Error(`Unknown action type: ${action.type}`)
}
}
async _sendMessage (action, data) {
const roomId = action.roomId || data.roomId
let text = action.text || ''
// Template substitution
text = text
.replace(/\{author\}/g, data.author || data.name || '')
.replace(/\{text\}/g, data.text || '')
.replace(/\{channel\}/g, data.channel || '')
.replace(/\{timestamp\}/g, new Date().toISOString())
if (!roomId || !text) return { sent: false, reason: 'missing roomId or text' }
await this._keetRpc('send', { roomId, text })
return { sent: true, roomId, text }
}
async _httpCall (action) {
const url = new URL(action.url)
return new Promise((resolve, reject) => {
const req = http.request({
hostname: url.hostname,
port: url.port || 80,
path: url.pathname + url.search,
method: action.method || 'GET',
headers: action.headers || {}
}, (res) => {
let body = ''
res.on('data', (c) => { body += c.toString() })
res.on('end', () => resolve({ status: res.statusCode, body }))
})
req.on('error', reject)
req.setTimeout(10000, () => req.destroy(new Error('timeout')))
if (action.body) req.write(action.body)
req.end()
})
}
_logAction (action, data) {
const msg = action.message || JSON.stringify(data)
this._log(`[daemon] ${msg}`)
return { logged: true }
}
// Raw TCP for keet-daemon (duplicate Content-Length workaround)
_keetRpc (method, params) {
return new Promise((resolve, reject) => {
const socket = net.connect(this.keetPort, this.keetHost)
const payload = JSON.stringify({ method, params })
socket.on('connect', () => {
let req = `POST /rpc HTTP/1.1\r\n`
req += `Host: ${this.keetHost}:${this.keetPort}\r\n`
req += `Content-Type: application/json\r\n`
req += `Content-Length: ${Buffer.byteLength(payload)}\r\n`
req += `Connection: close\r\n\r\n`
req += payload
socket.write(req)
})
let data = ''
socket.on('data', (chunk) => { data += chunk.toString() })
socket.on('end', () => {
const idx = data.indexOf('\r\n\r\n')
if (idx === -1) return reject(new Error('No body'))
try {
const parsed = JSON.parse(data.slice(idx + 4))
if (!parsed.ok) reject(new Error(parsed.error || 'RPC error'))
else resolve(parsed.data)
} catch {
reject(new Error('Invalid response'))
}
})
socket.on('error', reject)
socket.setTimeout(10000, () => socket.destroy(new Error('timeout')))
})
}
}
export { ActionRunner }