@@ -16,6 +16,18 @@ const WILDCARD = '*';
1616const OBJECT_ID_PATTERN = / ^ [ 0 - 9 a - f A - F ] { 24 } $ / ;
1717const MAX_NAME_LENGTH = 80 ;
1818
19+ // Outbound payload shapes. 'json' ships the raw envelope; 'slack' and
20+ // 'discord' render the event into that platform's incoming-webhook format.
21+ const WEBHOOK_FORMATS = Object . freeze ( [ 'json' , 'slack' , 'discord' ] ) ;
22+
23+ const EVENT_LABELS = Object . freeze ( {
24+ 'task.created' : 'Task created' ,
25+ 'task.updated' : 'Task updated' ,
26+ 'task.deleted' : 'Task deleted' ,
27+ 'task.archived' : 'Task archived' ,
28+ 'task.restored' : 'Task restored' ,
29+ } ) ;
30+
1931const isObjectIdString = ( id ) => OBJECT_ID_PATTERN . test ( String ( id || '' ) ) ;
2032
2133const isValidUrl = ( value ) => {
@@ -28,7 +40,7 @@ const isValidUrl = (value) => {
2840} ;
2941
3042/* Validate create/update input. Returns { valid, reason }. */
31- const validateWebhookInput = ( { name, url, events } ) => {
43+ const validateWebhookInput = ( { name, url, events, format } ) => {
3244 if ( ! name || ! String ( name ) . trim ( ) || String ( name ) . length > MAX_NAME_LENGTH ) {
3345 return { valid : false , reason : `A name up to ${ MAX_NAME_LENGTH } characters is required.` } ;
3446 }
@@ -42,9 +54,52 @@ const validateWebhookInput = ({ name, url, events }) => {
4254 if ( unknown . length ) {
4355 return { valid : false , reason : `Unknown events: ${ unknown . join ( ', ' ) } .` } ;
4456 }
57+ if ( format !== undefined && ! WEBHOOK_FORMATS . includes ( String ( format ) ) ) {
58+ return { valid : false , reason : `format must be one of: ${ WEBHOOK_FORMATS . join ( ', ' ) } .` } ;
59+ }
4560 return { valid : true , reason : '' } ;
4661} ;
4762
63+ const normalizeFormat = ( format ) => ( WEBHOOK_FORMATS . includes ( String ( format ) ) ? String ( format ) : 'json' ) ;
64+
65+ // Best-effort human status label from the trimmed task (status may be an
66+ // object, a string, or absent).
67+ const statusLabel = ( task ) => {
68+ const s = task . status ;
69+ return String ( ( s && ( s . text || s . statusName || s . name ) ) || task . statusType || '—' ) ;
70+ } ;
71+
72+ /* Render the canonical delivery body into the target platform's shape.
73+ * Slack expects { text, blocks }; Discord expects { embeds }. 'json' (and
74+ * anything unknown) ships the raw envelope unchanged. Pure — no I/O. */
75+ const formatForTarget = ( format , body ) => {
76+ const fmt = normalizeFormat ( format ) ;
77+ if ( fmt === 'json' ) return body ;
78+
79+ const task = body . data || { } ;
80+ const label = EVENT_LABELS [ body . event ] || body . event ;
81+ const title = `${ task . TaskKey ? task . TaskKey + ' — ' : '' } ${ task . TaskName || 'Task' } ` ;
82+ const meta = `Status: ${ statusLabel ( task ) } · Priority: ${ task . Task_Priority || '—' } ${ task . DueDate ? ' · Due: ' + task . DueDate : '' } ` ;
83+
84+ if ( fmt === 'slack' ) {
85+ return {
86+ text : `${ label } : ${ title } ` ,
87+ blocks : [
88+ { type : 'section' , text : { type : 'mrkdwn' , text : `*${ label } *\n*${ task . TaskKey || '' } * ${ task . TaskName || '' } ` . trim ( ) } } ,
89+ { type : 'context' , elements : [ { type : 'mrkdwn' , text : meta } ] } ,
90+ ] ,
91+ } ;
92+ }
93+
94+ // discord
95+ const fields = [
96+ { name : 'Status' , value : statusLabel ( task ) , inline : true } ,
97+ { name : 'Priority' , value : String ( task . Task_Priority || '—' ) , inline : true } ,
98+ ] ;
99+ if ( task . DueDate ) fields . push ( { name : 'Due' , value : String ( task . DueDate ) , inline : true } ) ;
100+ return { embeds : [ { title, description : label , fields } ] } ;
101+ } ;
102+
48103/* Does a webhook's subscription cover this event? */
49104const subscribesTo = ( webhook , event ) => {
50105 const events = webhook ?. events || [ ] ;
@@ -96,6 +151,7 @@ const generateSecret = () => crypto.randomBytes(24).toString('hex');
96151module . exports = {
97152 EVENT_TYPES ,
98153 WILDCARD ,
154+ WEBHOOK_FORMATS ,
99155 isObjectIdString,
100156 isValidUrl,
101157 validateWebhookInput,
@@ -104,4 +160,6 @@ module.exports = {
104160 trimTaskForDelivery,
105161 signPayload,
106162 generateSecret,
163+ normalizeFormat,
164+ formatForTarget,
107165} ;
0 commit comments