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_TELEGRAM_BRIDGE = "telegram-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_WEBHOOKS_BRIDGE = "webhooks-bridge";
|
||||
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 {
|
||||
Github = "uk.half-shot.matrix-hookshot.github.repository",
|
||||
Jira = "uk.half-shot.matrix-hookshot.jira.project",
|
||||
Webhook = "uk.half-shot.matrix-hookshot.generic.hook",
|
||||
}
|
||||
|
||||
export interface HookshotConnection {
|
||||
@ -90,3 +91,7 @@ export interface HookshotJiraProject {
|
||||
name: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
export interface HookshotWebhookRoomConfig extends HookshotConnection {
|
||||
config: {};
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
import {
|
||||
Bridge,
|
||||
HookshotGithubBridgeConfiguration,
|
||||
HookshotJiraBridgeConfiguration,
|
||||
HookshotJiraBridgeConfiguration, HookshotWebhookBridgeConfiguration,
|
||||
SlackBridgeConfiguration,
|
||||
TelegramBridgeConfiguration,
|
||||
WebhookBridgeConfiguration
|
||||
@ -14,6 +14,8 @@ import { WebhooksBridge } from "../bridges/WebhooksBridge";
|
||||
import { SlackBridge } from "../bridges/SlackBridge";
|
||||
import { HookshotGithubBridge } from "../bridges/HookshotGithubBridge";
|
||||
import { HookshotJiraBridge } from "../bridges/HookshotJiraBridge";
|
||||
import { HookshotWebhookBridge } from "../bridges/HookshotWebhookBridge";
|
||||
import HookshotWebhookBridgeRecord from "./models/HookshotWebhookBridgeRecord";
|
||||
|
||||
export class BridgeStore {
|
||||
|
||||
@ -88,6 +90,9 @@ export class BridgeStore {
|
||||
} else if (record.type === "hookshot_jira") {
|
||||
const hookshot = new HookshotJiraBridge(requestingUserId);
|
||||
return hookshot.isBridgingEnabled();
|
||||
} else if (record.type === "hookshot_webhook") {
|
||||
const hookshot = new HookshotWebhookBridge(requestingUserId);
|
||||
return hookshot.isBridgingEnabled();
|
||||
} else return true;
|
||||
}
|
||||
|
||||
@ -110,6 +115,9 @@ export class BridgeStore {
|
||||
} else if (record.type === "hookshot_jira") {
|
||||
const hookshot = new HookshotJiraBridge(requestingUserId);
|
||||
return hookshot.isBridgingEnabled();
|
||||
} else if (record.type === "hookshot_webhook") {
|
||||
const hookshot = new HookshotWebhookBridge(requestingUserId);
|
||||
return hookshot.isBridgingEnabled();
|
||||
} else return false;
|
||||
}
|
||||
|
||||
@ -168,6 +176,15 @@ export class BridgeStore {
|
||||
loggedIn: userInfo.loggedIn,
|
||||
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 {};
|
||||
}
|
||||
|
||||
|
@ -31,6 +31,7 @@ import TermsSignedRecord from "./models/TermsSignedRecord";
|
||||
import TermsUpstreamRecord from "./models/TermsUpstreamRecord";
|
||||
import HookshotGithubBridgeRecord from "./models/HookshotGithubBridgeRecord";
|
||||
import HookshotJiraBridgeRecord from "./models/HookshotJiraBridgeRecord";
|
||||
import HookshotWebhookBridgeRecord from "./models/HookshotWebhookBridgeRecord";
|
||||
|
||||
class _DimensionStore {
|
||||
private sequelize: Sequelize;
|
||||
@ -79,6 +80,7 @@ class _DimensionStore {
|
||||
TermsUpstreamRecord,
|
||||
HookshotGithubBridgeRecord,
|
||||
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 { WebhookConfiguration } from "../bridges/models/webhooks";
|
||||
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"];
|
||||
|
||||
@ -49,12 +54,17 @@ export interface SlackBridgeConfiguration {
|
||||
|
||||
export interface HookshotGithubBridgeConfiguration {
|
||||
botUserId: string;
|
||||
connections: HookshotConnection[];
|
||||
connections: HookshotGithubRoomConfig[];
|
||||
}
|
||||
|
||||
export interface HookshotJiraBridgeConfiguration {
|
||||
botUserId: string;
|
||||
connections: HookshotConnection[];
|
||||
connections: HookshotJiraRoomConfig[];
|
||||
loggedIn: boolean;
|
||||
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 { HookshotJiraApiService } from "./shared/services/integrations/hookshot-jira-api.service";
|
||||
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
|
||||
export function HttpLoaderFactory(http: HttpClient) {
|
||||
@ -258,6 +267,9 @@ export function HttpLoaderFactory(http: HttpClient) {
|
||||
AdminHookshotJiraBridgeComponent,
|
||||
AdminHookshotJiraBridgeManageSelfhostedComponent,
|
||||
HookshotJiraBridgeConfigComponent,
|
||||
AdminHookshotWebhookBridgeComponent,
|
||||
AdminHookshotWebhookBridgeManageSelfhostedComponent,
|
||||
HookshotWebhookBridgeConfigComponent,
|
||||
|
||||
// Vendor
|
||||
],
|
||||
@ -291,6 +303,8 @@ export function HttpLoaderFactory(http: HttpClient) {
|
||||
HookshotGithubApiService,
|
||||
AdminHookshotJiraApiService,
|
||||
HookshotJiraApiService,
|
||||
AdminHookshotWebhookApiService,
|
||||
HookshotWebhookApiService,
|
||||
{provide: Window, useValue: window},
|
||||
|
||||
// Vendor
|
||||
|
@ -54,6 +54,10 @@ import { AdminHookshotGithubBridgeComponent } from "./admin/bridges/hookshot-git
|
||||
import { HookshotGithubBridgeConfigComponent } from "./configs/bridge/hookshot-github/hookshot-github.bridge.component";
|
||||
import { AdminHookshotJiraBridgeComponent } from "./admin/bridges/hookshot-jira/hookshot-jira.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 = [
|
||||
{path: "", component: HomeComponent},
|
||||
@ -150,6 +154,11 @@ const routes: Routes = [
|
||||
component: AdminHookshotJiraBridgeComponent,
|
||||
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,
|
||||
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": {},
|
||||
"hookshot_github": {},
|
||||
"hookshot_jira": {},
|
||||
"hookshot_webhook": {},
|
||||
},
|
||||
"widget": {
|
||||
"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