mirror of
https://github.com/turt2live/matrix-dimension.git
synced 2024-07-18 17:01:57 +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_STICKERS = "stickers";
|
||||||
export const CACHE_TELEGRAM_BRIDGE = "telegram-bridge";
|
export const CACHE_TELEGRAM_BRIDGE = "telegram-bridge";
|
||||||
export const CACHE_HOOKSHOT_GITHUB_BRIDGE = "hookshot-github-bridge";
|
export const CACHE_HOOKSHOT_GITHUB_BRIDGE = "hookshot-github-bridge";
|
||||||
|
export const CACHE_HOOKSHOT_JIRA_BRIDGE = "hookshot-jira-bridge";
|
||||||
export const CACHE_WEBHOOKS_BRIDGE = "webhooks-bridge";
|
export const CACHE_WEBHOOKS_BRIDGE = "webhooks-bridge";
|
||||||
export const CACHE_SIMPLE_BOTS = "simple-bots";
|
export const CACHE_SIMPLE_BOTS = "simple-bots";
|
||||||
export const CACHE_SLACK_BRIDGE = "slack-bridge";
|
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 HookshotGithubBridgeRecord from "../db/models/HookshotGithubBridgeRecord";
|
||||||
import {
|
import {
|
||||||
HookshotConnection,
|
HookshotConnection,
|
||||||
HookshotConnectionsResponse,
|
|
||||||
HookshotGithubRoomConfig,
|
HookshotGithubRoomConfig,
|
||||||
HookshotTypes
|
HookshotTypes
|
||||||
} from "./models/hookshot";
|
} from "./models/hookshot";
|
||||||
|
import { HookshotBridge } from "./HookshotBridge";
|
||||||
|
|
||||||
export class HookshotGithubBridge {
|
export class HookshotGithubBridge extends HookshotBridge {
|
||||||
constructor(private requestingUserId: string) {
|
constructor(requestingUserId: string) {
|
||||||
|
super(requestingUserId);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getDefaultBridge(): Promise<HookshotGithubBridgeRecord> {
|
protected async getDefaultBridge(): Promise<HookshotGithubBridgeRecord> {
|
||||||
const bridges = await HookshotGithubBridgeRecord.findAll({where: {isEnabled: true}});
|
const bridges = await HookshotGithubBridgeRecord.findAll({where: {isEnabled: true}});
|
||||||
if (!bridges || bridges.length !== 1) {
|
if (!bridges || bridges.length !== 1) {
|
||||||
throw new Error("No bridges or too many bridges found");
|
throw new Error("No bridges or too many bridges found");
|
||||||
|
@ -21,24 +20,19 @@ export class HookshotGithubBridge {
|
||||||
return bridges[0];
|
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> {
|
public async isBridgingEnabled(): Promise<boolean> {
|
||||||
const bridges = await HookshotGithubBridgeRecord.findAll({where: {isEnabled: true}});
|
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[]> {
|
public async getRoomConfigurations(inRoomId: string): Promise<HookshotGithubRoomConfig[]> {
|
||||||
const bridge = await this.getDefaultBridge();
|
return (await this.getAllRoomConfigurations(inRoomId)).filter(c => c.eventType === HookshotTypes.Github);
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async bridgeRoom(roomId: string): Promise<HookshotGithubRoomConfig> {
|
public async bridgeRoom(roomId: string): Promise<HookshotGithubRoomConfig> {
|
||||||
|
@ -52,50 +46,4 @@ export class HookshotGithubBridge {
|
||||||
const bridge = await this.getDefaultBridge();
|
const bridge = await this.getDefaultBridge();
|
||||||
await this.doProvisionRequest(bridge, "DELETE", `/v1/${roomId}/connections/${connectionId}`);
|
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 {
|
export interface HookshotConnection {
|
||||||
type: string;
|
type: string;
|
||||||
|
eventType: string; // state key in the connection
|
||||||
id: string;
|
id: string;
|
||||||
service: string; // human-readable
|
service: string; // human-readable
|
||||||
details: any; // context-specific
|
botUserId: string;
|
||||||
|
config: any; // context-specific
|
||||||
}
|
}
|
||||||
|
|
||||||
export type HookshotConnectionsResponse = HookshotConnection[];
|
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 interface HookshotGithubRoomConfig {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum SupportedJiraEventType {
|
||||||
|
IssueCreated = "issue.created",
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface HookshotJiraRoomConfig {
|
||||||
|
id: string;
|
||||||
|
url: string;
|
||||||
|
events: SupportedJiraEventType[];
|
||||||
|
commandPrefix: string;
|
||||||
|
}
|
||||||
|
|
||||||
export enum HookshotTypes {
|
export enum HookshotTypes {
|
||||||
Github = "uk.half-shot.matrix-hookshot.github.repository",
|
Github = "uk.half-shot.matrix-hookshot.github.repository",
|
||||||
|
Jira = "uk.half-shot.matrix-hookshot.jira.project",
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import {
|
import {
|
||||||
Bridge, HookshotGithubBridgeConfiguration,
|
Bridge, HookshotGithubBridgeConfiguration, HookshotJiraBridgeConfiguration,
|
||||||
SlackBridgeConfiguration,
|
SlackBridgeConfiguration,
|
||||||
TelegramBridgeConfiguration,
|
TelegramBridgeConfiguration,
|
||||||
WebhookBridgeConfiguration
|
WebhookBridgeConfiguration
|
||||||
|
@ -11,6 +11,7 @@ import { TelegramBridge } from "../bridges/TelegramBridge";
|
||||||
import { WebhooksBridge } from "../bridges/WebhooksBridge";
|
import { WebhooksBridge } from "../bridges/WebhooksBridge";
|
||||||
import { SlackBridge } from "../bridges/SlackBridge";
|
import { SlackBridge } from "../bridges/SlackBridge";
|
||||||
import { HookshotGithubBridge } from "../bridges/HookshotGithubBridge";
|
import { HookshotGithubBridge } from "../bridges/HookshotGithubBridge";
|
||||||
|
import { HookshotJiraBridge } from "../bridges/HookshotJiraBridge";
|
||||||
|
|
||||||
export class BridgeStore {
|
export class BridgeStore {
|
||||||
|
|
||||||
|
@ -60,7 +61,7 @@ export class BridgeStore {
|
||||||
const record = await BridgeRecord.findOne({where: {type: integrationType}});
|
const record = await BridgeRecord.findOne({where: {type: integrationType}});
|
||||||
if (!record) throw new Error("Bridge not found");
|
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) {
|
if (hasDedicatedApi.indexOf(integrationType) !== -1) {
|
||||||
throw new Error("This bridge should be modified with the dedicated API");
|
throw new Error("This bridge should be modified with the dedicated API");
|
||||||
} else throw new Error("Unsupported bridge");
|
} else throw new Error("Unsupported bridge");
|
||||||
|
@ -82,6 +83,9 @@ export class BridgeStore {
|
||||||
} else if (record.type === "hookshot_github") {
|
} else if (record.type === "hookshot_github") {
|
||||||
const hookshot = new HookshotGithubBridge(requestingUserId);
|
const hookshot = new HookshotGithubBridge(requestingUserId);
|
||||||
return hookshot.isBridgingEnabled();
|
return hookshot.isBridgingEnabled();
|
||||||
|
} else if (record.type === "hookshot_jira") {
|
||||||
|
const hookshot = new HookshotJiraBridge(requestingUserId);
|
||||||
|
return hookshot.isBridgingEnabled();
|
||||||
} else return true;
|
} else return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -101,6 +105,9 @@ export class BridgeStore {
|
||||||
} else if (record.type === "hookshot_github") {
|
} else if (record.type === "hookshot_github") {
|
||||||
const hookshot = new HookshotGithubBridge(requestingUserId);
|
const hookshot = new HookshotGithubBridge(requestingUserId);
|
||||||
return hookshot.isBridgingEnabled();
|
return hookshot.isBridgingEnabled();
|
||||||
|
} else if (record.type === "hookshot_jira") {
|
||||||
|
const hookshot = new HookshotJiraBridge(requestingUserId);
|
||||||
|
return hookshot.isBridgingEnabled();
|
||||||
} else return false;
|
} else return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -141,9 +148,19 @@ export class BridgeStore {
|
||||||
} else if (record.type === "hookshot_github") {
|
} else if (record.type === "hookshot_github") {
|
||||||
if (!inRoomId) return {}; // The bridge's admin config is handled by other APIs
|
if (!inRoomId) return {}; // The bridge's admin config is handled by other APIs
|
||||||
const hookshot = new HookshotGithubBridge(requestingUserId);
|
const hookshot = new HookshotGithubBridge(requestingUserId);
|
||||||
|
const botUserId = await hookshot.getBotUserId();
|
||||||
const connections = await hookshot.getRoomConfigurations(inRoomId);
|
const connections = await hookshot.getRoomConfigurations(inRoomId);
|
||||||
return <HookshotGithubBridgeConfiguration>{
|
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,
|
connections: connections,
|
||||||
};
|
};
|
||||||
} else return {};
|
} else return {};
|
||||||
|
|
|
@ -30,6 +30,7 @@ import TermsTextRecord from "./models/TermsTextRecord";
|
||||||
import TermsSignedRecord from "./models/TermsSignedRecord";
|
import TermsSignedRecord from "./models/TermsSignedRecord";
|
||||||
import TermsUpstreamRecord from "./models/TermsUpstreamRecord";
|
import TermsUpstreamRecord from "./models/TermsUpstreamRecord";
|
||||||
import HookshotGithubBridgeRecord from "./models/HookshotGithubBridgeRecord";
|
import HookshotGithubBridgeRecord from "./models/HookshotGithubBridgeRecord";
|
||||||
|
import HookshotJiraBridgeRecord from "./models/HookshotJiraBridgeRecord";
|
||||||
|
|
||||||
class _DimensionStore {
|
class _DimensionStore {
|
||||||
private sequelize: Sequelize;
|
private sequelize: Sequelize;
|
||||||
|
@ -77,6 +78,7 @@ class _DimensionStore {
|
||||||
TermsSignedRecord,
|
TermsSignedRecord,
|
||||||
TermsUpstreamRecord,
|
TermsUpstreamRecord,
|
||||||
HookshotGithubBridgeRecord,
|
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 { AllowNull, AutoIncrement, Column, ForeignKey, Model, PrimaryKey, Table } from "sequelize-typescript";
|
||||||
import Upstream from "./Upstream";
|
import Upstream from "./Upstream";
|
||||||
|
import { IHookshotBridgeRecord } from "./IHookshotBridgeRecord";
|
||||||
|
|
||||||
@Table({
|
@Table({
|
||||||
tableName: "dimension_hookshot_github_bridges",
|
tableName: "dimension_hookshot_github_bridges",
|
||||||
underscored: false,
|
underscored: false,
|
||||||
timestamps: false,
|
timestamps: false,
|
||||||
})
|
})
|
||||||
export default class HookshotGithubBridgeRecord extends Model {
|
export default class HookshotGithubBridgeRecord extends Model implements IHookshotBridgeRecord {
|
||||||
@PrimaryKey
|
@PrimaryKey
|
||||||
@AutoIncrement
|
@AutoIncrement
|
||||||
@Column
|
@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;
|
botUserId: string;
|
||||||
connections: HookshotConnection[];
|
connections: HookshotConnection[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface HookshotJiraBridgeConfiguration {
|
||||||
|
botUserId: string;
|
||||||
|
connections: HookshotConnection[];
|
||||||
|
}
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
{{'Provisioning URL' | translate}}
|
{{'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>
|
<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"
|
<input type="text" class="form-control"
|
||||||
placeholder="http://localhost:9999/_matrix/provision/v1"
|
placeholder="http://localhost:9000"
|
||||||
[(ngModel)]="provisionUrl" [disabled]="isSaving"/>
|
[(ngModel)]="provisionUrl" [disabled]="isSaving"/>
|
||||||
</label>
|
</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 { AdminHookshotGithubApiService } from "./shared/services/admin/admin-hookshot-github-api.service";
|
||||||
import { HookshotGithubApiService } from "./shared/services/integrations/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 { 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
|
// AoT requires an exported function for factories
|
||||||
export function HttpLoaderFactory(http: HttpClient) {
|
export function HttpLoaderFactory(http: HttpClient) {
|
||||||
|
@ -236,6 +243,9 @@ export function HttpLoaderFactory(http: HttpClient) {
|
||||||
AdminHookshotGithubBridgeComponent,
|
AdminHookshotGithubBridgeComponent,
|
||||||
AdminHookshotGithubBridgeManageSelfhostedComponent,
|
AdminHookshotGithubBridgeManageSelfhostedComponent,
|
||||||
HookshotGithubBridgeConfigComponent,
|
HookshotGithubBridgeConfigComponent,
|
||||||
|
AdminHookshotJiraBridgeComponent,
|
||||||
|
AdminHookshotJiraBridgeManageSelfhostedComponent,
|
||||||
|
HookshotJiraBridgeConfigComponent,
|
||||||
|
|
||||||
// Vendor
|
// Vendor
|
||||||
],
|
],
|
||||||
|
@ -267,6 +277,8 @@ export function HttpLoaderFactory(http: HttpClient) {
|
||||||
AdminTermsApiService,
|
AdminTermsApiService,
|
||||||
AdminHookshotGithubApiService,
|
AdminHookshotGithubApiService,
|
||||||
HookshotGithubApiService,
|
HookshotGithubApiService,
|
||||||
|
AdminHookshotJiraApiService,
|
||||||
|
HookshotJiraApiService,
|
||||||
{provide: Window, useValue: window},
|
{provide: Window, useValue: window},
|
||||||
|
|
||||||
// Vendor
|
// Vendor
|
||||||
|
@ -292,7 +304,7 @@ export function HttpLoaderFactory(http: HttpClient) {
|
||||||
AdminSlackBridgeManageSelfhostedComponent,
|
AdminSlackBridgeManageSelfhostedComponent,
|
||||||
AdminLogoutConfirmationDialogComponent,
|
AdminLogoutConfirmationDialogComponent,
|
||||||
AdminTermsNewEditPublishDialogComponent,
|
AdminTermsNewEditPublishDialogComponent,
|
||||||
AdminWidgetWhiteboardConfigComponent
|
AdminWidgetWhiteboardConfigComponent,
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class AppModule {
|
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 { WhiteboardWidgetComponent } from "./configs/widget/whiteboard/whiteboard.widget.component";
|
||||||
import { AdminHookshotGithubBridgeComponent } from "./admin/bridges/hookshot-github/hookshot-github.component";
|
import { AdminHookshotGithubBridgeComponent } from "./admin/bridges/hookshot-github/hookshot-github.component";
|
||||||
import { HookshotGithubBridgeConfigComponent } from "./configs/bridge/hookshot-github/hookshot-github.bridge.component";
|
import { HookshotGithubBridgeConfigComponent } from "./configs/bridge/hookshot-github/hookshot-github.bridge.component";
|
||||||
|
import { AdminHookshotJiraBridgeComponent } from "./admin/bridges/hookshot-jira/hookshot-jira.component";
|
||||||
|
import { HookshotJiraBridgeConfigComponent } from "./configs/bridge/hookshot-jira/hookshot-jira.bridge.component";
|
||||||
|
|
||||||
const routes: Routes = [
|
const routes: Routes = [
|
||||||
{path: "", component: HomeComponent},
|
{path: "", component: HomeComponent},
|
||||||
{path: "riot", pathMatch: "full", redirectTo: "riot-app"},
|
{path: "riot", pathMatch: "full", redirectTo: "riot-app", data: {breadcrumb: "Home", name: "Dimension"}},
|
||||||
{path: "element", pathMatch: "full", redirectTo: "riot-app"},
|
{path: "element", pathMatch: "full", redirectTo: "riot-app", data: {breadcrumb: "Home", name: "Dimension"}},
|
||||||
{
|
{
|
||||||
path: "riot-app",
|
path: "riot-app",
|
||||||
component: RiotComponent,
|
component: RiotComponent,
|
||||||
|
@ -141,6 +143,11 @@ const routes: Routes = [
|
||||||
component: AdminHookshotGithubBridgeComponent,
|
component: AdminHookshotGithubBridgeComponent,
|
||||||
data: {breadcrumb: "Github Bridge", name: "Github Bridge"},
|
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",
|
path: "complex-bot",
|
||||||
|
data: {breadcrumb: {skip: true}},
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: "rss",
|
path: "rss",
|
||||||
|
@ -258,6 +266,7 @@ const routes: Routes = [
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "bridge",
|
path: "bridge",
|
||||||
|
data: {breadcrumb: {skip: true}},
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: "irc",
|
path: "irc",
|
||||||
|
@ -284,6 +293,11 @@ const routes: Routes = [
|
||||||
component: HookshotGithubBridgeConfigComponent,
|
component: HookshotGithubBridgeConfigComponent,
|
||||||
data: {breadcrumb: "Github Bridge Configuration", name: "Github Bridge Configuration"},
|
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",
|
path: "widgets",
|
||||||
|
data: {breadcrumb: {skip: true}},
|
||||||
children: [
|
children: [
|
||||||
{path: "terms/:shortcode/:lang/:version", component: TermsWidgetWrapperComponent},
|
{path: "terms/:shortcode/:lang/:version", component: TermsWidgetWrapperComponent},
|
||||||
{path: "generic", component: GenericWidgetWrapperComponent},
|
{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": {},
|
"webhooks": {},
|
||||||
"slack": {},
|
"slack": {},
|
||||||
"hookshot_github": {},
|
"hookshot_github": {},
|
||||||
|
"hookshot_jira": {},
|
||||||
},
|
},
|
||||||
"widget": {
|
"widget": {
|
||||||
"custom": {
|
"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.",
|
"The shared secret defined in the configuration for provisioning.": "The shared secret defined in the configuration for provisioning.",
|
||||||
"Save": "Save",
|
"Save": "Save",
|
||||||
"Cancel": "Cancel",
|
"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 ",
|
"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",
|
"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.",
|
"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.",
|
"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",
|
"Organization": "Organization",
|
||||||
"Repository": "Repository",
|
"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",
|
"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.",
|
"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",
|
"Channel Name": "Channel Name",
|
||||||
|
@ -320,6 +324,11 @@
|
||||||
"Failed to create Github bridge": "Failed to create Github bridge",
|
"Failed to create Github bridge": "Failed to create Github bridge",
|
||||||
"Github bridge updated": "Github bridge updated",
|
"Github bridge updated": "Github bridge updated",
|
||||||
"Failed to update Github bridge": "Failed to update Github bridge",
|
"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",
|
"IRC Bridge added": "IRC Bridge added",
|
||||||
"Failed to create IRC bridge": "Failed to create IRC bridge",
|
"Failed to create IRC bridge": "Failed to create IRC bridge",
|
||||||
"Click the pencil icon to enable networks.": "Click the pencil icon to enable networks.",
|
"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.",
|
"The shared secret defined in the configuration for provisioning.": "The shared secret defined in the configuration for provisioning.",
|
||||||
"Save": "Save",
|
"Save": "Save",
|
||||||
"Cancel": "Cancel",
|
"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",
|
"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",
|
"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.",
|
"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.",
|
"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",
|
"Organization": "Organization",
|
||||||
"Repository": "Repository",
|
"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",
|
"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.",
|
"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",
|
"Channel Name": "Channel Name",
|
||||||
|
@ -320,6 +324,11 @@
|
||||||
"Failed to create Github bridge": "Failed to create Github bridge",
|
"Failed to create Github bridge": "Failed to create Github bridge",
|
||||||
"Github bridge updated": "Github bridge updated",
|
"Github bridge updated": "Github bridge updated",
|
||||||
"Failed to update Github bridge": "Failed to update Github bridge",
|
"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",
|
"IRC Bridge added": "IRC Bridge added",
|
||||||
"Failed to create IRC bridge": "Failed to create IRC bridge",
|
"Failed to create IRC bridge": "Failed to create IRC bridge",
|
||||||
"Click the pencil icon to enable networks.": "Click the pencil icon to enable networks.",
|
"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