Add the frontend for Gitter bridging

Fixes https://github.com/turt2live/matrix-dimension/issues/4
Fixes https://github.com/turt2live/matrix-dimension/issues/7
This commit is contained in:
Travis Ralston 2018-10-21 14:20:37 -06:00
parent 2e844a707f
commit edbbd3b8c0
11 changed files with 210 additions and 5 deletions

View File

@ -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<BridgedRoom> {
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<BridgedRoom> {
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<any> {
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");
}
}
}

View File

@ -38,7 +38,7 @@ export class GitterBridge {
const bridge = await this.getDefaultBridge();
if (bridge.upstreamId) {
const info = await this.doUpstreamRequest<ModularGitterResponse<GetBotUserIdResponse>>(bridge, "POST", "/bridges/gitter/_matrix/provision/getbotid");
const info = await this.doUpstreamRequest<ModularGitterResponse<GetBotUserIdResponse>>(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<ModularGitterResponse<BridgedRoomResponse>>(bridge, "POST", "/bridge/gitter/_matrix/provision/getlink", null, requestBody);
const link = await this.doUpstreamRequest<ModularGitterResponse<BridgedRoomResponse>>(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);

View File

@ -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,

View File

@ -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

View File

@ -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"},
},
],
},
{

View File

@ -0,0 +1,27 @@
<my-bridge-config [bridgeComponent]="this">
<ng-template #bridgeParamsTemplate>
<my-ibox [isCollapsible]="false">
<h5 class="my-ibox-title">
Bridge to Gitter
</h5>
<div class="my-ibox-content">
<div *ngIf="isBridged">
This room is bridged to "{{ bridge.config.link.gitterRoomName }}" on Gitter.
<button type="button" class="btn btn-sm btn-danger" [disabled]="isBusy" (click)="unbridgeRoom()">
Unbridge
</button>
</div>
<div *ngIf="!isBridged">
<label class="label-block">
Gitter Room
<input title="room name" type="text" class="form-control form-control-sm col-md-3"
[(ngModel)]="gitterRoomName" [disabled]="isBusy" placeholder="my-org/room" />
</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>

View File

@ -0,0 +1,4 @@
.actions-col {
width: 120px;
text-align: center;
}

View File

@ -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<GitterConfig> {
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<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.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");
});
}
}

View File

@ -3,4 +3,9 @@ export interface FE_GitterBridge {
upstreamId?: number;
provisionUrl?: string;
isEnabled: boolean;
}
export interface FE_GitterLink {
roomId: string;
gitterRoomName: string;
}

View File

@ -19,6 +19,7 @@ export class IntegrationsRegistry {
"irc": {},
"telegram": {},
"webhooks": {},
"gitter": {},
},
"widget": {
"custom": {

View File

@ -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<FE_GitterLink> {
return this.authedPost("/api/v1/dimension/gitter/room/" + roomId + "/link", {gitterRoomName})
.map(r => r.json()).toPromise();
}
public unbridgeRoom(roomId: string): Promise<any> {
return this.authedDelete("/api/v1/dimension/gitter/room/" + roomId + "/link")
.map(r => r.json()).toPromise();
}
public getLink(roomId: string): Promise<FE_GitterLink> {
return this.authedGet("/api/v1/dimension/gitter/room/" + roomId + "/link")
.map(r => r.json()).toPromise();
}
}