OverviewFilesCommitsIssuesDocsReleasesPeers

hypercore-kb / hyper-social / bridge-discord / lib / bridge.js

147 lines · javascript ref:
import { DiscordClient } from './discord-client.js'
import { KeetClient } from './keet-client.js'
import { Router } from './router.js'
import { discordToEnvelope, keetToDiscord } from './adapter.js'

class Bridge {
  constructor (opts) {
    this.discordToken = opts.discordToken
    this.discordChannel = opts.discordChannel
    this.discordGuild = opts.discordGuild || null

    this.keetRoom = opts.keetRoom
    this.keetHost = opts.keetHost || 'localhost'
    this.keetPort = opts.keetPort || 3117

    this._discord = null
    this._keet = null
    this._router = null
    this._running = false
    this._bridgeIdentity = null

    this.onLog = opts.onLog || console.log
  }

  async start () {
    this.onLog('[bridge] Starting Discord ↔ Keet bridge')
    this.onLog(`[bridge] Discord channel: ${this.discordChannel}`)
    this.onLog(`[bridge] Keet room: ${this.keetRoom}`)

    // Initialize Keet client
    this._keet = new KeetClient({
      host: this.keetHost,
      port: this.keetPort
    })

    try {
      const identity = await this._keet.identity()
      this._bridgeIdentity = identity.memberId || identity.id
      this.onLog(`[bridge] Keet identity: ${identity.displayName || identity.username || identity.name || 'unknown'}`)
    } catch (err) {
      this.onLog(`[bridge] Warning: could not get Keet identity: ${err.message}`)
    }

    // Initialize router
    this._router = new Router({
      onIrcMessage: (name, text) => {
        // "IrcMessage" is a misnomer inherited from shared router — sends to Discord
        this._discord.send(`**${name}** (Keet): ${text}`).catch((err) => {
          this.onLog(`[bridge] Discord send error: ${err.message}`)
        })
      },
      onKeetMessage: (nick, text) => {
        this._keet.send(this.keetRoom, `[Discord] <${nick}> ${text}`).catch((err) => {
          this.onLog(`[bridge] Keet send error: ${err.message}`)
        })
      }
    })

    // Initialize Discord client
    this._discord = new DiscordClient({
      token: this.discordToken,
      channelId: this.discordChannel,
      guildId: this.discordGuild,
      onMessage: (message) => {
        const author = message.author.tag || message.author.username
        const text = message.content

        // Skip bridged messages
        if (text.startsWith('**') && text.includes('(Keet):')) return

        this.onLog(`[discord→keet] <${author}> ${text}`)

        // Handle attachments
        let fullText = text
        for (const [, att] of message.attachments) {
          fullText += (fullText ? '\n' : '') + att.url
        }

        this._router.routeToKeet(author, fullText)
      },
      onReaction: (reaction, user) => {
        const emoji = reaction.emoji.name || '?'
        const author = user.tag || user.username
        this.onLog(`[discord→keet] ${author} reacted ${emoji}`)
      },
      onError: (err) => {
        this.onLog(`[bridge] Discord error: ${err.message}`)
      }
    })

    // Connect to Discord
    await this._discord.connect()
    this.onLog(`[bridge] Connected to Discord as ${this._discord.botUser?.tag || 'bot'}`)

    // Verify Keet daemon
    try {
      const status = await this._keet.status()
      this.onLog(`[bridge] Keet daemon online (${status.rooms || '?'} rooms)`)
    } catch (err) {
      this.onLog(`[bridge] Warning: Keet daemon unreachable: ${err.message}`)
    }

    // Start watching Keet room
    this._startKeetWatch()

    this._running = true
    this.onLog('[bridge] Bridge active — messages flowing bidirectionally')
  }

  _startKeetWatch () {
    this._keet.watch(this.keetRoom, (msg) => {
      const relayed = keetToDiscord(msg, this._bridgeIdentity)
      if (!relayed) return

      // Skip bridged messages
      if (relayed.text.includes('[Discord]')) return

      this.onLog(`[keet→discord] ${relayed.text}`)
      this._router.routeToIrc(relayed.name, msg.text)
    })
  }

  async stop () {
    this.onLog('[bridge] Shutting down...')
    this._running = false

    if (this._keet) this._keet.stopWatch()
    if (this._discord) this._discord.disconnect()

    this.onLog('[bridge] Stopped')
  }

  get running () {
    return this._running
  }

  get stats () {
    return {
      running: this._running,
      discordConnected: this._discord?.connected || false,
      deduped: this._router?.seenCount || 0
    }
  }
}

export { Bridge }