diff --git a/src/api/admin/AdminIntegrationsService.ts b/src/api/admin/AdminIntegrationsService.ts index 5509734..47beddd 100644 --- a/src/api/admin/AdminIntegrationsService.ts +++ b/src/api/admin/AdminIntegrationsService.ts @@ -1,9 +1,10 @@ import { GET, Path, PathParam, POST, QueryParam } from "typescript-rest"; import { ApiError } from "../ApiError"; import { AdminService } from "./AdminService"; -import { DimensionIntegrationsService, IntegrationsResponse } from "../dimension/DimensionIntegrationsService"; +import { DimensionIntegrationsService } from "../dimension/DimensionIntegrationsService"; import { WidgetStore } from "../../db/WidgetStore"; import { Cache, CACHE_INTEGRATIONS } from "../../MemoryCache"; +import { Integration } from "../../integrations/Integration"; interface SetEnabledRequest { enabled: boolean; @@ -42,9 +43,11 @@ export class AdminIntegrationsService { } @GET - @Path("all") - public async getAllIntegrations(@QueryParam("scalar_token") scalarToken: string): Promise { + @Path(":category/all") + public async getAllIntegrations(@QueryParam("scalar_token") scalarToken: string, @QueryParam("category") category: string): Promise { await AdminService.validateAndGetAdminTokenOwner(scalarToken); - return DimensionIntegrationsService.getIntegrations(null); + + if (category === "widget") return await DimensionIntegrationsService.getWidgets(false); + else throw new ApiError(400, "Unrecongized category"); } } \ No newline at end of file diff --git a/src/api/admin/AdminNebService.ts b/src/api/admin/AdminNebService.ts index 91121c9..06d1911 100644 --- a/src/api/admin/AdminNebService.ts +++ b/src/api/admin/AdminNebService.ts @@ -1,6 +1,6 @@ import { GET, Path, PathParam, POST, QueryParam } from "typescript-rest"; import { AdminService } from "./AdminService"; -import { Cache, CACHE_NEB } from "../../MemoryCache"; +import { Cache, CACHE_INTEGRATIONS, CACHE_NEB } from "../../MemoryCache"; import { NebStore } from "../../db/NebStore"; import { NebConfig } from "../../models/neb"; import { LogService } from "matrix-js-snippets"; @@ -51,6 +51,7 @@ export class AdminNebService { await AdminService.validateAndGetAdminTokenOwner(scalarToken); await NebStore.setIntegrationEnabled(nebId, integrationType, request.enabled); Cache.for(CACHE_NEB).clear(); + Cache.for(CACHE_INTEGRATIONS).clear(); return {}; // 200 OK } @@ -60,6 +61,7 @@ export class AdminNebService { await AdminService.validateAndGetAdminTokenOwner(scalarToken); await NebStore.setIntegrationConfig(nebId, integrationType, newConfig); Cache.for(CACHE_NEB).clear(); + Cache.for(CACHE_INTEGRATIONS).clear(); return {}; // 200 OK } @@ -78,6 +80,7 @@ export class AdminNebService { try { const neb = await NebStore.createForUpstream(request.upstreamId); Cache.for(CACHE_NEB).clear(); + Cache.for(CACHE_INTEGRATIONS).clear(); return neb; } catch (err) { LogService.error("DimensionNebAdminService", err); @@ -93,6 +96,7 @@ export class AdminNebService { try { const neb = await NebStore.createForAppservice(request.appserviceId, request.adminUrl); Cache.for(CACHE_NEB).clear(); + Cache.for(CACHE_INTEGRATIONS).clear(); return neb; } catch (err) { LogService.error("DimensionNebAdminService", err); diff --git a/src/api/dimension/DimensionIntegrationsService.ts b/src/api/dimension/DimensionIntegrationsService.ts index 6cb11fc..9367c87 100644 --- a/src/api/dimension/DimensionIntegrationsService.ts +++ b/src/api/dimension/DimensionIntegrationsService.ts @@ -5,35 +5,43 @@ import { Cache, CACHE_INTEGRATIONS } from "../../MemoryCache"; import { Integration } from "../../integrations/Integration"; import { ApiError } from "../ApiError"; import { WidgetStore } from "../../db/WidgetStore"; +import { SimpleBot } from "../../integrations/SimpleBot"; import { NebStore } from "../../db/NebStore"; export interface IntegrationsResponse { widgets: Widget[], + bots: SimpleBot[], } @Path("/api/v1/dimension/integrations") export class DimensionIntegrationsService { - public static async getIntegrations(isEnabledCheck?: boolean): Promise { - const cachedIntegrations = Cache.for(CACHE_INTEGRATIONS).get("integrations_" + isEnabledCheck); - if (cachedIntegrations) { - return cachedIntegrations; - } + public static async getWidgets(enabledOnly: boolean): Promise { + const cached = Cache.for(CACHE_INTEGRATIONS).get("widgets"); + if (cached) return cached; - const integrations = { - widgets: await WidgetStore.listAll(isEnabledCheck), - bots: await NebStore.listSimpleBots(), // No enabled check - managed internally - }; + const widgets = await WidgetStore.listAll(enabledOnly ? true : null); + Cache.for(CACHE_INTEGRATIONS).put("widgets", widgets); + return widgets; + } - Cache.for(CACHE_INTEGRATIONS).put("integrations_" + isEnabledCheck, integrations); - return integrations; + public static async getSimpleBots(userId: string): Promise { + const cached = Cache.for(CACHE_INTEGRATIONS).get("simple_bots"); + if (cached) return cached; + + const bots = await NebStore.listSimpleBots(userId); + Cache.for(CACHE_INTEGRATIONS).put("simple_bots", bots); + return bots; } @GET @Path("enabled") public async getEnabledIntegrations(@QueryParam("scalar_token") scalarToken: string): Promise { - await ScalarService.getTokenOwner(scalarToken); - return DimensionIntegrationsService.getIntegrations(true); + const userId = await ScalarService.getTokenOwner(scalarToken); + return { + widgets: await DimensionIntegrationsService.getWidgets(true), + bots: await DimensionIntegrationsService.getSimpleBots(userId), + }; } @GET @@ -48,10 +56,14 @@ export class DimensionIntegrationsService { @Path(":category/:type") public async getIntegration(@PathParam("category") category: string, @PathParam("type") type: string): Promise { // This is intentionally an unauthed endpoint to ensure we can use it in widgets - const integrationsResponse = await DimensionIntegrationsService.getIntegrations(true); - for (const key in integrationsResponse) { - for (const integration of integrationsResponse[key]) { - if (integration.category === category && integration.type === type) return integration; + + let integrations: Integration[] = []; + if (category === "widget") integrations = await DimensionIntegrationsService.getWidgets(true); + else throw new ApiError(400, "Unsupported category"); + + for (const integration of integrations) { + if (integration.category === category && integration.type === type) { + return integration; } } diff --git a/src/db/NebStore.ts b/src/db/NebStore.ts index 745a00e..f782d6f 100644 --- a/src/db/NebStore.ts +++ b/src/db/NebStore.ts @@ -10,6 +10,7 @@ import NebNotificationUser from "./models/NebNotificationUser"; import { AppserviceStore } from "./AppserviceStore"; import config from "../config"; import { SimpleBot } from "../integrations/SimpleBot"; +import { NebProxy } from "../neb/NebProxy"; export interface SupportedIntegration { type: string; @@ -89,7 +90,7 @@ export class NebStore { }, }; - public static async listSimpleBots(): Promise { + public static async listSimpleBots(requestingUserId: string): Promise { const configs = await NebStore.getAllConfigs(); const integrations: { integration: NebIntegration, userId: string }[] = []; const hasTypes: string[] = []; @@ -102,10 +103,11 @@ export class NebStore { if (!metadata || !metadata.simple) continue; if (hasTypes.indexOf(integration.type) !== -1) continue; - // TODO: Handle case of upstream bots - const user = await NebStore.getOrCreateBotUser(config.id, integration.type); - - integrations.push({integration: integration, userId: user.appserviceUserId}); + const proxy = new NebProxy(config, requestingUserId); + integrations.push({ + integration: integration, + userId: await proxy.getBotUserId(integration), + }); hasTypes.push(integration.type); } } diff --git a/src/models/ModularResponses.ts b/src/models/ModularResponses.ts new file mode 100644 index 0000000..01c0720 --- /dev/null +++ b/src/models/ModularResponses.ts @@ -0,0 +1,3 @@ +export interface ModularIntegrationInfoResponse { + bot_user_id: string; +} \ No newline at end of file diff --git a/src/neb/NebClient.ts b/src/neb/NebClient.ts index 4b6755f..18208a0 100644 --- a/src/neb/NebClient.ts +++ b/src/neb/NebClient.ts @@ -15,7 +15,7 @@ export class NebClient { return user.accessToken; } - private getNebType(type: string): string { + public static getNebType(type: string): string { if (type === "rss") return "rssbot"; if (type === "travisci") return "travis-ci"; @@ -37,7 +37,7 @@ export class NebClient { public async setServiceConfig(serviceId: string, userId: string, type: string, serviceConfig: any): Promise { const nebRequest: Service = { ID: serviceId, - Type: this.getNebType(type), + Type: NebClient.getNebType(type), UserID: userId, Config: serviceConfig, }; diff --git a/src/neb/NebProxy.ts b/src/neb/NebProxy.ts new file mode 100644 index 0000000..e5e8de9 --- /dev/null +++ b/src/neb/NebProxy.ts @@ -0,0 +1,66 @@ +import { NebConfig } from "../models/neb"; +import NebIntegration from "../db/models/NebIntegration"; +import { NebStore } from "../db/NebStore"; +import { LogService } from "matrix-js-snippets"; +import * as request from "request"; +import Upstream from "../db/models/Upstream"; +import UserScalarToken from "../db/models/UserScalarToken"; +import { NebClient } from "./NebClient"; +import { ModularIntegrationInfoResponse } from "../models/ModularResponses"; + +export class NebProxy { + constructor(private neb: NebConfig, private requestingUserId: string) { + + } + + public async getBotUserId(integration: NebIntegration) { + 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("/integrations/" + NebClient.getNebType(integration.type)); + return response.bot_user_id; + } catch (err) { + LogService.error("NebProxy", err); + return null; + } + } else { + return (await NebStore.getOrCreateBotUser(this.neb.id, integration.type)).appserviceUserId; + } + } + + private async doUpstreamRequest(endpoint: string, body?: any): Promise { + const upstream = await Upstream.findByPrimary(this.neb.upstreamId); + const token = await UserScalarToken.findOne({ + where: { + upstreamId: upstream.id, + isDimensionToken: false, + userId: this.requestingUserId, + }, + }); + + const apiUrl = upstream.apiUrl.endsWith("/") ? upstream.apiUrl.substring(0, upstream.apiUrl.length - 1) : upstream.apiUrl; + const url = apiUrl + (endpoint.startsWith("/") ? endpoint : "/" + endpoint); + + return new Promise((resolve, reject) => { + request({ + method: "POST", + url: url, + qs: {scalar_token: token.scalarToken}, + json: body, + }, (err, res, _body) => { + if (err) { + LogService.error("NebProxy", "Error calling" + url); + LogService.error("NebProxy", err); + reject(err); + } else if (res.statusCode !== 200) { + LogService.error("NebProxy", "Got status code " + res.statusCode + " when calling " + url); + reject(new Error("Request failed")); + } else { + if (typeof(res.body) === "string") res.body = JSON.parse(res.body); + resolve(res.body); + } + }); + }); + } +} \ No newline at end of file diff --git a/web/app/admin/widgets/widgets.component.ts b/web/app/admin/widgets/widgets.component.ts index 24b341c..3a98c00 100644 --- a/web/app/admin/widgets/widgets.component.ts +++ b/web/app/admin/widgets/widgets.component.ts @@ -22,7 +22,7 @@ export class AdminWidgetsComponent { public widgets: FE_Widget[]; constructor(private adminIntegrationsApi: AdminIntegrationsApiService, private toaster: ToasterService, private modal: Modal) { - this.adminIntegrationsApi.getAllIntegrations().then(integrations => { + this.adminIntegrationsApi.getAllWidgets().then(integrations => { this.isLoading = false; this.widgets = integrations.widgets; }); 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 b1933a1..633a368 100644 --- a/web/app/shared/services/admin/admin-integrations-api.service.ts +++ b/web/app/shared/services/admin/admin-integrations-api.service.ts @@ -9,8 +9,8 @@ export class AdminIntegrationsApiService extends AuthedApi { super(http); } - public getAllIntegrations(): Promise { - return this.authedGet("/api/v1/dimension/admin/integrations/all").map(r => r.json()).toPromise(); + public getAllWidgets(): Promise { + return this.authedGet("/api/v1/dimension/admin/integrations/widget/all").map(r => r.json()).toPromise(); } public toggleIntegration(category: string, type: string, enabled: boolean): Promise {