11import 'server-only' ;
22import { DISCORD_BOT_TOKEN } from '@/lib/config.server' ;
33import { captureException } from '@sentry/nextjs' ;
4+ import { buildDiscordApiUrl , parseDiscordSnowflake } from './discord-id' ;
45
56export type DiscordEventContext = {
67 channelId : string ;
@@ -44,14 +45,15 @@ type DiscordApiMessage = {
4445} ;
4546
4647async function fetchDiscordApi < T > (
47- path : string
48+ pathSegments : string [ ] ,
49+ query ?: Record < string , string | number >
4850) : Promise < { ok : true ; data : T } | { ok : false ; error : string } > {
4951 if ( ! DISCORD_BOT_TOKEN ) {
5052 return { ok : false , error : 'DISCORD_BOT_TOKEN is not configured' } ;
5153 }
5254
5355 try {
54- const response = await fetch ( `https://discord.com/api/v10 ${ path } ` , {
56+ const response = await fetch ( buildDiscordApiUrl ( pathSegments , query ) , {
5557 headers : { Authorization : `Bot ${ DISCORD_BOT_TOKEN } ` } ,
5658 } ) ;
5759
@@ -71,7 +73,14 @@ async function fetchDiscordApi<T>(
7173async function getChannelInfo (
7274 channelId : string
7375) : Promise < { ok : true ; channel : DiscordChannelInfo } | { ok : false ; error : string } > {
74- const result = await fetchDiscordApi < DiscordApiChannel > ( `/channels/${ channelId } ` ) ;
76+ let validatedChannelId : string ;
77+ try {
78+ validatedChannelId = parseDiscordSnowflake ( channelId , 'channel ID' ) ;
79+ } catch ( error ) {
80+ return { ok : false , error : error instanceof Error ? error . message : 'Invalid channel ID' } ;
81+ }
82+
83+ const result = await fetchDiscordApi < DiscordApiChannel > ( [ 'channels' , validatedChannelId ] ) ;
7584 if ( ! result . ok ) return result ;
7685
7786 return {
@@ -89,8 +98,22 @@ async function getChannelMessages(
8998 channelId : string ,
9099 limit : number
91100) : Promise < { ok : true ; messages : DiscordMessageForPrompt [ ] } | { ok : false ; error : string } > {
101+ let validatedChannelId : string ;
102+ try {
103+ validatedChannelId = parseDiscordSnowflake ( channelId , 'channel ID' ) ;
104+ } catch ( error ) {
105+ return { ok : false , error : error instanceof Error ? error . message : 'Invalid channel ID' } ;
106+ }
107+
108+ if ( ! Number . isInteger ( limit ) || limit < 1 || limit > 100 ) {
109+ return { ok : false , error : 'Invalid Discord channel message limit' } ;
110+ }
111+
92112 const result = await fetchDiscordApi < DiscordApiMessage [ ] > (
93- `/channels/${ channelId } /messages?limit=${ limit } `
113+ [ 'channels' , validatedChannelId , 'messages' ] ,
114+ {
115+ limit,
116+ }
94117 ) ;
95118 if ( ! result . ok ) return result ;
96119
@@ -111,6 +134,31 @@ export async function getDiscordConversationContext(
111134 const channelMessagesLimit = limits ?. channelMessages ?? 12 ;
112135 const errors : string [ ] = [ ] ;
113136
137+ const contextIds = [
138+ { fieldName : 'guild ID' , value : context . guildId } ,
139+ { fieldName : 'channel ID' , value : context . channelId } ,
140+ { fieldName : 'user ID' , value : context . userId } ,
141+ { fieldName : 'message ID' , value : context . messageId } ,
142+ ] ;
143+
144+ for ( const { fieldName, value } of contextIds ) {
145+ try {
146+ parseDiscordSnowflake ( value , fieldName ) ;
147+ } catch ( error ) {
148+ errors . push ( error instanceof Error ? error . message : `Invalid Discord ${ fieldName } ` ) ;
149+ }
150+ }
151+
152+ if ( errors . length > 0 ) {
153+ captureException ( new Error ( 'Invalid Discord conversation context' ) , {
154+ level : 'warning' ,
155+ tags : { source : 'discord_conversation_context' } ,
156+ extra : { errors } ,
157+ } ) ;
158+
159+ return { channel : null , recentMessages : [ ] , errors } ;
160+ }
161+
114162 const [ channelInfoResult , messagesResult ] = await Promise . all ( [
115163 getChannelInfo ( context . channelId ) ,
116164 getChannelMessages ( context . channelId , channelMessagesLimit ) ,
0 commit comments