import { MatrixClient } from './matrix-client.js'
import { KeetClient } from './keet-client.js'
import { Router } from './router.js'
import { matrixToKeetText, keetToMatrixText } from './adapter.js'
class Bridge {
constructor (opts) {
this.matrixHomeserver = opts.matrixHomeserver || 'matrix.org'
this.matrixUser = opts.matrixUser
this.matrixPassword = opts.matrixPassword
this.matrixRoom = opts.matrixRoom
this.keetRoom = opts.keetRoom
this.keetHost = opts.keetHost || 'localhost'
this.keetPort = opts.keetPort || 3117
this._matrix = null
this._keet = null
this._router = null
this._running = false
this._bridgeIdentity = null
this._lastSeq = -1
this._pollTimer = null
this.onLog = opts.onLog || console.log
}
async start () {
this.onLog('[bridge] Starting Matrix <-> Keet bridge')
this.onLog(`[bridge] Matrix: ${this.matrixHomeserver} room: ${this.matrixRoom}`)
this.onLog(`[bridge] Keet room: ${this.keetRoom}`)
// Initialize Keet client
this._keet = new KeetClient({
host: this.keetHost,
port: this.keetPort
})
// Get bridge identity so we can filter our own messages
try {
const identity = await this._keet.identity()
this._bridgeIdentity = identity.memberId || identity.id
this.onLog(`[bridge] Keet identity: ${identity.displayName || identity.username || 'unknown'}`)
} catch (err) {
this.onLog(`[bridge] Warning: could not get Keet identity: ${err.message}`)
}
// Snapshot the latest message seq to distinguish history from new
try {
const msgs = await this._keet.messages(this.keetRoom, 1)
if (msgs && msgs.length > 0) {
this._lastSeq = msgs[msgs.length - 1].seq || 0
this.onLog(`[bridge] History cutoff: seq ${this._lastSeq}`)
}
} catch (err) {
this.onLog(`[bridge] Warning: could not get message history: ${err.message}`)
}
// Initialize Matrix client
this._matrix = new MatrixClient({
homeserver: this.matrixHomeserver,
onMessage: (roomId, sender, text, eventId) => {
// Only relay messages from the configured room
if (roomId !== this.matrixRoom) return
this.onLog(`[matrix->keet] <${sender}> ${text}`)
const keetText = matrixToKeetText(sender, text)
this._router.routeToKeet(sender, keetText)
}
})
// Login to Matrix
this.onLog('[bridge] Logging in to Matrix...')
const loginRes = await this._matrix.login(this.matrixHomeserver, this.matrixUser, this.matrixPassword)
this.onLog(`[bridge] Matrix logged in as ${loginRes.user_id}`)
// Join the Matrix room
try {
await this._matrix.joinRoom(this.matrixRoom)
const roomName = await this._matrix.getRoomName(this.matrixRoom)
this.onLog(`[bridge] Joined Matrix room: ${roomName || this.matrixRoom}`)
} catch (err) {
this.onLog(`[bridge] Warning: join room error: ${err.message}`)
}
// Initialize router with dedup
this._router = new Router({
onKeetMessage: (author, text) => {
this._keet.send(this.keetRoom, text).catch((err) => {
this.onLog(`[bridge] Keet send error: ${err.message}`)
})
},
onExternalMessage: (name, text) => {
this._matrix.sendMessage(this.matrixRoom, text).catch((err) => {
this.onLog(`[bridge] Matrix send error: ${err.message}`)
})
}
})
// Verify Keet daemon is reachable
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 Matrix sync loop
this._matrix._startSync()
// Start polling Keet room for new messages
this._startKeetPoll()
this._running = true
this.onLog('[bridge] Bridge active -- messages flowing bidirectionally')
}
_startKeetPoll () {
this._pollTimer = setInterval(async () => {
try {
const msgs = await this._keet.messages(this.keetRoom, 10)
if (!msgs || msgs.length === 0) return
for (const msg of msgs) {
if (msg.seq === undefined || msg.seq <= this._lastSeq) continue
this._lastSeq = msg.seq
const relayed = keetToMatrixText(msg, this._bridgeIdentity)
if (!relayed) continue
// Skip messages bridged from Matrix (prevents echo)
if (msg.text && msg.text.startsWith('[Matrix]')) continue
const displayText = `<${relayed.sender}> ${relayed.text}`
this.onLog(`[keet->matrix] ${displayText}`)
this._router.routeToExternal(relayed.sender, displayText)
}
} catch (err) {
this.onLog(`[bridge] Keet poll error: ${err.message}`)
}
}, 2000)
}
async stop () {
this.onLog('[bridge] Shutting down...')
this._running = false
if (this._pollTimer) clearInterval(this._pollTimer)
if (this._matrix) this._matrix.stopSync()
this.onLog('[bridge] Stopped')
}
get running () {
return this._running
}
get stats () {
return {
running: this._running,
matrixUser: this._matrix?.userId || null,
deduped: this._router?.seenCount || 0
}
}
}
export { Bridge }