import { Component } from "@angular/core"; import { ActivatedRoute, Router } from "@angular/router"; import { ScalarClientApiService } from "../../shared/services/scalar/scalar-client-api.service"; import * as _ from "lodash"; import { ScalarServerApiService } from "../../shared/services/scalar/scalar-server-api.service"; import { FE_Integration, FE_IntegrationRequirement, FE_SimpleBot, } from "../../shared/models/integration"; import { IntegrationsRegistry } from "../../shared/registry/integrations.registry"; import { SessionStorage } from "../../shared/SessionStorage"; import { AdminApiService } from "../../shared/services/admin/admin-api.service"; import { IntegrationsApiService } from "../../shared/services/integrations/integrations-api.service"; import { ToasterService } from "angular2-toaster"; import { StickerApiService } from "../../shared/services/integrations/sticker-api.service"; import { TranslateService } from "@ngx-translate/core"; import { NgbModal } from "@ng-bootstrap/ng-bootstrap"; import { SimpleBotController } from "../../shared/helpers/SimpleBotController"; const CATEGORY_MAP = { Widgets: ["widget"], Bots: ["complex-bot", "bot"], Bridges: ["bridge"], }; @Component({ selector: "my-element-home", templateUrl: "./home.component.html", styleUrls: ["./home.component.scss"], }) export class ElementHomeComponent { public isLoading = true; public isError = false; public errorMessage: string; public isRoomEncrypted: boolean; public hasStickerPacks = false; private roomId: string; private userId: string; private requestedScreen: string = null; private requestedIntegrationId: string = null; public integrationsForCategory: { [category: string]: FE_Integration[] } = {}; private categoryMap: { [categoryName: string]: string[] } = CATEGORY_MAP; constructor( private activatedRoute: ActivatedRoute, private scalarApi: ScalarServerApiService, private scalar: ScalarClientApiService, private integrationsApi: IntegrationsApiService, private stickerApi: StickerApiService, private adminApi: AdminApiService, private router: Router, private modal: NgbModal, private toaster: ToasterService, public translate: TranslateService ) { this.translate = translate; const params: any = this.activatedRoute.snapshot.queryParams; this.requestedScreen = params.screen; this.requestedIntegrationId = params.integ_id; if (SessionStorage.roomId && SessionStorage.userId) { this.roomId = SessionStorage.roomId; this.userId = SessionStorage.userId; console.log( "Already checked scalar token and other params - continuing startup" ); this.prepareIntegrations(); return; } if (!params.scalar_token || !params.room_id) { console.error( "Unable to load Dimension. Missing room ID or scalar token." ); this.isError = true; this.isLoading = false; this.translate .get("Unable to load Dimension - missing room ID or token.") .subscribe((res: string) => { this.errorMessage = res; }); } else { this.roomId = params.room_id; SessionStorage.scalarToken = params.scalar_token; SessionStorage.roomId = this.roomId; this.scalarApi .getAccount() .then((response) => { const userId = response.user_id; SessionStorage.userId = userId; if (!userId) { console.error( "No user returned for token. Is the token registered in Dimension?" ); this.isError = true; this.isLoading = false; this.translate .get( "Could not verify your token. Please try logging out of Element and back in. Be sure to back up your encryption keys!" ) .subscribe((res: string) => { this.errorMessage = res; }); } else { this.userId = userId; console.log("Scalar token belongs to " + userId); this.checkAdmin(); this.prepareIntegrations(); } }) .catch((err) => { console.error(err); this.isError = true; this.isLoading = false; this.translate .get( "Unable to communicate with Dimension due to an unknown error." ) .subscribe((res: string) => { this.errorMessage = res; }); }); } } private checkAdmin() { this.adminApi .isAdmin() .then(() => { console.log( SessionStorage.userId + " is an admin for this Dimension instance" ); SessionStorage.isAdmin = true; }) .catch(() => (SessionStorage.isAdmin = false)); } public hasIntegrations(): boolean { for (const category of this.getCategories()) { if (this.getIntegrationsIn(category).length > 0) return true; } return false; } public getCategories(): string[] { return Object.keys(this.categoryMap); } public getIntegrationsIn(category: string): FE_Integration[] { return this.integrationsForCategory[category]; } private getIntegrations(): FE_Integration[] { const result: FE_Integration[] = []; for (const category of this.getCategories()) { for (const integration of this.getIntegrationsIn(category)) { result.push(integration); } } return result; } public modifyIntegration(integration: FE_Integration) { if (!integration._isSupported) { console.log( this.userId + " tried to modify " + integration.displayName + " with error: " + integration._notSupportedReason ); this.translate .get( "You do not appear to have permission to modify widgets in this room" ) .subscribe((res: string) => { const reason = integration.category === "widget" ? res : integration._notSupportedReason; this.toaster.pop("error", reason); }); return; } SessionStorage.editIntegration = integration; SessionStorage.editsRequested++; console.log( this.userId + " is trying to modify " + integration.displayName ); if (integration.category === "bot") { this.toggleBot(integration); } else { console.log( "Navigating to edit screen for " + integration.category + " " + integration.type ); this.router.navigate( ["riot-app", integration.category, integration.type], { queryParams: { roomId: this.roomId } } ); } } public toggleBot(bot: FE_SimpleBot) { const controller = new SimpleBotController(bot, this.scalar, this.integrationsApi, this.toaster, this.translate); controller.toggle(this.roomId); } private prepareIntegrations() { this.scalar .isRoomEncrypted(this.roomId) .then((payload) => { this.isRoomEncrypted = payload.response; return this.integrationsApi.getIntegrations(this.roomId); }) .then((response) => { const integrations: FE_Integration[] = _.flatten( Object.keys(response).map((k) => response[k]) ); const supportedIntegrations: FE_Integration[] = _.filter( integrations, (i) => IntegrationsRegistry.isSupported(i) ); // Flag integrations that aren't supported in encrypted rooms if (this.isRoomEncrypted) { for (const integration of supportedIntegrations) { if (!integration.isEncryptionSupported) { integration._isSupported = false; integration._notSupportedReason = "This integration is not supported in encrypted rooms"; } } } // Set up the categories for (const category of Object.keys(this.categoryMap)) { const supportedTypes = this.categoryMap[category]; this.integrationsForCategory[category] = _.filter( supportedIntegrations, (i) => supportedTypes.indexOf(i.category) !== -1 ); } const promises = supportedIntegrations.map((i) => this.updateIntegrationState(i) ); return Promise.all(promises); }) .then(() => { this.isLoading = false; // HACK: We wait for the digest cycle so we actually have components to look at setTimeout(() => this.tryOpenConfigScreen(), 20); }) .catch((err) => { console.error(err); this.isError = true; this.isLoading = false; this.translate .get( "Unable to set up Dimension. This version of Element may not supported or there may be a problem with the server." ) .subscribe((res: string) => { this.errorMessage = res; }); }); this.stickerApi .getPacks() .then((packs) => { this.hasStickerPacks = packs.length > 0; }) .catch((err) => { console.error(err); }); } private tryOpenConfigScreen() { let category = null; let type = null; if (!this.requestedScreen) return; if (this.requestedScreen === "type_m.stickerpicker") { console.log( "Intercepting config screen handling to open sticker picker config" ); this.router.navigate(["riot-app", "stickerpicker"]); return; } const targetIntegration = IntegrationsRegistry.getIntegrationForScreen( this.requestedScreen ); if (targetIntegration) { category = targetIntegration.category; type = targetIntegration.type; } else { console.log("Unknown screen requested: " + this.requestedScreen); } console.log("Searching for integration for requested screen"); for (const integration of this.getIntegrations()) { if (integration.category === category && integration.type === type) { console.log( "Configuring integration " + this.requestedIntegrationId + " category=" + category + " type=" + type ); SessionStorage.editIntegration = integration; SessionStorage.editIntegrationId = this.requestedIntegrationId; this.modifyIntegration(integration); return; } } console.log( "Failed to find integration component for category=" + category + " type=" + type ); } private async updateIntegrationState(integration: FE_Integration) { integration._isUpdating = false; if (!integration.isOnline) { integration._isSupported = false; this.translate .get("This integration is offline or unavailable") .subscribe((res: string) => { integration._notSupportedReason = res; }); return; } if (!integration.requirements) return; const promises = integration.requirements.map((r) => this.checkRequirement(r) ); if (integration.category === "bot") { const state = await this.scalar.getMembershipState( this.roomId, (integration).userId ); if (state && state.response && state.response.membership) { integration._inRoom = ["join", "invite"].indexOf(state.response.membership) !== -1; } else integration._inRoom = false; } return Promise.all(promises).then( () => { integration._isSupported = true; integration._notSupportedReason = null; }, (error) => { console.error(error); integration._isSupported = false; integration._notSupportedReason = error; } ); } private checkRequirement(requirement: FE_IntegrationRequirement) { switch (requirement.condition) { case "publicRoom": return this.scalar.getJoinRule(this.roomId).then((payload) => { if (!payload.response) { return new Promise((resolve, reject) => { this.translate .get("Could not communicate with Element") .subscribe((res: string) => reject(res)); }); } const isPublic = payload.response.join_rule === "public"; if (isPublic !== requirement.expectedValue) { return new Promise((resolve, reject) => { const keys = ["The room must be", "to use this integration"]; this.translate .get(keys) .subscribe((res: any) => { reject(res[keys[0]] + ' ' + (isPublic ? "non-public" : "public") + ' ' + res[keys[1]]); }); }); } else return Promise.resolve(); }); case "canSendEventTypes": const processPayload = (payload) => { const response = payload.response; if (response === true) return Promise.resolve(); if (response.error || response.error.message) { let message: string; this.translate .get("You cannot modify widgets in this room") .subscribe((res: string) => { message = res; }); return Promise.reject(message); } let message: string; this.translate .get("Error communicating with Element") .subscribe((res: string) => { message = res; }); return Promise.reject(message); }; let promiseChain = Promise.resolve(); requirement.argument.forEach( (e) => (promiseChain = promiseChain.then(() => this.scalar .canSendEvent(this.roomId, e.type, e.isState) .then(processPayload) .catch(processPayload) )) ); return promiseChain .then(() => { if (!requirement.expectedValue) { let message: string; this.translate .get("Expected to not be able to send specific event types") .subscribe((res: string) => { message = res; }); return Promise.reject(message); } }) .catch((err) => { console.error(err); if (requirement.expectedValue) { let message: string; this.translate .get("Expected to be able to send specific event types") .subscribe((res: string) => { message = res; }); return Promise.reject(message); } }); case "userInRoom": // TODO: Implement default: let message: string; let message1: string; this.translate .get(["Requirement", "not found"]) .subscribe((res: string) => { message = res[0]; message1 = res[1]; }); return Promise.reject(message + requirement.condition + message1); } } }