OverviewFilesCommitsIssuesDocsReleasesPeers

hypercore-kb / hyper-social / hyper-daemon / lib / actions.js

104 lines · javascript ref:
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 }