mirror of
https://github.com/turt2live/matrix-dimension.git
synced 2024-08-31 06:02:09 +00:00
Add the shell for configuring IRC bridges
This commit is contained in:
parent
bd03db7674
commit
76931819af
@ -6,6 +6,7 @@ import { WidgetStore } from "../../db/WidgetStore";
|
|||||||
import { Cache, CACHE_INTEGRATIONS } from "../../MemoryCache";
|
import { Cache, CACHE_INTEGRATIONS } from "../../MemoryCache";
|
||||||
import { Integration } from "../../integrations/Integration";
|
import { Integration } from "../../integrations/Integration";
|
||||||
import { LogService } from "matrix-js-snippets";
|
import { LogService } from "matrix-js-snippets";
|
||||||
|
import { BridgeStore } from "../../db/BridgeStore";
|
||||||
|
|
||||||
interface SetEnabledRequest {
|
interface SetEnabledRequest {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
@ -42,6 +43,7 @@ export class AdminIntegrationsService {
|
|||||||
const userId = await AdminService.validateAndGetAdminTokenOwner(scalarToken);
|
const userId = await AdminService.validateAndGetAdminTokenOwner(scalarToken);
|
||||||
|
|
||||||
if (category === "widget") await WidgetStore.setEnabled(type, body.enabled);
|
if (category === "widget") await WidgetStore.setEnabled(type, body.enabled);
|
||||||
|
else if (category === "bridge") await BridgeStore.setEnabled(type, body.enabled);
|
||||||
else throw new ApiError(400, "Unrecognized category");
|
else throw new ApiError(400, "Unrecognized category");
|
||||||
|
|
||||||
LogService.info("AdminIntegrationsService", userId + " set " + category + "/" + type + " to " + (body.enabled ? "enabled" : "disabled"));
|
LogService.info("AdminIntegrationsService", userId + " set " + category + "/" + type + " to " + (body.enabled ? "enabled" : "disabled"));
|
||||||
@ -51,10 +53,11 @@ export class AdminIntegrationsService {
|
|||||||
|
|
||||||
@GET
|
@GET
|
||||||
@Path(":category/all")
|
@Path(":category/all")
|
||||||
public async getAllIntegrations(@QueryParam("scalar_token") scalarToken: string, @QueryParam("category") category: string): Promise<Integration[]> {
|
public async getAllIntegrations(@QueryParam("scalar_token") scalarToken: string, @PathParam("category") category: string): Promise<Integration[]> {
|
||||||
await AdminService.validateAndGetAdminTokenOwner(scalarToken);
|
const userId = await AdminService.validateAndGetAdminTokenOwner(scalarToken);
|
||||||
|
|
||||||
if (category === "widget") return await DimensionIntegrationsService.getWidgets(false);
|
if (category === "widget") return await DimensionIntegrationsService.getWidgets(false);
|
||||||
|
else if (category === "bridge") return await DimensionIntegrationsService.getBridges(false, userId);
|
||||||
else throw new ApiError(400, "Unrecongized category");
|
else throw new ApiError(400, "Unrecongized category");
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -8,11 +8,14 @@ import { WidgetStore } from "../../db/WidgetStore";
|
|||||||
import { SimpleBot } from "../../integrations/SimpleBot";
|
import { SimpleBot } from "../../integrations/SimpleBot";
|
||||||
import { NebStore } from "../../db/NebStore";
|
import { NebStore } from "../../db/NebStore";
|
||||||
import { ComplexBot } from "../../integrations/ComplexBot";
|
import { ComplexBot } from "../../integrations/ComplexBot";
|
||||||
|
import { Bridge } from "../../integrations/Bridge";
|
||||||
|
import { BridgeStore } from "../../db/BridgeStore";
|
||||||
|
|
||||||
export interface IntegrationsResponse {
|
export interface IntegrationsResponse {
|
||||||
widgets: Widget[],
|
widgets: Widget[],
|
||||||
bots: SimpleBot[],
|
bots: SimpleBot[],
|
||||||
complexBots: ComplexBot[],
|
complexBots: ComplexBot[],
|
||||||
|
bridges: Bridge[],
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -35,6 +38,24 @@ export class DimensionIntegrationsService {
|
|||||||
return widgets;
|
return widgets;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets a list of bridges
|
||||||
|
* @param {boolean} enabledOnly True to only return the enabled bridges
|
||||||
|
* @param {string} forUserId The requesting user ID
|
||||||
|
* @param {string} inRoomId If specified, the room ID to list the bridges in
|
||||||
|
* @returns {Promise<Bridge[]>} Resolves to the bridge list
|
||||||
|
*/
|
||||||
|
public static async getBridges(enabledOnly: boolean, forUserId: string, inRoomId?: string): Promise<Bridge[]> {
|
||||||
|
const cacheKey = inRoomId ? "bridges_" + inRoomId : "bridges";
|
||||||
|
|
||||||
|
const cached = Cache.for(CACHE_INTEGRATIONS).get(cacheKey);
|
||||||
|
if (cached) return cached;
|
||||||
|
|
||||||
|
const bridges = await BridgeStore.listAll(forUserId, enabledOnly ? true : null, inRoomId);
|
||||||
|
Cache.for(CACHE_INTEGRATIONS).put(cacheKey, bridges);
|
||||||
|
return bridges;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets a list of simple bots
|
* Gets a list of simple bots
|
||||||
* @param {string} userId The requesting user ID
|
* @param {string} userId The requesting user ID
|
||||||
@ -72,6 +93,7 @@ export class DimensionIntegrationsService {
|
|||||||
widgets: await DimensionIntegrationsService.getWidgets(true),
|
widgets: await DimensionIntegrationsService.getWidgets(true),
|
||||||
bots: await DimensionIntegrationsService.getSimpleBots(userId),
|
bots: await DimensionIntegrationsService.getSimpleBots(userId),
|
||||||
complexBots: await DimensionIntegrationsService.getComplexBots(userId, roomId),
|
complexBots: await DimensionIntegrationsService.getComplexBots(userId, roomId),
|
||||||
|
bridges: await DimensionIntegrationsService.getBridges(true, userId, roomId),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -83,6 +105,7 @@ export class DimensionIntegrationsService {
|
|||||||
if (category === "widget") return roomConfig.widgets.find(i => i.type === integrationType);
|
if (category === "widget") return roomConfig.widgets.find(i => i.type === integrationType);
|
||||||
else if (category === "bot") return roomConfig.bots.find(i => i.type === integrationType);
|
else if (category === "bot") return roomConfig.bots.find(i => i.type === integrationType);
|
||||||
else if (category === "complex-bot") return roomConfig.complexBots.find(i => i.type === integrationType);
|
else if (category === "complex-bot") return roomConfig.complexBots.find(i => i.type === integrationType);
|
||||||
|
else if (category === "bridge") return roomConfig.bridges.find(i => i.type === integrationType);
|
||||||
else throw new ApiError(400, "Unrecognized category");
|
else throw new ApiError(400, "Unrecognized category");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -92,6 +115,7 @@ export class DimensionIntegrationsService {
|
|||||||
const userId = await ScalarService.getTokenOwner(scalarToken);
|
const userId = await ScalarService.getTokenOwner(scalarToken);
|
||||||
|
|
||||||
if (category === "complex-bot") await NebStore.setComplexBotConfig(userId, integrationType, roomId, newConfig);
|
if (category === "complex-bot") await NebStore.setComplexBotConfig(userId, integrationType, roomId, newConfig);
|
||||||
|
else if (category === "bridge") await BridgeStore.setBridgeRoomConfig(userId, integrationType, roomId, newConfig);
|
||||||
else throw new ApiError(400, "Unrecognized category");
|
else throw new ApiError(400, "Unrecognized category");
|
||||||
|
|
||||||
Cache.for(CACHE_INTEGRATIONS).clear(); // TODO: Improve which cache we invalidate
|
Cache.for(CACHE_INTEGRATIONS).clear(); // TODO: Improve which cache we invalidate
|
||||||
@ -106,6 +130,7 @@ export class DimensionIntegrationsService {
|
|||||||
if (category === "widget") throw new ApiError(400, "Widgets should be removed client-side");
|
if (category === "widget") throw new ApiError(400, "Widgets should be removed client-side");
|
||||||
else if (category === "bot") await NebStore.removeSimpleBot(integrationType, roomId, userId);
|
else if (category === "bot") await NebStore.removeSimpleBot(integrationType, roomId, userId);
|
||||||
else if (category === "complex-bot") throw new ApiError(400, "Complex bots should be removed automatically");
|
else if (category === "complex-bot") throw new ApiError(400, "Complex bots should be removed automatically");
|
||||||
|
else if (category === "bridge") throw new ApiError(400, "Bridges should be removed automatically");
|
||||||
else throw new ApiError(400, "Unrecognized category");
|
else throw new ApiError(400, "Unrecognized category");
|
||||||
|
|
||||||
Cache.for(CACHE_INTEGRATIONS).clear(); // TODO: Improve which cache we invalidate
|
Cache.for(CACHE_INTEGRATIONS).clear(); // TODO: Improve which cache we invalidate
|
||||||
|
19
src/bridges/IrcBridge.ts
Normal file
19
src/bridges/IrcBridge.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import BridgeRecord from "../db/models/BridgeRecord";
|
||||||
|
import { IrcBridgeConfiguration } from "../integrations/Bridge";
|
||||||
|
|
||||||
|
export class IrcBridge {
|
||||||
|
constructor(private bridgeRecord: BridgeRecord) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public async hasNetworks(): Promise<boolean> {
|
||||||
|
return !!this.bridgeRecord;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getRoomConfiguration(requestingUserId: string, inRoomId: string): Promise<IrcBridgeConfiguration> {
|
||||||
|
return <any>{requestingUserId, inRoomId};
|
||||||
|
}
|
||||||
|
|
||||||
|
public async setRoomConfiguration(requestingUserId: string, inRoomId: string, newConfig: IrcBridgeConfiguration): Promise<any> {
|
||||||
|
return <any>{requestingUserId, inRoomId, newConfig};
|
||||||
|
}
|
||||||
|
}
|
66
src/db/BridgeStore.ts
Normal file
66
src/db/BridgeStore.ts
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
import { Bridge } from "../integrations/Bridge";
|
||||||
|
import BridgeRecord from "./models/BridgeRecord";
|
||||||
|
import { IrcBridge } from "../bridges/IrcBridge";
|
||||||
|
|
||||||
|
export class BridgeStore {
|
||||||
|
|
||||||
|
public static async listAll(requestingUserId: string, isEnabled?: boolean, inRoomId?: string): Promise<Bridge[]> {
|
||||||
|
let conditions = {};
|
||||||
|
if (isEnabled === true || isEnabled === false) conditions = {where: {isEnabled: isEnabled}};
|
||||||
|
|
||||||
|
const allRecords = await BridgeRecord.findAll(conditions);
|
||||||
|
const enabledBridges: Bridge[] = [];
|
||||||
|
|
||||||
|
for (const bridgeRecord of allRecords) {
|
||||||
|
if (isEnabled === true || isEnabled === false) {
|
||||||
|
const isLogicallyEnabled = await BridgeStore.isLogicallyEnabled(bridgeRecord);
|
||||||
|
if (isLogicallyEnabled !== isEnabled) continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const bridgeConfig = await BridgeStore.getConfiguration(bridgeRecord, requestingUserId, inRoomId);
|
||||||
|
enabledBridges.push(new Bridge(bridgeRecord, bridgeConfig));
|
||||||
|
}
|
||||||
|
|
||||||
|
return enabledBridges;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async setEnabled(type: string, isEnabled: boolean): Promise<any> {
|
||||||
|
const bridge = await BridgeRecord.findOne({where: {type: type}});
|
||||||
|
if (!bridge) throw new Error("Bridge not found");
|
||||||
|
|
||||||
|
bridge.isEnabled = isEnabled;
|
||||||
|
return bridge.save();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async setBridgeRoomConfig(requestingUserId: string, integrationType: string, inRoomId: string, newConfig: any): Promise<any> {
|
||||||
|
console.log(requestingUserId);
|
||||||
|
console.log(inRoomId);
|
||||||
|
console.log(newConfig);
|
||||||
|
const record = await BridgeRecord.findOne({where: {type: integrationType}});
|
||||||
|
if (!record) throw new Error("Bridge not found");
|
||||||
|
|
||||||
|
if (integrationType === "irc") {
|
||||||
|
const irc = new IrcBridge(record);
|
||||||
|
return irc.setRoomConfiguration(requestingUserId, inRoomId, newConfig);
|
||||||
|
} else throw new Error("Unsupported bridge");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async isLogicallyEnabled(record: BridgeRecord): Promise<boolean> {
|
||||||
|
if (record.type === "irc") {
|
||||||
|
const irc = new IrcBridge(record);
|
||||||
|
return irc.hasNetworks();
|
||||||
|
} else return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async getConfiguration(record: BridgeRecord, requestingUserId: string, inRoomId?: string): Promise<any> {
|
||||||
|
if (record.type === "irc") {
|
||||||
|
if (!inRoomId) return {}; // The bridge's admin config is handled by other APIs
|
||||||
|
const irc = new IrcBridge(record);
|
||||||
|
return irc.getRoomConfiguration(requestingUserId, inRoomId);
|
||||||
|
} else return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
private constructor() {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -15,6 +15,7 @@ import NebBotUser from "./models/NebBotUser";
|
|||||||
import NebNotificationUser from "./models/NebNotificationUser";
|
import NebNotificationUser from "./models/NebNotificationUser";
|
||||||
import NebIntegrationConfig from "./models/NebIntegrationConfig";
|
import NebIntegrationConfig from "./models/NebIntegrationConfig";
|
||||||
import Webhook from "./models/Webhook";
|
import Webhook from "./models/Webhook";
|
||||||
|
import BridgeRecord from "./models/BridgeRecord";
|
||||||
|
|
||||||
class _DimensionStore {
|
class _DimensionStore {
|
||||||
private sequelize: Sequelize;
|
private sequelize: Sequelize;
|
||||||
@ -41,6 +42,7 @@ class _DimensionStore {
|
|||||||
NebNotificationUser,
|
NebNotificationUser,
|
||||||
NebIntegrationConfig,
|
NebIntegrationConfig,
|
||||||
Webhook,
|
Webhook,
|
||||||
|
BridgeRecord,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
30
src/db/migrations/20180330172445-AddBridges.ts
Normal file
30
src/db/migrations/20180330172445-AddBridges.ts
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import { QueryInterface } from "sequelize";
|
||||||
|
import { DataType } from "sequelize-typescript";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
up: (queryInterface: QueryInterface) => {
|
||||||
|
return Promise.resolve()
|
||||||
|
.then(() => queryInterface.createTable("dimension_bridges", {
|
||||||
|
"id": {type: DataType.INTEGER, primaryKey: true, autoIncrement: true, allowNull: false},
|
||||||
|
"type": {type: DataType.STRING, allowNull: false},
|
||||||
|
"name": {type: DataType.STRING, allowNull: false},
|
||||||
|
"avatarUrl": {type: DataType.STRING, allowNull: false},
|
||||||
|
"description": {type: DataType.STRING, allowNull: false},
|
||||||
|
"isEnabled": {type: DataType.BOOLEAN, allowNull: false},
|
||||||
|
"isPublic": {type: DataType.BOOLEAN, allowNull: false},
|
||||||
|
}))
|
||||||
|
.then(() => queryInterface.bulkInsert("dimension_bridges", [
|
||||||
|
{
|
||||||
|
type: "irc",
|
||||||
|
name: "IRC Bridge",
|
||||||
|
avatarUrl: "/img/avatars/irc.png",
|
||||||
|
isEnabled: true,
|
||||||
|
isPublic: true,
|
||||||
|
description: "Bridges IRC channels to rooms, supporting multiple networks",
|
||||||
|
},
|
||||||
|
]));
|
||||||
|
},
|
||||||
|
down: (queryInterface: QueryInterface) => {
|
||||||
|
return queryInterface.dropTable("dimension_bridges");
|
||||||
|
}
|
||||||
|
}
|
32
src/db/models/BridgeRecord.ts
Normal file
32
src/db/models/BridgeRecord.ts
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import { AutoIncrement, Column, Model, PrimaryKey, Table } from "sequelize-typescript";
|
||||||
|
import { IntegrationRecord } from "./IntegrationRecord";
|
||||||
|
|
||||||
|
@Table({
|
||||||
|
tableName: "dimension_bridges",
|
||||||
|
underscoredAll: false,
|
||||||
|
timestamps: false,
|
||||||
|
})
|
||||||
|
export default class BridgeRecord extends Model<BridgeRecord> implements IntegrationRecord {
|
||||||
|
@PrimaryKey
|
||||||
|
@AutoIncrement
|
||||||
|
@Column
|
||||||
|
id: number;
|
||||||
|
|
||||||
|
@Column
|
||||||
|
type: string;
|
||||||
|
|
||||||
|
@Column
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
@Column
|
||||||
|
avatarUrl: string;
|
||||||
|
|
||||||
|
@Column
|
||||||
|
description: string;
|
||||||
|
|
||||||
|
@Column
|
||||||
|
isEnabled: boolean;
|
||||||
|
|
||||||
|
@Column
|
||||||
|
isPublic: boolean;
|
||||||
|
}
|
32
src/integrations/Bridge.ts
Normal file
32
src/integrations/Bridge.ts
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import { Integration } from "./Integration";
|
||||||
|
import BridgeRecord from "../db/models/BridgeRecord";
|
||||||
|
|
||||||
|
export class Bridge extends Integration {
|
||||||
|
constructor(bridge: BridgeRecord, public config: any) {
|
||||||
|
super(bridge);
|
||||||
|
this.category = "bridge";
|
||||||
|
this.requirements = [{
|
||||||
|
condition: "publicRoom",
|
||||||
|
expectedValue: true,
|
||||||
|
argument: null, // not used
|
||||||
|
}];
|
||||||
|
|
||||||
|
// We'll just say we aren't
|
||||||
|
this.isEncryptionSupported = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IrcBridgeConfiguration {
|
||||||
|
availableNetworks: {
|
||||||
|
[networkId: string]: {
|
||||||
|
name: string;
|
||||||
|
bridgeUserId: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
links: {
|
||||||
|
[networkId: string]: {
|
||||||
|
channelName: string;
|
||||||
|
addedByUserId: string;
|
||||||
|
}[];
|
||||||
|
};
|
||||||
|
}
|
@ -2,7 +2,6 @@ Release checklist:
|
|||||||
* IRC Bridge
|
* IRC Bridge
|
||||||
* Update documentation
|
* Update documentation
|
||||||
* Configuration migration (if possible)
|
* Configuration migration (if possible)
|
||||||
* Lots of logging
|
|
||||||
* Final testing (widgets, bots, etc)
|
* Final testing (widgets, bots, etc)
|
||||||
|
|
||||||
After release:
|
After release:
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
<li (click)="goto('')" [ngClass]="[isActive('', true) ? 'active' : '']">Dashboard</li>
|
<li (click)="goto('')" [ngClass]="[isActive('', true) ? 'active' : '']">Dashboard</li>
|
||||||
<li (click)="goto('widgets')" [ngClass]="[isActive('widgets') ? 'active' : '']">Widgets</li>
|
<li (click)="goto('widgets')" [ngClass]="[isActive('widgets') ? 'active' : '']">Widgets</li>
|
||||||
<li (click)="goto('neb')" [ngClass]="[isActive('neb') ? 'active' : '']">go-neb</li>
|
<li (click)="goto('neb')" [ngClass]="[isActive('neb') ? 'active' : '']">go-neb</li>
|
||||||
|
<li (click)="goto('bridges')" [ngClass]="[isActive('bridges') ? 'active' : '']">Bridges</li>
|
||||||
</ul>
|
</ul>
|
||||||
<span class="version">{{ version }}</span>
|
<span class="version">{{ version }}</span>
|
||||||
|
|
||||||
|
37
web/app/admin/bridges/bridges.component.html
Normal file
37
web/app/admin/bridges/bridges.component.html
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
<div *ngIf="isLoading">
|
||||||
|
<my-spinner></my-spinner>
|
||||||
|
</div>
|
||||||
|
<div *ngIf="!isLoading">
|
||||||
|
<my-ibox title="Bridges">
|
||||||
|
<div class="my-ibox-content">
|
||||||
|
<p>
|
||||||
|
Bridges provide a way for rooms to interact with and/or bring in events from a third party network. For
|
||||||
|
example, an IRC bridge can allow IRC and matrix users to communicate with each other.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<table class="table table-striped table-condensed table-bordered">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Description</th>
|
||||||
|
<th class="text-center" style="width: 120px;">Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr *ngIf="!bridges || bridges.length === 0">
|
||||||
|
<td colspan="2"><i>No bridges.</i></td>
|
||||||
|
</tr>
|
||||||
|
<tr *ngFor="let bridge of bridges trackById">
|
||||||
|
<td>{{ bridge.displayName }}</td>
|
||||||
|
<td>{{ bridge.description }}</td>
|
||||||
|
<td class="text-center">
|
||||||
|
<span class="editButton" [routerLink]="[bridge.type]" title="edit">
|
||||||
|
<i class="fa fa-pencil-alt"></i>
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</my-ibox>
|
||||||
|
</div>
|
8
web/app/admin/bridges/bridges.component.scss
Normal file
8
web/app/admin/bridges/bridges.component.scss
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
tr td:last-child {
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.appsvcConfigButton,
|
||||||
|
.editButton {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
28
web/app/admin/bridges/bridges.component.ts
Normal file
28
web/app/admin/bridges/bridges.component.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import { Component, OnInit } from "@angular/core";
|
||||||
|
import { ToasterService } from "angular2-toaster";
|
||||||
|
import { FE_Bridge } from "../../shared/models/integration";
|
||||||
|
import { AdminIntegrationsApiService } from "../../shared/services/admin/admin-integrations-api.service";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
templateUrl: "./bridges.component.html",
|
||||||
|
styleUrls: ["./bridges.component.scss"],
|
||||||
|
})
|
||||||
|
export class AdminBridgesComponent implements OnInit {
|
||||||
|
|
||||||
|
public isLoading = true;
|
||||||
|
public bridges: FE_Bridge<any>[];
|
||||||
|
|
||||||
|
constructor(private adminIntegrations: AdminIntegrationsApiService,
|
||||||
|
private toaster: ToasterService) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public ngOnInit() {
|
||||||
|
this.adminIntegrations.getAllBridges().then(bridges => {
|
||||||
|
this.bridges = bridges.filter(b => b.isEnabled);
|
||||||
|
this.isLoading = false;
|
||||||
|
}).catch(err => {
|
||||||
|
console.error(err);
|
||||||
|
this.toaster.pop("error", "Failed to load bridges");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
49
web/app/admin/bridges/irc/irc.component.html
Normal file
49
web/app/admin/bridges/irc/irc.component.html
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
<div *ngIf="isLoading">
|
||||||
|
<my-spinner></my-spinner>
|
||||||
|
</div>
|
||||||
|
<div *ngIf="!isLoading">
|
||||||
|
<my-ibox title="IRC Bridge Configurations">
|
||||||
|
<div class="my-ibox-content">
|
||||||
|
<p>
|
||||||
|
<a href="https://github.com/matrix-org/matrix-appservice-irc" target="_blank">matrix-appservice-irc</a>
|
||||||
|
is an IRC bridge that supports multiple IRC networks. Dimension is capable of using multiple IRC
|
||||||
|
bridges to better distribute the load across multiple networks in large deployments.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<table class="table table-striped table-condensed table-bordered">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Enabled Networks</th>
|
||||||
|
<th class="text-center" style="width: 120px;">Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr *ngIf="!configurations || configurations.length === 0">
|
||||||
|
<td colspan="2"><i>No bridge configurations.</i></td>
|
||||||
|
</tr>
|
||||||
|
<tr *ngFor="let bridge of configurations trackById">
|
||||||
|
<td>
|
||||||
|
{{ bridge.upstreamId ? "matrix.org's bridge" : "Self-hosted bridge" }}
|
||||||
|
<span class="text-muted" style="display: inline-block;" *ngIf="!bridge.upstreamId">({{ bridge.adminUrl }})</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{{ getEnabledNetworksString(bridge) }}
|
||||||
|
</td>
|
||||||
|
<td class="text-center">
|
||||||
|
<button type="button" class="editButton" (click)="editNetworks(bridge)">
|
||||||
|
<i class="fa fa-pencil-alt"></i>
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<button type="button" class="btn btn-success btn-sm" (click)="addModularHostedBridge()" *ngIf="!hasModularBridge">
|
||||||
|
<i class="fa fa-plus"></i> Add matrix.org's bridge
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-success btn-sm" (click)="addSelfHostedBridge()">
|
||||||
|
<i class="fa fa-plus"></i> Add self-hosted bridge
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</my-ibox>
|
||||||
|
</div>
|
0
web/app/admin/bridges/irc/irc.component.scss
Normal file
0
web/app/admin/bridges/irc/irc.component.scss
Normal file
31
web/app/admin/bridges/irc/irc.component.ts
Normal file
31
web/app/admin/bridges/irc/irc.component.ts
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import { Component } from "@angular/core";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
templateUrl: "./irc.component.html",
|
||||||
|
styleUrls: ["./irc.component.scss"],
|
||||||
|
})
|
||||||
|
export class AdminIrcBridgeComponent {
|
||||||
|
|
||||||
|
public isLoading = true;
|
||||||
|
public hasModularBridge = false;
|
||||||
|
public configurations: any[] = [];
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public getEnabledNetworksString(bridge: any): string {
|
||||||
|
return "TODO: " + bridge;
|
||||||
|
}
|
||||||
|
|
||||||
|
public addModularHostedBridge() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public addSelfHostedBridge() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public editNetworks(bridge: any) {
|
||||||
|
console.log(bridge);
|
||||||
|
}
|
||||||
|
}
|
@ -22,9 +22,12 @@ export class AdminWidgetsComponent {
|
|||||||
public widgets: FE_Widget[];
|
public widgets: FE_Widget[];
|
||||||
|
|
||||||
constructor(private adminIntegrationsApi: AdminIntegrationsApiService, private toaster: ToasterService, private modal: Modal) {
|
constructor(private adminIntegrationsApi: AdminIntegrationsApiService, private toaster: ToasterService, private modal: Modal) {
|
||||||
this.adminIntegrationsApi.getAllWidgets().then(integrations => {
|
this.adminIntegrationsApi.getAllWidgets().then(widgets => {
|
||||||
this.isLoading = false;
|
this.isLoading = false;
|
||||||
this.widgets = integrations.widgets;
|
this.widgets = widgets;
|
||||||
|
}).catch(err => {
|
||||||
|
console.error(err);
|
||||||
|
this.toaster.pop("error", "Failed to load widgets");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -62,6 +62,9 @@ import { ConfigSimpleBotComponent } from "./configs/simple-bot/simple-bot.compon
|
|||||||
import { ConfigScreenComplexBotComponent } from "./configs/complex-bot/config-screen/config-screen.complex-bot.component";
|
import { ConfigScreenComplexBotComponent } from "./configs/complex-bot/config-screen/config-screen.complex-bot.component";
|
||||||
import { RssComplexBotConfigComponent } from "./configs/complex-bot/rss/rss.complex-bot.component";
|
import { RssComplexBotConfigComponent } from "./configs/complex-bot/rss/rss.complex-bot.component";
|
||||||
import { TravisCiComplexBotConfigComponent } from "./configs/complex-bot/travisci/travisci.complex-bot.component";
|
import { TravisCiComplexBotConfigComponent } from "./configs/complex-bot/travisci/travisci.complex-bot.component";
|
||||||
|
import { ConfigScreenBridgeComponent } from "./configs/bridge/config-screen/config-screen.bridge.component";
|
||||||
|
import { AdminBridgesComponent } from "./admin/bridges/bridges.component";
|
||||||
|
import { AdminIrcBridgeComponent } from "./admin/bridges/irc/irc.component";
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
@ -118,6 +121,9 @@ import { TravisCiComplexBotConfigComponent } from "./configs/complex-bot/travisc
|
|||||||
ConfigScreenComplexBotComponent,
|
ConfigScreenComplexBotComponent,
|
||||||
RssComplexBotConfigComponent,
|
RssComplexBotConfigComponent,
|
||||||
TravisCiComplexBotConfigComponent,
|
TravisCiComplexBotConfigComponent,
|
||||||
|
ConfigScreenBridgeComponent,
|
||||||
|
AdminBridgesComponent,
|
||||||
|
AdminIrcBridgeComponent,
|
||||||
|
|
||||||
// Vendor
|
// Vendor
|
||||||
],
|
],
|
||||||
|
@ -21,6 +21,8 @@ import { AdminEditNebComponent } from "./admin/neb/edit/edit.component";
|
|||||||
import { AdminAddSelfhostedNebComponent } from "./admin/neb/add-selfhosted/add-selfhosted.component";
|
import { AdminAddSelfhostedNebComponent } from "./admin/neb/add-selfhosted/add-selfhosted.component";
|
||||||
import { RssComplexBotConfigComponent } from "./configs/complex-bot/rss/rss.complex-bot.component";
|
import { RssComplexBotConfigComponent } from "./configs/complex-bot/rss/rss.complex-bot.component";
|
||||||
import { TravisCiComplexBotConfigComponent } from "./configs/complex-bot/travisci/travisci.complex-bot.component";
|
import { TravisCiComplexBotConfigComponent } from "./configs/complex-bot/travisci/travisci.complex-bot.component";
|
||||||
|
import { AdminBridgesComponent } from "./admin/bridges/bridges.component";
|
||||||
|
import { AdminIrcBridgeComponent } from "./admin/bridges/irc/irc.component";
|
||||||
|
|
||||||
const routes: Routes = [
|
const routes: Routes = [
|
||||||
{path: "", component: HomeComponent},
|
{path: "", component: HomeComponent},
|
||||||
@ -68,6 +70,21 @@ const routes: Routes = [
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "bridges",
|
||||||
|
data: {breadcrumb: "Bridges", name: "Bridges"},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: "",
|
||||||
|
component: AdminBridgesComponent,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "irc",
|
||||||
|
component: AdminIrcBridgeComponent,
|
||||||
|
data: {breadcrumb: "IRC Bridge", name: "IRC Bridge"},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -125,6 +142,15 @@ const routes: Routes = [
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
// {
|
||||||
|
// path: "bridge",
|
||||||
|
// children: [
|
||||||
|
// {
|
||||||
|
// path: "irc",
|
||||||
|
//
|
||||||
|
// }
|
||||||
|
// ]
|
||||||
|
// }
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
69
web/app/configs/bridge/bridge.component.ts
Normal file
69
web/app/configs/bridge/bridge.component.ts
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
import { OnDestroy, OnInit } from "@angular/core";
|
||||||
|
import { FE_Bridge } from "../../shared/models/integration";
|
||||||
|
import { ActivatedRoute } from "@angular/router";
|
||||||
|
import { Subscription } from "rxjs/Subscription";
|
||||||
|
import { IntegrationsApiService } from "../../shared/services/integrations/integrations-api.service";
|
||||||
|
import { ToasterService } from "angular2-toaster";
|
||||||
|
import { ServiceLocator } from "../../shared/registry/locator.service";
|
||||||
|
import { ScalarClientApiService } from "../../shared/services/scalar/scalar-client-api.service";
|
||||||
|
|
||||||
|
export class BridgeComponent<T> implements OnInit, OnDestroy {
|
||||||
|
|
||||||
|
public isLoading = true;
|
||||||
|
public isUpdating = false;
|
||||||
|
public bridge: FE_Bridge<T>;
|
||||||
|
public newConfig: T;
|
||||||
|
public roomId: string;
|
||||||
|
|
||||||
|
private routeQuerySubscription: Subscription;
|
||||||
|
|
||||||
|
protected toaster = ServiceLocator.injector.get(ToasterService);
|
||||||
|
protected integrationsApi = ServiceLocator.injector.get(IntegrationsApiService);
|
||||||
|
protected route = ServiceLocator.injector.get(ActivatedRoute);
|
||||||
|
protected scalarClientApi = ServiceLocator.injector.get(ScalarClientApiService);
|
||||||
|
|
||||||
|
constructor(private integrationType: string) {
|
||||||
|
this.isLoading = true;
|
||||||
|
this.isUpdating = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ngOnInit(): void {
|
||||||
|
this.routeQuerySubscription = this.route.queryParams.subscribe(params => {
|
||||||
|
this.roomId = params['roomId'];
|
||||||
|
this.loadBridge();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public ngOnDestroy(): void {
|
||||||
|
if (this.routeQuerySubscription) this.routeQuerySubscription.unsubscribe();
|
||||||
|
}
|
||||||
|
|
||||||
|
private loadBridge() {
|
||||||
|
this.isLoading = true;
|
||||||
|
this.isUpdating = false;
|
||||||
|
|
||||||
|
this.newConfig = <T>{};
|
||||||
|
|
||||||
|
this.integrationsApi.getIntegrationInRoom("bridge", this.integrationType, this.roomId).then(i => {
|
||||||
|
this.bridge = <FE_Bridge<T>>i;
|
||||||
|
this.newConfig = JSON.parse(JSON.stringify(this.bridge.config));
|
||||||
|
this.isLoading = false;
|
||||||
|
}).catch(err => {
|
||||||
|
console.error(err);
|
||||||
|
this.toaster.pop("error", "Failed to load configuration");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public save(): void {
|
||||||
|
this.isUpdating = true;
|
||||||
|
this.integrationsApi.setIntegrationConfiguration("bridge", this.integrationType, this.roomId, this.newConfig).then(() => {
|
||||||
|
this.toaster.pop("success", "Configuration updated");
|
||||||
|
this.bridge.config = this.newConfig;
|
||||||
|
this.isUpdating = false;
|
||||||
|
}).catch(err => {
|
||||||
|
console.error(err);
|
||||||
|
this.toaster.pop("error", "Error updating configuration");
|
||||||
|
this.isUpdating = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,6 @@
|
|||||||
|
<div *ngIf="bridgeComponent.isLoading">
|
||||||
|
<my-spinner></my-spinner>
|
||||||
|
</div>
|
||||||
|
<div *ngIf="!bridgeComponent.isLoading">
|
||||||
|
<ng-container *ngTemplateOutlet="bridgeParamsTemplate"></ng-container>
|
||||||
|
</div>
|
@ -0,0 +1,16 @@
|
|||||||
|
import { Component, ContentChild, Input, TemplateRef } from "@angular/core";
|
||||||
|
import { BridgeComponent } from "../bridge.component";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: "my-bridge-config",
|
||||||
|
templateUrl: "config-screen.bridge.component.html",
|
||||||
|
styleUrls: ["config-screen.bridge.component.scss"],
|
||||||
|
})
|
||||||
|
export class ConfigScreenBridgeComponent {
|
||||||
|
|
||||||
|
@Input() bridgeComponent: BridgeComponent<any>;
|
||||||
|
@ContentChild(TemplateRef) bridgeParamsTemplate: TemplateRef<any>;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
}
|
||||||
|
}
|
@ -259,6 +259,8 @@ export class RiotHomeComponent {
|
|||||||
console.error(err);
|
console.error(err);
|
||||||
if (requirement.expectedValue) return Promise.reject("Expected to be able to send specific event types");
|
if (requirement.expectedValue) return Promise.reject("Expected to be able to send specific event types");
|
||||||
});
|
});
|
||||||
|
case "userInRoom":
|
||||||
|
// TODO: Implement
|
||||||
default:
|
default:
|
||||||
return Promise.reject("Requirement '" + requirement.condition + "' not found");
|
return Promise.reject("Requirement '" + requirement.condition + "' not found");
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { FE_Widget } from "./integration";
|
import { FE_Bridge, FE_Widget } from "./integration";
|
||||||
|
|
||||||
export interface FE_IntegrationsResponse {
|
export interface FE_IntegrationsResponse {
|
||||||
widgets: FE_Widget[];
|
widgets: FE_Widget[];
|
||||||
|
bridges: FE_Bridge<any>[];
|
||||||
}
|
}
|
@ -26,6 +26,11 @@ export interface FE_ComplexBot<T> extends FE_Integration {
|
|||||||
config: T;
|
config: T;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface FE_Bridge<T> extends FE_Integration {
|
||||||
|
bridgeUserId: string;
|
||||||
|
config: T;
|
||||||
|
}
|
||||||
|
|
||||||
export interface FE_Widget extends FE_Integration {
|
export interface FE_Widget extends FE_Integration {
|
||||||
options: any;
|
options: any;
|
||||||
}
|
}
|
||||||
@ -44,7 +49,7 @@ export interface FE_JitsiWidget extends FE_Widget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface FE_IntegrationRequirement {
|
export interface FE_IntegrationRequirement {
|
||||||
condition: "publicRoom" | "canSendEventTypes";
|
condition: "publicRoom" | "canSendEventTypes" | "userInRoom";
|
||||||
argument: any;
|
argument: any;
|
||||||
expectedValue: any;
|
expectedValue: any;
|
||||||
}
|
}
|
@ -1,7 +1,7 @@
|
|||||||
import { Injectable } from "@angular/core";
|
import { Injectable } from "@angular/core";
|
||||||
import { Http } from "@angular/http";
|
import { Http } from "@angular/http";
|
||||||
import { AuthedApi } from "../authed-api";
|
import { AuthedApi } from "../authed-api";
|
||||||
import { FE_IntegrationsResponse } from "../../models/dimension-responses";
|
import { FE_Bridge, FE_Widget } from "../../models/integration";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AdminIntegrationsApiService extends AuthedApi {
|
export class AdminIntegrationsApiService extends AuthedApi {
|
||||||
@ -9,10 +9,14 @@ export class AdminIntegrationsApiService extends AuthedApi {
|
|||||||
super(http);
|
super(http);
|
||||||
}
|
}
|
||||||
|
|
||||||
public getAllWidgets(): Promise<FE_IntegrationsResponse> {
|
public getAllWidgets(): Promise<FE_Widget[]> {
|
||||||
return this.authedGet("/api/v1/dimension/admin/integrations/widget/all").map(r => r.json()).toPromise();
|
return this.authedGet("/api/v1/dimension/admin/integrations/widget/all").map(r => r.json()).toPromise();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getAllBridges(): Promise<FE_Bridge<any>[]> {
|
||||||
|
return this.authedGet("/api/v1/dimension/admin/integrations/bridge/all").map(r => r.json()).toPromise();
|
||||||
|
}
|
||||||
|
|
||||||
public toggleIntegration(category: string, type: string, enabled: boolean): Promise<any> {
|
public toggleIntegration(category: string, type: string, enabled: boolean): Promise<any> {
|
||||||
return this.authedPost("/api/v1/dimension/admin/integrations/" + category + "/" + type + "/enabled", {enabled: enabled}).map(r => r.json()).toPromise();
|
return this.authedPost("/api/v1/dimension/admin/integrations/" + category + "/" + type + "/enabled", {enabled: enabled}).map(r => r.json()).toPromise();
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user