142 lines
		
	
	
		
			4.2 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
		
		
			
		
	
	
			142 lines
		
	
	
		
			4.2 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
|  | import * as http from 'node:http'; | ||
|  | import * as https from 'node:https'; | ||
|  | import { URL } from 'node:url'; | ||
|  | import { Action, type IActionConfig, type IActionContext } from './base-action.ts'; | ||
|  | import { logger } from '../logger.ts'; | ||
|  | 
 | ||
|  | /** | ||
|  |  * WebhookAction - Calls an HTTP webhook with UPS state information | ||
|  |  * | ||
|  |  * Sends UPS status to a configured webhook URL via GET or POST. | ||
|  |  * This is useful for remote notifications and integrations with external systems. | ||
|  |  */ | ||
|  | export class WebhookAction extends Action { | ||
|  |   readonly type = 'webhook'; | ||
|  | 
 | ||
|  |   /** | ||
|  |    * Execute the webhook action | ||
|  |    * @param context Action context with UPS state | ||
|  |    */ | ||
|  |   async execute(context: IActionContext): Promise<void> { | ||
|  |     // Check if we should execute based on trigger mode
 | ||
|  |     if (!this.shouldExecute(context)) { | ||
|  |       logger.info(`Webhook action skipped (trigger mode: ${this.config.triggerMode || 'powerChangesAndThresholds'})`); | ||
|  |       return; | ||
|  |     } | ||
|  | 
 | ||
|  |     if (!this.config.webhookUrl) { | ||
|  |       logger.error('Webhook URL not configured'); | ||
|  |       return; | ||
|  |     } | ||
|  | 
 | ||
|  |     const method = this.config.webhookMethod || 'POST'; | ||
|  |     const timeout = this.config.webhookTimeout || 10000; | ||
|  | 
 | ||
|  |     logger.info(`Calling webhook: ${method} ${this.config.webhookUrl}`); | ||
|  | 
 | ||
|  |     try { | ||
|  |       await this.callWebhook(context, method, timeout); | ||
|  |       logger.success('Webhook call successful'); | ||
|  |     } catch (error) { | ||
|  |       logger.error( | ||
|  |         `Webhook call failed: ${error instanceof Error ? error.message : String(error)}`, | ||
|  |       ); | ||
|  |       // Don't throw - webhook failures shouldn't stop other actions
 | ||
|  |     } | ||
|  |   } | ||
|  | 
 | ||
|  |   /** | ||
|  |    * Call the webhook with UPS state data | ||
|  |    * @param context Action context | ||
|  |    * @param method HTTP method (GET or POST) | ||
|  |    * @param timeout Request timeout in milliseconds | ||
|  |    */ | ||
|  |   private async callWebhook( | ||
|  |     context: IActionContext, | ||
|  |     method: 'GET' | 'POST', | ||
|  |     timeout: number, | ||
|  |   ): Promise<void> { | ||
|  |     const payload: any = { | ||
|  |       upsId: context.upsId, | ||
|  |       upsName: context.upsName, | ||
|  |       powerStatus: context.powerStatus, | ||
|  |       batteryCapacity: context.batteryCapacity, | ||
|  |       batteryRuntime: context.batteryRuntime, | ||
|  |       triggerReason: context.triggerReason, | ||
|  |       timestamp: context.timestamp, | ||
|  |     }; | ||
|  | 
 | ||
|  |     // Include action's own thresholds if configured
 | ||
|  |     if (this.config.thresholds) { | ||
|  |       payload.thresholds = { | ||
|  |         battery: this.config.thresholds.battery, | ||
|  |         runtime: this.config.thresholds.runtime, | ||
|  |       }; | ||
|  |     } | ||
|  | 
 | ||
|  |     const url = new URL(this.config.webhookUrl!); | ||
|  | 
 | ||
|  |     if (method === 'GET') { | ||
|  |       // Append payload as query parameters for GET
 | ||
|  |       url.searchParams.append('upsId', payload.upsId); | ||
|  |       url.searchParams.append('upsName', payload.upsName); | ||
|  |       url.searchParams.append('powerStatus', payload.powerStatus); | ||
|  |       url.searchParams.append('batteryCapacity', String(payload.batteryCapacity)); | ||
|  |       url.searchParams.append('batteryRuntime', String(payload.batteryRuntime)); | ||
|  |        | ||
|  |       url.searchParams.append('triggerReason', payload.triggerReason); | ||
|  |       url.searchParams.append('timestamp', String(payload.timestamp)); | ||
|  |     } | ||
|  | 
 | ||
|  |     return new Promise((resolve, reject) => { | ||
|  |       const protocol = url.protocol === 'https:' ? https : http; | ||
|  | 
 | ||
|  |       const options: http.RequestOptions = { | ||
|  |         method, | ||
|  |         headers: method === 'POST' | ||
|  |           ? { | ||
|  |             'Content-Type': 'application/json', | ||
|  |             'User-Agent': 'nupst', | ||
|  |           } | ||
|  |           : { | ||
|  |             'User-Agent': 'nupst', | ||
|  |           }, | ||
|  |         timeout, | ||
|  |       }; | ||
|  | 
 | ||
|  |       const req = protocol.request(url, options, (res) => { | ||
|  |         let data = ''; | ||
|  | 
 | ||
|  |         res.on('data', (chunk) => { | ||
|  |           data += chunk; | ||
|  |         }); | ||
|  | 
 | ||
|  |         res.on('end', () => { | ||
|  |           if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) { | ||
|  |             logger.dim(`Webhook response (${res.statusCode}): ${data.substring(0, 100)}`); | ||
|  |             resolve(); | ||
|  |           } else { | ||
|  |             reject(new Error(`Webhook returned status ${res.statusCode}`)); | ||
|  |           } | ||
|  |         }); | ||
|  |       }); | ||
|  | 
 | ||
|  |       req.on('error', (error) => { | ||
|  |         reject(error); | ||
|  |       }); | ||
|  | 
 | ||
|  |       req.on('timeout', () => { | ||
|  |         req.destroy(); | ||
|  |         reject(new Error(`Webhook request timed out after ${timeout}ms`)); | ||
|  |       }); | ||
|  | 
 | ||
|  |       // Send POST data if applicable
 | ||
|  |       if (method === 'POST') { | ||
|  |         req.write(JSON.stringify(payload)); | ||
|  |       } | ||
|  | 
 | ||
|  |       req.end(); | ||
|  |     }); | ||
|  |   } | ||
|  | } |