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