mirror of
https://github.com/matrix-org/matrix-hookshot.git
synced 2025-03-10 13:17:08 +00:00
Store generic hook requests
This commit is contained in:
parent
cbc7718808
commit
f7bc38d941
@ -564,12 +564,12 @@ export class Bridge {
|
||||
try {
|
||||
// TODO: Support webhook responses to more than one room
|
||||
if (index !== 0) {
|
||||
await c.onGenericHook(data.hookData);
|
||||
await c.onGenericHook(data);
|
||||
return;
|
||||
}
|
||||
let successful: boolean|null = null;
|
||||
if (this.config.generic?.waitForComplete) {
|
||||
successful = await c.onGenericHook(data.hookData);
|
||||
successful = await c.onGenericHook(data);
|
||||
}
|
||||
await this.queue.push<GenericWebhookEventResult>({
|
||||
data: {successful},
|
||||
@ -579,7 +579,7 @@ export class Bridge {
|
||||
});
|
||||
didPush = true;
|
||||
if (!this.config.generic?.waitForComplete) {
|
||||
await c.onGenericHook(data.hookData);
|
||||
await c.onGenericHook(data);
|
||||
}
|
||||
}
|
||||
catch (ex) {
|
||||
|
@ -10,6 +10,7 @@ import { ApiError, ErrCode } from "../api";
|
||||
import { BaseConnection } from "./BaseConnection";
|
||||
import { GetConnectionsResponseItem } from "../provisioning/api";
|
||||
import { BridgeConfigGenericWebhooks } from "../Config/Config";
|
||||
import { GenericWebhookEvent } from "../generic/types";
|
||||
|
||||
export interface GenericHookConnectionState extends IConnectionState {
|
||||
/**
|
||||
@ -32,6 +33,10 @@ export interface GenericHookSecrets {
|
||||
* The hookId of the webhook.
|
||||
*/
|
||||
hookId: string;
|
||||
/**
|
||||
* The results of webhook actions.
|
||||
*/
|
||||
lastResults: Array<LastResult>;
|
||||
}
|
||||
|
||||
export type GenericHookResponseItem = GetConnectionsResponseItem<GenericHookConnectionState, GenericHookSecrets>;
|
||||
@ -52,12 +57,33 @@ interface WebhookTransformationResult {
|
||||
empty?: boolean;
|
||||
}
|
||||
|
||||
interface TransformationResult {
|
||||
logs: string;
|
||||
content: {
|
||||
plain: string;
|
||||
msgtype?: string;
|
||||
html?: string;
|
||||
}|null,
|
||||
}
|
||||
|
||||
export interface LastResult {
|
||||
timestamp: number;
|
||||
metadata: {
|
||||
userAgent?: string;
|
||||
contentType?: string;
|
||||
};
|
||||
logs?: string;
|
||||
ok: boolean;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
const log = new LogWrapper("GenericHookConnection");
|
||||
const md = new markdownit();
|
||||
|
||||
const TRANSFORMATION_TIMEOUT_MS = 500;
|
||||
const SANITIZE_MAX_DEPTH = 5;
|
||||
const SANITIZE_MAX_BREADTH = 25;
|
||||
const MAX_LAST_RESULT_ITEMS = 5;
|
||||
|
||||
/**
|
||||
* Handles rooms connected to a generic webhook.
|
||||
@ -192,6 +218,7 @@ export class GenericHookConnection extends BaseConnection implements IConnection
|
||||
|
||||
private transformationFunction?: Script;
|
||||
private cachedDisplayname?: string;
|
||||
private readonly lastResults = new Array<LastResult>();
|
||||
constructor(roomId: string,
|
||||
private state: GenericHookConnectionState,
|
||||
public readonly hookId: string,
|
||||
@ -293,27 +320,50 @@ export class GenericHookConnection extends BaseConnection implements IConnection
|
||||
return msg;
|
||||
}
|
||||
|
||||
public executeTransformationFunction(data: unknown): {plain: string, html?: string, msgtype?: string}|null {
|
||||
public executeTransformationFunction(data: unknown): TransformationResult {
|
||||
if (!this.transformationFunction) {
|
||||
throw Error('Transformation function not defined');
|
||||
}
|
||||
let logs = "";
|
||||
const vm = new NodeVM({
|
||||
console: 'off',
|
||||
console: 'redirect',
|
||||
wrapper: 'none',
|
||||
wasm: false,
|
||||
eval: false,
|
||||
timeout: TRANSFORMATION_TIMEOUT_MS,
|
||||
});
|
||||
vm.setGlobal('HookshotApiVersion', 'v2');
|
||||
vm.setGlobal('data', data);
|
||||
})
|
||||
.setGlobal('HookshotApiVersion', 'v2')
|
||||
.setGlobal('data', data)
|
||||
.on('console.log', (msg, ...args) => {
|
||||
logs += `\n${msg} ${args.join(' ')}`;
|
||||
})
|
||||
.on('console.info', (msg, ...args) => {
|
||||
logs += `\ninfo: ${msg} ${args.join(' ')}`;
|
||||
})
|
||||
.on('console.warn', (msg, ...args) => {
|
||||
logs += `\nwarn: ${msg} ${args.join(' ')}`;
|
||||
})
|
||||
.on('console.error', (msg, ...args) => {
|
||||
logs += `\nerror: ${msg} ${args.join(' ')}`;
|
||||
})
|
||||
vm.run(this.transformationFunction);
|
||||
const result = vm.getGlobal('result');
|
||||
|
||||
// Legacy v1 api
|
||||
if (typeof result === "string") {
|
||||
return {plain: `Received webhook: ${result}`};
|
||||
return {
|
||||
content: {
|
||||
plain: `Received webhook: ${result}`
|
||||
},
|
||||
logs
|
||||
};
|
||||
} else if (typeof result !== "object") {
|
||||
return {plain: `No content`};
|
||||
return {
|
||||
content: {
|
||||
plain: "No content"
|
||||
},
|
||||
logs
|
||||
};
|
||||
}
|
||||
const transformationResult = result as WebhookTransformationResult;
|
||||
if (transformationResult.version !== "v2") {
|
||||
@ -321,7 +371,7 @@ export class GenericHookConnection extends BaseConnection implements IConnection
|
||||
}
|
||||
|
||||
if (transformationResult.empty) {
|
||||
return null; // No-op
|
||||
return { logs, content: null }; // No-op
|
||||
}
|
||||
|
||||
const plain = transformationResult.plain;
|
||||
@ -336,10 +386,18 @@ export class GenericHookConnection extends BaseConnection implements IConnection
|
||||
}
|
||||
|
||||
return {
|
||||
plain: plain,
|
||||
html: transformationResult.html,
|
||||
msgtype: transformationResult.msgtype,
|
||||
}
|
||||
content: {
|
||||
plain,
|
||||
html: transformationResult.html,
|
||||
msgtype: transformationResult.msgtype,
|
||||
},
|
||||
logs
|
||||
};
|
||||
}
|
||||
|
||||
public addLastResult(result: LastResult) {
|
||||
this.lastResults.unshift(result);
|
||||
this.lastResults.splice(MAX_LAST_RESULT_ITEMS-1, 1);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -347,22 +405,38 @@ export class GenericHookConnection extends BaseConnection implements IConnection
|
||||
* @param data Structured data. This may either be a string, or an object.
|
||||
* @returns `true` if the webhook completed, or `false` if it failed to complete
|
||||
*/
|
||||
public async onGenericHook(data: unknown): Promise<boolean> {
|
||||
public async onGenericHook(event: GenericWebhookEvent): Promise<boolean> {
|
||||
const data = event.hookData;
|
||||
log.info(`onGenericHook ${this.roomId} ${this.hookId}`);
|
||||
let content: {plain: string, html?: string, msgtype?: string};
|
||||
let success = true;
|
||||
let functionResult: TransformationResult|undefined;
|
||||
let error: string|undefined;
|
||||
|
||||
// Matrix cannot handle float data, so make sure we parse out any floats.
|
||||
const safeData = GenericHookConnection.sanitiseObjectForMatrixJSON(data);
|
||||
if (!this.transformationFunction) {
|
||||
content = this.transformHookData(data);
|
||||
} else {
|
||||
try {
|
||||
const potentialContent = this.executeTransformationFunction(data);
|
||||
if (potentialContent === null) {
|
||||
functionResult = this.executeTransformationFunction(data);
|
||||
if (functionResult.content === null) {
|
||||
// Explitly no action
|
||||
this.addLastResult({
|
||||
ok: success,
|
||||
timestamp: Date.now(),
|
||||
logs: functionResult.logs,
|
||||
metadata: {
|
||||
userAgent: event.userAgent,
|
||||
contentType: event.contentType,
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
content = potentialContent;
|
||||
content = functionResult.content;
|
||||
} catch (ex) {
|
||||
log.warn(`Failed to run transformation function`, ex);
|
||||
error = ex.message;
|
||||
content = {plain: `Webhook received but failed to process via transformation function`};
|
||||
success = false;
|
||||
}
|
||||
@ -370,9 +444,6 @@ export class GenericHookConnection extends BaseConnection implements IConnection
|
||||
|
||||
const sender = this.getUserId();
|
||||
await this.ensureDisplayname();
|
||||
|
||||
// Matrix cannot handle float data, so make sure we parse out any floats.
|
||||
const safeData = GenericHookConnection.sanitiseObjectForMatrixJSON(data);
|
||||
|
||||
await this.messageClient.sendMatrixMessage(this.roomId, {
|
||||
msgtype: content.msgtype || "m.notice",
|
||||
@ -382,8 +453,17 @@ export class GenericHookConnection extends BaseConnection implements IConnection
|
||||
format: "org.matrix.custom.html",
|
||||
"uk.half-shot.hookshot.webhook_data": safeData,
|
||||
}, 'm.room.message', sender);
|
||||
this.addLastResult({
|
||||
ok: success,
|
||||
timestamp: Date.now(),
|
||||
logs: functionResult?.logs,
|
||||
error,
|
||||
metadata: {
|
||||
userAgent: event.userAgent,
|
||||
contentType: event.contentType,
|
||||
}
|
||||
});
|
||||
return success;
|
||||
|
||||
}
|
||||
|
||||
public static getProvisionerDetails(botUserId: string) {
|
||||
@ -407,6 +487,7 @@ export class GenericHookConnection extends BaseConnection implements IConnection
|
||||
...(showSecrets ? { secrets: {
|
||||
url: new URL(this.hookId, this.config.parsedUrlPrefix),
|
||||
hookId: this.hookId,
|
||||
lastResults: this.lastResults,
|
||||
} as GenericHookSecrets} : undefined)
|
||||
}
|
||||
}
|
||||
|
@ -33,6 +33,8 @@ export class GenericWebhooksRouter {
|
||||
data: {
|
||||
hookData: body,
|
||||
hookId: req.params.hookId,
|
||||
userAgent: req.headers["user-agent"],
|
||||
contentType: req.headers["content-type"],
|
||||
},
|
||||
}, WEBHOOK_RESPONSE_TIMEOUT).then((response) => {
|
||||
if (response.notFound) {
|
||||
|
@ -1,6 +1,8 @@
|
||||
export interface GenericWebhookEvent {
|
||||
hookData: unknown;
|
||||
hookId: string;
|
||||
userAgent?: string;
|
||||
contentType?: string;
|
||||
}
|
||||
|
||||
export interface GenericWebhookEventResult {
|
||||
|
Loading…
x
Reference in New Issue
Block a user