From d3f81c9a14415a9e9623bd70ce57dfd353308820 Mon Sep 17 00:00:00 2001 From: jarvis2f <137974272+jarvis2f@users.noreply.github.com> Date: Sun, 12 Jan 2025 14:04:50 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9A=A1=EF=B8=8F=20feat:=20Optimize=20websock?= =?UTF-8?q?et=20binding=20method,=20optimize=20download=20speed=20event=20?= =?UTF-8?q?current=20limit?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../telegram/files/AutoDownloadVerticle.java | 2 +- .../main/java/telegram/files/EventEnum.java | 8 + .../java/telegram/files/HttpVerticle.java | 69 ++++-- .../java/telegram/files/TelegramVerticle.java | 37 ++- web/src/components/account-creator.tsx | 4 +- web/src/components/file-extra.tsx | 229 ++++++++++-------- web/src/hooks/use-file-speed.ts | 11 +- web/src/hooks/use-files.ts | 11 +- web/src/hooks/use-websocket.tsx | 21 +- 9 files changed, 234 insertions(+), 158 deletions(-) diff --git a/api/src/main/java/telegram/files/AutoDownloadVerticle.java b/api/src/main/java/telegram/files/AutoDownloadVerticle.java index b6571d7..262bf1e 100644 --- a/api/src/main/java/telegram/files/AutoDownloadVerticle.java +++ b/api/src/main/java/telegram/files/AutoDownloadVerticle.java @@ -110,7 +110,7 @@ public class AutoDownloadVerticle extends AbstractVerticle { }); vertx.eventBus().consumer(EventEnum.SETTING_UPDATE.address(SettingKey.autoDownloadLimit.name()), message -> { log.debug("Auto download limit update: %s".formatted(message.body())); - this.limit = Convert.toInt(message.body()); + this.limit = Convert.toInt(message.body(), DEFAULT_LIMIT); }); vertx.eventBus().consumer(EventEnum.MESSAGE_RECEIVED.address(), message -> { log.trace("Auto download message received: %s".formatted(message.body())); diff --git a/api/src/main/java/telegram/files/EventEnum.java b/api/src/main/java/telegram/files/EventEnum.java index 9031377..c889d07 100644 --- a/api/src/main/java/telegram/files/EventEnum.java +++ b/api/src/main/java/telegram/files/EventEnum.java @@ -23,6 +23,14 @@ public enum EventEnum { * body = JSONObject with "telegramId", "chatId", "messageId" */ MESSAGE_RECEIVED, + + /** + * suffix = null
+ * body = JSONObject with "telegramId", "payload" + * + * @see telegram.files.EventPayload + */ + TELEGRAM_EVENT, ; public String address() { diff --git a/api/src/main/java/telegram/files/HttpVerticle.java b/api/src/main/java/telegram/files/HttpVerticle.java index 01138da..5138b5b 100644 --- a/api/src/main/java/telegram/files/HttpVerticle.java +++ b/api/src/main/java/telegram/files/HttpVerticle.java @@ -13,6 +13,7 @@ import io.vertx.core.http.CookieSameSite; import io.vertx.core.http.HttpMethod; import io.vertx.core.http.HttpServerOptions; import io.vertx.core.http.HttpServerResponse; +import io.vertx.core.json.Json; import io.vertx.core.json.JsonArray; import io.vertx.core.json.JsonObject; import io.vertx.ext.healthchecks.HealthCheckHandler; @@ -55,6 +56,7 @@ public class HttpVerticle extends AbstractVerticle { initHttpServer() .compose(r -> initTelegramVerticles()) .compose(r -> initAutoDownloadVerticle()) + .compose(r -> initEventConsumer()) .onSuccess(startPromise::complete) .onFailure(startPromise::fail); } @@ -216,12 +218,37 @@ public class HttpVerticle extends AbstractVerticle { .mapEmpty(); } + private Future initEventConsumer() { + vertx.eventBus().consumer(EventEnum.TELEGRAM_EVENT.address(), message -> { + log.debug("Received telegram event: %s".formatted(message.body())); + JsonObject jsonObject = (JsonObject) message.body(); + String telegramId = jsonObject.getString("telegramId"); + EventPayload payload = jsonObject.getJsonObject("payload").mapTo(EventPayload.class); + + sessionTelegramVerticles.entrySet().stream() + .filter(e -> Objects.equals(Convert.toStr(e.getValue().getId()), telegramId)) + .map(Map.Entry::getKey) + .forEach(sessionId -> { + String wsHandlerId = clients.get(sessionId); + if (StrUtil.isNotBlank(wsHandlerId)) { + vertx.eventBus().send(wsHandlerId, Json.encode(payload)); + } + }); + }); + + return Future.succeededFuture(); + } + private void handleWebSocket(RoutingContext ctx) { String sessionId = ctx.session().id(); + String telegramId = ctx.request().getParam("telegramId"); ctx.request().toWebSocket() .onSuccess(ws -> { log.debug("Upgraded to WebSocket. SessionId: %s".formatted(sessionId)); clients.put(sessionId, ws.textHandlerID()); + if (StrUtil.isNotBlank(telegramId) && !handleTelegramChange(sessionId, telegramId)) { + log.debug("Failed to change telegram verticle. SessionId: %s".formatted(sessionId)); + } long timerId = vertx.setPeriodic(30000, id -> { if (!ws.isClosed()) { @@ -237,9 +264,7 @@ public class HttpVerticle extends AbstractVerticle { log.debug("WebSocket closed. SessionId: %s".formatted(sessionId)); }); - ws.textMessageHandler(text -> { - log.debug("Received WebSocket message: " + text); - }); + ws.textMessageHandler(text -> log.debug("Received WebSocket message: " + text)); }) .onFailure(err -> log.warn("Failed to upgrade to WebSocket: %s".formatted(err.getMessage()))); } @@ -303,7 +328,6 @@ public class HttpVerticle extends AbstractVerticle { String proxyName = jsonObject.getString("proxyName"); TelegramVerticle newTelegramVerticle = new TelegramVerticle(DataVerticle.telegramRepository.getRootPath()); - newTelegramVerticle.bindHttpSession(sessionId); newTelegramVerticle.setProxy(proxyName); sessionTelegramVerticles.put(sessionId, newTelegramVerticle); telegramVerticles.add(newTelegramVerticle); @@ -356,11 +380,10 @@ public class HttpVerticle extends AbstractVerticle { String query = ctx.request().getParam("query"); String chatId = ctx.request().getParam("chatId"); getTelegramVerticle(telegramId) - .ifPresentOrElse(telegramVerticle -> { - telegramVerticle.getChats(Convert.toLong(chatId), query) - .onSuccess(ctx::json) - .onFailure(ctx::fail); - }, () -> ctx.fail(404)); + .ifPresentOrElse(telegramVerticle -> + telegramVerticle.getChats(Convert.toLong(chatId), query) + .onSuccess(ctx::json) + .onFailure(ctx::fail), () -> ctx.fail(404)); } private void handleTelegramFiles(RoutingContext ctx) { @@ -427,16 +450,24 @@ public class HttpVerticle extends AbstractVerticle { private void handleTelegramChange(RoutingContext ctx) { String sessionId = ctx.session().id(); String telegramId = ctx.request().getParam("telegramId"); + if (handleTelegramChange(sessionId, telegramId)) { + ctx.end(); + } else { + ctx.fail(400); + } + } + + private boolean handleTelegramChange(String sessionId, String telegramId) { if (StrUtil.isBlank(telegramId)) { sessionTelegramVerticles.remove(sessionId); - ctx.end(); + return true; } - getTelegramVerticle(telegramId) - .ifPresentOrElse(telegramVerticle -> { - telegramVerticle.bindHttpSession(sessionId); - sessionTelegramVerticles.put(sessionId, telegramVerticle); - ctx.end(); - }, () -> ctx.fail(404)); + Optional optionalTelegramVerticle = getTelegramVerticle(telegramId); + if (optionalTelegramVerticle.isEmpty()) { + return false; + } + sessionTelegramVerticles.put(sessionId, optionalTelegramVerticle.get()); + return true; } private void handleTelegramToggleProxy(RoutingContext ctx) { @@ -478,13 +509,11 @@ public class HttpVerticle extends AbstractVerticle { ctx.fail(400); return; } - String sessionId = ctx.session().id(); TelegramVerticle telegramVerticle = getTelegramVerticle(ctx); if (telegramVerticle == null) { return; } JsonObject params = ctx.body().asJsonObject(); - telegramVerticle.bindHttpSession(sessionId); telegramVerticle.execute(method, params == null ? null : params.getMap()) .onSuccess(code -> ctx.json(JsonObject.of("code", code))) .onFailure(ctx::fail); @@ -637,10 +666,6 @@ public class HttpVerticle extends AbstractVerticle { .findFirst(); } - public static String getWSHandlerId(String sessionId) { - return clients.get(sessionId); - } - private TelegramVerticle getTelegramVerticle(RoutingContext ctx) { String sessionId = ctx.session().id(); TelegramVerticle telegramVerticle = sessionTelegramVerticles.get(sessionId); diff --git a/api/src/main/java/telegram/files/TelegramVerticle.java b/api/src/main/java/telegram/files/TelegramVerticle.java index 1af2be3..9ab8e09 100644 --- a/api/src/main/java/telegram/files/TelegramVerticle.java +++ b/api/src/main/java/telegram/files/TelegramVerticle.java @@ -42,8 +42,6 @@ public class TelegramVerticle extends AbstractVerticle { private Client client; - private volatile String httpSessionId; - public boolean authorized = false; public TdApi.AuthorizationState lastAuthorizationState; @@ -62,6 +60,10 @@ public class TelegramVerticle extends AbstractVerticle { private long avgSpeedPersistenceTimerId; + private long lastFileEventTime; + + private long lastFileDownloadEventTime; + static { Client.setLogMessageHandler(0, new LogMessageHandler()); @@ -137,10 +139,6 @@ public class TelegramVerticle extends AbstractVerticle { return true; } - public void bindHttpSession(String httpSessionId) { - this.httpSessionId = httpSessionId; - } - public Future getTelegramAccount() { return Future.future(promise -> { if (!authorized) { @@ -441,11 +439,6 @@ public class TelegramVerticle extends AbstractVerticle { return this.execute(new TdApi.ToggleDownloadIsPaused(fileId, isPaused)); }) - .onSuccess(r -> - sendHttpEvent(EventPayload.build(EventPayload.TYPE_FILE_STATUS, new JsonObject() - .put("fileId", fileId) - .put("downloadStatus", isPaused ? FileRecord.DownloadStatus.paused : FileRecord.DownloadStatus.downloading) - ))) .mapEmpty(); } @@ -644,14 +637,8 @@ public class TelegramVerticle extends AbstractVerticle { } private void sendHttpEvent(EventPayload payload) { - if (this.httpSessionId == null) return; - - String address = HttpVerticle.getWSHandlerId(httpSessionId); - if (address == null) { - log.debug("[%s] Can not found websocket textHandlerID for session id:%s".formatted(getRootId(), httpSessionId)); - return; - } - vertx.eventBus().send(address, Json.encode(payload)); + vertx.eventBus().send(EventEnum.TELEGRAM_EVENT.address(), + JsonObject.of("telegramId", this.getId(), "payload", JsonObject.mapFrom(payload))); } private void handleAuthorizationResult(TdApi.Object object) { @@ -797,6 +784,7 @@ public class TelegramVerticle extends AbstractVerticle { if (file != null) { String localPath = null; Long completionDate = null; + long downloadedSize = file.local == null ? 0 : file.local.downloadedSize; if (file.local != null && file.local.isDownloadingCompleted) { localPath = file.local.path; completionDate = System.currentTimeMillis(); @@ -814,16 +802,23 @@ public class TelegramVerticle extends AbstractVerticle { .put("downloadStatus", r.getString("downloadStatus")) .put("localPath", r.getString("localPath")) .put("completionDate", r.getLong("completionDate")) + .put("downloadedSize", downloadedSize) )); }); } - sendHttpEvent(EventPayload.build(EventPayload.TYPE_FILE, updateFile)); + if (lastFileEventTime == 0 || System.currentTimeMillis() - lastFileEventTime > 1000) { + sendHttpEvent(EventPayload.build(EventPayload.TYPE_FILE, updateFile)); + lastFileEventTime = System.currentTimeMillis(); + } } private void onFileDownloadsUpdated(TdApi.UpdateFileDownloads updateFileDownloads) { log.trace("[%s] Receive file downloads update: %s".formatted(getRootId(), updateFileDownloads)); avgSpeed.update(updateFileDownloads.downloadedSize, System.currentTimeMillis()); - sendHttpEvent(EventPayload.build(EventPayload.TYPE_FILE_DOWNLOAD, updateFileDownloads)); + if (lastFileDownloadEventTime == 0 || System.currentTimeMillis() - lastFileDownloadEventTime > 1000) { + sendHttpEvent(EventPayload.build(EventPayload.TYPE_FILE_DOWNLOAD, updateFileDownloads)); + lastFileDownloadEventTime = System.currentTimeMillis(); + } } private void onMessageReceived(TdApi.Message message) { diff --git a/web/src/components/account-creator.tsx b/web/src/components/account-creator.tsx index 8afa4fe..d14ce76 100644 --- a/web/src/components/account-creator.tsx +++ b/web/src/components/account-creator.tsx @@ -171,8 +171,8 @@ export default function AccountCreator({ if (!authState && !isMethodExecuting) { return ( -
-

Waiting for the telegram account to be initialized, please wait

+
+

Waiting for the telegram account to be initialized, please wait.

If it takes too long, please refresh the page or try again later.

diff --git a/web/src/components/file-extra.tsx b/web/src/components/file-extra.tsx index 2d8e064..297b6ea 100644 --- a/web/src/components/file-extra.tsx +++ b/web/src/components/file-extra.tsx @@ -25,110 +25,141 @@ interface FileExtraProps { rowHeight: RowHeight; } -export default function FileExtra({ file, rowHeight }: FileExtraProps) { +function FileName({ file }: { file: TelegramFile }) { + if (!file.fileName) { + return null; + } + return ( + +

+ + + {file.fileName} + +

+
+ ); +} + +function FileCaption({ file, rowHeight }: FileExtraProps) { + if (!file.caption) { + return null; + } + return ( + + + +
+ +

+ {file.caption} +

+
+
+ +

"), + }} + >

+
+
+
+ ); +} + +function FilePath({ file }: { file: TelegramFile }) { const [, copyToClipboard] = useCopyToClipboard(); + if (!file.localPath) { + return null; + } + return ( + + +
+ +

copyToClipboard(file.localPath)} + > + {file.localPath.split("/").pop()} + +

+
+
+ +
{file.localPath}
+
+
+ ); +} + +function FileTime({ file }: { file: TelegramFile }) { + return ( +
+ + +

+ + + {formatDistanceToNow(new Date(file.date * 1000), { + addSuffix: true, + })} + +

+
+ +
+ {`Message received at ${new Date(file.date * 1000).toLocaleString()}`} +
+
+
+ {file.completionDate && ( + + +

+ + + {formatDistanceToNow(new Date(file.completionDate), { + addSuffix: true, + })} + +

+
+ +
+ {`File downloaded at ${new Date(file.completionDate).toLocaleString()}`} +
+
+
+ )} +
+ ); +} + +export default function FileExtra({ file, rowHeight }: FileExtraProps) { + if (rowHeight === "s") { + return ( + + {file.fileName ? : } + + ); + } + return (
- {file.fileName && ( - -

- - - {file.fileName} - -

-
- )} - {rowHeight !== "s" && file.caption && ( - - - -
- -

- {file.caption} -

-
-
- -

"), - }} - >

-
-
-
- )} - {rowHeight !== "s" && file.localPath && ( - - -
- -

copyToClipboard(file.localPath)} - > - {file.localPath.split("/").pop()} - -

-
-
- -
- {file.localPath} -
-
-
- )} - {rowHeight !== "s" && ( -
- - -

- - - {formatDistanceToNow(new Date(file.date * 1000), { - addSuffix: true, - })} - -

-
- -
- {`Message received at ${new Date(file.date * 1000).toLocaleString()}`} -
-
-
- {file.completionDate && ( - - -

- - - {formatDistanceToNow(new Date(file.completionDate), { - addSuffix: true, - })} - -

-
- -
- {`File downloaded at ${new Date(file.completionDate).toLocaleString()}`} -
-
-
- )} -
- )} + + + +
); diff --git a/web/src/hooks/use-file-speed.ts b/web/src/hooks/use-file-speed.ts index 318fdd7..ae21029 100644 --- a/web/src/hooks/use-file-speed.ts +++ b/web/src/hooks/use-file-speed.ts @@ -14,9 +14,14 @@ export function useFileSpeed(fileId: number) { lastTimestamp: 0, }); - const [debounceSpeed] = useDebounce(downloadSpeed.speed, 1000, { + const [debounceSpeed] = useDebounce(downloadSpeed.speed, 300, { leading: true, - maxWait: 2000, + maxWait: 1000, + }); + + const [debounceProgress] = useDebounce(downloadProgress, 300, { + leading: true, + maxWait: 1000, }); useEffect(() => { @@ -94,7 +99,7 @@ export function useFileSpeed(fileId: number) { }, []); return { - downloadProgress, + downloadProgress: debounceProgress, downloadSpeed: debounceSpeed, }; } diff --git a/web/src/hooks/use-files.ts b/web/src/hooks/use-files.ts index 12f730f..f06f755 100644 --- a/web/src/hooks/use-files.ts +++ b/web/src/hooks/use-files.ts @@ -8,7 +8,7 @@ import useSWRInfinite from "swr/infinite"; import { useWebsocket } from "@/hooks/use-websocket"; import { WebSocketMessageType } from "@/lib/websocket-types"; import useLocalStorage from "@/hooks/use-local-storage"; -import {useDebounce} from "use-debounce"; +import { useDebounce } from "use-debounce"; const DEFAULT_FILTERS: FileFilter = { search: "", @@ -31,6 +31,7 @@ export function useFiles(accountId: string, chatId: string) { downloadStatus: DownloadStatus; localPath: string; completionDate: number; + downloadedSize: number; } > >({}); @@ -76,6 +77,7 @@ export function useFiles(accountId: string, chatId: string) { downloadStatus: DownloadStatus; localPath: string; completionDate: number; + downloadedSize: number; }; setLatestFileStatus((prev) => ({ @@ -84,7 +86,10 @@ export function useFiles(accountId: string, chatId: string) { downloadStatus: data.downloadStatus ?? prev[data.fileId]?.downloadStatus, localPath: data.localPath ?? prev[data.fileId]?.localPath, - completionDate: data.completionDate ?? prev[data.fileId]?.completionDate, + completionDate: + data.completionDate ?? prev[data.fileId]?.completionDate, + downloadedSize: + data.downloadedSize ?? prev[data.fileId]?.downloadedSize, }, })); }, [lastJsonMessage]); @@ -101,6 +106,8 @@ export function useFiles(accountId: string, chatId: string) { localPath: latestFilesStatus[file.id]?.localPath ?? file.localPath, completionDate: latestFilesStatus[file.id]?.completionDate ?? file.completionDate, + downloadedSize: + latestFilesStatus[file.id]?.downloadedSize ?? file.downloadedSize, }); }); }); diff --git a/web/src/hooks/use-websocket.tsx b/web/src/hooks/use-websocket.tsx index 17fac6f..19363b4 100644 --- a/web/src/hooks/use-websocket.tsx +++ b/web/src/hooks/use-websocket.tsx @@ -17,6 +17,7 @@ import { import { useToast } from "./use-toast"; import { useDebounce } from "use-debounce"; import { getWsUrl } from "@/lib/api"; +import { useParams } from "next/navigation"; const WS_URL = `${getWsUrl()}`; @@ -39,6 +40,7 @@ interface WebSocketProviderProps { export const WebSocketProvider: React.FC = ({ children, }) => { + const params = useParams(); const [isReady, setIsReady] = useState(false); const [accountDownloadSpeed, setAccountDownloadSpeed] = useState({ speed: 0, @@ -46,17 +48,20 @@ export const WebSocketProvider: React.FC = ({ lastTimestamp: 0, }); const { toast } = useToast(); - const [debounceSpeed] = useDebounce(accountDownloadSpeed.speed, 1000, { + const [debounceSpeed] = useDebounce(accountDownloadSpeed.speed, 300, { leading: true, - maxWait: 2000, + maxWait: 1000, }); const { sendMessage, lastJsonMessage, readyState } = - useWebSocket(WS_URL, { - shouldReconnect: (closeEvent) => true, - reconnectAttempts: 3, - reconnectInterval: 3000, - }); + useWebSocket( + `${WS_URL}?telegramId=${(params.accountId as string) ?? ""}`, + { + shouldReconnect: (closeEvent) => true, + reconnectAttempts: 3, + reconnectInterval: 3000, + }, + ); const connectionStatus = { [ReadyState.CONNECTING]: "Connecting", @@ -106,7 +111,7 @@ export const WebSocketProvider: React.FC = ({ lastDownloadedSize: downloadedSize, }; const timeDiff = (timestamp - prev.lastTimestamp) / 1000; - if (prev.lastTimestamp === 0 || timeDiff <= 0) { + if (prev.lastTimestamp === 0 || timeDiff <= 0 || downloadedSize <= prev.lastDownloadedSize) { return { ...state, speed: prev.speed,