diff --git a/src/MemoryCache.ts b/src/MemoryCache.ts index 6934d32..2fdae45 100644 --- a/src/MemoryCache.ts +++ b/src/MemoryCache.ts @@ -22,4 +22,26 @@ export class MemoryCache { public clear(): void { this.internalCache.clear(); } -} \ No newline at end of file +} + +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"; \ No newline at end of file diff --git a/src/api/dimension/DimensionAdminService.ts b/src/api/dimension/DimensionAdminService.ts index 9d70638..10f5b38 100644 --- a/src/api/dimension/DimensionAdminService.ts +++ b/src/api/dimension/DimensionAdminService.ts @@ -28,7 +28,7 @@ export class DimensionAdminService { } public static validateAndGetAdminTokenOwner(scalarToken: string): Promise { - 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; diff --git a/src/api/dimension/DimensionIntegrationsAdminService.ts b/src/api/dimension/DimensionIntegrationsAdminService.ts index 1038271..50db610 100644 --- a/src/api/dimension/DimensionIntegrationsAdminService.ts +++ b/src/api/dimension/DimensionIntegrationsAdminService.ts @@ -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 diff --git a/src/api/dimension/DimensionIntegrationsService.ts b/src/api/dimension/DimensionIntegrationsService.ts index 52dfbef..fd6617f 100644 --- a/src/api/dimension/DimensionIntegrationsService.ts +++ b/src/api/dimension/DimensionIntegrationsService.ts @@ -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 { - 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); } diff --git a/src/api/dimension/DimensionNebAdminService.ts b/src/api/dimension/DimensionNebAdminService.ts new file mode 100644 index 0000000..eeebc2b --- /dev/null +++ b/src/api/dimension/DimensionNebAdminService.ts @@ -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 { + 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 { + 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 { + 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 { + 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 { + 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; + }); + } +} \ No newline at end of file diff --git a/src/api/dimension/DimensionUpstreamAdminService.ts b/src/api/dimension/DimensionUpstreamAdminService.ts new file mode 100644 index 0000000..bc8fc6a --- /dev/null +++ b/src/api/dimension/DimensionUpstreamAdminService.ts @@ -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 { + 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 { + 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 + }; + } +} \ No newline at end of file diff --git a/src/api/scalar/ScalarService.ts b/src/api/scalar/ScalarService.ts index bf804ec..3d7c467 100644 --- a/src/api/scalar/ScalarService.ts +++ b/src/api/scalar/ScalarService.ts @@ -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 { - const cachedUserId = ScalarService.accountCache.get(scalarToken); + public static getTokenOwner(scalarToken: string, ignoreUpstreams?: boolean): Promise { + 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); }); } diff --git a/src/api/scalar/ScalarWidgetService.ts b/src/api/scalar/ScalarWidgetService.ts index 39f91bd..46e40b8 100644 --- a/src/api/scalar/ScalarWidgetService.ts +++ b/src/api/scalar/ScalarWidgetService.ts @@ -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 { - 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"); diff --git a/src/db/NebStore.ts b/src/db/NebStore.ts new file mode 100644 index 0000000..faf8849 --- /dev/null +++ b/src/db/NebStore.ts @@ -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 ", + }, + "guggy": { + name: "Guggy", + avatarUrl: "/img/avatars/guggy.png", + description: "Send a reaction GIF using !guggy ", + }, + "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 ", + }, + "imgur": { + name: "Imgur", + avatarUrl: "/img/avatars/imgur.png", + description: "Searches and posts images from Imgur using !imgur ", + }, + // 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 ", + }, + }; + + public static getAllConfigs(): Promise { + return NebConfiguration.findAll().then(configs => { + return Promise.all((configs || []).map(c => NebStore.getConfig(c.id))); + }); + } + + public static getConfig(id: number): Promise { + 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 { + 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 { + 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 { + 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 { + 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() { + } +} \ No newline at end of file diff --git a/src/db/ScalarStore.ts b/src/db/ScalarStore.ts index 909fb5e..b34c5ea 100644 --- a/src/db/ScalarStore.ts +++ b/src/db/ScalarStore.ts @@ -30,7 +30,7 @@ export class ScalarStore { }); } - public static getTokenOwner(scalarToken: string): Promise { + public static getTokenOwner(scalarToken: string, ignoreUpstreams?: boolean): Promise { 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) { diff --git a/src/matrix/helpers.ts b/src/matrix/helpers.ts index 97c16ae..4700890 100644 --- a/src/matrix/helpers.ts +++ b/src/matrix/helpers.ts @@ -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 { - 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 { }).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; }); } diff --git a/src/models/neb.ts b/src/models/neb.ts new file mode 100644 index 0000000..ea94eba --- /dev/null +++ b/src/models/neb.ts @@ -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)); + } +} \ No newline at end of file diff --git a/web/app/admin/admin.component.html b/web/app/admin/admin.component.html index 2556572..000c411 100644 --- a/web/app/admin/admin.component.html +++ b/web/app/admin/admin.component.html @@ -1,6 +1,7 @@
  • Dashboard
  • Widgets
  • +
  • go-neb
{{ version }} diff --git a/web/app/admin/neb/edit/edit.component.html b/web/app/admin/neb/edit/edit.component.html new file mode 100644 index 0000000..1fb290b --- /dev/null +++ b/web/app/admin/neb/edit/edit.component.html @@ -0,0 +1,37 @@ +
+ +
+
+ +
+

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.

+ + + + + + + + + + + + + + + + +
NameDescriptionActions
{{ bot.displayName }}{{ bot.description }} + + + + + +
+
+
+
\ No newline at end of file diff --git a/web/app/admin/neb/edit/edit.component.scss b/web/app/admin/neb/edit/edit.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/web/app/admin/neb/edit/edit.component.ts b/web/app/admin/neb/edit/edit.component.ts new file mode 100644 index 0000000..245b7b3 --- /dev/null +++ b/web/app/admin/neb/edit/edit.component.ts @@ -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"); + }); + } +} diff --git a/web/app/admin/neb/neb.component.html b/web/app/admin/neb/neb.component.html new file mode 100644 index 0000000..11d5803 --- /dev/null +++ b/web/app/admin/neb/neb.component.html @@ -0,0 +1,45 @@ +
+ +
+
+ +
+

go-neb 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).

+ + + + + + + + + + + + + + + + + +
NameActions
No go-neb configurations.
{{ neb.upstreamId ? "matrix.org's go-neb" : "Self-hosted go-neb" }} + + + + + + +
+ + +
+
+
\ No newline at end of file diff --git a/web/app/admin/neb/neb.component.scss b/web/app/admin/neb/neb.component.scss new file mode 100644 index 0000000..847d207 --- /dev/null +++ b/web/app/admin/neb/neb.component.scss @@ -0,0 +1,8 @@ +tr td:last-child { + vertical-align: middle; +} + +.appsvcConfigButton, +.editButton { + cursor: pointer; +} \ No newline at end of file diff --git a/web/app/admin/neb/neb.component.ts b/web/app/admin/neb/neb.component.ts new file mode 100644 index 0000000..619da60 --- /dev/null +++ b/web/app/admin/neb/neb.component.ts @@ -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 { + return Promise.all([ + this.loadAppservices(), + this.loadConfigurations(), + this.loadUpstreams(), + ]); + } + + private loadUpstreams(): Promise { + return this.upstreamApi.getUpstreams().then(upstreams => { + this.upstreams = upstreams; + }); + } + + private loadAppservices(): Promise { + return this.appserviceApi.getAppservices().then(appservices => { + this.appservices = appservices; + }); + } + + private loadConfigurations(): Promise { + 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]); + } +} diff --git a/web/app/admin/widgets/widgets.component.html b/web/app/admin/widgets/widgets.component.html index 2beebd6..ed5c11b 100644 --- a/web/app/admin/widgets/widgets.component.html +++ b/web/app/admin/widgets/widgets.component.html @@ -24,7 +24,7 @@ + (change)="toggleWidget(widget)"> diff --git a/web/app/admin/widgets/widgets.component.scss b/web/app/admin/widgets/widgets.component.scss index 65b9b23..1894618 100644 --- a/web/app/admin/widgets/widgets.component.scss +++ b/web/app/admin/widgets/widgets.component.scss @@ -1,7 +1,3 @@ -ul { - padding-left: 25px; -} - .editButton { cursor: pointer; position: relative; diff --git a/web/app/admin/widgets/widgets.component.ts b/web/app/admin/widgets/widgets.component.ts index 510d1e0..24b341c 100644 --- a/web/app/admin/widgets/widgets.component.ts +++ b/web/app/admin/widgets/widgets.component.ts @@ -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) { diff --git a/web/app/app.module.ts b/web/app/app.module.ts index 7475561..ab11e38 100644 --- a/web/app/app.module.ts +++ b/web/app/app.module.ts @@ -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 diff --git a/web/app/app.routing.ts b/web/app/app.routing.ts index dda540f..f2ad93f 100644 --- a/web/app/app.routing.ts +++ b/web/app/app.routing.ts @@ -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"}, + } + ] + }, ], }, { diff --git a/web/app/shared/models/admin_responses.ts b/web/app/shared/models/admin_responses.ts index 697c8cd..134ae51 100644 --- a/web/app/shared/models/admin_responses.ts +++ b/web/app/shared/models/admin_responses.ts @@ -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[]; } \ No newline at end of file diff --git a/web/app/shared/models/neb.ts b/web/app/shared/models/neb.ts new file mode 100644 index 0000000..33a98e6 --- /dev/null +++ b/web/app/shared/models/neb.ts @@ -0,0 +1,8 @@ +export const NEB_HAS_CONFIG = [ + "giphy", + "guggy", + "github", + "google", + "imgur", + "wikipedia", +]; \ No newline at end of file diff --git a/web/app/shared/services/admin/admin-appservice-api.service.ts b/web/app/shared/services/admin/admin-appservice-api.service.ts new file mode 100644 index 0000000..81b2bf3 --- /dev/null +++ b/web/app/shared/services/admin/admin-appservice-api.service.ts @@ -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 { + return this.authedGet("/api/v1/dimension/admin/appservices/all").map(r => r.json()).toPromise(); + } +} diff --git a/web/app/shared/services/admin/admin-integrations-api.service.ts b/web/app/shared/services/admin/admin-integrations-api.service.ts index e2aee60..b639685 100644 --- a/web/app/shared/services/admin/admin-integrations-api.service.ts +++ b/web/app/shared/services/admin/admin-integrations-api.service.ts @@ -10,14 +10,14 @@ export class AdminIntegrationsApiService extends AuthedApi { } public getAllIntegrations(): Promise { - 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 { - 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 { - 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(); } } diff --git a/web/app/shared/services/admin/admin-neb-api.service.ts b/web/app/shared/services/admin/admin-neb-api.service.ts new file mode 100644 index 0000000..eb2f5e0 --- /dev/null +++ b/web/app/shared/services/admin/admin-neb-api.service.ts @@ -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 { + return this.authedGet("/api/v1/dimension/admin/neb/all").map(r => r.json()).toPromise(); + } + + public getConfiguration(nebId: number): Promise { + return this.authedGet("/api/v1/dimension/admin/neb/" + nebId + "/config").map(r => r.json()).toPromise(); + } + + public newUpstreamConfiguration(upstream: FE_Upstream): Promise { + 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 { + 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 { + return this.authedPost("/api/v1/dimension/admin/neb/" + nebId + "/integration/" + integrationType + "/enabled", {enabled: setEnabled}).map(r => r.json()).toPromise(); + } +} diff --git a/web/app/shared/services/admin/admin-upstream-api.service.ts b/web/app/shared/services/admin/admin-upstream-api.service.ts new file mode 100644 index 0000000..800d06f --- /dev/null +++ b/web/app/shared/services/admin/admin-upstream-api.service.ts @@ -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 { + 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 { + return this.authedPost("/api/v1/dimension/admin/upstreams/new", { + name: name, + type: type, + scalarUrl: scalarUrl, + apiUrl: apiUrl, + }).map(r => r.json()).toPromise(); + } +}