diff options
author | Maksim Sukharev <antreesy.web@gmail.com> | 2024-04-28 22:19:46 +0200 |
---|---|---|
committer | Maksim Sukharev <antreesy.web@gmail.com> | 2024-06-14 10:27:24 +0200 |
commit | 477a6d1a6a8885a0998c196ed93c5df78e1b68d2 (patch) | |
tree | 0498c0bb743fc59abaeaf580d5fc4830ef667544 /src | |
parent | 529393ee0bb7f5e6c122602aaa1d0dcc28f45a72 (diff) |
feat(capabilities): implement manager/wrapper for capabilities
- check talk-hash when joining federated conversation
- keep and retrieve capabilities from BrowserStorage
- distinguish cases for local and remote capabilities
- implement 'hasTalkFeature' and 'getTalkConfig' helpers
- respect local-only features and configs
Signed-off-by: Maksim Sukharev <antreesy.web@gmail.com>
Diffstat (limited to 'src')
-rw-r--r-- | src/services/CapabilitiesManager.ts | 90 | ||||
-rw-r--r-- | src/services/federationService.ts | 13 | ||||
-rw-r--r-- | src/store/participantsStore.js | 6 | ||||
-rw-r--r-- | src/types/index.ts | 15 |
4 files changed, 123 insertions, 1 deletions
diff --git a/src/services/CapabilitiesManager.ts b/src/services/CapabilitiesManager.ts new file mode 100644 index 000000000..bfec18c8b --- /dev/null +++ b/src/services/CapabilitiesManager.ts @@ -0,0 +1,90 @@ +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import { getCapabilities as _getCapabilities } from '@nextcloud/capabilities' +import { showError, TOAST_PERMANENT_TIMEOUT } from '@nextcloud/dialogs' + +import { getRemoteCapabilities } from './federationService.ts' +import BrowserStorage from '../services/BrowserStorage.js' +import type { Capabilities, JoinRoomFullResponse } from '../types' + +type Config = Capabilities['spreed']['config'] +type RemoteCapabilities = Record<string, Capabilities & Partial<{ hash: string }>> + +const localCapabilities: Capabilities = _getCapabilities() as Capabilities +const remoteCapabilities: RemoteCapabilities = restoreRemoteCapabilities() + +/** + * Check whether the feature is presented (in case of federation - on both servers) + * @param token conversation token + * @param feature feature capability in string format + */ +export function hasTalkFeature(token: string = 'local', feature: string): boolean { + const hasLocalTalkFeature = localCapabilities?.spreed?.features?.includes(feature) ?? false + if (localCapabilities?.spreed?.['features-local']?.includes(feature)) { + return hasLocalTalkFeature + } else if (token === 'local' || !remoteCapabilities[token]) { + return hasLocalTalkFeature + } else { + return hasLocalTalkFeature && (remoteCapabilities[token]?.spreed?.features?.includes(feature) ?? false) + } +} + +/** + * Get an according config value from local or remote capabilities + * @param token conversation token + * @param key1 top-level key (e.g. 'attachments') + * @param key2 second-level key (e.g. 'allowed') + */ +export function getTalkConfig(token: string = 'local', key1: keyof Config, key2: keyof Config[keyof Config]) { + if (localCapabilities?.spreed?.['config-local']?.[key1]?.[key2]) { + return localCapabilities?.spreed?.config?.[key1]?.[key2] + } else if (token === 'local' || !remoteCapabilities[token]) { + return localCapabilities?.spreed?.config?.[key1]?.[key2] + } else { + // TODO discuss handling remote config (respect remote only / both / minimal) + return remoteCapabilities[token]?.spreed?.config?.[key1]?.[key2] + } +} + +/** + * Compares talk hash from remote instance and fetch new capabilities if it doesn't match + * @param joinRoomResponse server response + */ +export async function setRemoteCapabilities(joinRoomResponse: JoinRoomFullResponse): Promise<void> { + const token = joinRoomResponse.data.ocs.data.token + + // Check if remote capabilities have not changed since last check + if (joinRoomResponse.headers['x-nextcloud-talk-proxy-hash'] === remoteCapabilities[token]?.hash) { + return + } + + const response = await getRemoteCapabilities(token) + if (Array.isArray(response.data.ocs.data)) { + // unknown[] received from server, nothing to update with + return + } + + remoteCapabilities[token] = { spreed: response.data.ocs.data } + remoteCapabilities[token].hash = joinRoomResponse.headers['x-nextcloud-talk-proxy-hash'] + BrowserStorage.setItem('remoteCapabilities', JSON.stringify(remoteCapabilities)) + + // As normal capabilities update, requires a reload to take effect + showError(t('spreed', 'Nextcloud Talk Federation was updated, please reload the page'), { + timeout: TOAST_PERMANENT_TIMEOUT, + }) +} + +/** + * Restores capabilities from BrowserStorage + */ +function restoreRemoteCapabilities(): RemoteCapabilities { + const remoteCapabilities = BrowserStorage.getItem('remoteCapabilities') + if (!remoteCapabilities?.length) { + return {} + } + + return JSON.parse(remoteCapabilities) as RemoteCapabilities +} diff --git a/src/services/federationService.ts b/src/services/federationService.ts index f59b5f2ba..ff76e6ffd 100644 --- a/src/services/federationService.ts +++ b/src/services/federationService.ts @@ -6,7 +6,7 @@ import axios from '@nextcloud/axios' import { generateOcsUrl } from '@nextcloud/router' -import type { acceptShareResponse, getSharesResponse, rejectShareResponse } from '../types' +import type { acceptShareResponse, getSharesResponse, rejectShareResponse, getCapabilitiesResponse } from '../types' /** * Fetches list of shares for a current user @@ -37,8 +37,19 @@ const rejectShare = async function(id: number, options?: object): rejectShareRes return axios.delete(generateOcsUrl('apps/spreed/api/v1/federation/invitation/{id}', { id }, options), options) } +/** + * Fetches capabilities of remote server by local conversation token + * + * @param token local conversation token; + * @param [options] options; + */ +const getRemoteCapabilities = async function(token: string, options?: object): getCapabilitiesResponse { + return axios.get(generateOcsUrl('apps/spreed/api/v4/room/{token}/capabilities', { token }, options), options) +} + export { getShares, acceptShare, rejectShare, + getRemoteCapabilities, } diff --git a/src/store/participantsStore.js b/src/store/participantsStore.js index de6117b15..e5ce56b7b 100644 --- a/src/store/participantsStore.js +++ b/src/store/participantsStore.js @@ -15,6 +15,7 @@ import { joinCall, leaveCall, } from '../services/callsService.js' +import { setRemoteCapabilities } from '../services/CapabilitiesManager.ts' import { EventBus } from '../services/EventBus.js' import { promoteToModerator, @@ -852,6 +853,11 @@ const actions = { sessionId: response.data.ocs.data.sessionId, }) + if (response.data.ocs.data.remoteServer) { + // fetch and store remote capabilities for federated conversation + await setRemoteCapabilities(response) + } + SessionStorage.setItem('joined_conversation', token) EventBus.emit('joined-conversation', { token }) return response diff --git a/src/types/index.ts b/src/types/index.ts index b82dbb8da..5b7f65eba 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -7,6 +7,16 @@ import type { components, operations } from './openapi/openapi-full.ts' // General type ApiOptions<T> = { params: T } type ApiResponse<T> = Promise<{ data: T }> +type ApiResponseHeaders<T extends { headers: object }> = { + [K in keyof T['headers'] as Lowercase<string & K>]: T['headers'][K]; +} + +// Capabilities +export type Capabilities = { + [key: string]: Record<string, unknown>, + spreed: components['schemas']['Capabilities'], +} +export type getCapabilitiesResponse = ApiResponse<operations['room-get-capabilities']['responses'][200]['content']['application/json']> // Notifications type NotificationAction = { @@ -40,6 +50,11 @@ export type Notification<T = Record<string, RichObject & Record<string, unknown> // Conversations export type Conversation = components['schemas']['Room'] +export type JoinRoomFullResponse = { + headers: ApiResponseHeaders<operations['room-join-room']['responses']['200']>, + data: operations['room-join-room']['responses']['200']['content']['application/json'] +} + // Participants export type Participant = components['schemas']['Participant'] |