mirror of
https://github.com/turt2live/matrix-dimension.git
synced 2024-07-08 03:51:48 +00:00
Initial support for matrix-hookshot#jira
This commit is contained in:
parent
089925ee4c
commit
43f795f4da
|
@ -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_JIRA_BRIDGE = "hookshot-jira-bridge";
|
||||
export const CACHE_WEBHOOKS_BRIDGE = "webhooks-bridge";
|
||||
export const CACHE_SIMPLE_BOTS = "simple-bots";
|
||||
export const CACHE_SLACK_BRIDGE = "slack-bridge";
|
||||
|
|
110
src/api/admin/AdminHookshotJiraService.ts
Normal file
110
src/api/admin/AdminHookshotJiraService.ts
Normal file
|
@ -0,0 +1,110 @@
|
|||
import { Context, GET, Path, PathParam, POST, QueryParam, Security, ServiceContext } from "typescript-rest";
|
||||
import { Cache, CACHE_HOOKSHOT_JIRA_BRIDGE, CACHE_INTEGRATIONS } from "../../MemoryCache";
|
||||
import { LogService } from "matrix-bot-sdk";
|
||||
import { ApiError } from "../ApiError";
|
||||
import { ROLE_ADMIN, ROLE_USER } from "../security/MatrixSecurity";
|
||||
import HookshotJiraBridgeRecord from "../../db/models/HookshotJiraBridgeRecord";
|
||||
|
||||
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 Jira bridge instances.
|
||||
*/
|
||||
@Path("/api/v1/dimension/admin/hookshot/jira")
|
||||
export class AdminHookshotJiraService {
|
||||
|
||||
@Context
|
||||
private context: ServiceContext;
|
||||
|
||||
@GET
|
||||
@Path("all")
|
||||
@Security([ROLE_USER, ROLE_ADMIN])
|
||||
public async getBridges(): Promise<BridgeResponse[]> {
|
||||
const bridges = await HookshotJiraBridgeRecord.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 jiraBridge = await HookshotJiraBridgeRecord.findByPk(bridgeId);
|
||||
if (!jiraBridge) throw new ApiError(404, "Jira Bridge not found");
|
||||
|
||||
return {
|
||||
id: jiraBridge.id,
|
||||
upstreamId: jiraBridge.upstreamId,
|
||||
provisionUrl: jiraBridge.provisionUrl,
|
||||
sharedSecret: jiraBridge.sharedSecret,
|
||||
isEnabled: jiraBridge.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 HookshotJiraBridgeRecord.findByPk(bridgeId);
|
||||
if (!bridge) throw new ApiError(404, "Bridge not found");
|
||||
|
||||
bridge.provisionUrl = request.provisionUrl;
|
||||
bridge.sharedSecret = request.sharedSecret;
|
||||
await bridge.save();
|
||||
|
||||
LogService.info("AdminHookshotJiraService", userId + " updated Hookshot Jira Bridge " + bridge.id);
|
||||
|
||||
Cache.for(CACHE_HOOKSHOT_JIRA_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 jira 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 HookshotJiraBridgeRecord.create({
|
||||
provisionUrl: request.provisionUrl,
|
||||
sharedSecret: request.sharedSecret,
|
||||
isEnabled: true,
|
||||
});
|
||||
LogService.info("AdminHookshotJiraService", userId + " created a new Hookshot Jira Bridge with provisioning URL " + request.provisionUrl);
|
||||
|
||||
Cache.for(CACHE_HOOKSHOT_JIRA_BRIDGE).clear();
|
||||
Cache.for(CACHE_INTEGRATIONS).clear();
|
||||
return this.getBridge(bridge.id);
|
||||
}
|
||||
}
|
79
src/bridges/HookshotBridge.ts
Normal file
79
src/bridges/HookshotBridge.ts
Normal file
|
@ -0,0 +1,79 @@
|
|||
import { LogService } from "matrix-bot-sdk";
|
||||
import * as request from "request";
|
||||
import {
|
||||
HookshotConnectionsResponse, HookshotConnectionTypeDefinition
|
||||
} from "./models/hookshot";
|
||||
import { IHookshotBridgeRecord } from "../db/models/IHookshotBridgeRecord";
|
||||
|
||||
export abstract class HookshotBridge {
|
||||
protected constructor(private requestingUserId: string) {
|
||||
}
|
||||
|
||||
protected abstract getDefaultBridge(): Promise<IHookshotBridgeRecord>;
|
||||
|
||||
protected async getAllRoomConfigurations(inRoomId: string): Promise<HookshotConnectionsResponse> {
|
||||
const bridge = await this.getDefaultBridge();
|
||||
|
||||
try {
|
||||
return await this.doProvisionRequest<HookshotConnectionsResponse>(bridge, "GET", `/v1/${inRoomId}/connections`);
|
||||
} catch (e) {
|
||||
if (e.errBody['errcode'] === "HS_NOT_IN_ROOM") {
|
||||
return [];
|
||||
}
|
||||
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
protected async getAllServiceInformation(): Promise<HookshotConnectionTypeDefinition[]> {
|
||||
const bridge = await this.getDefaultBridge();
|
||||
const connections = await this.doProvisionRequest(bridge, "GET", `/v1/connectiontypes`);
|
||||
return Object.values(connections);
|
||||
}
|
||||
|
||||
protected async doProvisionRequest<T>(bridge: IHookshotBridgeRecord, method: string, endpoint: string, qs?: any, body?: any): Promise<T> {
|
||||
const provisionUrl = bridge.provisionUrl;
|
||||
const apiUrl = provisionUrl.endsWith("/") ? provisionUrl.substring(0, provisionUrl.length - 1) : provisionUrl;
|
||||
const url = apiUrl + (endpoint.startsWith("/") ? endpoint : "/" + endpoint);
|
||||
LogService.info("HookshotBridge", "Doing provision Hookshot Bridge request: " + url);
|
||||
|
||||
if (!qs) qs = {};
|
||||
|
||||
if (qs["userId"] === false) delete qs["userId"];
|
||||
else if (!qs["userId"]) qs["userId"] = this.requestingUserId;
|
||||
|
||||
return new Promise<T>((resolve, reject) => {
|
||||
request({
|
||||
method: method,
|
||||
url: url,
|
||||
qs: qs,
|
||||
json: body,
|
||||
headers: {
|
||||
"Authorization": `Bearer ${bridge.sharedSecret}`,
|
||||
},
|
||||
}, (err, res, _body) => {
|
||||
try {
|
||||
if (err) {
|
||||
LogService.error("HookshotBridge", "Error calling" + url);
|
||||
LogService.error("HookshotBridge", err);
|
||||
reject(err);
|
||||
} else if (!res) {
|
||||
LogService.error("HookshotBridge", "There is no response for " + url);
|
||||
reject(new Error("No response provided - is the service online?"));
|
||||
} else if (res.statusCode !== 200 && res.statusCode !== 202) {
|
||||
LogService.error("HookshotBridge", "Got status code " + res.statusCode + " when calling " + url);
|
||||
LogService.error("HookshotBridge", res.body);
|
||||
if (typeof (res.body) === "string") res.body = JSON.parse(res.body);
|
||||
reject({errBody: res.body, error: new Error("Request failed")});
|
||||
} else {
|
||||
if (typeof (res.body) === "string") res.body = JSON.parse(res.body);
|
||||
resolve(res.body);
|
||||
}
|
||||
} catch (e) {
|
||||
LogService.error("HookshotBridge", e);
|
||||
reject(e);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
|
@ -1,18 +1,17 @@
|
|||
import { LogService } from "matrix-bot-sdk";
|
||||
import * as request from "request";
|
||||
import HookshotGithubBridgeRecord from "../db/models/HookshotGithubBridgeRecord";
|
||||
import {
|
||||
HookshotConnection,
|
||||
HookshotConnectionsResponse,
|
||||
HookshotGithubRoomConfig,
|
||||
HookshotTypes
|
||||
} from "./models/hookshot";
|
||||
import { HookshotBridge } from "./HookshotBridge";
|
||||
|
||||
export class HookshotGithubBridge {
|
||||
constructor(private requestingUserId: string) {
|
||||
export class HookshotGithubBridge extends HookshotBridge {
|
||||
constructor(requestingUserId: string) {
|
||||
super(requestingUserId);
|
||||
}
|
||||
|
||||
private async getDefaultBridge(): Promise<HookshotGithubBridgeRecord> {
|
||||
protected async getDefaultBridge(): Promise<HookshotGithubBridgeRecord> {
|
||||
const bridges = await HookshotGithubBridgeRecord.findAll({where: {isEnabled: true}});
|
||||
if (!bridges || bridges.length !== 1) {
|
||||
throw new Error("No bridges or too many bridges found");
|
||||
|
@ -21,24 +20,19 @@ export class HookshotGithubBridge {
|
|||
return bridges[0];
|
||||
}
|
||||
|
||||
public async getBotUserId(): Promise<string> {
|
||||
const confs = await this.getAllServiceInformation();
|
||||
const conf = confs.find(c => c.eventType === HookshotTypes.Github);
|
||||
return conf?.botUserId;
|
||||
}
|
||||
|
||||
public async isBridgingEnabled(): Promise<boolean> {
|
||||
const bridges = await HookshotGithubBridgeRecord.findAll({where: {isEnabled: true}});
|
||||
return !!bridges && bridges.length > 0;
|
||||
return !!bridges && bridges.length > 0 && !!(await this.getBotUserId());
|
||||
}
|
||||
|
||||
public async getRoomConfigurations(inRoomId: string): Promise<HookshotGithubRoomConfig[]> {
|
||||
const bridge = await this.getDefaultBridge();
|
||||
|
||||
try {
|
||||
const connections = await this.doProvisionRequest<HookshotConnectionsResponse>(bridge, "GET", `/v1/${inRoomId}/connections`);
|
||||
return connections.filter(c => c.type === HookshotTypes.Github);
|
||||
} catch (e) {
|
||||
if (e.errBody['error'] === "Could not determine if the user is in the room.") {
|
||||
return [];
|
||||
}
|
||||
|
||||
throw e;
|
||||
}
|
||||
return (await this.getAllRoomConfigurations(inRoomId)).filter(c => c.eventType === HookshotTypes.Github);
|
||||
}
|
||||
|
||||
public async bridgeRoom(roomId: string): Promise<HookshotGithubRoomConfig> {
|
||||
|
@ -52,50 +46,4 @@ export class HookshotGithubBridge {
|
|||
const bridge = await this.getDefaultBridge();
|
||||
await this.doProvisionRequest(bridge, "DELETE", `/v1/${roomId}/connections/${connectionId}`);
|
||||
}
|
||||
|
||||
private async doProvisionRequest<T>(bridge: HookshotGithubBridgeRecord, method: string, endpoint: string, qs?: any, body?: any): Promise<T> {
|
||||
const provisionUrl = bridge.provisionUrl;
|
||||
const apiUrl = provisionUrl.endsWith("/") ? provisionUrl.substring(0, provisionUrl.length - 1) : provisionUrl;
|
||||
const url = apiUrl + (endpoint.startsWith("/") ? endpoint : "/" + endpoint);
|
||||
LogService.info("TelegramBridge", "Doing provision Github Hookshot Bridge request: " + url);
|
||||
|
||||
if (!qs) qs = {};
|
||||
|
||||
if (qs["userId"] === false) delete qs["userId"];
|
||||
else if (!qs["userId"]) qs["userId"] = this.requestingUserId;
|
||||
|
||||
return new Promise<T>((resolve, reject) => {
|
||||
request({
|
||||
method: method,
|
||||
url: url,
|
||||
qs: qs,
|
||||
json: body,
|
||||
headers: {
|
||||
"Authorization": `Bearer ${bridge.sharedSecret}`,
|
||||
},
|
||||
}, (err, res, _body) => {
|
||||
try {
|
||||
if (err) {
|
||||
LogService.error("GithubHookshotBridge", "Error calling" + url);
|
||||
LogService.error("GithubHookshotBridge", err);
|
||||
reject(err);
|
||||
} else if (!res) {
|
||||
LogService.error("GithubHookshotBridge", "There is no response for " + url);
|
||||
reject(new Error("No response provided - is the service online?"));
|
||||
} else if (res.statusCode !== 200 && res.statusCode !== 202) {
|
||||
LogService.error("GithubHookshotBridge", "Got status code " + res.statusCode + " when calling " + url);
|
||||
LogService.error("GithubHookshotBridge", res.body);
|
||||
if (typeof (res.body) === "string") res.body = JSON.parse(res.body);
|
||||
reject({errBody: res.body, error: new Error("Request failed")});
|
||||
} else {
|
||||
if (typeof (res.body) === "string") res.body = JSON.parse(res.body);
|
||||
resolve(res.body);
|
||||
}
|
||||
} catch (e) {
|
||||
LogService.error("GithubHookshotBridge", e);
|
||||
reject(e);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
50
src/bridges/HookshotJiraBridge.ts
Normal file
50
src/bridges/HookshotJiraBridge.ts
Normal file
|
@ -0,0 +1,50 @@
|
|||
import HookshotJiraBridgeRecord from "../db/models/HookshotJiraBridgeRecord";
|
||||
import {
|
||||
HookshotConnection,
|
||||
HookshotGithubRoomConfig,
|
||||
HookshotJiraRoomConfig,
|
||||
HookshotTypes
|
||||
} from "./models/hookshot";
|
||||
import { HookshotBridge } from "./HookshotBridge";
|
||||
|
||||
export class HookshotJiraBridge extends HookshotBridge {
|
||||
constructor(requestingUserId: string) {
|
||||
super(requestingUserId);
|
||||
}
|
||||
|
||||
protected async getDefaultBridge(): Promise<HookshotJiraBridgeRecord> {
|
||||
const bridges = await HookshotJiraBridgeRecord.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.Jira);
|
||||
return conf?.botUserId;
|
||||
}
|
||||
|
||||
public async isBridgingEnabled(): Promise<boolean> {
|
||||
const bridges = await HookshotJiraBridgeRecord.findAll({where: {isEnabled: true}});
|
||||
return !!bridges && bridges.length > 0 && !!(await this.getBotUserId());
|
||||
}
|
||||
|
||||
public async getRoomConfigurations(inRoomId: string): Promise<HookshotGithubRoomConfig[]> {
|
||||
return (await this.getAllRoomConfigurations(inRoomId)).filter(c => c.eventType === HookshotTypes.Jira);
|
||||
}
|
||||
|
||||
public async bridgeRoom(roomId: string): Promise<HookshotJiraRoomConfig> {
|
||||
const bridge = await this.getDefaultBridge();
|
||||
|
||||
const body = {};
|
||||
return await this.doProvisionRequest<HookshotConnection>(bridge, "PUT", `/v1/${roomId}/connections/${HookshotTypes.Jira}`, null, body);
|
||||
}
|
||||
|
||||
public async unbridgeRoom(roomId: string, connectionId: string): Promise<void> {
|
||||
const bridge = await this.getDefaultBridge();
|
||||
await this.doProvisionRequest(bridge, "DELETE", `/v1/${roomId}/connections/${connectionId}`);
|
||||
}
|
||||
}
|
|
@ -1,16 +1,37 @@
|
|||
export interface HookshotConnection {
|
||||
type: string;
|
||||
eventType: string; // state key in the connection
|
||||
id: string;
|
||||
service: string; // human-readable
|
||||
details: any; // context-specific
|
||||
botUserId: string;
|
||||
config: any; // context-specific
|
||||
}
|
||||
|
||||
export type HookshotConnectionsResponse = HookshotConnection[];
|
||||
|
||||
export interface HookshotConnectionTypeDefinition {
|
||||
type: string; // name of connection
|
||||
eventType: string; // state key in the connection
|
||||
service: string; // human-readable
|
||||
botUserId: string;
|
||||
}
|
||||
|
||||
export interface HookshotGithubRoomConfig {
|
||||
|
||||
}
|
||||
|
||||
export enum SupportedJiraEventType {
|
||||
IssueCreated = "issue.created",
|
||||
}
|
||||
|
||||
export interface HookshotJiraRoomConfig {
|
||||
id: string;
|
||||
url: string;
|
||||
events: SupportedJiraEventType[];
|
||||
commandPrefix: string;
|
||||
}
|
||||
|
||||
export enum HookshotTypes {
|
||||
Github = "uk.half-shot.matrix-hookshot.github.repository",
|
||||
Jira = "uk.half-shot.matrix-hookshot.jira.project",
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import {
|
||||
Bridge, HookshotGithubBridgeConfiguration,
|
||||
Bridge, HookshotGithubBridgeConfiguration, HookshotJiraBridgeConfiguration,
|
||||
SlackBridgeConfiguration,
|
||||
TelegramBridgeConfiguration,
|
||||
WebhookBridgeConfiguration
|
||||
|
@ -11,6 +11,7 @@ import { TelegramBridge } from "../bridges/TelegramBridge";
|
|||
import { WebhooksBridge } from "../bridges/WebhooksBridge";
|
||||
import { SlackBridge } from "../bridges/SlackBridge";
|
||||
import { HookshotGithubBridge } from "../bridges/HookshotGithubBridge";
|
||||
import { HookshotJiraBridge } from "../bridges/HookshotJiraBridge";
|
||||
|
||||
export class BridgeStore {
|
||||
|
||||
|
@ -60,7 +61,7 @@ export class BridgeStore {
|
|||
const record = await BridgeRecord.findOne({where: {type: integrationType}});
|
||||
if (!record) throw new Error("Bridge not found");
|
||||
|
||||
const hasDedicatedApi = ["irc", "telegram", "webhooks", "slack", "hookshot_github"];
|
||||
const hasDedicatedApi = ["irc", "telegram", "webhooks", "slack", "hookshot_github", "hookshot_jira"];
|
||||
if (hasDedicatedApi.indexOf(integrationType) !== -1) {
|
||||
throw new Error("This bridge should be modified with the dedicated API");
|
||||
} else throw new Error("Unsupported bridge");
|
||||
|
@ -82,6 +83,9 @@ export class BridgeStore {
|
|||
} else if (record.type === "hookshot_github") {
|
||||
const hookshot = new HookshotGithubBridge(requestingUserId);
|
||||
return hookshot.isBridgingEnabled();
|
||||
} else if (record.type === "hookshot_jira") {
|
||||
const hookshot = new HookshotJiraBridge(requestingUserId);
|
||||
return hookshot.isBridgingEnabled();
|
||||
} else return true;
|
||||
}
|
||||
|
||||
|
@ -101,6 +105,9 @@ export class BridgeStore {
|
|||
} else if (record.type === "hookshot_github") {
|
||||
const hookshot = new HookshotGithubBridge(requestingUserId);
|
||||
return hookshot.isBridgingEnabled();
|
||||
} else if (record.type === "hookshot_jira") {
|
||||
const hookshot = new HookshotJiraBridge(requestingUserId);
|
||||
return hookshot.isBridgingEnabled();
|
||||
} else return false;
|
||||
}
|
||||
|
||||
|
@ -141,9 +148,19 @@ export class BridgeStore {
|
|||
} else if (record.type === "hookshot_github") {
|
||||
if (!inRoomId) return {}; // The bridge's admin config is handled by other APIs
|
||||
const hookshot = new HookshotGithubBridge(requestingUserId);
|
||||
const botUserId = await hookshot.getBotUserId();
|
||||
const connections = await hookshot.getRoomConfigurations(inRoomId);
|
||||
return <HookshotGithubBridgeConfiguration>{
|
||||
botUserId: "@hookshot_bot:localhost", // TODO
|
||||
botUserId: botUserId,
|
||||
connections: connections,
|
||||
};
|
||||
} else if (record.type === "hookshot_jira") {
|
||||
if (!inRoomId) return {}; // The bridge's admin config is handled by other APIs
|
||||
const hookshot = new HookshotJiraBridge(requestingUserId);
|
||||
const botUserId = await hookshot.getBotUserId();
|
||||
const connections = await hookshot.getRoomConfigurations(inRoomId);
|
||||
return <HookshotJiraBridgeConfiguration>{
|
||||
botUserId: botUserId,
|
||||
connections: connections,
|
||||
};
|
||||
} else return {};
|
||||
|
|
|
@ -30,6 +30,7 @@ import TermsTextRecord from "./models/TermsTextRecord";
|
|||
import TermsSignedRecord from "./models/TermsSignedRecord";
|
||||
import TermsUpstreamRecord from "./models/TermsUpstreamRecord";
|
||||
import HookshotGithubBridgeRecord from "./models/HookshotGithubBridgeRecord";
|
||||
import HookshotJiraBridgeRecord from "./models/HookshotJiraBridgeRecord";
|
||||
|
||||
class _DimensionStore {
|
||||
private sequelize: Sequelize;
|
||||
|
@ -77,6 +78,7 @@ class _DimensionStore {
|
|||
TermsSignedRecord,
|
||||
TermsUpstreamRecord,
|
||||
HookshotGithubBridgeRecord,
|
||||
HookshotJiraBridgeRecord,
|
||||
]);
|
||||
}
|
||||
|
||||
|
|
23
src/db/migrations/20211130153845-AddHookshotJiraBridge.ts
Normal file
23
src/db/migrations/20211130153845-AddHookshotJiraBridge.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_jira_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_jira_bridges"));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
import { QueryInterface } from "sequelize";
|
||||
|
||||
export default {
|
||||
up: (queryInterface: QueryInterface) => {
|
||||
return Promise.resolve()
|
||||
.then(() => queryInterface.bulkInsert("dimension_bridges", [
|
||||
{
|
||||
type: "hookshot_jira",
|
||||
name: "Jira Bridge",
|
||||
avatarUrl: "/assets/img/avatars/jira.png",
|
||||
isEnabled: true,
|
||||
isPublic: true,
|
||||
description: "Bridges Jira issues to Matrix",
|
||||
},
|
||||
]));
|
||||
},
|
||||
down: (queryInterface: QueryInterface) => {
|
||||
return Promise.resolve()
|
||||
.then(() => queryInterface.bulkDelete("dimension_bridges", {
|
||||
type: "hookshot_jira",
|
||||
}));
|
||||
}
|
||||
}
|
|
@ -1,12 +1,13 @@
|
|||
import { AllowNull, AutoIncrement, Column, ForeignKey, Model, PrimaryKey, Table } from "sequelize-typescript";
|
||||
import Upstream from "./Upstream";
|
||||
import { IHookshotBridgeRecord } from "./IHookshotBridgeRecord";
|
||||
|
||||
@Table({
|
||||
tableName: "dimension_hookshot_github_bridges",
|
||||
underscored: false,
|
||||
timestamps: false,
|
||||
})
|
||||
export default class HookshotGithubBridgeRecord extends Model {
|
||||
export default class HookshotGithubBridgeRecord extends Model implements IHookshotBridgeRecord {
|
||||
@PrimaryKey
|
||||
@AutoIncrement
|
||||
@Column
|
||||
|
|
31
src/db/models/HookshotJiraBridgeRecord.ts
Normal file
31
src/db/models/HookshotJiraBridgeRecord.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_jira_bridges",
|
||||
underscored: false,
|
||||
timestamps: false,
|
||||
})
|
||||
export default class HookshotJiraBridgeRecord 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;
|
||||
}
|
8
src/db/models/IHookshotBridgeRecord.ts
Normal file
8
src/db/models/IHookshotBridgeRecord.ts
Normal file
|
@ -0,0 +1,8 @@
|
|||
import { AllowNull, Column } from "sequelize-typescript";
|
||||
|
||||
export interface IHookshotBridgeRecord {
|
||||
upstreamId?: number;
|
||||
provisionUrl?: string;
|
||||
sharedSecret?: string;
|
||||
isEnabled: boolean;
|
||||
}
|
|
@ -51,3 +51,8 @@ export interface HookshotGithubBridgeConfiguration {
|
|||
botUserId: string;
|
||||
connections: HookshotConnection[];
|
||||
}
|
||||
|
||||
export interface HookshotJiraBridgeConfiguration {
|
||||
botUserId: string;
|
||||
connections: HookshotConnection[];
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
{{'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:9999/_matrix/provision/v1"
|
||||
placeholder="http://localhost:9000"
|
||||
[(ngModel)]="provisionUrl" [disabled]="isSaving"/>
|
||||
</label>
|
||||
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
<div *ngIf="isLoading">
|
||||
<my-spinner></my-spinner>
|
||||
</div>
|
||||
<div *ngIf="!isLoading">
|
||||
<my-ibox boxTitle="Jira 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 Jira as an integration. If enabled in the configuration, it can be used here to offer a UI for setting up a room to pipe to a repository.' | 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 {
|
||||
AdminHookshotJiraBridgeManageSelfhostedComponent,
|
||||
ManageSelfhostedHookshotJiraBridgeDialogContext
|
||||
} from "./manage-selfhosted/manage-selfhosted.component";
|
||||
import { TranslateService } from "@ngx-translate/core";
|
||||
import { NgbModal } from "@ng-bootstrap/ng-bootstrap";
|
||||
import { FE_HookshotJiraBridge } from "../../../shared/models/hookshot_jira";
|
||||
import { AdminHookshotJiraApiService } from "../../../shared/services/admin/admin-hookshot-jira-api.service";
|
||||
|
||||
@Component({
|
||||
templateUrl: "./hookshot-jira.component.html",
|
||||
styleUrls: ["./hookshot-jira.component.scss"],
|
||||
})
|
||||
export class AdminHookshotJiraBridgeComponent implements OnInit {
|
||||
|
||||
public isLoading = true;
|
||||
public isUpdating = false;
|
||||
public configurations: FE_HookshotJiraBridge[] = [];
|
||||
|
||||
constructor(private hookshotApi: AdminHookshotJiraApiService,
|
||||
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(AdminHookshotJiraBridgeManageSelfhostedComponent, {
|
||||
backdrop: 'static',
|
||||
size: 'lg',
|
||||
});
|
||||
selfhostedRef.result.then(() => {
|
||||
try {
|
||||
this.reload()
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
this.translate.get('Failed to get an updated Jira bridge list').subscribe((res: string) => {
|
||||
this.toaster.pop("error", res);
|
||||
});
|
||||
}
|
||||
})
|
||||
const selfhostedInstance = selfhostedRef.componentInstance as ManageSelfhostedHookshotJiraBridgeDialogContext;
|
||||
selfhostedInstance.provisionUrl = '';
|
||||
selfhostedInstance.sharedSecret = '';
|
||||
}
|
||||
|
||||
public editBridge(bridge: FE_HookshotJiraBridge) {
|
||||
const selfhostedRef = this.modal.open(AdminHookshotJiraBridgeManageSelfhostedComponent, {
|
||||
backdrop: 'static',
|
||||
size: 'lg',
|
||||
});
|
||||
selfhostedRef.result.then(() => {
|
||||
try {
|
||||
this.reload()
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
this.translate.get('Failed to get an updated Jira bridge list').subscribe((res: string) => {
|
||||
this.toaster.pop("error", res);
|
||||
});
|
||||
}
|
||||
})
|
||||
const selfhostedInstance = selfhostedRef.componentInstance as ManageSelfhostedHookshotJiraBridgeDialogContext;
|
||||
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 Jira 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 { AdminHookshotJiraApiService } from "../../../../shared/services/admin/admin-hookshot-jira-api.service";
|
||||
|
||||
export interface ManageSelfhostedHookshotJiraBridgeDialogContext {
|
||||
provisionUrl: string;
|
||||
sharedSecret: string;
|
||||
bridgeId: number;
|
||||
isAdding: boolean;
|
||||
}
|
||||
|
||||
@Component({
|
||||
templateUrl: "./manage-selfhosted.component.html",
|
||||
styleUrls: ["./manage-selfhosted.component.scss"],
|
||||
})
|
||||
export class AdminHookshotJiraBridgeManageSelfhostedComponent {
|
||||
|
||||
isSaving = false;
|
||||
provisionUrl: string;
|
||||
sharedSecret: string;
|
||||
bridgeId: number;
|
||||
isAdding = true;
|
||||
|
||||
constructor(public modal: NgbActiveModal,
|
||||
private hookshotApi: AdminHookshotJiraApiService,
|
||||
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('Jira 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 Jira bridge').subscribe((res: string) => {
|
||||
this.toaster.pop("error", res);
|
||||
});
|
||||
});
|
||||
} else {
|
||||
this.hookshotApi.updateSelfhosted(this.bridgeId, this.provisionUrl, this.sharedSecret).then(() => {
|
||||
this.translate.get('Jira 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 Jira bridge').subscribe((res: string) => {
|
||||
this.toaster.pop("error", res);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
|
@ -124,6 +124,13 @@ import { AdminHookshotGithubBridgeManageSelfhostedComponent } from "./admin/brid
|
|||
import { AdminHookshotGithubApiService } from "./shared/services/admin/admin-hookshot-github-api.service";
|
||||
import { HookshotGithubApiService } from "./shared/services/integrations/hookshot-github-api.service";
|
||||
import { HookshotGithubBridgeConfigComponent } from "./configs/bridge/hookshot-github/hookshot-github.bridge.component";
|
||||
import { AdminHookshotJiraBridgeComponent } from "./admin/bridges/hookshot-jira/hookshot-jira.component";
|
||||
import {
|
||||
AdminHookshotJiraBridgeManageSelfhostedComponent
|
||||
} from "./admin/bridges/hookshot-jira/manage-selfhosted/manage-selfhosted.component";
|
||||
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";
|
||||
|
||||
// AoT requires an exported function for factories
|
||||
export function HttpLoaderFactory(http: HttpClient) {
|
||||
|
@ -236,6 +243,9 @@ export function HttpLoaderFactory(http: HttpClient) {
|
|||
AdminHookshotGithubBridgeComponent,
|
||||
AdminHookshotGithubBridgeManageSelfhostedComponent,
|
||||
HookshotGithubBridgeConfigComponent,
|
||||
AdminHookshotJiraBridgeComponent,
|
||||
AdminHookshotJiraBridgeManageSelfhostedComponent,
|
||||
HookshotJiraBridgeConfigComponent,
|
||||
|
||||
// Vendor
|
||||
],
|
||||
|
@ -267,6 +277,8 @@ export function HttpLoaderFactory(http: HttpClient) {
|
|||
AdminTermsApiService,
|
||||
AdminHookshotGithubApiService,
|
||||
HookshotGithubApiService,
|
||||
AdminHookshotJiraApiService,
|
||||
HookshotJiraApiService,
|
||||
{provide: Window, useValue: window},
|
||||
|
||||
// Vendor
|
||||
|
@ -292,7 +304,7 @@ export function HttpLoaderFactory(http: HttpClient) {
|
|||
AdminSlackBridgeManageSelfhostedComponent,
|
||||
AdminLogoutConfirmationDialogComponent,
|
||||
AdminTermsNewEditPublishDialogComponent,
|
||||
AdminWidgetWhiteboardConfigComponent
|
||||
AdminWidgetWhiteboardConfigComponent,
|
||||
]
|
||||
})
|
||||
export class AppModule {
|
||||
|
|
|
@ -50,11 +50,13 @@ import { TermsWidgetWrapperComponent } from "./widget-wrappers/terms/terms.compo
|
|||
import { WhiteboardWidgetComponent } from "./configs/widget/whiteboard/whiteboard.widget.component";
|
||||
import { AdminHookshotGithubBridgeComponent } from "./admin/bridges/hookshot-github/hookshot-github.component";
|
||||
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";
|
||||
|
||||
const routes: Routes = [
|
||||
{path: "", component: HomeComponent},
|
||||
{path: "riot", pathMatch: "full", redirectTo: "riot-app"},
|
||||
{path: "element", pathMatch: "full", redirectTo: "riot-app"},
|
||||
{path: "riot", pathMatch: "full", redirectTo: "riot-app", data: {breadcrumb: "Home", name: "Dimension"}},
|
||||
{path: "element", pathMatch: "full", redirectTo: "riot-app", data: {breadcrumb: "Home", name: "Dimension"}},
|
||||
{
|
||||
path: "riot-app",
|
||||
component: RiotComponent,
|
||||
|
@ -141,6 +143,11 @@ const routes: Routes = [
|
|||
component: AdminHookshotGithubBridgeComponent,
|
||||
data: {breadcrumb: "Github Bridge", name: "Github Bridge"},
|
||||
},
|
||||
{
|
||||
path: "hookshot_jira",
|
||||
component: AdminHookshotJiraBridgeComponent,
|
||||
data: {breadcrumb: "Jira Bridge", name: "Jira Bridge"},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
@ -243,6 +250,7 @@ const routes: Routes = [
|
|||
},
|
||||
{
|
||||
path: "complex-bot",
|
||||
data: {breadcrumb: {skip: true}},
|
||||
children: [
|
||||
{
|
||||
path: "rss",
|
||||
|
@ -258,6 +266,7 @@ const routes: Routes = [
|
|||
},
|
||||
{
|
||||
path: "bridge",
|
||||
data: {breadcrumb: {skip: true}},
|
||||
children: [
|
||||
{
|
||||
path: "irc",
|
||||
|
@ -284,6 +293,11 @@ const routes: Routes = [
|
|||
component: HookshotGithubBridgeConfigComponent,
|
||||
data: {breadcrumb: "Github Bridge Configuration", name: "Github Bridge Configuration"},
|
||||
},
|
||||
{
|
||||
path: "hookshot_jira",
|
||||
component: HookshotJiraBridgeConfigComponent,
|
||||
data: {breadcrumb: "Jira Bridge Configuration", name: "Jira Bridge Configuration"},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
@ -295,6 +309,7 @@ const routes: Routes = [
|
|||
},
|
||||
{
|
||||
path: "widgets",
|
||||
data: {breadcrumb: {skip: true}},
|
||||
children: [
|
||||
{path: "terms/:shortcode/:lang/:version", component: TermsWidgetWrapperComponent},
|
||||
{path: "generic", component: GenericWidgetWrapperComponent},
|
||||
|
|
|
@ -0,0 +1,44 @@
|
|||
<my-bridge-config [bridgeComponent]="this">
|
||||
<ng-template #bridgeParamsTemplate>
|
||||
<my-ibox [isCollapsible]="false">
|
||||
<h5 class="my-ibox-title">
|
||||
{{'Bridge to Jira' | translate}}
|
||||
</h5>
|
||||
<div class="my-ibox-content" *ngIf="loadingConnections">
|
||||
<my-spinner></my-spinner>
|
||||
</div>
|
||||
<div class="my-ibox-content" *ngIf="!loadingConnections">
|
||||
<div *ngIf="!isBridged && needsAuth">
|
||||
<p>
|
||||
{{'In order to bridge to Jira, you\'ll need to authorize the bridge to access your organization(s). Please click the button below to do so.' | translate}}
|
||||
</p>
|
||||
<a [href]="authUrl" rel="noopener" target="_blank">
|
||||
<img src="/assets/img/slack_auth_button.png" alt="sign in with slack"/>
|
||||
</a>
|
||||
</div>
|
||||
<div *ngIf="!isBridged && !needsAuth">
|
||||
<label class="label-block">
|
||||
{{'Organization' | translate}}
|
||||
<select class="form-control form-control-sm" [(ngModel)]="orgId"
|
||||
(change)="loadRepos()" [disabled]="isBusy">
|
||||
<option *ngFor="let org of orgs" [ngValue]="org">
|
||||
{{ org }}
|
||||
</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="label-block">
|
||||
{{'Repository' | translate}}
|
||||
<select class="form-control form-control-sm" [(ngModel)]="repoId" [disabled]="isBusy">
|
||||
<option *ngFor="let repo of repos" [ngValue]="repo">
|
||||
{{ repo }}
|
||||
</option>
|
||||
</select>
|
||||
</label>
|
||||
<button type="button" class="btn btn-sm btn-primary" [disabled]="isBusy" (click)="bridgeRoom()">
|
||||
Bridge
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</my-ibox>
|
||||
</ng-template>
|
||||
</my-bridge-config>
|
|
@ -0,0 +1,4 @@
|
|||
.actions-col {
|
||||
width: 120px;
|
||||
text-align: center;
|
||||
}
|
|
@ -0,0 +1,99 @@
|
|||
import { Component, OnInit } from "@angular/core";
|
||||
import { BridgeComponent } from "../bridge.component";
|
||||
import { ScalarClientApiService } from "../../../shared/services/scalar/scalar-client-api.service";
|
||||
import { SafeUrl } from "@angular/platform-browser";
|
||||
import { TranslateService } from "@ngx-translate/core";
|
||||
import { HookshotJiraApiService } from "../../../shared/services/integrations/hookshot-jira-api.service";
|
||||
import { FE_HookshotJiraConnection } from "../../../shared/models/hookshot_jira";
|
||||
|
||||
interface HookshotConfig {
|
||||
botUserId: string;
|
||||
connections: FE_HookshotJiraConnection[];
|
||||
}
|
||||
|
||||
@Component({
|
||||
templateUrl: "hookshot-jira.bridge.component.html",
|
||||
styleUrls: ["hookshot-jira.bridge.component.scss"],
|
||||
})
|
||||
export class HookshotJiraBridgeConfigComponent extends BridgeComponent<HookshotConfig> implements OnInit {
|
||||
|
||||
public isBusy: boolean;
|
||||
public needsAuth = false;
|
||||
public authUrl: SafeUrl;
|
||||
public loadingConnections = false;
|
||||
public orgs: string[] = [];
|
||||
public repos: string[] = []; // for org
|
||||
public orgId: string;
|
||||
public repoId: string;
|
||||
|
||||
constructor(private hookshot: HookshotJiraApiService, private scalar: ScalarClientApiService, public translate: TranslateService) {
|
||||
super("hookshot_jira", translate);
|
||||
this.translate = translate;
|
||||
}
|
||||
|
||||
public ngOnInit() {
|
||||
super.ngOnInit();
|
||||
|
||||
this.prepare();
|
||||
}
|
||||
|
||||
private prepare() {
|
||||
|
||||
}
|
||||
|
||||
public loadRepos() {
|
||||
// TODO
|
||||
}
|
||||
|
||||
public get isBridged(): boolean {
|
||||
return this.bridge.config.connections.length > 0;
|
||||
}
|
||||
|
||||
public async bridgeRoom(): Promise<any> {
|
||||
this.isBusy = true;
|
||||
|
||||
try {
|
||||
await this.scalar.inviteUser(this.roomId, this.bridge.config.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.hookshot.bridgeRoom(this.roomId).then(conn => {
|
||||
this.bridge.config.connections.push(conn);
|
||||
this.isBusy = false;
|
||||
this.translate.get('Bridge requested').subscribe((res: string) => {
|
||||
this.toaster.pop("success", res);
|
||||
});
|
||||
}).catch(error => {
|
||||
this.isBusy = false;
|
||||
console.error(error);
|
||||
this.translate.get('Error requesting bridge').subscribe((res: string) => {
|
||||
this.toaster.pop("error", res);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public unbridgeRoom(): void {
|
||||
this.isBusy = true;
|
||||
this.hookshot.unbridgeRoom(this.roomId).then(() => {
|
||||
this.bridge.config.connections = [];
|
||||
this.isBusy = false;
|
||||
this.translate.get('Bridge removed').subscribe((res: string) => {
|
||||
this.toaster.pop("success", res);
|
||||
});
|
||||
}).catch(error => {
|
||||
this.isBusy = false;
|
||||
console.error(error);
|
||||
this.translate.get('Error removing bridge').subscribe((res: string) => {
|
||||
this.toaster.pop("error", res);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
11
web/app/shared/models/hookshot_jira.ts
Normal file
11
web/app/shared/models/hookshot_jira.ts
Normal file
|
@ -0,0 +1,11 @@
|
|||
export interface FE_HookshotJiraBridge {
|
||||
id: number;
|
||||
upstreamId?: number;
|
||||
provisionUrl?: string;
|
||||
sharedSecret?: string;
|
||||
isEnabled: boolean;
|
||||
}
|
||||
|
||||
export interface FE_HookshotJiraConnection {
|
||||
|
||||
}
|
|
@ -32,6 +32,7 @@ export class IntegrationsRegistry {
|
|||
"webhooks": {},
|
||||
"slack": {},
|
||||
"hookshot_github": {},
|
||||
"hookshot_jira": {},
|
||||
},
|
||||
"widget": {
|
||||
"custom": {
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
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_HookshotJiraBridge } from "../../models/hookshot_jira";
|
||||
|
||||
@Injectable()
|
||||
export class AdminHookshotJiraApiService extends AuthedApi {
|
||||
constructor(http: HttpClient) {
|
||||
super(http);
|
||||
}
|
||||
|
||||
public getBridges(): Promise<FE_HookshotJiraBridge[]> {
|
||||
return this.authedGet<FE_HookshotJiraBridge[]>("/api/v1/dimension/admin/hookshot/jira/all").toPromise();
|
||||
}
|
||||
|
||||
public getBridge(bridgeId: number): Promise<FE_HookshotJiraBridge> {
|
||||
return this.authedGet<FE_HookshotJiraBridge>("/api/v1/dimension/admin/hookshot/jira/" + bridgeId).toPromise();
|
||||
}
|
||||
|
||||
public newFromUpstream(upstream: FE_Upstream): Promise<FE_HookshotJiraBridge> {
|
||||
return this.authedPost<FE_HookshotJiraBridge>("/api/v1/dimension/admin/hookshot/jira/new/upstream", {upstreamId: upstream.id}).toPromise();
|
||||
}
|
||||
|
||||
public newSelfhosted(provisionUrl: string, sharedSecret: string): Promise<FE_HookshotJiraBridge> {
|
||||
return this.authedPost<FE_HookshotJiraBridge>("/api/v1/dimension/admin/hookshot/jira/new/selfhosted", {
|
||||
provisionUrl: provisionUrl,
|
||||
sharedSecret: sharedSecret,
|
||||
}).toPromise();
|
||||
}
|
||||
|
||||
public updateSelfhosted(bridgeId: number, provisionUrl: string, sharedSecret: string): Promise<FE_HookshotJiraBridge> {
|
||||
return this.authedPost<FE_HookshotJiraBridge>("/api/v1/dimension/admin/hookshot/jira/" + bridgeId, {
|
||||
provisionUrl: provisionUrl,
|
||||
sharedSecret: sharedSecret,
|
||||
}).toPromise();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
import { Injectable } from "@angular/core";
|
||||
import { AuthedApi } from "../authed-api";
|
||||
import { HttpClient } from "@angular/common/http";
|
||||
import { FE_HookshotJiraConnection } from "../../models/hookshot_jira";
|
||||
|
||||
@Injectable()
|
||||
export class HookshotJiraApiService extends AuthedApi {
|
||||
constructor(http: HttpClient) {
|
||||
super(http);
|
||||
}
|
||||
|
||||
public bridgeRoom(roomId: string): Promise<FE_HookshotJiraConnection> {
|
||||
return this.authedPost<FE_HookshotJiraConnection>("/api/v1/dimension/hookshot/jira/room/" + roomId + "/connect", {
|
||||
// TODO
|
||||
}).toPromise();
|
||||
}
|
||||
|
||||
public unbridgeRoom(roomId: string): Promise<any> {
|
||||
return this.authedDelete("/api/v1/dimension/hookshot/jira/" + roomId + "/connections/all").toPromise();
|
||||
}
|
||||
}
|
|
@ -25,6 +25,8 @@
|
|||
"The shared secret defined in the configuration for provisioning.": "The shared secret defined in the configuration for provisioning.",
|
||||
"Save": "Save",
|
||||
"Cancel": "Cancel",
|
||||
"is a multi-purpose bridge which supports Jira as an integration. If enabled in the configuration, it can be used here to offer a UI for setting up a room to pipe to a repository.": "is a multi-purpose bridge which supports Jira as an integration. If enabled in the configuration, it can be used here to offer a UI for setting up a room to pipe to a repository.",
|
||||
"Self-hosted Jira bridges must have": "Self-hosted Jira bridges must have",
|
||||
"Add a new self-hosted IRC Bridge": "Add a new self-hosted IRC Bridge ",
|
||||
"Self-hosted IRC bridges must have": "Self-hosted IRC bridges must have",
|
||||
"The provisioning URL for the bridge. This is usually the same as the URL given in the registration. This API is not authenticated and should be treated with caution.": "The provisioning URL for the bridge. This is usually the same as the URL given in the registration. This API is not authenticated and should be treated with caution.",
|
||||
|
@ -154,6 +156,8 @@
|
|||
"In order to bridge to Github, you'll need to authorize the bridge to access your organization(s). Please click the button below to do so.": "In order to bridge to Github, you'll need to authorize the bridge to access your organization(s). Please click the button below to do so.",
|
||||
"Organization": "Organization",
|
||||
"Repository": "Repository",
|
||||
"Bridge to Jira": "Bridge to Jira",
|
||||
"In order to bridge to Jira, you'll need to authorize the bridge to access your organization(s). Please click the button below to do so.": "In order to bridge to Jira, you'll need to authorize the bridge to access your organization(s). Please click the button below to do so.",
|
||||
"Add an IRC channel": "Add an IRC channel",
|
||||
"Bridging a channel requires authorization from a channel operator. When entering a channel below, a bot will join the channel to ensure it exists and has operators available.": "Bridging a channel requires authorization from a channel operator. When entering a channel below, a bot will join the channel to ensure it exists and has operators available.",
|
||||
"Channel Name": "Channel Name",
|
||||
|
@ -320,6 +324,11 @@
|
|||
"Failed to create Github bridge": "Failed to create Github bridge",
|
||||
"Github bridge updated": "Github bridge updated",
|
||||
"Failed to update Github bridge": "Failed to update Github bridge",
|
||||
"Failed to get an updated Jira bridge list": "Failed to get an updated Jira bridge list",
|
||||
"Jira bridge added": "Jira bridge added",
|
||||
"Failed to create Jira bridge": "Failed to create Jira bridge",
|
||||
"Jira bridge updated": "Jira bridge updated",
|
||||
"Failed to update Jira bridge": "Failed to update Jira bridge",
|
||||
"IRC Bridge added": "IRC Bridge added",
|
||||
"Failed to create IRC bridge": "Failed to create IRC bridge",
|
||||
"Click the pencil icon to enable networks.": "Click the pencil icon to enable networks.",
|
||||
|
|
|
@ -25,6 +25,8 @@
|
|||
"The shared secret defined in the configuration for provisioning.": "The shared secret defined in the configuration for provisioning.",
|
||||
"Save": "Save",
|
||||
"Cancel": "Cancel",
|
||||
"is a multi-purpose bridge which supports Jira as an integration. If enabled in the configuration, it can be used here to offer a UI for setting up a room to pipe to a repository.": "is a multi-purpose bridge which supports Jira as an integration. If enabled in the configuration, it can be used here to offer a UI for setting up a room to pipe to a repository.",
|
||||
"Self-hosted Jira bridges must have": "Self-hosted Jira bridges must have",
|
||||
"Add a new self-hosted IRC Bridge": "Add a new self-hosted IRC Bridge",
|
||||
"Self-hosted IRC bridges must have": "Self-hosted IRC bridges must have",
|
||||
"The provisioning URL for the bridge. This is usually the same as the URL given in the registration. This API is not authenticated and should be treated with caution.": "The provisioning URL for the bridge. This is usually the same as the URL given in the registration. This API is not authenticated and should be treated with caution.",
|
||||
|
@ -154,6 +156,8 @@
|
|||
"In order to bridge to Github, you'll need to authorize the bridge to access your organization(s). Please click the button below to do so.": "In order to bridge to Github, you'll need to authorize the bridge to access your organization(s). Please click the button below to do so.",
|
||||
"Organization": "Organization",
|
||||
"Repository": "Repository",
|
||||
"Bridge to Jira": "Bridge to Jira",
|
||||
"In order to bridge to Jira, you'll need to authorize the bridge to access your organization(s). Please click the button below to do so.": "In order to bridge to Jira, you'll need to authorize the bridge to access your organization(s). Please click the button below to do so.",
|
||||
"Add an IRC channel": "Add an IRC channel",
|
||||
"Bridging a channel requires authorization from a channel operator. When entering a channel below, a bot will join the channel to ensure it exists and has operators available.": "Bridging a channel requires authorization from a channel operator. When entering a channel below, a bot will join the channel to ensure it exists and has operators available.",
|
||||
"Channel Name": "Channel Name",
|
||||
|
@ -320,6 +324,11 @@
|
|||
"Failed to create Github bridge": "Failed to create Github bridge",
|
||||
"Github bridge updated": "Github bridge updated",
|
||||
"Failed to update Github bridge": "Failed to update Github bridge",
|
||||
"Failed to get an updated Jira bridge list": "Failed to get an updated Jira bridge list",
|
||||
"Jira bridge added": "Jira bridge added",
|
||||
"Failed to create Jira bridge": "Failed to create Jira bridge",
|
||||
"Jira bridge updated": "Jira bridge updated",
|
||||
"Failed to update Jira bridge": "Failed to update Jira bridge",
|
||||
"IRC Bridge added": "IRC Bridge added",
|
||||
"Failed to create IRC bridge": "Failed to create IRC bridge",
|
||||
"Click the pencil icon to enable networks.": "Click the pencil icon to enable networks.",
|
||||
|
|
BIN
web/assets/img/avatars/jira.png
Normal file
BIN
web/assets/img/avatars/jira.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 17 KiB |
Loading…
Reference in New Issue
Block a user