Support configuring the upstream go-neb bots

This commit is contained in:
Travis Ralston 2017-12-28 18:22:50 -07:00
parent 5314bea52d
commit 8b3f6e37ce
30 changed files with 817 additions and 45 deletions

View File

@ -22,4 +22,26 @@ export class MemoryCache {
public clear(): void {
this.internalCache.clear();
}
}
}
class _CacheManager {
private caches: { [namespace: string]: MemoryCache } = {};
public for(namespace: string): MemoryCache {
let cache = this.caches[namespace];
if (!cache) {
cache = this.caches[namespace] = new MemoryCache();
}
return cache;
}
}
export const Cache = new _CacheManager();
export const CACHE_INTEGRATIONS = "integrations";
export const CACHE_NEB = "neb";
export const CACHE_UPSTREAM = "upstream";
export const CACHE_SCALAR_ACCOUNTS = "scalar-accounts";
export const CACHE_WIDGET_TITLES = "widget-titles";
export const CACHE_FEDERATION = "federation";

View File

@ -28,7 +28,7 @@ export class DimensionAdminService {
}
public static validateAndGetAdminTokenOwner(scalarToken: string): Promise<string> {
return ScalarService.getTokenOwner(scalarToken).then(userId => {
return ScalarService.getTokenOwner(scalarToken, true).then(userId => {
if (!DimensionAdminService.isAdmin(userId))
throw new ApiError(401, {message: "You must be an administrator to use this API"});
else return userId;

View File

@ -4,6 +4,7 @@ import { ApiError } from "../ApiError";
import { DimensionAdminService } from "./DimensionAdminService";
import { DimensionIntegrationsService, IntegrationsResponse } from "./DimensionIntegrationsService";
import { WidgetStore } from "../../db/WidgetStore";
import { CACHE_INTEGRATIONS, Cache } from "../../MemoryCache";
interface SetEnabledRequest {
enabled: boolean;
@ -23,7 +24,7 @@ export class DimensionIntegrationsAdminService {
if (category === "widget") {
return WidgetStore.setOptions(type, body.options);
} else throw new ApiError(400, "Unrecongized category");
}).then(() => DimensionIntegrationsService.clearIntegrationCache());
}).then(() => Cache.for(CACHE_INTEGRATIONS).clear());
}
@POST
@ -33,7 +34,7 @@ export class DimensionIntegrationsAdminService {
if (category === "widget") {
return WidgetStore.setEnabled(type, body.enabled);
} else throw new ApiError(400, "Unrecongized category");
}).then(() => DimensionIntegrationsService.clearIntegrationCache());
}).then(() => Cache.for(CACHE_INTEGRATIONS).clear());
}
@GET

View File

@ -2,7 +2,7 @@ import { GET, Path, PathParam, QueryParam } from "typescript-rest";
import * as Promise from "bluebird";
import { ScalarService } from "../scalar/ScalarService";
import { Widget } from "../../integrations/Widget";
import { MemoryCache } from "../../MemoryCache";
import { CACHE_INTEGRATIONS, Cache } from "../../MemoryCache";
import { Integration } from "../../integrations/Integration";
import { ApiError } from "../ApiError";
import { WidgetStore } from "../../db/WidgetStore";
@ -14,14 +14,8 @@ export interface IntegrationsResponse {
@Path("/api/v1/dimension/integrations")
export class DimensionIntegrationsService {
private static integrationCache = new MemoryCache();
public static clearIntegrationCache() {
DimensionIntegrationsService.integrationCache.clear();
}
public static getIntegrations(isEnabledCheck?: boolean): Promise<IntegrationsResponse> {
const cachedResponse = DimensionIntegrationsService.integrationCache.get("integrations_" + isEnabledCheck);
const cachedResponse = Cache.for(CACHE_INTEGRATIONS).get("integrations_" + isEnabledCheck);
if (cachedResponse) {
return cachedResponse;
}
@ -33,7 +27,7 @@ export class DimensionIntegrationsService {
.then(widgets => response.widgets = widgets)
// Cache and return response
.then(() => DimensionIntegrationsService.integrationCache.put("integrations_" + isEnabledCheck, response))
.then(() => Cache.for(CACHE_INTEGRATIONS).put("integrations_" + isEnabledCheck, response))
.then(() => response);
}

View File

@ -0,0 +1,91 @@
import { GET, Path, PathParam, POST, QueryParam } from "typescript-rest";
import * as Promise from "bluebird";
import { DimensionAdminService } from "./DimensionAdminService";
import { Cache, CACHE_NEB } from "../../MemoryCache";
import { NebStore } from "../../db/NebStore";
import { NebConfig } from "../../models/neb";
import { LogService } from "matrix-js-snippets";
import { ApiError } from "../ApiError";
interface CreateWithUpstream {
upstreamId: number;
}
interface CreateWithAppservice {
appserviceId: string;
adminUrl: string;
}
interface SetEnabledRequest {
enabled: boolean;
}
@Path("/api/v1/dimension/admin/neb")
export class DimensionNebAdminService {
@GET
@Path("all")
public getNebConfigs(@QueryParam("scalar_token") scalarToken: string): Promise<NebConfig[]> {
return DimensionAdminService.validateAndGetAdminTokenOwner(scalarToken).then(_userId => {
const cachedConfigs = Cache.for(CACHE_NEB).get("configurations");
if (cachedConfigs) return cachedConfigs;
return NebStore.getAllConfigs().then(configs => {
Cache.for(CACHE_NEB).put("configurations", configs);
return configs;
});
});
}
@GET
@Path(":id/config")
public getNebConfig(@QueryParam("scalar_token") scalarToken: string, @PathParam("id") nebId: number): Promise<NebConfig> {
return this.getNebConfigs(scalarToken).then(configs => {
for (const config of configs) {
if (config.id === nebId) return config;
}
throw new ApiError(404, "Configuration not found");
});
}
@POST
@Path(":id/integration/:type/enabled")
public setIntegrationEnabled(@QueryParam("scalar_token") scalarToken: string, @PathParam("id") nebId: number, @PathParam("type") integrationType: string, request: SetEnabledRequest): Promise<any> {
return DimensionAdminService.validateAndGetAdminTokenOwner(scalarToken).then(_userId => {
return NebStore.getOrCreateIntegration(nebId, integrationType);
}).then(integration => {
integration.isEnabled = request.enabled;
return integration.save();
}).then(() => Cache.for(CACHE_NEB).clear());
}
@POST
@Path("new/upstream")
public newConfigForUpstream(@QueryParam("scalar_token") scalarToken: string, request: CreateWithUpstream): Promise<NebConfig> {
return DimensionAdminService.validateAndGetAdminTokenOwner(scalarToken).then(_userId => {
return NebStore.createForUpstream(request.upstreamId).catch(err => {
LogService.error("DimensionNebAdminService", err);
throw new ApiError(500, "Error creating go-neb instance");
});
}).then(config => {
Cache.for(CACHE_NEB).clear();
return config;
});
}
@POST
@Path("new/appservice")
public newConfigForAppservice(@QueryParam("scalar_token") scalarToken: string, request: CreateWithAppservice): Promise<NebConfig> {
return DimensionAdminService.validateAndGetAdminTokenOwner(scalarToken).then(_userId => {
return NebStore.createForAppservice(request.appserviceId, request.adminUrl).catch(err => {
LogService.error("DimensionNebAdminService", err);
throw new ApiError(500, "Error creating go-neb instance");
});
}).then(config => {
Cache.for(CACHE_NEB).clear();
return config;
});
}
}

View File

@ -0,0 +1,65 @@
import { GET, Path, POST, QueryParam } from "typescript-rest";
import * as Promise from "bluebird";
import { DimensionAdminService } from "./DimensionAdminService";
import { Cache, CACHE_UPSTREAM } from "../../MemoryCache";
import Upstream from "../../db/models/Upstream";
interface UpstreamRepsonse {
id: number;
name: string;
type: string;
scalarUrl: string;
apiUrl: string;
}
interface NewUpstreamRequest {
name: string;
type: string;
scalarUrl: string;
apiUrl: string;
}
@Path("/api/v1/dimension/admin/upstreams")
export class DimensionUpstreamAdminService {
@GET
@Path("all")
public getUpstreams(@QueryParam("scalar_token") scalarToken: string): Promise<UpstreamRepsonse[]> {
return DimensionAdminService.validateAndGetAdminTokenOwner(scalarToken).then(_userId => {
const cachedUpstreams = Cache.for(CACHE_UPSTREAM).get("upstreams");
if (cachedUpstreams) return cachedUpstreams;
return Upstream.findAll().then(upstreams => {
const mapped = upstreams.map(this.mapUpstream);
Cache.for(CACHE_UPSTREAM).put("upstreams", mapped);
return mapped;
});
});
}
@POST
@Path("new")
public createUpstream(@QueryParam("scalar_token") scalarToken: string, request: NewUpstreamRequest): Promise<UpstreamRepsonse> {
return DimensionAdminService.validateAndGetAdminTokenOwner(scalarToken).then(_userId => {
return Upstream.create({
name: request.name,
type: request.type,
scalarUrl: request.scalarUrl,
apiUrl: request.apiUrl,
});
}).then(upstream => {
Cache.for(CACHE_UPSTREAM).clear();
return this.mapUpstream(upstream);
});
}
private mapUpstream(upstream: Upstream): UpstreamRepsonse {
return {
id: upstream.id,
name: upstream.name,
type: upstream.type,
scalarUrl: upstream.scalarUrl,
apiUrl: upstream.apiUrl
};
}
}

View File

@ -10,7 +10,7 @@ import { ApiError } from "../ApiError";
import * as randomString from "random-string";
import { OpenId } from "../../models/OpenId";
import { ScalarAccountResponse, ScalarRegisterResponse } from "../../models/ScalarResponses";
import { MemoryCache } from "../../MemoryCache";
import { Cache, CACHE_SCALAR_ACCOUNTS } from "../../MemoryCache";
import { ScalarStore } from "../../db/ScalarStore";
interface RegisterRequest {
@ -23,19 +23,13 @@ interface RegisterRequest {
@Path("/api/v1/scalar")
export class ScalarService {
private static accountCache = new MemoryCache();
public static clearAccountCache(): void {
ScalarService.accountCache.clear();
}
public static getTokenOwner(scalarToken: string): Promise<string> {
const cachedUserId = ScalarService.accountCache.get(scalarToken);
public static getTokenOwner(scalarToken: string, ignoreUpstreams?: boolean): Promise<string> {
const cachedUserId = Cache.for(CACHE_SCALAR_ACCOUNTS).get(scalarToken);
if (cachedUserId) return Promise.resolve(cachedUserId);
return ScalarStore.getTokenOwner(scalarToken).then(user => {
return ScalarStore.getTokenOwner(scalarToken, ignoreUpstreams).then(user => {
if (!user) return Promise.reject("Invalid token");
ScalarService.accountCache.put(scalarToken, user.userId, 30 * 60 * 1000); // 30 minutes
Cache.for(CACHE_SCALAR_ACCOUNTS).put(scalarToken, user.userId, 30 * 60 * 1000); // 30 minutes
return Promise.resolve(user.userId);
});
}

View File

@ -1,7 +1,7 @@
import { GET, Path, QueryParam } from "typescript-rest";
import * as Promise from "bluebird";
import { LogService } from "matrix-js-snippets";
import { MemoryCache } from "../../MemoryCache";
import { CACHE_WIDGET_TITLES, Cache } from "../../MemoryCache";
import { MatrixLiteClient } from "../../matrix/MatrixLiteClient";
import config from "../../config";
import { ScalarService } from "./ScalarService";
@ -22,10 +22,8 @@ interface UrlPreviewResponse {
@Path("/api/v1/scalar/widgets")
export class ScalarWidgetService {
private static urlCache = new MemoryCache();
private static getUrlTitle(url: string): Promise<UrlPreviewResponse> {
const cachedResult = ScalarWidgetService.urlCache.get(url);
const cachedResult = Cache.for(CACHE_WIDGET_TITLES).get(url);
if (cachedResult) {
cachedResult.cached_response = true;
return Promise.resolve(cachedResult);
@ -44,7 +42,7 @@ export class ScalarWidgetService {
},
error: {message: null},
};
ScalarWidgetService.urlCache.put(url, cachedItem, expirationTime);
Cache.for(CACHE_WIDGET_TITLES).put(url, cachedItem, expirationTime);
return cachedItem;
}).catch(err => {
LogService.error("ScalarWidgetService", "Error getting URL preview");

179
src/db/NebStore.ts Normal file
View File

@ -0,0 +1,179 @@
import * as Promise from "bluebird";
import { resolveIfExists } from "./DimensionStore";
import { NebConfig } from "../models/neb";
import NebConfiguration from "./models/NebConfiguration";
import NebIntegration from "./models/NebIntegration";
import Upstream from "./models/Upstream";
import AppService from "./models/AppService";
import { LogService } from "matrix-js-snippets";
export interface SupportedIntegration {
type: string;
name: string;
avatarUrl: string;
description: string;
}
export class NebStore {
private static INTEGRATIONS_MODULAR_SUPPORTED = ["giphy", "guggy", "github", "google", "imgur", "rss", "travisci", "wikipedia"];
private static INTEGRATIONS = {
"circleci": {
name: "Circle CI",
avatarUrl: "/img/avatars/circleci.png",
description: "Announces build results from Circle CI to the room.",
},
"echo": {
name: "Echo",
avatarUrl: "/img/avatars/echo.png", // TODO: Make this image
description: "Repeats text given to it from !echo",
},
"giphy": {
name: "Giphy",
avatarUrl: "/img/avatars/giphy.png",
description: "Posts a GIF from Giphy using !giphy <query>",
},
"guggy": {
name: "Guggy",
avatarUrl: "/img/avatars/guggy.png",
description: "Send a reaction GIF using !guggy <query>",
},
"github": {
name: "Github",
avatarUrl: "/img/avatars/github.png",
description: "Github issue management and announcements for a repository",
},
"google": {
name: "Google",
avatarUrl: "/img/avatars/google.png",
description: "Searches Google Images using !google image <query>",
},
"imgur": {
name: "Imgur",
avatarUrl: "/img/avatars/imgur.png",
description: "Searches and posts images from Imgur using !imgur <query>",
},
// TODO: Support JIRA
// "jira": {
// name: "Jira",
// avatarUrl: "/img/avatars/jira.png",
// description: "Jira issue management and announcements for a project",
// },
"rss": {
name: "RSS",
avatarUrl: "/img/avatars/rssbot.png",
description: "Announces changes to RSS feeds in the room",
},
"travisci": {
name: "Travis CI",
avatarUrl: "/img/avatars/travisci.png",
description: "Announces build results from Travis CI to the room",
},
"wikipedia": {
name: "Wikipedia",
avatarUrl: "/img/avatars/wikipedia.png",
description: "Searches wikipedia using !wikipedia <query>",
},
};
public static getAllConfigs(): Promise<NebConfig[]> {
return NebConfiguration.findAll().then(configs => {
return Promise.all((configs || []).map(c => NebStore.getConfig(c.id)));
});
}
public static getConfig(id: number): Promise<NebConfig> {
let nebConfig: NebConfiguration;
return NebConfiguration.findByPrimary(id).then(resolveIfExists).then(conf => {
nebConfig = conf;
return NebIntegration.findAll({where: {nebId: id}});
}).then(integrations => {
return NebStore.getCompleteIntegrations(nebConfig, integrations);
}).then(integrations => {
return new NebConfig(nebConfig, integrations);
});
}
public static createForUpstream(upstreamId: number): Promise<NebConfig> {
return Upstream.findByPrimary(upstreamId).then(resolveIfExists).then(upstream => {
return NebConfiguration.create({
upstreamId: upstream.id,
});
}).then(config => {
return NebStore.getConfig(config.id);
});
}
public static createForAppservice(appserviceId: string, adminUrl: string): Promise<NebConfig> {
return AppService.findByPrimary(appserviceId).then(resolveIfExists).then(appservice => {
return NebConfiguration.create({
appserviceId: appservice.id,
adminUrl: adminUrl,
});
}).then(config => {
return NebStore.getConfig(config.id);
});
}
public static getOrCreateIntegration(configurationId: number, integrationType: string): Promise<NebIntegration> {
if (!NebStore.INTEGRATIONS[integrationType]) return Promise.reject(new Error("Integration not supported"));
return NebConfiguration.findByPrimary(configurationId).then(resolveIfExists).then(config => {
return NebIntegration.findOne({where: {nebId: config.id, type: integrationType}});
}).then(integration => {
if (!integration) {
LogService.info("NebStore", "Creating integration " + integrationType + " for NEB " + configurationId);
return NebIntegration.create({
type: integrationType,
name: NebStore.INTEGRATIONS[integrationType].name,
avatarUrl: NebStore.INTEGRATIONS[integrationType].avatarUrl,
description: NebStore.INTEGRATIONS[integrationType].description,
isEnabled: false,
isPublic: true,
nebId: configurationId,
});
} else return Promise.resolve(integration);
});
}
public static getCompleteIntegrations(nebConfig: NebConfiguration, knownIntegrations: NebIntegration[]): Promise<NebIntegration[]> {
const supported = NebStore.getSupportedIntegrations(nebConfig);
const notSupported: SupportedIntegration[] = [];
for (const supportedIntegration of supported) {
let isSupported = false;
for (const integration of knownIntegrations) {
if (integration.type === supportedIntegration.type) {
isSupported = true;
break;
}
}
if (!isSupported) notSupported.push(supportedIntegration);
}
const promises = [];
for (const missingIntegration of notSupported) {
promises.push(NebStore.getOrCreateIntegration(nebConfig.id, missingIntegration.type));
}
return Promise.all(promises).then(addedIntegrations => (addedIntegrations || []).concat(knownIntegrations));
}
public static getSupportedIntegrations(nebConfig: NebConfiguration): SupportedIntegration[] {
const result = [];
for (const type of Object.keys(NebStore.INTEGRATIONS)) {
if (nebConfig.upstreamId && NebStore.INTEGRATIONS_MODULAR_SUPPORTED.indexOf(type) === -1) continue;
const config = JSON.parse(JSON.stringify(NebStore.INTEGRATIONS[type]));
config["type"] = type;
result.push(config);
}
return result;
}
private constructor() {
}
}

View File

@ -30,7 +30,7 @@ export class ScalarStore {
});
}
public static getTokenOwner(scalarToken: string): Promise<User> {
public static getTokenOwner(scalarToken: string, ignoreUpstreams?: boolean): Promise<User> {
let user: User = null;
return UserScalarToken.findAll({
where: {isDimensionToken: true, scalarToken: scalarToken},
@ -41,6 +41,7 @@ export class ScalarStore {
}
user = tokens[0].user;
if (ignoreUpstreams) return true; // they have all the upstreams as far as we're concerned
return ScalarStore.doesUserHaveTokensForAllUpstreams(user.userId);
}).then(hasUpstreams => {
if (!hasUpstreams) {

View File

@ -1,13 +1,11 @@
import * as dns from "dns-then";
import * as Promise from "bluebird";
import { LogService } from "matrix-js-snippets";
import { MemoryCache } from "../MemoryCache";
import { Cache, CACHE_FEDERATION } from "../MemoryCache";
import * as request from "request";
const federationUrlCache = new MemoryCache();
export function getFederationUrl(serverName: string): Promise<string> {
const cachedUrl = federationUrlCache.get(serverName);
const cachedUrl = Cache.for(CACHE_FEDERATION).get(serverName);
if (cachedUrl) {
LogService.verbose("matrix", "Cached federation URL for " + serverName + " is " + cachedUrl);
return Promise.resolve(cachedUrl);
@ -30,7 +28,7 @@ export function getFederationUrl(serverName: string): Promise<string> {
}).then(() => {
if (!serverUrl) serverUrl = "https://" + serverName + ":8448";
LogService.verbose("matrix", "Federation URL for " + serverName + " is " + serverUrl + " - caching for " + expirationMs + " ms");
federationUrlCache.put(serverName, serverUrl, expirationMs);
Cache.for(CACHE_FEDERATION).put(serverName, serverUrl, expirationMs);
return serverUrl;
});
}

19
src/models/neb.ts Normal file
View File

@ -0,0 +1,19 @@
import NebConfiguration from "../db/models/NebConfiguration";
import { Integration } from "../integrations/Integration";
import NebIntegration from "../db/models/NebIntegration";
export class NebConfig {
public id: number;
public adminUrl?: string;
public appserviceId?: string;
public upstreamId?: number;
public integrations: Integration[];
public constructor(config: NebConfiguration, integrations: NebIntegration[]) {
this.id = config.id;
this.adminUrl = config.adminUrl;
this.appserviceId = config.appserviceId;
this.upstreamId = config.upstreamId;
this.integrations = integrations.map(i => new Integration(i));
}
}

View File

@ -1,6 +1,7 @@
<ul class="adminNav">
<li (click)="goto('')" [ngClass]="[isActive('', true) ? 'active' : '']">Dashboard</li>
<li (click)="goto('widgets')" [ngClass]="[isActive('widgets') ? 'active' : '']">Widgets</li>
<li (click)="goto('neb')" [ngClass]="[isActive('neb') ? 'active' : '']">go-neb</li>
</ul>
<span class="version">{{ version }}</span>

View File

@ -0,0 +1,37 @@
<div *ngIf="isLoading">
<my-spinner></my-spinner>
</div>
<div *ngIf="!isLoading">
<my-ibox title="go-neb configuration">
<div class="my-ibox-content">
<p>go-neb supports many different types of bots, each of which is listed below. Here you can configure which
bots this go-neb instance should use.</p>
<table class="table table-striped table-condensed table-bordered">
<thead>
<tr>
<th>Name</th>
<th>Description</th>
<th class="text-center">Actions</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let bot of nebConfig.integrations trackById">
<td>{{ bot.displayName }}</td>
<td>{{ bot.description }}</td>
<td class="text-right">
<span class="editButton" (click)="editBot(widget)"
*ngIf="bot.isEnabled && hasConfig(bot) && !isUpstream">
<i class="fa fa-pencil-alt"></i>
</span>
<ui-switch [checked]="bot.isEnabled" size="small" [disabled]="isUpdating"
(change)="toggleBot(bot)" *ngIf="!isOverlapping(bot)"></ui-switch>
<ui-switch [checked]="false" size="small" [disabled]="true" *ngIf="isOverlapping(bot)"
ngbTooltip="This bot is handled by another go-neb instance"></ui-switch>
</td>
</tr>
</tbody>
</table>
</div>
</my-ibox>
</div>

View File

@ -0,0 +1,87 @@
import { Component, OnDestroy, OnInit } from "@angular/core";
import { FE_NebConfiguration } from "../../../shared/models/admin_responses";
import { AdminNebApiService } from "../../../shared/services/admin/admin-neb-api.service";
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";
@Component({
templateUrl: "./edit.component.html",
styleUrls: ["./edit.component.scss"],
})
export class AdminEditNebComponent implements OnInit, OnDestroy {
public isLoading = true;
public isUpdating = false;
public isUpstream = false;
public nebConfig: FE_NebConfiguration;
private subscription: any;
private overlappingTypes: string[];
private botTypes: string[];
constructor(private nebApi: AdminNebApiService, private route: ActivatedRoute, private toaster: ToasterService) {
}
public ngOnInit() {
this.subscription = this.route.params.subscribe(params => {
this.loadNeb(params["nebId"]);
});
}
public ngOnDestroy() {
this.subscription.unsubscribe();
}
public isOverlapping(bot: FE_Integration) {
return this.overlappingTypes.indexOf(bot.type) !== -1;
}
public hasConfig(bot: FE_Integration): boolean {
return NEB_HAS_CONFIG.indexOf(bot.type) !== -1;
}
public toggleBot(bot: FE_Integration) {
bot.isEnabled = !bot.isEnabled;
this.isUpdating = true;
this.nebApi.toggleIntegration(this.nebConfig.id, bot.type, bot.isEnabled).then(() => {
this.isUpdating = false;
this.toaster.pop("success", "Integration updated");
}).catch(err => {
console.error(err);
bot.isEnabled = !bot.isEnabled; // revert change
this.isUpdating = false;
this.toaster.pop("error", "Error updating integration");
});
}
public editBot(bot: FE_Integration) {
console.log(bot);
}
private loadNeb(nebId: number) {
this.isLoading = true;
this.nebApi.getConfigurations().then(configs => {
const handledTypes: string[] = [];
for (const config of configs) {
if (config.id == nebId) {
this.nebConfig = config;
} else {
for (const type of config.integrations) {
this.botTypes.push(type.type);
handledTypes.push(type.type);
}
}
}
this.overlappingTypes = handledTypes;
this.isUpstream = !!this.nebConfig.upstreamId;
this.isLoading = false;
}).catch(err => {
console.error(err);
this.toaster.pop('error', "Could not get go-neb configuration");
});
}
}

View File

@ -0,0 +1,45 @@
<div *ngIf="isLoading">
<my-spinner></my-spinner>
</div>
<div *ngIf="!isLoading">
<my-ibox title="go-neb configurations">
<div class="my-ibox-content">
<p><a href="https://github.com/matrix-org/go-neb" target="_blank">go-neb</a> is a multi-purpose bot that can
provide various services, such as reaction GIFs and Github notifications. There are two options for
go-neb support in Dimension: using matrix.org's or self-hosting it. Each go-neb instance can have
multiple services associated with it (ie: one go-neb here can do everything).</p>
<table class="table table-striped table-condensed table-bordered">
<thead>
<tr>
<th>Name</th>
<th class="text-center" style="width: 120px;">Actions</th>
</tr>
</thead>
<tbody>
<tr *ngIf="!configurations || configurations.length === 0">
<td colspan="2"><i>No go-neb configurations.</i></td>
</tr>
<tr *ngFor="let neb of configurations trackById">
<td>{{ neb.upstreamId ? "matrix.org's go-neb" : "Self-hosted go-neb" }}</td>
<td class="text-center">
<span class="appsvcConfigButton" (click)="showAppserviceConfig(neb)"
*ngIf="!neb.upstreamId">
<i class="far fa-file"></i>
</span>
<span class="editButton" [routerLink]="[neb.id, 'edit']" title="edit">
<i class="fa fa-pencil-alt"></i>
</span>
</td>
</tr>
</tbody>
</table>
<button type="button" class="btn btn-success btn-sm" (click)="addModularHostedNeb()" *ngIf="!hasModularNeb">
<i class="fa fa-plus"></i> Add matrix.org's go-neb
</button>
<button type="button" class="btn btn-success btn-sm" (click)="addSelfHostedNeb()">
<i class="fa fa-plus"></i> Add self-hosted go-neb
</button>
</div>
</my-ibox>
</div>

View File

@ -0,0 +1,8 @@
tr td:last-child {
vertical-align: middle;
}
.appsvcConfigButton,
.editButton {
cursor: pointer;
}

View File

@ -0,0 +1,103 @@
import { Component } from "@angular/core";
import { ToasterService } from "angular2-toaster";
import { AdminNebApiService } from "../../shared/services/admin/admin-neb-api.service";
import { AdminUpstreamApiService } from "../../shared/services/admin/admin-upstream-api.service";
import { AdminAppserviceApiService } from "../../shared/services/admin/admin-appservice-api.service";
import { FE_Appservice, FE_NebConfiguration, FE_Upstream } from "../../shared/models/admin_responses";
@Component({
templateUrl: "./neb.component.html",
styleUrls: ["./neb.component.scss"],
})
export class AdminNebComponent {
public isLoading = true;
public isAddingModularNeb = false;
public hasModularNeb = false;
public upstreams: FE_Upstream[];
public appservices: FE_Appservice[];
public configurations: FE_NebConfiguration[];
constructor(private nebApi: AdminNebApiService,
private upstreamApi: AdminUpstreamApiService,
private appserviceApi: AdminAppserviceApiService,
private toaster: ToasterService) {
this.reload().then(() => this.isLoading = false).catch(error => {
console.error(error);
this.toaster.pop("error", "Error loading go-neb configuration");
});
}
private reload(): Promise<any> {
return Promise.all([
this.loadAppservices(),
this.loadConfigurations(),
this.loadUpstreams(),
]);
}
private loadUpstreams(): Promise<any> {
return this.upstreamApi.getUpstreams().then(upstreams => {
this.upstreams = upstreams;
});
}
private loadAppservices(): Promise<any> {
return this.appserviceApi.getAppservices().then(appservices => {
this.appservices = appservices;
});
}
private loadConfigurations(): Promise<any> {
return this.nebApi.getConfigurations().then(nebConfigs => {
this.configurations = nebConfigs;
this.isLoading = false;
this.hasModularNeb = false;
for (const neb of this.configurations) {
if (neb.upstreamId) {
this.hasModularNeb = true;
break;
}
}
});
}
public showAppserviceConfig(neb: FE_NebConfiguration) {
console.log(neb);
}
public addSelfHostedNeb() {
console.log("ADD Hosted");
}
public addModularHostedNeb() {
this.isAddingModularNeb = true;
const createNeb = (upstream: FE_Upstream) => {
this.nebApi.newUpstreamConfiguration(upstream).then(neb => {
this.configurations.push(neb);
this.toaster.pop("success", "matrix.org's go-neb added", "Click the pencil icon to enable the bots.");
this.isAddingModularNeb = false;
this.hasModularNeb = true;
}).catch(error => {
console.error(error);
this.isAddingModularNeb = false;
this.toaster.pop("error", "Error adding matrix.org's go-neb");
});
};
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);
createNeb(upstream);
}).catch(err => {
console.error(err);
this.toaster.pop("error", "Error creating matrix.org go-neb");
});
} else createNeb(vectorUpstreams[0]);
}
}

View File

@ -24,7 +24,7 @@
<i class="fa fa-pencil-alt"></i>
</span>
<ui-switch [checked]="widget.isEnabled" size="small" [disabled]="isUpdating"
(change)="disableWidget(widget)"></ui-switch>
(change)="toggleWidget(widget)"></ui-switch>
</td>
</tr>
</tbody>

View File

@ -1,7 +1,3 @@
ul {
padding-left: 25px;
}
.editButton {
cursor: pointer;
position: relative;

View File

@ -28,7 +28,7 @@ export class AdminWidgetsComponent {
});
}
public disableWidget(widget: FE_Widget) {
public toggleWidget(widget: FE_Widget) {
widget.isEnabled = !widget.isEnabled;
this.isUpdating = true;
this.adminIntegrationsApi.toggleIntegration(widget.category, widget.type, widget.isEnabled).then(() => {
@ -39,7 +39,7 @@ export class AdminWidgetsComponent {
widget.isEnabled = !widget.isEnabled; // revert change
this.isUpdating = false;
this.toaster.pop("error", "Error updating widget");
})
});
}
public editWidget(widget: FE_Widget) {

View File

@ -47,6 +47,11 @@ import { AdminWidgetJitsiConfigComponent } from "./admin/widgets/jitsi/jitsi.com
import { AdminIntegrationsApiService } from "./shared/services/admin/admin-integrations-api.service";
import { IntegrationsApiService } from "./shared/services/integrations/integrations-api.service";
import { WidgetApiService } from "./shared/services/integrations/widget-api.service";
import { AdminAppserviceApiService } from "./shared/services/admin/admin-appservice-api.service";
import { AdminNebApiService } from "./shared/services/admin/admin-neb-api.service";
import { AdminUpstreamApiService } from "./shared/services/admin/admin-upstream-api.service";
import { AdminNebComponent } from "./admin/neb/neb.component";
import { AdminEditNebComponent } from "./admin/neb/edit/edit.component";
@NgModule({
imports: [
@ -91,6 +96,8 @@ import { WidgetApiService } from "./shared/services/integrations/widget-api.serv
AdminWidgetsComponent,
AdminWidgetEtherpadConfigComponent,
AdminWidgetJitsiConfigComponent,
AdminNebComponent,
AdminEditNebComponent,
// Vendor
],
@ -102,6 +109,9 @@ import { WidgetApiService } from "./shared/services/integrations/widget-api.serv
ScalarClientApiService,
ScalarServerApiService,
NameService,
AdminAppserviceApiService,
AdminNebApiService,
AdminUpstreamApiService,
{provide: Window, useValue: window},
// Vendor

View File

@ -16,6 +16,8 @@ import { YoutubeWidgetConfigComponent } from "./configs/widget/youtube/youtube.w
import { AdminComponent } from "./admin/admin.component";
import { AdminHomeComponent } from "./admin/home/home.component";
import { AdminWidgetsComponent } from "./admin/widgets/widgets.component";
import { AdminNebComponent } from "./admin/neb/neb.component";
import { AdminEditNebComponent } from "./admin/neb/edit/edit.component";
const routes: Routes = [
{path: "", component: HomeComponent},
@ -43,6 +45,21 @@ const routes: Routes = [
component: AdminWidgetsComponent,
data: {breadcrumb: "Widgets", name: "Widgets"},
},
{
path: "neb",
data: {breadcrumb: "go-neb", name: "go-neb configuration"},
children: [
{
path: "",
component: AdminNebComponent,
},
{
path: ":nebId/edit",
component: AdminEditNebComponent,
data: {breadcrumb: "Edit go-neb", name: "Edit go-neb"},
}
]
},
],
},
{

View File

@ -1,3 +1,5 @@
import { FE_Integration } from "./integration";
export interface FE_DimensionConfig {
admins: string[];
widgetBlacklist: string[];
@ -10,4 +12,27 @@ export interface FE_DimensionConfig {
export interface FE_DimensionVersion {
version: string;
}
export interface FE_Upstream {
id: number;
name: string;
type: string;
scalarUrl: string;
apiUrl: string;
}
export interface FE_Appservice {
id: number;
hsToken: string;
asToken: string;
userPrefix: string;
}
export interface FE_NebConfiguration {
id: number;
adminUrl?: string;
appserviceId?: string;
upstreamId?: string;
integrations: FE_Integration[];
}

View File

@ -0,0 +1,8 @@
export const NEB_HAS_CONFIG = [
"giphy",
"guggy",
"github",
"google",
"imgur",
"wikipedia",
];

View File

@ -0,0 +1,15 @@
import { Injectable } from "@angular/core";
import { Http } from "@angular/http";
import { AuthedApi } from "../AuthedApi";
import { FE_Appservice } from "../../models/admin_responses";
@Injectable()
export class AdminAppserviceApiService extends AuthedApi {
constructor(http: Http) {
super(http);
}
public getAppservices(): Promise<FE_Appservice[]> {
return this.authedGet("/api/v1/dimension/admin/appservices/all").map(r => r.json()).toPromise();
}
}

View File

@ -10,14 +10,14 @@ export class AdminIntegrationsApiService extends AuthedApi {
}
public getAllIntegrations(): Promise<FE_IntegrationsResponse> {
return this.authedGet("/api/v1/dimension/integrations/all").map(r => r.json()).toPromise();
return this.authedGet("/api/v1/dimension/admin/integrations/all").map(r => r.json()).toPromise();
}
public toggleIntegration(category: string, type: string, enabled: boolean): Promise<any> {
return this.authedPost("/api/v1/dimension/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();
}
public setIntegrationOptions(category: string, type: string, options: any): Promise<any> {
return this.authedPost("/api/v1/dimension/integrations/" + category + "/" + type + "/options", {options: options}).map(r => r.json()).toPromise();
return this.authedPost("/api/v1/dimension/admin/integrations/" + category + "/" + type + "/options", {options: options}).map(r => r.json()).toPromise();
}
}

View File

@ -0,0 +1,34 @@
import { Injectable } from "@angular/core";
import { Http } from "@angular/http";
import { AuthedApi } from "../AuthedApi";
import { FE_Appservice, FE_NebConfiguration, FE_Upstream } from "../../models/admin_responses";
@Injectable()
export class AdminNebApiService extends AuthedApi {
constructor(http: Http) {
super(http);
}
public getConfigurations(): Promise<FE_NebConfiguration[]> {
return this.authedGet("/api/v1/dimension/admin/neb/all").map(r => r.json()).toPromise();
}
public getConfiguration(nebId: number): Promise<FE_NebConfiguration> {
return this.authedGet("/api/v1/dimension/admin/neb/" + nebId + "/config").map(r => r.json()).toPromise();
}
public newUpstreamConfiguration(upstream: FE_Upstream): Promise<FE_NebConfiguration> {
return this.authedPost("/api/v1/dimension/admin/neb/new/upstream", {upstreamId: upstream.id}).map(r => r.json()).toPromise();
}
public newAppserviceConfiguration(adminUrl: string, appservice: FE_Appservice): Promise<FE_NebConfiguration> {
return this.authedPost("/api/v1/dimension/admin/neb/new/appservice", {
adminUrl: adminUrl,
appserviceId: appservice.id
}).map(r => r.json()).toPromise();
}
public toggleIntegration(nebId: number, integrationType: string, setEnabled: boolean): Promise<any> {
return this.authedPost("/api/v1/dimension/admin/neb/" + nebId + "/integration/" + integrationType + "/enabled", {enabled: setEnabled}).map(r => r.json()).toPromise();
}
}

View File

@ -0,0 +1,24 @@
import { Injectable } from "@angular/core";
import { Http } from "@angular/http";
import { AuthedApi } from "../AuthedApi";
import { FE_Upstream } from "../../models/admin_responses";
@Injectable()
export class AdminUpstreamApiService extends AuthedApi {
constructor(http: Http) {
super(http);
}
public getUpstreams(): Promise<FE_Upstream[]> {
return this.authedGet("/api/v1/dimension/admin/upstreams/all").map(r => r.json()).toPromise();
}
public newUpstream(name: string, type: string, scalarUrl: string, apiUrl: string): Promise<FE_Upstream> {
return this.authedPost("/api/v1/dimension/admin/upstreams/new", {
name: name,
type: type,
scalarUrl: scalarUrl,
apiUrl: apiUrl,
}).map(r => r.json()).toPromise();
}
}