feat(file-sharing): Uses a filesharing management from ljm.

squash: Change the way we pass the list of files from the backend.

squash: Remove actions to sync local state with remote.
This commit is contained in:
damencho
2025-06-13 15:12:48 -05:00
committed by Дамян Минков
parent b296776ed7
commit e5187de9c3
8 changed files with 288 additions and 84 deletions

View File

@@ -148,6 +148,9 @@ Component "endconference.jitmeet.example.com" "end_conference"
Component "avmoderation.jitmeet.example.com" "av_moderation_component"
muc_component = "conference.jitmeet.example.com"
Component "filesharing.jitmeet.example.com" "filesharing_component"
muc_component = "conference.jitmeet.example.com"
Component "lobby.jitmeet.example.com" "muc"
storage = "memory"
restrict_room_creation = true

View File

@@ -98,6 +98,7 @@ export interface IJitsiConference {
end: Function;
getBreakoutRooms: Function;
getConnection: Function;
getFileSharing: Function;
getLocalParticipantProperty: Function;
getLocalTracks: Function;
getMeetingUniqueId: Function;

View File

@@ -30,7 +30,27 @@ export const UPLOAD_FILES = 'UPLOAD_FILES';
export const ADD_FILE = 'ADD_FILE';
/**
* The type of redux action to remove a file from the state.
* The type of redux action to add files to the state.
*
* {
* type: _FILE_LIST_RECEIVED,
* files: Array<IFileMetadata>
* }
*/
export const _FILE_LIST_RECEIVED = '_FILE_LIST_RECEIVED';
/**
* The type of redux action to remove a file from the state as it was removed from the backend.
*
* {
* type: _FILE_REMOVED,
* fileId: string
* }
*/
export const _FILE_REMOVED = '_FILE_REMOVED';
/**
* The type of redux action to remove a file from the backend.
*
* {
* type: REMOVE_FILE,

View File

@@ -49,7 +49,7 @@ export function addFile(file: IFileMetadata) {
}
/**
* Remove a file.
* Remove a file from the backend.
*
* @param {string} fileId - The ID of the file to remove.
* @returns {Object}

View File

@@ -1,14 +1,3 @@
/**
* The key prefix for file sharing metadata in the conference.
*/
export const FILE_SHARING_PREFIX = 'files';
/**
* Used for requesting file sharing service short-term credentials.
*/
export const FILE_SHARING_SERVICE = 'file-sharing';
/**
* The max file size we accept for upload.
*/

View File

@@ -2,18 +2,47 @@ import { v4 as uuidv4 } from 'uuid';
import { IStore } from '../app/types';
import { getCurrentConference } from '../base/conference/functions';
import { JitsiConferenceEvents } from '../base/lib-jitsi-meet';
import { getLocalParticipant, getParticipantDisplayName } from '../base/participants/functions';
import MiddlewareRegistry from '../base/redux/MiddlewareRegistry';
import StateListenerRegistry from '../base/redux/StateListenerRegistry';
import { showErrorNotification } from '../notifications/actions';
import { NOTIFICATION_TIMEOUT_TYPE, NOTIFICATION_TYPE } from '../notifications/constants';
import { DOWNLOAD_FILE, REMOVE_FILE, UPLOAD_FILES } from './actionTypes';
import { DOWNLOAD_FILE, REMOVE_FILE, UPLOAD_FILES, _FILE_LIST_RECEIVED, _FILE_REMOVED } from './actionTypes';
import { addFile, removeFile, updateFileProgress } from './actions';
import { FILE_SHARING_PREFIX, FILE_SHARING_SERVICE } from './constants';
import { getFileExtension } from './functions.any';
import logger from './logger';
import { IFileMetadata } from './types';
/**
* Registers a change handler for state['features/base/conference'].conference to
* set the event listeners needed for the file sharing feature to operate.
*/
StateListenerRegistry.register(
state => state['features/base/conference'].conference,
(conference, { dispatch }, previousConference) => {
if (conference && !previousConference) {
conference.on(JitsiConferenceEvents.FILE_SHARING_FILE_ADDED, (file: IFileMetadata) => {
dispatch(addFile(file));
});
conference.on(JitsiConferenceEvents.FILE_SHARING_FILE_REMOVED, (fileId: string) => {
dispatch({
type: _FILE_REMOVED,
fileId
});
});
conference.on(JitsiConferenceEvents.FILE_SHARING_FILES_RECEIVED, (files: object) => {
dispatch({
type: _FILE_LIST_RECEIVED,
files
});
});
}
});
/**
* Middleware that handles file sharing actions.
*
@@ -26,7 +55,7 @@ MiddlewareRegistry.register(store => next => action => {
const state = store.getState();
const conference = getCurrentConference(state);
conference?.getShortTermCredentials(FILE_SHARING_SERVICE).then((token: string) => {
conference?.getShortTermCredentials(conference?.getFileSharing()?.getIdentityType()).then((token: string) => {
for (const file of action.files) {
uploadFile(file, store, token);
}
@@ -38,29 +67,30 @@ MiddlewareRegistry.register(store => next => action => {
case REMOVE_FILE: {
const state = store.getState();
const conference = getCurrentConference(state);
const { fileSharing } = state['features/base/config'];
const sessionId = conference?.getMeetingUniqueId();
let doDelete = false;
const { files } = state['features/file-sharing'];
const fileId = action.fileId;
const existingMetadata = files.get(fileId);
// First remove the file metadata so others won't attempt to download it anymore.
const metadataHandler = conference?.getMetadataHandler();
if (metadataHandler) {
const metadataId = `${FILE_SHARING_PREFIX}.${action.fileId}`;
const existingMetadata = metadataHandler.getMetadata()[metadataId] ?? {};
doDelete = (existingMetadata?.process ?? 100) === 100;
metadataHandler.setMetadata(metadataId, {});
}
if (!doDelete) {
// ignore remove a file till the file is actually uploaded
if (!conference || (existingMetadata?.progress ?? 100) !== 100) {
return next(action);
}
// First, remove the file metadata so others won't attempt to download it anymore.
conference.getFileSharing().removeFile(fileId);
// remove it from local state
store.dispatch({
type: _FILE_REMOVED,
fileId
});
const { fileSharing } = state['features/base/config'];
const sessionId = conference.getMeetingUniqueId();
// Now delete it from the server.
conference?.getShortTermCredentials(FILE_SHARING_SERVICE)
.then((token: string) => fetch(`${fileSharing!.apiUrl!}/sessions/${sessionId}/files/${action.fileId}`, {
conference.getShortTermCredentials(conference.getFileSharing().getIdentityType())
.then((token: string) => fetch(`${fileSharing!.apiUrl!}/sessions/${sessionId}/files/${fileId}`, {
method: 'DELETE',
headers: {
'Authorization': `Bearer ${token}`
@@ -84,7 +114,7 @@ MiddlewareRegistry.register(store => next => action => {
const conference = getCurrentConference(state);
const sessionId = conference?.getMeetingUniqueId();
conference?.getShortTermCredentials(FILE_SHARING_SERVICE)
conference?.getShortTermCredentials(conference?.getFileSharing()?.getIdentityType())
.then((token: string) => fetch(`${fileSharing!.apiUrl!}/sessions/${sessionId}/files/${action.fileId}`, {
method: 'GET',
headers: {
@@ -195,9 +225,9 @@ function uploadFile(file: File, store: IStore, token: string): void {
if (xhr.status >= 200 && xhr.status < 300) {
store.dispatch(updateFileProgress(fileId, 100));
const metadataHandler = conference?.getMetadataHandler();
const fileSharingHandler = conference?.getFileSharing();
metadataHandler?.setMetadata(`${FILE_SHARING_PREFIX}.${fileId}`, fileMetadata);
fileSharingHandler.addFile(fileMetadata);
} else {
handleError();
}

View File

@@ -1,14 +1,6 @@
import { isEqual } from 'lodash-es';
import { UPDATE_CONFERENCE_METADATA } from '../base/conference/actionTypes';
import ReducerRegistry from '../base/redux/ReducerRegistry';
import {
ADD_FILE,
REMOVE_FILE,
UPDATE_FILE_UPLOAD_PROGRESS
} from './actionTypes';
import { FILE_SHARING_PREFIX } from './constants';
import { ADD_FILE, UPDATE_FILE_UPLOAD_PROGRESS, _FILE_LIST_RECEIVED, _FILE_REMOVED } from './actionTypes';
import { IFileMetadata } from './types';
export interface IFileSharingState {
@@ -32,7 +24,7 @@ ReducerRegistry.register<IFileSharingState>('features/file-sharing',
};
}
case REMOVE_FILE: {
case _FILE_REMOVED: {
const newFiles = new Map(state.files);
newFiles.delete(action.fileId);
@@ -55,44 +47,9 @@ ReducerRegistry.register<IFileSharingState>('features/file-sharing',
};
}
case UPDATE_CONFERENCE_METADATA: {
const { metadata } = action;
if (!metadata) {
return state;
}
const files = new Map();
for (const [ key, value ] of Object.entries(metadata)) {
if (key.startsWith(FILE_SHARING_PREFIX)) {
const fileId = key.substring(FILE_SHARING_PREFIX.length + 1);
files.set(fileId, value);
}
}
if (files.size === 0) {
return state;
}
const newFiles: Map<string, IFileMetadata> = new Map(state.files);
for (const [ key, value ] of files) {
// Deleted files will not have fileId.
if (!value.fileId) {
newFiles.delete(key);
} else {
newFiles.set(key, value);
}
}
if (isEqual(newFiles, state.files)) {
return state;
}
case _FILE_LIST_RECEIVED: {
return {
files: newFiles
files: new Map(Object.entries(action.files))
};
}

View File

@@ -0,0 +1,204 @@
local json = require 'cjson.safe';
local jid = require 'util.jid';
local st = require 'util.stanza';
local util = module:require 'util';
local internal_room_jid_match_rewrite = util.internal_room_jid_match_rewrite;
local is_admin = util.is_admin;
local process_host_module = util.process_host_module;
local FILE_SHARING_IDENTITY_TYPE = 'file-sharing';
local JSON_TYPE_ADD_FILE = 'add';
local JSON_TYPE_REMOVE_FILE = 'remove';
local JSON_TYPE_LIST_FILES = 'list';
local NICK_NS = 'http://jabber.org/protocol/nick';
local muc_component_host = module:get_option_string('muc_component');
if muc_component_host == nil then
module:log('error', 'No muc_component specified. No muc to operate on!');
return;
end
local muc_domain_base = module:get_option_string("muc_mapper_domain_base");
if not muc_domain_base then
module:log("warn", "No 'muc_domain_base' option set, disabling file sharing component.");
return ;
end
-- receives messages from clients to the component sending file sharing commands for adding or removing files
function on_message(event)
local session, stanza = event.origin, event.stanza;
-- Check the type of the incoming stanza to avoid loops:
if stanza.attr.type == 'error' then
return; -- We do not want to reply to these, so leave.
end
if not session or not session.jitsi_web_query_room then
return false;
end
local message = stanza:get_child(FILE_SHARING_IDENTITY_TYPE, 'http://jitsi.org/jitmeet');
if not message then
return false;
end
-- get room name with tenant and find room
local room = get_room_by_name_and_subdomain(session.jitsi_web_query_room, session.jitsi_web_query_prefix);
if not room then
module:log('warn', 'No room found for %s/%s', session.jitsi_web_query_prefix, session.jitsi_web_query_room);
return false;
end
-- check that the participant sending the message is an occupant in the room
local from = stanza.attr.from;
local occupant = room:get_occupant_by_real_jid(from);
if not occupant then
module:log('warn', 'No occupant %s found for %s', from, room.jid);
return false;
end
if not is_feature_allowed(
'file-upload',
session.jitsi_meet_context_features,
room:get_affiliation(stanza.attr.from) == 'owner') then
session.send(st.error_reply(stanza, 'auth', 'forbidden'));
return true;
end
if message.attr.type == JSON_TYPE_ADD_FILE then
local msg_obj, error = json.decode(message:get_text());
if error then
module:log('error','Error decoding data error:%s %s', error, stanza);
return false;
end
if not msg_obj.fileId then
module:log('error', 'Error missing required field: %s', stanza);
return false;
end
-- make sure we overwrite data for sender so we avoid spoofing
msg_obj.authorParticipantId = jid.resource(occupant.nick);
msg_obj.authorParticipantJid = from;
local nick_element = occupant:get_presence():get_child('nick', NICK_NS);
if nick_element then
msg_obj.authorParticipantName = nick_element:get_text();
else
msg_obj.authorParticipantName = 'anonymous';
end
msg_obj.conferenceFullName = internal_room_jid_match_rewrite(room.jid);
if not room.jitsi_shared_files then
room.jitsi_shared_files = {};
end
room.jitsi_shared_files[msg_obj.fileId] = msg_obj;
local json_msg, error = json.encode({
type = FILE_SHARING_IDENTITY_TYPE,
event = JSON_TYPE_ADD_FILE,
file = msg_obj
});
if not json_msg then
module:log('error', 'skip sending add request room:%s error:%s', room.jid, error);
return false
end
local stanza = st.message({ from = module.host; }):tag('json-message', { xmlns = 'http://jitsi.org/jitmeet' })
:text(json_msg):up();
-- send add file to all occupants except jicofo and sender
for _, room_occupant in room:each_occupant() do
if not is_admin(room_occupant.bare_jid) and room_occupant.nick ~= occupant.nick then
local to_send = st.clone(stanza);
to_send.attr.to = room_occupant.jid;
module:send(to_send);
end
end
return true;
elseif message.attr.type == JSON_TYPE_REMOVE_FILE then
if not message.attr.fileId then
module:log('error', 'Error missing required field: %s', stanza);
return true;
end
room.jitsi_shared_files[message.attr.fileId] = nil;
local json_msg, error = json.encode({
type = FILE_SHARING_IDENTITY_TYPE,
event = JSON_TYPE_REMOVE_FILE,
fileId = message.attr.fileId
});
if not json_msg then
module:log('error', 'skip sending remove request room:%s error:%s', room.jid, error);
return false
end
local stanza = st.message({ from = module.host; }):tag('json-message', { xmlns = 'http://jitsi.org/jitmeet' })
:text(json_msg):up();
-- send remove file to all occupants except jicofo and sender
for _, room_occupant in room:each_occupant() do
if not is_admin(room_occupant.bare_jid) and room_occupant.nick ~= occupant.nick then
local to_send = st.clone(stanza);
to_send.attr.to = room_occupant.jid;
module:send(to_send);
end
end
return true;
else
-- return error.
return false;
end
end
-- handles new occupants to inform them about any file shared by other participants
function occupant_joined(event)
local room, occupant = event.room, event.occupant;
-- healthcheck rooms does not have shared files
if not room.jitsi_shared_files
or is_admin(occupant.bare_jid)
or not room.jitsi_shared_files
or next(room.jitsi_shared_files) == nil then
return;
end
-- send file list to the new occupant
local json_msg, error = json.encode({
type = FILE_SHARING_IDENTITY_TYPE,
event = JSON_TYPE_LIST_FILES,
files = room.jitsi_shared_files
});
local stanza = st.message({ from = module.host; to = occupant.jid; })
:tag('json-message', { xmlns = 'http://jitsi.org/jitmeet' })
:text(json_msg):up();
module:send(stanza);
end
process_host_module(muc_component_host, function(host_module, host)
module:log('info','Hook to muc events on %s', host);
host_module:hook('muc-occupant-joined', occupant_joined, -10); -- make sure it runs after allowners or similar
end);
-- we will receive messages from the clients
module:hook('message/host', on_message);
process_host_module(muc_domain_base, function(host_module, host)
module:context(host_module.host):fire_event('jitsi-add-identity', {
name = FILE_SHARING_IDENTITY_TYPE; host = module.host;
});
end);