From f7ed7394239e87ddcc6f43bc6b2e53a89800b4f5 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 28 Dec 2020 21:10:23 -0700 Subject: [PATCH] Remove legacy gitter bridge support --- src/MemoryCache.ts | 3 +- src/api/admin/AdminGitterService.ts | 115 ---------- src/api/dimension/DimensionGitterService.ts | 64 ------ src/bridges/GitterBridge.ts | 205 ------------------ src/db/BridgeStore.ts | 21 +- src/db/DimensionStore.ts | 2 - src/db/models/GitterBridgeRecord.ts | 26 --- src/integrations/Bridge.ts | 10 +- src/models/ModularResponses.ts | 9 +- .../bridges/gitter/gitter.component.html | 47 ---- .../bridges/gitter/gitter.component.scss | 3 - .../admin/bridges/gitter/gitter.component.ts | 105 --------- .../manage-selfhosted.component.html | 28 --- .../manage-selfhosted.component.scss | 0 .../manage-selfhosted.component.ts | 56 ----- web/app/app.module.ts | 11 - web/app/app.routing.ts | 12 - .../gitter/gitter.bridge.component.html | 27 --- .../gitter/gitter.bridge.component.scss | 4 - .../bridge/gitter/gitter.bridge.component.ts | 68 ------ web/app/shared/models/gitter.ts | 11 - .../shared/registry/integrations.registry.ts | 1 - .../admin/admin-gitter-api.service.ts | 36 --- .../integrations/gitter-api.service.ts | 23 -- web/public/img/avatars/gitter.png | Bin 3128 -> 0 bytes 25 files changed, 6 insertions(+), 881 deletions(-) delete mode 100644 src/api/admin/AdminGitterService.ts delete mode 100644 src/api/dimension/DimensionGitterService.ts delete mode 100644 src/bridges/GitterBridge.ts delete mode 100644 src/db/models/GitterBridgeRecord.ts delete mode 100644 web/app/admin/bridges/gitter/gitter.component.html delete mode 100644 web/app/admin/bridges/gitter/gitter.component.scss delete mode 100644 web/app/admin/bridges/gitter/gitter.component.ts delete mode 100644 web/app/admin/bridges/gitter/manage-selfhosted/manage-selfhosted.component.html delete mode 100644 web/app/admin/bridges/gitter/manage-selfhosted/manage-selfhosted.component.scss delete mode 100644 web/app/admin/bridges/gitter/manage-selfhosted/manage-selfhosted.component.ts delete mode 100644 web/app/configs/bridge/gitter/gitter.bridge.component.html delete mode 100644 web/app/configs/bridge/gitter/gitter.bridge.component.scss delete mode 100644 web/app/configs/bridge/gitter/gitter.bridge.component.ts delete mode 100644 web/app/shared/models/gitter.ts delete mode 100644 web/app/shared/services/admin/admin-gitter-api.service.ts delete mode 100644 web/app/shared/services/integrations/gitter-api.service.ts delete mode 100644 web/public/img/avatars/gitter.png diff --git a/src/MemoryCache.ts b/src/MemoryCache.ts index 0b9ae7a..0eb78c5 100644 --- a/src/MemoryCache.ts +++ b/src/MemoryCache.ts @@ -52,7 +52,6 @@ export const CACHE_IRC_BRIDGE = "irc-bridge"; export const CACHE_STICKERS = "stickers"; export const CACHE_TELEGRAM_BRIDGE = "telegram-bridge"; export const CACHE_WEBHOOKS_BRIDGE = "webhooks-bridge"; -export const CACHE_GITTER_BRIDGE = "gitter-bridge"; export const CACHE_SIMPLE_BOTS = "simple-bots"; export const CACHE_SLACK_BRIDGE = "slack-bridge"; -export const CACHE_TERMS = "terms"; \ No newline at end of file +export const CACHE_TERMS = "terms"; diff --git a/src/api/admin/AdminGitterService.ts b/src/api/admin/AdminGitterService.ts deleted file mode 100644 index 8680276..0000000 --- a/src/api/admin/AdminGitterService.ts +++ /dev/null @@ -1,115 +0,0 @@ -import { Context, GET, Path, PathParam, POST, Security, ServiceContext } from "typescript-rest"; -import { Cache, CACHE_GITTER_BRIDGE, CACHE_INTEGRATIONS } from "../../MemoryCache"; -import { LogService } from "matrix-js-snippets"; -import { ApiError } from "../ApiError"; -import GitterBridgeRecord from "../../db/models/GitterBridgeRecord"; -import Upstream from "../../db/models/Upstream"; -import { ROLE_ADMIN, ROLE_USER } from "../security/MatrixSecurity"; - -interface CreateWithUpstream { - upstreamId: number; -} - -interface CreateSelfhosted { - provisionUrl: string; -} - -interface BridgeResponse { - id: number; - upstreamId?: number; - provisionUrl?: string; - isEnabled: boolean; -} - -/** - * Administrative API for configuring Gitter bridge instances. - */ -@Path("/api/v1/dimension/admin/gitter") -export class AdminGitterService { - - @Context - private context: ServiceContext; - - @GET - @Path("all") - @Security([ROLE_USER, ROLE_ADMIN]) - public async getBridges(): Promise { - const bridges = await GitterBridgeRecord.findAll(); - return Promise.all(bridges.map(async b => { - return { - id: b.id, - upstreamId: b.upstreamId, - provisionUrl: b.provisionUrl, - isEnabled: b.isEnabled, - }; - })); - } - - @GET - @Path(":bridgeId") - @Security([ROLE_USER, ROLE_ADMIN]) - public async getBridge(@PathParam("bridgeId") bridgeId: number): Promise { - const telegramBridge = await GitterBridgeRecord.findByPk(bridgeId); - if (!telegramBridge) throw new ApiError(404, "Gitter Bridge not found"); - - return { - id: telegramBridge.id, - upstreamId: telegramBridge.upstreamId, - provisionUrl: telegramBridge.provisionUrl, - isEnabled: telegramBridge.isEnabled, - }; - } - - @POST - @Path(":bridgeId") - @Security([ROLE_USER, ROLE_ADMIN]) - public async updateBridge(@PathParam("bridgeId") bridgeId: number, request: CreateSelfhosted): Promise { - const userId = this.context.request.user.userId; - const bridge = await GitterBridgeRecord.findByPk(bridgeId); - if (!bridge) throw new ApiError(404, "Bridge not found"); - - bridge.provisionUrl = request.provisionUrl; - await bridge.save(); - - LogService.info("AdminGitterService", userId + " updated Gitter Bridge " + bridge.id); - - Cache.for(CACHE_GITTER_BRIDGE).clear(); - Cache.for(CACHE_INTEGRATIONS).clear(); - return this.getBridge(bridge.id); - } - - @POST - @Path("new/upstream") - @Security([ROLE_USER, ROLE_ADMIN]) - public async newConfigForUpstream(request: CreateWithUpstream): Promise { - const userId = this.context.request.user.userId; - const upstream = await Upstream.findByPk(request.upstreamId); - if (!upstream) throw new ApiError(400, "Upstream not found"); - - const bridge = await GitterBridgeRecord.create({ - upstreamId: request.upstreamId, - isEnabled: true, - }); - LogService.info("AdminGitterService", userId + " created a new Gitter Bridge from upstream " + request.upstreamId); - - Cache.for(CACHE_GITTER_BRIDGE).clear(); - Cache.for(CACHE_INTEGRATIONS).clear(); - return this.getBridge(bridge.id); - } - - @POST - @Path("new/selfhosted") - @Security([ROLE_USER, ROLE_ADMIN]) - public async newSelfhosted(request: CreateSelfhosted): Promise { - const userId = this.context.request.user.userId; - const bridge = await GitterBridgeRecord.create({ - provisionUrl: request.provisionUrl, - isEnabled: true, - }); - LogService.info("AdminGitterService", userId + " created a new Gitter Bridge with provisioning URL " + request.provisionUrl); - - Cache.for(CACHE_GITTER_BRIDGE).clear(); - Cache.for(CACHE_INTEGRATIONS).clear(); - return this.getBridge(bridge.id); - } -} \ No newline at end of file diff --git a/src/api/dimension/DimensionGitterService.ts b/src/api/dimension/DimensionGitterService.ts deleted file mode 100644 index b2c889b..0000000 --- a/src/api/dimension/DimensionGitterService.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { Context, DELETE, GET, Path, PathParam, POST, Security, ServiceContext } from "typescript-rest"; -import { ApiError } from "../ApiError"; -import { BridgedRoom, GitterBridge } from "../../bridges/GitterBridge"; -import { LogService } from "matrix-js-snippets"; -import { ROLE_USER } from "../security/MatrixSecurity"; - -interface BridgeRoomRequest { - gitterRoomName: string; -} - -/** - * API for interacting with the Gitter bridge - */ -@Path("/api/v1/dimension/gitter") -export class DimensionGitterService { - - @Context - private context: ServiceContext; - - @GET - @Path("room/:roomId/link") - @Security(ROLE_USER) - public async getLink(@PathParam("roomId") roomId: string): Promise { - const userId = this.context.request.user.userId; - try { - const gitter = new GitterBridge(userId); - return gitter.getLink(roomId); - } catch (e) { - LogService.error("DimensionGitterService", e); - throw new ApiError(400, "Error getting bridge info"); - } - } - - @POST - @Path("room/:roomId/link") - @Security(ROLE_USER) - public async bridgeRoom(@PathParam("roomId") roomId: string, request: BridgeRoomRequest): Promise { - const userId = this.context.request.user.userId; - try { - const gitter = new GitterBridge(userId); - await gitter.requestLink(roomId, request.gitterRoomName); - return gitter.getLink(roomId); - } catch (e) { - LogService.error("DimensionGitterService", e); - throw new ApiError(400, "Error bridging room"); - } - } - - @DELETE - @Path("room/:roomId/link") - @Security(ROLE_USER) - public async unbridgeRoom(@PathParam("roomId") roomId: string): Promise { - const userId = this.context.request.user.userId; - try { - const gitter = new GitterBridge(userId); - const link = await gitter.getLink(roomId); - await gitter.removeLink(roomId, link.gitterRoomName); - return {}; // 200 OK - } catch (e) { - LogService.error("DimensionGitterService", e); - throw new ApiError(400, "Error unbridging room"); - } - } -} \ No newline at end of file diff --git a/src/bridges/GitterBridge.ts b/src/bridges/GitterBridge.ts deleted file mode 100644 index 70715bd..0000000 --- a/src/bridges/GitterBridge.ts +++ /dev/null @@ -1,205 +0,0 @@ -import IrcBridgeRecord from "../db/models/IrcBridgeRecord"; -import Upstream from "../db/models/Upstream"; -import UserScalarToken from "../db/models/UserScalarToken"; -import { LogService } from "matrix-js-snippets"; -import * as request from "request"; -import { ModularGitterResponse } from "../models/ModularResponses"; -import GitterBridgeRecord from "../db/models/GitterBridgeRecord"; -import { BridgedRoomResponse, GetBotUserIdResponse } from "./models/gitter"; - -export interface GitterBridgeInfo { - botUserId: string; -} - -export interface BridgedRoom { - roomId: string; - gitterRoomName: string; -} - -export class GitterBridge { - - constructor(private requestingUserId: string) { - } - - private async getDefaultBridge(): Promise { - const bridges = await GitterBridgeRecord.findAll({where: {isEnabled: true}}); - if (!bridges || bridges.length !== 1) { - throw new Error("No bridges or too many bridges found"); - } - return bridges[0]; - } - - public async isBridgingEnabled(): Promise { - const bridges = await GitterBridgeRecord.findAll({where: {isEnabled: true}}); - return !!bridges && bridges.length > 0; - } - - public async getBridgeInfo(): Promise { - const bridge = await this.getDefaultBridge(); - - if (bridge.upstreamId) { - const info = await this.doUpstreamRequest>(bridge, "POST", "/bridges/gitter/_matrix/provision/getbotid/", null, {}); - if (!info || !info.replies || !info.replies[0] || !info.replies[0].response) { - throw new Error("Invalid response from Modular for Gitter bot user ID"); - } - return {botUserId: info.replies[0].response.bot_user_id}; - } else { - const info = await this.doProvisionRequest(bridge, "POST", "/_matrix/provision/getbotid"); - return {botUserId: info.bot_user_id}; - } - } - - public async getLink(roomId: string): Promise { - const bridge = await this.getDefaultBridge(); - - const requestBody = { - matrix_room_id: roomId, - user_id: this.requestingUserId, - }; - try { - if (bridge.upstreamId) { - delete requestBody["user_id"]; - const link = await this.doUpstreamRequest>(bridge, "POST", "/bridges/gitter/_matrix/provision/getlink", null, requestBody); - if (!link || !link.replies || !link.replies[0] || !link.replies[0].response) { - // noinspection ExceptionCaughtLocallyJS - throw new Error("Invalid response from Modular for Gitter list links in " + roomId); - } - return { - roomId: link.replies[0].response.matrix_room_id, - gitterRoomName: link.replies[0].response.remote_room_name, - }; - } else { - const link = await this.doProvisionRequest(bridge, "POST", "/_matrix/provision/getlink", null, requestBody); - return { - roomId: link.matrix_room_id, - gitterRoomName: link.remote_room_name, - }; - } - } catch (e) { - if (e.status === 404) return null; - LogService.error("GitterBridge", e); - throw e; - } - } - - public async requestLink(roomId: string, remoteName: string): Promise { - const bridge = await this.getDefaultBridge(); - - const requestBody = { - matrix_room_id: roomId, - remote_room_name: remoteName, - user_id: this.requestingUserId, - }; - - if (bridge.upstreamId) { - delete requestBody["user_id"]; - await this.doUpstreamRequest(bridge, "POST", "/bridges/gitter/_matrix/provision/link", null, requestBody); - } else { - await this.doProvisionRequest(bridge, "POST", "/_matrix/provision/link", null, requestBody); - } - } - - public async removeLink(roomId: string, remoteName: string): Promise { - const bridge = await this.getDefaultBridge(); - - const requestBody = { - matrix_room_id: roomId, - remote_room_name: remoteName, - user_id: this.requestingUserId, - }; - - if (bridge.upstreamId) { - delete requestBody["user_id"]; - await this.doUpstreamRequest(bridge, "POST", "/bridges/gitter/_matrix/provision/unlink", null, requestBody); - } else { - await this.doProvisionRequest(bridge, "POST", "/_matrix/provision/unlink", null, requestBody); - } - } - - private async doUpstreamRequest(bridge: IrcBridgeRecord, method: string, endpoint: string, qs?: any, body?: any): Promise { - const upstream = await Upstream.findByPk(bridge.upstreamId); - const token = await UserScalarToken.findOne({ - where: { - upstreamId: upstream.id, - isDimensionToken: false, - userId: this.requestingUserId, - }, - }); - - if (!qs) qs = {}; - qs["scalar_token"] = token.scalarToken; - - const apiUrl = upstream.apiUrl.endsWith("/") ? upstream.apiUrl.substring(0, upstream.apiUrl.length - 1) : upstream.apiUrl; - const url = apiUrl + (endpoint.startsWith("/") ? endpoint : "/" + endpoint); - LogService.info("GitterBridge", "Doing upstream Gitter Bridge request: " + url); - - return new Promise((resolve, reject) => { - request({ - method: method, - url: url, - qs: qs, - json: body, - }, (err, res, _body) => { - try { - if (err) { - LogService.error("GitterBridge", "Error calling " + url); - LogService.error("GitterBridge", err); - reject(err); - } else if (!res) { - LogService.error("GitterBridge", "There is no response for " + url); - reject(new Error("No response provided - is the service online?")); - } else if (res.statusCode !== 200) { - if (typeof (res.body) === "string") res.body = JSON.parse(res.body); - LogService.error("GitterBridge", "Got status code " + res.statusCode + " when calling " + url); - LogService.error("GitterBridge", res.body); - reject({body: res.body, status: res.statusCode}); - } else { - if (typeof (res.body) === "string") res.body = JSON.parse(res.body); - resolve(res.body); - } - } catch (e) { - LogService.error("GitterBridge", e); - reject(e); - } - }); - }); - } - - private async doProvisionRequest(bridge: IrcBridgeRecord, method: string, endpoint: string, qs?: any, body?: any): Promise { - const provisionUrl = bridge.provisionUrl; - const apiUrl = provisionUrl.endsWith("/") ? provisionUrl.substring(0, provisionUrl.length - 1) : provisionUrl; - const url = apiUrl + (endpoint.startsWith("/") ? endpoint : "/" + endpoint); - LogService.info("GitterBridge", "Doing provision Gitter Bridge request: " + url); - - return new Promise((resolve, reject) => { - request({ - method: method, - url: url, - qs: qs, - json: body, - }, (err, res, _body) => { - try { - if (err) { - LogService.error("GitterBridge", "Error calling" + url); - LogService.error("GitterBridge", err); - reject(err); - } else if (!res) { - LogService.error("GitterBridge", "There is no response for " + url); - reject(new Error("No response provided - is the service online?")); - } else if (res.statusCode !== 200) { - if (typeof (res.body) === "string") res.body = JSON.parse(res.body); - LogService.error("GitterBridge", "Got status code " + res.statusCode + " when calling " + url); - LogService.error("GitterBridge", res.body); - reject({body: res.body, status: res.statusCode}); - } else { - if (typeof (res.body) === "string") res.body = JSON.parse(res.body); - resolve(res.body); - } - } catch (e) { - LogService.error("GitterBridge", e); - reject(e); - } - }); - }); - } -} \ No newline at end of file diff --git a/src/db/BridgeStore.ts b/src/db/BridgeStore.ts index fcbc4e7..0a9737e 100644 --- a/src/db/BridgeStore.ts +++ b/src/db/BridgeStore.ts @@ -1,6 +1,5 @@ import { Bridge, - GitterBridgeConfiguration, SlackBridgeConfiguration, TelegramBridgeConfiguration, WebhookBridgeConfiguration @@ -10,7 +9,6 @@ import { IrcBridge } from "../bridges/IrcBridge"; import { LogService } from "matrix-js-snippets"; import { TelegramBridge } from "../bridges/TelegramBridge"; import { WebhooksBridge } from "../bridges/WebhooksBridge"; -import { GitterBridge } from "../bridges/GitterBridge"; import { SlackBridge } from "../bridges/SlackBridge"; export class BridgeStore { @@ -61,7 +59,7 @@ export class BridgeStore { const record = await BridgeRecord.findOne({where: {type: integrationType}}); if (!record) throw new Error("Bridge not found"); - const hasDedicatedApi = ["irc", "telegram", "webhooks", "gitter", "slack"]; + const hasDedicatedApi = ["irc", "telegram", "webhooks", "slack"]; if (hasDedicatedApi.indexOf(integrationType) !== -1) { throw new Error("This bridge should be modified with the dedicated API"); } else throw new Error("Unsupported bridge"); @@ -77,9 +75,6 @@ export class BridgeStore { } else if (record.type === "webhooks") { const webhooks = new WebhooksBridge(requestingUserId); return webhooks.isBridgingEnabled(); - } else if (record.type === "gitter") { - const gitter = new GitterBridge(requestingUserId); - return gitter.isBridgingEnabled(); } else if (record.type === "slack") { const slack = new SlackBridge(requestingUserId); return slack.isBridgingEnabled(); @@ -96,9 +91,6 @@ export class BridgeStore { } else if (record.type === "webhooks") { const webhooks = new WebhooksBridge(requestingUserId); return webhooks.isBridgingEnabled(); - } else if (record.type === "gitter") { - const gitter = new GitterBridge(requestingUserId); - return gitter.isBridgingEnabled(); } else if (record.type === "slack") { const slack = new SlackBridge(requestingUserId); return slack.isBridgingEnabled(); @@ -130,15 +122,6 @@ export class BridgeStore { webhooks: hooks, botUserId: info.botUserId, }; - } else if (record.type === "gitter") { - if (!inRoomId) return {}; // The bridge's admin config is handled by other APIs - const gitter = new GitterBridge(requestingUserId); - const info = await gitter.getBridgeInfo(); - const link = await gitter.getLink(inRoomId); - return { - link: link, - botUserId: info.botUserId, - }; } else if (record.type === "slack") { if (!inRoomId) return {}; // The bridge's admin config is handled by other APIs const slack = new SlackBridge(requestingUserId); @@ -154,4 +137,4 @@ export class BridgeStore { private constructor() { } -} \ No newline at end of file +} diff --git a/src/db/DimensionStore.ts b/src/db/DimensionStore.ts index 9fe4eac..973d82a 100644 --- a/src/db/DimensionStore.ts +++ b/src/db/DimensionStore.ts @@ -23,7 +23,6 @@ import Sticker from "./models/Sticker"; import UserStickerPack from "./models/UserStickerPack"; import TelegramBridgeRecord from "./models/TelegramBridgeRecord"; import WebhookBridgeRecord from "./models/WebhookBridgeRecord"; -import GitterBridgeRecord from "./models/GitterBridgeRecord"; import CustomSimpleBotRecord from "./models/CustomSimpleBotRecord"; import SlackBridgeRecord from "./models/SlackBridgeRecord"; import TermsRecord from "./models/TermsRecord"; @@ -70,7 +69,6 @@ class _DimensionStore { UserStickerPack, TelegramBridgeRecord, WebhookBridgeRecord, - GitterBridgeRecord, CustomSimpleBotRecord, SlackBridgeRecord, TermsRecord, diff --git a/src/db/models/GitterBridgeRecord.ts b/src/db/models/GitterBridgeRecord.ts deleted file mode 100644 index 463a1c9..0000000 --- a/src/db/models/GitterBridgeRecord.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { AllowNull, AutoIncrement, Column, ForeignKey, Model, PrimaryKey, Table } from "sequelize-typescript"; -import Upstream from "./Upstream"; - -@Table({ - tableName: "dimension_gitter_bridges", - underscored: false, - timestamps: false, -}) -export default class GitterBridgeRecord extends Model { - @PrimaryKey - @AutoIncrement - @Column - id: number; - - @AllowNull - @Column - @ForeignKey(() => Upstream) - upstreamId?: number; - - @AllowNull - @Column - provisionUrl?: string; - - @Column - isEnabled: boolean; -} \ No newline at end of file diff --git a/src/integrations/Bridge.ts b/src/integrations/Bridge.ts index 4685fa0..9de882f 100644 --- a/src/integrations/Bridge.ts +++ b/src/integrations/Bridge.ts @@ -3,10 +3,9 @@ import BridgeRecord from "../db/models/BridgeRecord"; import { AvailableNetworks, LinkedChannels } from "../bridges/IrcBridge"; import { PortalInfo, PuppetInfo } from "../bridges/TelegramBridge"; import { WebhookConfiguration } from "../bridges/models/webhooks"; -import { BridgedRoom } from "../bridges/GitterBridge"; import { BridgedChannel } from "../bridges/SlackBridge"; -const PRIVATE_ACCESS_SUPPORTED_BRIDGES = ["webhooks", "gitter"]; +const PRIVATE_ACCESS_SUPPORTED_BRIDGES = ["webhooks"]; export class Bridge extends Integration { constructor(bridge: BridgeRecord, public config: any) { @@ -42,12 +41,7 @@ export interface WebhookBridgeConfiguration { botUserId: string; } -export interface GitterBridgeConfiguration { - link: BridgedRoom; - botUserId: string; -} - export interface SlackBridgeConfiguration { link: BridgedChannel; botUserId: string; -} \ No newline at end of file +} diff --git a/src/models/ModularResponses.ts b/src/models/ModularResponses.ts index 9322b48..b3d8519 100644 --- a/src/models/ModularResponses.ts +++ b/src/models/ModularResponses.ts @@ -10,16 +10,9 @@ export interface ModularIrcResponse { }[]; } -export interface ModularGitterResponse { - replies: { - rid: string; - response: T; - }[]; -} - export interface ModularSlackResponse { replies: { rid: string; response: T; }[]; -} \ No newline at end of file +} diff --git a/web/app/admin/bridges/gitter/gitter.component.html b/web/app/admin/bridges/gitter/gitter.component.html deleted file mode 100644 index c504284..0000000 --- a/web/app/admin/bridges/gitter/gitter.component.html +++ /dev/null @@ -1,47 +0,0 @@ -
- -
-
- -
-

- {{'matrix-appservice-gitter' | translate}} - {{'is a Gitter bridge that supports bridging Gitter rooms to Matrix. Users on Matrix are represented as a single bot user in Gitter, however Gitter users are represented as real-looking Matrix users in the room.' | translate}} -

- - - - - - - - - - - - - - - - - -
{{'Name' | translate}}{{'Actions' | translate}}
{{'No bridge configurations.' | translate}}
- {{ bridge.upstreamId ? "matrix.org's bridge" : "Self-hosted bridge" }} - ({{ bridge.provisionUrl }}) - - - - -
- - -
-
-
diff --git a/web/app/admin/bridges/gitter/gitter.component.scss b/web/app/admin/bridges/gitter/gitter.component.scss deleted file mode 100644 index 788d7ed..0000000 --- a/web/app/admin/bridges/gitter/gitter.component.scss +++ /dev/null @@ -1,3 +0,0 @@ -.editButton { - cursor: pointer; -} \ No newline at end of file diff --git a/web/app/admin/bridges/gitter/gitter.component.ts b/web/app/admin/bridges/gitter/gitter.component.ts deleted file mode 100644 index 0d77140..0000000 --- a/web/app/admin/bridges/gitter/gitter.component.ts +++ /dev/null @@ -1,105 +0,0 @@ -import { Component, OnInit } from "@angular/core"; -import { ToasterService } from "angular2-toaster"; -import { Modal, overlayConfigFactory } from "ngx-modialog"; -import { - AdminGitterBridgeManageSelfhostedComponent, - ManageSelfhostedGitterBridgeDialogContext -} from "./manage-selfhosted/manage-selfhosted.component"; -import { AdminGitterApiService } from "../../../shared/services/admin/admin-gitter-api.service"; -import { FE_GitterBridge } from "../../../shared/models/gitter"; -import { FE_Upstream } from "../../../shared/models/admin-responses"; -import { AdminUpstreamApiService } from "../../../shared/services/admin/admin-upstream-api.service"; -import { TranslateService } from "@ngx-translate/core"; - -@Component({ - templateUrl: "./gitter.component.html", - styleUrls: ["./gitter.component.scss"], -}) -export class AdminGitterBridgeComponent implements OnInit { - - public isLoading = true; - public isUpdating = false; - public configurations: FE_GitterBridge[] = []; - - private upstreams: FE_Upstream[]; - - constructor(private gitterApi: AdminGitterApiService, - private upstreamApi: AdminUpstreamApiService, - private toaster: ToasterService, - private modal: Modal, - public translate: TranslateService) { - this.translate = translate; - } - - public ngOnInit() { - this.reload().then(() => this.isLoading = false); - } - - private async reload(): Promise { - try { - this.upstreams = await this.upstreamApi.getUpstreams(); - this.configurations = await this.gitterApi.getBridges(); - } catch (err) { - console.error(err); - this.translate.get('Error loading bridges').subscribe((res: string) => {this.toaster.pop("error", res); } ); - } - } - - public addModularHostedBridge() { - this.isUpdating = true; - - const createBridge = (upstream: FE_Upstream) => { - return this.gitterApi.newFromUpstream(upstream).then(bridge => { - this.configurations.push(bridge); - this.translate.get('matrix.org\'s Gitter bridge added').subscribe((res: string) => {this.toaster.pop("success", res); }); - this.isUpdating = false; - }).catch(err => { - console.error(err); - this.isUpdating = false; - this.translate.get('Error adding matrix.org\'s Gitter Bridge').subscribe((res: string) => {this.toaster.pop("error", res); } ); - }); - }; - - const vectorUpstreams = this.upstreams.filter(u => u.type === "vector"); - if (vectorUpstreams.length === 0) { - console.log("Creating default scalar upstream"); - const scalarUrl = "https://scalar.vector.im/api"; - this.upstreamApi.newUpstream("modular", "vector", scalarUrl, scalarUrl).then(upstream => { - this.upstreams.push(upstream); - createBridge(upstream); - }).catch(err => { - console.error(err); - this.translate.get('Error creating matrix.org\'s Gitter Bridge').subscribe((res: string) => {this.toaster.pop("error", res); } ); - }); - } else createBridge(vectorUpstreams[0]); - } - - public addSelfHostedBridge() { - this.modal.open(AdminGitterBridgeManageSelfhostedComponent, overlayConfigFactory({ - isBlocking: true, - size: 'lg', - - provisionUrl: '', - }, ManageSelfhostedGitterBridgeDialogContext)).result.then(() => { - this.reload().catch(err => { - console.error(err); - this.translate.get('Failed to get an update Gitter bridge list').subscribe((res: string) => {this.toaster.pop("error", res); } ); - }); - }); - } - - public editBridge(bridge: FE_GitterBridge) { - this.modal.open(AdminGitterBridgeManageSelfhostedComponent, overlayConfigFactory({ - isBlocking: true, - size: 'lg', - - provisionUrl: bridge.provisionUrl, - bridgeId: bridge.id, - }, ManageSelfhostedGitterBridgeDialogContext)).result.then(() => { - this.reload().catch(err => { - console.error(err); - this.translate.get('Failed to get an update Gitter bridge list').subscribe((res: string) => {this.toaster.pop("error", res); } ); - }); - }); - } -} diff --git a/web/app/admin/bridges/gitter/manage-selfhosted/manage-selfhosted.component.html b/web/app/admin/bridges/gitter/manage-selfhosted/manage-selfhosted.component.html deleted file mode 100644 index 813275a..0000000 --- a/web/app/admin/bridges/gitter/manage-selfhosted/manage-selfhosted.component.html +++ /dev/null @@ -1,28 +0,0 @@ -
-
-

{{'self-hosted Gitter bridge' | translate}} ({{ isAdding ? "Add a new" : "Edit" }})

-
-
-

- {{'Self-hosted Gitter bridges already have provisioning enabled. Be careful not to expose the API to the public internet.' | translate}} -

- - -
- -
diff --git a/web/app/admin/bridges/gitter/manage-selfhosted/manage-selfhosted.component.scss b/web/app/admin/bridges/gitter/manage-selfhosted/manage-selfhosted.component.scss deleted file mode 100644 index e69de29..0000000 diff --git a/web/app/admin/bridges/gitter/manage-selfhosted/manage-selfhosted.component.ts b/web/app/admin/bridges/gitter/manage-selfhosted/manage-selfhosted.component.ts deleted file mode 100644 index 5dbd481..0000000 --- a/web/app/admin/bridges/gitter/manage-selfhosted/manage-selfhosted.component.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { Component } from "@angular/core"; -import { ToasterService } from "angular2-toaster"; -import { DialogRef, ModalComponent } from "ngx-modialog"; -import { BSModalContext } from "ngx-modialog/plugins/bootstrap"; -import { AdminGitterApiService } from "../../../../shared/services/admin/admin-gitter-api.service"; -import { TranslateService } from "@ngx-translate/core"; - -export class ManageSelfhostedGitterBridgeDialogContext extends BSModalContext { - public provisionUrl: string; - public bridgeId: number; -} - -@Component({ - templateUrl: "./manage-selfhosted.component.html", - styleUrls: ["./manage-selfhosted.component.scss"], -}) -export class AdminGitterBridgeManageSelfhostedComponent implements ModalComponent { - - public isSaving = false; - public provisionUrl: string; - public bridgeId: number; - public isAdding = false; - - constructor(public dialog: DialogRef, - private gitterApi: AdminGitterApiService, - private toaster: ToasterService, - public translate: TranslateService) { - this.translate = translate; - this.provisionUrl = dialog.context.provisionUrl; - this.bridgeId = dialog.context.bridgeId; - this.isAdding = !this.bridgeId; - } - - public add() { - this.isSaving = true; - if (this.isAdding) { - this.gitterApi.newSelfhosted(this.provisionUrl).then(() => { - this.translate.get('Gitter bridge added').subscribe((res: string) => {this.toaster.pop("success", res); }); - this.dialog.close(); - }).catch(err => { - console.error(err); - this.isSaving = false; - this.translate.get('Failed to create Gitter bridge').subscribe((res: string) => { this.toaster.pop("error", res); }); - }); - } else { - this.gitterApi.updateSelfhosted(this.bridgeId, this.provisionUrl).then(() => { - this.translate.get('Gitter bridge updated').subscribe((res: string) => {this.toaster.pop("success", res); }); - this.dialog.close(); - }).catch(err => { - console.error(err); - this.isSaving = false; - this.translate.get('Failed to update Gitter bridge').subscribe((res: string) => {this.toaster.pop("error", res); }); - }); - } - } -} diff --git a/web/app/app.module.ts b/web/app/app.module.ts index c78d2d7..5f1f022 100644 --- a/web/app/app.module.ts +++ b/web/app/app.module.ts @@ -90,11 +90,6 @@ import { AdminWebhooksBridgeComponent } from "./admin/bridges/webhooks/webhooks. import { AdminWebhooksApiService } from "./shared/services/admin/admin-webhooks-api.service"; import { WebhooksApiService } from "./shared/services/integrations/webhooks-api.service"; import { WebhooksBridgeConfigComponent } from "./configs/bridge/webhooks/webhooks.bridge.component"; -import { AdminGitterBridgeComponent } from "./admin/bridges/gitter/gitter.component"; -import { AdminGitterBridgeManageSelfhostedComponent } from "./admin/bridges/gitter/manage-selfhosted/manage-selfhosted.component"; -import { AdminGitterApiService } from "./shared/services/admin/admin-gitter-api.service"; -import { GitterBridgeConfigComponent } from "./configs/bridge/gitter/gitter.bridge.component"; -import { GitterApiService } from "./shared/services/integrations/gitter-api.service"; import { GenericFullscreenWidgetWrapperComponent } from "./widget-wrappers/generic-fullscreen/generic-fullscreen.component"; import { GrafanaWidgetConfigComponent } from "./configs/widget/grafana/grafana.widget.component"; import { TradingViewWidgetConfigComponent } from "./configs/widget/tradingview/tradingview.widget.component"; @@ -215,9 +210,6 @@ export function HttpLoaderFactory(http: HttpClient) { AdminWebhooksBridgeManageSelfhostedComponent, AdminWebhooksBridgeComponent, WebhooksBridgeConfigComponent, - AdminGitterBridgeComponent, - AdminGitterBridgeManageSelfhostedComponent, - GitterBridgeConfigComponent, GenericFullscreenWidgetWrapperComponent, GrafanaWidgetConfigComponent, TradingViewWidgetConfigComponent, @@ -262,8 +254,6 @@ export function HttpLoaderFactory(http: HttpClient) { TelegramApiService, AdminWebhooksApiService, WebhooksApiService, - AdminGitterApiService, - GitterApiService, AdminCustomSimpleBotsApiService, SlackApiService, AdminSlackApiService, @@ -290,7 +280,6 @@ export function HttpLoaderFactory(http: HttpClient) { TelegramAskUnbridgeComponent, TelegramCannotUnbridgeComponent, AdminWebhooksBridgeManageSelfhostedComponent, - AdminGitterBridgeManageSelfhostedComponent, AdminAddCustomBotComponent, AdminSlackBridgeManageSelfhostedComponent, AdminLogoutConfirmationDialogComponent, diff --git a/web/app/app.routing.ts b/web/app/app.routing.ts index 25641f6..a93ba2d 100644 --- a/web/app/app.routing.ts +++ b/web/app/app.routing.ts @@ -33,8 +33,6 @@ import { AdminTelegramBridgeComponent } from "./admin/bridges/telegram/telegram. import { TelegramBridgeConfigComponent } from "./configs/bridge/telegram/telegram.bridge.component"; import { AdminWebhooksBridgeComponent } from "./admin/bridges/webhooks/webhooks.component"; import { WebhooksBridgeConfigComponent } from "./configs/bridge/webhooks/webhooks.bridge.component"; -import { AdminGitterBridgeComponent } from "./admin/bridges/gitter/gitter.component"; -import { GitterBridgeConfigComponent } from "./configs/bridge/gitter/gitter.bridge.component"; import { GenericFullscreenWidgetWrapperComponent } from "./widget-wrappers/generic-fullscreen/generic-fullscreen.component"; import { GrafanaWidgetConfigComponent } from "./configs/widget/grafana/grafana.widget.component"; import { TradingViewWidgetConfigComponent } from "./configs/widget/tradingview/tradingview.widget.component"; @@ -131,11 +129,6 @@ const routes: Routes = [ component: AdminWebhooksBridgeComponent, data: {breadcrumb: "Webhook Bridge", name: "Webhook Bridge"}, }, - { - path: "gitter", - component: AdminGitterBridgeComponent, - data: {breadcrumb: "Gitter Bridge", name: "Gitter Bridge"}, - }, { path: "slack", component: AdminSlackBridgeComponent, @@ -273,11 +266,6 @@ const routes: Routes = [ component: WebhooksBridgeConfigComponent, data: {breadcrumb: "Webhook Bridge Configuration", name: "Webhook Bridge Configuration"}, }, - { - path: "gitter", - component: GitterBridgeConfigComponent, - data: {breadcrumb: "Gitter Bridge Configuration", name: "Gitter Bridge Configuration"}, - }, { path: "slack", component: SlackBridgeConfigComponent, diff --git a/web/app/configs/bridge/gitter/gitter.bridge.component.html b/web/app/configs/bridge/gitter/gitter.bridge.component.html deleted file mode 100644 index a5f23d3..0000000 --- a/web/app/configs/bridge/gitter/gitter.bridge.component.html +++ /dev/null @@ -1,27 +0,0 @@ - - - -
- {{'Bridge to Gitter' | translate}} -
-
-
- {{'This room is bridged to on Gitter' | translate}} "{{ bridge.config.link.gitterRoomName }}" {{'on Gitter' | translate}}. - -
-
- - -
-
-
-
-
diff --git a/web/app/configs/bridge/gitter/gitter.bridge.component.scss b/web/app/configs/bridge/gitter/gitter.bridge.component.scss deleted file mode 100644 index 7c9eeab..0000000 --- a/web/app/configs/bridge/gitter/gitter.bridge.component.scss +++ /dev/null @@ -1,4 +0,0 @@ -.actions-col { - width: 120px; - text-align: center; -} \ No newline at end of file diff --git a/web/app/configs/bridge/gitter/gitter.bridge.component.ts b/web/app/configs/bridge/gitter/gitter.bridge.component.ts deleted file mode 100644 index b9436f2..0000000 --- a/web/app/configs/bridge/gitter/gitter.bridge.component.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { Component } from "@angular/core"; -import { BridgeComponent } from "../bridge.component"; -import { FE_GitterLink } from "../../../shared/models/gitter"; -import { GitterApiService } from "../../../shared/services/integrations/gitter-api.service"; -import { ScalarClientApiService } from "../../../shared/services/scalar/scalar-client-api.service"; -import { TranslateService } from "@ngx-translate/core"; - -interface GitterConfig { - botUserId: string; - link: FE_GitterLink; -} - -@Component({ - templateUrl: "gitter.bridge.component.html", - styleUrls: ["gitter.bridge.component.scss"], -}) -export class GitterBridgeConfigComponent extends BridgeComponent { - - public gitterRoomName: string; - public isBusy: boolean; - - constructor(private gitter: GitterApiService, private scalar: ScalarClientApiService, public translate: TranslateService) { - super("gitter", translate); - this.translate = translate; - } - - public get isBridged(): boolean { - return this.bridge.config.link && !!this.bridge.config.link.gitterRoomName; - } - - public async bridgeRoom(): Promise { - 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.gitter.bridgeRoom(this.roomId, this.gitterRoomName).then(link => { - this.bridge.config.link = link; - 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.gitter.unbridgeRoom(this.roomId).then(() => { - this.bridge.config.link = null; - 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); }); - }); - } -} diff --git a/web/app/shared/models/gitter.ts b/web/app/shared/models/gitter.ts deleted file mode 100644 index ee242b5..0000000 --- a/web/app/shared/models/gitter.ts +++ /dev/null @@ -1,11 +0,0 @@ -export interface FE_GitterBridge { - id: number; - upstreamId?: number; - provisionUrl?: string; - isEnabled: boolean; -} - -export interface FE_GitterLink { - roomId: string; - gitterRoomName: string; -} \ No newline at end of file diff --git a/web/app/shared/registry/integrations.registry.ts b/web/app/shared/registry/integrations.registry.ts index 4c834bf..6f60fd3 100644 --- a/web/app/shared/registry/integrations.registry.ts +++ b/web/app/shared/registry/integrations.registry.ts @@ -30,7 +30,6 @@ export class IntegrationsRegistry { "irc": {}, "telegram": {}, "webhooks": {}, - "gitter": {}, "slack": {}, }, "widget": { diff --git a/web/app/shared/services/admin/admin-gitter-api.service.ts b/web/app/shared/services/admin/admin-gitter-api.service.ts deleted file mode 100644 index 04eff0c..0000000 --- a/web/app/shared/services/admin/admin-gitter-api.service.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { Injectable } from "@angular/core"; -import { AuthedApi } from "../authed-api"; -import { FE_Upstream } from "../../models/admin-responses"; -import { FE_GitterBridge } from "../../models/gitter"; -import { HttpClient } from "@angular/common/http"; - -@Injectable() -export class AdminGitterApiService extends AuthedApi { - constructor(http: HttpClient) { - super(http); - } - - public getBridges(): Promise { - return this.authedGet("/api/v1/dimension/admin/gitter/all").toPromise(); - } - - public getBridge(bridgeId: number): Promise { - return this.authedGet("/api/v1/dimension/admin/gitter/" + bridgeId).toPromise(); - } - - public newFromUpstream(upstream: FE_Upstream): Promise { - return this.authedPost("/api/v1/dimension/admin/gitter/new/upstream", {upstreamId: upstream.id}).toPromise(); - } - - public newSelfhosted(provisionUrl: string): Promise { - return this.authedPost("/api/v1/dimension/admin/gitter/new/selfhosted", { - provisionUrl: provisionUrl, - }).toPromise(); - } - - public updateSelfhosted(bridgeId: number, provisionUrl: string): Promise { - return this.authedPost("/api/v1/dimension/admin/gitter/" + bridgeId, { - provisionUrl: provisionUrl, - }).toPromise(); - } -} diff --git a/web/app/shared/services/integrations/gitter-api.service.ts b/web/app/shared/services/integrations/gitter-api.service.ts deleted file mode 100644 index 3bc3aa5..0000000 --- a/web/app/shared/services/integrations/gitter-api.service.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { Injectable } from "@angular/core"; -import { AuthedApi } from "../authed-api"; -import { FE_GitterLink } from "../../models/gitter"; -import { HttpClient } from "@angular/common/http"; - -@Injectable() -export class GitterApiService extends AuthedApi { - constructor(http: HttpClient) { - super(http); - } - - public bridgeRoom(roomId: string, gitterRoomName: string): Promise { - return this.authedPost("/api/v1/dimension/gitter/room/" + roomId + "/link", {gitterRoomName}).toPromise(); - } - - public unbridgeRoom(roomId: string): Promise { - return this.authedDelete("/api/v1/dimension/gitter/room/" + roomId + "/link").toPromise(); - } - - public getLink(roomId: string): Promise { - return this.authedGet("/api/v1/dimension/gitter/room/" + roomId + "/link").toPromise(); - } -} \ No newline at end of file diff --git a/web/public/img/avatars/gitter.png b/web/public/img/avatars/gitter.png deleted file mode 100644 index 52d83b70867bf31bb60ec99f01bf3e0e595ea27d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3128 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 SI3TwG0000