// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. import {BehaviorSubject, combineLatest, of as of$} from 'rxjs'; import {switchMap, distinctUntilChanged} from 'rxjs/operators'; import DatabaseManager from '@database/manager'; import {getChannelById} from '@queries/servers/channel'; import {getConfig, getCurrentChannelId, getCurrentTeamId, getCurrentUserId, observeConfigBooleanValue} from '@queries/servers/system'; import {validateBindings} from '@utils/apps'; import {logDebug} from '@utils/log'; import NetworkManager from './network_manager'; const emptyBindings: AppBinding[] = []; const normalizeBindings = (bindings: AppBinding[]) => bindings.reduce((acc, v) => (v.bindings ? acc.concat(v.bindings) : acc), []); class AppsManager { private enabled: {[serverUrl: string]: BehaviorSubject} = {}; private bindings: {[serverUrl: string]: BehaviorSubject} = {}; private threadBindings: {[serverUrl: string]: BehaviorSubject<{channelId: string; bindings: AppBinding[]}>} = {}; private commandForms: {[serverUrl: string]: {[location: string]: AppForm}} = {}; private threadCommandForms: {[serverUrl: string]: {[location: string]: AppForm}} = {}; private getEnabledSubject = (serverUrl: string) => { if (!this.enabled[serverUrl]) { this.enabled[serverUrl] = new BehaviorSubject(true); } return this.enabled[serverUrl]; }; private getBindingsSubject = (serverUrl: string) => { if (!this.bindings[serverUrl]) { this.bindings[serverUrl] = new BehaviorSubject([]); } return this.bindings[serverUrl]; }; private getThreadsBindingsSubject = (serverUrl: string) => { if (!this.threadBindings[serverUrl]) { this.threadBindings[serverUrl] = new BehaviorSubject({channelId: '', bindings: emptyBindings}); } return this.threadBindings[serverUrl]; }; private handleError = (serverUrl: string) => { const enabled = this.getEnabledSubject(serverUrl); if (enabled.value) { enabled.next(false); } this.getBindingsSubject(serverUrl).next(emptyBindings); this.getThreadsBindingsSubject(serverUrl).next({channelId: '', bindings: emptyBindings}); this.commandForms[serverUrl] = {}; this.threadCommandForms[serverUrl] = {}; }; removeServer = (serverUrl: string) => { delete (this.enabled[serverUrl]); delete (this.bindings[serverUrl]); delete (this.threadBindings[serverUrl]); delete (this.commandForms[serverUrl]); delete (this.threadCommandForms[serverUrl]); }; clearServer = (serverUrl: string) => { this.clearBindings(serverUrl); this.clearBindings(serverUrl, true); this.commandForms[serverUrl] = {}; this.threadCommandForms[serverUrl] = {}; }; isAppsEnabled = async (serverUrl: string) => { try { const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl); const config = await getConfig(database); return this.getEnabledSubject(serverUrl).value && config?.FeatureFlagAppsEnabled === 'true'; } catch { return false; } }; observeIsAppsEnabled = (serverUrl: string) => { try { const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl); const enabled = this.getEnabledSubject(serverUrl).asObservable(); const config = observeConfigBooleanValue(database, 'FeatureFlagAppsEnabled'); return combineLatest([enabled, config]).pipe( switchMap(([e, cfg]) => of$(e && cfg)), distinctUntilChanged(), ); } catch { return of$(false); } }; fetchBindings = async (serverUrl: string, channelId: string, forThread = false) => { try { const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl); const userId = await getCurrentUserId(database); const channel = await getChannelById(database, channelId); let teamId = channel?.teamId; if (!teamId) { teamId = await getCurrentTeamId(database); } const client = NetworkManager.getClient(serverUrl); const fetchedBindings = await client.getAppsBindings(userId, channelId, teamId); const validatedBindings = validateBindings(fetchedBindings); const bindingsToStore = validatedBindings.length ? validatedBindings : emptyBindings; const enabled = this.getEnabledSubject(serverUrl); if (!enabled.value) { enabled.next(true); } if (forThread) { this.getThreadsBindingsSubject(serverUrl).next({channelId, bindings: bindingsToStore}); this.threadCommandForms[serverUrl] = {}; } else { this.getBindingsSubject(serverUrl).next(bindingsToStore); this.commandForms[serverUrl] = {}; } } catch (error) { logDebug('Error fetching apps', error); this.handleError(serverUrl); } }; refreshAppBindings = async (serverUrl: string) => { try { const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl); const appsEnabled = (await getConfig(database))?.FeatureFlagAppsEnabled === 'true'; if (!appsEnabled) { this.getEnabledSubject(serverUrl).next(false); this.clearServer(serverUrl); } const channelId = await getCurrentChannelId(database); // We await here, since errors on this call may clear the thread bindings await this.fetchBindings(serverUrl, channelId); const threadChannelId = this.getThreadsBindingsSubject(serverUrl).value.channelId; if (threadChannelId) { await this.fetchBindings(serverUrl, threadChannelId, true); } } catch (error) { logDebug('Error refreshing apps', error); this.handleError(serverUrl); } }; copyMainBindingsToThread = async (serverUrl: string, channelId: string) => { this.getThreadsBindingsSubject(serverUrl).next({channelId, bindings: this.getBindingsSubject(serverUrl).value}); }; clearBindings = async (serverUrl: string, forThread = false) => { if (forThread) { this.getThreadsBindingsSubject(serverUrl).next({channelId: '', bindings: emptyBindings}); } else { this.getBindingsSubject(serverUrl).next(emptyBindings); } }; observeBindings = (serverUrl: string, location?: string, forThread = false) => { const isEnabled = this.observeIsAppsEnabled(serverUrl); const bindings = forThread ? this.getThreadsBindingsSubject(serverUrl).asObservable().pipe(switchMap(({bindings: bb}) => of$(bb))) : this.getBindingsSubject(serverUrl).asObservable(); return combineLatest([isEnabled, bindings]).pipe( switchMap(([e, bb]) => of$(e ? bb : emptyBindings)), switchMap((bb) => { let result = location ? bb.filter((b) => b.location === location) : bb; result = normalizeBindings(result); return of$(result.length ? result : emptyBindings); }), ); }; getBindings = (serverUrl: string, location?: string, forThread = false) => { let bindings = forThread ? this.getThreadsBindingsSubject(serverUrl).value.bindings : this.getBindingsSubject(serverUrl).value; if (location) { bindings = bindings.filter((b) => b.location === location); } return normalizeBindings(bindings); }; getCommandForm = (serverUrl: string, key: string, forThread = false) => { return forThread ? this.threadCommandForms[serverUrl]?.[key] : this.commandForms[serverUrl]?.[key]; }; setCommandForm = (serverUrl: string, key: string, form: AppForm, forThread = false) => { const toStore = forThread ? this.threadCommandForms : this.commandForms; if (!toStore[serverUrl]) { toStore[serverUrl] = {}; } toStore[serverUrl][key] = form; }; } export default new AppsManager();