my harness for niri
1import {
2 Client,
3 Events,
4 GatewayIntentBits,
5 Partials,
6 type Message,
7} from "discord.js"
8import { handleDiscordIngress } from "./pipeline.js"
9
10function asEnabled(value: string | undefined, fallback: boolean): boolean {
11 if (typeof value !== "string") return fallback
12 const normalized = value.trim().toLowerCase()
13 if (!normalized) return fallback
14 if (normalized === "false" || normalized === "0" || normalized === "no") return false
15 if (normalized === "true" || normalized === "1" || normalized === "yes") return true
16 return fallback
17}
18
19function buildIngressPayload(message: Message): Record<string, unknown> {
20 const channelName =
21 message.channel && "name" in message.channel && typeof message.channel.name === "string"
22 ? message.channel.name
23 : null
24
25 return {
26 message: {
27 id: message.id,
28 channel_id: message.channelId,
29 guild_id: message.guildId ?? null,
30 channel_type: message.channel?.type ?? null,
31 content: message.content ?? "",
32 timestamp: message.createdAt.toISOString(),
33 message_reference: message.reference?.messageId
34 ? {
35 message_id: message.reference.messageId,
36 channel_id: message.reference.channelId ?? message.channelId,
37 guild_id: message.reference.guildId ?? message.guildId ?? null,
38 }
39 : null,
40 author: {
41 id: message.author.id,
42 username: message.author.username,
43 global_name: message.author.globalName ?? null,
44 bot: message.author.bot,
45 },
46 mentions: message.mentions.users.map((u) => ({
47 id: u.id,
48 bot: u.bot,
49 })),
50 },
51 channel: {
52 id: message.channelId,
53 type: message.channel?.type ?? null,
54 guild_id: message.guildId ?? null,
55 name: channelName,
56 },
57 is_dm: message.guildId == null,
58 }
59}
60
61export type DiscordGatewayHandle = {
62 stop: () => Promise<void>
63}
64
65export async function startDiscordGateway(): Promise<DiscordGatewayHandle | null> {
66 const enabled = asEnabled(process.env.DISCORD_GATEWAY_ENABLED, true)
67 const trace = asEnabled(process.env.DISCORD_GATEWAY_TRACE, false)
68 const rawFallback = asEnabled(process.env.DISCORD_GATEWAY_RAW_FALLBACK, true)
69 if (!enabled) {
70 console.log("[discord gateway] disabled via DISCORD_GATEWAY_ENABLED=false")
71 return null
72 }
73
74 const token = process.env.DISCORD_BOT_TOKEN?.trim()
75 if (!token) {
76 console.log("[discord gateway] DISCORD_BOT_TOKEN not set; gateway listener disabled")
77 return null
78 }
79
80 const client = new Client({
81 intents: [
82 GatewayIntentBits.Guilds,
83 GatewayIntentBits.GuildMessages,
84 GatewayIntentBits.DirectMessages,
85 GatewayIntentBits.MessageContent,
86 ],
87 partials: [Partials.Channel],
88 })
89
90 if (trace) {
91 console.log("[discord gateway] trace enabled")
92 }
93
94 client.once(Events.ClientReady, (ready) => {
95 if (!process.env.DISCORD_BOT_USER_ID && ready.user?.id) {
96 process.env.DISCORD_BOT_USER_ID = ready.user.id
97 }
98 console.log(
99 `[discord gateway] connected as ${ready.user?.tag ?? ready.user?.id ?? "unknown"} (id=${ready.user?.id ?? "unknown"})`,
100 )
101 })
102
103 client.on("raw", (packet: { t?: string; d?: Record<string, unknown> }) => {
104 if (packet?.t !== "MESSAGE_CREATE") return
105 if (!rawFallback) return
106 const d = packet.d ?? {}
107 const author = (d.author ?? {}) as Record<string, unknown>
108 let ingestResultText = ""
109 try {
110 const result = handleDiscordIngress(d)
111 ingestResultText = ` ingested=${result.ingested} woke=${result.woke} reason=${result.reason}`
112 } catch (err) {
113 ingestResultText = ` ingest_error=${err instanceof Error ? err.message : String(err)}`
114 }
115
116 if (trace) {
117 console.log(
118 `[discord gateway/raw] MESSAGE_CREATE id=${String(d.id ?? "unknown")} channel=${String(d.channel_id ?? "unknown")} guild=${String(d.guild_id ?? "dm")} author=${String(author.username ?? author.id ?? "unknown")} bot=${String(Boolean(author.bot))}${ingestResultText}`,
119 )
120 }
121 })
122
123 client.on(Events.MessageCreate, (message) => {
124 try {
125 const payload = buildIngressPayload(message)
126 const result = handleDiscordIngress(payload)
127 if (trace) {
128 const channelType = typeof message.channel?.type === "number" ? message.channel.type : null
129 const isDm = message.guildId == null
130 console.log(
131 `[discord gateway] messageCreate id=${message.id} channel=${message.channelId} guild=${message.guildId ?? "dm"} type=${channelType ?? "unknown"} is_dm=${isDm} ingested=${result.ingested} woke=${result.woke} reason=${result.reason}`,
132 )
133 }
134 } catch (err) {
135 console.warn("[discord gateway] failed to ingest message:", err)
136 }
137 })
138
139 client.on(Events.Error, (err) => {
140 console.warn("[discord gateway] client error:", err)
141 })
142
143 await client.login(token)
144
145 return {
146 stop: async () => {
147 await client.destroy()
148 console.log("[discord gateway] disconnected")
149 },
150 }
151}