diff --git a/src/api/dimension/DimensionGitterService.ts b/src/api/dimension/DimensionGitterService.ts new file mode 100644 index 0000000..68fd3f7 --- /dev/null +++ b/src/api/dimension/DimensionGitterService.ts @@ -0,0 +1,61 @@ +import { DELETE, GET, Path, PathParam, POST, QueryParam } from "typescript-rest"; +import { ScalarService } from "../scalar/ScalarService"; +import { ApiError } from "../ApiError"; +import { BridgedRoom, GitterBridge } from "../../bridges/GitterBridge"; +import { LogService } from "matrix-js-snippets"; + +interface BridgeRoomRequest { + gitterRoomName: string; +} + +/** + * API for interacting with the Gitter bridge + */ +@Path("/api/v1/dimension/gitter") +export class DimensionGitterService { + + @GET + @Path("room/:roomId/link") + public async getLink(@QueryParam("scalar_token") scalarToken: string, @PathParam("roomId") roomId: string): Promise { + const userId = await ScalarService.getTokenOwner(scalarToken); + + 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") + public async bridgeRoom(@QueryParam("scalar_token") scalarToken: string, @PathParam("roomId") roomId: string, request: BridgeRoomRequest): Promise { + const userId = await ScalarService.getTokenOwner(scalarToken); + + 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") + public async unbridgeRoom(@QueryParam("scalar_token") scalarToken: string, @PathParam("roomId") roomId: string): Promise { + const userId = await ScalarService.getTokenOwner(scalarToken); + + 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 index e107529..398c901 100644 --- a/src/bridges/GitterBridge.ts +++ b/src/bridges/GitterBridge.ts @@ -38,7 +38,7 @@ export class GitterBridge { const bridge = await this.getDefaultBridge(); if (bridge.upstreamId) { - const info = await this.doUpstreamRequest>(bridge, "POST", "/bridges/gitter/_matrix/provision/getbotid"); + 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"); } @@ -59,7 +59,7 @@ export class GitterBridge { try { if (bridge.upstreamId) { delete requestBody["user_id"]; - const link = await this.doUpstreamRequest>(bridge, "POST", "/bridge/gitter/_matrix/provision/getlink", null, requestBody); + 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); @@ -76,6 +76,7 @@ export class GitterBridge { }; } } catch (e) { + if (e.status === 404) return null; LogService.error("GitterBridge", e); throw e; } @@ -147,9 +148,10 @@ export class GitterBridge { 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(new Error("Request failed")); + reject({body: res.body, status: res.statusCode}); } else { if (typeof(res.body) === "string") res.body = JSON.parse(res.body); resolve(res.body); @@ -179,9 +181,10 @@ export class GitterBridge { 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(new Error("Request failed")); + reject({body: res.body, status: res.statusCode}); } else { if (typeof(res.body) === "string") res.body = JSON.parse(res.body); resolve(res.body); diff --git a/src/integrations/Bridge.ts b/src/integrations/Bridge.ts index 59b4510..9233249 100644 --- a/src/integrations/Bridge.ts +++ b/src/integrations/Bridge.ts @@ -5,12 +5,14 @@ import { PortalInfo, PuppetInfo } from "../bridges/TelegramBridge"; import { WebhookConfiguration } from "../bridges/models/webhooks"; import { BridgedRoom } from "../bridges/GitterBridge"; +const PRIVATE_ACCESS_SUPPORTED_BRIDGES = ["webhooks", "gitter"]; + export class Bridge extends Integration { constructor(bridge: BridgeRecord, public config: any) { super(bridge); this.category = "bridge"; - if (bridge.type === "webhooks") this.requirements = []; + if (PRIVATE_ACCESS_SUPPORTED_BRIDGES.indexOf(bridge.type) !== -1) this.requirements = []; else this.requirements = [{ condition: "publicRoom", expectedValue: true, diff --git a/web/app/app.module.ts b/web/app/app.module.ts index 3ba4ac6..c66e69f 100644 --- a/web/app/app.module.ts +++ b/web/app/app.module.ts @@ -93,6 +93,8 @@ import { WebhooksBridgeConfigComponent } from "./configs/bridge/webhooks/webhook 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"; @NgModule({ imports: [ @@ -170,6 +172,7 @@ import { AdminGitterApiService } from "./shared/services/admin/admin-gitter-api. WebhooksBridgeConfigComponent, AdminGitterBridgeComponent, AdminGitterBridgeManageSelfhostedComponent, + GitterBridgeConfigComponent, // Vendor ], @@ -194,6 +197,7 @@ import { AdminGitterApiService } from "./shared/services/admin/admin-gitter-api. AdminWebhooksApiService, WebhooksApiService, AdminGitterApiService, + GitterApiService, {provide: Window, useValue: window}, // Vendor diff --git a/web/app/app.routing.ts b/web/app/app.routing.ts index ca377b5..af463cd 100644 --- a/web/app/app.routing.ts +++ b/web/app/app.routing.ts @@ -32,6 +32,7 @@ import { TelegramBridgeConfigComponent } from "./configs/bridge/telegram/telegra 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"; const routes: Routes = [ {path: "", component: HomeComponent}, @@ -194,6 +195,11 @@ 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"}, + }, ], }, { diff --git a/web/app/configs/bridge/gitter/gitter.bridge.component.html b/web/app/configs/bridge/gitter/gitter.bridge.component.html new file mode 100644 index 0000000..f6f4bcf --- /dev/null +++ b/web/app/configs/bridge/gitter/gitter.bridge.component.html @@ -0,0 +1,27 @@ + + + +
+ Bridge to Gitter +
+
+
+ This room is bridged to "{{ bridge.config.link.gitterRoomName }}" on Gitter. + +
+
+ + +
+
+
+
+
\ No newline at end of file diff --git a/web/app/configs/bridge/gitter/gitter.bridge.component.scss b/web/app/configs/bridge/gitter/gitter.bridge.component.scss new file mode 100644 index 0000000..7c9eeab --- /dev/null +++ b/web/app/configs/bridge/gitter/gitter.bridge.component.scss @@ -0,0 +1,4 @@ +.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 new file mode 100644 index 0000000..060b9c5 --- /dev/null +++ b/web/app/configs/bridge/gitter/gitter.bridge.component.ts @@ -0,0 +1,66 @@ +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"; + +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) { + super("gitter"); + } + + 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.toaster.pop("error", "Error inviting bridge"); + return; + } + } + + this.gitter.bridgeRoom(this.roomId, this.gitterRoomName).then(link => { + this.bridge.config.link = link; + this.isBusy = false; + this.toaster.pop("success", "Bridge requested"); + }).catch(error => { + this.isBusy = false; + console.error(error); + this.toaster.pop("error", "Error requesting bridge"); + }); + } + + public unbridgeRoom(): void { + this.isBusy = true; + this.gitter.unbridgeRoom(this.roomId).then(() => { + this.bridge.config.link = null; + this.isBusy = false; + this.toaster.pop("success", "Bridge removed"); + }).catch(error => { + this.isBusy = false; + console.error(error); + this.toaster.pop("error", "Error removing bridge"); + }); + } +} \ No newline at end of file diff --git a/web/app/shared/models/gitter.ts b/web/app/shared/models/gitter.ts index 0480502..ee242b5 100644 --- a/web/app/shared/models/gitter.ts +++ b/web/app/shared/models/gitter.ts @@ -3,4 +3,9 @@ export interface FE_GitterBridge { 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 cc79200..0a62bd0 100644 --- a/web/app/shared/registry/integrations.registry.ts +++ b/web/app/shared/registry/integrations.registry.ts @@ -19,6 +19,7 @@ export class IntegrationsRegistry { "irc": {}, "telegram": {}, "webhooks": {}, + "gitter": {}, }, "widget": { "custom": { diff --git a/web/app/shared/services/integrations/gitter-api.service.ts b/web/app/shared/services/integrations/gitter-api.service.ts new file mode 100644 index 0000000..5cb9a09 --- /dev/null +++ b/web/app/shared/services/integrations/gitter-api.service.ts @@ -0,0 +1,26 @@ +import { Injectable } from "@angular/core"; +import { Http } from "@angular/http"; +import { AuthedApi } from "../authed-api"; +import { FE_GitterLink } from "../../models/gitter"; + +@Injectable() +export class GitterApiService extends AuthedApi { + constructor(http: Http) { + super(http); + } + + public bridgeRoom(roomId: string, gitterRoomName: string): Promise { + return this.authedPost("/api/v1/dimension/gitter/room/" + roomId + "/link", {gitterRoomName}) + .map(r => r.json()).toPromise(); + } + + public unbridgeRoom(roomId: string): Promise { + return this.authedDelete("/api/v1/dimension/gitter/room/" + roomId + "/link") + .map(r => r.json()).toPromise(); + } + + public getLink(roomId: string): Promise { + return this.authedGet("/api/v1/dimension/gitter/room/" + roomId + "/link") + .map(r => r.json()).toPromise(); + } +} \ No newline at end of file