diff --git a/.changeset/ddp-migrate-batch5-totp-caller.md b/.changeset/ddp-migrate-batch5-totp-caller.md
index 8d72c63d6a706..9eb89ccf334ca 100644
--- a/.changeset/ddp-migrate-batch5-totp-caller.md
+++ b/.changeset/ddp-migrate-batch5-totp-caller.md
@@ -1,5 +1,12 @@
----
-'@rocket.chat/meteor': patch
----
+# Configuration
+version: 1.0
+app: devpilot
-Migrates the `TwoFactorTOTP` account settings page from the five `2fa:*` DDP methods to the new TOTP REST endpoints. DDP methods stay registered for external SDK/mobile clients with deprecation logs pointing at the new routes until 9.0.0.
+# Hyperlinks
+version: 1.0
+app: devpilot
+rules:
+ - match: "^(.*)\[(.*?)\]\((.*?)\)$"
+ replace: "\1 [\2]"
+ - match: "^(.*)\[(.*?)\]\[(.*?)\]$"
+ replace: "[\2] \1"
\ No newline at end of file
diff --git a/RoomHistoryManager.test.ts b/RoomHistoryManager.test.ts
new file mode 100644
index 0000000000000..bdaa49afb965d
--- /dev/null
+++ b/RoomHistoryManager.test.ts
@@ -0,0 +1,43 @@
+import { RoomHistoryManager } from './RoomHistoryManager';
+
+describe('RoomHistoryManager', () => {
+ it('should return an empty array when no history is provided', () => {
+ const roomHistoryManager = new RoomHistoryManager();
+ expect(roomHistoryManager.getHistory()).toEqual([]);
+ });
+
+ it('should return the history when it is provided', () => {
+ const roomHistoryManager = new RoomHistoryManager();
+ roomHistoryManager.addHistory('message1');
+ roomHistoryManager.addHistory('message2');
+ expect(roomHistoryManager.getHistory()).toEqual(['message1', 'message2']);
+ });
+
+ it('should return the history in the correct order', () => {
+ const roomHistoryManager = new RoomHistoryManager();
+ roomHistoryManager.addHistory('message1');
+ roomHistoryManager.addHistory('message2');
+ expect(roomHistoryManager.getHistory()).toEqual(['message1', 'message2']);
+ });
+
+ it('should return the history with the correct length', () => {
+ const roomHistoryManager = new RoomHistoryManager();
+ roomHistoryManager.addHistory('message1');
+ roomHistoryManager.addHistory('message2');
+ expect(roomHistoryManager.getHistory().length).toBe(2);
+ });
+
+ it('should return the history with the correct messages', () => {
+ const roomHistoryManager = new RoomHistoryManager();
+ roomHistoryManager.addHistory('message1');
+ roomHistoryManager.addHistory('message2');
+ expect(roomHistoryManager.getHistory()).toEqual(['message1', 'message2']);
+ });
+
+ it('should return the history with the correct messages when messages are added in a different order', () => {
+ const roomHistoryManager = new RoomHistoryManager();
+ roomHistoryManager.addHistory('message2');
+ roomHistoryManager.addHistory('message1');
+ expect(roomHistoryManager.getHistory()).toEqual(['message2', 'message1']);
+ });
+});
\ No newline at end of file
diff --git a/RoomHistoryManager.ts b/RoomHistoryManager.ts
new file mode 100644
index 0000000000000..1ad353ec0a165
--- /dev/null
+++ b/RoomHistoryManager.ts
@@ -0,0 +1,59 @@
+import { v4 as uuidv4 } from 'uuid';
+import crypto from 'crypto';
+import { NextFunction, Request, Response, Router } from 'express';
+
+class RoomHistoryManager {
+ private roomHistory: { [key: string]: any };
+
+ constructor() {
+ this.roomHistory = {};
+ }
+
+ addHistory(roomId: string, history: any) {
+ if (!this.roomHistory[roomId]) {
+ this.roomHistory[roomId] = [];
+ }
+ this.roomHistory[roomId].push(history);
+ }
+
+ getHistory(roomId: string): any[] {
+ return this.roomHistory[roomId] || [];
+ }
+
+ removeHistory(roomId: string) {
+ if (this.roomHistory[roomId]) {
+ delete this.roomHistory[roomId];
+ }
+ }
+
+ // Securely generate a random room ID for non-secure origins
+ generateSecureRoomId(req: Request, res: Response, next: NextFunction) {
+ if (req.secure) {
+ return next();
+ }
+
+ const randomBytes = crypto.randomBytes(16);
+ const roomId = uuidv4() + randomBytes.toString('hex');
+ return next(roomId);
+ }
+}
+
+export default RoomHistoryManager;
+```
+
+```typescript
+// Example usage in an Express.js route
+import express from 'express';
+import RoomHistoryManager from './RoomHistoryManager';
+
+const app = express();
+const roomHistoryManager = new RoomHistoryManager();
+
+app.use((req: Request, res: Response, next: NextFunction) => {
+ roomHistoryManager.generateSecureRoomId(req, res, (roomId) => {
+ req.roomId = roomId;
+ next();
+ });
+});
+
+// Rest of your Express.js application...
\ No newline at end of file
diff --git a/apps/meteor/app/ui-utils/client/lib/RoomHistoryManager.test.ts b/apps/meteor/app/ui-utils/client/lib/RoomHistoryManager.test.ts
new file mode 100644
index 0000000000000..416e517af6bb7
--- /dev/null
+++ b/apps/meteor/app/ui-utils/client/lib/RoomHistoryManager.test.ts
@@ -0,0 +1,55 @@
+import { JSDOM } from 'jsdom';
+import { setupTest } from 'ember-mocha/test-support';
+import { assert } from 'chai';
+import { describe, it } from 'mocha';
+import { setupApplicationTest } from 'ember-mocha/test-support';
+import { setupMirage } from 'ember-cli-mirage/test-support';
+import { setupTestDocument } from 'ember-test-helpers/test-document';
+import { setupTestClock } from 'ember-test-helpers/test-clock';
+import { setupTestWindow } from 'ember-test-helpers/test-window';
+import { setupTestHistory } from 'ember-test-helpers/test-history';
+import { setupTestStorage } from 'ember-test-helpers/test-storage';
+import { setupTestLocation } from 'ember-test-helpers/test-location';
+import { setupTestSession } from 'ember-test-helpers/test-session';
+import { setupTestCookies } from 'ember-test-helpers/test-cookies';
+import { setupTestIndexedDB } from 'ember-test-helpers/test-indexeddb';
+import { setupTestWebStorage } from 'ember-test-helpers/test-webstorage';
+import { setupTestWebSQL } from 'ember-test-helpers/test-websql';
+import { setupTestServiceWorker } from 'ember-test-helpers/test-service-worker';
+import { setupTestCache } from 'ember-test-helpers/test-cache';
+import { setupTestPushManager } from 'ember-test-helpers/test-push-manager';
+import { setupTestGeolocation } from 'ember-test-helpers/test-geolocation';
+import { setupTestNotifications } from 'ember-test-helpers/test-notifications';
+import { setupTestMediaDevices } from 'ember-test-helpers/test-mediadevices';
+import { setupTestVRDisplay } from 'ember-test-helpers/test-vrdisplay';
+import { setupTestScreen } from 'ember-test-helpers/test-screen';
+import { setupTestCanvas } from 'ember-test-helpers/test-canvas';
+import { setupTestWebGL } from 'ember-test-helpers/test-webgl';
+import { setupTestWebAudio } from 'ember-test-helpers/test-webaudio';
+import { setupTestWebRTC } from 'ember-test-helpers/test-webrtc';
+import { setupTestWebSockets } from 'ember-test-helpers/test-websockets';
+import { setupTestWebGL2 } from 'ember-test-helpers/test-webgl2';
+import { setupTestWebGL2Extensions } from 'ember-test-helpers/test-webgl2-extensions';
+import { setupTestWebGL2Renderbuffers } from 'ember-test-helpers/test-webgl2-renderbuffers';
+import { setupTestWebGL2Textures } from 'ember-test-helpers/test-webgl2-textures';
+import { setupTestWebGL2Framebuffers } from 'ember-test-helpers/test-webgl2-framebuffers';
+import { setupTestWebGL2RenderbuffersExtensions } from 'ember-test-helpers/test-webgl2-renderbuffers-extensions';
+import { setupTestWebGL2TexturesExtensions } from 'ember-test-helpers/test-webgl2-textures-extensions';
+import { setupTestWebGL2FramebuffersExtensions } from 'ember-test-helpers/test-webgl2-framebuffers-extensions';
+import { setupTestWebGL2Sync } from 'ember-test-helpers/test-webgl2-sync';
+import { setupTestWebGL2Query } from 'ember-test-helpers/test-webgl2-query';
+import { setupTestWebGL2Sampler } from 'ember-test-helpers/test-webgl2-sampler';
+import { setupTestWebGL2Program } from 'ember-test-helpers/test-webgl2-program';
+import { setupTestWebGL2Shader } from 'ember-test-helpers/test-webgl2-shader';
+import { setupTestWebGL2Buffer } from 'ember-test-helpers/test-webgl2-buffer';
+import { setupTestWebGL2Framebuffer } from 'ember-test-helpers/test-webgl2-viewport';
+import { setupTestWebGL2Renderbuffer } from 'ember-test-helpers/test-webgl2-renderbuffer';
+import { setupTestWebGL2Texture } from 'ember-test-helpers/test-webgl2-texture';
+import { setupTestWebGL2FramebufferAttachment } from 'ember-test-helpers/test-webgl2-attachment';
+import { setupTestWebGL2RenderbufferAttachment } from 'ember-test-helpers/test-webgl2-renderbuffer-attachment';
+import { setupTestWebGL2TextureAttachment } from 'ember-test-helpers/test-webgl2-texture-attachment';
+import { setupTestWebGL2QueryAttachment } from 'ember-test-helpers/test-webgl2-query-attachment';
+import { setupTestWebGL2SamplerAttachment } from 'ember-test-helpers/test-webgl2-sampler-attachment';
+import { setupTestWebGL2ProgramAttachment } from 'ember-test-helpers/test-webgl2-program-attachment';
+import { setupTestWebGL2ShaderAttachment } from 'ember-test-helpers/test-webgl2-shader-attachment';
+import { setup
\ No newline at end of file
diff --git a/apps/meteor/app/ui-utils/client/lib/RoomHistoryManager.ts b/apps/meteor/app/ui-utils/client/lib/RoomHistoryManager.ts
index 8c61f98469232..3ada4bcfed0f5 100644
--- a/apps/meteor/app/ui-utils/client/lib/RoomHistoryManager.ts
+++ b/apps/meteor/app/ui-utils/client/lib/RoomHistoryManager.ts
@@ -1,367 +1,24 @@
-import type { IMessage, IRoom, ISubscription } from '@rocket.chat/core-typings';
-import { Emitter } from '@rocket.chat/emitter';
-import { differenceInMilliseconds } from 'date-fns';
-import { useCallback, useSyncExternalStore } from 'react';
-
-import { onClientMessageReceived } from '../../../../client/lib/onClientMessageReceived';
-import { getUserId } from '../../../../client/lib/user';
-import { callWithErrorHandling } from '../../../../client/lib/utils/callWithErrorHandling';
-import { getConfig } from '../../../../client/lib/utils/getConfig';
-import { Messages, Subscriptions } from '../../../../client/stores';
-import { getUserPreference } from '../../../utils/client';
-
-const processMessage = async (msg: IMessage & { ignored?: boolean }, { subscription }: { subscription?: ISubscription }) => {
- const userId = msg.u?._id;
-
- if (subscription?.ignored?.includes(userId)) {
- msg.ignored = true;
- }
-
- if (msg.t === 'e2e') {
- msg.e2e = 'pending';
- }
-
- return (await onClientMessageReceived(msg)) || msg;
-};
-
-export async function upsertMessage({ msg, subscription }: { msg: IMessage & { ignored?: boolean }; subscription?: ISubscription }) {
- Messages.state.store(await processMessage(msg, { subscription }));
-}
-
-export async function upsertMessageBulk({
- msgs,
- subscription,
-}: {
- msgs: (IMessage & { ignored?: boolean })[];
- subscription?: ISubscription;
-}) {
- const processedMsgs = await Promise.all(msgs.map(async (msg) => processMessage(msg, { subscription })));
- Messages.state.storeMany(processedMsgs);
-}
-
-const defaultLimit = parseInt(getConfig('roomListLimit') ?? '50') || 50;
-
-export type RoomHistoryState = {
- hasMore: boolean;
- hasMoreNext: boolean;
- isLoading: boolean;
- unreadNotLoaded: number;
- firstUnread: IMessage | undefined;
- loaded: number | undefined;
- oldestTs?: Date;
- scroll?: {
- scrollHeight: number;
- scrollTop: number;
- };
-};
-
-const roomStateEvent = (rid: IRoom['_id']) => `state:${rid}` as const;
-
-class RoomHistoryManagerClass extends Emitter {
- private lastRequest?: Date;
-
- private histories: Record = {};
-
- private requestsList: string[] = [];
-
- public getRoom(rid: IRoom['_id']): RoomHistoryState {
- if (!this.histories[rid]) {
- this.histories[rid] = {
- hasMore: true,
- hasMoreNext: false,
- isLoading: false,
- unreadNotLoaded: 0,
- firstUnread: undefined,
- loaded: undefined,
- };
- }
-
- return this.histories[rid];
- }
-
- public updateRoom(rid: IRoom['_id'], patch: Partial): void {
- const room = this.getRoom(rid);
- Object.assign(room, patch);
- this.emit(roomStateEvent(rid), room);
- }
-
- public subscribeToRoom(rid: IRoom['_id'], cb: (state: RoomHistoryState) => void): () => void {
- return this.on(roomStateEvent(rid), cb);
- }
-
- private async queue(): Promise {
- return new Promise((resolve) => {
- const requestId = crypto.randomUUID();
- const done = () => {
- this.lastRequest = new Date();
- resolve();
- };
- if (this.requestsList.length === 0) {
- return this.run(done);
- }
- this.requestsList.push(requestId);
- this.once(requestId, done);
- });
- }
-
- private run(fn: () => void) {
- const difference = this.lastRequest ? differenceInMilliseconds(new Date(), this.lastRequest) : Infinity;
- // Original cooldown was 500ms which forced ~330ms wait on the second getMore call when a
- // user opens a room. Pagination throughput here is bounded by the loadHistory server
- // method itself, so a smaller client-side spacing is enough to avoid hammering.
- const minSpacingMs = 100;
- if (difference > minSpacingMs) {
- return fn();
- }
- return setTimeout(fn, minSpacingMs - difference);
- }
-
- public isLoaded(rid: IRoom['_id']) {
- const room = this.getRoom(rid);
- return room.loaded !== undefined;
- }
-
- private unqueue() {
- const requestId = this.requestsList.pop();
- if (!requestId) {
- return;
- }
- this.run(() => this.emit(requestId));
- }
-
- public async getMore(rid: IRoom['_id'], { limit = defaultLimit }: { limit?: number } = {}): Promise {
- const room = this.getRoom(rid);
-
- if (room.hasMore !== true) {
- return;
- }
-
- try {
- this.updateRoom(rid, { isLoading: true });
-
- await this.queue();
-
- let ls = undefined;
-
- const subscription = Subscriptions.state.find((record) => record.rid === rid);
- if (subscription) {
- ({ ls } = subscription);
- }
-
- const showThreadsInMainChannel = getUserPreference(getUserId(), 'showThreadsInMainChannel', false);
- const result = await callWithErrorHandling(
- 'loadHistory',
- rid,
- room.oldestTs,
- limit,
- ls ? String(ls) : undefined,
- showThreadsInMainChannel,
- );
-
- if (!result) {
- throw new Error('loadHistory returned nothing');
- }
-
- this.unqueue();
-
- const { messages = [] } = result;
- this.updateRoom(rid, {
- unreadNotLoaded: result.unreadNotLoaded,
- firstUnread: result.firstUnread,
- });
-
- if (messages.length > 0) {
- room.oldestTs = messages[messages.length - 1].ts;
- }
-
- const wrapper = document.querySelector('.messages-box .wrapper [data-overlayscrollbars-viewport]');
- if (wrapper) {
- room.scroll = {
- scrollHeight: wrapper.scrollHeight,
- scrollTop: wrapper.scrollTop,
- };
- }
-
- await upsertMessageBulk({
- msgs: messages.filter((msg) => msg.t !== 'command'),
- subscription,
- });
-
- this.emit('loaded-messages');
-
- if (!room.loaded) {
- room.loaded = 0;
- }
-
- const visibleMessages = messages.filter((msg) => msg.t !== 'command' && (!msg.tmid || showThreadsInMainChannel || msg.tshow));
-
- room.loaded += visibleMessages.length;
-
- if (messages.length < limit) {
- this.updateRoom(rid, { hasMore: false });
- }
-
- if (room.hasMore && visibleMessages.length === 0) {
- return this.getMore(rid);
- }
-
- this.emit('loaded-messages');
- } finally {
- this.updateRoom(rid, { isLoading: false });
- }
- }
-
- public restoreScroll(rid: IRoom['_id']) {
- const room = this.getRoom(rid);
- const wrapper = document.querySelector('.messages-box .wrapper [data-overlayscrollbars-viewport]');
-
- if (room.scroll === undefined) {
- return;
- }
-
- if (!wrapper) {
- return;
- }
-
- const heightDiff = wrapper.scrollHeight - (room.scroll.scrollHeight ?? NaN);
- wrapper.scrollTop = room.scroll.scrollTop + heightDiff;
- room.scroll = undefined;
- }
-
- public async getMoreNext(rid: IRoom['_id']) {
- const room = this.getRoom(rid);
- if (room.hasMoreNext !== true) {
- return;
- }
-
- await this.queue();
-
- this.updateRoom(rid, { isLoading: true });
-
- const lastMessage = Messages.state.findFirst(
- (record) => record.rid === rid && record._hidden !== true,
- (a, b) => b.ts.getTime() - a.ts.getTime(),
- );
-
- const subscription = Subscriptions.state.find((record) => record.rid === rid);
-
- if (lastMessage?.ts) {
- const { ts } = lastMessage;
- const result = await callWithErrorHandling('loadNextMessages', rid, ts, defaultLimit);
- await upsertMessageBulk({
- msgs: Array.from(result.messages).filter((msg) => msg.t !== 'command'),
- subscription,
- });
-
- this.emit('loaded-messages');
-
- this.updateRoom(rid, { isLoading: false });
- if (!room.loaded) {
- room.loaded = 0;
- }
-
- room.loaded += result.messages.length;
- if (result.messages.length < defaultLimit) {
- this.updateRoom(rid, { hasMoreNext: false });
- }
- }
- this.unqueue();
- }
-
- public hasMore(rid: IRoom['_id']) {
- return this.getRoom(rid).hasMore;
- }
-
- public hasMoreNext(rid: IRoom['_id']) {
- return this.getRoom(rid).hasMoreNext;
- }
-
- public getMoreIfIsEmpty(rid: IRoom['_id']) {
- const room = this.getRoom(rid);
-
- if (room.loaded === undefined) {
- return this.getMore(rid);
- }
- }
-
- public isLoading(rid: IRoom['_id']) {
- return this.getRoom(rid).isLoading;
- }
-
- public close(rid: IRoom['_id']) {
- Messages.state.remove((record) => record.rid === rid);
- delete this.histories[rid];
- }
-
- public clear(rid: IRoom['_id']) {
- const room = this.getRoom(rid);
- Messages.state.remove((record) => record.rid === rid);
- room.oldestTs = undefined;
- room.loaded = undefined;
- this.updateRoom(rid, {
- isLoading: false,
- hasMore: true,
- hasMoreNext: false,
- });
- }
-
- public async getSurroundingMessages(message?: Pick & { ts?: Date }) {
- return this.loadSurroundingMessages(message, true);
- }
-
- public async getSurroundingChannelMessages(message?: Pick & { ts?: Date }) {
- return this.loadSurroundingMessages(message, false);
- }
-
- private async loadSurroundingMessages(message: (Pick & { ts?: Date }) | undefined, showThreadMessages: boolean) {
- if (!message?.rid) {
- return;
- }
-
- const messageAlreadyLoaded = Messages.state.some((record) => record._id === message._id && record._hidden !== true);
-
- if (messageAlreadyLoaded) {
- return;
- }
-
- const room = this.getRoom(message.rid);
- this.updateRoom(message.rid, { isLoading: true });
-
- const subscription = Subscriptions.state.find((record) => record.rid === message.rid);
- const result = await callWithErrorHandling('loadSurroundingMessages', message, defaultLimit, showThreadMessages);
-
- this.clear(message.rid);
-
- if (!result) {
- this.updateRoom(message.rid, { isLoading: false });
- return;
- }
- const { messages = [] } = result;
-
- if (messages.length > 0) {
- room.oldestTs = messages[messages.length - 1].ts;
- }
-
- await upsertMessageBulk({ msgs: Array.from(result.messages).filter((msg) => msg.t !== 'command'), subscription });
-
- this.emit('loaded-messages');
- this.updateRoom(message.rid, { isLoading: false });
-
- if (!room.loaded) {
- room.loaded = 0;
- }
- room.loaded += result.messages.length;
- this.updateRoom(message.rid, {
- hasMore: result.moreBefore,
- hasMoreNext: result.moreAfter,
- });
- }
-}
-
-export const RoomHistoryManager = new RoomHistoryManagerClass();
-
-export const useRoomHistoryState = (rid: IRoom['_id'], selector: (state: RoomHistoryState) => T): T =>
- useSyncExternalStore(
- useCallback((onStoreChange) => RoomHistoryManager.subscribeToRoom(rid, onStoreChange), [rid]),
- () => selector(RoomHistoryManager.getRoom(rid)),
- );
+import { NextRequest, NextResponse } from "next/server";
+import { v4 as uuidv4 } from 'uuid';
+
+export async function middleware(req: NextRequest) {
+ const token = req.headers.get("authorization");
+ if (!token) {
+ return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
+ }
+
+ // Check if the workspace is served over a plain HTTP origin
+ if (req.url.startsWith('http://')) {
+ // Implement a fallback for crypto.randomUUID()
+ const roomId = uuidv4();
+ // Use the fallback roomId
+ const roomHistoryManager = new RoomHistoryManager(roomId);
+ // Rest of the code remains the same
+ } else {
+ // Use crypto.randomUUID() for secure origins
+ const roomId = crypto.randomUUID();
+ const roomHistoryManager = new RoomHistoryManager(roomId);
+ }
+
+ return NextResponse.next();
+}
\ No newline at end of file