mirror of
https://github.com/turt2live/matrix-dimension.git
synced 2024-10-01 01:05:53 -04:00
Very early support for configuring complex bots
Using the RSS Bot as an example. Notably missing features: * Configuration (feeds) not retrieved * No actual configuration page
This commit is contained in:
parent
b5aec06c04
commit
2c1366d9d7
@ -7,10 +7,12 @@ import { ApiError } from "../ApiError";
|
||||
import { WidgetStore } from "../../db/WidgetStore";
|
||||
import { SimpleBot } from "../../integrations/SimpleBot";
|
||||
import { NebStore } from "../../db/NebStore";
|
||||
import { ComplexBot } from "../../integrations/ComplexBot";
|
||||
|
||||
export interface IntegrationsResponse {
|
||||
widgets: Widget[],
|
||||
bots: SimpleBot[],
|
||||
complexBots: ComplexBot[],
|
||||
}
|
||||
|
||||
@Path("/api/v1/dimension/integrations")
|
||||
@ -34,22 +36,35 @@ export class DimensionIntegrationsService {
|
||||
return bots;
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("enabled")
|
||||
public async getEnabledIntegrations(@QueryParam("scalar_token") scalarToken: string): Promise<IntegrationsResponse> {
|
||||
const userId = await ScalarService.getTokenOwner(scalarToken);
|
||||
return {
|
||||
widgets: await DimensionIntegrationsService.getWidgets(true),
|
||||
bots: await DimensionIntegrationsService.getSimpleBots(userId),
|
||||
};
|
||||
public static async getComplexBots(userId: string, roomId: string): Promise<ComplexBot[]> {
|
||||
const cached = Cache.for(CACHE_INTEGRATIONS).get("complex_bots_" + roomId);
|
||||
if (cached) return cached;
|
||||
|
||||
const bots = await NebStore.listComplexBots(userId, roomId);
|
||||
Cache.for(CACHE_INTEGRATIONS).put("complex_bots_" + roomId, bots);
|
||||
return bots;
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("room/:roomId")
|
||||
public async getIntegrationsInRoom(@QueryParam("scalar_token") scalarToken: string, @PathParam("roomId") roomId: string): Promise<IntegrationsResponse> {
|
||||
console.log(roomId);
|
||||
// TODO: Other integrations
|
||||
return this.getEnabledIntegrations(scalarToken);
|
||||
const userId = await ScalarService.getTokenOwner(scalarToken);
|
||||
return {
|
||||
widgets: await DimensionIntegrationsService.getWidgets(true),
|
||||
bots: await DimensionIntegrationsService.getSimpleBots(userId),
|
||||
complexBots: await DimensionIntegrationsService.getComplexBots(userId, roomId),
|
||||
};
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("room/:roomId/integrations/:category/:type")
|
||||
public async getIntegrationInRoom(@QueryParam("scalar_token") scalarToken: string, @PathParam("roomId") roomId: string, @PathParam("category") category: string, @PathParam("type") integrationType: string): Promise<any> {
|
||||
const roomConfig = await this.getIntegrationsInRoom(scalarToken, roomId); // does auth for us
|
||||
|
||||
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 === "complex-bot") return roomConfig.complexBots.find(i => i.type === integrationType);
|
||||
else throw new ApiError(400, "Unrecognized category");
|
||||
}
|
||||
|
||||
@DELETE
|
||||
|
@ -11,6 +11,7 @@ import { AppserviceStore } from "./AppserviceStore";
|
||||
import config from "../config";
|
||||
import { SimpleBot } from "../integrations/SimpleBot";
|
||||
import { NebProxy } from "../neb/NebProxy";
|
||||
import { ComplexBot } from "../integrations/ComplexBot";
|
||||
|
||||
export interface SupportedIntegration {
|
||||
type: string;
|
||||
@ -29,6 +30,7 @@ export class NebStore {
|
||||
// name: "Circle CI",
|
||||
// avatarUrl: "/img/avatars/circleci.png",
|
||||
// description: "Announces build results from Circle CI to the room.",
|
||||
// simple: false,
|
||||
// },
|
||||
"echo": {
|
||||
name: "Echo",
|
||||
@ -53,6 +55,7 @@ export class NebStore {
|
||||
// name: "Github",
|
||||
// avatarUrl: "/img/avatars/github.png",
|
||||
// description: "Github issue management and announcements for a repository",
|
||||
// simple: false,
|
||||
// },
|
||||
"google": {
|
||||
name: "Google",
|
||||
@ -71,16 +74,19 @@ export class NebStore {
|
||||
// name: "Jira",
|
||||
// avatarUrl: "/img/avatars/jira.png",
|
||||
// description: "Jira issue management and announcements for a project",
|
||||
// simple: false,
|
||||
// },
|
||||
"rss": {
|
||||
name: "RSS",
|
||||
avatarUrl: "/img/avatars/rssbot.png",
|
||||
description: "Announces changes to RSS feeds in the room",
|
||||
simple: false,
|
||||
},
|
||||
"travisci": {
|
||||
name: "Travis CI",
|
||||
avatarUrl: "/img/avatars/travisci.png",
|
||||
description: "Announces build results from Travis CI to the room",
|
||||
simple: false,
|
||||
},
|
||||
"wikipedia": {
|
||||
name: "Wikipedia",
|
||||
@ -90,9 +96,9 @@ export class NebStore {
|
||||
},
|
||||
};
|
||||
|
||||
public static async listEnabledNebSimpleBots(): Promise<{neb: NebConfig, integration: NebIntegration}[]>{
|
||||
private static async listEnabledNebBots(simple: boolean): Promise<{ neb: NebConfig, integration: NebIntegration }[]> {
|
||||
const nebConfigs = await NebStore.getAllConfigs();
|
||||
const integrations: {neb: NebConfig, integration: NebIntegration}[] = [];
|
||||
const integrations: { neb: NebConfig, integration: NebIntegration }[] = [];
|
||||
const hasTypes: string[] = [];
|
||||
|
||||
for (const neb of nebConfigs) {
|
||||
@ -100,7 +106,7 @@ export class NebStore {
|
||||
if (!integration.isEnabled) continue;
|
||||
|
||||
const metadata = NebStore.INTEGRATIONS[integration.type];
|
||||
if (!metadata || !metadata.simple) continue;
|
||||
if (!metadata || metadata.simple !== simple) continue;
|
||||
if (hasTypes.indexOf(integration.type) !== -1) continue;
|
||||
|
||||
integrations.push({neb, integration});
|
||||
@ -111,11 +117,30 @@ export class NebStore {
|
||||
return integrations;
|
||||
}
|
||||
|
||||
public static async listEnabledNebSimpleBots(): Promise<{ neb: NebConfig, integration: NebIntegration }[]> {
|
||||
return NebStore.listEnabledNebBots(true);
|
||||
}
|
||||
|
||||
public static async listEnabledNebComplexBots(): Promise<{ neb: NebConfig, integration: NebIntegration }[]> {
|
||||
return NebStore.listEnabledNebBots(false);
|
||||
}
|
||||
|
||||
public static async listSimpleBots(requestingUserId: string): Promise<SimpleBot[]> {
|
||||
const rawIntegrations = await NebStore.listEnabledNebSimpleBots();
|
||||
return Promise.all(rawIntegrations.map(async i => {
|
||||
const proxy = new NebProxy(i.neb, requestingUserId);
|
||||
return new SimpleBot(i.integration, await proxy.getBotUserId(i.integration));
|
||||
const proxy = new NebProxy(i.neb, requestingUserId);
|
||||
return new SimpleBot(i.integration, await proxy.getBotUserId(i.integration));
|
||||
}));
|
||||
}
|
||||
|
||||
public static async listComplexBots(requestingUserId: string, roomId: string): Promise<ComplexBot[]> {
|
||||
const rawIntegrations = await NebStore.listEnabledNebComplexBots();
|
||||
return Promise.all(rawIntegrations.map(async i => {
|
||||
const proxy = new NebProxy(i.neb, requestingUserId);
|
||||
const notifUserId = await proxy.getNotificationUserId(i.integration, roomId, requestingUserId);
|
||||
const botUserId = null; // TODO: For github
|
||||
// TODO: Get configuration
|
||||
return new ComplexBot(i.integration, notifUserId, botUserId);
|
||||
}));
|
||||
}
|
||||
|
||||
@ -271,6 +296,33 @@ export class NebStore {
|
||||
return users[0];
|
||||
}
|
||||
|
||||
public static async getOrCreateNotificationUser(configurationId: number, integrationType: string, forUserId: string): Promise<NebNotificationUser> {
|
||||
const neb = await NebStore.getConfig(configurationId);
|
||||
if (!neb.appserviceId) throw new Error("Instance not bound to an appservice");
|
||||
|
||||
const integration = await this.getOrCreateIntegration(configurationId, integrationType);
|
||||
const users = await NebNotificationUser.findAll({where: {integrationId: integration.id, ownerId: forUserId}});
|
||||
if (!users || users.length === 0) {
|
||||
const safeUserId = AppserviceStore.getSafeUserId(forUserId);
|
||||
const appservice = await AppserviceStore.getAppservice(neb.appserviceId);
|
||||
const userId = "@" + appservice.userPrefix + "_" + integrationType + "_notifications_" + safeUserId + ":" + config.homeserver.name;
|
||||
const appserviceUser = await AppserviceStore.getOrCreateUser(neb.appserviceId, userId);
|
||||
|
||||
const client = new NebClient(neb);
|
||||
await client.updateUser(userId, integration.isEnabled, false); // creates the user in go-neb
|
||||
|
||||
const serviceId = appservice.id + "_integration_" + integrationType + "_notifications_" + safeUserId;
|
||||
return NebNotificationUser.create({
|
||||
serviceId: serviceId,
|
||||
appserviceUserId: appserviceUser.id,
|
||||
integrationId: integration.id,
|
||||
ownerId: forUserId,
|
||||
});
|
||||
}
|
||||
|
||||
return users[0];
|
||||
}
|
||||
|
||||
public static async setIntegrationConfig(configurationId: number, integrationType: string, newConfig: any): Promise<any> {
|
||||
const botUser = await NebStore.getOrCreateBotUser(configurationId, integrationType);
|
||||
const neb = await NebStore.getConfig(configurationId);
|
||||
|
13
src/integrations/ComplexBot.ts
Normal file
13
src/integrations/ComplexBot.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import { Integration } from "./Integration";
|
||||
import NebIntegration from "../db/models/NebIntegration";
|
||||
|
||||
export class ComplexBot extends Integration {
|
||||
constructor(bot: NebIntegration, public notificationUserId: string, public botUserId?: string) {
|
||||
super(bot);
|
||||
this.category = "complex-bot";
|
||||
this.requirements = [];
|
||||
|
||||
// Notification bots are technically supported in e2e rooms
|
||||
this.isEncryptionSupported = true;
|
||||
}
|
||||
}
|
@ -15,7 +15,7 @@ export class NebProxy {
|
||||
|
||||
}
|
||||
|
||||
public async getBotUserId(integration: NebIntegration) {
|
||||
public async getBotUserId(integration: NebIntegration): Promise<string> {
|
||||
if (integration.nebId !== this.neb.id) throw new Error("Integration is not for this NEB proxy");
|
||||
|
||||
if (this.neb.upstreamId) {
|
||||
@ -31,7 +31,29 @@ export class NebProxy {
|
||||
}
|
||||
}
|
||||
|
||||
public async removeBotFromRoom(integration: NebIntegration, roomId: string) {
|
||||
public async getNotificationUserId(integration: NebIntegration, inRoomId: string, forUserId: string): Promise<string> {
|
||||
if (integration.nebId !== this.neb.id) throw new Error("Integration is not for this NEB proxy");
|
||||
|
||||
if (this.neb.upstreamId) {
|
||||
try {
|
||||
const response = await this.doUpstreamRequest<ModularIntegrationInfoResponse>("/integrations/" + NebClient.getNebType(integration.type), {
|
||||
room_id: inRoomId,
|
||||
});
|
||||
return response.bot_user_id;
|
||||
} catch (err) {
|
||||
LogService.error("NebProxy", err);
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
return (await NebStore.getOrCreateNotificationUser(this.neb.id, integration.type, forUserId)).appserviceUserId;
|
||||
}
|
||||
}
|
||||
|
||||
// public async getComplexBotConfiguration(integration: NebIntegration, roomId: string): Promise<any> {
|
||||
//
|
||||
// }
|
||||
|
||||
public async removeBotFromRoom(integration: NebIntegration, roomId: string): Promise<any> {
|
||||
if (integration.nebId !== this.neb.id) throw new Error("Integration is not for this NEB proxy");
|
||||
|
||||
if (this.neb.upstreamId) {
|
||||
|
@ -1,7 +1,14 @@
|
||||
Release checklist:
|
||||
* Manage custom bots (not sure if anyone actually uses this?)
|
||||
* RSS bot
|
||||
* Travis CI
|
||||
* IRC Bridge
|
||||
* Update documentation
|
||||
* Configuration migration (if possible)
|
||||
* Final testing (widgets, bots, etc)
|
||||
* Lots of logging
|
||||
* Final testing (widgets, bots, etc)
|
||||
|
||||
After release:
|
||||
* Manage custom bots
|
||||
* Github
|
||||
* Circle CI
|
||||
* Avatars/display names for managed go-neb bots
|
@ -4,7 +4,7 @@ import { AdminNebApiService } from "../../../shared/services/admin/admin-neb-api
|
||||
import { ActivatedRoute } from "@angular/router";
|
||||
import { ToasterService } from "angular2-toaster";
|
||||
import { FE_Integration } from "../../../shared/models/integration";
|
||||
import { NEB_HAS_CONFIG } from "../../../shared/models/neb";
|
||||
import { NEB_HAS_CONFIG, NEB_IS_COMPLEX } from "../../../shared/models/neb";
|
||||
import { Modal, overlayConfigFactory } from "ngx-modialog";
|
||||
import { AdminNebGiphyConfigComponent } from "../config/giphy/giphy.component";
|
||||
import { NebBotConfigurationDialogContext } from "../config/config-context";
|
||||
@ -72,7 +72,7 @@ export class AdminEditNebComponent implements OnInit, OnDestroy {
|
||||
if (bot.isEnabled && !this.isUpstream) {
|
||||
if (this.hasConfig(bot)) {
|
||||
this.editBot(bot);
|
||||
} else {
|
||||
} else if (NEB_IS_COMPLEX.indexOf(bot.type) === -1) {
|
||||
try {
|
||||
await this.nebApi.setIntegrationConfiguration(this.nebConfig.id, bot.type, {});
|
||||
} catch (err) {
|
||||
|
@ -59,6 +59,8 @@ import { AdminNebGuggyConfigComponent } from "./admin/neb/config/guggy/guggy.com
|
||||
import { AdminNebGoogleConfigComponent } from "./admin/neb/config/google/google.component";
|
||||
import { AdminNebImgurConfigComponent } from "./admin/neb/config/imgur/imgur.component";
|
||||
import { ConfigSimpleBotComponent } from "./configs/simple-bot/simple-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";
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
@ -112,6 +114,8 @@ import { ConfigSimpleBotComponent } from "./configs/simple-bot/simple-bot.compon
|
||||
AdminNebGoogleConfigComponent,
|
||||
AdminNebImgurConfigComponent,
|
||||
ConfigSimpleBotComponent,
|
||||
ConfigScreenComplexBotComponent,
|
||||
RssComplexBotConfigComponent,
|
||||
|
||||
// Vendor
|
||||
],
|
||||
|
@ -19,6 +19,7 @@ import { AdminWidgetsComponent } from "./admin/widgets/widgets.component";
|
||||
import { AdminNebComponent } from "./admin/neb/neb.component";
|
||||
import { AdminEditNebComponent } from "./admin/neb/edit/edit.component";
|
||||
import { AdminAddSelfhostedNebComponent } from "./admin/neb/add-selfhosted/add-selfhosted.component";
|
||||
import { RssComplexBotConfigComponent } from "./configs/complex-bot/rss/rss.complex-bot.component";
|
||||
|
||||
const routes: Routes = [
|
||||
{path: "", component: HomeComponent},
|
||||
@ -108,6 +109,16 @@ const routes: Routes = [
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "complex-bot",
|
||||
children: [
|
||||
{
|
||||
path: "rss",
|
||||
component: RssComplexBotConfigComponent,
|
||||
data: {breadcrumb: "RSS Bot Configuration", name: "RSS Bot Configuration"}
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
54
web/app/configs/complex-bot/complex-bot.component.ts
Normal file
54
web/app/configs/complex-bot/complex-bot.component.ts
Normal file
@ -0,0 +1,54 @@
|
||||
import { OnDestroy, OnInit } from "@angular/core";
|
||||
import { FE_ComplexBot } 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";
|
||||
|
||||
export class ComplexBotComponent<T> implements OnInit, OnDestroy {
|
||||
|
||||
public isLoading = true;
|
||||
public isUpdating = false;
|
||||
public bot: FE_ComplexBot<T>;
|
||||
public newConfig: T;
|
||||
|
||||
private roomId: string;
|
||||
|
||||
private routeQuerySubscription: Subscription;
|
||||
|
||||
protected toaster = ServiceLocator.injector.get(ToasterService);
|
||||
protected integrationsApi = ServiceLocator.injector.get(IntegrationsApiService);
|
||||
protected route = ServiceLocator.injector.get(ActivatedRoute);
|
||||
|
||||
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.loadBot();
|
||||
});
|
||||
}
|
||||
|
||||
public ngOnDestroy(): void {
|
||||
if (this.routeQuerySubscription) this.routeQuerySubscription.unsubscribe();
|
||||
}
|
||||
|
||||
private loadBot() {
|
||||
this.isLoading = true;
|
||||
this.isUpdating = false;
|
||||
|
||||
this.newConfig = <T>{};
|
||||
|
||||
this.integrationsApi.getIntegrationInRoom("complex-bot", this.integrationType, this.roomId).then(i => {
|
||||
this.bot = <FE_ComplexBot<T>>i;
|
||||
this.isLoading = false;
|
||||
}).catch(err => {
|
||||
console.error(err);
|
||||
this.toaster.pop("error", "Failed to load configuration");
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
<div *ngIf="botComponent.isLoading">
|
||||
<my-spinner></my-spinner>
|
||||
</div>
|
||||
<div *ngIf="!botComponent.isLoading">
|
||||
<my-ibox>
|
||||
<h5 class="my-ibox-title">
|
||||
{{ botComponent.bot.displayName }} configuration
|
||||
</h5>
|
||||
<div class="my-ibox-content">
|
||||
<form (submit)="botComponent.save()" novalidate name="saveForm">
|
||||
<ng-container *ngTemplateOutlet="botParamsTemplate"></ng-container>
|
||||
|
||||
<div style="margin-top: 25px">
|
||||
<button type="submit" class="btn btn-sm btn-success" [disabled]="botComponent.isUpdating">
|
||||
<i class="far fa-save"></i> Save
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</my-ibox>
|
||||
</div>
|
@ -0,0 +1,16 @@
|
||||
import { ComplexBotComponent } from "../complex-bot.component";
|
||||
import { Component, ContentChild, Input, TemplateRef } from "@angular/core";
|
||||
|
||||
@Component({
|
||||
selector: "my-complex-bot-config",
|
||||
templateUrl: "config-screen.complex-bot.component.html",
|
||||
styleUrls: ["config-screen.complex-bot.component.scss"],
|
||||
})
|
||||
export class ConfigScreenComplexBotComponent {
|
||||
|
||||
@Input() botComponent: ComplexBotComponent<any>;
|
||||
@ContentChild(TemplateRef) botParamsTemplate: TemplateRef<any>;
|
||||
|
||||
constructor() {
|
||||
}
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
<my-complex-bot-config [botComponent]="this">
|
||||
<ng-template #botParamsTemplate>
|
||||
<p>{{ bot | json }}</p>
|
||||
</ng-template>
|
||||
</my-complex-bot-config>
|
18
web/app/configs/complex-bot/rss/rss.complex-bot.component.ts
Normal file
18
web/app/configs/complex-bot/rss/rss.complex-bot.component.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import { ComplexBotComponent } from "../complex-bot.component";
|
||||
import { Component } from "@angular/core";
|
||||
|
||||
interface RssConfig {
|
||||
feeds: {
|
||||
[feedUrl: string]: {}; // No options currently
|
||||
};
|
||||
}
|
||||
|
||||
@Component({
|
||||
templateUrl: "rss.complex-bot.component.html",
|
||||
styleUrls: ["rss.complex-bot.component.scss"],
|
||||
})
|
||||
export class RssComplexBotConfigComponent extends ComplexBotComponent<RssConfig> {
|
||||
constructor() {
|
||||
super("rss");
|
||||
}
|
||||
}
|
@ -138,7 +138,7 @@ export class RiotHomeComponent {
|
||||
}, SimpleBotConfigDialogContext));
|
||||
} else {
|
||||
console.log("Navigating to edit screen for " + integration.category + " " + integration.type);
|
||||
this.router.navigate(['riot-app', integration.category, integration.type]);
|
||||
this.router.navigate(['riot-app', integration.category, integration.type], {queryParams: {roomId: this.roomId}});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -20,6 +20,12 @@ export interface FE_SimpleBot extends FE_Integration {
|
||||
userId: string;
|
||||
}
|
||||
|
||||
export interface FE_ComplexBot<T> extends FE_Integration {
|
||||
notificationUserId: string;
|
||||
botUserId?: string;
|
||||
config: T;
|
||||
}
|
||||
|
||||
export interface FE_Widget extends FE_Integration {
|
||||
options: any;
|
||||
}
|
||||
|
@ -4,4 +4,9 @@ export const NEB_HAS_CONFIG = [
|
||||
"github",
|
||||
"google",
|
||||
"imgur",
|
||||
];
|
||||
|
||||
export const NEB_IS_COMPLEX = [
|
||||
"rss",
|
||||
"travisci",
|
||||
];
|
@ -18,6 +18,10 @@ export class IntegrationsApiService extends AuthedApi {
|
||||
return this.http.get("/api/v1/dimension/integrations/" + category + "/" + type).map(r => r.json()).toPromise();
|
||||
}
|
||||
|
||||
public getIntegrationInRoom(category: string, type: string, roomId: string): Promise<FE_Integration> {
|
||||
return this.authedGet("/api/v1/dimension/integrations/room/" + roomId + "/integrations/" + category + "/" + type).map(r => r.json()).toPromise();
|
||||
}
|
||||
|
||||
public getWidget(type: string): Promise<FE_Widget> {
|
||||
return this.getIntegration("widget", type).then(i => <FE_Widget>i);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user