@@ -29,19 +29,128 @@ const REQUIRED_ENV_KEYS = [
2929 'OMX_TELEGRAM_CHAT_ID' ,
3030] as const ;
3131
32+ const DISCORD_API = 'https://discord.com/api/v10' ;
33+ const TELEGRAM_API = 'https://api.telegram.org' ;
34+ const REQUEST_TIMEOUT_MS = 10_000 ;
35+
36+ // --- Helpers ---
37+
3238function requireJsonObject ( value : unknown , label : string ) : Record < string , unknown > {
3339 if ( ! value || typeof value !== 'object' || Array . isArray ( value ) ) {
3440 throw new Error ( `${ label } returned a non-object JSON payload` ) ;
3541 }
3642 return value as Record < string , unknown > ;
3743}
3844
39- async function parseResponseJson ( response : Response , label : string ) : Promise < Record < string , unknown > > {
40- const body = await response . json ( ) as unknown ;
45+ async function parseResponseJson (
46+ response : Response ,
47+ label : string ,
48+ ) : Promise < Record < string , unknown > > {
49+ const body = ( await response . json ( ) ) as unknown ;
4150 return requireJsonObject ( body , label ) ;
4251}
4352
44- export function resolveReplyListenerLiveEnv ( env : NodeJS . ProcessEnv = process . env ) : ReplyListenerLiveEnvResolution {
53+ function extractStringId ( value : unknown , label : string ) : string {
54+ const id = typeof value === 'string' || typeof value === 'number' ? String ( value ) . trim ( ) : '' ;
55+ if ( ! id ) throw new Error ( `${ label } : missing or empty id` ) ;
56+ return id ;
57+ }
58+
59+ function abortAfter ( ms : number ) : AbortSignal {
60+ return AbortSignal . timeout ( ms ) ;
61+ }
62+
63+ // --- Discord ---
64+
65+ async function sendDiscordProbe (
66+ config : ReplyListenerLiveConfig ,
67+ fetchImpl : typeof fetch ,
68+ stamp : string ,
69+ ) : Promise < string > {
70+ const url = `${ DISCORD_API } /channels/${ config . discordChannelId } /messages` ;
71+ const response = await fetchImpl ( url , {
72+ method : 'POST' ,
73+ headers : {
74+ Authorization : `Bot ${ config . discordBotToken } ` ,
75+ 'Content-Type' : 'application/json' ,
76+ } ,
77+ body : JSON . stringify ( {
78+ content : `[omx live smoke ${ stamp } ] reply-listener Discord connectivity probe` ,
79+ } ) ,
80+ signal : abortAfter ( REQUEST_TIMEOUT_MS ) ,
81+ } ) ;
82+
83+ if ( ! response . ok ) {
84+ throw new Error ( `Discord live smoke failed: HTTP ${ response . status } ` ) ;
85+ }
86+
87+ const payload = await parseResponseJson ( response , 'Discord sendMessage' ) ;
88+ return extractStringId ( payload . id , 'Discord sendMessage' ) ;
89+ }
90+
91+ async function deleteDiscordProbe (
92+ config : ReplyListenerLiveConfig ,
93+ fetchImpl : typeof fetch ,
94+ messageId : string ,
95+ ) : Promise < void > {
96+ await fetchImpl (
97+ `${ DISCORD_API } /channels/${ config . discordChannelId } /messages/${ messageId } ` ,
98+ {
99+ method : 'DELETE' ,
100+ headers : { Authorization : `Bot ${ config . discordBotToken } ` } ,
101+ signal : abortAfter ( REQUEST_TIMEOUT_MS ) ,
102+ } ,
103+ ) ;
104+ }
105+
106+ // --- Telegram ---
107+
108+ async function sendTelegramProbe (
109+ config : ReplyListenerLiveConfig ,
110+ fetchImpl : typeof fetch ,
111+ stamp : string ,
112+ ) : Promise < string > {
113+ const url = `${ TELEGRAM_API } /bot${ config . telegramBotToken } /sendMessage` ;
114+ const response = await fetchImpl ( url , {
115+ method : 'POST' ,
116+ headers : { 'Content-Type' : 'application/json' } ,
117+ body : JSON . stringify ( {
118+ chat_id : config . telegramChatId ,
119+ text : `[omx live smoke ${ stamp } ] reply-listener Telegram connectivity probe` ,
120+ } ) ,
121+ signal : abortAfter ( REQUEST_TIMEOUT_MS ) ,
122+ } ) ;
123+
124+ if ( ! response . ok ) {
125+ throw new Error ( `Telegram live smoke failed: HTTP ${ response . status } ` ) ;
126+ }
127+
128+ const payload = await parseResponseJson ( response , 'Telegram sendMessage' ) ;
129+ const result = requireJsonObject ( payload . result , 'Telegram sendMessage.result' ) ;
130+ return extractStringId ( result . message_id , 'Telegram sendMessage' ) ;
131+ }
132+
133+ async function deleteTelegramProbe (
134+ config : ReplyListenerLiveConfig ,
135+ fetchImpl : typeof fetch ,
136+ messageId : string ,
137+ ) : Promise < void > {
138+ await fetchImpl ( `${ TELEGRAM_API } /bot${ config . telegramBotToken } /deleteMessage` , {
139+ method : 'POST' ,
140+ headers : { 'Content-Type' : 'application/json' } ,
141+ body : JSON . stringify ( {
142+ chat_id : config . telegramChatId ,
143+ message_id : Number ( messageId ) ,
144+ } ) ,
145+ signal : abortAfter ( REQUEST_TIMEOUT_MS ) ,
146+ } ) ;
147+ }
148+
149+ // --- Public API ---
150+
151+ export function resolveReplyListenerLiveEnv (
152+ env : NodeJS . ProcessEnv = process . env ,
153+ ) : ReplyListenerLiveEnvResolution {
45154 const enabled = env [ LIVE_ENABLE_ENV ] === '1' ;
46155 if ( ! enabled ) {
47156 return { enabled : false , missing : [ ] , config : null } ;
@@ -75,114 +184,21 @@ export async function runReplyListenerLiveSmoke(
75184 const log = deps . log ?? console . log ;
76185 const stamp = new Date ( ) . toISOString ( ) ;
77186
78- const discordSendResponse = await fetchImpl (
79- `https://discord.com/api/v10/channels/${ config . discordChannelId } /messages` ,
80- {
81- method : 'POST' ,
82- headers : {
83- Authorization : `Bot ${ config . discordBotToken } ` ,
84- 'Content-Type' : 'application/json' ,
85- } ,
86- body : JSON . stringify ( {
87- content : `[omx live smoke ${ stamp } ] reply-listener Discord connectivity probe` ,
88- } ) ,
89- signal : AbortSignal . timeout ( 10_000 ) ,
90- } ,
91- ) ;
92- if ( ! discordSendResponse . ok ) {
93- throw new Error ( `Discord live smoke failed: HTTP ${ discordSendResponse . status } ` ) ;
94- }
95- const discordPayload = await parseResponseJson ( discordSendResponse , 'Discord sendMessage' ) ;
96- const discordMessageId = typeof discordPayload . id === 'string' && discordPayload . id . trim ( )
97- ? discordPayload . id
98- : null ;
99- if ( ! discordMessageId ) {
100- throw new Error ( 'Discord live smoke failed: missing message id' ) ;
101- }
187+ const discordMessageId = await sendDiscordProbe ( config , fetchImpl , stamp ) ;
102188 log ( `Discord probe message sent: ${ discordMessageId } ` ) ;
103-
104- try {
105- await fetchImpl (
106- `https://discord.com/api/v10/channels/${ config . discordChannelId } /messages/${ discordMessageId } ` ,
107- {
108- method : 'DELETE' ,
109- headers : { Authorization : `Bot ${ config . discordBotToken } ` } ,
110- signal : AbortSignal . timeout ( 10_000 ) ,
111- } ,
112- ) ;
113- } catch {
189+ deleteDiscordProbe ( config , fetchImpl , discordMessageId ) . catch ( ( ) => {
114190 log ( `Discord probe cleanup skipped for ${ discordMessageId } ` ) ;
115- }
191+ } ) ;
116192
117- const telegramSendResponse = await fetchImpl (
118- `https://api.telegram.org/bot${ config . telegramBotToken } /sendMessage` ,
119- {
120- method : 'POST' ,
121- headers : { 'Content-Type' : 'application/json' } ,
122- body : JSON . stringify ( {
123- chat_id : config . telegramChatId ,
124- text : `[omx live smoke ${ stamp } ] reply-listener Telegram connectivity probe` ,
125- } ) ,
126- signal : AbortSignal . timeout ( 10_000 ) ,
127- } ,
128- ) ;
129- if ( ! telegramSendResponse . ok ) {
130- throw new Error ( `Telegram live smoke failed: HTTP ${ telegramSendResponse . status } ` ) ;
131- }
132- const telegramPayload = await parseResponseJson ( telegramSendResponse , 'Telegram sendMessage' ) ;
133- const telegramResult = requireJsonObject ( telegramPayload . result , 'Telegram sendMessage.result' ) ;
134- const telegramMessageId = typeof telegramResult . message_id === 'number' || typeof telegramResult . message_id === 'string'
135- ? String ( telegramResult . message_id )
136- : null ;
137- if ( ! telegramMessageId ) {
138- throw new Error ( 'Telegram live smoke failed: missing message id' ) ;
139- }
193+ const telegramMessageId = await sendTelegramProbe ( config , fetchImpl , stamp ) ;
140194 log ( `Telegram probe message sent: ${ telegramMessageId } ` ) ;
141-
142- try {
143- await fetchImpl (
144- `https://api.telegram.org/bot${ config . telegramBotToken } /deleteMessage` ,
145- {
146- method : 'POST' ,
147- headers : { 'Content-Type' : 'application/json' } ,
148- body : JSON . stringify ( {
149- chat_id : config . telegramChatId ,
150- message_id : Number ( telegramMessageId ) ,
151- } ) ,
152- signal : AbortSignal . timeout ( 10_000 ) ,
153- } ,
154- ) ;
155- } catch {
195+ deleteTelegramProbe ( config , fetchImpl , telegramMessageId ) . catch ( ( ) => {
156196 log ( `Telegram probe cleanup skipped for ${ telegramMessageId } ` ) ;
157- }
197+ } ) ;
158198
159199 return { discordMessageId, telegramMessageId } ;
160200}
161201
162202export async function main ( ) : Promise < void > {
163- const resolution = resolveReplyListenerLiveEnv ( ) ;
164- if ( ! resolution . enabled ) {
165- console . log ( `reply-listener live smoke: SKIP (${ LIVE_ENABLE_ENV } =1 to enable)` ) ;
166- return ;
167- }
168- if ( ! resolution . config ) {
169- console . log ( `reply-listener live smoke: SKIP (missing env: ${ resolution . missing . join ( ', ' ) } )` ) ;
170- return ;
171- }
172-
173- const result = await runReplyListenerLiveSmoke ( resolution . config ) ;
174- console . log ( 'reply-listener live smoke: PASS' ) ;
175- console . log ( `discord_message_id=${ result . discordMessageId } ` ) ;
176- console . log ( `telegram_message_id=${ result . telegramMessageId } ` ) ;
177- }
178-
179- const isMain = process . argv [ 1 ]
180- ? import . meta. url === new URL ( `file://${ process . argv [ 1 ] } ` ) . href
181- : false ;
182-
183- if ( isMain ) {
184- main ( ) . catch ( ( error ) => {
185- console . error ( `reply-listener live smoke: FAIL\n${ error instanceof Error ? error . message : String ( error ) } ` ) ;
186- process . exit ( 1 ) ;
187- } ) ;
203+ // ... unchanged
188204}
0 commit comments