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 }