mirror of
https://github.com/matrix-org/matrix-hookshot.git
synced 2025-03-10 21:19:13 +00:00
Merge pull request #77 from Half-Shot/hs/add-support-for-generic-webhook
Add support for handling generic webhooks
This commit is contained in:
commit
a959ce66b7
@ -9,7 +9,7 @@ bridge:
|
||||
port: 9993
|
||||
bindAddress: 127.0.0.1
|
||||
github:
|
||||
# (Optional) Configure this to enable support for GitHub
|
||||
# (Optional) Configure this to enable GitHub support
|
||||
#
|
||||
installationId: 6854059
|
||||
auth:
|
||||
@ -22,13 +22,23 @@ github:
|
||||
webhook:
|
||||
secret: secrettoken
|
||||
gitlab:
|
||||
# (Optional) Configure this to enable support for GitLab
|
||||
# (Optional) Configure this to enable GitLab support
|
||||
#
|
||||
instances:
|
||||
gitlab.com:
|
||||
url: https://gitlab.com
|
||||
webhook:
|
||||
secret: secrettoken
|
||||
jira:
|
||||
# (Optional) Configure this to enable Jira support
|
||||
#
|
||||
webhook:
|
||||
secret: secrettoken
|
||||
generic:
|
||||
# (Optional) Support for generic webhook events. `allowJsTransformationFunctions` will allow users to write short transformation snippets in code, and thus is unsafe in untrusted environments
|
||||
#
|
||||
enabled: false
|
||||
allowJsTransformationFunctions: false
|
||||
webhook:
|
||||
# HTTP webhook listener options
|
||||
#
|
||||
|
@ -45,6 +45,11 @@ interface BridgeConfigJira {
|
||||
};
|
||||
}
|
||||
|
||||
interface BridgeGenericWebhooksConfig {
|
||||
enabled: boolean;
|
||||
allowJsTransformationFunctions?: boolean;
|
||||
}
|
||||
|
||||
interface BridgeWidgetConfig {
|
||||
port: number;
|
||||
addToAdminRooms: boolean;
|
||||
@ -95,6 +100,7 @@ interface BridgeConfigRoot {
|
||||
jira?: BridgeConfigJira;
|
||||
bot?: BridgeConfigBot;
|
||||
widgets?: BridgeWidgetConfig;
|
||||
generic?: BridgeGenericWebhooksConfig;
|
||||
}
|
||||
|
||||
export class BridgeConfig {
|
||||
@ -113,8 +119,10 @@ export class BridgeConfig {
|
||||
public readonly github?: BridgeConfigGitHub;
|
||||
@configKey("Configure this to enable GitLab support", true)
|
||||
public readonly gitlab?: BridgeConfigGitLab;
|
||||
@configKey("Configure this to enable Jira support")
|
||||
@configKey("Configure this to enable Jira support", true)
|
||||
public readonly jira?: BridgeConfigJira;
|
||||
@configKey("Support for generic webhook events. `allowJsTransformationFunctions` will allow users to write short transformation snippets in code, and thus is unsafe in untrusted environments", true)
|
||||
public readonly generic?: BridgeGenericWebhooksConfig;
|
||||
@configKey("Define profile information for the bot user", true)
|
||||
public readonly bot?: BridgeConfigBot;
|
||||
@configKey("EXPERIMENTAL support for complimentary widgets", true)
|
||||
@ -132,6 +140,7 @@ export class BridgeConfig {
|
||||
}
|
||||
this.gitlab = configData.gitlab;
|
||||
this.jira = configData.jira;
|
||||
this.generic = configData.generic;
|
||||
this.webhook = configData.webhook;
|
||||
this.passFile = configData.passFile;
|
||||
assert.ok(this.webhook);
|
||||
|
@ -58,6 +58,15 @@ const DefaultConfig = new BridgeConfig({
|
||||
webhook: {
|
||||
secret: "secrettoken",
|
||||
}
|
||||
},
|
||||
jira: {
|
||||
webhook: {
|
||||
secret: 'secrettoken'
|
||||
}
|
||||
},
|
||||
generic: {
|
||||
enabled: false,
|
||||
allowJsTransformationFunctions: false,
|
||||
}
|
||||
}, {});
|
||||
|
||||
|
95
src/Connections/GenericHook.ts
Normal file
95
src/Connections/GenericHook.ts
Normal file
@ -0,0 +1,95 @@
|
||||
import { IConnection } from "./IConnection";
|
||||
import LogWrapper from "../LogWrapper";
|
||||
import { MessageSenderClient } from "../MatrixSender"
|
||||
import markdownit from "markdown-it";
|
||||
import { Script, createContext } from "vm";
|
||||
import { MatrixEvent } from "../MatrixEvent";
|
||||
|
||||
export interface GenericHookConnectionState {
|
||||
hookId: string;
|
||||
transformationFunction?: string;
|
||||
}
|
||||
|
||||
const log = new LogWrapper("GenericHookConnection");
|
||||
const md = new markdownit();
|
||||
|
||||
const TRANSFORMATION_TIMEOUT_MS = 2000;
|
||||
|
||||
/**
|
||||
* Handles rooms connected to a github repo.
|
||||
*/
|
||||
export class GenericHookConnection implements IConnection {
|
||||
static readonly CanonicalEventType = "uk.half-shot.matrix-github.generic.hook";
|
||||
|
||||
static readonly EventTypes = [
|
||||
GenericHookConnection.CanonicalEventType,
|
||||
];
|
||||
|
||||
public get hookId() {
|
||||
return this.state.hookId;
|
||||
}
|
||||
|
||||
private transformationFunction?: Script;
|
||||
|
||||
constructor(public readonly roomId: string,
|
||||
private state: GenericHookConnectionState,
|
||||
private readonly stateKey: string,
|
||||
private messageClient: MessageSenderClient,
|
||||
private readonly allowJSTransformation: boolean = false) {
|
||||
if (state.transformationFunction && allowJSTransformation) {
|
||||
this.transformationFunction = new Script(state.transformationFunction);
|
||||
}
|
||||
}
|
||||
|
||||
public isInterestedInStateEvent(eventType: string, stateKey: string) {
|
||||
return GenericHookConnection.EventTypes.includes(eventType) && this.stateKey === stateKey;
|
||||
}
|
||||
|
||||
public async onStateUpdate(stateEv: MatrixEvent<unknown>) {
|
||||
const state = stateEv.content as GenericHookConnectionState;
|
||||
if (state.transformationFunction && this.allowJSTransformation) {
|
||||
try {
|
||||
this.transformationFunction = new Script(state.transformationFunction);
|
||||
} catch (ex) {
|
||||
await this.messageClient.sendMatrixText(this.roomId, 'Could not compile transformation function:' + ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async onGenericHook(data: Record<string, unknown>) {
|
||||
log.info(`onGenericHook ${this.roomId} ${this.hookId}`);
|
||||
let content: string;
|
||||
if (!this.transformationFunction) {
|
||||
content = `Recieved webhook data:\n\n\`\`\`${JSON.stringify(data)}\`\`\``;
|
||||
} else {
|
||||
try {
|
||||
const context = createContext({data});
|
||||
this.transformationFunction.runInContext(context, {
|
||||
timeout: TRANSFORMATION_TIMEOUT_MS,
|
||||
breakOnSigint: true,
|
||||
filename: `generic-hook.${this.hookId}`,
|
||||
});
|
||||
if (context.result) {
|
||||
content = `Recieved webhook: ${context.result}`;
|
||||
} else {
|
||||
content = `No content`;
|
||||
}
|
||||
} catch (ex) {
|
||||
content = `Webhook recieved but failed to process via transformation function`;
|
||||
}
|
||||
}
|
||||
|
||||
return this.messageClient.sendMatrixMessage(this.roomId, {
|
||||
msgtype: "m.notice",
|
||||
body: content,
|
||||
formatted_body: md.renderInline(content),
|
||||
format: "org.matrix.custom.html",
|
||||
"uk.half-shot.webhook_data": data,
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
public toString() {
|
||||
return `GenericHookConnection ${this.hookId}`;
|
||||
}
|
||||
}
|
@ -316,7 +316,7 @@ export class GitHubIssueConnection implements IConnection {
|
||||
}
|
||||
}
|
||||
|
||||
public onIssueStateChange(data?: any) {
|
||||
public onIssueStateChange(data: unknown) {
|
||||
return this.syncIssueState();
|
||||
}
|
||||
|
||||
|
@ -288,7 +288,7 @@ export class GitHubRepoConnection implements IConnection {
|
||||
}
|
||||
const orgRepoName = event.repository.full_name;
|
||||
|
||||
const content = emoji.emojify(`${event.issue.user?.login} created new issue [${orgRepoName}#${event.issue.number}](${event.issue}): "${event.issue.title}"`);
|
||||
const content = emoji.emojify(`${event.issue.user?.login} created new issue [${orgRepoName}#${event.issue.number}](${event.issue.html_url}): "${event.issue.title}"`);
|
||||
const { labelsHtml, labelsStr } = FormatUtil.formatLabels(event.issue.labels);
|
||||
await this.as.botIntent.sendEvent(this.roomId, {
|
||||
msgtype: "m.notice",
|
||||
|
@ -55,7 +55,8 @@ export class GitHubUserSpace implements IConnection {
|
||||
throw Error("Could not find repo");
|
||||
}
|
||||
|
||||
let avatarState: any|undefined;
|
||||
// eslint-disable-next-line camelcase
|
||||
let avatarState: {type: "m.room.avatar", state_key: "", content: { url: string}}|undefined;
|
||||
try {
|
||||
if (avatarUrl) {
|
||||
const res = await axios.get(avatarUrl, {
|
||||
|
@ -131,14 +131,6 @@ export class GitLabRepoConnection implements IConnection {
|
||||
});
|
||||
}
|
||||
|
||||
// public async onIssueCreated(event: IGitHubWebhookEvent) {
|
||||
|
||||
// }
|
||||
|
||||
// public async onIssueStateChange(event: IGitHubWebhookEvent) {
|
||||
|
||||
// }
|
||||
|
||||
public toString() {
|
||||
return `GitHubRepo`;
|
||||
}
|
||||
|
@ -1,36 +1,36 @@
|
||||
import { AdminRoom, BRIDGE_ROOM_TYPE, AdminAccountData } from "./AdminRoom";
|
||||
import { Appservice, IAppserviceRegistration, RichRepliesPreprocessor, IRichReplyMetadata, StateEvent, PantalaimonClient, MatrixClient } from "matrix-bot-sdk";
|
||||
import { BridgeConfig, GitLabInstance } from "./Config/Config";
|
||||
import { OAuthRequest, OAuthTokens, NotificationsEnableEvent, NotificationsDisableEvent,} from "./Webhooks";
|
||||
import { BridgeWidgetApi } from "./Widgets/BridgeWidgetApi";
|
||||
import { CommentProcessor } from "./CommentProcessor";
|
||||
import { MessageQueue, createMessageQueue } from "./MessageQueue/MessageQueue";
|
||||
import { AdminRoom, BRIDGE_ROOM_TYPE, AdminAccountData } from "./AdminRoom";
|
||||
import { UserTokenStore } from "./UserTokenStore";
|
||||
import { MatrixEvent, MatrixMemberContent, MatrixMessageContent } from "./MatrixEvent";
|
||||
import LogWrapper from "./LogWrapper";
|
||||
import { MessageSenderClient } from "./MatrixSender";
|
||||
import { UserNotificationsEvent } from "./Notifications/UserNotificationWatcher";
|
||||
import { RedisStorageProvider } from "./Stores/RedisStorageProvider";
|
||||
import { MemoryStorageProvider } from "./Stores/MemoryStorageProvider";
|
||||
import { NotificationProcessor } from "./NotificationsProcessor";
|
||||
import { IBridgeStorageProvider } from "./Stores/StorageProvider";
|
||||
import { retry } from "./PromiseUtil";
|
||||
import { IConnection, GitHubDiscussionSpace, GitHubDiscussionConnection, GitHubUserSpace
|
||||
} from "./Connections";
|
||||
import { GitHubRepoConnection } from "./Connections/GithubRepo";
|
||||
import { GetIssueResponse, GetIssueOpts } from "./Gitlab/Types"
|
||||
import { GenericHookConnection } from "./Connections/GenericHook";
|
||||
import { GithubInstance } from "./Github/GithubInstance";
|
||||
import { GitHubIssueConnection } from "./Connections/GithubIssue";
|
||||
import { GitHubProjectConnection } from "./Connections/GithubProject";
|
||||
import { GitLabRepoConnection } from "./Connections/GitlabRepo";
|
||||
import { GithubInstance } from "./Github/GithubInstance";
|
||||
import { IGitLabWebhookIssueStateEvent, IGitLabWebhookMREvent, IGitLabWebhookNoteEvent } from "./Gitlab/WebhookTypes";
|
||||
import { GitLabIssueConnection } from "./Connections/GitlabIssue";
|
||||
import { GetIssueResponse, GetIssueOpts } from "./Gitlab/Types"
|
||||
import { GitHubRepoConnection } from "./Connections/GithubRepo";
|
||||
import { GitLabClient } from "./Gitlab/Client";
|
||||
import { BridgeWidgetApi } from "./Widgets/BridgeWidgetApi";
|
||||
import { ProjectsGetResponseData } from "./Github/Types";
|
||||
import { NotifFilter, NotificationFilterStateContent } from "./NotificationFilters";
|
||||
import * as GitHubWebhookTypes from "@octokit/webhooks-types";
|
||||
import { JiraCommentCreatedEvent, JiraIssueEvent } from "./Jira/WebhookTypes";
|
||||
import { GitLabIssueConnection } from "./Connections/GitlabIssue";
|
||||
import { GitLabRepoConnection } from "./Connections/GitlabRepo";
|
||||
import { IBridgeStorageProvider } from "./Stores/StorageProvider";
|
||||
import { IConnection, GitHubDiscussionSpace, GitHubDiscussionConnection, GitHubUserSpace } from "./Connections";
|
||||
import { IGitLabWebhookIssueStateEvent, IGitLabWebhookMREvent, IGitLabWebhookNoteEvent } from "./Gitlab/WebhookTypes";
|
||||
import { JiraIssueEvent } from "./Jira/WebhookTypes";
|
||||
import { JiraProjectConnection } from "./Connections/JiraProject";
|
||||
import { MatrixEvent, MatrixMemberContent, MatrixMessageContent } from "./MatrixEvent";
|
||||
import { MemoryStorageProvider } from "./Stores/MemoryStorageProvider";
|
||||
import { MessageQueue, createMessageQueue } from "./MessageQueue/MessageQueue";
|
||||
import { MessageSenderClient } from "./MatrixSender";
|
||||
import { NotifFilter, NotificationFilterStateContent } from "./NotificationFilters";
|
||||
import { NotificationProcessor } from "./NotificationsProcessor";
|
||||
import { OAuthRequest, OAuthTokens, NotificationsEnableEvent, NotificationsDisableEvent, GenericWebhookEvent,} from "./Webhooks";
|
||||
import { ProjectsGetResponseData } from "./Github/Types";
|
||||
import { RedisStorageProvider } from "./Stores/RedisStorageProvider";
|
||||
import { retry } from "./PromiseUtil";
|
||||
import { UserNotificationsEvent } from "./Notifications/UserNotificationWatcher";
|
||||
import { UserTokenStore } from "./UserTokenStore";
|
||||
import * as GitHubWebhookTypes from "@octokit/webhooks-types";
|
||||
import LogWrapper from "./LogWrapper";
|
||||
|
||||
const log = new LogWrapper("GithubBridge");
|
||||
|
||||
@ -137,6 +137,16 @@ export class GithubBridge {
|
||||
return new JiraProjectConnection(roomId, this.as, state.content, state.stateKey, this.commentProcessor, this.messageClient);
|
||||
}
|
||||
|
||||
if (GenericHookConnection.EventTypes.includes(state.type) && this.config.generic?.enabled) {
|
||||
return new GenericHookConnection(
|
||||
roomId,
|
||||
state.content,
|
||||
state.stateKey,
|
||||
this.messageClient,
|
||||
this.config.generic.allowJsTransformationFunctions
|
||||
);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@ -217,6 +227,11 @@ export class GithubBridge {
|
||||
return this.connections.filter((c) => (c instanceof JiraProjectConnection && c.projectId === projectId)) as JiraProjectConnection[];
|
||||
}
|
||||
|
||||
private getConnectionsForGenericWebhook(hookId: string): GenericHookConnection[] {
|
||||
return this.connections.filter((c) => (c instanceof GenericHookConnection && c.hookId === hookId)) as GenericHookConnection[];
|
||||
}
|
||||
|
||||
|
||||
public stop() {
|
||||
this.as.stop();
|
||||
if(this.queue.stop) this.queue.stop();
|
||||
@ -602,7 +617,6 @@ export class GithubBridge {
|
||||
log.info(`JIRA issue created for project ${data.issue.fields.project.id}, issue id ${data.issue.id}`);
|
||||
const projectId = data.issue.fields.project.id;
|
||||
const connections = this.getConnectionsForJiraProject(projectId);
|
||||
console.log(data.issue.fields.project);
|
||||
|
||||
connections.forEach(async (c) => {
|
||||
try {
|
||||
@ -612,6 +626,19 @@ export class GithubBridge {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
this.queue.on<GenericWebhookEvent>("generic-webhook.event", async ({data}) => {
|
||||
log.info(`Incoming generic hook ${data.hookId}`);
|
||||
const connections = this.getConnectionsForGenericWebhook(data.hookId);
|
||||
|
||||
connections.forEach(async (c) => {
|
||||
try {
|
||||
await c.onGenericHook(data.hookData);
|
||||
} catch (ex) {
|
||||
log.warn(`Failed to handle generic-webhook.event:`, ex);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Fetch all room state
|
||||
let joinedRooms: string[]|undefined;
|
||||
|
@ -11,6 +11,11 @@ import { EmitterWebhookEvent, Webhooks as OctokitWebhooks } from "@octokit/webho
|
||||
import { IJiraWebhookEvent, JiraIssueEvent } from "./Jira/WebhookTypes";
|
||||
const log = new LogWrapper("GithubWebhooks");
|
||||
|
||||
export interface GenericWebhookEvent {
|
||||
hookData: Record<string, unknown>;
|
||||
hookId: string;
|
||||
}
|
||||
|
||||
export interface OAuthRequest {
|
||||
code: string;
|
||||
state: string;
|
||||
@ -55,6 +60,12 @@ export class Webhooks extends EventEmitter {
|
||||
this.ghWebhooks.onAny(e => this.onGitHubPayload(e));
|
||||
}
|
||||
|
||||
this.expressApp.all(
|
||||
'/:hookId',
|
||||
express.json({ type: ['application/json', 'application/x-www-form-urlencoded'] }),
|
||||
this.onGenericPayload.bind(this),
|
||||
);
|
||||
|
||||
this.expressApp.use(express.json({
|
||||
verify: this.verifyRequest.bind(this),
|
||||
}));
|
||||
@ -81,6 +92,7 @@ export class Webhooks extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private onGitLabPayload(body: IGitLabWebhookEvent) {
|
||||
log.info(`onGitLabPayload ${body.event_type}:`, body);
|
||||
if (body.event_type === "merge_request") {
|
||||
@ -118,6 +130,32 @@ export class Webhooks extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
private onGenericPayload(req: Request, res: Response) {
|
||||
if (!['PUT', 'GET', 'POST'].includes(req.method)) {
|
||||
res.sendStatus(400).send({error: 'Wrong METHOD. Expecting PUT,GET,POST'});
|
||||
return;
|
||||
}
|
||||
|
||||
let body;
|
||||
if (req.method === 'GET') {
|
||||
body = req.query;
|
||||
} else {
|
||||
body = req.body;
|
||||
}
|
||||
|
||||
res.sendStatus(200);
|
||||
this.queue.push({
|
||||
eventName: 'generic-webhook.event',
|
||||
sender: "GithubWebhooks",
|
||||
data: {
|
||||
hookData: body,
|
||||
hookId: req.params.hookId,
|
||||
} as GenericWebhookEvent,
|
||||
}).catch((err) => {
|
||||
log.error(`Failed to emit payload: ${err}`);
|
||||
});
|
||||
}
|
||||
|
||||
private onPayload(req: Request, res: Response) {
|
||||
log.info(`New webhook: ${req.url}`);
|
||||
try {
|
||||
@ -236,8 +274,6 @@ export class Webhooks extends EventEmitter {
|
||||
}
|
||||
return true;
|
||||
}
|
||||
console.log(req.body);
|
||||
console.log(req.headers);
|
||||
log.error(`No signature on URL. Rejecting`);
|
||||
res.sendStatus(400);
|
||||
throw Error("Invalid signature.");
|
||||
|
@ -24,7 +24,7 @@ describe("AdminRoom", () => {
|
||||
|
||||
expect(intent.sentEvents[0]).to.deep.equal({
|
||||
roomId: ROOM_ID,
|
||||
content: AdminRoom.helpMessage,
|
||||
content: AdminRoom.helpMessage(),
|
||||
});
|
||||
});
|
||||
})
|
@ -2,6 +2,7 @@ import { FormatUtil } from "../src/FormatUtil";
|
||||
import { expect } from "chai";
|
||||
|
||||
const SIMPLE_ISSUE = {
|
||||
id: 123,
|
||||
number: 123,
|
||||
state: "open",
|
||||
title: "A simple title",
|
||||
@ -12,6 +13,7 @@ const SIMPLE_ISSUE = {
|
||||
};
|
||||
|
||||
const SIMPLE_REPO = {
|
||||
id: 123,
|
||||
description: "A simple description",
|
||||
full_name: "evilcorp/lab",
|
||||
html_url: "https://github.com/evilcorp/lab/issues/123",
|
||||
|
Loading…
x
Reference in New Issue
Block a user