diff --git a/lib/Matrix.js b/lib/Matrix.js index 9e35fed2..9d21a866 100644 --- a/lib/Matrix.js +++ b/lib/Matrix.js @@ -343,19 +343,27 @@ function useMatrixProvider(auth) { const createRoom = useCallback( async (name, isSpace, topic, joinRule, type, template, parentId) => { - const room = await authenticationProvider.createRoom(name, isSpace, topic, joinRule, type, template, parentId); + const room = await authenticationProvider.createRoom(name, isSpace, topic, joinRule, type, template, parentId) + .catch((error) => { + return handleRateLimit(error, () => createRoom(name, isSpace, topic, joinRule, type, template, parentId)) + .catch(error => { + return error.message; + }); + }); + // in case of recursion room will already return the desired roomId + const roomId = room.room_id || room; if (isSpace) { - if (!spaces.has(room.room_id)) { - setSpaces(setRoom(room.room_id)); + if (!spaces.has(roomId)) { + setSpaces(setRoom(roomId)); } } else { - if (!rooms.has(room.room_id)) { - setRooms(setRoom(room.room_id)); + if (!rooms.has(roomId)) { + setRooms(setRoom(roomId)); } } - return room.room_id; + return roomId; }, [authenticationProvider, rooms, setRoom, setRooms, setSpaces, spaces], ); @@ -375,14 +383,16 @@ function useMatrixProvider(auth) { setApplicationsFolder(existingApplicationsSpace.roomId); } else { logger.debug('Creating new root "Applications" space...'); - const newApplicationsSpace = await createRoom( + const createNewApplicationsSpace = async () => await createRoom( 'Applications', true, 'This is your private applications space. You can find all your application data in here.', 'invite', 'context', 'applications', - ); + ) + .catch(error => logger.debug(error)); + const newApplicationsSpace = await createNewApplicationsSpace(); logger.debug('Created new "Applications" space', { newApplicationsSpace }); setApplicationsFolder(newApplicationsSpace); } @@ -420,7 +430,7 @@ function useMatrixProvider(auth) { }); } else { logger.debug('Creating new service space...', { serviceType: element }); - const roomId = await createRoom( + const createNewServiceRoom = async () => await createRoom( element, true, `This is your private space for the application ${element}. You can find all your ${element} data in here.`, @@ -428,15 +438,24 @@ function useMatrixProvider(auth) { 'context', 'application', applicationsFolder, - ); - logger.debug('Created new service space', { serviceType: element, roomId }); - await authenticationProvider.addSpaceChild(applicationsFolder, roomId); + ).catch(error => logger.debug(error)); + + const roomId = await createNewServiceRoom(); + logger.debug('Created new service space...', { serviceType: element, roomId }); + + const addServiceRoomToParent = async () => await authenticationProvider.addSpaceChild(applicationsFolder, roomId) + .catch(error => { + return handleRateLimit(error, () => addServiceRoomToParent()) + .catch(error => logger.debug(error.message)); + }); + + await addServiceRoomToParent(); setServiceSpaces((object) => { object[element] = roomId; }); } } - }, 500); + }, 1000); applicationsFolder && _.isEmpty(serviceSpaces) && lookForServiceSpaces(); @@ -492,6 +511,61 @@ function useMatrixProvider(auth) { return metaEvent; }; + /** + * Handles retrying a function when encountering a rate limiting error (HTTP 429) from matrix. + * + * @param {Object} error - The error object, typically containing an HTTP status code and additional data. + * @param {Function} retryFunction - The function to retry after a delay in case of a rate limiting error. + * @returns {Promise<*>} - A Promise that resolves with the result of the retryFunction. + * + * @throws {Error} - Throws an error if `error` is falsy or does not have an `httpStatus` property. + */ + const handleRateLimit = async (error, retryFunction) => { + // Handle other errors + if (error.httpStatus !== 429) throw new Error(error.data.error || 'Something went wrong. Please try again.'); + // Handle rate limiting with retry_after_ms + const retryAfterMs = error.data['retry_after_ms'] || 5000; + logger.debug('Retry after (ms):', retryAfterMs); + + // Retry the function after the specified delay, defaults to 5000ms + await new Promise((resolve) => setTimeout(resolve, retryAfterMs)); + + return retryFunction(); + }; + + /** + * Adds a child to a parent space using the authentication provider. + * + * @param {string} parent - The ID or name of the parent space. + * @param {string} child - The ID or name of the child space or room to be added. + * @param {boolean} suggested - Indicates whether the addition is suggested (i.e. in element). + * @returns {Promise} A promise that resolves with a success message or rejects with an error message. + * @throws {Error} If the authentication provider encounters an error other than 429 during the operation. + */ + const addSpaceChild = async (parent, child, suggested) => { + await authenticationProvider.addSpaceChild(parent, child, suggested) + .catch((error) => { + return handleRateLimit(error, () => addSpaceChild(parent, child, suggested)); + }); + }; + + /** + * Sends a text message to a specified matrix room using the Matrix client. + * + * @param {string} roomId - The ID of the room where the message will be sent. + * @param {string} message - The text message to be sent. + * @returns {Promise} A promise that resolves with a success message + * @throws {Error} If there is an issue other than 429 sending the message or if a rate limit is encountered. + */ + const sendMessage = async (roomId, message) => { + await matrixClient.sendMessage(roomId, { + msgtype: 'm.text', + body: message, + }).catch((error) => { + return handleRateLimit(error, () => sendMessage(roomId, message)); + }); + }; + return { rooms, spaces, @@ -507,6 +581,9 @@ function useMatrixProvider(auth) { hydrateRoomContent, getMetaEvent, isConnectedToServer, + handleRateLimit, + addSpaceChild, + sendMessage, }; } diff --git a/lib/auth/MatrixAuthProvider.js b/lib/auth/MatrixAuthProvider.js index 83fa9258..d0fe050c 100644 --- a/lib/auth/MatrixAuthProvider.js +++ b/lib/auth/MatrixAuthProvider.js @@ -137,7 +137,14 @@ class MatrixAuthProvider { type: 'm.room.join_rules', content: { join_rule: joinRule }, // can be set to either public, invite or knock }, - ], + { + type: 'dev.medienhaus.meta', + content: { + type: type, + template: template, + version: '0.4', + }, + }], power_level_content_override: { // we only want users with moderation rights to be able to do any actions, people joining the room will have a default level of 0. ban: 50, @@ -171,12 +178,6 @@ class MatrixAuthProvider { } const room = await this.matrixClient.createRoom(opts); - const medienhausMetaEvent = { - type: type, - template: template, - version: '0.4', - }; - await this.matrixClient.sendStateEvent(room.room_id, 'dev.medienhaus.meta', medienhausMetaEvent); return room; } diff --git a/pages/etherpad/[[...roomId]].js b/pages/etherpad/[[...roomId]].js index e5ac8cb8..dc07adda 100644 --- a/pages/etherpad/[[...roomId]].js +++ b/pages/etherpad/[[...roomId]].js @@ -64,11 +64,11 @@ export default function Etherpad() { const auth = useAuth(); const matrix = useMatrix(); - const matrixClient = auth.getAuthenticationProvider('matrix').getMatrixClient(); const etherpad = auth.getAuthenticationProvider('etherpad'); const [serverPads, setServerPads] = useState(null); const [isDeletingPad, setIsDeletingPad] = useState(false); + const [errorMessage, setErrorMessage] = useState(''); /** * A roomId is set when the route is /etherpad/, otherwise it's undefined @@ -137,17 +137,24 @@ export default function Etherpad() { if (!link || !name) return; logger.debug('Creating new Matrix room for pad', { link, name }); + const room = await matrix + .createRoom(name, false, '', 'invite', 'content', 'etherpad', matrix.serviceSpaces.etherpad) + .catch((error) => setErrorMessage(error.message)); - const room = await matrix.createRoom(name, false, '', 'invite', 'content', 'etherpad', matrix.serviceSpaces.etherpad); - await auth.getAuthenticationProvider('matrix').addSpaceChild(matrix.serviceSpaces.etherpad, room); - await matrixClient.sendMessage(room, { - msgtype: 'm.text', - body: link, - }); + await matrix.addSpaceChild(matrix.serviceSpaces.etherpad, room).catch((error) => setErrorMessage(error.message)); + + await matrix.sendMessage(room, link).catch((error) => setErrorMessage(error.message)); + + if (getConfig().publicRuntimeConfig.authProviders.etherpad.myPads?.api) { + await etherpad.syncAllPads(); + setServerPads(etherpad.getAllPads()); + } + + setErrorMessage(''); return room; }, - [auth, matrix, matrixClient], + [auth, matrix, etherpad], ); /** @@ -270,6 +277,7 @@ export default function Etherpad() { const listEntries = useMemo(() => { return matrix.spaces.get(matrix.serviceSpaces.etherpad)?.children?.map((writeRoomId) => { + if (!matrix.roomContents?.get(writeRoomId)) return; const name = _.get(matrix.rooms.get(writeRoomId), 'name'); const etherpadId = matrix.roomContents .get(writeRoomId) @@ -295,7 +303,7 @@ export default function Etherpad() { }, [matrix.roomContents, matrix.rooms, matrix.serviceSpaces.etherpad, matrix.spaces, roomId, serverPads]); // Add the following parameters to the iframe URL: - // - user's Matrix displayname as parameter so that it shows up in Etherpad as username + // - user's Matrix display name as parameter so that it shows up in Etherpad as username // - user's MyPads auth token so that we skip having to enter a password for password protected pads owned by user let iframeUrl; @@ -335,6 +343,7 @@ export default function Etherpad() { {listEntries} + {errorMessage && {t(errorMessage)}} )} diff --git a/pages/spacedeck/[[...roomId]].js b/pages/spacedeck/[[...roomId]].js index 40506b6c..2ea7e87b 100644 --- a/pages/spacedeck/[[...roomId]].js +++ b/pages/spacedeck/[[...roomId]].js @@ -43,7 +43,7 @@ export default function Spacedeck() { const spacedeck = auth.getAuthenticationProvider('spacedeck'); // Whenever the roomId changes (e.g. after a new sketch was created), automatically focus that element. - // This makes the sidebar scroll to the element if it is outside of the current viewport. + // This makes the sidebar scroll to the element if it is outside the current viewport. const selectedSketchRef = useRef(null); useEffect(() => { selectedSketchRef.current?.focus(); @@ -150,25 +150,23 @@ export default function Spacedeck() { }, [spacedeck]); async function createSketchRoom(link, name, parent = serviceSpaceId) { - // eslint-disable-next-line no-undef - logger.debug('creating room for ' + name); - const room = await matrix.createRoom(name, false, '', 'invite', 'content', 'spacedeck', parent).catch(() => { - setErrorMessage(t('Something went wrong when trying to create a new room')); - }); - await auth - .getAuthenticationProvider('matrix') - .addSpaceChild(parent, room) - .catch(() => { - setErrorMessage(t("Couldn't add the new room to your sketch folder")); - }); - await matrixClient - .sendMessage(room, { - msgtype: 'm.text', - body: link, - }) - .catch(() => { - setErrorMessage(t('Something went wrong when trying to save the new sketch link')); - }); + // Create the room with retry handling + const room = await matrix.createRoom(name, false, '', 'invite', 'content', 'spacedeck', parent) + .catch(error => setErrorMessage(error)); + + // Log debug information about current progress + logger.debug(`Created room for ${name} with id ${room}`); + + // Add the room as a child to the parent space + await matrix.addSpaceChild(parent, room) + .catch(error => setErrorMessage(error)); + + // Log debug information about current progress + logger.debug('Added %s to parent %s', name, parent); + + // Send the message to the room with retry handling + await matrix.sendMessage(room, link) + .catch(error => setErrorMessage(error)); return room; } diff --git a/public/locales/de/_defaults.json b/public/locales/de/_defaults.json index 9b9f21ff..d6ddfb13 100644 --- a/public/locales/de/_defaults.json +++ b/public/locales/de/_defaults.json @@ -1,25 +1,27 @@ { - "Cancel": "Abbrechen", - "Confirm": "Bestätigen", - "Copy link to clipboard": "Link kopieren", - "Connection lost": "Verbindung unterbrochen", - "Delete": "Löschen", - "Make sure your link includes \"{{url}}\"": "Dein Link muss \"{{url}}\" beinhalten", - "Username": "Benutzername", - "Name": "Bezeichnung", - "Password": "Passwort", - "password protected": "Passwortgeschützt", - "Upload": "Hochladen", - "Save": "Speichern", - "Select action": "Aktion wählen", - "What would you like to do?": "Was möchtest du tun?", - "{{error}}, please wait {{time}} seconds.": "{{error}}, bitte warte {{time}} Sekunden.", - "Invitations": "Einladungen", - "From": "Von", - "Accept": "Annehmen", - "Decline": "Ablehnen", - "The session for {{service}} has expired.": "Die Sitzung für {{service}} ist abgelaufen.", - "Please enter your account password to continue using {{service}}:": "Bitte gib dein Passwort ein, um {{service}} wieder nutzen zu können:", - "Something went wrong! Please try again.": "Da ist etwas schief gelaufen! Bitte versuche es erneut.", - "loading...": "lädt..." + "Accept": "Annehmen", + "Cancel": "Abbrechen", + "Confirm": "Bestätigen", + "Copy link to clipboard": "Link kopieren", + "Connection lost": "Verbindung unterbrochen", + "Decline": "Ablehnen", + "Delete": "Löschen", + "From": "Von", + "Invitations": "Einladungen", + "loading...": "lädt...", + "Make sure your link includes \"{{url}}\"": "Dein Link muss \"{{url}}\" beinhalten", + "Name": "Bezeichnung", + "Password": "Passwort", + "password protected": "Passwortgeschützt", + "Please enter your account password to continue using {{service}}:": "Bitte gib dein Passwort ein, um {{service}} wieder nutzen zu können:", + "Save": "Speichern", + "Select action": "Aktion wählen", + "Something went wrong! Please try again.": "Da ist etwas schief gelaufen! Bitte versuche es erneut.", + "Syncing more entries": "Weitere Einträge werden geladen", + "The session for {{service}} has expired.": "Die Sitzung für {{service}} ist abgelaufen.", + "Upload": "Hochladen", + "Username": "Benutzername", + "What would you like to do?": "Was möchtest Du tun?", + "{{error}}, please wait {{time}} seconds.": "{{error}}, bitte warte {{time}} Sekunden." + }