@@ -15,6 +15,7 @@ import { GenericWebhookEventResult } from "../generic/types";
1515import { StatusCodes } from "http-status-codes" ;
1616import { IBridgeStorageProvider } from "../Stores/StorageProvider" ;
1717import { formatDuration , isMatch , millisecondsToHours } from "date-fns" ;
18+ import { ExecuteResultContent , ExecuteResultWebhookResponse , WebhookTransformer } from "../generic/transformer" ;
1819
1920export interface GenericHookConnectionState extends IConnectionState {
2021 /**
@@ -63,21 +64,6 @@ export interface GenericHookAccountData {
6364 [ hookId : string ] : string ;
6465}
6566
66- export interface WebhookResponse {
67- body : string ;
68- contentType ?: string ;
69- statusCode ?: number ;
70- }
71-
72- interface WebhookTransformationResult {
73- version : string ;
74- plain ?: string ;
75- html ?: string ;
76- msgtype ?: string ;
77- empty ?: boolean ;
78- webhookResponse ?: WebhookResponse ;
79- }
80-
8167export interface GenericHookServiceConfig {
8268 userIdPrefix ?: string ;
8369 allowJsTransformationFunctions ?: boolean ,
@@ -89,7 +75,6 @@ export interface GenericHookServiceConfig {
8975const log = new Logger ( "GenericHookConnection" ) ;
9076const md = new markdownit ( ) ;
9177
92- const TRANSFORMATION_TIMEOUT_MS = 500 ;
9378const SANITIZE_MAX_DEPTH = 10 ;
9479const SANITIZE_MAX_BREADTH = 50 ;
9580
@@ -104,12 +89,6 @@ const EXPIRY_NOTICE_MESSAGE = "The webhook **%NAME** will be expiring in %TIME."
10489 */
10590@Connection
10691export class GenericHookConnection extends BaseConnection implements IConnection {
107- private static quickModule ?: QuickJSWASMModule ;
108-
109- public static async initialiseQuickJS ( ) {
110- GenericHookConnection . quickModule = await newQuickJSWASMModule ( ) ;
111- }
112-
11392 /**
11493 * Ensures a JSON payload is compatible with Matrix JSON requirements, such
11594 * as disallowing floating point values.
@@ -164,7 +143,7 @@ export class GenericHookConnection extends BaseConnection implements IConnection
164143 }
165144 // Use !=, not !==, to check for both undefined and null
166145 if ( transformationFunction != undefined ) {
167- if ( ! this . quickModule ) {
146+ if ( ! WebhookTransformer . canTransform ) {
168147 throw new ApiError ( 'Transformation functions are not allowed' , ErrCode . DisabledFeature ) ;
169148 }
170149 if ( typeof transformationFunction !== "string" ) {
@@ -284,7 +263,7 @@ export class GenericHookConnection extends BaseConnection implements IConnection
284263 GenericHookConnection . LegacyCanonicalEventType ,
285264 ] ;
286265
287- private transformationFunction ?: string ;
266+ private webhookTransformer ?: WebhookTransformer ;
288267 private cachedDisplayname ?: string ;
289268 private warnOnExpiryInterval ?: NodeJS . Timeout ;
290269
@@ -303,8 +282,8 @@ export class GenericHookConnection extends BaseConnection implements IConnection
303282 private readonly storage : IBridgeStorageProvider ,
304283 ) {
305284 super ( roomId , stateKey , GenericHookConnection . CanonicalEventType ) ;
306- if ( state . transformationFunction && GenericHookConnection . quickModule ) {
307- this . transformationFunction = state . transformationFunction ;
285+ if ( state . transformationFunction && WebhookTransformer . canTransform ) {
286+ this . webhookTransformer = new WebhookTransformer ( state . transformationFunction ) ;
308287 }
309288 this . handleExpiryTimeUpdate ( false ) . catch ( ex => {
310289 log . warn ( "Failed to configure expiry time warning for hook" , ex ) ;
@@ -372,27 +351,20 @@ export class GenericHookConnection extends BaseConnection implements IConnection
372351 public async onStateUpdate ( stateEv : MatrixEvent < unknown > ) {
373352 const validatedConfig = GenericHookConnection . validateState ( stateEv . content as Record < string , unknown > ) ;
374353 if ( validatedConfig . transformationFunction ) {
375- const ctx = GenericHookConnection . quickModule ! . newContext ( ) ;
376- const codeEvalResult = ctx . evalCode ( `function f(data) {${ validatedConfig . transformationFunction } }` , undefined , { compileOnly : true } ) ;
377- if ( codeEvalResult . error ) {
378- const errorString = JSON . stringify ( ctx . dump ( codeEvalResult . error ) , null , 2 ) ;
379- codeEvalResult . error . dispose ( ) ;
380- ctx . dispose ( ) ;
381-
354+ const error = WebhookTransformer . validateScript ( validatedConfig . transformationFunction ) ;
355+ if ( error ) {
382356 const errorPrefix = "Could not compile transformation function:" ;
383357 await this . intent . sendEvent ( this . roomId , {
384358 msgtype : "m.text" ,
385- body : errorPrefix + "\n\n```json\n\n" + errorString + "\n\n```" ,
386- formatted_body : `<p>${ errorPrefix } </p><p><pre><code class=\\"language-json\\">${ errorString } </code></pre></p>` ,
359+ body : errorPrefix + "\n\n```json\n\n" + error + "\n\n```" ,
360+ formatted_body : `<p>${ errorPrefix } </p><p><pre><code class=\\"language-json\\">${ error } </code></pre></p>` ,
387361 format : "org.matrix.custom.html" ,
388362 } ) ;
389363 } else {
390- codeEvalResult . value . dispose ( ) ;
391- ctx . dispose ( ) ;
392- this . transformationFunction = validatedConfig . transformationFunction ;
364+ this . webhookTransformer = new WebhookTransformer ( validatedConfig . transformationFunction ) ; ;
393365 }
394366 } else {
395- this . transformationFunction = undefined ;
367+ this . webhookTransformer = undefined ;
396368 }
397369
398370 const prevDate = this . state . expirationDate ;
@@ -469,78 +441,6 @@ export class GenericHookConnection extends BaseConnection implements IConnection
469441 return msg ;
470442 }
471443
472- public executeTransformationFunction ( data : unknown ) : { content ?: { plain : string , html ?: string , msgtype ?: string } , webhookResponse ?: WebhookResponse } {
473- if ( ! this . transformationFunction ) {
474- throw Error ( 'Transformation function not defined' ) ;
475- }
476- let result ;
477- const ctx = GenericHookConnection . quickModule ! . newContext ( ) ;
478- ctx . runtime . setInterruptHandler ( shouldInterruptAfterDeadline ( Date . now ( ) + TRANSFORMATION_TIMEOUT_MS ) ) ;
479- try {
480- ctx . setProp ( ctx . global , 'HookshotApiVersion' , ctx . newString ( 'v2' ) ) ;
481- const ctxResult = ctx . evalCode ( `const data = ${ JSON . stringify ( data ) } ;\n(() => { ${ this . state . transformationFunction } })();` ) ;
482-
483- if ( ctxResult . error ) {
484- const e = Error ( `Transformation failed to run: ${ JSON . stringify ( ctx . dump ( ctxResult . error ) ) } ` ) ;
485- ctxResult . error . dispose ( ) ;
486- throw e ;
487- } else {
488- const value = ctx . getProp ( ctx . global , 'result' ) ;
489- result = ctx . dump ( value ) ;
490- value . dispose ( ) ;
491- ctxResult . value . dispose ( ) ;
492- }
493- } finally {
494- ctx . global . dispose ( ) ;
495- ctx . dispose ( ) ;
496- }
497-
498- // Legacy v1 api
499- if ( typeof result === "string" ) {
500- return { content : { plain : `Received webhook: ${ result } ` } } ;
501- } else if ( typeof result !== "object" ) {
502- return { content : { plain : `No content` } } ;
503- }
504- const transformationResult = result as WebhookTransformationResult ;
505- if ( transformationResult . version !== "v2" ) {
506- throw Error ( "Result returned from transformation didn't specify version = v2" ) ;
507- }
508-
509- let content ;
510- if ( ! transformationResult . empty ) {
511- if ( typeof transformationResult . plain !== "string" ) {
512- throw Error ( "Result returned from transformation didn't provide a string value for plain" ) ;
513- }
514- if ( transformationResult . html !== undefined && typeof transformationResult . html !== "string" ) {
515- throw Error ( "Result returned from transformation didn't provide a string value for html" ) ;
516- }
517- if ( transformationResult . msgtype !== undefined && typeof transformationResult . msgtype !== "string" ) {
518- throw Error ( "Result returned from transformation didn't provide a string value for msgtype" ) ;
519- }
520- content = {
521- plain : transformationResult . plain ,
522- html : transformationResult . html ,
523- msgtype : transformationResult . msgtype ,
524- } ;
525- }
526-
527- if ( transformationResult . webhookResponse ) {
528- if ( typeof transformationResult . webhookResponse . body !== "string" ) {
529- throw Error ( "Result returned from transformation didn't provide a string value for webhookResponse.body" ) ;
530- }
531- if ( transformationResult . webhookResponse . statusCode !== undefined && typeof transformationResult . webhookResponse . statusCode !== "number" && Number . isInteger ( transformationResult . webhookResponse . statusCode ) ) {
532- throw Error ( "Result returned from transformation didn't provide a number value for webhookResponse.statusCode" ) ;
533- }
534- if ( transformationResult . webhookResponse . contentType !== undefined && typeof transformationResult . webhookResponse . contentType !== "string" ) {
535- throw Error ( "Result returned from transformation didn't provide a contentType value for msgtype" ) ;
536- }
537- }
538-
539- return {
540- content,
541- webhookResponse : transformationResult . webhookResponse ,
542- }
543- }
544444
545445 /**
546446 * Processes an incoming generic hook
@@ -559,21 +459,21 @@ export class GenericHookConnection extends BaseConnection implements IConnection
559459 } ;
560460 }
561461
562- let content : { plain : string , html ?: string , msgtype ?: string } | undefined ;
563- let webhookResponse : WebhookResponse | undefined ;
462+ let content : ExecuteResultContent | undefined ;
463+ let webhookResponse : ExecuteResultWebhookResponse | undefined ;
564464 let successful = true ;
565- if ( ! this . transformationFunction ) {
566- content = this . transformHookData ( data ) ;
567- } else {
465+ if ( this . webhookTransformer ) {
568466 try {
569- const result = this . executeTransformationFunction ( data ) ;
467+ const result = this . webhookTransformer . execute ( data ) ;
570468 content = result . content ;
571469 webhookResponse = result . webhookResponse ;
572470 } catch ( ex ) {
573471 log . warn ( `Failed to run transformation function` , ex ) ;
574472 content = { plain : `Webhook received but failed to process via transformation function` } ;
575473 successful = false ;
576474 }
475+ } else {
476+ content = this . transformHookData ( data ) ;
577477 }
578478
579479 if ( content ) {
@@ -591,6 +491,7 @@ export class GenericHookConnection extends BaseConnection implements IConnection
591491 body : content . plain ,
592492 // render can output redundant trailing newlines, so trim it.
593493 formatted_body : content . html || md . render ( content . plain ) . trim ( ) ,
494+ ...( content . mentions ? { "m.mentions" : content . mentions } : undefined ) ,
594495 format : "org.matrix.custom.html" ,
595496 "uk.half-shot.hookshot.webhook_data" : safeData ,
596497 } , 'm.room.message' , sender ) ;
0 commit comments