my harness for niri
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

at master 151 lines 5.0 kB view raw
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}