mirror of
https://github.com/turt2live/matrix-dimension.git
synced 2024-10-01 01:05:53 -04:00
Majority of a matrix-hookshot#generic_webhook implementation
This commit is contained in:
parent
ca7f1fbbe6
commit
2a41474094
@ -52,6 +52,7 @@ export const CACHE_IRC_BRIDGE = "irc-bridge";
|
|||||||
export const CACHE_STICKERS = "stickers";
|
export const CACHE_STICKERS = "stickers";
|
||||||
export const CACHE_TELEGRAM_BRIDGE = "telegram-bridge";
|
export const CACHE_TELEGRAM_BRIDGE = "telegram-bridge";
|
||||||
export const CACHE_HOOKSHOT_GITHUB_BRIDGE = "hookshot-github-bridge";
|
export const CACHE_HOOKSHOT_GITHUB_BRIDGE = "hookshot-github-bridge";
|
||||||
|
export const CACHE_HOOKSHOT_WEBHOOK_BRIDGE = "hookshot-webhook-bridge";
|
||||||
export const CACHE_HOOKSHOT_JIRA_BRIDGE = "hookshot-jira-bridge";
|
export const CACHE_HOOKSHOT_JIRA_BRIDGE = "hookshot-jira-bridge";
|
||||||
export const CACHE_WEBHOOKS_BRIDGE = "webhooks-bridge";
|
export const CACHE_WEBHOOKS_BRIDGE = "webhooks-bridge";
|
||||||
export const CACHE_SIMPLE_BOTS = "simple-bots";
|
export const CACHE_SIMPLE_BOTS = "simple-bots";
|
||||||
|
111
src/api/admin/AdminHookshotWebhookService.ts
Normal file
111
src/api/admin/AdminHookshotWebhookService.ts
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
import { Context, GET, Path, PathParam, POST, QueryParam, Security, ServiceContext } from "typescript-rest";
|
||||||
|
import { Cache, CACHE_HOOKSHOT_WEBHOOK_BRIDGE, CACHE_INTEGRATIONS } from "../../MemoryCache";
|
||||||
|
import { LogService } from "matrix-bot-sdk";
|
||||||
|
import { ApiError } from "../ApiError";
|
||||||
|
import { ROLE_ADMIN, ROLE_USER } from "../security/MatrixSecurity";
|
||||||
|
import HookshotGithubBridgeRecord from "../../db/models/HookshotGithubBridgeRecord";
|
||||||
|
import HookshotWebhookBridgeRecord from "../../db/models/HookshotWebhookBridgeRecord";
|
||||||
|
|
||||||
|
interface CreateWithUpstream {
|
||||||
|
upstreamId: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CreateSelfhosted {
|
||||||
|
provisionUrl: string;
|
||||||
|
sharedSecret: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BridgeResponse {
|
||||||
|
id: number;
|
||||||
|
upstreamId?: number;
|
||||||
|
provisionUrl?: string;
|
||||||
|
sharedSecret?: string;
|
||||||
|
isEnabled: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Administrative API for configuring Hookshot Webhook bridge instances.
|
||||||
|
*/
|
||||||
|
@Path("/api/v1/dimension/admin/hookshot/webhook")
|
||||||
|
export class AdminHookshotWebhookService {
|
||||||
|
|
||||||
|
@Context
|
||||||
|
private context: ServiceContext;
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("all")
|
||||||
|
@Security([ROLE_USER, ROLE_ADMIN])
|
||||||
|
public async getBridges(): Promise<BridgeResponse[]> {
|
||||||
|
const bridges = await HookshotWebhookBridgeRecord.findAll();
|
||||||
|
return Promise.all(bridges.map(async b => {
|
||||||
|
return {
|
||||||
|
id: b.id,
|
||||||
|
upstreamId: b.upstreamId,
|
||||||
|
provisionUrl: b.provisionUrl,
|
||||||
|
sharedSecret: b.sharedSecret,
|
||||||
|
isEnabled: b.isEnabled,
|
||||||
|
};
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path(":bridgeId")
|
||||||
|
@Security([ROLE_USER, ROLE_ADMIN])
|
||||||
|
public async getBridge(@PathParam("bridgeId") bridgeId: number): Promise<BridgeResponse> {
|
||||||
|
const bridge = await HookshotWebhookBridgeRecord.findByPk(bridgeId);
|
||||||
|
if (!bridge) throw new ApiError(404, "Webhook Bridge not found");
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: bridge.id,
|
||||||
|
upstreamId: bridge.upstreamId,
|
||||||
|
provisionUrl: bridge.provisionUrl,
|
||||||
|
sharedSecret: bridge.sharedSecret,
|
||||||
|
isEnabled: bridge.isEnabled,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Path(":bridgeId")
|
||||||
|
@Security([ROLE_USER, ROLE_ADMIN])
|
||||||
|
public async updateBridge(@PathParam("bridgeId") bridgeId: number, request: CreateSelfhosted): Promise<BridgeResponse> {
|
||||||
|
const userId = this.context.request.user.userId;
|
||||||
|
|
||||||
|
const bridge = await HookshotWebhookBridgeRecord.findByPk(bridgeId);
|
||||||
|
if (!bridge) throw new ApiError(404, "Bridge not found");
|
||||||
|
|
||||||
|
bridge.provisionUrl = request.provisionUrl;
|
||||||
|
bridge.sharedSecret = request.sharedSecret;
|
||||||
|
await bridge.save();
|
||||||
|
|
||||||
|
LogService.info("AdminHookshotWebhookService", userId + " updated Hookshot Webhook Bridge " + bridge.id);
|
||||||
|
|
||||||
|
Cache.for(CACHE_HOOKSHOT_WEBHOOK_BRIDGE).clear();
|
||||||
|
Cache.for(CACHE_INTEGRATIONS).clear();
|
||||||
|
return this.getBridge(bridge.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Path("new/upstream")
|
||||||
|
@Security([ROLE_USER, ROLE_ADMIN])
|
||||||
|
public async newConfigForUpstream(@QueryParam("scalar_token") _scalarToken: string, _request: CreateWithUpstream): Promise<BridgeResponse> {
|
||||||
|
throw new ApiError(400, "Cannot create a webhook bridge from an upstream");
|
||||||
|
}
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Path("new/selfhosted")
|
||||||
|
@Security([ROLE_USER, ROLE_ADMIN])
|
||||||
|
public async newSelfhosted(request: CreateSelfhosted): Promise<BridgeResponse> {
|
||||||
|
const userId = this.context.request.user.userId;
|
||||||
|
|
||||||
|
const bridge = await HookshotWebhookBridgeRecord.create({
|
||||||
|
provisionUrl: request.provisionUrl,
|
||||||
|
sharedSecret: request.sharedSecret,
|
||||||
|
isEnabled: true,
|
||||||
|
});
|
||||||
|
LogService.info("AdminHookshotWebhookService", userId + " created a new Hookshot Webhook Bridge with provisioning URL " + request.provisionUrl);
|
||||||
|
|
||||||
|
Cache.for(CACHE_HOOKSHOT_WEBHOOK_BRIDGE).clear();
|
||||||
|
Cache.for(CACHE_INTEGRATIONS).clear();
|
||||||
|
return this.getBridge(bridge.id);
|
||||||
|
}
|
||||||
|
}
|
56
src/api/dimension/DimensionHookshotWebhookService.ts
Normal file
56
src/api/dimension/DimensionHookshotWebhookService.ts
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
import { Context, DELETE, GET, Path, PathParam, POST, Security, ServiceContext } from "typescript-rest";
|
||||||
|
import { ApiError } from "../ApiError";
|
||||||
|
import { LogService } from "matrix-bot-sdk";
|
||||||
|
import { ROLE_USER } from "../security/MatrixSecurity";
|
||||||
|
import {
|
||||||
|
HookshotGithubOrg,
|
||||||
|
HookshotGithubRepo,
|
||||||
|
HookshotGithubRoomConfig,
|
||||||
|
HookshotWebhookRoomConfig
|
||||||
|
} from "../../bridges/models/hookshot";
|
||||||
|
import { HookshotGithubBridge } from "../../bridges/HookshotGithubBridge";
|
||||||
|
import { HookshotWebhookBridge } from "../../bridges/HookshotWebhookBridge";
|
||||||
|
|
||||||
|
interface BridgeRoomRequest {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* API for interacting with the Hookshot/Webhook bridge
|
||||||
|
*/
|
||||||
|
@Path("/api/v1/dimension/hookshot/webhook")
|
||||||
|
export class DimensionHookshotWebhookService {
|
||||||
|
|
||||||
|
@Context
|
||||||
|
private context: ServiceContext;
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Path("room/:roomId/connect")
|
||||||
|
@Security(ROLE_USER)
|
||||||
|
public async createWebhook(@PathParam("roomId") roomId: string, request: BridgeRoomRequest): Promise<HookshotWebhookRoomConfig> {
|
||||||
|
const userId = this.context.request.user.userId;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const hookshot = new HookshotWebhookBridge(userId);
|
||||||
|
return hookshot.newConnection(roomId);
|
||||||
|
} catch (e) {
|
||||||
|
LogService.error("DimensionHookshotWebhookService", e);
|
||||||
|
throw new ApiError(400, "Error bridging room");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@DELETE
|
||||||
|
@Path("room/:roomId/connection/:connectionId/disconnect")
|
||||||
|
@Security(ROLE_USER)
|
||||||
|
public async removeWebhook(@PathParam("roomId") roomId: string, @PathParam("connectionId") connectionId: string): Promise<any> {
|
||||||
|
const userId = this.context.request.user.userId;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const hookshot = new HookshotWebhookBridge(userId);
|
||||||
|
await hookshot.removeConnection(roomId, connectionId);
|
||||||
|
return {}; // 200 OK
|
||||||
|
} catch (e) {
|
||||||
|
LogService.error("DimensionHookshotWebhookService", e);
|
||||||
|
throw new ApiError(400, "Error unbridging room");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
49
src/bridges/HookshotWebhookBridge.ts
Normal file
49
src/bridges/HookshotWebhookBridge.ts
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
import {
|
||||||
|
HookshotTypes,
|
||||||
|
HookshotWebhookRoomConfig
|
||||||
|
} from "./models/hookshot";
|
||||||
|
import { HookshotBridge } from "./HookshotBridge";
|
||||||
|
import HookshotWebhookBridgeRecord from "../db/models/HookshotWebhookBridgeRecord";
|
||||||
|
|
||||||
|
export class HookshotWebhookBridge extends HookshotBridge {
|
||||||
|
constructor(requestingUserId: string) {
|
||||||
|
super(requestingUserId);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async getDefaultBridge(): Promise<HookshotWebhookBridgeRecord> {
|
||||||
|
const bridges = await HookshotWebhookBridgeRecord.findAll({where: {isEnabled: true}});
|
||||||
|
if (!bridges || bridges.length !== 1) {
|
||||||
|
throw new Error("No bridges or too many bridges found");
|
||||||
|
}
|
||||||
|
|
||||||
|
return bridges[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getBotUserId(): Promise<string> {
|
||||||
|
const confs = await this.getAllServiceInformation();
|
||||||
|
const conf = confs.find(c => c.eventType === HookshotTypes.Webhook);
|
||||||
|
return conf?.botUserId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async isBridgingEnabled(): Promise<boolean> {
|
||||||
|
const bridges = await HookshotWebhookBridgeRecord.findAll({where: {isEnabled: true}});
|
||||||
|
return !!bridges && bridges.length > 0 && !!(await this.getBotUserId());
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getRoomConfigurations(inRoomId: string): Promise<HookshotWebhookRoomConfig[]> {
|
||||||
|
return (await this.getAllRoomConfigurations(inRoomId)).filter(c => c.eventType === HookshotTypes.Webhook);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async newConnection(roomId: string): Promise<HookshotWebhookRoomConfig> {
|
||||||
|
const bridge = await this.getDefaultBridge();
|
||||||
|
|
||||||
|
const body = {
|
||||||
|
};
|
||||||
|
return await this.doProvisionRequest<HookshotWebhookRoomConfig>(bridge, "PUT", `/v1/${roomId}/connections/${HookshotTypes.Webhook}`, null, body);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async removeConnection(roomId: string, connectionId: string): Promise<void> {
|
||||||
|
const bridge = await this.getDefaultBridge();
|
||||||
|
await this.doProvisionRequest(bridge, "DELETE", `/v1/${roomId}/connections/${connectionId}`);
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,7 @@
|
|||||||
export enum HookshotTypes {
|
export enum HookshotTypes {
|
||||||
Github = "uk.half-shot.matrix-hookshot.github.repository",
|
Github = "uk.half-shot.matrix-hookshot.github.repository",
|
||||||
Jira = "uk.half-shot.matrix-hookshot.jira.project",
|
Jira = "uk.half-shot.matrix-hookshot.jira.project",
|
||||||
|
Webhook = "uk.half-shot.matrix-hookshot.generic.hook",
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface HookshotConnection {
|
export interface HookshotConnection {
|
||||||
@ -90,3 +91,7 @@ export interface HookshotJiraProject {
|
|||||||
name: string;
|
name: string;
|
||||||
url: string;
|
url: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface HookshotWebhookRoomConfig extends HookshotConnection {
|
||||||
|
config: {};
|
||||||
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
Bridge,
|
Bridge,
|
||||||
HookshotGithubBridgeConfiguration,
|
HookshotGithubBridgeConfiguration,
|
||||||
HookshotJiraBridgeConfiguration,
|
HookshotJiraBridgeConfiguration, HookshotWebhookBridgeConfiguration,
|
||||||
SlackBridgeConfiguration,
|
SlackBridgeConfiguration,
|
||||||
TelegramBridgeConfiguration,
|
TelegramBridgeConfiguration,
|
||||||
WebhookBridgeConfiguration
|
WebhookBridgeConfiguration
|
||||||
@ -14,6 +14,8 @@ import { WebhooksBridge } from "../bridges/WebhooksBridge";
|
|||||||
import { SlackBridge } from "../bridges/SlackBridge";
|
import { SlackBridge } from "../bridges/SlackBridge";
|
||||||
import { HookshotGithubBridge } from "../bridges/HookshotGithubBridge";
|
import { HookshotGithubBridge } from "../bridges/HookshotGithubBridge";
|
||||||
import { HookshotJiraBridge } from "../bridges/HookshotJiraBridge";
|
import { HookshotJiraBridge } from "../bridges/HookshotJiraBridge";
|
||||||
|
import { HookshotWebhookBridge } from "../bridges/HookshotWebhookBridge";
|
||||||
|
import HookshotWebhookBridgeRecord from "./models/HookshotWebhookBridgeRecord";
|
||||||
|
|
||||||
export class BridgeStore {
|
export class BridgeStore {
|
||||||
|
|
||||||
@ -88,6 +90,9 @@ export class BridgeStore {
|
|||||||
} else if (record.type === "hookshot_jira") {
|
} else if (record.type === "hookshot_jira") {
|
||||||
const hookshot = new HookshotJiraBridge(requestingUserId);
|
const hookshot = new HookshotJiraBridge(requestingUserId);
|
||||||
return hookshot.isBridgingEnabled();
|
return hookshot.isBridgingEnabled();
|
||||||
|
} else if (record.type === "hookshot_webhook") {
|
||||||
|
const hookshot = new HookshotWebhookBridge(requestingUserId);
|
||||||
|
return hookshot.isBridgingEnabled();
|
||||||
} else return true;
|
} else return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -110,6 +115,9 @@ export class BridgeStore {
|
|||||||
} else if (record.type === "hookshot_jira") {
|
} else if (record.type === "hookshot_jira") {
|
||||||
const hookshot = new HookshotJiraBridge(requestingUserId);
|
const hookshot = new HookshotJiraBridge(requestingUserId);
|
||||||
return hookshot.isBridgingEnabled();
|
return hookshot.isBridgingEnabled();
|
||||||
|
} else if (record.type === "hookshot_webhook") {
|
||||||
|
const hookshot = new HookshotWebhookBridge(requestingUserId);
|
||||||
|
return hookshot.isBridgingEnabled();
|
||||||
} else return false;
|
} else return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -168,6 +176,15 @@ export class BridgeStore {
|
|||||||
loggedIn: userInfo.loggedIn,
|
loggedIn: userInfo.loggedIn,
|
||||||
instances: userInfo.instances,
|
instances: userInfo.instances,
|
||||||
};
|
};
|
||||||
|
} else if (record.type === "hookshot_webhook") {
|
||||||
|
if (!inRoomId) return {}; // The bridge's admin config is handled by other APIs
|
||||||
|
const hookshot = new HookshotWebhookBridge(requestingUserId);
|
||||||
|
const botUserId = await hookshot.getBotUserId();
|
||||||
|
const connections = await hookshot.getRoomConfigurations(inRoomId);
|
||||||
|
return <HookshotWebhookBridgeConfiguration>{
|
||||||
|
botUserId: botUserId,
|
||||||
|
connections: connections,
|
||||||
|
};
|
||||||
} else return {};
|
} else return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -31,6 +31,7 @@ import TermsSignedRecord from "./models/TermsSignedRecord";
|
|||||||
import TermsUpstreamRecord from "./models/TermsUpstreamRecord";
|
import TermsUpstreamRecord from "./models/TermsUpstreamRecord";
|
||||||
import HookshotGithubBridgeRecord from "./models/HookshotGithubBridgeRecord";
|
import HookshotGithubBridgeRecord from "./models/HookshotGithubBridgeRecord";
|
||||||
import HookshotJiraBridgeRecord from "./models/HookshotJiraBridgeRecord";
|
import HookshotJiraBridgeRecord from "./models/HookshotJiraBridgeRecord";
|
||||||
|
import HookshotWebhookBridgeRecord from "./models/HookshotWebhookBridgeRecord";
|
||||||
|
|
||||||
class _DimensionStore {
|
class _DimensionStore {
|
||||||
private sequelize: Sequelize;
|
private sequelize: Sequelize;
|
||||||
@ -79,6 +80,7 @@ class _DimensionStore {
|
|||||||
TermsUpstreamRecord,
|
TermsUpstreamRecord,
|
||||||
HookshotGithubBridgeRecord,
|
HookshotGithubBridgeRecord,
|
||||||
HookshotJiraBridgeRecord,
|
HookshotJiraBridgeRecord,
|
||||||
|
HookshotWebhookBridgeRecord,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
23
src/db/migrations/20211202181645-AddHookshotWebhookBridge.ts
Normal file
23
src/db/migrations/20211202181645-AddHookshotWebhookBridge.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { QueryInterface } from "sequelize";
|
||||||
|
import { DataType } from "sequelize-typescript";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
up: (queryInterface: QueryInterface) => {
|
||||||
|
return Promise.resolve()
|
||||||
|
.then(() => queryInterface.createTable("dimension_hookshot_webhook_bridges", {
|
||||||
|
"id": {type: DataType.INTEGER, primaryKey: true, autoIncrement: true, allowNull: false},
|
||||||
|
"upstreamId": {
|
||||||
|
type: DataType.INTEGER, allowNull: true,
|
||||||
|
references: {model: "dimension_upstreams", key: "id"},
|
||||||
|
onUpdate: "cascade", onDelete: "cascade",
|
||||||
|
},
|
||||||
|
"provisionUrl": {type: DataType.STRING, allowNull: true},
|
||||||
|
"sharedSecret": {type: DataType.STRING, allowNull: true},
|
||||||
|
"isEnabled": {type: DataType.BOOLEAN, allowNull: false},
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
down: (queryInterface: QueryInterface) => {
|
||||||
|
return Promise.resolve()
|
||||||
|
.then(() => queryInterface.dropTable("dimension_hookshot_webhook_bridges"));
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,23 @@
|
|||||||
|
import { QueryInterface } from "sequelize";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
up: (queryInterface: QueryInterface) => {
|
||||||
|
return Promise.resolve()
|
||||||
|
.then(() => queryInterface.bulkInsert("dimension_bridges", [
|
||||||
|
{
|
||||||
|
type: "hookshot_webhook",
|
||||||
|
name: "Webhooks Bridge",
|
||||||
|
avatarUrl: "/assets/img/avatars/webhooks.png",
|
||||||
|
isEnabled: true,
|
||||||
|
isPublic: true,
|
||||||
|
description: "Webhooks to Matrix",
|
||||||
|
},
|
||||||
|
]));
|
||||||
|
},
|
||||||
|
down: (queryInterface: QueryInterface) => {
|
||||||
|
return Promise.resolve()
|
||||||
|
.then(() => queryInterface.bulkDelete("dimension_bridges", {
|
||||||
|
type: "hookshot_webhook",
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,16 @@
|
|||||||
|
import { QueryInterface } from "sequelize";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
up: (queryInterface: QueryInterface) => {
|
||||||
|
return Promise.resolve()
|
||||||
|
.then(() => queryInterface.bulkUpdate("dimension_bridges", {
|
||||||
|
name: "Webhooks Bridge",
|
||||||
|
}, { type: "webhooks" }));
|
||||||
|
},
|
||||||
|
down: (queryInterface: QueryInterface) => {
|
||||||
|
return Promise.resolve()
|
||||||
|
.then(() => queryInterface.bulkUpdate("dimension_bridges", {
|
||||||
|
name: "Webhook Bridge",
|
||||||
|
}, { type: "webhooks" }));
|
||||||
|
}
|
||||||
|
}
|
31
src/db/models/HookshotWebhookBridgeRecord.ts
Normal file
31
src/db/models/HookshotWebhookBridgeRecord.ts
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import { AllowNull, AutoIncrement, Column, ForeignKey, Model, PrimaryKey, Table } from "sequelize-typescript";
|
||||||
|
import Upstream from "./Upstream";
|
||||||
|
import { IHookshotBridgeRecord } from "./IHookshotBridgeRecord";
|
||||||
|
|
||||||
|
@Table({
|
||||||
|
tableName: "dimension_hookshot_webhook_bridges",
|
||||||
|
underscored: false,
|
||||||
|
timestamps: false,
|
||||||
|
})
|
||||||
|
export default class HookshotWebhookBridgeRecord extends Model implements IHookshotBridgeRecord {
|
||||||
|
@PrimaryKey
|
||||||
|
@AutoIncrement
|
||||||
|
@Column
|
||||||
|
id: number;
|
||||||
|
|
||||||
|
@AllowNull
|
||||||
|
@Column
|
||||||
|
@ForeignKey(() => Upstream)
|
||||||
|
upstreamId?: number;
|
||||||
|
|
||||||
|
@AllowNull
|
||||||
|
@Column
|
||||||
|
provisionUrl?: string;
|
||||||
|
|
||||||
|
@AllowNull
|
||||||
|
@Column
|
||||||
|
sharedSecret?: string;
|
||||||
|
|
||||||
|
@Column
|
||||||
|
isEnabled: boolean;
|
||||||
|
}
|
@ -4,7 +4,12 @@ import { AvailableNetworks, LinkedChannels } from "../bridges/IrcBridge";
|
|||||||
import { PortalInfo, PuppetInfo } from "../bridges/TelegramBridge";
|
import { PortalInfo, PuppetInfo } from "../bridges/TelegramBridge";
|
||||||
import { WebhookConfiguration } from "../bridges/models/webhooks";
|
import { WebhookConfiguration } from "../bridges/models/webhooks";
|
||||||
import { BridgedChannel } from "../bridges/SlackBridge";
|
import { BridgedChannel } from "../bridges/SlackBridge";
|
||||||
import { HookshotConnection, HookshotJiraInstance } from "../bridges/models/hookshot";
|
import {
|
||||||
|
HookshotConnection, HookshotGithubRoomConfig,
|
||||||
|
HookshotJiraInstance,
|
||||||
|
HookshotJiraRoomConfig,
|
||||||
|
HookshotWebhookRoomConfig
|
||||||
|
} from "../bridges/models/hookshot";
|
||||||
|
|
||||||
const PRIVATE_ACCESS_SUPPORTED_BRIDGES = ["webhooks"];
|
const PRIVATE_ACCESS_SUPPORTED_BRIDGES = ["webhooks"];
|
||||||
|
|
||||||
@ -49,12 +54,17 @@ export interface SlackBridgeConfiguration {
|
|||||||
|
|
||||||
export interface HookshotGithubBridgeConfiguration {
|
export interface HookshotGithubBridgeConfiguration {
|
||||||
botUserId: string;
|
botUserId: string;
|
||||||
connections: HookshotConnection[];
|
connections: HookshotGithubRoomConfig[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface HookshotJiraBridgeConfiguration {
|
export interface HookshotJiraBridgeConfiguration {
|
||||||
botUserId: string;
|
botUserId: string;
|
||||||
connections: HookshotConnection[];
|
connections: HookshotJiraRoomConfig[];
|
||||||
loggedIn: boolean;
|
loggedIn: boolean;
|
||||||
instances?: HookshotJiraInstance[];
|
instances?: HookshotJiraInstance[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface HookshotWebhookBridgeConfiguration {
|
||||||
|
botUserId: string;
|
||||||
|
connections: HookshotWebhookRoomConfig[];
|
||||||
|
}
|
||||||
|
@ -0,0 +1,41 @@
|
|||||||
|
<div *ngIf="isLoading">
|
||||||
|
<my-spinner></my-spinner>
|
||||||
|
</div>
|
||||||
|
<div *ngIf="!isLoading">
|
||||||
|
<my-ibox boxTitle="Webhook Bridge Configurations">
|
||||||
|
<div class="my-ibox-content">
|
||||||
|
<p>
|
||||||
|
<a href="https://github.com/half-shot/matrix-hookshot" target="_blank">{{'matrix-hookshot' | translate}}</a>
|
||||||
|
{{'is a multi-purpose bridge which supports Generic Webhooks as an integration. If enabled in the configuration, it can be used here to offer a UI for setting up a webhook into the room.' | translate}}
|
||||||
|
<table class="table table-striped table-condensed table-bordered">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>{{'Name' | translate}}</th>
|
||||||
|
<th class="text-center" style="width: 120px;">{{'Actions' | translate}}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr *ngIf="!configurations || configurations.length === 0">
|
||||||
|
<td colspan="3"><i>{{'No bridge configurations.' | translate}}</i></td>
|
||||||
|
</tr>
|
||||||
|
<tr *ngFor="let bridge of configurations trackById">
|
||||||
|
<td>
|
||||||
|
{{ bridge.upstreamId ? "matrix.org's bridge" : "Self-hosted bridge" }}
|
||||||
|
<span class="text-muted" style="display: inline-block;"
|
||||||
|
*ngIf="!bridge.upstreamId">({{ bridge.provisionUrl }})</span>
|
||||||
|
</td>
|
||||||
|
<td class="text-center">
|
||||||
|
<span class="editButton" (click)="editBridge(bridge)">
|
||||||
|
<i class="fa fa-pencil-alt"></i>
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<button type="button" class="btn btn-success btn-sm" (click)="addSelfHostedBridge()"
|
||||||
|
[disabled]="configurations && configurations.length > 0">
|
||||||
|
<i class="fa fa-plus"></i> {{'Add self-hosted bridge' | translate}}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</my-ibox>
|
||||||
|
</div>
|
@ -0,0 +1,3 @@
|
|||||||
|
.editButton {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
@ -0,0 +1,85 @@
|
|||||||
|
import { Component, OnInit } from "@angular/core";
|
||||||
|
import { ToasterService } from "angular2-toaster";
|
||||||
|
import {
|
||||||
|
AdminHookshotWebhookBridgeManageSelfhostedComponent,
|
||||||
|
ManageSelfhostedHookshotWebhookBridgeDialogContext
|
||||||
|
} from "./manage-selfhosted/manage-selfhosted.component";
|
||||||
|
import { TranslateService } from "@ngx-translate/core";
|
||||||
|
import { NgbModal } from "@ng-bootstrap/ng-bootstrap";
|
||||||
|
import { AdminHookshotWebhookApiService } from "../../../shared/services/admin/admin-hookshot-webhook-api.service";
|
||||||
|
import { FE_HookshotWebhookBridge } from "../../../shared/models/hookshot_webhook";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
templateUrl: "./hookshot-webhook.component.html",
|
||||||
|
styleUrls: ["./hookshot-webhook.component.scss"],
|
||||||
|
})
|
||||||
|
export class AdminHookshotWebhookBridgeComponent implements OnInit {
|
||||||
|
|
||||||
|
public isLoading = true;
|
||||||
|
public isUpdating = false;
|
||||||
|
public configurations: FE_HookshotWebhookBridge[] = [];
|
||||||
|
|
||||||
|
constructor(private hookshotApi: AdminHookshotWebhookApiService,
|
||||||
|
private toaster: ToasterService,
|
||||||
|
private modal: NgbModal,
|
||||||
|
public translate: TranslateService) {
|
||||||
|
this.translate = translate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ngOnInit() {
|
||||||
|
this.reload().then(() => this.isLoading = false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async reload(): Promise<any> {
|
||||||
|
try {
|
||||||
|
this.configurations = await this.hookshotApi.getBridges();
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
this.translate.get('Error loading bridges').subscribe((res: string) => {
|
||||||
|
this.toaster.pop("error", res);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public addSelfHostedBridge() {
|
||||||
|
const selfhostedRef = this.modal.open(AdminHookshotWebhookBridgeManageSelfhostedComponent, {
|
||||||
|
backdrop: 'static',
|
||||||
|
size: 'lg',
|
||||||
|
});
|
||||||
|
selfhostedRef.result.then(() => {
|
||||||
|
try {
|
||||||
|
this.reload()
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
this.translate.get('Failed to get an updated Webhook bridge list').subscribe((res: string) => {
|
||||||
|
this.toaster.pop("error", res);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const selfhostedInstance = selfhostedRef.componentInstance as ManageSelfhostedHookshotWebhookBridgeDialogContext;
|
||||||
|
selfhostedInstance.provisionUrl = '';
|
||||||
|
selfhostedInstance.sharedSecret = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
public editBridge(bridge: FE_HookshotWebhookBridge) {
|
||||||
|
const selfhostedRef = this.modal.open(AdminHookshotWebhookBridgeManageSelfhostedComponent, {
|
||||||
|
backdrop: 'static',
|
||||||
|
size: 'lg',
|
||||||
|
});
|
||||||
|
selfhostedRef.result.then(() => {
|
||||||
|
try {
|
||||||
|
this.reload()
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
this.translate.get('Failed to get an updated Webhook bridge list').subscribe((res: string) => {
|
||||||
|
this.toaster.pop("error", res);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const selfhostedInstance = selfhostedRef.componentInstance as ManageSelfhostedHookshotWebhookBridgeDialogContext;
|
||||||
|
selfhostedInstance.provisionUrl = bridge.provisionUrl;
|
||||||
|
selfhostedInstance.sharedSecret = bridge.sharedSecret;
|
||||||
|
selfhostedInstance.bridgeId = bridge.id;
|
||||||
|
selfhostedInstance.isAdding = !bridge.id;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,31 @@
|
|||||||
|
<div class="modal-header">
|
||||||
|
<h4 class="modal-title" id="modal-basic-title">{{'self-hosted Github bridge' | translate}} ({{ isAdding ? "Add a new" : "Edit" }})</h4>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" (click)="modal.close()"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<p>{{'Self-hosted Webhook bridges must have' | translate}} <code>{{'provisioning' | translate}}</code> {{'enabled in the configuration.' | translate}}</p>
|
||||||
|
|
||||||
|
<label class="label-block">
|
||||||
|
{{'Provisioning URL' | translate}}
|
||||||
|
<span class="text-muted ">{{'The provisioning URL for the bridge. This is the specific address for the bridge given in the configuration.' | translate}}</span>
|
||||||
|
<input type="text" class="form-control"
|
||||||
|
placeholder="http://localhost:9000"
|
||||||
|
[(ngModel)]="provisionUrl" [disabled]="isSaving"/>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="label-block">
|
||||||
|
{{'Shared Secret' | translate}}
|
||||||
|
<span class="text-muted ">{{'The shared secret defined in the configuration for provisioning.' | translate}}</span>
|
||||||
|
<input type="text" class="form-control"
|
||||||
|
placeholder="some_secret_value"
|
||||||
|
[(ngModel)]="sharedSecret" [disabled]="isSaving"/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" (click)="add()" title="save" class="btn btn-primary btn-sm">
|
||||||
|
<i class="far fa-save"></i> {{'Save' | translate}}
|
||||||
|
</button>
|
||||||
|
<button type="button" (click)="modal.close()" title="close" class="btn btn-secondary btn-sm">
|
||||||
|
<i class="far fa-times-circle"></i> {{'Cancel' | translate}}
|
||||||
|
</button>
|
||||||
|
</div>
|
@ -0,0 +1,63 @@
|
|||||||
|
import { Component } from "@angular/core";
|
||||||
|
import { NgbActiveModal } from "@ng-bootstrap/ng-bootstrap";
|
||||||
|
import { ToasterService } from "angular2-toaster";
|
||||||
|
import { TranslateService } from "@ngx-translate/core";
|
||||||
|
import { AdminHookshotWebhookApiService } from "../../../../shared/services/admin/admin-hookshot-webhook-api.service";
|
||||||
|
|
||||||
|
export interface ManageSelfhostedHookshotWebhookBridgeDialogContext {
|
||||||
|
provisionUrl: string;
|
||||||
|
sharedSecret: string;
|
||||||
|
bridgeId: number;
|
||||||
|
isAdding: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
templateUrl: "./manage-selfhosted.component.html",
|
||||||
|
styleUrls: ["./manage-selfhosted.component.scss"],
|
||||||
|
})
|
||||||
|
export class AdminHookshotWebhookBridgeManageSelfhostedComponent {
|
||||||
|
|
||||||
|
isSaving = false;
|
||||||
|
provisionUrl: string;
|
||||||
|
sharedSecret: string;
|
||||||
|
bridgeId: number;
|
||||||
|
isAdding = true;
|
||||||
|
|
||||||
|
constructor(public modal: NgbActiveModal,
|
||||||
|
private hookshotApi: AdminHookshotWebhookApiService,
|
||||||
|
private toaster: ToasterService,
|
||||||
|
public translate: TranslateService) {
|
||||||
|
this.translate = translate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public add() {
|
||||||
|
this.isSaving = true;
|
||||||
|
if (this.isAdding) {
|
||||||
|
this.hookshotApi.newSelfhosted(this.provisionUrl, this.sharedSecret).then(() => {
|
||||||
|
this.translate.get('Webhook bridge added').subscribe((res: string) => {
|
||||||
|
this.toaster.pop("success", res);
|
||||||
|
});
|
||||||
|
this.modal.close();
|
||||||
|
}).catch(err => {
|
||||||
|
console.error(err);
|
||||||
|
this.isSaving = false;
|
||||||
|
this.translate.get('Failed to create Webhook bridge').subscribe((res: string) => {
|
||||||
|
this.toaster.pop("error", res);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.hookshotApi.updateSelfhosted(this.bridgeId, this.provisionUrl, this.sharedSecret).then(() => {
|
||||||
|
this.translate.get('Webhook bridge updated').subscribe((res: string) => {
|
||||||
|
this.toaster.pop("success", res);
|
||||||
|
});
|
||||||
|
this.modal.close();
|
||||||
|
}).catch(err => {
|
||||||
|
console.error(err);
|
||||||
|
this.isSaving = false;
|
||||||
|
this.translate.get('Failed to update Webhook bridge').subscribe((res: string) => {
|
||||||
|
this.toaster.pop("error", res);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -143,6 +143,15 @@ import {
|
|||||||
import { AdminHookshotJiraApiService } from "./shared/services/admin/admin-hookshot-jira-api.service";
|
import { AdminHookshotJiraApiService } from "./shared/services/admin/admin-hookshot-jira-api.service";
|
||||||
import { HookshotJiraApiService } from "./shared/services/integrations/hookshot-jira-api.service";
|
import { HookshotJiraApiService } from "./shared/services/integrations/hookshot-jira-api.service";
|
||||||
import { HookshotJiraBridgeConfigComponent } from "./configs/bridge/hookshot-jira/hookshot-jira.bridge.component";
|
import { HookshotJiraBridgeConfigComponent } from "./configs/bridge/hookshot-jira/hookshot-jira.bridge.component";
|
||||||
|
import { AdminHookshotWebhookBridgeComponent } from "./admin/bridges/hookshot-webhook/hookshot-webhook.component";
|
||||||
|
import {
|
||||||
|
AdminHookshotWebhookBridgeManageSelfhostedComponent
|
||||||
|
} from "./admin/bridges/hookshot-webhook/manage-selfhosted/manage-selfhosted.component";
|
||||||
|
import { AdminHookshotWebhookApiService } from "./shared/services/admin/admin-hookshot-webhook-api.service";
|
||||||
|
import { HookshotWebhookApiService } from "./shared/services/integrations/hookshot-webhook-api.service";
|
||||||
|
import {
|
||||||
|
HookshotWebhookBridgeConfigComponent
|
||||||
|
} from "./configs/bridge/hookshot-webhook/hookshot-webhook.bridge.component";
|
||||||
|
|
||||||
// AoT requires an exported function for factories
|
// AoT requires an exported function for factories
|
||||||
export function HttpLoaderFactory(http: HttpClient) {
|
export function HttpLoaderFactory(http: HttpClient) {
|
||||||
@ -258,6 +267,9 @@ export function HttpLoaderFactory(http: HttpClient) {
|
|||||||
AdminHookshotJiraBridgeComponent,
|
AdminHookshotJiraBridgeComponent,
|
||||||
AdminHookshotJiraBridgeManageSelfhostedComponent,
|
AdminHookshotJiraBridgeManageSelfhostedComponent,
|
||||||
HookshotJiraBridgeConfigComponent,
|
HookshotJiraBridgeConfigComponent,
|
||||||
|
AdminHookshotWebhookBridgeComponent,
|
||||||
|
AdminHookshotWebhookBridgeManageSelfhostedComponent,
|
||||||
|
HookshotWebhookBridgeConfigComponent,
|
||||||
|
|
||||||
// Vendor
|
// Vendor
|
||||||
],
|
],
|
||||||
@ -291,6 +303,8 @@ export function HttpLoaderFactory(http: HttpClient) {
|
|||||||
HookshotGithubApiService,
|
HookshotGithubApiService,
|
||||||
AdminHookshotJiraApiService,
|
AdminHookshotJiraApiService,
|
||||||
HookshotJiraApiService,
|
HookshotJiraApiService,
|
||||||
|
AdminHookshotWebhookApiService,
|
||||||
|
HookshotWebhookApiService,
|
||||||
{provide: Window, useValue: window},
|
{provide: Window, useValue: window},
|
||||||
|
|
||||||
// Vendor
|
// Vendor
|
||||||
|
@ -54,6 +54,10 @@ import { AdminHookshotGithubBridgeComponent } from "./admin/bridges/hookshot-git
|
|||||||
import { HookshotGithubBridgeConfigComponent } from "./configs/bridge/hookshot-github/hookshot-github.bridge.component";
|
import { HookshotGithubBridgeConfigComponent } from "./configs/bridge/hookshot-github/hookshot-github.bridge.component";
|
||||||
import { AdminHookshotJiraBridgeComponent } from "./admin/bridges/hookshot-jira/hookshot-jira.component";
|
import { AdminHookshotJiraBridgeComponent } from "./admin/bridges/hookshot-jira/hookshot-jira.component";
|
||||||
import { HookshotJiraBridgeConfigComponent } from "./configs/bridge/hookshot-jira/hookshot-jira.bridge.component";
|
import { HookshotJiraBridgeConfigComponent } from "./configs/bridge/hookshot-jira/hookshot-jira.bridge.component";
|
||||||
|
import { AdminHookshotWebhookBridgeComponent } from "./admin/bridges/hookshot-webhook/hookshot-webhook.component";
|
||||||
|
import {
|
||||||
|
HookshotWebhookBridgeConfigComponent
|
||||||
|
} from "./configs/bridge/hookshot-webhook/hookshot-webhook.bridge.component";
|
||||||
|
|
||||||
const routes: Routes = [
|
const routes: Routes = [
|
||||||
{path: "", component: HomeComponent},
|
{path: "", component: HomeComponent},
|
||||||
@ -150,6 +154,11 @@ const routes: Routes = [
|
|||||||
component: AdminHookshotJiraBridgeComponent,
|
component: AdminHookshotJiraBridgeComponent,
|
||||||
data: {breadcrumb: "Jira Bridge", name: "Jira Bridge"},
|
data: {breadcrumb: "Jira Bridge", name: "Jira Bridge"},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "hookshot_webhook",
|
||||||
|
component: AdminHookshotWebhookBridgeComponent,
|
||||||
|
data: {breadcrumb: "Webhook Bridge", name: "Webhook Bridge"},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -300,6 +309,11 @@ const routes: Routes = [
|
|||||||
component: HookshotJiraBridgeConfigComponent,
|
component: HookshotJiraBridgeConfigComponent,
|
||||||
data: {breadcrumb: "Jira Bridge Configuration", name: "Jira Bridge Configuration"},
|
data: {breadcrumb: "Jira Bridge Configuration", name: "Jira Bridge Configuration"},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "hookshot_webhook",
|
||||||
|
component: HookshotWebhookBridgeConfigComponent,
|
||||||
|
data: {breadcrumb: "Webhook Bridge Configuration", name: "Webhook Bridge Configuration"},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -0,0 +1,54 @@
|
|||||||
|
<my-bridge-config [bridgeComponent]="this">
|
||||||
|
<ng-template #bridgeParamsTemplate>
|
||||||
|
<my-ibox [isCollapsible]="true">
|
||||||
|
<h5 class="my-ibox-title">
|
||||||
|
{{'Add a new webhook' | translate}}
|
||||||
|
</h5>
|
||||||
|
<div class="my-ibox-content">
|
||||||
|
<label class="label-block">
|
||||||
|
{{'Webhook Name' | translate}}
|
||||||
|
<input title="webhook name" type="text" class="form-control form-control-sm"
|
||||||
|
[(ngModel)]="webhookName" [disabled]="isBusy">
|
||||||
|
</label>
|
||||||
|
<div style="margin-top: 25px">
|
||||||
|
<button type="button" class="btn btn-sm btn-primary" [disabled]="isBusy" (click)="newHook()">
|
||||||
|
{{'Create' | translate}}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</my-ibox>
|
||||||
|
<my-ibox [isCollapsible]="true">
|
||||||
|
<h5 class="my-ibox-title">
|
||||||
|
Webhooks
|
||||||
|
</h5>
|
||||||
|
<div class="my-ibox-content">
|
||||||
|
<table class="table table-striped table-condensed table-bordered">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>URL</th>
|
||||||
|
<th class="actions-col">{{'Actions' | translate}}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr *ngIf="newConfig.connections.length === 0">
|
||||||
|
<td colspan="4">{{'No webhooks' | translate}}</td>
|
||||||
|
</tr>
|
||||||
|
<tr *ngFor="let hook of newConfig.connections">
|
||||||
|
<td *ngIf="hook.config.name">{{ hook.config.name }}</td>
|
||||||
|
<td *ngIf="!hook.config.name"><i>{{'No name' | translate}}</i></td>
|
||||||
|
<td class="webhook-url"><a [href]="hook.config.url" target="_blank">{{ hook.config.url }}</a></td>
|
||||||
|
<td class="actions-col">
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-danger"
|
||||||
|
[disabled]="isBusy"
|
||||||
|
(click)="removeHook(hook)">
|
||||||
|
<i class="far fa-trash-alt"></i> {{'Delete' | translate}}
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</my-ibox>
|
||||||
|
</ng-template>
|
||||||
|
</my-bridge-config>
|
@ -0,0 +1,3 @@
|
|||||||
|
.webhook-url {
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
@ -0,0 +1,78 @@
|
|||||||
|
import { Component } from "@angular/core";
|
||||||
|
import { BridgeComponent } from "../bridge.component";
|
||||||
|
import { FE_Webhook } from "../../../shared/models/webhooks";
|
||||||
|
import { WebhooksApiService } from "../../../shared/services/integrations/webhooks-api.service";
|
||||||
|
import { ScalarClientApiService } from "../../../shared/services/scalar/scalar-client-api.service";
|
||||||
|
import { TranslateService } from "@ngx-translate/core";
|
||||||
|
import { FE_HookshotJiraConnection } from "../../../shared/models/hookshot_jira";
|
||||||
|
import { FE_HookshotWebhookConnection } from "../../../shared/models/hookshot_webhook";
|
||||||
|
import { HookshotWebhookApiService } from "../../../shared/services/integrations/hookshot-webhook-api.service";
|
||||||
|
|
||||||
|
interface HookshotConfig {
|
||||||
|
botUserId: string;
|
||||||
|
connections: FE_HookshotWebhookConnection[];
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
templateUrl: "hookshot-webhook.bridge.component.html",
|
||||||
|
styleUrls: ["hookshot-webhook.bridge.component.scss"],
|
||||||
|
})
|
||||||
|
export class HookshotWebhookBridgeConfigComponent extends BridgeComponent<HookshotConfig> {
|
||||||
|
|
||||||
|
public webhookName: string;
|
||||||
|
public isBusy = false;
|
||||||
|
|
||||||
|
constructor(private webhooks: HookshotWebhookApiService, private scalar: ScalarClientApiService, public translate: TranslateService) {
|
||||||
|
super("hookshot_webhook", translate);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async newHook() {
|
||||||
|
this.isBusy = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.scalar.inviteUser(this.roomId, this.newConfig.botUserId);
|
||||||
|
} catch (e) {
|
||||||
|
if (!e.response || !e.response.error || !e.response.error._error ||
|
||||||
|
e.response.error._error.message.indexOf("already in the room") === -1) {
|
||||||
|
this.isBusy = false;
|
||||||
|
this.translate.get('Error inviting bridge').subscribe((res: string) => {
|
||||||
|
this.toaster.pop("error", res);
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.webhooks.createWebhook(this.roomId, this.webhookName).then(hook => {
|
||||||
|
this.newConfig.connections.push(hook);
|
||||||
|
this.isBusy = false;
|
||||||
|
this.webhookName = "";
|
||||||
|
this.translate.get('Webhook created').subscribe((res: string) => {
|
||||||
|
this.toaster.pop("success", res);
|
||||||
|
});
|
||||||
|
}).catch(err => {
|
||||||
|
console.error(err);
|
||||||
|
this.isBusy = false;
|
||||||
|
this.translate.get('Error creating webhook').subscribe((res: string) => {
|
||||||
|
this.toaster.pop("error", res);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public removeHook(hook: FE_HookshotWebhookConnection) {
|
||||||
|
this.isBusy = true;
|
||||||
|
this.webhooks.deleteWebhook(this.roomId, hook.id).then(() => {
|
||||||
|
const idx = this.newConfig.connections.indexOf(hook);
|
||||||
|
if (idx !== -1) this.newConfig.connections.splice(idx, 1);
|
||||||
|
this.isBusy = false;
|
||||||
|
this.translate.get('Webhook deleted').subscribe((res: string) => {
|
||||||
|
this.toaster.pop("success", res);
|
||||||
|
});
|
||||||
|
}).catch(err => {
|
||||||
|
console.error(err);
|
||||||
|
this.isBusy = false;
|
||||||
|
this.translate.get('Error deleting webhook').subscribe((res: string) => {
|
||||||
|
this.toaster.pop("error", res);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
15
web/app/shared/models/hookshot_webhook.ts
Normal file
15
web/app/shared/models/hookshot_webhook.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
export interface FE_HookshotWebhookBridge {
|
||||||
|
id: number;
|
||||||
|
upstreamId?: number;
|
||||||
|
provisionUrl?: string;
|
||||||
|
sharedSecret?: string;
|
||||||
|
isEnabled: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FE_HookshotWebhookConnection {
|
||||||
|
id: string;
|
||||||
|
config: {
|
||||||
|
name?: string; // TODO: Update according to bridge support
|
||||||
|
url: string;
|
||||||
|
};
|
||||||
|
}
|
@ -33,6 +33,7 @@ export class IntegrationsRegistry {
|
|||||||
"slack": {},
|
"slack": {},
|
||||||
"hookshot_github": {},
|
"hookshot_github": {},
|
||||||
"hookshot_jira": {},
|
"hookshot_jira": {},
|
||||||
|
"hookshot_webhook": {},
|
||||||
},
|
},
|
||||||
"widget": {
|
"widget": {
|
||||||
"custom": {
|
"custom": {
|
||||||
|
@ -0,0 +1,39 @@
|
|||||||
|
import { Injectable } from "@angular/core";
|
||||||
|
import { AuthedApi } from "../authed-api";
|
||||||
|
import { FE_Upstream } from "../../models/admin-responses";
|
||||||
|
import { HttpClient } from "@angular/common/http";
|
||||||
|
import { FE_HookshotGithubBridge } from "../../models/hookshot_github";
|
||||||
|
import { FE_HookshotWebhookBridge } from "../../models/hookshot_webhook";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AdminHookshotWebhookApiService extends AuthedApi {
|
||||||
|
constructor(http: HttpClient) {
|
||||||
|
super(http);
|
||||||
|
}
|
||||||
|
|
||||||
|
public getBridges(): Promise<FE_HookshotWebhookBridge[]> {
|
||||||
|
return this.authedGet<FE_HookshotWebhookBridge[]>("/api/v1/dimension/admin/hookshot/webhook/all").toPromise();
|
||||||
|
}
|
||||||
|
|
||||||
|
public getBridge(bridgeId: number): Promise<FE_HookshotWebhookBridge> {
|
||||||
|
return this.authedGet<FE_HookshotWebhookBridge>("/api/v1/dimension/admin/hookshot/webhook/" + bridgeId).toPromise();
|
||||||
|
}
|
||||||
|
|
||||||
|
public newFromUpstream(upstream: FE_Upstream): Promise<FE_HookshotWebhookBridge> {
|
||||||
|
return this.authedPost<FE_HookshotWebhookBridge>("/api/v1/dimension/admin/hookshot/webhook/new/upstream", {upstreamId: upstream.id}).toPromise();
|
||||||
|
}
|
||||||
|
|
||||||
|
public newSelfhosted(provisionUrl: string, sharedSecret: string): Promise<FE_HookshotWebhookBridge> {
|
||||||
|
return this.authedPost<FE_HookshotWebhookBridge>("/api/v1/dimension/admin/hookshot/webhook/new/selfhosted", {
|
||||||
|
provisionUrl: provisionUrl,
|
||||||
|
sharedSecret: sharedSecret,
|
||||||
|
}).toPromise();
|
||||||
|
}
|
||||||
|
|
||||||
|
public updateSelfhosted(bridgeId: number, provisionUrl: string, sharedSecret: string): Promise<FE_HookshotWebhookBridge> {
|
||||||
|
return this.authedPost<FE_HookshotWebhookBridge>("/api/v1/dimension/admin/hookshot/webhook/" + bridgeId, {
|
||||||
|
provisionUrl: provisionUrl,
|
||||||
|
sharedSecret: sharedSecret,
|
||||||
|
}).toPromise();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,22 @@
|
|||||||
|
import { Injectable } from "@angular/core";
|
||||||
|
import { AuthedApi } from "../authed-api";
|
||||||
|
import { HttpClient } from "@angular/common/http";
|
||||||
|
import { FE_HookshotWebhookConnection } from "../../models/hookshot_webhook";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class HookshotWebhookApiService extends AuthedApi {
|
||||||
|
constructor(http: HttpClient) {
|
||||||
|
super(http);
|
||||||
|
}
|
||||||
|
|
||||||
|
public createWebhook(roomId: string, name: string): Promise<FE_HookshotWebhookConnection> {
|
||||||
|
return this.authedPost<FE_HookshotWebhookConnection>("/api/v1/dimension/hookshot/webhook/room/" + roomId + "/connect", {
|
||||||
|
name,
|
||||||
|
}).toPromise();
|
||||||
|
}
|
||||||
|
|
||||||
|
public deleteWebhook(roomId: string, webhookId: string): Promise<any> {
|
||||||
|
return this.authedDelete("/api/v1/dimension/hookshot/webhook/room/" + roomId + "/connection/" + encodeURIComponent(webhookId) + "/disconnect").toPromise();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user