diff --git a/src/api/dimension/DimensionHookshotJiraService.ts b/src/api/dimension/DimensionHookshotJiraService.ts new file mode 100644 index 0000000..461a95b --- /dev/null +++ b/src/api/dimension/DimensionHookshotJiraService.ts @@ -0,0 +1,103 @@ +import { Context, DELETE, GET, Path, PathParam, POST, Security, ServiceContext } from "typescript-rest"; +import { ApiError } from "../ApiError"; +import { LogService } from "matrix-bot-sdk"; +import { BridgedChannel, SlackBridge } from "../../bridges/SlackBridge"; +import { SlackChannel, SlackTeam } from "../../bridges/models/slack"; +import { ROLE_USER } from "../security/MatrixSecurity"; +import { HookshotJiraBridge } from "../../bridges/HookshotJiraBridge"; +import { + HookshotConnection, + HookshotJiraInstance, + HookshotJiraProject, + HookshotJiraRoomConfig +} from "../../bridges/models/hookshot"; + +interface BridgeRoomRequest { + instanceName: string; + projectKey: string; +} + +/** + * API for interacting with the Hookshot/Jira bridge + */ +@Path("/api/v1/dimension/hookshot/jira") +export class DimensionHookshotJiraService { + + @Context + private context: ServiceContext; + + @GET + @Path("auth") + @Security(ROLE_USER) + public async getAuthUrl(): Promise<{ authUrl: string }> { + const userId = this.context.request.user.userId; + + try { + const hookshot = new HookshotJiraBridge(userId); + const authUrl = await hookshot.getAuthUrl(); + return {authUrl}; + } catch (e) { + LogService.error("DimensionHookshotJiraService", e); + throw new ApiError(400, "Error getting auth info"); + } + } + + @GET + @Path("instances") + @Security(ROLE_USER) + public async getInstances(): Promise<{ instances: HookshotJiraInstance[] }> { + const userId = this.context.request.user.userId; + + const hookshot = new HookshotJiraBridge(userId); + const userInfo = await hookshot.getLoggedInUserInfo(); + if (!userInfo.loggedIn) { + throw new ApiError(403, "Not logged in", "T2B_NOT_LOGGED_IN"); + } + return {instances: userInfo.instances}; + } + + @GET + @Path("instance/:instanceName/projects") + @Security(ROLE_USER) + public async getProjects(@PathParam("instanceName") instanceName: string): Promise<{ projects: HookshotJiraProject[] }> { + const userId = this.context.request.user.userId; + + const hookshot = new HookshotJiraBridge(userId); + const projects = await hookshot.getProjects(instanceName); + return {projects}; + } + + @POST + @Path("room/:roomId/connect") + @Security(ROLE_USER) + public async bridgeRoom(@PathParam("roomId") roomId: string, request: BridgeRoomRequest): Promise { + const userId = this.context.request.user.userId; + + try { + const hookshot = new HookshotJiraBridge(userId); + return hookshot.bridgeRoom(roomId, request.instanceName, request.projectKey); + } catch (e) { + LogService.error("DimensionHookshotJiraService", e); + throw new ApiError(400, "Error bridging room"); + } + } + + @DELETE + @Path("room/:roomId/connections/all") + @Security(ROLE_USER) + public async unbridgeRoom(@PathParam("roomId") roomId: string): Promise { + const userId = this.context.request.user.userId; + + try { + const hookshot = new HookshotJiraBridge(userId); + const connections = await hookshot.getRoomConfigurations(roomId); + for (const conn of connections) { + await hookshot.unbridgeRoom(roomId, conn.id); + } + return {}; // 200 OK + } catch (e) { + LogService.error("DimensionHookshotJiraService", e); + throw new ApiError(400, "Error unbridging room"); + } + } +} diff --git a/src/api/dimension/DimensionSlackService.ts b/src/api/dimension/DimensionSlackService.ts index 883c6e5..038db41 100644 --- a/src/api/dimension/DimensionSlackService.ts +++ b/src/api/dimension/DimensionSlackService.ts @@ -52,6 +52,7 @@ export class DimensionSlackService { @DELETE @Path("room/:roomId/link") + @Security(ROLE_USER) public async unbridgeRoom(@PathParam("roomId") roomId: string): Promise { const userId = this.context.request.user.userId; diff --git a/src/bridges/HookshotJiraBridge.ts b/src/bridges/HookshotJiraBridge.ts index 9f0e6bd..31dd9fd 100644 --- a/src/bridges/HookshotJiraBridge.ts +++ b/src/bridges/HookshotJiraBridge.ts @@ -1,5 +1,11 @@ import HookshotJiraBridgeRecord from "../db/models/HookshotJiraBridgeRecord"; -import { HookshotConnection, HookshotGithubRoomConfig, HookshotJiraRoomConfig, HookshotTypes } from "./models/hookshot"; +import { + HookshotConnection, + HookshotGithubRoomConfig, HookshotJiraProject, + HookshotJiraRoomConfig, + HookshotJiraUserInfo, + HookshotTypes +} from "./models/hookshot"; import { HookshotBridge } from "./HookshotBridge"; export class HookshotJiraBridge extends HookshotBridge { @@ -16,6 +22,21 @@ export class HookshotJiraBridge extends HookshotBridge { return bridges[0]; } + public async getAuthUrl(): Promise { + const bridge = await this.getDefaultBridge(); + return this.doProvisionRequest(bridge, "GET", `/v1/jira/oauth`).then(r => r['url']); + } + + public async getLoggedInUserInfo(): Promise { + const bridge = await this.getDefaultBridge(); + return this.doProvisionRequest(bridge, "GET", `/v1/jira/account`); + } + + public async getProjects(instanceName: string): Promise { + const bridge = await this.getDefaultBridge(); + return this.doProvisionRequest(bridge, "GET", `/v1/jira/instances/${instanceName}/projects`); + } + public async getBotUserId(): Promise { const confs = await this.getAllServiceInformation(); const conf = confs.find(c => c.eventType === HookshotTypes.Jira); @@ -27,19 +48,27 @@ export class HookshotJiraBridge extends HookshotBridge { return !!bridges && bridges.length > 0 && !!(await this.getBotUserId()); } - public async getRoomConfigurations(inRoomId: string): Promise { - return (await this.getAllRoomConfigurations(inRoomId)).filter(c => c.eventType === HookshotTypes.Jira); + public async getRoomConfigurations(inRoomId: string): Promise { + return (await this.getAllRoomConfigurations(inRoomId)) + .filter(c => c.eventType === HookshotTypes.Jira); } - public async bridgeRoom(roomId: string): Promise { + public async bridgeRoom(roomId: string, instanceName: string, projectKey: string): Promise { const bridge = await this.getDefaultBridge(); - const body = {}; + const projects = await this.getProjects(instanceName); + const project = projects.find(p => p.key === projectKey); + if (!project) throw new Error("Could not find project"); + + const body = { + url: project.url, + commandPrefix: "!jira", + }; return await this.doProvisionRequest(bridge, "PUT", `/v1/${roomId}/connections/${HookshotTypes.Jira}`, null, body); } public async unbridgeRoom(roomId: string, connectionId: string): Promise { const bridge = await this.getDefaultBridge(); - await this.doProvisionRequest(bridge, "DELETE", `/v1/${roomId}/connections/${connectionId}`); + await this.doProvisionRequest(bridge, "DELETE", `/v1/${roomId}/connections/${encodeURIComponent(connectionId)}`); } } diff --git a/src/bridges/models/hookshot.ts b/src/bridges/models/hookshot.ts index b49be28..205463d 100644 --- a/src/bridges/models/hookshot.ts +++ b/src/bridges/models/hookshot.ts @@ -24,14 +24,31 @@ export enum SupportedJiraEventType { IssueCreated = "issue.created", } -export interface HookshotJiraRoomConfig { - id: string; - url: string; - events: SupportedJiraEventType[]; - commandPrefix: string; +export interface HookshotJiraRoomConfig extends HookshotConnection { + config: { + 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", } + +export interface HookshotJiraUserInfo { + loggedIn: boolean; + instances?: HookshotJiraInstance[]; +} + +export interface HookshotJiraInstance { + name: string; + url: string; +} + +export interface HookshotJiraProject { + key: string; + name: string; + url: string; +} diff --git a/src/db/BridgeStore.ts b/src/db/BridgeStore.ts index b14cfc9..677b360 100644 --- a/src/db/BridgeStore.ts +++ b/src/db/BridgeStore.ts @@ -161,9 +161,12 @@ export class BridgeStore { const hookshot = new HookshotJiraBridge(requestingUserId); const botUserId = await hookshot.getBotUserId(); const connections = await hookshot.getRoomConfigurations(inRoomId); + const userInfo = await hookshot.getLoggedInUserInfo(); return { botUserId: botUserId, connections: connections, + loggedIn: userInfo.loggedIn, + instances: userInfo.instances, }; } else return {}; } diff --git a/src/index.ts b/src/index.ts index dfe905b..7e445cf 100644 --- a/src/index.ts +++ b/src/index.ts @@ -21,7 +21,7 @@ LogService.info("index", "Starting dimension " + CURRENT_VERSION); async function startup() { const schemas = await DimensionStore.updateSchema(); - LogService.info("DimensionStore", schemas); + LogService.info("DimensionStore", "Applied schemas: ", schemas); const webserver = new Webserver(); await webserver.start(); diff --git a/src/integrations/Bridge.ts b/src/integrations/Bridge.ts index c28b2cb..2178c05 100644 --- a/src/integrations/Bridge.ts +++ b/src/integrations/Bridge.ts @@ -4,7 +4,7 @@ import { AvailableNetworks, LinkedChannels } from "../bridges/IrcBridge"; import { PortalInfo, PuppetInfo } from "../bridges/TelegramBridge"; import { WebhookConfiguration } from "../bridges/models/webhooks"; import { BridgedChannel } from "../bridges/SlackBridge"; -import { HookshotConnection } from "../bridges/models/hookshot"; +import { HookshotConnection, HookshotJiraInstance } from "../bridges/models/hookshot"; const PRIVATE_ACCESS_SUPPORTED_BRIDGES = ["webhooks"]; @@ -55,4 +55,6 @@ export interface HookshotGithubBridgeConfiguration { export interface HookshotJiraBridgeConfiguration { botUserId: string; connections: HookshotConnection[]; + loggedIn: boolean; + instances?: HookshotJiraInstance[]; } diff --git a/web/app/configs/bridge/hookshot-jira/hookshot-jira.bridge.component.html b/web/app/configs/bridge/hookshot-jira/hookshot-jira.bridge.component.html index 7edaa68..69a4683 100644 --- a/web/app/configs/bridge/hookshot-jira/hookshot-jira.bridge.component.html +++ b/web/app/configs/bridge/hookshot-jira/hookshot-jira.bridge.component.html @@ -8,29 +8,35 @@
-
+
+

{{'This room is bridged to' | translate}} {{bridgedProjectUrlUnsafe}}

+ +
+

{{'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}}

- - sign in with slack + + {{'Sign in with Jira' | translate}}
-
+
diff --git a/web/app/configs/bridge/hookshot-jira/hookshot-jira.bridge.component.ts b/web/app/configs/bridge/hookshot-jira/hookshot-jira.bridge.component.ts index 0801dc7..1459ec3 100644 --- a/web/app/configs/bridge/hookshot-jira/hookshot-jira.bridge.component.ts +++ b/web/app/configs/bridge/hookshot-jira/hookshot-jira.bridge.component.ts @@ -1,14 +1,20 @@ 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 { DomSanitizer, 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"; +import { + FE_HookshotJiraConnection, + FE_HookshotJiraInstance, + FE_HookshotJiraProject +} from "../../../shared/models/hookshot_jira"; interface HookshotConfig { botUserId: string; connections: FE_HookshotJiraConnection[]; + loggedIn: boolean; + instances?: FE_HookshotJiraInstance[]; } @Component({ @@ -18,31 +24,76 @@ interface HookshotConfig { export class HookshotJiraBridgeConfigComponent extends BridgeComponent 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; + public loadingConnections = true; + public bridgedProjectUrl: SafeUrl; + public bridgedProjectUrlUnsafe: string; - constructor(private hookshot: HookshotJiraApiService, private scalar: ScalarClientApiService, public translate: TranslateService) { + public instances: FE_HookshotJiraInstance[] = []; + public instance: FE_HookshotJiraInstance; + + public projects: FE_HookshotJiraProject[] = []; + public project: FE_HookshotJiraProject; + + private timerId: any; + + constructor(private hookshot: HookshotJiraApiService, private scalar: ScalarClientApiService, private sanitizer: DomSanitizer, public translate: TranslateService) { super("hookshot_jira", translate); - this.translate = translate; } public ngOnInit() { super.ngOnInit(); - - this.prepare(); + this.tryLoadInstances(); } - private prepare() { + private tryLoadInstances() { + this.loadingConnections = true; + this.hookshot.getInstances().then(r => { + this.instances = r; + this.instance = this.instances[0]; + this.loadProjects(); + if (this.timerId) { + clearInterval(this.timerId); + } + }).catch(e => { + if (e.status === 403 && e.error.dim_errcode === "T2B_NOT_LOGGED_IN") { + this.hookshot.getAuthUrl().then(url => { + this.authUrl = this.sanitizer.bypassSecurityTrustResourceUrl(url); + this.loadingConnections = false; + this.timerId = setInterval(() => { + this.tryLoadInstances(); + }, 1000); + }); + } else { + console.error(e); + this.translate.get('Error getting Jira information').subscribe((res: string) => { + this.toaster.pop("error", res); + }); + } + }); } - public loadRepos() { - // TODO + public loadProjects() { + this.isBusy = true; + this.hookshot.getProjects(this.instance.name).then(projects => { + this.projects = projects; + this.project = this.projects[0]; + + if (this.isBridged) { + this.bridgedProjectUrlUnsafe = this.bridge.config.connections[0].config.url; + this.bridgedProjectUrl = this.sanitizer.bypassSecurityTrustResourceUrl(this.bridgedProjectUrlUnsafe); + } + + this.isBusy = false; + this.loadingConnections = false; + }).catch(e => { + console.error(e); + this.isBusy = false; + this.translate.get('Error getting Jira information').subscribe((res: string) => { + this.toaster.pop("error", res); + }); + }); } public get isBridged(): boolean { @@ -65,7 +116,9 @@ export class HookshotJiraBridgeConfigComponent extends BridgeComponent { + await this.scalar.setUserPowerLevel(this.roomId, this.bridge.config.botUserId, 50); + + this.hookshot.bridgeRoom(this.roomId, this.instance.name, this.project.key).then(conn => { this.bridge.config.connections.push(conn); this.isBusy = false; this.translate.get('Bridge requested').subscribe((res: string) => { diff --git a/web/app/shared/models/hookshot_jira.ts b/web/app/shared/models/hookshot_jira.ts index d1d80de..d439f7f 100644 --- a/web/app/shared/models/hookshot_jira.ts +++ b/web/app/shared/models/hookshot_jira.ts @@ -7,5 +7,19 @@ export interface FE_HookshotJiraBridge { } export interface FE_HookshotJiraConnection { - + config: { + url: string; + commandPrefix?: string; + }; +} + +export interface FE_HookshotJiraInstance { + name: string; + url: string; +} + +export interface FE_HookshotJiraProject { + key: string; + name: string; + url: string; } diff --git a/web/app/shared/services/integrations/hookshot-jira-api.service.ts b/web/app/shared/services/integrations/hookshot-jira-api.service.ts index 3f2fb28..ad45079 100644 --- a/web/app/shared/services/integrations/hookshot-jira-api.service.ts +++ b/web/app/shared/services/integrations/hookshot-jira-api.service.ts @@ -1,7 +1,7 @@ import { Injectable } from "@angular/core"; import { AuthedApi } from "../authed-api"; import { HttpClient } from "@angular/common/http"; -import { FE_HookshotJiraConnection } from "../../models/hookshot_jira"; +import { FE_HookshotJiraConnection, FE_HookshotJiraInstance, FE_HookshotJiraProject } from "../../models/hookshot_jira"; @Injectable() export class HookshotJiraApiService extends AuthedApi { @@ -9,13 +9,26 @@ export class HookshotJiraApiService extends AuthedApi { super(http); } - public bridgeRoom(roomId: string): Promise { + public getProjects(instanceName: string): Promise { + return this.authedGet("/api/v1/dimension/hookshot/jira/instance/" + instanceName + "/projects").toPromise().then(r => r['projects']); + } + + public getInstances(): Promise { + return this.authedGet("/api/v1/dimension/hookshot/jira/instances").toPromise().then(r => r['instances']); + } + + public getAuthUrl(): Promise { + return this.authedGet("/api/v1/dimension/hookshot/jira/auth").toPromise().then(r => r['authUrl']); + } + + public bridgeRoom(roomId: string, instanceName: string, projectKey: string): Promise { return this.authedPost("/api/v1/dimension/hookshot/jira/room/" + roomId + "/connect", { - // TODO + instanceName: instanceName, + projectKey: projectKey, }).toPromise(); } public unbridgeRoom(roomId: string): Promise { - return this.authedDelete("/api/v1/dimension/hookshot/jira/" + roomId + "/connections/all").toPromise(); + return this.authedDelete("/api/v1/dimension/hookshot/jira/room/" + roomId + "/connections/all").toPromise(); } } diff --git a/web/assets/i18n/en.json b/web/assets/i18n/en.json index 5a60c02..6053f9b 100644 --- a/web/assets/i18n/en.json +++ b/web/assets/i18n/en.json @@ -158,6 +158,9 @@ "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.", + "Sign in with Jira": "Sign in with Jira", + "Instance / Organization": "Instance / Organization", + "Project": "Project", "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", @@ -243,6 +246,14 @@ "Whiteboard Name": "Whiteboard Name", "Whiteboard URL": "Whiteboard URL", "Video URL": "Video URL", + "Looking for your sticker packs?": "Looking for your sticker packs?", + "Click here": "Click here", + "This room is encrypted": "This room is encrypted", + "Integrations are not encrypted!": "Integrations are not encrypted!", + "This means that some information about yourself and the room may be leaked to the bot, bridge, or widget. This information includes the room ID, your display name, your username, your avatar, information about Element, and other similar details. Add integrations with caution.": "This means that some information about yourself and the room may be leaked to the bot, bridge, or widget. This information includes the room ID, your display name, your username, your avatar, information about Element, and other similar details. Add integrations with caution.", + "There are currently no integrations which support encrypted rooms. Sorry about that!": "There are currently no integrations which support encrypted rooms. Sorry about that!", + "No integrations available": "No integrations available", + "This room does not have any compatible integrations. Please contact the server owner if you're seeing this message.": "This room does not have any compatible integrations. Please contact the server owner if you're seeing this message.", "An open source integration manager for Matrix": "An open source integration manager for Matrix", "Self-host your favourite bots, bridges, and widgets.": "Self-host your favourite bots, bridges, and widgets.", "source": "source", @@ -294,14 +305,6 @@ "Using tools like the": "Using tools like the", "federation tester": "federation tester", ", make sure that federation is working on your homeserver.": ", make sure that federation is working on your homeserver.", - "Looking for your sticker packs?": "Looking for your sticker packs?", - "Click here": "Click here", - "This room is encrypted": "This room is encrypted", - "Integrations are not encrypted!": "Integrations are not encrypted!", - "This means that some information about yourself and the room may be leaked to the bot, bridge, or widget. This information includes the room ID, your display name, your username, your avatar, information about Element, and other similar details. Add integrations with caution.": "This means that some information about yourself and the room may be leaked to the bot, bridge, or widget. This information includes the room ID, your display name, your username, your avatar, information about Element, and other similar details. Add integrations with caution.", - "There are currently no integrations which support encrypted rooms. Sorry about that!": "There are currently no integrations which support encrypted rooms. Sorry about that!", - "No integrations available": "No integrations available", - "This room does not have any compatible integrations. Please contact the server owner if you're seeing this message.": "This room does not have any compatible integrations. Please contact the server owner if you're seeing this message.", "BigBlueButton Conference": "BigBlueButton Conference", "Join Conference": "Join Conference", "Sorry, this content cannot be embedded": "Sorry, this content cannot be embedded", @@ -410,6 +413,7 @@ "Error requesting bridge": "Error requesting bridge", "Bridge removed": "Bridge removed", "Error removing bridge": "Error removing bridge", + "Error getting Jira information": "Error getting Jira information", "Please enter a channel name": "Please enter a channel name", "Error loading channel operators": "Error loading channel operators", "Failed to make the bridge an administrator": "Failed to make the bridge an administrator", diff --git a/web/assets/i18n/template.json b/web/assets/i18n/template.json index 1b3be02..6f6c9b6 100644 --- a/web/assets/i18n/template.json +++ b/web/assets/i18n/template.json @@ -158,6 +158,9 @@ "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.", + "Sign in with Jira": "Sign in with Jira", + "Instance / Organization": "Instance / Organization", + "Project": "Project", "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", @@ -243,6 +246,14 @@ "Whiteboard Name": "Whiteboard Name", "Whiteboard URL": "Whiteboard URL", "Video URL": "Video URL", + "Looking for your sticker packs?": "Looking for your sticker packs?", + "Click here": "Click here", + "This room is encrypted": "This room is encrypted", + "Integrations are not encrypted!": "Integrations are not encrypted!", + "This means that some information about yourself and the room may be leaked to the bot, bridge, or widget. This information includes the room ID, your display name, your username, your avatar, information about Element, and other similar details. Add integrations with caution.": "This means that some information about yourself and the room may be leaked to the bot, bridge, or widget. This information includes the room ID, your display name, your username, your avatar, information about Element, and other similar details. Add integrations with caution.", + "There are currently no integrations which support encrypted rooms. Sorry about that!": "There are currently no integrations which support encrypted rooms. Sorry about that!", + "No integrations available": "No integrations available", + "This room does not have any compatible integrations. Please contact the server owner if you're seeing this message.": "This room does not have any compatible integrations. Please contact the server owner if you're seeing this message.", "An open source integration manager for Matrix": "An open source integration manager for Matrix", "Self-host your favourite bots, bridges, and widgets.": "Self-host your favourite bots, bridges, and widgets.", "source": "source", @@ -294,14 +305,6 @@ "Using tools like the": "Using tools like the", "federation tester": "federation tester", ", make sure that federation is working on your homeserver.": ", make sure that federation is working on your homeserver.", - "Looking for your sticker packs?": "Looking for your sticker packs?", - "Click here": "Click here", - "This room is encrypted": "This room is encrypted", - "Integrations are not encrypted!": "Integrations are not encrypted!", - "This means that some information about yourself and the room may be leaked to the bot, bridge, or widget. This information includes the room ID, your display name, your username, your avatar, information about Element, and other similar details. Add integrations with caution.": "This means that some information about yourself and the room may be leaked to the bot, bridge, or widget. This information includes the room ID, your display name, your username, your avatar, information about Element, and other similar details. Add integrations with caution.", - "There are currently no integrations which support encrypted rooms. Sorry about that!": "There are currently no integrations which support encrypted rooms. Sorry about that!", - "No integrations available": "No integrations available", - "This room does not have any compatible integrations. Please contact the server owner if you're seeing this message.": "This room does not have any compatible integrations. Please contact the server owner if you're seeing this message.", "BigBlueButton Conference": "BigBlueButton Conference", "Join Conference": "Join Conference", "Sorry, this content cannot be embedded": "Sorry, this content cannot be embedded", @@ -410,6 +413,7 @@ "Error requesting bridge": "Error requesting bridge", "Bridge removed": "Bridge removed", "Error removing bridge": "Error removing bridge", + "Error getting Jira information": "Error getting Jira information", "Please enter a channel name": "Please enter a channel name", "Error loading channel operators": "Error loading channel operators", "Failed to make the bridge an administrator": "Failed to make the bridge an administrator", diff --git a/web/style/components/bootstrap_override.scss b/web/style/components/bootstrap_override.scss index d8fa372..4932269 100644 --- a/web/style/components/bootstrap_override.scss +++ b/web/style/components/bootstrap_override.scss @@ -4,6 +4,7 @@ .main-app { a { color: themed(anchorColor); + text-decoration: none; } table, td, th { @@ -32,4 +33,4 @@ display: flex; } } -} \ No newline at end of file +}