// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. import EventEmitter from 'events'; import {Server, ConfigServer, ConfigView} from 'types/config'; import {RemoteInfo} from 'types/server'; import Config from 'common/config'; import { SERVERS_URL_MODIFIED, SERVERS_UPDATE, } from 'common/communication'; import {Logger, getLevel} from 'common/log'; import {MattermostServer} from 'common/servers/MattermostServer'; import {TAB_FOCALBOARD, TAB_MESSAGING, TAB_PLAYBOOKS, MattermostView, getDefaultViews} from 'common/views/View'; import MessagingView from 'common/views/MessagingView'; import FocalboardView from 'common/views/FocalboardView'; import PlaybooksView from 'common/views/PlaybooksView'; import {isInternalURL, parseURL} from 'common/utils/url'; import Utils from 'common/utils/util'; const log = new Logger('ServerManager'); export class ServerManager extends EventEmitter { private servers: Map; private remoteInfo: Map; private serverOrder: string[]; private currentServerId?: string; private views: Map; private viewOrder: Map; private lastActiveView: Map; constructor() { super(); this.servers = new Map(); this.remoteInfo = new Map(); this.serverOrder = []; this.views = new Map(); this.viewOrder = new Map(); this.lastActiveView = new Map(); } getOrderedTabsForServer = (serverId: string) => { log.withPrefix(serverId).debug('getOrderedTabsForServer'); const viewOrder = this.viewOrder.get(serverId); if (!viewOrder) { return []; } return viewOrder.reduce((views, viewId) => { const view = this.views.get(viewId); if (view) { views.push(view); } return views; }, [] as MattermostView[]); } getOrderedServers = () => { log.debug('getOrderedServers'); return this.serverOrder.reduce((servers, srv) => { const server = this.servers.get(srv); if (server) { servers.push(server); } return servers; }, [] as MattermostServer[]); } getCurrentServer = () => { log.debug('getCurrentServer'); if (!this.currentServerId) { throw new Error('No server set as current'); } const server = this.servers.get(this.currentServerId); if (!server) { throw new Error('Current server does not exist'); } return server; } getLastActiveTabForServer = (serverId: string) => { log.withPrefix(serverId).debug('getLastActiveTabForServer'); const lastActiveView = this.lastActiveView.get(serverId); if (lastActiveView) { const view = this.views.get(lastActiveView); if (view && view?.isOpen) { return view; } } return this.getFirstOpenViewForServer(serverId); } getServer = (id: string) => { return this.servers.get(id); } getView = (id: string) => { return this.views.get(id); } getAllServers = () => { return [...this.servers.values()]; } hasServers = () => { return Boolean(this.servers.size); } getRemoteInfo = (serverId: string) => { return this.remoteInfo.get(serverId); } updateRemoteInfos = (remoteInfos: Map) => { let hasUpdates = false; remoteInfos.forEach((remoteInfo, serverId) => { this.remoteInfo.set(serverId, remoteInfo); hasUpdates = this.updateServerURL(serverId) || hasUpdates; hasUpdates = this.openExtraViews(serverId) || hasUpdates; }); if (hasUpdates) { this.persistServers(); } } lookupViewByURL = (inputURL: URL | string, ignoreScheme = false) => { log.silly('lookupViewByURL', `${inputURL}`, ignoreScheme); const parsedURL = parseURL(inputURL); if (!parsedURL) { return undefined; } const server = this.getAllServers().find((server) => { return isInternalURL(parsedURL, server.url, ignoreScheme) && parsedURL.pathname.match(new RegExp(`^${server.url.pathname}(.+)?(/(.+))?$`)); }); if (!server) { return undefined; } const views = this.getOrderedTabsForServer(server.id); let selectedView = views.find((view) => view && view.type === TAB_MESSAGING); views. filter((view) => view && view.type !== TAB_MESSAGING). forEach((view) => { if (parsedURL.pathname.match(new RegExp(`^${view.url.pathname}(/(.+))?`))) { selectedView = view; } }); return selectedView; } updateServerOrder = (serverOrder: string[]) => { log.debug('updateServerOrder', serverOrder); this.serverOrder = serverOrder; this.persistServers(); } updateTabOrder = (serverId: string, viewOrder: string[]) => { log.withPrefix(serverId).debug('updateTabOrder', viewOrder); this.viewOrder.set(serverId, viewOrder); this.persistServers(); } addServer = (server: Server) => { const newServer = new MattermostServer(server, false); if (this.servers.has(newServer.id)) { throw new Error('ID Collision detected. Cannot add server.'); } this.servers.set(newServer.id, newServer); this.serverOrder.push(newServer.id); const viewOrder: string[] = []; getDefaultViews().forEach((view) => { const newView = this.getNewView(newServer, view.name, view.isOpen); this.views.set(newView.id, newView); viewOrder.push(newView.id); }); this.viewOrder.set(newServer.id, viewOrder); if (!this.currentServerId) { this.currentServerId = newServer.id; } // Emit this event whenever we update a server URL to ensure remote info is fetched this.emit(SERVERS_URL_MODIFIED, [newServer.id]); this.persistServers(); return newServer; } editServer = (serverId: string, server: Server) => { const existingServer = this.servers.get(serverId); if (!existingServer) { return; } let urlModified; if (existingServer.url.toString() !== parseURL(server.url)?.toString()) { // Emit this event whenever we update a server URL to ensure remote info is fetched urlModified = () => this.emit(SERVERS_URL_MODIFIED, [serverId]); } existingServer.name = server.name; existingServer.updateURL(server.url); this.servers.set(serverId, existingServer); this.viewOrder.get(serverId)?.forEach((viewId) => { const view = this.views.get(viewId); if (view) { view.server = existingServer; this.views.set(viewId, view); } }); urlModified?.(); this.persistServers(); } removeServer = (serverId: string) => { this.viewOrder.get(serverId)?.forEach((viewId) => this.views.delete(viewId)); this.viewOrder.delete(serverId); this.lastActiveView.delete(serverId); const index = this.serverOrder.findIndex((id) => id === serverId); this.serverOrder.splice(index, 1); this.remoteInfo.delete(serverId); this.servers.delete(serverId); if (this.currentServerId === serverId && this.hasServers()) { this.currentServerId = this.serverOrder[0]; } this.persistServers(); } setViewIsOpen = (viewId: string, isOpen: boolean) => { const view = this.views.get(viewId); if (!view) { return; } view.isOpen = isOpen; this.persistServers(); } updateLastActive = (viewId: string) => { const view = this.views.get(viewId); if (!view) { return; } this.lastActiveView.set(view.server.id, viewId); this.currentServerId = view.server.id; const serverOrder = this.serverOrder.findIndex((srv) => srv === view.server.id); if (serverOrder < 0) { throw new Error('Server order corrupt, ID not found.'); } this.persistServers(serverOrder); } reloadFromConfig = () => { const serverOrder: string[] = []; Config.predefinedServers.forEach((server) => { const id = this.initServer(server, true); serverOrder.push(id); }); if (Config.enableServerManagement) { Config.localServers.sort((a, b) => a.order - b.order).forEach((server) => { const id = this.initServer(server, false); serverOrder.push(id); }); } this.filterOutDuplicateServers(); this.serverOrder = serverOrder; if (Config.lastActiveServer && this.serverOrder[Config.lastActiveServer]) { this.currentServerId = this.serverOrder[Config.lastActiveServer]; } else { this.currentServerId = this.serverOrder[0]; } } private filterOutDuplicateServers = () => { const servers = [...this.servers.keys()].map((key) => ({key, value: this.servers.get(key)!})); const uniqueServers = new Set(); servers.forEach((server) => { if (uniqueServers.has(`${server.value.name}:${server.value.url}`)) { this.servers.delete(server.key); } else { uniqueServers.add(`${server.value.name}:${server.value.url}`); } }); } private initServer = (configServer: ConfigServer, isPredefined: boolean) => { const server = new MattermostServer(configServer, isPredefined); this.servers.set(server.id, server); log.withPrefix(server.id).debug('initialized server'); const viewOrder: string[] = []; configServer.tabs.sort((a, b) => a.order - b.order).forEach((view) => { const mattermostView = this.getNewView(server, view.name, view.isOpen); log.withPrefix(mattermostView.id).debug('initialized view'); this.views.set(mattermostView.id, mattermostView); viewOrder.push(mattermostView.id); }); this.viewOrder.set(server.id, viewOrder); if (typeof configServer.lastActiveTab !== 'undefined') { this.lastActiveView.set(server.id, viewOrder[configServer.lastActiveTab]); } return server.id; } private getFirstOpenViewForServer = (serverId: string) => { const viewOrder = this.getOrderedTabsForServer(serverId); const openViews = viewOrder.filter((view) => view.isOpen); const firstView = openViews[0]; if (!firstView) { throw new Error(`No views open for server id ${serverId}`); } return firstView; } private persistServers = async (lastActiveServer?: number) => { this.emit(SERVERS_UPDATE); const localServers = [...this.servers.values()]. reduce((servers, srv) => { if (srv.isPredefined) { return servers; } servers.push(this.toConfigServer(srv)); return servers; }, [] as ConfigServer[]); await Config.setServers(localServers, lastActiveServer); } private getLastActiveView = (serverId: string) => { let lastActiveView: number | undefined; if (this.lastActiveView.has(serverId)) { const index = this.viewOrder.get(serverId)?.indexOf(this.lastActiveView.get(serverId)!); if (typeof index !== 'undefined' && index >= 0) { lastActiveView = index; } } return lastActiveView; } private toConfigServer = (server: MattermostServer): ConfigServer => { return { name: server.name, url: `${server.url}`, order: this.serverOrder.indexOf(server.id), lastActiveTab: this.getLastActiveView(server.id), tabs: this.viewOrder.get(server.id)?.reduce((views, viewId, index) => { const view = this.views.get(viewId); if (!view) { return views; } views.push({ name: view?.type, order: index, isOpen: view.isOpen, }); return views; }, [] as ConfigView[]) ?? [], }; } private getNewView = (srv: MattermostServer, viewName: string, isOpen?: boolean) => { switch (viewName) { case TAB_MESSAGING: return new MessagingView(srv, isOpen); case TAB_FOCALBOARD: return new FocalboardView(srv, isOpen); case TAB_PLAYBOOKS: return new PlaybooksView(srv, isOpen); default: throw new Error('Not implemeneted'); } } private updateServerURL = (serverId: string) => { const server = this.servers.get(serverId); const remoteInfo = this.remoteInfo.get(serverId); if (!(server && remoteInfo)) { return false; } if (remoteInfo.siteURL && server.url.toString() !== new URL(remoteInfo.siteURL).toString()) { server.updateURL(remoteInfo.siteURL); this.servers.set(serverId, server); return true; } return false; } private openExtraViews = (serverId: string) => { const server = this.servers.get(serverId); const remoteInfo = this.remoteInfo.get(serverId); if (!(server && remoteInfo)) { return false; } if (!(remoteInfo.serverVersion && Utils.isVersionGreaterThanOrEqualTo(remoteInfo.serverVersion, '6.0.0'))) { return false; } let hasUpdates = false; const viewOrder = this.viewOrder.get(serverId); if (viewOrder) { viewOrder.forEach((viewId) => { const view = this.views.get(viewId); if (view) { if (view.type === TAB_PLAYBOOKS && remoteInfo.hasPlaybooks && typeof view.isOpen === 'undefined') { log.withPrefix(view.id).verbose('opening Playbooks'); view.isOpen = true; this.views.set(viewId, view); hasUpdates = true; } if (view.type === TAB_FOCALBOARD && remoteInfo.hasFocalboard && typeof view.isOpen === 'undefined') { log.withPrefix(view.id).verbose('opening Boards'); view.isOpen = true; this.views.set(viewId, view); hasUpdates = true; } } }); } return hasUpdates; } private includeId = (id: string, ...prefixes: string[]) => { const shouldInclude = ['debug', 'silly'].includes(getLevel()); return shouldInclude ? [id, ...prefixes] : prefixes; }; getServerLog = (serverId: string, ...additionalPrefixes: string[]) => { const server = this.getServer(serverId); if (!server) { return new Logger(serverId); } return new Logger(...additionalPrefixes, ...this.includeId(serverId, server.name)); }; getViewLog = (viewId: string, ...additionalPrefixes: string[]) => { const view = this.getView(viewId); if (!view) { return new Logger(viewId); } return new Logger(...additionalPrefixes, ...this.includeId(viewId, view.server.name, view.type)); }; } const serverManager = new ServerManager(); export default serverManager;