summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorMaksim Sukharev <antreesy.web@gmail.com>2024-04-28 22:19:46 +0200
committerMaksim Sukharev <antreesy.web@gmail.com>2024-06-14 10:27:24 +0200
commit477a6d1a6a8885a0998c196ed93c5df78e1b68d2 (patch)
tree0498c0bb743fc59abaeaf580d5fc4830ef667544 /src
parent529393ee0bb7f5e6c122602aaa1d0dcc28f45a72 (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.ts90
-rw-r--r--src/services/federationService.ts13
-rw-r--r--src/store/participantsStore.js6
-rw-r--r--src/types/index.ts15
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']