mirror of
https://gitcode.com/GitHub_Trending/ji/jitsi-meet.git
synced 2025-12-30 03:12:29 +00:00
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:
@@ -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
|
||||
|
||||
@@ -98,6 +98,7 @@ export interface IJitsiConference {
|
||||
end: Function;
|
||||
getBreakoutRooms: Function;
|
||||
getConnection: Function;
|
||||
getFileSharing: Function;
|
||||
getLocalParticipantProperty: Function;
|
||||
getLocalTracks: Function;
|
||||
getMeetingUniqueId: Function;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
204
resources/prosody-plugins/mod_filesharing_component.lua
Normal file
204
resources/prosody-plugins/mod_filesharing_component.lua
Normal 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);
|
||||
Reference in New Issue
Block a user