From 962ad675af2af3fc098a8017e6976186f37124e2 Mon Sep 17 00:00:00 2001 From: SegoCode <35817798+SegoCode@users.noreply.github.com> Date: Thu, 13 Mar 2025 18:41:46 +0100 Subject: [PATCH 01/72] refactor code and add web panel --- code/pom.xml | 21 ++ code/src/main/java/org/segocode/Main.java | 15 +- .../main/java/org/segocode/bot/Webdlbot.java | 55 +++- .../segocode/bot/model/DataRootContainer.java | 28 ++ .../java/org/segocode/bot/model/User.java | 40 +++ .../segocode/bot/service/MessageService.java | 32 +++ .../segocode/bot/service/VideoService.java | 62 +++++ .../org/segocode/bot/util/MessageUtil.java | 12 + .../org/segocode/panel/PanelController.java | 62 +++++ .../java/org/segocode/panel/views/admin.html | 250 ++++++++++++++++++ .../system/command/CommandExecutor.java | 55 ++++ 11 files changed, 625 insertions(+), 7 deletions(-) create mode 100644 code/src/main/java/org/segocode/bot/model/DataRootContainer.java create mode 100644 code/src/main/java/org/segocode/bot/model/User.java create mode 100644 code/src/main/java/org/segocode/bot/service/MessageService.java create mode 100644 code/src/main/java/org/segocode/bot/service/VideoService.java create mode 100644 code/src/main/java/org/segocode/bot/util/MessageUtil.java create mode 100644 code/src/main/java/org/segocode/panel/PanelController.java create mode 100644 code/src/main/java/org/segocode/panel/views/admin.html create mode 100644 code/src/main/java/org/segocode/system/command/CommandExecutor.java diff --git a/code/pom.xml b/code/pom.xml index 9c6c9da..bf1f834 100644 --- a/code/pom.xml +++ b/code/pom.xml @@ -25,6 +25,27 @@ logback-classic 1.4.14 + + org.projectlombok + lombok + 1.18.36 + provided + + + org.eclipse.store + storage-embedded + 2.1.2 + + + io.javalin + javalin + 6.5.0 + + + com.google.code.gson + gson + 2.12.1 + diff --git a/code/src/main/java/org/segocode/Main.java b/code/src/main/java/org/segocode/Main.java index bbafafc..2454510 100644 --- a/code/src/main/java/org/segocode/Main.java +++ b/code/src/main/java/org/segocode/Main.java @@ -1,19 +1,30 @@ package org.segocode; +import io.javalin.Javalin; +import org.eclipse.store.storage.embedded.types.EmbeddedStorage; +import org.eclipse.store.storage.embedded.types.EmbeddedStorageManager; +import org.eclipse.store.storage.types.StorageManager; import org.segocode.bot.Webdlbot; +import org.segocode.panel.PanelController; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.telegram.telegrambots.meta.TelegramBotsApi; import org.telegram.telegrambots.updatesreceivers.DefaultBotSession; +import java.util.Date; + public class Main { private static final Logger LOGGER = LoggerFactory.getLogger(Main.class); public static void main(String[] args) { try { - LOGGER.info("Starting the video download bot..."); + LOGGER.info("Starting storage manager..."); + final EmbeddedStorageManager storageManager = EmbeddedStorage.start(); + LOGGER.info("Starting webp anel app..."); + PanelController.start(storageManager); TelegramBotsApi botsApi = new TelegramBotsApi(DefaultBotSession.class); - botsApi.registerBot(new Webdlbot()); + LOGGER.info("Starting the video download bot..."); + botsApi.registerBot(new Webdlbot(storageManager)); LOGGER.info("Bot started successfully and ready to download videos 🚀"); } catch (Exception e) { LOGGER.error("Error while attempting to start the bot. Error details:", e); diff --git a/code/src/main/java/org/segocode/bot/Webdlbot.java b/code/src/main/java/org/segocode/bot/Webdlbot.java index ff7ca36..fa7a891 100644 --- a/code/src/main/java/org/segocode/bot/Webdlbot.java +++ b/code/src/main/java/org/segocode/bot/Webdlbot.java @@ -1,26 +1,32 @@ package org.segocode.bot; -import org.segocode.bot.utils.Utils; -import org.segocode.system.CommandExecutor; -import org.segocode.service.VideoService; +import org.eclipse.store.storage.types.StorageManager; +import org.segocode.bot.model.DataRootContainer; +import org.segocode.bot.model.User; +import org.segocode.system.command.CommandExecutor; +import org.segocode.bot.service.VideoService; import org.slf4j.LoggerFactory; import org.telegram.telegrambots.bots.TelegramLongPollingBot; import org.telegram.telegrambots.meta.api.objects.Message; import org.telegram.telegrambots.meta.api.objects.Update; -import org.segocode.service.MessageService; +import org.segocode.bot.service.MessageService; import org.slf4j.Logger; import org.telegram.telegrambots.meta.exceptions.TelegramApiException; +import java.time.LocalDateTime; +import java.util.Optional; import java.util.concurrent.*; import static org.segocode.bot.constants.Messages.*; -import static org.segocode.bot.utils.Utils.*; +import static org.segocode.bot.util.MessageUtil.*; import static org.segocode.system.util.FileUtil.*; public class Webdlbot extends TelegramLongPollingBot { private static final Logger LOGGER = LoggerFactory.getLogger(Webdlbot.class); private static final String BOT_TOKEN = System.getenv("BOT_TOKEN"); + private StorageManager storageManager; + // Create a ThreadPoolExecutor with a single virtual thread private final ThreadPoolExecutor executorService = new ThreadPoolExecutor( 1, // corePoolSize: the number of threads to keep in the pool, even if they are idle @@ -31,6 +37,20 @@ public class Webdlbot extends TelegramLongPollingBot { Thread.ofVirtual().factory() // the factory to use when creating new threads ); + public Webdlbot(StorageManager storageManager) { + this.storageManager = storageManager; + initializeRootContainer(); + } + + private void initializeRootContainer() { + Object root = storageManager.root(); + if (root == null || !(root instanceof DataRootContainer)) { + storageManager.setRoot(new DataRootContainer()); + LOGGER.info("Initialized new DataRootContainer as db root object"); + } + LOGGER.info("Root db object contains {} users", ((DataRootContainer) storageManager.root()).getUsers().size()); + } + @Override public String getBotUsername() { return "webdl"; @@ -74,6 +94,9 @@ public void onUpdateReceived(Update update) { } catch (Exception e) { LOGGER.error("Failed on onUpdateReceived, error: {}", e.getMessage(), e); handleDispatchError(update, e); + } finally { + loadMetricsData(update); + storageManager.storeRoot(); } } } @@ -107,6 +130,28 @@ private void handleDispatchError(Update update, Exception e) { cleanDownloadsFolder(); } } + + private void loadMetricsData(Update update) { + // Create or update user data from the Telegram update + org.telegram.telegrambots.meta.api.objects.User telegramUser = update.getMessage().getFrom(); + + DataRootContainer rootContainer = (DataRootContainer) storageManager.root(); + + // Find existing user or create new one + String userId = telegramUser.getId().toString(); + Optional existingUser = rootContainer.findUserById(userId); + + if (existingUser.isPresent()) { + User user = existingUser.get(); + user.recordNewMessage(); + } else { + User newUser = new User(userId, update.getMessage().getChatId(), telegramUser.getUserName(), telegramUser.getFirstName(), + telegramUser.getLastName(), telegramUser.getLanguageCode(), telegramUser.getIsPremium(), + 1,LocalDateTime.now().toString()); + rootContainer.getUsers().add(newUser); + LOGGER.info("New user registered: {}", newUser.getUserName()); + } + } } diff --git a/code/src/main/java/org/segocode/bot/model/DataRootContainer.java b/code/src/main/java/org/segocode/bot/model/DataRootContainer.java new file mode 100644 index 0000000..98ca8a0 --- /dev/null +++ b/code/src/main/java/org/segocode/bot/model/DataRootContainer.java @@ -0,0 +1,28 @@ +package org.segocode.bot.model; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class DataRootContainer { + private List users = new ArrayList<>(); + + /** + * Finds a user by their ID + * + * @param id The ID of the user to find + * @return An Optional containing the user if found, empty otherwise + */ + public Optional findUserById(String id) { + return users.stream() + .filter(user -> id.equals(user.getId())) + .findFirst(); + } +} diff --git a/code/src/main/java/org/segocode/bot/model/User.java b/code/src/main/java/org/segocode/bot/model/User.java new file mode 100644 index 0000000..ac76581 --- /dev/null +++ b/code/src/main/java/org/segocode/bot/model/User.java @@ -0,0 +1,40 @@ +package org.segocode.bot.model; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class User { + // Unique identifiers + private String id; + private Long chatId; + private String userName; + + // Personal information + private String firstName; + private String lastName; + + // Preferences and metadata + private String languageCode; + private Boolean isPremium; + + // Usage statistics + private Integer messageCount; + private String lastMessageTime; + + /** + * Increments the message count and updates the last message time to current time + */ + public void recordNewMessage() { + if (this.messageCount == null) { + this.messageCount = 0; + } + this.messageCount++; + this.lastMessageTime = LocalDateTime.now().toString(); + } +} \ No newline at end of file diff --git a/code/src/main/java/org/segocode/bot/service/MessageService.java b/code/src/main/java/org/segocode/bot/service/MessageService.java new file mode 100644 index 0000000..9a2c996 --- /dev/null +++ b/code/src/main/java/org/segocode/bot/service/MessageService.java @@ -0,0 +1,32 @@ +package org.segocode.bot.service; + +import org.telegram.telegrambots.meta.api.methods.send.SendMessage; +import org.telegram.telegrambots.meta.api.methods.updatingmessages.DeleteMessage; + +public class MessageService { + + /** + * Sends a text message to a specific chat in Telegram. + * + * @param chatId The ID of the chat where the message will be sent. + * @param replyToMessageId The ID of the message to which this message will be a reply. + * @param text The text content of the message to be sent. + * @return The SendMessage object configured with the chat ID, text, and reply to message ID. + */ + public static SendMessage sendTextMessage(Long chatId, Integer replyToMessageId, String text) { + SendMessage message = new SendMessage(chatId.toString(), text); + message.setReplyToMessageId(replyToMessageId); + return message; + } + + /** + * Deletes a specific message in a Telegram chat. + * + * @param chatId The ID of the chat where the message is located. + * @param messageId The ID of the message to be deleted. + * @return A DeleteMessage object configured with the chat ID and message ID. + */ + public static DeleteMessage deleteMessage(Long chatId, Integer messageId) { + return new DeleteMessage(chatId.toString(), messageId); + } +} diff --git a/code/src/main/java/org/segocode/bot/service/VideoService.java b/code/src/main/java/org/segocode/bot/service/VideoService.java new file mode 100644 index 0000000..79d853b --- /dev/null +++ b/code/src/main/java/org/segocode/bot/service/VideoService.java @@ -0,0 +1,62 @@ +package org.segocode.bot.service; + +import org.segocode.system.util.FileUtil; +import org.slf4j.LoggerFactory; +import org.telegram.telegrambots.meta.api.methods.send.SendVideo; +import org.telegram.telegrambots.meta.api.objects.InputFile; + +import java.io.File; +import org.slf4j.Logger; + + +public class VideoService { + private static final Logger LOGGER = LoggerFactory.getLogger(VideoService.class); + + /** + * Sends a video to a specific chat in Telegram. + * + * @param chatId The ID of the chat where the video will be sent. + * @param replyToMessageId The ID of the message to which this video will be a reply. + * @return The SendVideo object configured with the chat ID and reply to message ID. + */ + public static SendVideo sendVideo(Long chatId, Integer replyToMessageId) { + String filePath = buildFilePath(replyToMessageId); + LOGGER.info("Locating video file for message ID {}: {}", replyToMessageId, filePath); + + File videoFile = FileUtil.locateVideoFile(filePath); + LOGGER.info("Video file located successfully: {}", videoFile.getPath()); + + SendVideo sendVideoRequest = createSendVideoRequest(chatId, replyToMessageId, videoFile); + LOGGER.info("Video is sending. Chat ID: {}, Reply to Message ID: {}", chatId, replyToMessageId); + + return sendVideoRequest; + } + + /** + * Creates a SendVideo request for sending a video. + * + * @param chatId The ID of the chat where the video will be sent. + * @param replyToMessageId The ID of the message to which this video will be a reply. + * @param videoFile The video file to be sent. + * @return A SendVideo object configured with the chat ID, reply ID, and video file. + */ + private static SendVideo createSendVideoRequest(Long chatId, Integer replyToMessageId, File videoFile) { + SendVideo sendVideoRequest = new SendVideo(); + sendVideoRequest.setChatId(chatId.toString()); + sendVideoRequest.setReplyToMessageId(replyToMessageId); + sendVideoRequest.setVideo(new InputFile(videoFile)); + return sendVideoRequest; + } + + /** + * Builds the file path for the video file based on the given replyToMessageId. + * + * @param replyToMessageId The ID of the message to which this video will be a reply. + * @return The file path as a String. + */ + private static String buildFilePath(Integer replyToMessageId) { + return "./downloads/" + replyToMessageId + ".mp4"; + } + + +} diff --git a/code/src/main/java/org/segocode/bot/util/MessageUtil.java b/code/src/main/java/org/segocode/bot/util/MessageUtil.java new file mode 100644 index 0000000..66341ae --- /dev/null +++ b/code/src/main/java/org/segocode/bot/util/MessageUtil.java @@ -0,0 +1,12 @@ +package org.segocode.bot.util; + +public class MessageUtil { + public static String extractUrlFromMessage(String messageText) { + int startIndex = messageText.indexOf("http"); + int endIndex = messageText.indexOf(" ", startIndex); + if (endIndex == -1) { + endIndex = messageText.length(); + } + return messageText.substring(startIndex, endIndex); + } +} diff --git a/code/src/main/java/org/segocode/panel/PanelController.java b/code/src/main/java/org/segocode/panel/PanelController.java new file mode 100644 index 0000000..b68e848 --- /dev/null +++ b/code/src/main/java/org/segocode/panel/PanelController.java @@ -0,0 +1,62 @@ +package org.segocode.panel; + +import com.google.gson.Gson; +import io.javalin.Javalin; +import io.javalin.http.Context; +import org.eclipse.store.storage.embedded.types.EmbeddedStorageManager; +import org.segocode.bot.model.DataRootContainer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +public class PanelController { + private static final Logger LOGGER = LoggerFactory.getLogger(PanelController.class); + private final Javalin app; + private final EmbeddedStorageManager storageManager; + + // Singleton instance + private static PanelController instance; + + // Private constructor to prevent instantiation + private PanelController(EmbeddedStorageManager storageManager) { + this.storageManager = storageManager; + this.app = Javalin.create().start(8080); + + // Configure routes + configureRoutes(); + + LOGGER.info("Panel controller started on port 8080"); + } + + public static synchronized PanelController start(EmbeddedStorageManager storageManager) { + if (instance == null) { + instance = new PanelController(storageManager); + } + return instance; + } + + private void configureRoutes() { + app.get("/", this::handleAdminRequest); + } + +private void handleAdminRequest(Context ctx) { + ctx.contentType("text/html"); + + try { + Path htmlFilePath = Paths.get("src/main/java/org/segocode/panel/views/admin.html"); + String htmlContent = Files.readString(htmlFilePath); + String jsonData = new Gson().toJson(((DataRootContainer)storageManager.root()).getUsers()); + htmlContent = htmlContent.replace("{{!user_data}}", jsonData); + + ctx.result(htmlContent); + } catch (IOException e) { + LOGGER.error("Failed to read admin HTML file", e); + ctx.status(500); + ctx.result("Error loading admin panel: " + e.getMessage()); + } +} +} \ No newline at end of file diff --git a/code/src/main/java/org/segocode/panel/views/admin.html b/code/src/main/java/org/segocode/panel/views/admin.html new file mode 100644 index 0000000..9bd6981 --- /dev/null +++ b/code/src/main/java/org/segocode/panel/views/admin.html @@ -0,0 +1,250 @@ + + + + + + Bot Activity dashboard + + + + + +
+
+

User activity dashboard

+
Last updated:
+
+
+ + +
+ +
+ +
+
+
+ + + +
+
+

Total users

+

0

+
+
+
+
+
+
+ + + +
+
+

Total messages

+

0

+
+
+
+
+
+
+ + + +
+
+

Avg messages/user

+

0

+
+
+
+
+
+
+ + + +
+
+

Active languages

+

0

+
+
+
+
+ + +
+
+

Messages per user

+ +
+
+

Language distribution

+ +
+
+ + +
+

User activity

+
+
+ Show + + entries +
+ +
+
+ + + + + + + + + + + +
NameUsernameLanguageMessagesLast active
+
+
+
Showing 0 to 0 of 0 entries
+
+ + +
+
+
+
+ + + + + diff --git a/code/src/main/java/org/segocode/system/command/CommandExecutor.java b/code/src/main/java/org/segocode/system/command/CommandExecutor.java new file mode 100644 index 0000000..b7cc96a --- /dev/null +++ b/code/src/main/java/org/segocode/system/command/CommandExecutor.java @@ -0,0 +1,55 @@ +package org.segocode.system.command; + +import java.io.File; +import java.io.IOException; +import java.util.concurrent.TimeUnit; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class CommandExecutor { + private static final Logger LOGGER = LoggerFactory.getLogger(CommandExecutor.class); + private static final int MAX_RETRIES = 5; + private static final int TIMEOUT = 120; // seconds + + /** + * Executes a command in the system's command line to download a video using yt-dlp. + * + * @param url The URL of the video to be downloaded. + * @param uuid The unique identifier used to name the downloaded file. + * @throws IOException If an I/O error occurs. + * @throws InterruptedException If the current thread is interrupted while waiting. + */ + public static void executeCommand(String url, String uuid) throws Exception { + String osName = System.getProperty("os.name").toLowerCase(); + String ytDlpCommand = osName.contains("win") ? "yt-dlp.exe" : "yt-dlp"; // This is needed? + String outputPath = "." + File.separator + "downloads" + File.separator + uuid + ".%(ext)s"; + + // The -q option is important to prevent deadlocks by ensuring the output buffer is not filled. + // TODO: make a StreamGobbler to handle buffered output and avoid deadlocks. + String[] command = { ytDlpCommand, "-q", "-S", "ext,res:420", "-o", outputPath, url }; + + LOGGER.debug("Starting download command: {}", String.join(" ", command)); + + int attempt = 0; + while (attempt++ < MAX_RETRIES) { + LOGGER.info("Attempt {} of " + MAX_RETRIES, attempt); + try { + Process process = new ProcessBuilder(command).start(); + if (process.waitFor(TIMEOUT, TimeUnit.SECONDS) ? process.exitValue() == 0 : process.destroyForcibly() == null) { + LOGGER.info("Download successful for URL: {}", url); + return; + } else { + LOGGER.warn("Download attempt {} failed or timed out.", attempt); + process.destroyForcibly(); + } + } catch (IOException e) { + LOGGER.error("I/O error occurred on attempt {} of " + MAX_RETRIES, attempt, e); + if (attempt >= MAX_RETRIES) { + throw e; + } + } + } + LOGGER.error("Max retries reached. Command failed for URL: {}", url); + throw new RuntimeException("Max retries reached. Command failed."); + } +} From 2efabf458025fb31185cd959639e3f8a1a6e9ab7 Mon Sep 17 00:00:00 2001 From: SegoCode <35817798+SegoCode@users.noreply.github.com> Date: Thu, 13 Mar 2025 19:36:53 +0100 Subject: [PATCH 02/72] Update .gitignore --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 280d3be..8c11a6b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,8 @@ ### Custom ### code/.idea/* +code/storage/* +code/target/* # Created by https://www.toptal.com/developers/gitignore/api/git,gpg,ssh,vim,linux,macos,windows,notepadpp,sublimetext,intellij+all,visualstudiocode,dotenv # Edit at https://www.toptal.com/developers/gitignore?templates=git,gpg,ssh,vim,linux,macos,windows,notepadpp,sublimetext,intellij+all,visualstudiocode,dotenv From 647fbeeb32529f74286d0385ca1cc38c46c2a5bd Mon Sep 17 00:00:00 2001 From: SegoCode <35817798+SegoCode@users.noreply.github.com> Date: Thu, 13 Mar 2025 19:38:28 +0100 Subject: [PATCH 03/72] Update PanelController.java --- code/src/main/java/org/segocode/panel/PanelController.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/code/src/main/java/org/segocode/panel/PanelController.java b/code/src/main/java/org/segocode/panel/PanelController.java index b68e848..6ed6c3f 100644 --- a/code/src/main/java/org/segocode/panel/PanelController.java +++ b/code/src/main/java/org/segocode/panel/PanelController.java @@ -32,11 +32,10 @@ private PanelController(EmbeddedStorageManager storageManager) { LOGGER.info("Panel controller started on port 8080"); } - public static synchronized PanelController start(EmbeddedStorageManager storageManager) { + public static synchronized void start(EmbeddedStorageManager storageManager) { if (instance == null) { instance = new PanelController(storageManager); } - return instance; } private void configureRoutes() { From 9de2f6fad9ee8408dc3d8d9eef7e8d566d92c707 Mon Sep 17 00:00:00 2001 From: SegoCode <35817798+SegoCode@users.noreply.github.com> Date: Thu, 13 Mar 2025 19:50:19 +0100 Subject: [PATCH 04/72] Update PanelController.java --- code/src/main/java/org/segocode/panel/PanelController.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/code/src/main/java/org/segocode/panel/PanelController.java b/code/src/main/java/org/segocode/panel/PanelController.java index 6ed6c3f..b9b7f2d 100644 --- a/code/src/main/java/org/segocode/panel/PanelController.java +++ b/code/src/main/java/org/segocode/panel/PanelController.java @@ -9,6 +9,8 @@ import org.slf4j.LoggerFactory; import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; @@ -46,8 +48,8 @@ private void handleAdminRequest(Context ctx) { ctx.contentType("text/html"); try { - Path htmlFilePath = Paths.get("src/main/java/org/segocode/panel/views/admin.html"); - String htmlContent = Files.readString(htmlFilePath); + InputStream inputStream = getClass().getResourceAsStream("/org/segocode/panel/views/admin.html"); + String htmlContent = new String(inputStream.readAllBytes(), StandardCharsets.UTF_8); String jsonData = new Gson().toJson(((DataRootContainer)storageManager.root()).getUsers()); htmlContent = htmlContent.replace("{{!user_data}}", jsonData); From 59f6960eee185c9faf396c1961395cd398b6b307 Mon Sep 17 00:00:00 2001 From: SegoCode <35817798+SegoCode@users.noreply.github.com> Date: Thu, 13 Mar 2025 19:58:44 +0100 Subject: [PATCH 05/72] move views to resource folder --- code/src/main/java/org/segocode/panel/PanelController.java | 5 +---- .../{java/org/segocode/panel => resources}/views/admin.html | 0 2 files changed, 1 insertion(+), 4 deletions(-) rename code/src/main/{java/org/segocode/panel => resources}/views/admin.html (100%) diff --git a/code/src/main/java/org/segocode/panel/PanelController.java b/code/src/main/java/org/segocode/panel/PanelController.java index b9b7f2d..f8d6fc7 100644 --- a/code/src/main/java/org/segocode/panel/PanelController.java +++ b/code/src/main/java/org/segocode/panel/PanelController.java @@ -11,9 +11,6 @@ import java.io.IOException; import java.io.InputStream; import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; public class PanelController { private static final Logger LOGGER = LoggerFactory.getLogger(PanelController.class); @@ -48,7 +45,7 @@ private void handleAdminRequest(Context ctx) { ctx.contentType("text/html"); try { - InputStream inputStream = getClass().getResourceAsStream("/org/segocode/panel/views/admin.html"); + InputStream inputStream = getClass().getResourceAsStream("/views/admin.html"); String htmlContent = new String(inputStream.readAllBytes(), StandardCharsets.UTF_8); String jsonData = new Gson().toJson(((DataRootContainer)storageManager.root()).getUsers()); htmlContent = htmlContent.replace("{{!user_data}}", jsonData); diff --git a/code/src/main/java/org/segocode/panel/views/admin.html b/code/src/main/resources/views/admin.html similarity index 100% rename from code/src/main/java/org/segocode/panel/views/admin.html rename to code/src/main/resources/views/admin.html From 4302910ea94f6bed34f8262dcd8078f0c58fa8f0 Mon Sep 17 00:00:00 2001 From: SegoCode <35817798+SegoCode@users.noreply.github.com> Date: Thu, 13 Mar 2025 23:48:35 +0100 Subject: [PATCH 06/72] Create demo3.png --- media/demo3.png | Bin 0 -> 43722 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 media/demo3.png diff --git a/media/demo3.png b/media/demo3.png new file mode 100644 index 0000000000000000000000000000000000000000..322d0a28c8663754633ab179127ef032170fa675 GIT binary patch literal 43722 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 C4<=^- literal 0 HcmV?d00001 From 25cb168bf032e5cec25c0407527a11cc16169e00 Mon Sep 17 00:00:00 2001 From: SegoCode <35817798+SegoCode@users.noreply.github.com> Date: Sun, 23 Mar 2025 14:03:15 +0100 Subject: [PATCH 07/72] Update CommandExecutor.java --- code/src/main/java/org/segocode/system/CommandExecutor.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code/src/main/java/org/segocode/system/CommandExecutor.java b/code/src/main/java/org/segocode/system/CommandExecutor.java index 392501d..c656ca3 100644 --- a/code/src/main/java/org/segocode/system/CommandExecutor.java +++ b/code/src/main/java/org/segocode/system/CommandExecutor.java @@ -27,7 +27,7 @@ public static void executeCommand(String url, String uuid) throws Exception { // The --quiet option is important to prevent deadlocks by ensuring the output buffer is not filled. // TODO: make a StreamGobbler to handle buffered output and avoid deadlocks. - String[] command = { ytDlpCommand, "-q", "-S", "ext,res:420", "-o", outputPath, url }; + String[] command = { ytDlpCommand, "-q", "-S", "ext,res:720", "-o", outputPath, url }; LOGGER.debug("Starting download command: {}", String.join(" ", command)); From d2c1860eec9af7b89e7ba3a5695a5245c74d307b Mon Sep 17 00:00:00 2001 From: SegoCode <35817798+SegoCode@users.noreply.github.com> Date: Sun, 23 Mar 2025 14:03:16 +0100 Subject: [PATCH 08/72] Update admin.html --- code/src/main/resources/views/admin.html | 22 +--------------------- 1 file changed, 1 insertion(+), 21 deletions(-) diff --git a/code/src/main/resources/views/admin.html b/code/src/main/resources/views/admin.html index 9bd6981..dc5647b 100644 --- a/code/src/main/resources/views/admin.html +++ b/code/src/main/resources/views/admin.html @@ -100,19 +100,6 @@

Language distribution

User activity

-
-
- Show - - entries -
- -
@@ -127,13 +114,6 @@

User activity

-
-
Showing 0 to 0 of 0 entries
-
- - -
-
@@ -170,7 +150,7 @@

User activity

const lastActiveDate = new Date(user.lastMessageTime); row.innerHTML = ` - ${user.firstName} ${user.lastName} + ${user.firstName} ${user.lastName || ''} @${user.userName} ${user.languageCode.toUpperCase()} From 4c6cc5329686a0b4134959e7ff9930603a9e7fac Mon Sep 17 00:00:00 2001 From: SegoCode <35817798+SegoCode@users.noreply.github.com> Date: Tue, 25 Mar 2025 00:03:00 +0100 Subject: [PATCH 09/72] Update admin.html --- code/src/main/resources/views/admin.html | 28 +++++++++++++----------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/code/src/main/resources/views/admin.html b/code/src/main/resources/views/admin.html index dc5647b..e138291 100644 --- a/code/src/main/resources/views/admin.html +++ b/code/src/main/resources/views/admin.html @@ -3,7 +3,7 @@ - Bot Activity dashboard + Usage dashboard @@ -11,7 +11,7 @@
-

User activity dashboard

+

Usage dashboard

Last updated:
@@ -120,10 +120,11 @@

User activity

- + @@ -89,11 +89,11 @@

Active languages

Messages per user

- +

Language distribution

- +
@@ -173,29 +173,46 @@

User activity

function initCharts() { // Messages per User Chart - new Chart(document.getElementById('messagesPerUserChart'), { - type: 'bar', - data: { - labels: users.map(user => user.userName), - datasets: [{ - label: 'Number of messages', - data: users.map(user => user.messageCount), - backgroundColor: '#4F46E5', - borderRadius: 6 - }] + const messagesPerUserOptions = { + series: [{ + name: 'Messages', + data: users.map(user => user.messageCount) + }], + chart: { + type: 'bar', + height: 350, + toolbar: { + show: false + } }, - options: { - responsive: true, - plugins: { legend: { display: false } }, - scales: { - y: { - beginAtZero: true, - ticks: { stepSize: 1 }, - title: { display: true, text: 'Message Count' } + plotOptions: { + bar: { + borderRadius: 6, + dataLabels: { + position: 'top' } } + }, + colors: ['#4F46E5'], + xaxis: { + categories: users.map(user => user.userName), + position: 'bottom' + }, + yaxis: { + title: { + text: 'Message Count' + } + }, + dataLabels: { + enabled: false + }, + grid: { + borderColor: '#f3f4f6' } - }); + }; + + const messagesPerUserChart = new ApexCharts(document.querySelector("#messagesPerUserChart"), messagesPerUserOptions); + messagesPerUserChart.render(); // Language Distribution Chart const languageCounts = users.reduce((counts, user) => { @@ -203,29 +220,54 @@

User activity

return counts; }, {}); - new Chart(document.getElementById('languageChart'), { - type: 'bar', - data: { - labels: Object.keys(languageCounts).map(lang => lang.toUpperCase()), - datasets: [{ - label: 'Users per Language', - data: Object.values(languageCounts), - backgroundColor: '#4F46E5', - borderRadius: 6 - }] + const languageChartOptions = { + series: [{ + name: 'Users', + data: Object.values(languageCounts) + }], + chart: { + type: 'bar', + height: 350, + toolbar: { + show: false + } }, - options: { - responsive: true, - plugins: { legend: { display: false } }, - scales: { - y: { - beginAtZero: true, - ticks: { stepSize: 1 }, - title: { display: true, text: 'Number of users' } + plotOptions: { + bar: { + borderRadius: 6, + dataLabels: { + position: 'top' } } + }, + colors: ['#4F46E5'], + xaxis: { + categories: Object.keys(languageCounts).map(lang => lang.toUpperCase()), + position: 'bottom' + }, + yaxis: { + title: { + text: 'Number of users' + } + }, + dataLabels: { + enabled: true, + formatter: function (val) { + return val; + }, + offsetY: -20, + style: { + fontSize: '12px', + colors: ["#304758"] + } + }, + grid: { + borderColor: '#f3f4f6' } - }); + }; + + const languageChart = new ApexCharts(document.querySelector("#languageChart"), languageChartOptions); + languageChart.render(); } From 386aee8f5e5f1f2dbf0f04d629745fbb826fae64 Mon Sep 17 00:00:00 2001 From: SegoCode <35817798+SegoCode@users.noreply.github.com> Date: Fri, 28 Mar 2025 11:53:08 +0100 Subject: [PATCH 26/72] Update admin.html --- code/src/main/resources/views/admin.html | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/code/src/main/resources/views/admin.html b/code/src/main/resources/views/admin.html index 4d63dfa..245e1f5 100644 --- a/code/src/main/resources/views/admin.html +++ b/code/src/main/resources/views/admin.html @@ -8,14 +8,28 @@ + -
-
-

Usage dashboard

-
Last updated:
+
+
+
+
+ + + +
+

Usage Dashboard

+
+
+ + + + Last updated: +
+
From a7727be6672e1603e65e1f51b1a7c9bfed0f5ad3 Mon Sep 17 00:00:00 2001 From: SegoCode <35817798+SegoCode@users.noreply.github.com> Date: Fri, 28 Mar 2025 11:56:56 +0100 Subject: [PATCH 27/72] Update admin.html --- code/src/main/resources/views/admin.html | 34 ++++++++++++------------ 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/code/src/main/resources/views/admin.html b/code/src/main/resources/views/admin.html index 245e1f5..fd489f5 100644 --- a/code/src/main/resources/views/admin.html +++ b/code/src/main/resources/views/admin.html @@ -92,8 +92,8 @@

Avg messages/user

-

Active languages

-

0

+

Active Countrys

+

0

@@ -106,8 +106,8 @@

Messages per user

-

Language distribution

-
+

Country distribution

+
@@ -120,7 +120,7 @@

User activity

Name Username - Language + Country Messages Last active @@ -148,17 +148,17 @@

User activity

const totalUsers = users.length; const totalMessages = users.reduce((sum, user) => sum + user.messageCount, 0); const avgMessages = totalUsers > 0 ? Math.round(totalMessages / totalUsers) : 0; - const activeLanguages = new Set(users.map(u => u.languageCode)).size; + const activeCountrys = new Set(users.map(u => u.CountryCode)).size; const totalUsersElement = document.getElementById('totalUsers'); const totalMessagesElement = document.getElementById('totalMessages'); const avgMessagesElement = document.getElementById('avgMessages'); - const activeLanguagesElement = document.getElementById('activeLanguages'); + const activeCountrysElement = document.getElementById('activeCountrys'); if (totalUsersElement) totalUsersElement.textContent = totalUsers; if (totalMessagesElement) totalMessagesElement.textContent = totalMessages.toLocaleString(); if (avgMessagesElement) avgMessagesElement.textContent = avgMessages; - if (activeLanguagesElement) activeLanguagesElement.textContent = activeLanguages; + if (activeCountrysElement) activeCountrysElement.textContent = activeCountrys; } function initTable() { @@ -173,7 +173,7 @@

User activity

${user.firstName} ${user.lastName || ''} @${user.userName} - ${user.languageCode.toUpperCase()} + ${user.CountryCode.toUpperCase()} ${user.messageCount.toLocaleString()} @@ -228,16 +228,16 @@

User activity

const messagesPerUserChart = new ApexCharts(document.querySelector("#messagesPerUserChart"), messagesPerUserOptions); messagesPerUserChart.render(); - // Language Distribution Chart - const languageCounts = users.reduce((counts, user) => { - counts[user.languageCode] = (counts[user.languageCode] || 0) + 1; + // Country Distribution Chart + const CountryCounts = users.reduce((counts, user) => { + counts[user.CountryCode] = (counts[user.CountryCode] || 0) + 1; return counts; }, {}); - const languageChartOptions = { + const CountryChartOptions = { series: [{ name: 'Users', - data: Object.values(languageCounts) + data: Object.values(CountryCounts) }], chart: { type: 'bar', @@ -256,7 +256,7 @@

User activity

}, colors: ['#4F46E5'], xaxis: { - categories: Object.keys(languageCounts).map(lang => lang.toUpperCase()), + categories: Object.keys(CountryCounts).map(lang => lang.toUpperCase()), position: 'bottom' }, yaxis: { @@ -280,8 +280,8 @@

User activity

} }; - const languageChart = new ApexCharts(document.querySelector("#languageChart"), languageChartOptions); - languageChart.render(); + const CountryChart = new ApexCharts(document.querySelector("#CountryChart"), CountryChartOptions); + CountryChart.render(); } From f0e6405a06d5545f67634352f10e6cbfde67dac1 Mon Sep 17 00:00:00 2001 From: SegoCode <35817798+SegoCode@users.noreply.github.com> Date: Fri, 28 Mar 2025 12:16:56 +0100 Subject: [PATCH 28/72] Update admin.html --- code/src/main/resources/views/admin.html | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/code/src/main/resources/views/admin.html b/code/src/main/resources/views/admin.html index fd489f5..38eba18 100644 --- a/code/src/main/resources/views/admin.html +++ b/code/src/main/resources/views/admin.html @@ -148,7 +148,7 @@

User activity

const totalUsers = users.length; const totalMessages = users.reduce((sum, user) => sum + user.messageCount, 0); const avgMessages = totalUsers > 0 ? Math.round(totalMessages / totalUsers) : 0; - const activeCountrys = new Set(users.map(u => u.CountryCode)).size; + const activeCountrys = new Set(users.map(u => u.languageCode)).size; const totalUsersElement = document.getElementById('totalUsers'); const totalMessagesElement = document.getElementById('totalMessages'); @@ -173,7 +173,7 @@

User activity

${user.firstName} ${user.lastName || ''} @${user.userName} - ${user.CountryCode.toUpperCase()} + ${user.languageCode.toUpperCase()} ${user.messageCount.toLocaleString()} @@ -230,7 +230,7 @@

User activity

// Country Distribution Chart const CountryCounts = users.reduce((counts, user) => { - counts[user.CountryCode] = (counts[user.CountryCode] || 0) + 1; + counts[user.languageCode] = (counts[user.languageCode] || 0) + 1; return counts; }, {}); From 6f47b4d83967d4ac9c3b11619403406ee7943a53 Mon Sep 17 00:00:00 2001 From: SegoCode <35817798+SegoCode@users.noreply.github.com> Date: Fri, 28 Mar 2025 18:05:30 +0100 Subject: [PATCH 29/72] Update admin.html --- code/src/main/resources/views/admin.html | 34 +++--------------------- 1 file changed, 4 insertions(+), 30 deletions(-) diff --git a/code/src/main/resources/views/admin.html b/code/src/main/resources/views/admin.html index 38eba18..c4313f2 100644 --- a/code/src/main/resources/views/admin.html +++ b/code/src/main/resources/views/admin.html @@ -195,16 +195,10 @@

User activity

chart: { type: 'bar', height: 350, - toolbar: { - show: false - } }, plotOptions: { bar: { - borderRadius: 6, - dataLabels: { - position: 'top' - } + borderRadius: 10, } }, colors: ['#4F46E5'], @@ -220,9 +214,6 @@

User activity

dataLabels: { enabled: false }, - grid: { - borderColor: '#f3f4f6' - } }; const messagesPerUserChart = new ApexCharts(document.querySelector("#messagesPerUserChart"), messagesPerUserOptions); @@ -241,17 +232,11 @@

User activity

}], chart: { type: 'bar', - height: 350, - toolbar: { - show: false - } + height: 350 }, plotOptions: { bar: { - borderRadius: 6, - dataLabels: { - position: 'top' - } + borderRadius: 10, } }, colors: ['#4F46E5'], @@ -265,18 +250,7 @@

User activity

} }, dataLabels: { - enabled: true, - formatter: function (val) { - return val; - }, - offsetY: -20, - style: { - fontSize: '12px', - colors: ["#304758"] - } - }, - grid: { - borderColor: '#f3f4f6' + enabled: false } }; From 8e525ada142685b3a7d69d3ccf27f1714a0c48ef Mon Sep 17 00:00:00 2001 From: SegoCode <35817798+SegoCode@users.noreply.github.com> Date: Fri, 28 Mar 2025 18:17:55 +0100 Subject: [PATCH 30/72] Update admin.html --- code/src/main/resources/views/admin.html | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/code/src/main/resources/views/admin.html b/code/src/main/resources/views/admin.html index c4313f2..6ec62bf 100644 --- a/code/src/main/resources/views/admin.html +++ b/code/src/main/resources/views/admin.html @@ -206,11 +206,6 @@

User activity

categories: users.map(user => user.userName), position: 'bottom' }, - yaxis: { - title: { - text: 'Message Count' - } - }, dataLabels: { enabled: false }, @@ -244,11 +239,6 @@

User activity

categories: Object.keys(CountryCounts).map(lang => lang.toUpperCase()), position: 'bottom' }, - yaxis: { - title: { - text: 'Number of users' - } - }, dataLabels: { enabled: false } From 1978b079ac9ab00958c4971617e8056c345475b7 Mon Sep 17 00:00:00 2001 From: SegoCode <35817798+SegoCode@users.noreply.github.com> Date: Tue, 1 Apr 2025 12:13:58 +0200 Subject: [PATCH 31/72] Update update-license.yml --- .github/workflows/update-license.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/update-license.yml b/.github/workflows/update-license.yml index 98f06bd..43f5ac4 100644 --- a/.github/workflows/update-license.yml +++ b/.github/workflows/update-license.yml @@ -16,7 +16,7 @@ jobs: - name: Checkout repository uses: actions/checkout@v4 with: - ref: main + ref: develop - name: Setup git run: | @@ -55,5 +55,5 @@ jobs: # Only commit if there are changes git diff --staged --quiet || git commit -m "Update LICENSE with repository information" - # Push directly to main branch - git push origin main + # Push directly to develop branch + git push origin develop From d5166ef0451f7e508a43baa0320e2c6745589bf9 Mon Sep 17 00:00:00 2001 From: SegoCode <35817798+SegoCode@users.noreply.github.com> Date: Wed, 2 Apr 2025 17:12:21 +0200 Subject: [PATCH 32/72] Create demoPanel.png --- media/demoPanel.png | Bin 0 -> 43183 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 media/demoPanel.png diff --git a/media/demoPanel.png b/media/demoPanel.png new file mode 100644 index 0000000000000000000000000000000000000000..59ff34302015b70f37d6591f3b596c7586758e57 GIT binary patch literal 43183 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 W*t>Q5P+fJjt8_H)LMm_DKKVanZDWf7 literal 0 HcmV?d00001 From 8960170d68c9b25b9070b835ca7b9b644d2bec57 Mon Sep 17 00:00:00 2001 From: SegoCode <35817798+SegoCode@users.noreply.github.com> Date: Wed, 2 Apr 2025 17:12:23 +0200 Subject: [PATCH 33/72] Update README.md --- README.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 95e5f8f..3aa2210 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ # webdl +

About • @@ -18,6 +19,8 @@ Telegram bot in Java for downloading social media videos using yt-dlp - Dynamic interaction with messages +- Panel with usage statistics + ## Quick Start & Information Webdl accepts a video URL, downloads it using [yt-dlp](https://github.com/yt-dlp/yt-dlp), and sends it back to the user as a video message. @@ -35,8 +38,11 @@ Use a temp volume for the download, it will delete after send. ``` mvn clean package docker build -t webdl-image . -docker run -e BOT_TOKEN=your-bot-token -v /mnt/drive/data/webdl:/downloads --name webdl webdl-image +docker run -e BOT_TOKEN=your-bot-token -p 8080:8080 -v /mnt/drive/data/webdl:/downloads --name webdl webdl-image ``` + + + ---

From cd36ca87d9abe51cb14b3d4593ba0d55c4ea23a4 Mon Sep 17 00:00:00 2001 From: github-actions Date: Wed, 2 Apr 2025 15:13:55 +0000 Subject: [PATCH 34/72] Update LICENSE with repository information --- LICENSE | 150 ++++++++++++++++++++++++++++---------------------------- 1 file changed, 75 insertions(+), 75 deletions(-) diff --git a/LICENSE b/LICENSE index 43ad7db..6ab3d8e 100644 --- a/LICENSE +++ b/LICENSE @@ -1,75 +1,75 @@ -Copyright (c) SegoCode -All rights reserved. - -Section 1 - Definitions - -1.1 "NonCommercial" pertains to any use, distribution, or modification of the - licensed material that does not primarily aim to achieve commercial - advantage or generate monetary compensation. This includes, but is not - limited to, activities such as distributing the licensed material as part - of an application or product that is sold, using the licensed material in - advertising, and creating products or services with the licensed material - that are subsequently sold. - -1.2 "Adapted Material" refers to any work derived from or based upon the licensed - material. This includes, but is not limited to, translations, alterations, - rearrangements, transformations, source code modifications, compiled code - alterations, architectural redesigns, or the integration of the licensed - material into other software projects. - -1.3 "NonAdapted Material" refers to exact copies of the licensed material, either - in source code or binary form, which are reproduced without any changes, - modifications, or transformations. - -Section 2 - License Conditions - -2.1 Distribution and usage in source or binary forms are permitted solely for - NonCommercial purposes for both NonAdapted Material and Adapted Material - excluding NonAdapted Material cases in 2.4 section. - -2.2 Redistributions for any NonAdapted Material or Adapted Material, either - in source code or binary form, must include the original copyright notice, - this license, the disclaimer, and comply with the requirements specified - herein. For binary form redistributions, these documents must be included - in any provided documentation or materials or a clearly accessible link - to this license ensuring that recipients can easily review the license terms. - -2.3 For any Adapted Material, whether in source code or binary form, and for any - instance of the licensed material utilized in applications accessible over - the internet that run on a server, the source code must be made accessible - through a public repository, a downloadable archive, or an equivalent - method, ensuring that users have the capability to access, review, and - download the code, and must clearly credit the original work and author. - -2.4 For any NonAdapted Material that are offered on download sites, marketplaces, or - software distribution platforms, are permitted under the condition that any - economic benefit derived from such distribution is strictly indirect, including - but not limited to advertisements or link redirectors. Direct sales or charges - for access to the licensed material are not permitted under this license. Upon - the original author distributing the material on the same platform, all alternative - downloads and any associated revenue-generating mechanisms must be discontinued - immediately. Acceptance of this license constitutes agreement that the copyright - holder may request the immediate cessation of such downloads and activities on - such platforms, and the distributor must comply with such request without delay. - -2.5 Distributing the licensed material alongside unrelated or harmful software, such as adware, - malware, or spyware or implying endorsement or association with the original author - or recognized entities without permission, is prohibited. - -Section 3 - Updates and Revisions - -3.1 The copyright holder retains the right to amend, update, or otherwise modify this - license at any time without prior notice. Users should periodically review the - license terms to stay informed of any changes. - -3.2 Continued use of the licensed material after such changes signifies acceptance of the - revised license terms. It is the user's responsibility to ensure ongoing compliance - with the most current version of the license. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +Copyright (c) SegoCode +All rights reserved. + +Section 1 - Definitions + +1.1 "NonCommercial" pertains to any use, distribution, or modification of the + licensed material that does not primarily aim to achieve commercial + advantage or generate monetary compensation. This includes, but is not + limited to, activities such as distributing the licensed material as part + of an application or product that is sold, using the licensed material in + advertising, and creating products or services with the licensed material + that are subsequently sold. + +1.2 "Adapted Material" refers to any work derived from or based upon the licensed + material. This includes, but is not limited to, translations, alterations, + rearrangements, transformations, source code modifications, compiled code + alterations, architectural redesigns, or the integration of the licensed + material into other software projects. + +1.3 "NonAdapted Material" refers to exact copies of the licensed material, either + in source code or binary form, which are reproduced without any changes, + modifications, or transformations. + +Section 2 - License Conditions + +2.1 Distribution and usage in source or binary forms are permitted solely for + NonCommercial purposes for both NonAdapted Material and Adapted Material + excluding NonAdapted Material cases in 2.4 section. + +2.2 Redistributions for any NonAdapted Material or Adapted Material, either + in source code or binary form, must include the original copyright notice, + this license, the disclaimer, and comply with the requirements specified + herein. For binary form redistributions, these documents must be included + in any provided documentation or materials or a clearly accessible link + to this license ensuring that recipients can easily review the license terms. + +2.3 For any Adapted Material, whether in source code or binary form, and for any + instance of the licensed material utilized in applications accessible over + the internet that run on a server, the source code must be made accessible + through a public repository, a downloadable archive, or an equivalent + method, ensuring that users have the capability to access, review, and + download the code, and must clearly credit the original work and author. + +2.4 For any NonAdapted Material that are offered on download sites, marketplaces, or + software distribution platforms, are permitted under the condition that any + economic benefit derived from such distribution is strictly indirect, including + but not limited to advertisements or link redirectors. Direct sales or charges + for access to the licensed material are not permitted under this license. Upon + the original author distributing the material on the same platform, all alternative + downloads and any associated revenue-generating mechanisms must be discontinued + immediately. Acceptance of this license constitutes agreement that the copyright + holder may request the immediate cessation of such downloads and activities on + such platforms, and the distributor must comply with such request without delay. + +2.5 Distributing the licensed material alongside unrelated or harmful software, such as adware, + malware, or spyware or implying endorsement or association with the original author + or recognized entities without permission, is prohibited. + +Section 3 - Updates and Revisions + +3.1 The copyright holder retains the right to amend, update, or otherwise modify this + license at any time without prior notice. Users should periodically review the + license terms to stay informed of any changes. + +3.2 Continued use of the licensed material after such changes signifies acceptance of the + revised license terms. It is the user's responsibility to ensure ongoing compliance + with the most current version of the license. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. From bbed8b24a2cdc7bb4d98efb61397058e347edfdd Mon Sep 17 00:00:00 2001 From: SegoCode <35817798+SegoCode@users.noreply.github.com> Date: Wed, 2 Apr 2025 17:18:21 +0200 Subject: [PATCH 35/72] Update demoPanel.png --- media/demoPanel.png | Bin 43183 -> 42605 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/media/demoPanel.png b/media/demoPanel.png index 59ff34302015b70f37d6591f3b596c7586758e57..e84ee6fbfb439f192f3484f2bce8d84c7e776057 100644 GIT binary patch literal 42605 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 W*t>Q5P+fJjt8_H)LMm_DKKVanZDWf7 From 757bf401fa794b454f4695f7ce79d2c0cdd544af Mon Sep 17 00:00:00 2001 From: SegoCode <35817798+SegoCode@users.noreply.github.com> Date: Wed, 2 Apr 2025 17:20:11 +0200 Subject: [PATCH 36/72] Delete demo3.png --- media/demo3.png | Bin 43722 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 media/demo3.png diff --git a/media/demo3.png b/media/demo3.png deleted file mode 100644 index 322d0a28c8663754633ab179127ef032170fa675..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 43722 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 C4<=^- From ec037182a34b552e87757fcb1c73a8bceb22ff3a Mon Sep 17 00:00:00 2001 From: SegoCode <35817798+SegoCode@users.noreply.github.com> Date: Fri, 23 May 2025 17:52:08 +0200 Subject: [PATCH 37/72] Simplify html view --- code/src/main/resources/views/admin.html | 152 ++++++----------------- 1 file changed, 35 insertions(+), 117 deletions(-) diff --git a/code/src/main/resources/views/admin.html b/code/src/main/resources/views/admin.html index 6ec62bf..77486dd 100644 --- a/code/src/main/resources/views/admin.html +++ b/code/src/main/resources/views/admin.html @@ -5,7 +5,6 @@ Usage dashboard - @@ -14,37 +13,31 @@

-
-
- +
+
- - + +
@@ -53,11 +46,13 @@

Total users

+ +
- - + +
@@ -66,16 +61,13 @@

Total messages

+ +
- - + +
@@ -84,33 +76,23 @@

Avg messages/user

+ +
- - + +
-

Active Countrys

-

0

+

Active Countries

+

0

- -
-
-

Messages per user

-
-
-
-

Country distribution

-
-
-
-

User activity

@@ -134,31 +116,30 @@

User activity

From ccb797c6889762a9d1de992617cf6817630ff76c Mon Sep 17 00:00:00 2001 From: SegoCode <35817798+SegoCode@users.noreply.github.com> Date: Tue, 9 Jun 2026 17:44:17 +0200 Subject: [PATCH 38/72] Update to latest local state --- code/Dockerfile | 12 +- .../main/java/org/segocode/bot/Webdlbot.java | 15 +- .../segocode/bot/service/VideoService.java | 3 + .../org/segocode/panel/PanelController.java | 5 +- .../org/segocode/service/MessageService.java | 32 - .../org/segocode/service/VideoService.java | 62 -- .../org/segocode/system/util/FileUtil.java | 18 - code/src/main/resources/views/admin.html | 626 ++++++++++++++---- 8 files changed, 517 insertions(+), 256 deletions(-) delete mode 100644 code/src/main/java/org/segocode/service/MessageService.java delete mode 100644 code/src/main/java/org/segocode/service/VideoService.java diff --git a/code/Dockerfile b/code/Dockerfile index 7121837..63189a7 100644 --- a/code/Dockerfile +++ b/code/Dockerfile @@ -8,17 +8,11 @@ COPY src ./src RUN mvn package -DskipTests # Runtime stage: Run the application -FROM alpine:latest +FROM eclipse-temurin:21-jre-alpine -RUN apk update && \ - apk add --no-cache wget curl ffmpeg && \ +RUN apk add --no-cache ffmpeg python3 && \ wget -O /usr/local/bin/yt-dlp https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp && \ - chmod a+rx /usr/local/bin/yt-dlp && \ - rm -rf /var/cache/apk/* - -RUN apk add --no-cache openjdk21 python3 py3-pip - -ENV YT_DLP=/usr/bin/yt-dlp PYTHONUNBUFFERED=1 + chmod a+rx /usr/local/bin/yt-dlp WORKDIR /app COPY --from=build /app/target/webdl.jar /app/webdl.jar diff --git a/code/src/main/java/org/segocode/bot/Webdlbot.java b/code/src/main/java/org/segocode/bot/Webdlbot.java index 11e65f1..f70d0d8 100644 --- a/code/src/main/java/org/segocode/bot/Webdlbot.java +++ b/code/src/main/java/org/segocode/bot/Webdlbot.java @@ -70,9 +70,10 @@ public String getBotToken() { public void onUpdateReceived(Update update) { if (update.hasMessage() && update.getMessage().hasText()) { try { + long queuedTasks = executorService.getTaskCount() - executorService.getCompletedTaskCount(); Integer queuedMessageId; - if (executorService.getActiveCount() > 0 || !executorService.getQueue().isEmpty()) { - String messageTime = DOWNLOAD_REQUEST_QUEUED + " (<" + executorService.getQueue().size() + "m)"; + if (queuedTasks > 0) { + String messageTime = DOWNLOAD_REQUEST_QUEUED + " (<" + queuedTasks + "m)"; LOGGER.info("Executor is busy, queuing the message from @{}", update.getMessage().getFrom().getUserName()); queuedMessageId = execute(MessageService.sendTextMessage(update.getMessage().getChatId(), update.getMessage().getMessageId(), messageTime)).getMessageId(); } else { @@ -89,14 +90,16 @@ public void onUpdateReceived(Update update) { } catch (Exception e) { LOGGER.error("Failed to launch dispatch, error: {}", e.getMessage(), e); handleDispatchError(update, e); + } finally { + synchronized (storageManager) { + storageManager.setRoot(loadMetricsData(update)); + storageManager.storeRoot(); + } } }); } catch (Exception e) { LOGGER.error("Failed on onUpdateReceived, error: {}", e.getMessage(), e); handleDispatchError(update, e); - } finally { - storageManager.setRoot(loadMetricsData(update)); - storageManager.storeRoot(); } } } @@ -106,7 +109,7 @@ private void dispatch(Update update) throws Exception { Message message = update.getMessage(); LOGGER.info("Starting message processing from @{}: {}", message.getFrom().getUserName(), message.getText()); - if(!update.getMessage().toString().contains("http")) { + if(!update.getMessage().getText().contains("http")) { execute(MessageService.sendTextMessage(message.getChatId(), message.getMessageId(), NOT_VALID_LINK)).getMessageId(); return; } else { diff --git a/code/src/main/java/org/segocode/bot/service/VideoService.java b/code/src/main/java/org/segocode/bot/service/VideoService.java index 79d853b..5a0064b 100644 --- a/code/src/main/java/org/segocode/bot/service/VideoService.java +++ b/code/src/main/java/org/segocode/bot/service/VideoService.java @@ -24,6 +24,9 @@ public static SendVideo sendVideo(Long chatId, Integer replyToMessageId) { LOGGER.info("Locating video file for message ID {}: {}", replyToMessageId, filePath); File videoFile = FileUtil.locateVideoFile(filePath); + if (videoFile == null) { + throw new RuntimeException("Video file not found for message ID: " + replyToMessageId); + } LOGGER.info("Video file located successfully: {}", videoFile.getPath()); SendVideo sendVideoRequest = createSendVideoRequest(chatId, replyToMessageId, videoFile); diff --git a/code/src/main/java/org/segocode/panel/PanelController.java b/code/src/main/java/org/segocode/panel/PanelController.java index f8d6fc7..f090588 100644 --- a/code/src/main/java/org/segocode/panel/PanelController.java +++ b/code/src/main/java/org/segocode/panel/PanelController.java @@ -47,7 +47,10 @@ private void handleAdminRequest(Context ctx) { try { InputStream inputStream = getClass().getResourceAsStream("/views/admin.html"); String htmlContent = new String(inputStream.readAllBytes(), StandardCharsets.UTF_8); - String jsonData = new Gson().toJson(((DataRootContainer)storageManager.root()).getUsers()); + String jsonData; + synchronized (storageManager) { + jsonData = new Gson().toJson(((DataRootContainer)storageManager.root()).getUsers()); + } htmlContent = htmlContent.replace("{{!user_data}}", jsonData); ctx.result(htmlContent); diff --git a/code/src/main/java/org/segocode/service/MessageService.java b/code/src/main/java/org/segocode/service/MessageService.java deleted file mode 100644 index fc53fbd..0000000 --- a/code/src/main/java/org/segocode/service/MessageService.java +++ /dev/null @@ -1,32 +0,0 @@ -package org.segocode.service; - -import org.telegram.telegrambots.meta.api.methods.send.SendMessage; -import org.telegram.telegrambots.meta.api.methods.updatingmessages.DeleteMessage; - -public class MessageService { - - /** - * Sends a text message to a specific chat in Telegram. - * - * @param chatId The ID of the chat where the message will be sent. - * @param replyToMessageId The ID of the message to which this message will be a reply. - * @param text The text content of the message to be sent. - * @return The SendMessage object configured with the chat ID, text, and reply to message ID. - */ - public static SendMessage sendTextMessage(Long chatId, Integer replyToMessageId, String text) { - SendMessage message = new SendMessage(chatId.toString(), text); - message.setReplyToMessageId(replyToMessageId); - return message; - } - - /** - * Deletes a specific message in a Telegram chat. - * - * @param chatId The ID of the chat where the message is located. - * @param messageId The ID of the message to be deleted. - * @return A DeleteMessage object configured with the chat ID and message ID. - */ - public static DeleteMessage deleteMessage(Long chatId, Integer messageId) { - return new DeleteMessage(chatId.toString(), messageId); - } -} diff --git a/code/src/main/java/org/segocode/service/VideoService.java b/code/src/main/java/org/segocode/service/VideoService.java deleted file mode 100644 index 88c1b94..0000000 --- a/code/src/main/java/org/segocode/service/VideoService.java +++ /dev/null @@ -1,62 +0,0 @@ -package org.segocode.service; - -import org.segocode.system.util.FileUtil; -import org.slf4j.LoggerFactory; -import org.telegram.telegrambots.meta.api.methods.send.SendVideo; -import org.telegram.telegrambots.meta.api.objects.InputFile; - -import java.io.File; -import org.slf4j.Logger; - - -public class VideoService { - private static final Logger LOGGER = LoggerFactory.getLogger(VideoService.class); - - /** - * Sends a video to a specific chat in Telegram. - * - * @param chatId The ID of the chat where the video will be sent. - * @param replyToMessageId The ID of the message to which this video will be a reply. - * @return The SendVideo object configured with the chat ID and reply to message ID. - */ - public static SendVideo sendVideo(Long chatId, Integer replyToMessageId) { - String filePath = buildFilePath(replyToMessageId); - LOGGER.info("Locating video file for message ID {}: {}", replyToMessageId, filePath); - - File videoFile = FileUtil.locateVideoFile(filePath); - LOGGER.info("Video file located successfully: {}", videoFile.getPath()); - - SendVideo sendVideoRequest = createSendVideoRequest(chatId, replyToMessageId, videoFile); - LOGGER.info("Video is sending. Chat ID: {}, Reply to Message ID: {}", chatId, replyToMessageId); - - return sendVideoRequest; - } - - /** - * Creates a SendVideo request for sending a video. - * - * @param chatId The ID of the chat where the video will be sent. - * @param replyToMessageId The ID of the message to which this video will be a reply. - * @param videoFile The video file to be sent. - * @return A SendVideo object configured with the chat ID, reply ID, and video file. - */ - private static SendVideo createSendVideoRequest(Long chatId, Integer replyToMessageId, File videoFile) { - SendVideo sendVideoRequest = new SendVideo(); - sendVideoRequest.setChatId(chatId.toString()); - sendVideoRequest.setReplyToMessageId(replyToMessageId); - sendVideoRequest.setVideo(new InputFile(videoFile)); - return sendVideoRequest; - } - - /** - * Builds the file path for the video file based on the given replyToMessageId. - * - * @param replyToMessageId The ID of the message to which this video will be a reply. - * @return The file path as a String. - */ - private static String buildFilePath(Integer replyToMessageId) { - return "./downloads/" + replyToMessageId + ".mp4"; - } - - -} diff --git a/code/src/main/java/org/segocode/system/util/FileUtil.java b/code/src/main/java/org/segocode/system/util/FileUtil.java index 4038d69..fb2cc6d 100644 --- a/code/src/main/java/org/segocode/system/util/FileUtil.java +++ b/code/src/main/java/org/segocode/system/util/FileUtil.java @@ -13,24 +13,6 @@ public class FileUtil { private static final Logger LOGGER = LoggerFactory.getLogger(FileUtil.class); - /** - * Deletes a specific file based on the provided ID. - * - * @param id the identifier of the file to delete - */ - // Currently, since the queue is for a single user at a time,we do not need to delete by ID. - public static void deleteFileById(int id) { - - Path path = Paths.get("./downloads/" + id + ".mp4"); - try { - boolean fileDeleted = Files.deleteIfExists(path); - LOGGER.info(fileDeleted ? "File successfully deleted: " + path : "File not found: " + path); - } catch (Exception e) { - LOGGER.error("Error deleting the file: {}", path, e); - cleanDownloadsFolder(); - } - } - /** * Deletes all files and folders within the 'downloads' folder. */ diff --git a/code/src/main/resources/views/admin.html b/code/src/main/resources/views/admin.html index 77486dd..11f5fd8 100644 --- a/code/src/main/resources/views/admin.html +++ b/code/src/main/resources/views/admin.html @@ -3,168 +3,538 @@ - Usage dashboard - + Dashboard + + + + - - - -
-
-
-
- - - -
-

Usage Dashboard

+ +
+
+
+

DASHBOARD

+

Bot activity & user statistics

-
- - - - Last updated: +
+
PANEL
+
UPDATED --
+
-
-
- - -
- -
- -
-
-
- - - -
-
-

Total users

-

0

-
-
+ + +
+
+

Total messages processed

+

0

- - -
-
-
- - - -
-
-

Total messages

-

0

-
+
+
+

Total users

+

0

-
- - -
-
-
- - - -
-
-

Avg messages/user

-

0

-
+
+

Average / user

+

0

-
- - -
-
-
- - - -
-
-

Active Countries

-

0

-
+
+

Active languages

+

0

-
- - -
-

User activity

-
- - - - - - - - - - - -
NameUsernameCountryMessagesLast active
+
+ +
+
+

Most active user

+

0

+
+
+
+

Recent activity (24h)

+

0

+
+
+
+

Active users (7d)

+

0

+
+
+
+ +
+
+

User activity table

+

Ordered by message volume

-
+ + + + + + + + + + + +
NameUsernameLanguageMessagesLast active
+
- From 1640689e8c074f9c5a8bdc7d4624fc87b1f37628 Mon Sep 17 00:00:00 2001 From: SegoCode <35817798+SegoCode@users.noreply.github.com> Date: Thu, 11 Jun 2026 08:47:03 +0200 Subject: [PATCH 39/72] Update admin.html --- code/src/main/resources/views/admin.html | 83 ------------------------ 1 file changed, 83 deletions(-) diff --git a/code/src/main/resources/views/admin.html b/code/src/main/resources/views/admin.html index 11f5fd8..d5a472c 100644 --- a/code/src/main/resources/views/admin.html +++ b/code/src/main/resources/views/admin.html @@ -179,51 +179,6 @@ font-weight: 500; } - .grid { - display: grid; - grid-template-columns: repeat(3, minmax(0, 1fr)); - gap: 12px; - margin-bottom: 26px; - } - - .stat { - border: 1px solid var(--line); - background: var(--surface); - padding: 16px; - } - - .stat-title { - margin: 0; - color: var(--text-secondary); - font-size: 0.7rem; - text-transform: uppercase; - letter-spacing: 0.14em; - font-family: "Space Mono", monospace; - } - - .stat-number { - margin: 11px 0 0; - color: var(--text-display); - font-size: 2rem; - font-weight: 700; - line-height: 1; - } - - .stat-bar { - margin-top: 12px; - height: 5px; - background: var(--line); - position: relative; - overflow: hidden; - } - - .stat-bar > span { - display: block; - height: 100%; - width: 0; - background: var(--text-display); - } - .table-wrap { border: 1px solid var(--line); background: var(--surface-2); @@ -312,10 +267,6 @@ border-right: 0; border-bottom: 1px solid var(--line); } - - .grid { - grid-template-columns: repeat(2, minmax(0, 1fr)); - } } @media (max-width: 640px) { @@ -332,10 +283,6 @@ .meta { align-items: flex-start; } - - .grid { - grid-template-columns: 1fr; - } } @@ -374,24 +321,6 @@

DASHBOARD

-
-
-

Most active user

-

0

-
-
-
-

Recent activity (24h)

-

0

-
-
-
-

Active users (7d)

-

0

-
-
-
-

User activity table

@@ -433,22 +362,10 @@

User activity table

const avgMessages = totalUsers > 0 ? Math.round(totalMessages / totalUsers) : 0; const activeCountries = new Set(users.map((u) => (u.languageCode || "").trim()).filter(Boolean)).size; - const sorted = [...users].sort((a, b) => safeNumber(b.messageCount) - safeNumber(a.messageCount)); - const topUserMessages = sorted.length > 0 ? safeNumber(sorted[0].messageCount) : 0; - const users24h = users.filter((u) => isWithinHours(u.lastMessageTime, 24)).length; - const users7d = users.filter((u) => isWithinHours(u.lastMessageTime, 24 * 7)).length; - setText("totalUsers", formatNumber(totalUsers)); setText("totalMessages", formatNumber(totalMessages)); setText("avgMessages", formatNumber(avgMessages)); setText("activeCountries", formatNumber(activeCountries)); - setText("topUserMessages", formatNumber(topUserMessages)); - setText("recentUsers", formatNumber(users24h)); - setText("weeklyUsers", formatNumber(users7d)); - - updateBar("topUserBar", topUserMessages, topUserMessages || 1); - updateBar("recentUsersBar", users24h, totalUsers || 1); - updateBar("weeklyUsersBar", users7d, totalUsers || 1); } function initTable() { From 99b39367c08bd167284c3f0f80fb22b2a43cd943 Mon Sep 17 00:00:00 2001 From: SegoCode <35817798+SegoCode@users.noreply.github.com> Date: Thu, 11 Jun 2026 09:10:58 +0200 Subject: [PATCH 40/72] update structure --- code/src/main/java/org/segocode/Main.java | 33 ---- .../main/java/org/segocode/bot/Webdlbot.java | 161 ------------------ .../org/segocode/bot/constants/Messages.java | 8 - .../segocode/bot/model/DataRootContainer.java | 28 --- .../java/org/segocode/bot/model/User.java | 40 ----- .../segocode/bot/service/MessageService.java | 32 ---- .../segocode/bot/service/VideoService.java | 65 ------- .../org/segocode/bot/util/MessageUtil.java | 12 -- .../org/segocode/panel/PanelController.java | 63 ------- .../system/command/CommandExecutor.java | 53 ------ .../org/segocode/system/util/FileUtil.java | 56 ------ 11 files changed, 551 deletions(-) delete mode 100644 code/src/main/java/org/segocode/Main.java delete mode 100644 code/src/main/java/org/segocode/bot/Webdlbot.java delete mode 100644 code/src/main/java/org/segocode/bot/constants/Messages.java delete mode 100644 code/src/main/java/org/segocode/bot/model/DataRootContainer.java delete mode 100644 code/src/main/java/org/segocode/bot/model/User.java delete mode 100644 code/src/main/java/org/segocode/bot/service/MessageService.java delete mode 100644 code/src/main/java/org/segocode/bot/service/VideoService.java delete mode 100644 code/src/main/java/org/segocode/bot/util/MessageUtil.java delete mode 100644 code/src/main/java/org/segocode/panel/PanelController.java delete mode 100644 code/src/main/java/org/segocode/system/command/CommandExecutor.java delete mode 100644 code/src/main/java/org/segocode/system/util/FileUtil.java diff --git a/code/src/main/java/org/segocode/Main.java b/code/src/main/java/org/segocode/Main.java deleted file mode 100644 index 0b4b414..0000000 --- a/code/src/main/java/org/segocode/Main.java +++ /dev/null @@ -1,33 +0,0 @@ -package org.segocode; - -import io.javalin.Javalin; -import org.eclipse.store.storage.embedded.types.EmbeddedStorage; -import org.eclipse.store.storage.embedded.types.EmbeddedStorageManager; -import org.eclipse.store.storage.types.StorageManager; -import org.segocode.bot.Webdlbot; -import org.segocode.panel.PanelController; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.telegram.telegrambots.meta.TelegramBotsApi; -import org.telegram.telegrambots.updatesreceivers.DefaultBotSession; - -import java.util.Date; - -public class Main { - private static final Logger LOGGER = LoggerFactory.getLogger(Main.class); - - public static void main(String[] args) { - try { - LOGGER.info("Starting storage manager..."); - final EmbeddedStorageManager storageManager = EmbeddedStorage.start(); - TelegramBotsApi botsApi = new TelegramBotsApi(DefaultBotSession.class); - LOGGER.info("Starting the video download bot..."); - botsApi.registerBot(new Webdlbot(storageManager)); - LOGGER.info("Bot started successfully and ready to download videos 🚀"); - LOGGER.info("Starting webp anel app..."); - PanelController.start(storageManager); - } catch (Exception e) { - LOGGER.error("Error while attempting to start the bot. Error details:", e); - } - } -} diff --git a/code/src/main/java/org/segocode/bot/Webdlbot.java b/code/src/main/java/org/segocode/bot/Webdlbot.java deleted file mode 100644 index f70d0d8..0000000 --- a/code/src/main/java/org/segocode/bot/Webdlbot.java +++ /dev/null @@ -1,161 +0,0 @@ -package org.segocode.bot; - -import org.eclipse.store.storage.types.StorageManager; -import org.segocode.bot.model.DataRootContainer; -import org.segocode.bot.model.User; -import org.segocode.system.command.CommandExecutor; -import org.segocode.bot.service.VideoService; -import org.slf4j.LoggerFactory; -import org.telegram.telegrambots.bots.TelegramLongPollingBot; -import org.telegram.telegrambots.meta.api.objects.Message; -import org.telegram.telegrambots.meta.api.objects.Update; -import org.segocode.bot.service.MessageService; -import org.slf4j.Logger; -import org.telegram.telegrambots.meta.exceptions.TelegramApiException; - -import java.time.LocalDateTime; -import java.util.Optional; -import java.util.concurrent.*; - -import static org.segocode.bot.constants.Messages.*; -import static org.segocode.bot.util.MessageUtil.*; -import static org.segocode.system.util.FileUtil.*; - -public class Webdlbot extends TelegramLongPollingBot { - private static final Logger LOGGER = LoggerFactory.getLogger(Webdlbot.class); - private static final String BOT_TOKEN = System.getenv("BOT_TOKEN"); - - private final StorageManager storageManager; - - // Create a ThreadPoolExecutor with a single virtual thread - private final ThreadPoolExecutor executorService = new ThreadPoolExecutor( - 1, // corePoolSize: the number of threads to keep in the pool, even if they are idle - 1, // maximumPoolSize: the maximum number of threads to allow in the pool - 0L, // keepAliveTime: when the number of threads is greater than the core - TimeUnit.MILLISECONDS, // the time unit for the keepAliveTime argument - new LinkedBlockingQueue<>(), // the queue to use for holding tasks before they are executed - Thread.ofVirtual().factory() // the factory to use when creating new threads - ); - - public Webdlbot(StorageManager storageManager) { - this.storageManager = storageManager; - initializeRootContainer(); - } - - private void initializeRootContainer() { - Object root = storageManager.root(); - if (!(root instanceof DataRootContainer)) { - storageManager.setRoot(new DataRootContainer()); - LOGGER.info("Initialized new DataRootContainer as db root object"); - } - LOGGER.info("Root db object contains {} users", ((DataRootContainer) storageManager.root()).getUsers().size()); - } - - @Override - public String getBotUsername() { - return "webdl"; - } - - @Override - public String getBotToken() { - if (BOT_TOKEN == null || BOT_TOKEN.isEmpty()) { - LOGGER.error("BOT_TOKEN is not set in the environment variables."); - throw new IllegalStateException("BOT_TOKEN is not set in the environment variables."); - } - return BOT_TOKEN; - } - - - @Override - public void onUpdateReceived(Update update) { - if (update.hasMessage() && update.getMessage().hasText()) { - try { - long queuedTasks = executorService.getTaskCount() - executorService.getCompletedTaskCount(); - Integer queuedMessageId; - if (queuedTasks > 0) { - String messageTime = DOWNLOAD_REQUEST_QUEUED + " (<" + queuedTasks + "m)"; - LOGGER.info("Executor is busy, queuing the message from @{}", update.getMessage().getFrom().getUserName()); - queuedMessageId = execute(MessageService.sendTextMessage(update.getMessage().getChatId(), update.getMessage().getMessageId(), messageTime)).getMessageId(); - } else { - queuedMessageId = null; - } - - executorService.submit(() -> { - try { - if (queuedMessageId != null) { - execute(MessageService.deleteMessage(update.getMessage().getChatId(), queuedMessageId)); - } - //Entry point download flow - dispatch(update); - } catch (Exception e) { - LOGGER.error("Failed to launch dispatch, error: {}", e.getMessage(), e); - handleDispatchError(update, e); - } finally { - synchronized (storageManager) { - storageManager.setRoot(loadMetricsData(update)); - storageManager.storeRoot(); - } - } - }); - } catch (Exception e) { - LOGGER.error("Failed on onUpdateReceived, error: {}", e.getMessage(), e); - handleDispatchError(update, e); - } - } - } - - private void dispatch(Update update) throws Exception { - String url = ""; - Message message = update.getMessage(); - LOGGER.info("Starting message processing from @{}: {}", message.getFrom().getUserName(), message.getText()); - - if(!update.getMessage().getText().contains("http")) { - execute(MessageService.sendTextMessage(message.getChatId(), message.getMessageId(), NOT_VALID_LINK)).getMessageId(); - return; - } else { - url = extractUrlFromMessage(update.getMessage().getText()); - LOGGER.info("Extracted URL: {} from {}", url, message.getFrom().getUserName()); - } - - final Integer responseId = execute(MessageService.sendTextMessage(message.getChatId(), message.getMessageId(), DOWNLOAD_REQUEST)).getMessageId(); - CommandExecutor.executeCommand(url, String.valueOf(message.getMessageId())); - execute(VideoService.sendVideo(message.getChatId(), message.getMessageId())); - execute(MessageService.deleteMessage(message.getChatId(), responseId)); - cleanDownloadsFolder(); - } - - private void handleDispatchError(Update update, Exception e) { - try { - execute(MessageService.sendTextMessage(update.getMessage().getChatId(), update.getMessage().getMessageId(), DOWNLOAD_REQUEST_ERROR)).getMessageId(); - } catch (TelegramApiException ex) { - LOGGER.error("Failed to send error message, error: {}", ex.getMessage(), ex); - } finally { - cleanDownloadsFolder(); - } - } - - private DataRootContainer loadMetricsData(Update update) { - // Create or update user data from the Telegram update - org.telegram.telegrambots.meta.api.objects.User telegramUser = update.getMessage().getFrom(); - - DataRootContainer rootContainer = (DataRootContainer) storageManager.root(); - - // Find existing user or create new one - String userId = telegramUser.getId().toString(); - Optional existingUser = rootContainer.findUserById(userId); - - if (existingUser.isPresent()) { - User user = existingUser.get(); - user.recordNewMessage(); - } else { - User newUser = new User(userId, update.getMessage().getChatId(), telegramUser.getUserName(), telegramUser.getFirstName(), - telegramUser.getLastName(), telegramUser.getLanguageCode(), telegramUser.getIsPremium(), - 1,LocalDateTime.now().toString()); - rootContainer.getUsers().add(newUser); - LOGGER.info("New user registered: {}", newUser.getUserName()); - } - return rootContainer; - } -} - - diff --git a/code/src/main/java/org/segocode/bot/constants/Messages.java b/code/src/main/java/org/segocode/bot/constants/Messages.java deleted file mode 100644 index 8ab2826..0000000 --- a/code/src/main/java/org/segocode/bot/constants/Messages.java +++ /dev/null @@ -1,8 +0,0 @@ -package org.segocode.bot.constants; - -public class Messages { - public static final String NOT_VALID_LINK = "❌ That doesn't seem to be a valid link"; - public static final String DOWNLOAD_REQUEST = "🔄 Download request processing..."; - public static final String DOWNLOAD_REQUEST_QUEUED = "⌛ Your request is in queue..."; - public static final String DOWNLOAD_REQUEST_ERROR = "❌ Unable to download: web not supported, private post or reach limit"; -} diff --git a/code/src/main/java/org/segocode/bot/model/DataRootContainer.java b/code/src/main/java/org/segocode/bot/model/DataRootContainer.java deleted file mode 100644 index 98ca8a0..0000000 --- a/code/src/main/java/org/segocode/bot/model/DataRootContainer.java +++ /dev/null @@ -1,28 +0,0 @@ -package org.segocode.bot.model; - -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; - -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; - -@Data -@NoArgsConstructor -@AllArgsConstructor -public class DataRootContainer { - private List users = new ArrayList<>(); - - /** - * Finds a user by their ID - * - * @param id The ID of the user to find - * @return An Optional containing the user if found, empty otherwise - */ - public Optional findUserById(String id) { - return users.stream() - .filter(user -> id.equals(user.getId())) - .findFirst(); - } -} diff --git a/code/src/main/java/org/segocode/bot/model/User.java b/code/src/main/java/org/segocode/bot/model/User.java deleted file mode 100644 index ac76581..0000000 --- a/code/src/main/java/org/segocode/bot/model/User.java +++ /dev/null @@ -1,40 +0,0 @@ -package org.segocode.bot.model; - -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; - -import java.time.LocalDateTime; - -@Data -@NoArgsConstructor -@AllArgsConstructor -public class User { - // Unique identifiers - private String id; - private Long chatId; - private String userName; - - // Personal information - private String firstName; - private String lastName; - - // Preferences and metadata - private String languageCode; - private Boolean isPremium; - - // Usage statistics - private Integer messageCount; - private String lastMessageTime; - - /** - * Increments the message count and updates the last message time to current time - */ - public void recordNewMessage() { - if (this.messageCount == null) { - this.messageCount = 0; - } - this.messageCount++; - this.lastMessageTime = LocalDateTime.now().toString(); - } -} \ No newline at end of file diff --git a/code/src/main/java/org/segocode/bot/service/MessageService.java b/code/src/main/java/org/segocode/bot/service/MessageService.java deleted file mode 100644 index 9a2c996..0000000 --- a/code/src/main/java/org/segocode/bot/service/MessageService.java +++ /dev/null @@ -1,32 +0,0 @@ -package org.segocode.bot.service; - -import org.telegram.telegrambots.meta.api.methods.send.SendMessage; -import org.telegram.telegrambots.meta.api.methods.updatingmessages.DeleteMessage; - -public class MessageService { - - /** - * Sends a text message to a specific chat in Telegram. - * - * @param chatId The ID of the chat where the message will be sent. - * @param replyToMessageId The ID of the message to which this message will be a reply. - * @param text The text content of the message to be sent. - * @return The SendMessage object configured with the chat ID, text, and reply to message ID. - */ - public static SendMessage sendTextMessage(Long chatId, Integer replyToMessageId, String text) { - SendMessage message = new SendMessage(chatId.toString(), text); - message.setReplyToMessageId(replyToMessageId); - return message; - } - - /** - * Deletes a specific message in a Telegram chat. - * - * @param chatId The ID of the chat where the message is located. - * @param messageId The ID of the message to be deleted. - * @return A DeleteMessage object configured with the chat ID and message ID. - */ - public static DeleteMessage deleteMessage(Long chatId, Integer messageId) { - return new DeleteMessage(chatId.toString(), messageId); - } -} diff --git a/code/src/main/java/org/segocode/bot/service/VideoService.java b/code/src/main/java/org/segocode/bot/service/VideoService.java deleted file mode 100644 index 5a0064b..0000000 --- a/code/src/main/java/org/segocode/bot/service/VideoService.java +++ /dev/null @@ -1,65 +0,0 @@ -package org.segocode.bot.service; - -import org.segocode.system.util.FileUtil; -import org.slf4j.LoggerFactory; -import org.telegram.telegrambots.meta.api.methods.send.SendVideo; -import org.telegram.telegrambots.meta.api.objects.InputFile; - -import java.io.File; -import org.slf4j.Logger; - - -public class VideoService { - private static final Logger LOGGER = LoggerFactory.getLogger(VideoService.class); - - /** - * Sends a video to a specific chat in Telegram. - * - * @param chatId The ID of the chat where the video will be sent. - * @param replyToMessageId The ID of the message to which this video will be a reply. - * @return The SendVideo object configured with the chat ID and reply to message ID. - */ - public static SendVideo sendVideo(Long chatId, Integer replyToMessageId) { - String filePath = buildFilePath(replyToMessageId); - LOGGER.info("Locating video file for message ID {}: {}", replyToMessageId, filePath); - - File videoFile = FileUtil.locateVideoFile(filePath); - if (videoFile == null) { - throw new RuntimeException("Video file not found for message ID: " + replyToMessageId); - } - LOGGER.info("Video file located successfully: {}", videoFile.getPath()); - - SendVideo sendVideoRequest = createSendVideoRequest(chatId, replyToMessageId, videoFile); - LOGGER.info("Video is sending. Chat ID: {}, Reply to Message ID: {}", chatId, replyToMessageId); - - return sendVideoRequest; - } - - /** - * Creates a SendVideo request for sending a video. - * - * @param chatId The ID of the chat where the video will be sent. - * @param replyToMessageId The ID of the message to which this video will be a reply. - * @param videoFile The video file to be sent. - * @return A SendVideo object configured with the chat ID, reply ID, and video file. - */ - private static SendVideo createSendVideoRequest(Long chatId, Integer replyToMessageId, File videoFile) { - SendVideo sendVideoRequest = new SendVideo(); - sendVideoRequest.setChatId(chatId.toString()); - sendVideoRequest.setReplyToMessageId(replyToMessageId); - sendVideoRequest.setVideo(new InputFile(videoFile)); - return sendVideoRequest; - } - - /** - * Builds the file path for the video file based on the given replyToMessageId. - * - * @param replyToMessageId The ID of the message to which this video will be a reply. - * @return The file path as a String. - */ - private static String buildFilePath(Integer replyToMessageId) { - return "./downloads/" + replyToMessageId + ".mp4"; - } - - -} diff --git a/code/src/main/java/org/segocode/bot/util/MessageUtil.java b/code/src/main/java/org/segocode/bot/util/MessageUtil.java deleted file mode 100644 index 66341ae..0000000 --- a/code/src/main/java/org/segocode/bot/util/MessageUtil.java +++ /dev/null @@ -1,12 +0,0 @@ -package org.segocode.bot.util; - -public class MessageUtil { - public static String extractUrlFromMessage(String messageText) { - int startIndex = messageText.indexOf("http"); - int endIndex = messageText.indexOf(" ", startIndex); - if (endIndex == -1) { - endIndex = messageText.length(); - } - return messageText.substring(startIndex, endIndex); - } -} diff --git a/code/src/main/java/org/segocode/panel/PanelController.java b/code/src/main/java/org/segocode/panel/PanelController.java deleted file mode 100644 index f090588..0000000 --- a/code/src/main/java/org/segocode/panel/PanelController.java +++ /dev/null @@ -1,63 +0,0 @@ -package org.segocode.panel; - -import com.google.gson.Gson; -import io.javalin.Javalin; -import io.javalin.http.Context; -import org.eclipse.store.storage.embedded.types.EmbeddedStorageManager; -import org.segocode.bot.model.DataRootContainer; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.IOException; -import java.io.InputStream; -import java.nio.charset.StandardCharsets; - -public class PanelController { - private static final Logger LOGGER = LoggerFactory.getLogger(PanelController.class); - private final Javalin app; - private final EmbeddedStorageManager storageManager; - - // Singleton instance - private static PanelController instance; - - // Private constructor to prevent instantiation - private PanelController(EmbeddedStorageManager storageManager) { - this.storageManager = storageManager; - this.app = Javalin.create().start(8080); - - // Configure routes - configureRoutes(); - - LOGGER.info("Panel controller started on port 8080"); - } - - public static synchronized void start(EmbeddedStorageManager storageManager) { - if (instance == null) { - instance = new PanelController(storageManager); - } - } - - private void configureRoutes() { - app.get("/", this::handleAdminRequest); - } - -private void handleAdminRequest(Context ctx) { - ctx.contentType("text/html"); - - try { - InputStream inputStream = getClass().getResourceAsStream("/views/admin.html"); - String htmlContent = new String(inputStream.readAllBytes(), StandardCharsets.UTF_8); - String jsonData; - synchronized (storageManager) { - jsonData = new Gson().toJson(((DataRootContainer)storageManager.root()).getUsers()); - } - htmlContent = htmlContent.replace("{{!user_data}}", jsonData); - - ctx.result(htmlContent); - } catch (IOException e) { - LOGGER.error("Failed to read admin HTML file", e); - ctx.status(500); - ctx.result("Error loading admin panel: " + e.getMessage()); - } -} -} \ No newline at end of file diff --git a/code/src/main/java/org/segocode/system/command/CommandExecutor.java b/code/src/main/java/org/segocode/system/command/CommandExecutor.java deleted file mode 100644 index 164db28..0000000 --- a/code/src/main/java/org/segocode/system/command/CommandExecutor.java +++ /dev/null @@ -1,53 +0,0 @@ -package org.segocode.system.command; - -import java.io.File; -import java.io.IOException; -import java.util.concurrent.TimeUnit; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class CommandExecutor { - private static final Logger LOGGER = LoggerFactory.getLogger(CommandExecutor.class); - private static final int MAX_RETRIES = 5; - private static final int TIMEOUT = 120; // seconds - - /** - * Executes a command in the system's command line to download a video using yt-dlp. - * - * @param url The URL of the video to be downloaded. - * @param uuid The unique identifier used to name the downloaded file. - * @throws IOException If an I/O error occurs. - * @throws InterruptedException If the current thread is interrupted while waiting. - */ - public static void executeCommand(String url, String uuid) throws Exception { - String osName = System.getProperty("os.name").toLowerCase(); - String ytDlpCommand = osName.contains("win") ? "yt-dlp.exe" : "yt-dlp"; // This is needed? - String outputPath = "." + File.separator + "downloads" + File.separator + uuid + ".%(ext)s"; - - // The -q option is important to prevent deadlocks by ensuring the output buffer is not filled. - // TODO: make a StreamGobbler to handle buffered output and avoid deadlocks. - String[] command = { ytDlpCommand, "-q", "-S", "ext,res:720", "-o", outputPath, url }; - - int attempt = 0; - while (attempt++ < MAX_RETRIES) { - LOGGER.info("Attempt {} of " + MAX_RETRIES, attempt); - try { - Process process = new ProcessBuilder(command).start(); - if (process.waitFor(TIMEOUT, TimeUnit.SECONDS) ? process.exitValue() == 0 : process.destroyForcibly() == null) { - LOGGER.info("Download successful for URL: {}", url); - return; - } else { - LOGGER.warn("Download attempt {} failed or timed out.", attempt); - process.destroyForcibly(); - } - } catch (IOException e) { - LOGGER.error("I/O error occurred on attempt {} of " + MAX_RETRIES, attempt, e); - if (attempt >= MAX_RETRIES) { - throw e; - } - } - } - LOGGER.error("Max retries reached. Command failed for URL: {}", url); - throw new RuntimeException("Max retries reached. Command failed."); - } -} diff --git a/code/src/main/java/org/segocode/system/util/FileUtil.java b/code/src/main/java/org/segocode/system/util/FileUtil.java deleted file mode 100644 index fb2cc6d..0000000 --- a/code/src/main/java/org/segocode/system/util/FileUtil.java +++ /dev/null @@ -1,56 +0,0 @@ -package org.segocode.system.util; - -import java.io.File; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.Comparator; -import java.util.stream.Stream; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - - -public class FileUtil { - private static final Logger LOGGER = LoggerFactory.getLogger(FileUtil.class); - - /** - * Deletes all files and folders within the 'downloads' folder. - */ - public static void cleanDownloadsFolder() { - Path downloadsFolder = Paths.get("./downloads/"); - try (Stream paths = Files.walk(downloadsFolder)) { - paths.sorted(Comparator.reverseOrder()) - .filter(p -> !p.equals(downloadsFolder)) - .forEach(p -> { - try { - Files.delete(p); - LOGGER.info("Deleted: {}", p); - } catch (Exception e) { - LOGGER.error("Error deleting: {}", p, e); - } - }); - } catch (Exception e) { - LOGGER.error("Error cleaning the downloads folder: {}", downloadsFolder, e); - } - } - - /** - * Locates the video file, attempting to find or rename it with a .mp4 extension. - * - * @param filePath The path of the video file. - * @return The video file if found or renamed successfully, null otherwise. - */ - public static File locateVideoFile(String filePath) { - File videoFile = new File(filePath); - if (videoFile.exists()) return videoFile; - - // Attempt to locate the file without the .mp4 extension - String filePathWithoutExtension = filePath.replace(".mp4", ""); - videoFile = new File(filePathWithoutExtension); - if (videoFile.exists()) { - File renamedFile = new File(filePathWithoutExtension + ".mp4"); - if (videoFile.renameTo(renamedFile)) return renamedFile; - } - return null; - } -} \ No newline at end of file From 741481a373816063b79e0470170fcadc94f800b1 Mon Sep 17 00:00:00 2001 From: SegoCode <35817798+SegoCode@users.noreply.github.com> Date: Thu, 11 Jun 2026 09:25:15 +0200 Subject: [PATCH 41/72] Update pom.xml --- code/pom.xml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/code/pom.xml b/code/pom.xml index bf1f834..0641747 100644 --- a/code/pom.xml +++ b/code/pom.xml @@ -72,6 +72,18 @@ + + com.diffplug.spotless + spotless-maven-plugin + 3.6.0 + + + + 2.92.0 + + + + From 10884819ea17b6466150069845d65de19dbc9bc8 Mon Sep 17 00:00:00 2001 From: SegoCode <35817798+SegoCode@users.noreply.github.com> Date: Thu, 11 Jun 2026 09:29:48 +0200 Subject: [PATCH 42/72] Revert "update structure" This reverts commit 99b39367c08bd167284c3f0f80fb22b2a43cd943. --- code/src/main/java/org/segocode/Main.java | 33 ++++ .../main/java/org/segocode/bot/Webdlbot.java | 161 ++++++++++++++++++ .../org/segocode/bot/constants/Messages.java | 8 + .../segocode/bot/model/DataRootContainer.java | 28 +++ .../java/org/segocode/bot/model/User.java | 40 +++++ .../segocode/bot/service/MessageService.java | 32 ++++ .../segocode/bot/service/VideoService.java | 65 +++++++ .../org/segocode/bot/util/MessageUtil.java | 12 ++ .../org/segocode/panel/PanelController.java | 63 +++++++ .../system/command/CommandExecutor.java | 53 ++++++ .../org/segocode/system/util/FileUtil.java | 56 ++++++ 11 files changed, 551 insertions(+) create mode 100644 code/src/main/java/org/segocode/Main.java create mode 100644 code/src/main/java/org/segocode/bot/Webdlbot.java create mode 100644 code/src/main/java/org/segocode/bot/constants/Messages.java create mode 100644 code/src/main/java/org/segocode/bot/model/DataRootContainer.java create mode 100644 code/src/main/java/org/segocode/bot/model/User.java create mode 100644 code/src/main/java/org/segocode/bot/service/MessageService.java create mode 100644 code/src/main/java/org/segocode/bot/service/VideoService.java create mode 100644 code/src/main/java/org/segocode/bot/util/MessageUtil.java create mode 100644 code/src/main/java/org/segocode/panel/PanelController.java create mode 100644 code/src/main/java/org/segocode/system/command/CommandExecutor.java create mode 100644 code/src/main/java/org/segocode/system/util/FileUtil.java diff --git a/code/src/main/java/org/segocode/Main.java b/code/src/main/java/org/segocode/Main.java new file mode 100644 index 0000000..0b4b414 --- /dev/null +++ b/code/src/main/java/org/segocode/Main.java @@ -0,0 +1,33 @@ +package org.segocode; + +import io.javalin.Javalin; +import org.eclipse.store.storage.embedded.types.EmbeddedStorage; +import org.eclipse.store.storage.embedded.types.EmbeddedStorageManager; +import org.eclipse.store.storage.types.StorageManager; +import org.segocode.bot.Webdlbot; +import org.segocode.panel.PanelController; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.telegram.telegrambots.meta.TelegramBotsApi; +import org.telegram.telegrambots.updatesreceivers.DefaultBotSession; + +import java.util.Date; + +public class Main { + private static final Logger LOGGER = LoggerFactory.getLogger(Main.class); + + public static void main(String[] args) { + try { + LOGGER.info("Starting storage manager..."); + final EmbeddedStorageManager storageManager = EmbeddedStorage.start(); + TelegramBotsApi botsApi = new TelegramBotsApi(DefaultBotSession.class); + LOGGER.info("Starting the video download bot..."); + botsApi.registerBot(new Webdlbot(storageManager)); + LOGGER.info("Bot started successfully and ready to download videos 🚀"); + LOGGER.info("Starting webp anel app..."); + PanelController.start(storageManager); + } catch (Exception e) { + LOGGER.error("Error while attempting to start the bot. Error details:", e); + } + } +} diff --git a/code/src/main/java/org/segocode/bot/Webdlbot.java b/code/src/main/java/org/segocode/bot/Webdlbot.java new file mode 100644 index 0000000..f70d0d8 --- /dev/null +++ b/code/src/main/java/org/segocode/bot/Webdlbot.java @@ -0,0 +1,161 @@ +package org.segocode.bot; + +import org.eclipse.store.storage.types.StorageManager; +import org.segocode.bot.model.DataRootContainer; +import org.segocode.bot.model.User; +import org.segocode.system.command.CommandExecutor; +import org.segocode.bot.service.VideoService; +import org.slf4j.LoggerFactory; +import org.telegram.telegrambots.bots.TelegramLongPollingBot; +import org.telegram.telegrambots.meta.api.objects.Message; +import org.telegram.telegrambots.meta.api.objects.Update; +import org.segocode.bot.service.MessageService; +import org.slf4j.Logger; +import org.telegram.telegrambots.meta.exceptions.TelegramApiException; + +import java.time.LocalDateTime; +import java.util.Optional; +import java.util.concurrent.*; + +import static org.segocode.bot.constants.Messages.*; +import static org.segocode.bot.util.MessageUtil.*; +import static org.segocode.system.util.FileUtil.*; + +public class Webdlbot extends TelegramLongPollingBot { + private static final Logger LOGGER = LoggerFactory.getLogger(Webdlbot.class); + private static final String BOT_TOKEN = System.getenv("BOT_TOKEN"); + + private final StorageManager storageManager; + + // Create a ThreadPoolExecutor with a single virtual thread + private final ThreadPoolExecutor executorService = new ThreadPoolExecutor( + 1, // corePoolSize: the number of threads to keep in the pool, even if they are idle + 1, // maximumPoolSize: the maximum number of threads to allow in the pool + 0L, // keepAliveTime: when the number of threads is greater than the core + TimeUnit.MILLISECONDS, // the time unit for the keepAliveTime argument + new LinkedBlockingQueue<>(), // the queue to use for holding tasks before they are executed + Thread.ofVirtual().factory() // the factory to use when creating new threads + ); + + public Webdlbot(StorageManager storageManager) { + this.storageManager = storageManager; + initializeRootContainer(); + } + + private void initializeRootContainer() { + Object root = storageManager.root(); + if (!(root instanceof DataRootContainer)) { + storageManager.setRoot(new DataRootContainer()); + LOGGER.info("Initialized new DataRootContainer as db root object"); + } + LOGGER.info("Root db object contains {} users", ((DataRootContainer) storageManager.root()).getUsers().size()); + } + + @Override + public String getBotUsername() { + return "webdl"; + } + + @Override + public String getBotToken() { + if (BOT_TOKEN == null || BOT_TOKEN.isEmpty()) { + LOGGER.error("BOT_TOKEN is not set in the environment variables."); + throw new IllegalStateException("BOT_TOKEN is not set in the environment variables."); + } + return BOT_TOKEN; + } + + + @Override + public void onUpdateReceived(Update update) { + if (update.hasMessage() && update.getMessage().hasText()) { + try { + long queuedTasks = executorService.getTaskCount() - executorService.getCompletedTaskCount(); + Integer queuedMessageId; + if (queuedTasks > 0) { + String messageTime = DOWNLOAD_REQUEST_QUEUED + " (<" + queuedTasks + "m)"; + LOGGER.info("Executor is busy, queuing the message from @{}", update.getMessage().getFrom().getUserName()); + queuedMessageId = execute(MessageService.sendTextMessage(update.getMessage().getChatId(), update.getMessage().getMessageId(), messageTime)).getMessageId(); + } else { + queuedMessageId = null; + } + + executorService.submit(() -> { + try { + if (queuedMessageId != null) { + execute(MessageService.deleteMessage(update.getMessage().getChatId(), queuedMessageId)); + } + //Entry point download flow + dispatch(update); + } catch (Exception e) { + LOGGER.error("Failed to launch dispatch, error: {}", e.getMessage(), e); + handleDispatchError(update, e); + } finally { + synchronized (storageManager) { + storageManager.setRoot(loadMetricsData(update)); + storageManager.storeRoot(); + } + } + }); + } catch (Exception e) { + LOGGER.error("Failed on onUpdateReceived, error: {}", e.getMessage(), e); + handleDispatchError(update, e); + } + } + } + + private void dispatch(Update update) throws Exception { + String url = ""; + Message message = update.getMessage(); + LOGGER.info("Starting message processing from @{}: {}", message.getFrom().getUserName(), message.getText()); + + if(!update.getMessage().getText().contains("http")) { + execute(MessageService.sendTextMessage(message.getChatId(), message.getMessageId(), NOT_VALID_LINK)).getMessageId(); + return; + } else { + url = extractUrlFromMessage(update.getMessage().getText()); + LOGGER.info("Extracted URL: {} from {}", url, message.getFrom().getUserName()); + } + + final Integer responseId = execute(MessageService.sendTextMessage(message.getChatId(), message.getMessageId(), DOWNLOAD_REQUEST)).getMessageId(); + CommandExecutor.executeCommand(url, String.valueOf(message.getMessageId())); + execute(VideoService.sendVideo(message.getChatId(), message.getMessageId())); + execute(MessageService.deleteMessage(message.getChatId(), responseId)); + cleanDownloadsFolder(); + } + + private void handleDispatchError(Update update, Exception e) { + try { + execute(MessageService.sendTextMessage(update.getMessage().getChatId(), update.getMessage().getMessageId(), DOWNLOAD_REQUEST_ERROR)).getMessageId(); + } catch (TelegramApiException ex) { + LOGGER.error("Failed to send error message, error: {}", ex.getMessage(), ex); + } finally { + cleanDownloadsFolder(); + } + } + + private DataRootContainer loadMetricsData(Update update) { + // Create or update user data from the Telegram update + org.telegram.telegrambots.meta.api.objects.User telegramUser = update.getMessage().getFrom(); + + DataRootContainer rootContainer = (DataRootContainer) storageManager.root(); + + // Find existing user or create new one + String userId = telegramUser.getId().toString(); + Optional existingUser = rootContainer.findUserById(userId); + + if (existingUser.isPresent()) { + User user = existingUser.get(); + user.recordNewMessage(); + } else { + User newUser = new User(userId, update.getMessage().getChatId(), telegramUser.getUserName(), telegramUser.getFirstName(), + telegramUser.getLastName(), telegramUser.getLanguageCode(), telegramUser.getIsPremium(), + 1,LocalDateTime.now().toString()); + rootContainer.getUsers().add(newUser); + LOGGER.info("New user registered: {}", newUser.getUserName()); + } + return rootContainer; + } +} + + diff --git a/code/src/main/java/org/segocode/bot/constants/Messages.java b/code/src/main/java/org/segocode/bot/constants/Messages.java new file mode 100644 index 0000000..8ab2826 --- /dev/null +++ b/code/src/main/java/org/segocode/bot/constants/Messages.java @@ -0,0 +1,8 @@ +package org.segocode.bot.constants; + +public class Messages { + public static final String NOT_VALID_LINK = "❌ That doesn't seem to be a valid link"; + public static final String DOWNLOAD_REQUEST = "🔄 Download request processing..."; + public static final String DOWNLOAD_REQUEST_QUEUED = "⌛ Your request is in queue..."; + public static final String DOWNLOAD_REQUEST_ERROR = "❌ Unable to download: web not supported, private post or reach limit"; +} diff --git a/code/src/main/java/org/segocode/bot/model/DataRootContainer.java b/code/src/main/java/org/segocode/bot/model/DataRootContainer.java new file mode 100644 index 0000000..98ca8a0 --- /dev/null +++ b/code/src/main/java/org/segocode/bot/model/DataRootContainer.java @@ -0,0 +1,28 @@ +package org.segocode.bot.model; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class DataRootContainer { + private List users = new ArrayList<>(); + + /** + * Finds a user by their ID + * + * @param id The ID of the user to find + * @return An Optional containing the user if found, empty otherwise + */ + public Optional findUserById(String id) { + return users.stream() + .filter(user -> id.equals(user.getId())) + .findFirst(); + } +} diff --git a/code/src/main/java/org/segocode/bot/model/User.java b/code/src/main/java/org/segocode/bot/model/User.java new file mode 100644 index 0000000..ac76581 --- /dev/null +++ b/code/src/main/java/org/segocode/bot/model/User.java @@ -0,0 +1,40 @@ +package org.segocode.bot.model; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class User { + // Unique identifiers + private String id; + private Long chatId; + private String userName; + + // Personal information + private String firstName; + private String lastName; + + // Preferences and metadata + private String languageCode; + private Boolean isPremium; + + // Usage statistics + private Integer messageCount; + private String lastMessageTime; + + /** + * Increments the message count and updates the last message time to current time + */ + public void recordNewMessage() { + if (this.messageCount == null) { + this.messageCount = 0; + } + this.messageCount++; + this.lastMessageTime = LocalDateTime.now().toString(); + } +} \ No newline at end of file diff --git a/code/src/main/java/org/segocode/bot/service/MessageService.java b/code/src/main/java/org/segocode/bot/service/MessageService.java new file mode 100644 index 0000000..9a2c996 --- /dev/null +++ b/code/src/main/java/org/segocode/bot/service/MessageService.java @@ -0,0 +1,32 @@ +package org.segocode.bot.service; + +import org.telegram.telegrambots.meta.api.methods.send.SendMessage; +import org.telegram.telegrambots.meta.api.methods.updatingmessages.DeleteMessage; + +public class MessageService { + + /** + * Sends a text message to a specific chat in Telegram. + * + * @param chatId The ID of the chat where the message will be sent. + * @param replyToMessageId The ID of the message to which this message will be a reply. + * @param text The text content of the message to be sent. + * @return The SendMessage object configured with the chat ID, text, and reply to message ID. + */ + public static SendMessage sendTextMessage(Long chatId, Integer replyToMessageId, String text) { + SendMessage message = new SendMessage(chatId.toString(), text); + message.setReplyToMessageId(replyToMessageId); + return message; + } + + /** + * Deletes a specific message in a Telegram chat. + * + * @param chatId The ID of the chat where the message is located. + * @param messageId The ID of the message to be deleted. + * @return A DeleteMessage object configured with the chat ID and message ID. + */ + public static DeleteMessage deleteMessage(Long chatId, Integer messageId) { + return new DeleteMessage(chatId.toString(), messageId); + } +} diff --git a/code/src/main/java/org/segocode/bot/service/VideoService.java b/code/src/main/java/org/segocode/bot/service/VideoService.java new file mode 100644 index 0000000..5a0064b --- /dev/null +++ b/code/src/main/java/org/segocode/bot/service/VideoService.java @@ -0,0 +1,65 @@ +package org.segocode.bot.service; + +import org.segocode.system.util.FileUtil; +import org.slf4j.LoggerFactory; +import org.telegram.telegrambots.meta.api.methods.send.SendVideo; +import org.telegram.telegrambots.meta.api.objects.InputFile; + +import java.io.File; +import org.slf4j.Logger; + + +public class VideoService { + private static final Logger LOGGER = LoggerFactory.getLogger(VideoService.class); + + /** + * Sends a video to a specific chat in Telegram. + * + * @param chatId The ID of the chat where the video will be sent. + * @param replyToMessageId The ID of the message to which this video will be a reply. + * @return The SendVideo object configured with the chat ID and reply to message ID. + */ + public static SendVideo sendVideo(Long chatId, Integer replyToMessageId) { + String filePath = buildFilePath(replyToMessageId); + LOGGER.info("Locating video file for message ID {}: {}", replyToMessageId, filePath); + + File videoFile = FileUtil.locateVideoFile(filePath); + if (videoFile == null) { + throw new RuntimeException("Video file not found for message ID: " + replyToMessageId); + } + LOGGER.info("Video file located successfully: {}", videoFile.getPath()); + + SendVideo sendVideoRequest = createSendVideoRequest(chatId, replyToMessageId, videoFile); + LOGGER.info("Video is sending. Chat ID: {}, Reply to Message ID: {}", chatId, replyToMessageId); + + return sendVideoRequest; + } + + /** + * Creates a SendVideo request for sending a video. + * + * @param chatId The ID of the chat where the video will be sent. + * @param replyToMessageId The ID of the message to which this video will be a reply. + * @param videoFile The video file to be sent. + * @return A SendVideo object configured with the chat ID, reply ID, and video file. + */ + private static SendVideo createSendVideoRequest(Long chatId, Integer replyToMessageId, File videoFile) { + SendVideo sendVideoRequest = new SendVideo(); + sendVideoRequest.setChatId(chatId.toString()); + sendVideoRequest.setReplyToMessageId(replyToMessageId); + sendVideoRequest.setVideo(new InputFile(videoFile)); + return sendVideoRequest; + } + + /** + * Builds the file path for the video file based on the given replyToMessageId. + * + * @param replyToMessageId The ID of the message to which this video will be a reply. + * @return The file path as a String. + */ + private static String buildFilePath(Integer replyToMessageId) { + return "./downloads/" + replyToMessageId + ".mp4"; + } + + +} diff --git a/code/src/main/java/org/segocode/bot/util/MessageUtil.java b/code/src/main/java/org/segocode/bot/util/MessageUtil.java new file mode 100644 index 0000000..66341ae --- /dev/null +++ b/code/src/main/java/org/segocode/bot/util/MessageUtil.java @@ -0,0 +1,12 @@ +package org.segocode.bot.util; + +public class MessageUtil { + public static String extractUrlFromMessage(String messageText) { + int startIndex = messageText.indexOf("http"); + int endIndex = messageText.indexOf(" ", startIndex); + if (endIndex == -1) { + endIndex = messageText.length(); + } + return messageText.substring(startIndex, endIndex); + } +} diff --git a/code/src/main/java/org/segocode/panel/PanelController.java b/code/src/main/java/org/segocode/panel/PanelController.java new file mode 100644 index 0000000..f090588 --- /dev/null +++ b/code/src/main/java/org/segocode/panel/PanelController.java @@ -0,0 +1,63 @@ +package org.segocode.panel; + +import com.google.gson.Gson; +import io.javalin.Javalin; +import io.javalin.http.Context; +import org.eclipse.store.storage.embedded.types.EmbeddedStorageManager; +import org.segocode.bot.model.DataRootContainer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; + +public class PanelController { + private static final Logger LOGGER = LoggerFactory.getLogger(PanelController.class); + private final Javalin app; + private final EmbeddedStorageManager storageManager; + + // Singleton instance + private static PanelController instance; + + // Private constructor to prevent instantiation + private PanelController(EmbeddedStorageManager storageManager) { + this.storageManager = storageManager; + this.app = Javalin.create().start(8080); + + // Configure routes + configureRoutes(); + + LOGGER.info("Panel controller started on port 8080"); + } + + public static synchronized void start(EmbeddedStorageManager storageManager) { + if (instance == null) { + instance = new PanelController(storageManager); + } + } + + private void configureRoutes() { + app.get("/", this::handleAdminRequest); + } + +private void handleAdminRequest(Context ctx) { + ctx.contentType("text/html"); + + try { + InputStream inputStream = getClass().getResourceAsStream("/views/admin.html"); + String htmlContent = new String(inputStream.readAllBytes(), StandardCharsets.UTF_8); + String jsonData; + synchronized (storageManager) { + jsonData = new Gson().toJson(((DataRootContainer)storageManager.root()).getUsers()); + } + htmlContent = htmlContent.replace("{{!user_data}}", jsonData); + + ctx.result(htmlContent); + } catch (IOException e) { + LOGGER.error("Failed to read admin HTML file", e); + ctx.status(500); + ctx.result("Error loading admin panel: " + e.getMessage()); + } +} +} \ No newline at end of file diff --git a/code/src/main/java/org/segocode/system/command/CommandExecutor.java b/code/src/main/java/org/segocode/system/command/CommandExecutor.java new file mode 100644 index 0000000..164db28 --- /dev/null +++ b/code/src/main/java/org/segocode/system/command/CommandExecutor.java @@ -0,0 +1,53 @@ +package org.segocode.system.command; + +import java.io.File; +import java.io.IOException; +import java.util.concurrent.TimeUnit; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class CommandExecutor { + private static final Logger LOGGER = LoggerFactory.getLogger(CommandExecutor.class); + private static final int MAX_RETRIES = 5; + private static final int TIMEOUT = 120; // seconds + + /** + * Executes a command in the system's command line to download a video using yt-dlp. + * + * @param url The URL of the video to be downloaded. + * @param uuid The unique identifier used to name the downloaded file. + * @throws IOException If an I/O error occurs. + * @throws InterruptedException If the current thread is interrupted while waiting. + */ + public static void executeCommand(String url, String uuid) throws Exception { + String osName = System.getProperty("os.name").toLowerCase(); + String ytDlpCommand = osName.contains("win") ? "yt-dlp.exe" : "yt-dlp"; // This is needed? + String outputPath = "." + File.separator + "downloads" + File.separator + uuid + ".%(ext)s"; + + // The -q option is important to prevent deadlocks by ensuring the output buffer is not filled. + // TODO: make a StreamGobbler to handle buffered output and avoid deadlocks. + String[] command = { ytDlpCommand, "-q", "-S", "ext,res:720", "-o", outputPath, url }; + + int attempt = 0; + while (attempt++ < MAX_RETRIES) { + LOGGER.info("Attempt {} of " + MAX_RETRIES, attempt); + try { + Process process = new ProcessBuilder(command).start(); + if (process.waitFor(TIMEOUT, TimeUnit.SECONDS) ? process.exitValue() == 0 : process.destroyForcibly() == null) { + LOGGER.info("Download successful for URL: {}", url); + return; + } else { + LOGGER.warn("Download attempt {} failed or timed out.", attempt); + process.destroyForcibly(); + } + } catch (IOException e) { + LOGGER.error("I/O error occurred on attempt {} of " + MAX_RETRIES, attempt, e); + if (attempt >= MAX_RETRIES) { + throw e; + } + } + } + LOGGER.error("Max retries reached. Command failed for URL: {}", url); + throw new RuntimeException("Max retries reached. Command failed."); + } +} diff --git a/code/src/main/java/org/segocode/system/util/FileUtil.java b/code/src/main/java/org/segocode/system/util/FileUtil.java new file mode 100644 index 0000000..fb2cc6d --- /dev/null +++ b/code/src/main/java/org/segocode/system/util/FileUtil.java @@ -0,0 +1,56 @@ +package org.segocode.system.util; + +import java.io.File; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Comparator; +import java.util.stream.Stream; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + + +public class FileUtil { + private static final Logger LOGGER = LoggerFactory.getLogger(FileUtil.class); + + /** + * Deletes all files and folders within the 'downloads' folder. + */ + public static void cleanDownloadsFolder() { + Path downloadsFolder = Paths.get("./downloads/"); + try (Stream paths = Files.walk(downloadsFolder)) { + paths.sorted(Comparator.reverseOrder()) + .filter(p -> !p.equals(downloadsFolder)) + .forEach(p -> { + try { + Files.delete(p); + LOGGER.info("Deleted: {}", p); + } catch (Exception e) { + LOGGER.error("Error deleting: {}", p, e); + } + }); + } catch (Exception e) { + LOGGER.error("Error cleaning the downloads folder: {}", downloadsFolder, e); + } + } + + /** + * Locates the video file, attempting to find or rename it with a .mp4 extension. + * + * @param filePath The path of the video file. + * @return The video file if found or renamed successfully, null otherwise. + */ + public static File locateVideoFile(String filePath) { + File videoFile = new File(filePath); + if (videoFile.exists()) return videoFile; + + // Attempt to locate the file without the .mp4 extension + String filePathWithoutExtension = filePath.replace(".mp4", ""); + videoFile = new File(filePathWithoutExtension); + if (videoFile.exists()) { + File renamedFile = new File(filePathWithoutExtension + ".mp4"); + if (videoFile.renameTo(renamedFile)) return renamedFile; + } + return null; + } +} \ No newline at end of file From 5fab74ab729e2f4fa06318f4ba18ba8857e9c109 Mon Sep 17 00:00:00 2001 From: SegoCode <35817798+SegoCode@users.noreply.github.com> Date: Thu, 11 Jun 2026 10:12:51 +0200 Subject: [PATCH 43/72] refactor --- .../org/segocode/panel/PanelController.java | 63 ---------- .../java/org/segocode/{ => webdl}/Main.java | 12 +- .../segocode/{ => webdl}/bot/Webdlbot.java | 111 +++++++++++------- .../{ => webdl}/bot/constants/Messages.java | 5 +- .../bot/model/DataRootContainer.java | 13 +- .../segocode/{ => webdl}/bot/model/User.java | 7 +- .../bot/service/MessageService.java | 2 +- .../{ => webdl}/bot/service/VideoService.java | 32 +++-- .../{ => webdl}/bot/util/MessageUtil.java | 2 +- .../segocode/webdl/panel/AdminController.java | 40 +++++++ .../webdl/panel/PanelApplication.java | 28 +++++ .../system/command/CommandExecutor.java | 12 +- .../{ => webdl}/system/util/FileUtil.java | 5 +- 13 files changed, 174 insertions(+), 158 deletions(-) delete mode 100644 code/src/main/java/org/segocode/panel/PanelController.java rename code/src/main/java/org/segocode/{ => webdl}/Main.java (81%) rename code/src/main/java/org/segocode/{ => webdl}/bot/Webdlbot.java (61%) rename code/src/main/java/org/segocode/{ => webdl}/bot/constants/Messages.java (61%) rename code/src/main/java/org/segocode/{ => webdl}/bot/model/DataRootContainer.java (77%) rename code/src/main/java/org/segocode/{ => webdl}/bot/model/User.java (95%) rename code/src/main/java/org/segocode/{ => webdl}/bot/service/MessageService.java (97%) rename code/src/main/java/org/segocode/{ => webdl}/bot/service/VideoService.java (70%) rename code/src/main/java/org/segocode/{ => webdl}/bot/util/MessageUtil.java (90%) create mode 100644 code/src/main/java/org/segocode/webdl/panel/AdminController.java create mode 100644 code/src/main/java/org/segocode/webdl/panel/PanelApplication.java rename code/src/main/java/org/segocode/{ => webdl}/system/command/CommandExecutor.java (83%) rename code/src/main/java/org/segocode/{ => webdl}/system/util/FileUtil.java (97%) diff --git a/code/src/main/java/org/segocode/panel/PanelController.java b/code/src/main/java/org/segocode/panel/PanelController.java deleted file mode 100644 index f090588..0000000 --- a/code/src/main/java/org/segocode/panel/PanelController.java +++ /dev/null @@ -1,63 +0,0 @@ -package org.segocode.panel; - -import com.google.gson.Gson; -import io.javalin.Javalin; -import io.javalin.http.Context; -import org.eclipse.store.storage.embedded.types.EmbeddedStorageManager; -import org.segocode.bot.model.DataRootContainer; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.IOException; -import java.io.InputStream; -import java.nio.charset.StandardCharsets; - -public class PanelController { - private static final Logger LOGGER = LoggerFactory.getLogger(PanelController.class); - private final Javalin app; - private final EmbeddedStorageManager storageManager; - - // Singleton instance - private static PanelController instance; - - // Private constructor to prevent instantiation - private PanelController(EmbeddedStorageManager storageManager) { - this.storageManager = storageManager; - this.app = Javalin.create().start(8080); - - // Configure routes - configureRoutes(); - - LOGGER.info("Panel controller started on port 8080"); - } - - public static synchronized void start(EmbeddedStorageManager storageManager) { - if (instance == null) { - instance = new PanelController(storageManager); - } - } - - private void configureRoutes() { - app.get("/", this::handleAdminRequest); - } - -private void handleAdminRequest(Context ctx) { - ctx.contentType("text/html"); - - try { - InputStream inputStream = getClass().getResourceAsStream("/views/admin.html"); - String htmlContent = new String(inputStream.readAllBytes(), StandardCharsets.UTF_8); - String jsonData; - synchronized (storageManager) { - jsonData = new Gson().toJson(((DataRootContainer)storageManager.root()).getUsers()); - } - htmlContent = htmlContent.replace("{{!user_data}}", jsonData); - - ctx.result(htmlContent); - } catch (IOException e) { - LOGGER.error("Failed to read admin HTML file", e); - ctx.status(500); - ctx.result("Error loading admin panel: " + e.getMessage()); - } -} -} \ No newline at end of file diff --git a/code/src/main/java/org/segocode/Main.java b/code/src/main/java/org/segocode/webdl/Main.java similarity index 81% rename from code/src/main/java/org/segocode/Main.java rename to code/src/main/java/org/segocode/webdl/Main.java index 0b4b414..a20f823 100644 --- a/code/src/main/java/org/segocode/Main.java +++ b/code/src/main/java/org/segocode/webdl/Main.java @@ -1,18 +1,14 @@ -package org.segocode; +package org.segocode.webdl; -import io.javalin.Javalin; import org.eclipse.store.storage.embedded.types.EmbeddedStorage; import org.eclipse.store.storage.embedded.types.EmbeddedStorageManager; -import org.eclipse.store.storage.types.StorageManager; -import org.segocode.bot.Webdlbot; -import org.segocode.panel.PanelController; +import org.segocode.webdl.bot.Webdlbot; +import org.segocode.webdl.panel.PanelApplication; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.telegram.telegrambots.meta.TelegramBotsApi; import org.telegram.telegrambots.updatesreceivers.DefaultBotSession; -import java.util.Date; - public class Main { private static final Logger LOGGER = LoggerFactory.getLogger(Main.class); @@ -25,7 +21,7 @@ public static void main(String[] args) { botsApi.registerBot(new Webdlbot(storageManager)); LOGGER.info("Bot started successfully and ready to download videos 🚀"); LOGGER.info("Starting webp anel app..."); - PanelController.start(storageManager); + PanelApplication.start(storageManager); } catch (Exception e) { LOGGER.error("Error while attempting to start the bot. Error details:", e); } diff --git a/code/src/main/java/org/segocode/bot/Webdlbot.java b/code/src/main/java/org/segocode/webdl/bot/Webdlbot.java similarity index 61% rename from code/src/main/java/org/segocode/bot/Webdlbot.java rename to code/src/main/java/org/segocode/webdl/bot/Webdlbot.java index f70d0d8..7f77268 100644 --- a/code/src/main/java/org/segocode/bot/Webdlbot.java +++ b/code/src/main/java/org/segocode/webdl/bot/Webdlbot.java @@ -1,26 +1,25 @@ -package org.segocode.bot; +package org.segocode.webdl.bot; +import static org.segocode.webdl.bot.constants.Messages.*; +import static org.segocode.webdl.bot.util.MessageUtil.*; +import static org.segocode.webdl.system.util.FileUtil.*; + +import java.time.LocalDateTime; +import java.util.Optional; +import java.util.concurrent.*; import org.eclipse.store.storage.types.StorageManager; -import org.segocode.bot.model.DataRootContainer; -import org.segocode.bot.model.User; -import org.segocode.system.command.CommandExecutor; -import org.segocode.bot.service.VideoService; +import org.segocode.webdl.bot.model.DataRootContainer; +import org.segocode.webdl.bot.model.User; +import org.segocode.webdl.bot.service.MessageService; +import org.segocode.webdl.bot.service.VideoService; +import org.segocode.webdl.system.command.CommandExecutor; +import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.telegram.telegrambots.bots.TelegramLongPollingBot; import org.telegram.telegrambots.meta.api.objects.Message; import org.telegram.telegrambots.meta.api.objects.Update; -import org.segocode.bot.service.MessageService; -import org.slf4j.Logger; import org.telegram.telegrambots.meta.exceptions.TelegramApiException; -import java.time.LocalDateTime; -import java.util.Optional; -import java.util.concurrent.*; - -import static org.segocode.bot.constants.Messages.*; -import static org.segocode.bot.util.MessageUtil.*; -import static org.segocode.system.util.FileUtil.*; - public class Webdlbot extends TelegramLongPollingBot { private static final Logger LOGGER = LoggerFactory.getLogger(Webdlbot.class); private static final String BOT_TOKEN = System.getenv("BOT_TOKEN"); @@ -35,7 +34,7 @@ public class Webdlbot extends TelegramLongPollingBot { TimeUnit.MILLISECONDS, // the time unit for the keepAliveTime argument new LinkedBlockingQueue<>(), // the queue to use for holding tasks before they are executed Thread.ofVirtual().factory() // the factory to use when creating new threads - ); + ); public Webdlbot(StorageManager storageManager) { this.storageManager = storageManager; @@ -48,7 +47,9 @@ private void initializeRootContainer() { storageManager.setRoot(new DataRootContainer()); LOGGER.info("Initialized new DataRootContainer as db root object"); } - LOGGER.info("Root db object contains {} users", ((DataRootContainer) storageManager.root()).getUsers().size()); + LOGGER.info( + "Root db object contains {} users", + ((DataRootContainer) storageManager.root()).getUsers().size()); } @Override @@ -65,7 +66,6 @@ public String getBotToken() { return BOT_TOKEN; } - @Override public void onUpdateReceived(Update update) { if (update.hasMessage() && update.getMessage().hasText()) { @@ -74,8 +74,14 @@ public void onUpdateReceived(Update update) { Integer queuedMessageId; if (queuedTasks > 0) { String messageTime = DOWNLOAD_REQUEST_QUEUED + " (<" + queuedTasks + "m)"; - LOGGER.info("Executor is busy, queuing the message from @{}", update.getMessage().getFrom().getUserName()); - queuedMessageId = execute(MessageService.sendTextMessage(update.getMessage().getChatId(), update.getMessage().getMessageId(), messageTime)).getMessageId(); + LOGGER.info( + "Executor is busy, queuing the message from @{}", + update.getMessage().getFrom().getUserName()); + queuedMessageId = execute(MessageService.sendTextMessage( + update.getMessage().getChatId(), + update.getMessage().getMessageId(), + messageTime)) + .getMessageId(); } else { queuedMessageId = null; } @@ -83,9 +89,10 @@ public void onUpdateReceived(Update update) { executorService.submit(() -> { try { if (queuedMessageId != null) { - execute(MessageService.deleteMessage(update.getMessage().getChatId(), queuedMessageId)); + execute(MessageService.deleteMessage( + update.getMessage().getChatId(), queuedMessageId)); } - //Entry point download flow + // Entry point download flow dispatch(update); } catch (Exception e) { LOGGER.error("Failed to launch dispatch, error: {}", e.getMessage(), e); @@ -105,28 +112,36 @@ public void onUpdateReceived(Update update) { } private void dispatch(Update update) throws Exception { - String url = ""; - Message message = update.getMessage(); - LOGGER.info("Starting message processing from @{}: {}", message.getFrom().getUserName(), message.getText()); - - if(!update.getMessage().getText().contains("http")) { - execute(MessageService.sendTextMessage(message.getChatId(), message.getMessageId(), NOT_VALID_LINK)).getMessageId(); - return; - } else { - url = extractUrlFromMessage(update.getMessage().getText()); - LOGGER.info("Extracted URL: {} from {}", url, message.getFrom().getUserName()); - } + String url = ""; + Message message = update.getMessage(); + LOGGER.info( + "Starting message processing from @{}: {}", message.getFrom().getUserName(), message.getText()); + + if (!update.getMessage().getText().contains("http")) { + execute(MessageService.sendTextMessage(message.getChatId(), message.getMessageId(), NOT_VALID_LINK)) + .getMessageId(); + return; + } else { + url = extractUrlFromMessage(update.getMessage().getText()); + LOGGER.info("Extracted URL: {} from {}", url, message.getFrom().getUserName()); + } - final Integer responseId = execute(MessageService.sendTextMessage(message.getChatId(), message.getMessageId(), DOWNLOAD_REQUEST)).getMessageId(); - CommandExecutor.executeCommand(url, String.valueOf(message.getMessageId())); - execute(VideoService.sendVideo(message.getChatId(), message.getMessageId())); - execute(MessageService.deleteMessage(message.getChatId(), responseId)); - cleanDownloadsFolder(); + final Integer responseId = execute( + MessageService.sendTextMessage(message.getChatId(), message.getMessageId(), DOWNLOAD_REQUEST)) + .getMessageId(); + CommandExecutor.executeCommand(url, String.valueOf(message.getMessageId())); + execute(VideoService.sendVideo(message.getChatId(), message.getMessageId())); + execute(MessageService.deleteMessage(message.getChatId(), responseId)); + cleanDownloadsFolder(); } private void handleDispatchError(Update update, Exception e) { try { - execute(MessageService.sendTextMessage(update.getMessage().getChatId(), update.getMessage().getMessageId(), DOWNLOAD_REQUEST_ERROR)).getMessageId(); + execute(MessageService.sendTextMessage( + update.getMessage().getChatId(), + update.getMessage().getMessageId(), + DOWNLOAD_REQUEST_ERROR)) + .getMessageId(); } catch (TelegramApiException ex) { LOGGER.error("Failed to send error message, error: {}", ex.getMessage(), ex); } finally { @@ -136,7 +151,8 @@ private void handleDispatchError(Update update, Exception e) { private DataRootContainer loadMetricsData(Update update) { // Create or update user data from the Telegram update - org.telegram.telegrambots.meta.api.objects.User telegramUser = update.getMessage().getFrom(); + org.telegram.telegrambots.meta.api.objects.User telegramUser = + update.getMessage().getFrom(); DataRootContainer rootContainer = (DataRootContainer) storageManager.root(); @@ -148,14 +164,19 @@ private DataRootContainer loadMetricsData(Update update) { User user = existingUser.get(); user.recordNewMessage(); } else { - User newUser = new User(userId, update.getMessage().getChatId(), telegramUser.getUserName(), telegramUser.getFirstName(), - telegramUser.getLastName(), telegramUser.getLanguageCode(), telegramUser.getIsPremium(), - 1,LocalDateTime.now().toString()); + User newUser = new User( + userId, + update.getMessage().getChatId(), + telegramUser.getUserName(), + telegramUser.getFirstName(), + telegramUser.getLastName(), + telegramUser.getLanguageCode(), + telegramUser.getIsPremium(), + 1, + LocalDateTime.now().toString()); rootContainer.getUsers().add(newUser); LOGGER.info("New user registered: {}", newUser.getUserName()); } return rootContainer; } } - - diff --git a/code/src/main/java/org/segocode/bot/constants/Messages.java b/code/src/main/java/org/segocode/webdl/bot/constants/Messages.java similarity index 61% rename from code/src/main/java/org/segocode/bot/constants/Messages.java rename to code/src/main/java/org/segocode/webdl/bot/constants/Messages.java index 8ab2826..868c653 100644 --- a/code/src/main/java/org/segocode/bot/constants/Messages.java +++ b/code/src/main/java/org/segocode/webdl/bot/constants/Messages.java @@ -1,8 +1,9 @@ -package org.segocode.bot.constants; +package org.segocode.webdl.bot.constants; public class Messages { public static final String NOT_VALID_LINK = "❌ That doesn't seem to be a valid link"; public static final String DOWNLOAD_REQUEST = "🔄 Download request processing..."; public static final String DOWNLOAD_REQUEST_QUEUED = "⌛ Your request is in queue..."; - public static final String DOWNLOAD_REQUEST_ERROR = "❌ Unable to download: web not supported, private post or reach limit"; + public static final String DOWNLOAD_REQUEST_ERROR = + "❌ Unable to download: web not supported, private post or reach limit"; } diff --git a/code/src/main/java/org/segocode/bot/model/DataRootContainer.java b/code/src/main/java/org/segocode/webdl/bot/model/DataRootContainer.java similarity index 77% rename from code/src/main/java/org/segocode/bot/model/DataRootContainer.java rename to code/src/main/java/org/segocode/webdl/bot/model/DataRootContainer.java index 98ca8a0..23c0951 100644 --- a/code/src/main/java/org/segocode/bot/model/DataRootContainer.java +++ b/code/src/main/java/org/segocode/webdl/bot/model/DataRootContainer.java @@ -1,12 +1,11 @@ -package org.segocode.bot.model; - -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; +package org.segocode.webdl.bot.model; import java.util.ArrayList; import java.util.List; import java.util.Optional; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; @Data @NoArgsConstructor @@ -21,8 +20,6 @@ public class DataRootContainer { * @return An Optional containing the user if found, empty otherwise */ public Optional findUserById(String id) { - return users.stream() - .filter(user -> id.equals(user.getId())) - .findFirst(); + return users.stream().filter(user -> id.equals(user.getId())).findFirst(); } } diff --git a/code/src/main/java/org/segocode/bot/model/User.java b/code/src/main/java/org/segocode/webdl/bot/model/User.java similarity index 95% rename from code/src/main/java/org/segocode/bot/model/User.java rename to code/src/main/java/org/segocode/webdl/bot/model/User.java index ac76581..481a310 100644 --- a/code/src/main/java/org/segocode/bot/model/User.java +++ b/code/src/main/java/org/segocode/webdl/bot/model/User.java @@ -1,11 +1,10 @@ -package org.segocode.bot.model; +package org.segocode.webdl.bot.model; +import java.time.LocalDateTime; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; -import java.time.LocalDateTime; - @Data @NoArgsConstructor @AllArgsConstructor @@ -37,4 +36,4 @@ public void recordNewMessage() { this.messageCount++; this.lastMessageTime = LocalDateTime.now().toString(); } -} \ No newline at end of file +} diff --git a/code/src/main/java/org/segocode/bot/service/MessageService.java b/code/src/main/java/org/segocode/webdl/bot/service/MessageService.java similarity index 97% rename from code/src/main/java/org/segocode/bot/service/MessageService.java rename to code/src/main/java/org/segocode/webdl/bot/service/MessageService.java index 9a2c996..e91e25b 100644 --- a/code/src/main/java/org/segocode/bot/service/MessageService.java +++ b/code/src/main/java/org/segocode/webdl/bot/service/MessageService.java @@ -1,4 +1,4 @@ -package org.segocode.bot.service; +package org.segocode.webdl.bot.service; import org.telegram.telegrambots.meta.api.methods.send.SendMessage; import org.telegram.telegrambots.meta.api.methods.updatingmessages.DeleteMessage; diff --git a/code/src/main/java/org/segocode/bot/service/VideoService.java b/code/src/main/java/org/segocode/webdl/bot/service/VideoService.java similarity index 70% rename from code/src/main/java/org/segocode/bot/service/VideoService.java rename to code/src/main/java/org/segocode/webdl/bot/service/VideoService.java index 5a0064b..3019d9e 100644 --- a/code/src/main/java/org/segocode/bot/service/VideoService.java +++ b/code/src/main/java/org/segocode/webdl/bot/service/VideoService.java @@ -1,14 +1,12 @@ -package org.segocode.bot.service; +package org.segocode.webdl.bot.service; -import org.segocode.system.util.FileUtil; +import java.io.File; +import org.segocode.webdl.system.util.FileUtil; +import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.telegram.telegrambots.meta.api.methods.send.SendVideo; import org.telegram.telegrambots.meta.api.objects.InputFile; -import java.io.File; -import org.slf4j.Logger; - - public class VideoService { private static final Logger LOGGER = LoggerFactory.getLogger(VideoService.class); @@ -20,19 +18,19 @@ public class VideoService { * @return The SendVideo object configured with the chat ID and reply to message ID. */ public static SendVideo sendVideo(Long chatId, Integer replyToMessageId) { - String filePath = buildFilePath(replyToMessageId); - LOGGER.info("Locating video file for message ID {}: {}", replyToMessageId, filePath); + String filePath = buildFilePath(replyToMessageId); + LOGGER.info("Locating video file for message ID {}: {}", replyToMessageId, filePath); - File videoFile = FileUtil.locateVideoFile(filePath); - if (videoFile == null) { - throw new RuntimeException("Video file not found for message ID: " + replyToMessageId); - } - LOGGER.info("Video file located successfully: {}", videoFile.getPath()); + File videoFile = FileUtil.locateVideoFile(filePath); + if (videoFile == null) { + throw new RuntimeException("Video file not found for message ID: " + replyToMessageId); + } + LOGGER.info("Video file located successfully: {}", videoFile.getPath()); - SendVideo sendVideoRequest = createSendVideoRequest(chatId, replyToMessageId, videoFile); - LOGGER.info("Video is sending. Chat ID: {}, Reply to Message ID: {}", chatId, replyToMessageId); + SendVideo sendVideoRequest = createSendVideoRequest(chatId, replyToMessageId, videoFile); + LOGGER.info("Video is sending. Chat ID: {}, Reply to Message ID: {}", chatId, replyToMessageId); - return sendVideoRequest; + return sendVideoRequest; } /** @@ -60,6 +58,4 @@ private static SendVideo createSendVideoRequest(Long chatId, Integer replyToMess private static String buildFilePath(Integer replyToMessageId) { return "./downloads/" + replyToMessageId + ".mp4"; } - - } diff --git a/code/src/main/java/org/segocode/bot/util/MessageUtil.java b/code/src/main/java/org/segocode/webdl/bot/util/MessageUtil.java similarity index 90% rename from code/src/main/java/org/segocode/bot/util/MessageUtil.java rename to code/src/main/java/org/segocode/webdl/bot/util/MessageUtil.java index 66341ae..576f565 100644 --- a/code/src/main/java/org/segocode/bot/util/MessageUtil.java +++ b/code/src/main/java/org/segocode/webdl/bot/util/MessageUtil.java @@ -1,4 +1,4 @@ -package org.segocode.bot.util; +package org.segocode.webdl.bot.util; public class MessageUtil { public static String extractUrlFromMessage(String messageText) { diff --git a/code/src/main/java/org/segocode/webdl/panel/AdminController.java b/code/src/main/java/org/segocode/webdl/panel/AdminController.java new file mode 100644 index 0000000..c3e86f8 --- /dev/null +++ b/code/src/main/java/org/segocode/webdl/panel/AdminController.java @@ -0,0 +1,40 @@ +package org.segocode.webdl.panel; + +import com.google.gson.Gson; +import io.javalin.http.Context; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import org.eclipse.store.storage.embedded.types.EmbeddedStorageManager; +import org.segocode.webdl.bot.model.DataRootContainer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class AdminController { + private static final Logger LOGGER = LoggerFactory.getLogger(AdminController.class); + private final EmbeddedStorageManager storageManager; + + public AdminController(EmbeddedStorageManager storageManager) { + this.storageManager = storageManager; + } + + public void handleAdminRequest(Context ctx) { + ctx.contentType("text/html"); + + try { + InputStream inputStream = getClass().getResourceAsStream("/views/admin.html"); + String htmlContent = new String(inputStream.readAllBytes(), StandardCharsets.UTF_8); + String jsonData; + synchronized (storageManager) { + jsonData = new Gson().toJson(((DataRootContainer) storageManager.root()).getUsers()); + } + htmlContent = htmlContent.replace("{{!user_data}}", jsonData); + + ctx.result(htmlContent); + } catch (IOException e) { + LOGGER.error("Failed to read admin HTML file", e); + ctx.status(500); + ctx.result("Error loading admin panel: " + e.getMessage()); + } + } +} diff --git a/code/src/main/java/org/segocode/webdl/panel/PanelApplication.java b/code/src/main/java/org/segocode/webdl/panel/PanelApplication.java new file mode 100644 index 0000000..ab354cd --- /dev/null +++ b/code/src/main/java/org/segocode/webdl/panel/PanelApplication.java @@ -0,0 +1,28 @@ +package org.segocode.webdl.panel; + +import io.javalin.Javalin; +import org.eclipse.store.storage.embedded.types.EmbeddedStorageManager; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class PanelApplication { + private static final Logger LOGGER = LoggerFactory.getLogger(PanelApplication.class); + private static PanelApplication instance; + + private PanelApplication(EmbeddedStorageManager storageManager) { + AdminController adminController = new AdminController(storageManager); + Javalin app = Javalin.create().start(8080); + configureRoutes(app, adminController); + LOGGER.info("Panel application started on port 8080"); + } + + public static synchronized void start(EmbeddedStorageManager storageManager) { + if (instance == null) { + instance = new PanelApplication(storageManager); + } + } + + private void configureRoutes(Javalin app, AdminController adminController) { + app.get("/", adminController::handleAdminRequest); + } +} diff --git a/code/src/main/java/org/segocode/system/command/CommandExecutor.java b/code/src/main/java/org/segocode/webdl/system/command/CommandExecutor.java similarity index 83% rename from code/src/main/java/org/segocode/system/command/CommandExecutor.java rename to code/src/main/java/org/segocode/webdl/system/command/CommandExecutor.java index 164db28..be467a6 100644 --- a/code/src/main/java/org/segocode/system/command/CommandExecutor.java +++ b/code/src/main/java/org/segocode/webdl/system/command/CommandExecutor.java @@ -1,4 +1,4 @@ -package org.segocode.system.command; +package org.segocode.webdl.system.command; import java.io.File; import java.io.IOException; @@ -26,14 +26,16 @@ public static void executeCommand(String url, String uuid) throws Exception { // The -q option is important to prevent deadlocks by ensuring the output buffer is not filled. // TODO: make a StreamGobbler to handle buffered output and avoid deadlocks. - String[] command = { ytDlpCommand, "-q", "-S", "ext,res:720", "-o", outputPath, url }; + String[] command = {ytDlpCommand, "-q", "-S", "ext,res:720", "-o", outputPath, url}; int attempt = 0; while (attempt++ < MAX_RETRIES) { - LOGGER.info("Attempt {} of " + MAX_RETRIES, attempt); + LOGGER.info("Attempt {} of {}", attempt, MAX_RETRIES); try { Process process = new ProcessBuilder(command).start(); - if (process.waitFor(TIMEOUT, TimeUnit.SECONDS) ? process.exitValue() == 0 : process.destroyForcibly() == null) { + if (process.waitFor(TIMEOUT, TimeUnit.SECONDS) + ? process.exitValue() == 0 + : process.destroyForcibly() == null) { LOGGER.info("Download successful for URL: {}", url); return; } else { @@ -41,7 +43,7 @@ public static void executeCommand(String url, String uuid) throws Exception { process.destroyForcibly(); } } catch (IOException e) { - LOGGER.error("I/O error occurred on attempt {} of " + MAX_RETRIES, attempt, e); + LOGGER.error("I/O error occurred on attempt {} of {}", attempt, MAX_RETRIES, e); if (attempt >= MAX_RETRIES) { throw e; } diff --git a/code/src/main/java/org/segocode/system/util/FileUtil.java b/code/src/main/java/org/segocode/webdl/system/util/FileUtil.java similarity index 97% rename from code/src/main/java/org/segocode/system/util/FileUtil.java rename to code/src/main/java/org/segocode/webdl/system/util/FileUtil.java index fb2cc6d..dfcb8ee 100644 --- a/code/src/main/java/org/segocode/system/util/FileUtil.java +++ b/code/src/main/java/org/segocode/webdl/system/util/FileUtil.java @@ -1,4 +1,4 @@ -package org.segocode.system.util; +package org.segocode.webdl.system.util; import java.io.File; import java.nio.file.Files; @@ -9,7 +9,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; - public class FileUtil { private static final Logger LOGGER = LoggerFactory.getLogger(FileUtil.class); @@ -53,4 +52,4 @@ public static File locateVideoFile(String filePath) { } return null; } -} \ No newline at end of file +} From 6e1cb3d6f3a466ba8f87cd81701503aeb5c64f76 Mon Sep 17 00:00:00 2001 From: SegoCode <35817798+SegoCode@users.noreply.github.com> Date: Thu, 11 Jun 2026 10:12:54 +0200 Subject: [PATCH 44/72] Update pom.xml --- code/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code/pom.xml b/code/pom.xml index 0641747..b4c1b7d 100644 --- a/code/pom.xml +++ b/code/pom.xml @@ -65,7 +65,7 @@ - org.segocode.Main + org.segocode.webdl.Main From 4813e86da27089c63f50a3cc2e9750c2e2c13353 Mon Sep 17 00:00:00 2001 From: SegoCode <35817798+SegoCode@users.noreply.github.com> Date: Thu, 11 Jun 2026 14:36:47 +0200 Subject: [PATCH 45/72] Update README.md --- README.md | 50 +++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 39 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 3aa2210..2f64978 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,6 @@ # webdl -

About • @@ -15,30 +14,59 @@ Telegram bot in Java for downloading social media videos using yt-dlp ## Features -- Non-blocking message queue processing +- Non-blocking message queue processing with virtual threads -- Dynamic interaction with messages +- Dynamic interaction with Telegram messages (send, delete, edit) -- Panel with usage statistics +- Web panel with usage statistics on port 8080 + +- Automatic retry on download failures ## Quick Start & Information Webdl accepts a video URL, downloads it using [yt-dlp](https://github.com/yt-dlp/yt-dlp), and sends it back to the user as a video message. -Clone and run the project from source code. +### From source + ``` git clone https://github.com/SegoCode/webdl -cd webdl\code -mvn clean package +cd webdl/code +mvn clean package -DskipTests java -jar target/webdl.jar ``` -For Docker deployment, make sure to set up environment variables. -Use a temp volume for the download, it will delete after send. +### Docker + ``` -mvn clean package +cd webdl/code +mvn clean package -DskipTests docker build -t webdl-image . -docker run -e BOT_TOKEN=your-bot-token -p 8080:8080 -v /mnt/drive/data/webdl:/downloads --name webdl webdl-image +docker run -d \ + --name webdl \ + --restart unless-stopped \ + -e BOT_TOKEN=your-bot-token \ + -p 8080:8080 \ + -v /mnt/drive/data/webdl:/downloads \ + webdl-image +``` + +### Project structure + +``` +code/src/main/java/org/segocode/webdl/ +├── Main.java # Entry point +├── bot/ +│ ├── Webdlbot.java # Telegram long-polling bot +│ ├── constants/Messages.java # User-facing message strings +│ ├── model/{User,DataRootContainer}.java # EclipseStore persistence +│ ├── service/{MessageService,VideoService}.java +│ └── util/MessageUtil.java +├── panel/ +│ ├── PanelApplication.java # Javalin web server bootstrap +│ └── AdminController.java # Admin panel route handler +└── system/ + ├── command/CommandExecutor.java # yt-dlp subprocess with retry + └── util/FileUtil.java ``` From e80b82705bb767ed55583cab8d7737c17cf51df9 Mon Sep 17 00:00:00 2001 From: SegoCode <35817798+SegoCode@users.noreply.github.com> Date: Fri, 7 Aug 2026 11:26:45 +0200 Subject: [PATCH 46/72] new structure --- .github/FUNDING.yml | 11 --- .github/ISSUE_TEMPLATE/bug_report.yml | 21 ++-- .github/ISSUE_TEMPLATE/config.yml | 2 +- .github/workflows/do-not-merge.yml | 64 ------------- .github/workflows/generate-tag.yml | 95 ++++++------------- .github/workflows/gitleaks.yml | 31 +++--- .github/workflows/greetings.yml | 8 +- .github/workflows/java-verify.yml | 61 ++++++++++++ .../workflows/sync-from-develop-to-main.yml | 94 ++++++++++-------- .../workflows/sync-from-main-to-develop.yml | 27 +++--- .github/workflows/update-license.yml | 59 ------------ .github/workflows/warn-direct-pr.yml | 61 ++++++++++++ .gitignore | 2 +- code/Dockerfile | 1 + code/pom.xml | 32 +++---- 15 files changed, 269 insertions(+), 300 deletions(-) delete mode 100644 .github/FUNDING.yml delete mode 100644 .github/workflows/do-not-merge.yml create mode 100644 .github/workflows/java-verify.yml delete mode 100644 .github/workflows/update-license.yml create mode 100644 .github/workflows/warn-direct-pr.yml diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml deleted file mode 100644 index df40ac8..0000000 --- a/.github/FUNDING.yml +++ /dev/null @@ -1,11 +0,0 @@ -github: [SegoCode] -community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry -issuehunt: # Replace with a single IssueHunt username -ko_fi: # Replace with a single ko_fi username -liberapay: # Replace with a single Liberapay username -open_collective: # Replace with a single open_collective username -patreon: # Replace with a single Patreon username -tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel -polar: # Replace with a single polar username -buy_me_a_coffee: # Replace with a single buy_me_a_coffee username -custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 761c8d2..d039ba4 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -42,14 +42,21 @@ body: - type: dropdown attributes: - label: OS version + label: Platform + description: Where does the issue occur? options: - - Windows 11 - - Windows 10 - - Windows Other - - Linux Debian - - Linux Arch - - Linux Other + - Web — Chrome (Blink) + - Web — Firefox (Gecko) + - Web — Safari (WebKit) + - Web — Other + - Desktop — macOS + - Desktop — Windows + - Desktop — Linux + - Desktop — Other + - Mobile — iOS + - Mobile — Android + - Mobile — Other + - Other validations: required: true diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index b242a1b..2056bc5 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,5 +1,5 @@ blank_issues_enabled: false contact_links: - name: Contact the developer - url: https://SegoCode.github.io/SegoCode/ + url: https://SegoCode.github.io/SegoCode/ about: To discuss any type of related topic diff --git a/.github/workflows/do-not-merge.yml b/.github/workflows/do-not-merge.yml deleted file mode 100644 index a6c326b..0000000 --- a/.github/workflows/do-not-merge.yml +++ /dev/null @@ -1,64 +0,0 @@ -name: Do not merge - -on: - pull_request: - types: [opened, edited, labeled, unlabeled, synchronize, ready_for_review] - -jobs: - checks: - runs-on: ubuntu-latest - steps: - - name: Check if PR is ready to be merged - uses: actions/github-script@v6 - with: - script: | - const forbiddenWords = ['wip', '[wip]', 'work in progress', 'in progress', 'do not merge', 'do-not-merge', 'do_not_merge', 'dont merge', 'draft', '🚧', 'needs work']; - const pr = context.payload.pull_request; - const prTitle = pr.title.toLowerCase(); - const labels = pr.labels.map(label => label.name.toLowerCase()); - const isDraft = pr.draft; - const hasForbiddenLabel = labels.some(label => forbiddenWords.some(word => label.includes(word))); - const hasForbiddenTitle = forbiddenWords.some(word => prTitle.includes(word)); - if (isDraft || hasForbiddenLabel || hasForbiddenTitle) { - core.setFailed('The Pull Request is in draft mode or has labels or title indicating it should not be merged.'); - } else { - console.log('The Pull Request is ready to be merged.'); - } - - - name: Check if PR is from develop to main - uses: actions/github-script@v6 - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - const pr = context.payload.pull_request; - const baseBranch = pr.base.ref; - const headBranch = pr.head.ref; - - if (headBranch === 'develop' && baseBranch === 'main') { - const repoFullName = context.payload.repository.full_name; - const syncWorkflowLink = `https://github.com/${repoFullName}/actions/workflows/sync-from-develop-to-main.yml`; - - const message = ` - ## ⚠️ Direct PR from develop to main detected - - This pull request is merging changes directly from \`develop\` to \`main\`. - - ### Important Note - Please consider using our [sync workflow](${syncWorkflowLink}) instead of this direct PR. - - **If you don't use the sync workflow:** - - This PR will not trigger a release - - You'll need to manually handle release processes - `; - - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: pr.number, - body: message - }); - - console.log('Added comment to PR from develop to main'); - } else { - console.log('PR is not from develop to main, no action needed'); - } diff --git a/.github/workflows/generate-tag.yml b/.github/workflows/generate-tag.yml index 79de8b0..7560e2c 100644 --- a/.github/workflows/generate-tag.yml +++ b/.github/workflows/generate-tag.yml @@ -1,98 +1,63 @@ name: Generate tag -on: +on: pull_request: types: [closed] +concurrency: + group: generate-tag + jobs: create_tag: - if: github.event.pull_request.merged == true && contains(github.event.pull_request.labels.*.name, 'auto-tag') + if: github.event.pull_request.merged == true && github.event.pull_request.base.ref == 'main' && contains(github.event.pull_request.labels.*.name, 'auto-tag') runs-on: ubuntu-latest + permissions: + contents: write + outputs: + new_version: ${{ steps.determine_version.outputs.new_version }} steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v7 with: - fetch-depth: 0 # Fetch all history for tag retrieval + fetch-depth: 0 - name: Set up git run: | - git config --global user.name "github-actions" - git config --global user.email "github-actions@github.com" - - - name: Fetch all tags - run: git fetch --tags - - - name: Get latest tag - id: get_latest_tag - run: | - # Get the latest tag - latest_tag=$(git describe --tags `git rev-list --tags --max-count=1` 2>/dev/null || echo "") - echo "latest_tag=$latest_tag" >> $GITHUB_ENV + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - name: Determine new version id: determine_version run: | - latest_tag=${{ env.latest_tag }} + latest_tag=$(git tag --list --sort=-version:refname | grep -m1 -E '^[0-9]+\.[0-9]+$' || true) if [ -z "$latest_tag" ]; then - # Initialize the version to 1.0 if no tags exist new_version="1.0" else - # Extract the major and minor version and increment the minor version - major_version=$(echo $latest_tag | cut -d. -f1) - minor_version=$(echo $latest_tag | cut -d. -f2) - new_minor_version=$((minor_version + 1)) - new_version="$major_version.$new_minor_version" - - # Check if the new version tag already exists - while git rev-parse "refs/tags/$new_version" >/dev/null 2>&1; do - new_minor_version=$((new_minor_version + 1)) - new_version="$major_version.$new_minor_version" - done + IFS=. read -r major minor <<< "$latest_tag" + if [[ "${{ contains(github.event.pull_request.labels.*.name, 'semver:major') }}" == "true" ]]; then + new_version="$((major + 1)).0" + else + new_version="$major.$((minor + 1))" + fi fi - echo "new_version=$new_version" >> $GITHUB_ENV - - - name: Checkout main branch - run: | - git checkout main + echo "new_version=$new_version" >> "$GITHUB_OUTPUT" - name: Create new tag - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | - new_version=${{ env.new_version }} - git tag -a $new_version -m "Automatically generated version $new_version" - git push origin $new_version + new_version="${{ steps.determine_version.outputs.new_version }}" + git tag -a "$new_version" -m "Release $new_version" "${{ github.event.pull_request.merge_commit_sha }}" + git push origin "$new_version" create_issue: needs: create_tag runs-on: ubuntu-latest + permissions: + issues: write steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - fetch-depth: 0 # Fetch all history for tag retrieval - - - name: Set up git - run: | - git config --global user.name "github-actions" - git config --global user.email "github-actions@github.com" - - - name: Fetch all tags - run: git fetch --tags - - - name: Get the latest tag - id: get_latest_tag - run: | - # Get the latest tag - latest_tag=$(git describe --tags `git rev-list --tags --max-count=1` 2>/dev/null || echo "") - echo "latest_tag=$latest_tag" >> $GITHUB_ENV - - name: Create issue for new tag env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - latest_tag: ${{ env.latest_tag }} + GH_TOKEN: ${{ github.token }} run: | - repository=${{ github.repository }} - issue_title="The tag \`${{ env.latest_tag }}\` was created" - issue_body=$'The **${{ env.latest_tag }}** tag for the **main branch** has been created. Please consider creating a release of this tag, if a release isn\'t needed, you can close this issue.\n\n[Click here to create a release of **${{ env.latest_tag }}** tag](../releases/new?tag=${{ env.latest_tag }})' - gh issue create --title "$issue_title" --body "$issue_body" --label "auto-tag" + issue_title="The tag \`${{ needs.create_tag.outputs.new_version }}\` was created" + issue_body=$'The **${{ needs.create_tag.outputs.new_version }}** tag for the **main branch** has been created.\nPlease consider creating a release of this tag, if a release isn\'t needed, you can close this issue.\n\n[Click here to create a release of **${{ needs.create_tag.outputs.new_version }}** tag](../releases/new?tag=${{ needs.create_tag.outputs.new_version }})' + gh issue create --repo "${{ github.repository }}" --title "$issue_title" --body "$issue_body" --label auto-tag diff --git a/.github/workflows/gitleaks.yml b/.github/workflows/gitleaks.yml index f53dea4..f16f194 100644 --- a/.github/workflows/gitleaks.yml +++ b/.github/workflows/gitleaks.yml @@ -1,46 +1,42 @@ name: Gitleaks on: + push: pull_request: workflow_dispatch: - schedule: - # Run at 00:00 UTC on the first day of each month - - cron: '0 0 1 * *' jobs: scan: name: Gitleaks Scan runs-on: ubuntu-latest - # Only run scheduled jobs on main branch - if: github.event_name != 'schedule' || github.ref == 'refs/heads/main' + permissions: + contents: read + issues: write steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v7 with: fetch-depth: 0 - name: Run Gitleaks id: gitleaks - uses: gitleaks/gitleaks-action@v2 + uses: gitleaks/gitleaks-action@v3 env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_TOKEN: ${{ github.token }} continue-on-error: true - name: Create issue if leaks found if: steps.gitleaks.outcome == 'failure' && (github.ref == 'refs/heads/main' || github.event_name == 'workflow_dispatch') env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_TOKEN: ${{ github.token }} run: | echo "Creating detailed security issue report..." - # Get current timestamp TIMESTAMP=$(date -u +"%Y-%m-%d %H:%M:%S UTC") - # Create an enriched issue body ISSUE_BODY="## 🚨 Security Alert: Potential Secrets Detected **Detection Time:** ${TIMESTAMP} **Branch:** ${GITHUB_REF#refs/heads/} - **Triggered by:** ${{ github.event_name == 'workflow_dispatch' && 'Manual workflow run' || (github.event_name == 'schedule' && 'Monthly scheduled scan' || 'Automated scan') }} **Detected by:** Gitleaks Security Scanner ### Details @@ -55,13 +51,14 @@ jobs: 3. Remove the secrets from the codebase 4. Consider using GitHub Secrets or environment variables instead - ### Contact - Please reach out to the security team for assistance if needed. - --- *This issue was automatically generated by the Gitleaks security scanning workflow.*" gh issue create \ - --repo ${{ github.repository }} \ - --title "Security Alert: Potential secrets detected in ${{ github.event_name == 'pull_request' && format('PR #{0}', github.event.pull_request.number) || 'main branch' }}" \ + --repo "${{ github.repository }}" \ + --title "Security Alert: Potential secrets detected in main branch" \ --body "${ISSUE_BODY}" + + - name: Fail if leaks were found + if: steps.gitleaks.outcome == 'failure' + run: exit 1 diff --git a/.github/workflows/greetings.yml b/.github/workflows/greetings.yml index 724954b..2450362 100644 --- a/.github/workflows/greetings.yml +++ b/.github/workflows/greetings.yml @@ -9,8 +9,8 @@ jobs: issues: write pull-requests: write steps: - - uses: actions/first-interaction@v1 + - uses: actions/first-interaction@v3.1.0 with: - repo-token: ${{ secrets.GITHUB_TOKEN }} - issue-message: "Thank you for your first issue. To better understand your request or the problem you've encountered, please provide as many details as possible. If the behavior changes or if you have new information about your request, don't hesitate to add it. It will be reviewed ASAP." - pr-message: "Thank you for your first pull request to the repository! We're grateful for your contribution and will review it ASAP." + repo_token: ${{ secrets.GITHUB_TOKEN }} + issue_message: "Thank you for your first issue. To better understand your request or the problem you've encountered, please provide as many details as possible. If the behavior changes or if you have new information about your request, don't hesitate to add it. It will be reviewed ASAP." + pr_message: "Thank you for your first pull request to the repository! We're grateful for your contribution and will review it ASAP." diff --git a/.github/workflows/java-verify.yml b/.github/workflows/java-verify.yml new file mode 100644 index 0000000..bd20429 --- /dev/null +++ b/.github/workflows/java-verify.yml @@ -0,0 +1,61 @@ +name: Java verify + +on: + pull_request: + workflow_dispatch: + workflow_call: + inputs: + working-directory: + description: Directory containing the Maven project + required: false + type: string + default: '' + java-version: + description: Java version used to build and test the project + required: false + type: string + default: '21' + +permissions: + contents: read + +concurrency: + group: java-verify-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + verify: + name: Compile and test + runs-on: ubuntu-latest + steps: + - name: Check out repository + uses: actions/checkout@v5 + + - name: Find Maven project + id: project + env: + CONFIGURED_DIRECTORY: ${{ inputs.working-directory }} + run: | + if [ -n "$CONFIGURED_DIRECTORY" ]; then + directory="$CONFIGURED_DIRECTORY" + elif [ -f pom.xml ]; then + directory="." + elif [ -f code/pom.xml ]; then + directory="code" + else + echo "No pom.xml found" >&2 + exit 1 + fi + echo "directory=$directory" >> "$GITHUB_OUTPUT" + + - name: Set up Java + uses: actions/setup-java@v5 + with: + distribution: temurin + java-version: ${{ inputs.java-version || '21' }} + cache: maven + cache-dependency-path: ${{ steps.project.outputs.directory }}/pom.xml + + - name: Verify Maven project + working-directory: ${{ steps.project.outputs.directory }} + run: mvn --batch-mode --no-transfer-progress verify diff --git a/.github/workflows/sync-from-develop-to-main.yml b/.github/workflows/sync-from-develop-to-main.yml index 7baa0b8..d684b83 100644 --- a/.github/workflows/sync-from-develop-to-main.yml +++ b/.github/workflows/sync-from-develop-to-main.yml @@ -1,52 +1,68 @@ name: Sync from develop to main -on: +on: workflow_dispatch: + inputs: + release: + description: Version to publish when the pull request is merged + required: true + default: minor + type: choice + options: + - minor + - major + - none + +concurrency: + group: promote-develop-to-main jobs: create_pr: runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: write + issues: write steps: - - name: Checkout repository - uses: actions/checkout@v2 - - - name: Setup git - run: | - git config --global user.name "github-actions" - git config --global user.email "github-actions@github.com" - - - name: Install GitHub CLI - run: | - sudo apt-get update - sudo apt-get install gh -y - - name: Check and create labels env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_TOKEN: ${{ github.token }} run: | - # Check if the 'auto-sync' label exists - AUTO_SYNC_LABEL=$(gh api repos/${{ github.repository }}/labels --jq '.[] | select(.name=="auto-sync")') - if [ -z "$AUTO_SYNC_LABEL" ]; then - # Create the 'auto-sync' label if it doesn't exist - gh api repos/${{ github.repository }}/labels -f name='auto-sync' -f color='C5DEF5' - fi - - # Check if the 'auto-tag' label exists - AUTO_TAG_LABEL=$(gh api repos/${{ github.repository }}/labels --jq '.[] | select(.name=="auto-tag")') - if [ -z "$AUTO_TAG_LABEL" ]; then - # Create the 'auto-tag' label if it doesn't exist - gh api repos/${{ github.repository }}/labels -f name='auto-tag' -f color='BFDADC' - fi - - - name: Create pull request + gh label create auto-sync --repo "${{ github.repository }}" --color C5DEF5 --force + gh label create auto-tag --repo "${{ github.repository }}" --color BFDADC --force + gh label create semver:major --repo "${{ github.repository }}" --color B60205 --force + + - name: Replace pull request env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_TOKEN: ${{ github.token }} run: | - # Create the pull request - PR_URL=$(gh pr create --base main --head develop --title "Sync from develop to main" --body $'This is an automated pull request to sync changes from the develop branch to the main branch. \n >[!WARNING]\n> If this pull request is merged with the \'**auto-tag**\' label, it will create a **new tag**. To prevent this, manually remove the \'**auto-tag**\' label.') - - # Extract PR number from URL - PR_NUMBER=$(basename $PR_URL) - - # Add the labels to the pull request - gh pr edit $PR_NUMBER --add-label "auto-sync" --add-label "auto-tag" + PR_BODY="> [!WARNING] + > This is an automated pull request to sync changes from the \`develop\` branch to the \`main\` branch. + > If this pull request is merged with the **auto-tag** label, it will create a **new tag**. + > To prevent this, manually remove the **auto-tag** label." + + gh pr list --repo "${{ github.repository }}" --base main --head develop --state open \ + --json number --jq '.[].number' | while read -r PR_NUMBER; do + gh pr close "$PR_NUMBER" --repo "${{ github.repository }}" + done + + PR_NUMBER=$(gh pr create --repo "${{ github.repository }}" --base main --head develop \ + --title "chore: promote develop to main" \ + --body "$PR_BODY" | sed 's#.*/##') + + gh pr edit "$PR_NUMBER" --repo "${{ github.repository }}" --add-label auto-sync + case "${{ inputs.release }}" in + major) + gh pr edit "$PR_NUMBER" --repo "${{ github.repository }}" --add-label auto-tag --add-label semver:major + ;; + minor) + gh pr edit "$PR_NUMBER" --repo "${{ github.repository }}" --remove-label semver:major 2>/dev/null || true + gh pr edit "$PR_NUMBER" --repo "${{ github.repository }}" --add-label auto-tag + ;; + none) + gh pr edit "$PR_NUMBER" --repo "${{ github.repository }}" --remove-label auto-tag 2>/dev/null || true + gh pr edit "$PR_NUMBER" --repo "${{ github.repository }}" --remove-label semver:major 2>/dev/null || true + ;; + esac + + gh pr view "$PR_NUMBER" --repo "${{ github.repository }}" --json url --jq .url diff --git a/.github/workflows/sync-from-main-to-develop.yml b/.github/workflows/sync-from-main-to-develop.yml index 7d43073..b17a2b9 100644 --- a/.github/workflows/sync-from-main-to-develop.yml +++ b/.github/workflows/sync-from-main-to-develop.yml @@ -5,28 +5,33 @@ on: branches: - main +concurrency: + group: sync-main-to-develop + jobs: sync_main_into_develop: runs-on: ubuntu-latest permissions: - contents: write + contents: write steps: - - name: Checkout develop branch - uses: actions/checkout@v4 + - name: Checkout repository + uses: actions/checkout@v7 with: - ref: develop + ref: main fetch-depth: 0 - name: Configure Git run: | - git config --global user.name "github-actions" - git config --global user.email "github-actions@github.com" + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - name: Merge main into develop run: | - git fetch origin main - git merge origin/main --no-edit - - - name: Push changes to develop - run: | + if ! git ls-remote --exit-code --heads origin develop; then + echo "develop does not exist yet; initialization will create it." + exit 0 + fi + git fetch origin develop + git checkout -B develop origin/develop + git merge "$GITHUB_SHA" --no-edit git push origin develop diff --git a/.github/workflows/update-license.yml b/.github/workflows/update-license.yml deleted file mode 100644 index 43f5ac4..0000000 --- a/.github/workflows/update-license.yml +++ /dev/null @@ -1,59 +0,0 @@ -name: Update license - -on: - workflow_dispatch: - schedule: - # Run at 00:00 UTC on the first day of each month - - cron: '0 0 1 * *' - -jobs: - update-license: - runs-on: ubuntu-latest - permissions: - contents: write - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - ref: develop - - - name: Setup git - run: | - git config --global user.name "github-actions" - git config --global user.email "github-actions@github.com" - - - name: Extract repository and username - id: extract - run: | - REPO_NAME="${{ github.repository }}" - USERNAME=$(echo $REPO_NAME | cut -d'/' -f1) - REPO_NAME_ONLY=$(echo $REPO_NAME | cut -d'/' -f2) - echo "username=$USERNAME" >> $GITHUB_OUTPUT - echo "reponame=$REPO_NAME_ONLY" >> $GITHUB_OUTPUT - - - name: Download LICENSE template - run: | - curl -s https://raw.githubusercontent.com/SegoCode/template/main/LICENSE -o LICENSE - - - name: Replace placeholders in LICENSE file - run: | - REPO_NAME_ONLY="${{ steps.extract.outputs.reponame }}" - USERNAME="${{ steps.extract.outputs.username }}" - REPO_NAME_ESCAPED=$(echo $REPO_NAME_ONLY | sed 's/\//\\\//g') - USERNAME_ESCAPED=$(echo $USERNAME | sed 's/\//\\\//g') - - # Replace placeholders with actual values - sed -i "s/{reponame}/$REPO_NAME_ESCAPED/g" LICENSE - sed -i "s/{username}/$USERNAME_ESCAPED/g" LICENSE - - - name: Commit and push changes - run: | - # Add LICENSE file - git add LICENSE - - # Only commit if there are changes - git diff --staged --quiet || git commit -m "Update LICENSE with repository information" - - # Push directly to develop branch - git push origin develop diff --git a/.github/workflows/warn-direct-pr.yml b/.github/workflows/warn-direct-pr.yml new file mode 100644 index 0000000..8d4c155 --- /dev/null +++ b/.github/workflows/warn-direct-pr.yml @@ -0,0 +1,61 @@ +name: Warn direct PR + +on: + pull_request_target: + types: [opened] + +concurrency: + group: warn-manual-develop-to-main-${{ github.event.pull_request.number }} + +jobs: + warn: + runs-on: ubuntu-latest + permissions: + issues: write + pull-requests: write + steps: + - name: Warn about manual promotion PR + uses: actions/github-script@v9 + with: + script: | + const pr = context.payload.pull_request; + const isPromotion = pr.base.ref === 'main' && pr.head.ref === 'develop'; + const isAutomated = pr.user.type === 'Bot'; + const hasAutoSync = pr.labels.some(({ name }) => name.toLowerCase() === 'auto-sync'); + + if (!isPromotion || isAutomated || hasAutoSync) { + return; + } + + const marker = ''; + const comments = await github.paginate(github.rest.issues.listComments, { + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: pr.number, + per_page: 100 + }); + + if (comments.some(({ body }) => body?.includes(marker))) { + return; + } + + const workflowUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/workflows/sync-from-develop-to-main.yml`; + const body = [ + marker, + '> [!WARNING]', + '> This pull request was opened manually from `develop` to `main` without the `auto-sync` label.', + '>', + `> The [Sync from develop to main workflow](${workflowUrl}) will close this PR and create a new promotion PR.`, + '> Select the release type:', + '>', + '> - `minor`: create the next minor tag.', + '> - `major`: create the next major tag.', + '> - `none`: merge without creating a tag.' + ].join('\n'); + + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: pr.number, + body + }); diff --git a/.gitignore b/.gitignore index 8c11a6b..4f9b440 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,8 @@ ### Custom ### code/.idea/* -code/storage/* code/target/* +code/downloads/* # Created by https://www.toptal.com/developers/gitignore/api/git,gpg,ssh,vim,linux,macos,windows,notepadpp,sublimetext,intellij+all,visualstudiocode,dotenv # Edit at https://www.toptal.com/developers/gitignore?templates=git,gpg,ssh,vim,linux,macos,windows,notepadpp,sublimetext,intellij+all,visualstudiocode,dotenv diff --git a/code/Dockerfile b/code/Dockerfile index 63189a7..bd1111b 100644 --- a/code/Dockerfile +++ b/code/Dockerfile @@ -15,5 +15,6 @@ RUN apk add --no-cache ffmpeg python3 && \ chmod a+rx /usr/local/bin/yt-dlp WORKDIR /app +RUN mkdir downloads COPY --from=build /app/target/webdl.jar /app/webdl.jar ENTRYPOINT ["java", "-jar", "/app/webdl.jar"] diff --git a/code/pom.xml b/code/pom.xml index b4c1b7d..8b8c7a8 100644 --- a/code/pom.xml +++ b/code/pom.xml @@ -9,8 +9,7 @@ 1.0 - 21 - 21 + 21 UTF-8 @@ -26,31 +25,21 @@ 1.4.14 - org.projectlombok - lombok - 1.18.36 - provided - - - org.eclipse.store - storage-embedded - 2.1.2 - - - io.javalin - javalin - 6.5.0 - - - com.google.code.gson - gson - 2.12.1 + org.junit.jupiter + junit-jupiter + 6.1.2 + test webdl + + org.apache.maven.plugins + maven-surefire-plugin + 3.5.4 + org.apache.maven.plugins maven-shade-plugin @@ -62,6 +51,7 @@ shade + false From fab9458ed7b5e059e5747d013da661a97f6be693 Mon Sep 17 00:00:00 2001 From: SegoCode <35817798+SegoCode@users.noreply.github.com> Date: Fri, 7 Aug 2026 11:26:50 +0200 Subject: [PATCH 47/72] Create DownloadQueue.java --- .../org/segocode/webdl/bot/DownloadQueue.java | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 code/src/main/java/org/segocode/webdl/bot/DownloadQueue.java diff --git a/code/src/main/java/org/segocode/webdl/bot/DownloadQueue.java b/code/src/main/java/org/segocode/webdl/bot/DownloadQueue.java new file mode 100644 index 0000000..ea04fa1 --- /dev/null +++ b/code/src/main/java/org/segocode/webdl/bot/DownloadQueue.java @@ -0,0 +1,28 @@ +package org.segocode.webdl.bot; + +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +final class DownloadQueue implements AutoCloseable { + private final ThreadPoolExecutor executor = new ThreadPoolExecutor( + 1, + 1, + 0L, + TimeUnit.MILLISECONDS, + new LinkedBlockingQueue<>(), + Thread.ofVirtual().factory()); + + int pendingTasks() { + return executor.getActiveCount() + executor.getQueue().size(); + } + + void submit(Runnable task) { + executor.submit(task); + } + + @Override + public void close() { + executor.shutdownNow(); + } +} From 2636f3b74a76a22d89eba0f457e46ac49bbd8691 Mon Sep 17 00:00:00 2001 From: SegoCode <35817798+SegoCode@users.noreply.github.com> Date: Fri, 7 Aug 2026 11:26:52 +0200 Subject: [PATCH 48/72] Delete DataRootContainer.java --- .../webdl/bot/model/DataRootContainer.java | 25 ------------------- 1 file changed, 25 deletions(-) delete mode 100644 code/src/main/java/org/segocode/webdl/bot/model/DataRootContainer.java diff --git a/code/src/main/java/org/segocode/webdl/bot/model/DataRootContainer.java b/code/src/main/java/org/segocode/webdl/bot/model/DataRootContainer.java deleted file mode 100644 index 23c0951..0000000 --- a/code/src/main/java/org/segocode/webdl/bot/model/DataRootContainer.java +++ /dev/null @@ -1,25 +0,0 @@ -package org.segocode.webdl.bot.model; - -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; - -@Data -@NoArgsConstructor -@AllArgsConstructor -public class DataRootContainer { - private List users = new ArrayList<>(); - - /** - * Finds a user by their ID - * - * @param id The ID of the user to find - * @return An Optional containing the user if found, empty otherwise - */ - public Optional findUserById(String id) { - return users.stream().filter(user -> id.equals(user.getId())).findFirst(); - } -} From 1fbfbdf148910f90b9bb2ffe5cc8ed48e076ab5a Mon Sep 17 00:00:00 2001 From: SegoCode <35817798+SegoCode@users.noreply.github.com> Date: Fri, 7 Aug 2026 11:26:54 +0200 Subject: [PATCH 49/72] Delete User.java --- .../org/segocode/webdl/bot/model/User.java | 39 ------------------- 1 file changed, 39 deletions(-) delete mode 100644 code/src/main/java/org/segocode/webdl/bot/model/User.java diff --git a/code/src/main/java/org/segocode/webdl/bot/model/User.java b/code/src/main/java/org/segocode/webdl/bot/model/User.java deleted file mode 100644 index 481a310..0000000 --- a/code/src/main/java/org/segocode/webdl/bot/model/User.java +++ /dev/null @@ -1,39 +0,0 @@ -package org.segocode.webdl.bot.model; - -import java.time.LocalDateTime; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; - -@Data -@NoArgsConstructor -@AllArgsConstructor -public class User { - // Unique identifiers - private String id; - private Long chatId; - private String userName; - - // Personal information - private String firstName; - private String lastName; - - // Preferences and metadata - private String languageCode; - private Boolean isPremium; - - // Usage statistics - private Integer messageCount; - private String lastMessageTime; - - /** - * Increments the message count and updates the last message time to current time - */ - public void recordNewMessage() { - if (this.messageCount == null) { - this.messageCount = 0; - } - this.messageCount++; - this.lastMessageTime = LocalDateTime.now().toString(); - } -} From 7c79c76514935788301bfcc773dde29d7267cdae Mon Sep 17 00:00:00 2001 From: SegoCode <35817798+SegoCode@users.noreply.github.com> Date: Fri, 7 Aug 2026 11:26:56 +0200 Subject: [PATCH 50/72] Update VideoService.java --- .../webdl/bot/service/VideoService.java | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/code/src/main/java/org/segocode/webdl/bot/service/VideoService.java b/code/src/main/java/org/segocode/webdl/bot/service/VideoService.java index 3019d9e..4c6cf42 100644 --- a/code/src/main/java/org/segocode/webdl/bot/service/VideoService.java +++ b/code/src/main/java/org/segocode/webdl/bot/service/VideoService.java @@ -1,6 +1,8 @@ package org.segocode.webdl.bot.service; import java.io.File; +import java.nio.file.Path; +import java.nio.file.Paths; import org.segocode.webdl.system.util.FileUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -18,7 +20,12 @@ public class VideoService { * @return The SendVideo object configured with the chat ID and reply to message ID. */ public static SendVideo sendVideo(Long chatId, Integer replyToMessageId) { - String filePath = buildFilePath(replyToMessageId); + return sendVideo(chatId, replyToMessageId, Paths.get("./downloads")); + } + + static SendVideo sendVideo(Long chatId, Integer replyToMessageId, Path downloadsDirectory) { + String filePath = + downloadsDirectory.resolve(replyToMessageId.toString()).toString(); LOGGER.info("Locating video file for message ID {}: {}", replyToMessageId, filePath); File videoFile = FileUtil.locateVideoFile(filePath); @@ -48,14 +55,4 @@ private static SendVideo createSendVideoRequest(Long chatId, Integer replyToMess sendVideoRequest.setVideo(new InputFile(videoFile)); return sendVideoRequest; } - - /** - * Builds the file path for the video file based on the given replyToMessageId. - * - * @param replyToMessageId The ID of the message to which this video will be a reply. - * @return The file path as a String. - */ - private static String buildFilePath(Integer replyToMessageId) { - return "./downloads/" + replyToMessageId + ".mp4"; - } } From c5c47c0a5b9746fbfafde71f6dd43bf7d26ce6ab Mon Sep 17 00:00:00 2001 From: SegoCode <35817798+SegoCode@users.noreply.github.com> Date: Fri, 7 Aug 2026 11:26:58 +0200 Subject: [PATCH 51/72] Update MessageUtil.java --- .../org/segocode/webdl/bot/util/MessageUtil.java | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/code/src/main/java/org/segocode/webdl/bot/util/MessageUtil.java b/code/src/main/java/org/segocode/webdl/bot/util/MessageUtil.java index 576f565..2fa1e2e 100644 --- a/code/src/main/java/org/segocode/webdl/bot/util/MessageUtil.java +++ b/code/src/main/java/org/segocode/webdl/bot/util/MessageUtil.java @@ -1,12 +1,13 @@ package org.segocode.webdl.bot.util; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + public class MessageUtil { + private static final Pattern URL_PATTERN = Pattern.compile("https?://\\S+"); + public static String extractUrlFromMessage(String messageText) { - int startIndex = messageText.indexOf("http"); - int endIndex = messageText.indexOf(" ", startIndex); - if (endIndex == -1) { - endIndex = messageText.length(); - } - return messageText.substring(startIndex, endIndex); + Matcher matcher = URL_PATTERN.matcher(messageText); + return matcher.find() ? matcher.group() : null; } } From e5ddcc8260656261abbe5214418706ea257a7828 Mon Sep 17 00:00:00 2001 From: SegoCode <35817798+SegoCode@users.noreply.github.com> Date: Fri, 7 Aug 2026 11:27:01 +0200 Subject: [PATCH 52/72] Update Webdlbot.java --- .../java/org/segocode/webdl/bot/Webdlbot.java | 108 ++++-------------- 1 file changed, 23 insertions(+), 85 deletions(-) diff --git a/code/src/main/java/org/segocode/webdl/bot/Webdlbot.java b/code/src/main/java/org/segocode/webdl/bot/Webdlbot.java index 7f77268..0830576 100644 --- a/code/src/main/java/org/segocode/webdl/bot/Webdlbot.java +++ b/code/src/main/java/org/segocode/webdl/bot/Webdlbot.java @@ -4,12 +4,6 @@ import static org.segocode.webdl.bot.util.MessageUtil.*; import static org.segocode.webdl.system.util.FileUtil.*; -import java.time.LocalDateTime; -import java.util.Optional; -import java.util.concurrent.*; -import org.eclipse.store.storage.types.StorageManager; -import org.segocode.webdl.bot.model.DataRootContainer; -import org.segocode.webdl.bot.model.User; import org.segocode.webdl.bot.service.MessageService; import org.segocode.webdl.bot.service.VideoService; import org.segocode.webdl.system.command.CommandExecutor; @@ -24,33 +18,7 @@ public class Webdlbot extends TelegramLongPollingBot { private static final Logger LOGGER = LoggerFactory.getLogger(Webdlbot.class); private static final String BOT_TOKEN = System.getenv("BOT_TOKEN"); - private final StorageManager storageManager; - - // Create a ThreadPoolExecutor with a single virtual thread - private final ThreadPoolExecutor executorService = new ThreadPoolExecutor( - 1, // corePoolSize: the number of threads to keep in the pool, even if they are idle - 1, // maximumPoolSize: the maximum number of threads to allow in the pool - 0L, // keepAliveTime: when the number of threads is greater than the core - TimeUnit.MILLISECONDS, // the time unit for the keepAliveTime argument - new LinkedBlockingQueue<>(), // the queue to use for holding tasks before they are executed - Thread.ofVirtual().factory() // the factory to use when creating new threads - ); - - public Webdlbot(StorageManager storageManager) { - this.storageManager = storageManager; - initializeRootContainer(); - } - - private void initializeRootContainer() { - Object root = storageManager.root(); - if (!(root instanceof DataRootContainer)) { - storageManager.setRoot(new DataRootContainer()); - LOGGER.info("Initialized new DataRootContainer as db root object"); - } - LOGGER.info( - "Root db object contains {} users", - ((DataRootContainer) storageManager.root()).getUsers().size()); - } + private final DownloadQueue downloadQueue = new DownloadQueue(); @Override public String getBotUsername() { @@ -59,7 +27,7 @@ public String getBotUsername() { @Override public String getBotToken() { - if (BOT_TOKEN == null || BOT_TOKEN.isEmpty()) { + if (BOT_TOKEN == null || BOT_TOKEN.isBlank()) { LOGGER.error("BOT_TOKEN is not set in the environment variables."); throw new IllegalStateException("BOT_TOKEN is not set in the environment variables."); } @@ -70,7 +38,7 @@ public String getBotToken() { public void onUpdateReceived(Update update) { if (update.hasMessage() && update.getMessage().hasText()) { try { - long queuedTasks = executorService.getTaskCount() - executorService.getCompletedTaskCount(); + int queuedTasks = downloadQueue.pendingTasks(); Integer queuedMessageId; if (queuedTasks > 0) { String messageTime = DOWNLOAD_REQUEST_QUEUED + " (<" + queuedTasks + "m)"; @@ -86,56 +54,51 @@ public void onUpdateReceived(Update update) { queuedMessageId = null; } - executorService.submit(() -> { + downloadQueue.submit(() -> { try { if (queuedMessageId != null) { - execute(MessageService.deleteMessage( - update.getMessage().getChatId(), queuedMessageId)); + deleteMessage(update.getMessage().getChatId(), queuedMessageId); } - // Entry point download flow dispatch(update); } catch (Exception e) { LOGGER.error("Failed to launch dispatch, error: {}", e.getMessage(), e); - handleDispatchError(update, e); + handleDispatchError(update); } finally { - synchronized (storageManager) { - storageManager.setRoot(loadMetricsData(update)); - storageManager.storeRoot(); - } + cleanDownloadsFolder(); } }); } catch (Exception e) { LOGGER.error("Failed on onUpdateReceived, error: {}", e.getMessage(), e); - handleDispatchError(update, e); + handleDispatchError(update); } } } private void dispatch(Update update) throws Exception { - String url = ""; Message message = update.getMessage(); LOGGER.info( "Starting message processing from @{}: {}", message.getFrom().getUserName(), message.getText()); - if (!update.getMessage().getText().contains("http")) { + String url = extractUrlFromMessage(update.getMessage().getText()); + if (url == null) { execute(MessageService.sendTextMessage(message.getChatId(), message.getMessageId(), NOT_VALID_LINK)) .getMessageId(); return; - } else { - url = extractUrlFromMessage(update.getMessage().getText()); - LOGGER.info("Extracted URL: {} from {}", url, message.getFrom().getUserName()); } + LOGGER.info("Extracted URL: {} from {}", url, message.getFrom().getUserName()); final Integer responseId = execute( MessageService.sendTextMessage(message.getChatId(), message.getMessageId(), DOWNLOAD_REQUEST)) .getMessageId(); - CommandExecutor.executeCommand(url, String.valueOf(message.getMessageId())); - execute(VideoService.sendVideo(message.getChatId(), message.getMessageId())); - execute(MessageService.deleteMessage(message.getChatId(), responseId)); - cleanDownloadsFolder(); + try { + CommandExecutor.executeCommand(url, String.valueOf(message.getMessageId())); + execute(VideoService.sendVideo(message.getChatId(), message.getMessageId())); + } finally { + deleteMessage(message.getChatId(), responseId); + } } - private void handleDispatchError(Update update, Exception e) { + private void handleDispatchError(Update update) { try { execute(MessageService.sendTextMessage( update.getMessage().getChatId(), @@ -144,39 +107,14 @@ private void handleDispatchError(Update update, Exception e) { .getMessageId(); } catch (TelegramApiException ex) { LOGGER.error("Failed to send error message, error: {}", ex.getMessage(), ex); - } finally { - cleanDownloadsFolder(); } } - private DataRootContainer loadMetricsData(Update update) { - // Create or update user data from the Telegram update - org.telegram.telegrambots.meta.api.objects.User telegramUser = - update.getMessage().getFrom(); - - DataRootContainer rootContainer = (DataRootContainer) storageManager.root(); - - // Find existing user or create new one - String userId = telegramUser.getId().toString(); - Optional existingUser = rootContainer.findUserById(userId); - - if (existingUser.isPresent()) { - User user = existingUser.get(); - user.recordNewMessage(); - } else { - User newUser = new User( - userId, - update.getMessage().getChatId(), - telegramUser.getUserName(), - telegramUser.getFirstName(), - telegramUser.getLastName(), - telegramUser.getLanguageCode(), - telegramUser.getIsPremium(), - 1, - LocalDateTime.now().toString()); - rootContainer.getUsers().add(newUser); - LOGGER.info("New user registered: {}", newUser.getUserName()); + private void deleteMessage(Long chatId, Integer messageId) { + try { + execute(MessageService.deleteMessage(chatId, messageId)); + } catch (TelegramApiException e) { + LOGGER.warn("Failed to delete temporary message {}: {}", messageId, e.getMessage()); } - return rootContainer; } } From 2ca878be144b055eb448955be76cf5790ecf5592 Mon Sep 17 00:00:00 2001 From: SegoCode <35817798+SegoCode@users.noreply.github.com> Date: Fri, 7 Aug 2026 11:27:03 +0200 Subject: [PATCH 53/72] Update Main.java --- .../main/java/org/segocode/webdl/Main.java | 42 +++++++++++++++---- 1 file changed, 34 insertions(+), 8 deletions(-) diff --git a/code/src/main/java/org/segocode/webdl/Main.java b/code/src/main/java/org/segocode/webdl/Main.java index a20f823..76344cd 100644 --- a/code/src/main/java/org/segocode/webdl/Main.java +++ b/code/src/main/java/org/segocode/webdl/Main.java @@ -1,12 +1,14 @@ package org.segocode.webdl; -import org.eclipse.store.storage.embedded.types.EmbeddedStorage; -import org.eclipse.store.storage.embedded.types.EmbeddedStorageManager; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; import org.segocode.webdl.bot.Webdlbot; -import org.segocode.webdl.panel.PanelApplication; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.telegram.telegrambots.meta.TelegramBotsApi; +import org.telegram.telegrambots.meta.api.methods.GetMe; +import org.telegram.telegrambots.meta.generics.BotSession; import org.telegram.telegrambots.updatesreceivers.DefaultBotSession; public class Main { @@ -14,16 +16,40 @@ public class Main { public static void main(String[] args) { try { - LOGGER.info("Starting storage manager..."); - final EmbeddedStorageManager storageManager = EmbeddedStorage.start(); TelegramBotsApi botsApi = new TelegramBotsApi(DefaultBotSession.class); LOGGER.info("Starting the video download bot..."); - botsApi.registerBot(new Webdlbot(storageManager)); + Webdlbot bot = new Webdlbot(); + BotSession session = botsApi.registerBot(bot); + startWatchdog(bot, session); LOGGER.info("Bot started successfully and ready to download videos 🚀"); - LOGGER.info("Starting webp anel app..."); - PanelApplication.start(storageManager); } catch (Exception e) { LOGGER.error("Error while attempting to start the bot. Error details:", e); + throw new IllegalStateException("Unable to start the bot", e); } } + + private static void startWatchdog(Webdlbot bot, BotSession session) { + AtomicInteger failures = new AtomicInteger(); + Executors.newSingleThreadScheduledExecutor(Thread.ofVirtual().factory()) + .scheduleWithFixedDelay( + () -> { + try { + if (!session.isRunning()) { + throw new IllegalStateException("Telegram bot session stopped"); + } + bot.execute(new GetMe()); + failures.set(0); + } catch (Exception e) { + int currentFailures = failures.incrementAndGet(); + LOGGER.warn("Telegram health check failed ({}/3): {}", currentFailures, e.getMessage()); + if (currentFailures >= 3) { + LOGGER.error("Telegram connection is unhealthy; exiting for Docker restart"); + System.exit(1); + } + } + }, + 1, + 1, + TimeUnit.MINUTES); + } } From 8556a1a76b58f331f819b7232c1acaeab4ae6832 Mon Sep 17 00:00:00 2001 From: SegoCode <35817798+SegoCode@users.noreply.github.com> Date: Fri, 7 Aug 2026 11:27:06 +0200 Subject: [PATCH 54/72] Delete AdminController.java --- .../segocode/webdl/panel/AdminController.java | 40 ------------------- 1 file changed, 40 deletions(-) delete mode 100644 code/src/main/java/org/segocode/webdl/panel/AdminController.java diff --git a/code/src/main/java/org/segocode/webdl/panel/AdminController.java b/code/src/main/java/org/segocode/webdl/panel/AdminController.java deleted file mode 100644 index c3e86f8..0000000 --- a/code/src/main/java/org/segocode/webdl/panel/AdminController.java +++ /dev/null @@ -1,40 +0,0 @@ -package org.segocode.webdl.panel; - -import com.google.gson.Gson; -import io.javalin.http.Context; -import java.io.IOException; -import java.io.InputStream; -import java.nio.charset.StandardCharsets; -import org.eclipse.store.storage.embedded.types.EmbeddedStorageManager; -import org.segocode.webdl.bot.model.DataRootContainer; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class AdminController { - private static final Logger LOGGER = LoggerFactory.getLogger(AdminController.class); - private final EmbeddedStorageManager storageManager; - - public AdminController(EmbeddedStorageManager storageManager) { - this.storageManager = storageManager; - } - - public void handleAdminRequest(Context ctx) { - ctx.contentType("text/html"); - - try { - InputStream inputStream = getClass().getResourceAsStream("/views/admin.html"); - String htmlContent = new String(inputStream.readAllBytes(), StandardCharsets.UTF_8); - String jsonData; - synchronized (storageManager) { - jsonData = new Gson().toJson(((DataRootContainer) storageManager.root()).getUsers()); - } - htmlContent = htmlContent.replace("{{!user_data}}", jsonData); - - ctx.result(htmlContent); - } catch (IOException e) { - LOGGER.error("Failed to read admin HTML file", e); - ctx.status(500); - ctx.result("Error loading admin panel: " + e.getMessage()); - } - } -} From b6b6377abd6f7d3633f1f9dd75bfea3cbc27de79 Mon Sep 17 00:00:00 2001 From: SegoCode <35817798+SegoCode@users.noreply.github.com> Date: Fri, 7 Aug 2026 11:27:08 +0200 Subject: [PATCH 55/72] Delete PanelApplication.java --- .../webdl/panel/PanelApplication.java | 28 ------------------- 1 file changed, 28 deletions(-) delete mode 100644 code/src/main/java/org/segocode/webdl/panel/PanelApplication.java diff --git a/code/src/main/java/org/segocode/webdl/panel/PanelApplication.java b/code/src/main/java/org/segocode/webdl/panel/PanelApplication.java deleted file mode 100644 index ab354cd..0000000 --- a/code/src/main/java/org/segocode/webdl/panel/PanelApplication.java +++ /dev/null @@ -1,28 +0,0 @@ -package org.segocode.webdl.panel; - -import io.javalin.Javalin; -import org.eclipse.store.storage.embedded.types.EmbeddedStorageManager; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class PanelApplication { - private static final Logger LOGGER = LoggerFactory.getLogger(PanelApplication.class); - private static PanelApplication instance; - - private PanelApplication(EmbeddedStorageManager storageManager) { - AdminController adminController = new AdminController(storageManager); - Javalin app = Javalin.create().start(8080); - configureRoutes(app, adminController); - LOGGER.info("Panel application started on port 8080"); - } - - public static synchronized void start(EmbeddedStorageManager storageManager) { - if (instance == null) { - instance = new PanelApplication(storageManager); - } - } - - private void configureRoutes(Javalin app, AdminController adminController) { - app.get("/", adminController::handleAdminRequest); - } -} From 28e965ff8df2421af676560a042bc9de7dce3199 Mon Sep 17 00:00:00 2001 From: SegoCode <35817798+SegoCode@users.noreply.github.com> Date: Fri, 7 Aug 2026 11:27:10 +0200 Subject: [PATCH 56/72] Update CommandExecutor.java --- .../webdl/system/command/CommandExecutor.java | 50 ++++++++++++------- 1 file changed, 33 insertions(+), 17 deletions(-) diff --git a/code/src/main/java/org/segocode/webdl/system/command/CommandExecutor.java b/code/src/main/java/org/segocode/webdl/system/command/CommandExecutor.java index be467a6..f179f76 100644 --- a/code/src/main/java/org/segocode/webdl/system/command/CommandExecutor.java +++ b/code/src/main/java/org/segocode/webdl/system/command/CommandExecutor.java @@ -8,8 +8,9 @@ public class CommandExecutor { private static final Logger LOGGER = LoggerFactory.getLogger(CommandExecutor.class); - private static final int MAX_RETRIES = 5; + private static final int MAX_RETRIES = 2; private static final int TIMEOUT = 120; // seconds + private static final int TERMINATION_TIMEOUT = 5; // seconds /** * Executes a command in the system's command line to download a video using yt-dlp. @@ -24,32 +25,47 @@ public static void executeCommand(String url, String uuid) throws Exception { String ytDlpCommand = osName.contains("win") ? "yt-dlp.exe" : "yt-dlp"; // This is needed? String outputPath = "." + File.separator + "downloads" + File.separator + uuid + ".%(ext)s"; - // The -q option is important to prevent deadlocks by ensuring the output buffer is not filled. - // TODO: make a StreamGobbler to handle buffered output and avoid deadlocks. - String[] command = {ytDlpCommand, "-q", "-S", "ext,res:720", "-o", outputPath, url}; + String[] command = { + ytDlpCommand, "-q", "--no-playlist", "-S", "res:720", "--recode-video", "mp4", "-o", outputPath, url + }; - int attempt = 0; - while (attempt++ < MAX_RETRIES) { + for (int attempt = 1; attempt <= MAX_RETRIES; attempt++) { LOGGER.info("Attempt {} of {}", attempt, MAX_RETRIES); + Process process; try { - Process process = new ProcessBuilder(command).start(); - if (process.waitFor(TIMEOUT, TimeUnit.SECONDS) - ? process.exitValue() == 0 - : process.destroyForcibly() == null) { + process = new ProcessBuilder(command) + .redirectOutput(ProcessBuilder.Redirect.INHERIT) + .redirectError(ProcessBuilder.Redirect.INHERIT) + .start(); + } catch (IOException e) { + throw new IOException("Unable to start yt-dlp", e); + } + + try { + if (!process.waitFor(TIMEOUT, TimeUnit.SECONDS)) { + terminate(process); + LOGGER.warn("Download attempt {} timed out.", attempt); + } else if (process.exitValue() == 0) { LOGGER.info("Download successful for URL: {}", url); return; } else { - LOGGER.warn("Download attempt {} failed or timed out.", attempt); - process.destroyForcibly(); - } - } catch (IOException e) { - LOGGER.error("I/O error occurred on attempt {} of {}", attempt, MAX_RETRIES, e); - if (attempt >= MAX_RETRIES) { - throw e; + LOGGER.warn("Download attempt {} failed with exit code {}.", attempt, process.exitValue()); } + } catch (InterruptedException e) { + terminate(process); + Thread.currentThread().interrupt(); + throw e; } } LOGGER.error("Max retries reached. Command failed for URL: {}", url); throw new RuntimeException("Max retries reached. Command failed."); } + + private static void terminate(Process process) throws InterruptedException { + process.descendants().forEach(ProcessHandle::destroyForcibly); + process.destroyForcibly(); + if (!process.waitFor(TERMINATION_TIMEOUT, TimeUnit.SECONDS)) { + LOGGER.error("yt-dlp did not terminate after {} seconds", TERMINATION_TIMEOUT); + } + } } From 0011a0dc870b088f3d3d593c9b32a0eed2bde281 Mon Sep 17 00:00:00 2001 From: SegoCode <35817798+SegoCode@users.noreply.github.com> Date: Fri, 7 Aug 2026 11:27:12 +0200 Subject: [PATCH 57/72] Update FileUtil.java --- .../segocode/webdl/system/util/FileUtil.java | 29 +++++++++---------- 1 file changed, 13 insertions(+), 16 deletions(-) diff --git a/code/src/main/java/org/segocode/webdl/system/util/FileUtil.java b/code/src/main/java/org/segocode/webdl/system/util/FileUtil.java index dfcb8ee..2f764d7 100644 --- a/code/src/main/java/org/segocode/webdl/system/util/FileUtil.java +++ b/code/src/main/java/org/segocode/webdl/system/util/FileUtil.java @@ -16,10 +16,16 @@ public class FileUtil { * Deletes all files and folders within the 'downloads' folder. */ public static void cleanDownloadsFolder() { - Path downloadsFolder = Paths.get("./downloads/"); - try (Stream paths = Files.walk(downloadsFolder)) { + cleanDirectory(Paths.get("./downloads/")); + } + + static void cleanDirectory(Path directory) { + if (Files.notExists(directory)) { + return; + } + try (Stream paths = Files.walk(directory)) { paths.sorted(Comparator.reverseOrder()) - .filter(p -> !p.equals(downloadsFolder)) + .filter(path -> !path.equals(directory)) .forEach(p -> { try { Files.delete(p); @@ -29,27 +35,18 @@ public static void cleanDownloadsFolder() { } }); } catch (Exception e) { - LOGGER.error("Error cleaning the downloads folder: {}", downloadsFolder, e); + LOGGER.error("Error cleaning directory: {}", directory, e); } } /** - * Locates the video file, attempting to find or rename it with a .mp4 extension. + * Locates the MP4 file generated by yt-dlp. * * @param filePath The path of the video file. * @return The video file if found or renamed successfully, null otherwise. */ public static File locateVideoFile(String filePath) { - File videoFile = new File(filePath); - if (videoFile.exists()) return videoFile; - - // Attempt to locate the file without the .mp4 extension - String filePathWithoutExtension = filePath.replace(".mp4", ""); - videoFile = new File(filePathWithoutExtension); - if (videoFile.exists()) { - File renamedFile = new File(filePathWithoutExtension + ".mp4"); - if (videoFile.renameTo(renamedFile)) return renamedFile; - } - return null; + Path video = Paths.get(filePath + ".mp4"); + return Files.isRegularFile(video) ? video.toFile() : null; } } From 15dbd19c26ba5796943b9a2f174b086d57947803 Mon Sep 17 00:00:00 2001 From: SegoCode <35817798+SegoCode@users.noreply.github.com> Date: Fri, 7 Aug 2026 11:27:14 +0200 Subject: [PATCH 58/72] Delete admin.html --- code/src/main/resources/views/admin.html | 457 ----------------------- 1 file changed, 457 deletions(-) delete mode 100644 code/src/main/resources/views/admin.html diff --git a/code/src/main/resources/views/admin.html b/code/src/main/resources/views/admin.html deleted file mode 100644 index d5a472c..0000000 --- a/code/src/main/resources/views/admin.html +++ /dev/null @@ -1,457 +0,0 @@ - - - - - - Dashboard - - - - - - -

-
-
-

DASHBOARD

-

Bot activity & user statistics

-
-
-
PANEL
-
UPDATED --
- -
-
- -
-
-

Total messages processed

-

0

-
-
-
-

Total users

-

0

-
-
-

Average / user

-

0

-
-
-

Active languages

-

0

-
-
-
- -
-
-

User activity table

-

Ordered by message volume

-
- - - - - - - - - - - -
NameUsernameLanguageMessagesLast active
-
-
- - - - From abb240fb55f5c6b0120d85bfe79ae872394ec646 Mon Sep 17 00:00:00 2001 From: SegoCode <35817798+SegoCode@users.noreply.github.com> Date: Fri, 7 Aug 2026 11:27:16 +0200 Subject: [PATCH 59/72] Create DownloadQueueTest.java --- .../segocode/webdl/bot/DownloadQueueTest.java | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 code/src/test/java/org/segocode/webdl/bot/DownloadQueueTest.java diff --git a/code/src/test/java/org/segocode/webdl/bot/DownloadQueueTest.java b/code/src/test/java/org/segocode/webdl/bot/DownloadQueueTest.java new file mode 100644 index 0000000..888b359 --- /dev/null +++ b/code/src/test/java/org/segocode/webdl/bot/DownloadQueueTest.java @@ -0,0 +1,65 @@ +package org.segocode.webdl.bot; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import org.junit.jupiter.api.Test; + +class DownloadQueueTest { + @Test + void processesTasksOneAtATimeInSubmissionOrder() throws Exception { + List executionOrder = Collections.synchronizedList(new ArrayList<>()); + AtomicInteger activeTasks = new AtomicInteger(); + AtomicInteger maximumActiveTasks = new AtomicInteger(); + CountDownLatch firstTaskStarted = new CountDownLatch(1); + CountDownLatch releaseFirstTask = new CountDownLatch(1); + CountDownLatch completed = new CountDownLatch(3); + + try (DownloadQueue queue = new DownloadQueue()) { + queue.submit(() -> runTask( + 1, executionOrder, activeTasks, maximumActiveTasks, firstTaskStarted, releaseFirstTask, completed)); + firstTaskStarted.await(1, TimeUnit.SECONDS); + queue.submit(() -> runTask(2, executionOrder, activeTasks, maximumActiveTasks, null, null, completed)); + queue.submit(() -> runTask(3, executionOrder, activeTasks, maximumActiveTasks, null, null, completed)); + + assertEquals(3, queue.pendingTasks()); + releaseFirstTask.countDown(); + org.junit.jupiter.api.Assertions.assertTimeoutPreemptively(Duration.ofSeconds(2), () -> completed.await()); + } + + assertEquals(List.of(1, 2, 3), executionOrder); + assertEquals(1, maximumActiveTasks.get()); + } + + private static void runTask( + int id, + List executionOrder, + AtomicInteger activeTasks, + AtomicInteger maximumActiveTasks, + CountDownLatch started, + CountDownLatch release, + CountDownLatch completed) { + int active = activeTasks.incrementAndGet(); + maximumActiveTasks.accumulateAndGet(active, Math::max); + executionOrder.add(id); + if (started != null) { + started.countDown(); + } + try { + if (release != null) { + release.await(); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } finally { + activeTasks.decrementAndGet(); + completed.countDown(); + } + } +} From caed02c384d0577839ac6a316a9a4c63655d94d5 Mon Sep 17 00:00:00 2001 From: SegoCode <35817798+SegoCode@users.noreply.github.com> Date: Fri, 7 Aug 2026 11:27:18 +0200 Subject: [PATCH 60/72] Create VideoServiceTest.java --- .../webdl/bot/service/VideoServiceTest.java | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 code/src/test/java/org/segocode/webdl/bot/service/VideoServiceTest.java diff --git a/code/src/test/java/org/segocode/webdl/bot/service/VideoServiceTest.java b/code/src/test/java/org/segocode/webdl/bot/service/VideoServiceTest.java new file mode 100644 index 0000000..531169b --- /dev/null +++ b/code/src/test/java/org/segocode/webdl/bot/service/VideoServiceTest.java @@ -0,0 +1,25 @@ +package org.segocode.webdl.bot.service; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.nio.file.Files; +import java.nio.file.Path; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.telegram.telegrambots.meta.api.methods.send.SendVideo; + +class VideoServiceTest { + @TempDir + Path temporaryDirectory; + + @Test + void createsTelegramRequestForDownloadedVideo() throws Exception { + Path video = Files.createFile(temporaryDirectory.resolve("42.mp4")); + + SendVideo request = VideoService.sendVideo(123L, 42, temporaryDirectory); + + assertEquals("123", request.getChatId()); + assertEquals(42, request.getReplyToMessageId()); + assertEquals(video.toFile(), request.getVideo().getNewMediaFile()); + } +} From f240a25e2d3422d4a6dc22922790497ad127fe83 Mon Sep 17 00:00:00 2001 From: SegoCode <35817798+SegoCode@users.noreply.github.com> Date: Fri, 7 Aug 2026 11:27:21 +0200 Subject: [PATCH 61/72] Create MessageUtilTest.java --- .../webdl/bot/util/MessageUtilTest.java | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 code/src/test/java/org/segocode/webdl/bot/util/MessageUtilTest.java diff --git a/code/src/test/java/org/segocode/webdl/bot/util/MessageUtilTest.java b/code/src/test/java/org/segocode/webdl/bot/util/MessageUtilTest.java new file mode 100644 index 0000000..dc17e40 --- /dev/null +++ b/code/src/test/java/org/segocode/webdl/bot/util/MessageUtilTest.java @@ -0,0 +1,21 @@ +package org.segocode.webdl.bot.util; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +import org.junit.jupiter.api.Test; + +class MessageUtilTest { + @Test + void extractsFirstHttpUrlFromMessage() { + assertEquals( + "https://example.com/video?id=1", + MessageUtil.extractUrlFromMessage("download https://example.com/video?id=1 now")); + } + + @Test + void rejectsTextWithoutHttpUrl() { + assertNull(MessageUtil.extractUrlFromMessage("example.com/video")); + assertNull(MessageUtil.extractUrlFromMessage("not a link")); + } +} From 3e2b50f175e3dc196126922f4ff91f3d64f24aae Mon Sep 17 00:00:00 2001 From: SegoCode <35817798+SegoCode@users.noreply.github.com> Date: Fri, 7 Aug 2026 11:27:23 +0200 Subject: [PATCH 62/72] Create FileUtilTest.java --- .../webdl/system/util/FileUtilTest.java | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 code/src/test/java/org/segocode/webdl/system/util/FileUtilTest.java diff --git a/code/src/test/java/org/segocode/webdl/system/util/FileUtilTest.java b/code/src/test/java/org/segocode/webdl/system/util/FileUtilTest.java new file mode 100644 index 0000000..978640b --- /dev/null +++ b/code/src/test/java/org/segocode/webdl/system/util/FileUtilTest.java @@ -0,0 +1,47 @@ +package org.segocode.webdl.system.util; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.nio.file.Files; +import java.nio.file.Path; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +class FileUtilTest { + @TempDir + Path temporaryDirectory; + + @Test + void locatesMp4Download() throws Exception { + Path video = Files.createFile(temporaryDirectory.resolve("42.mp4")); + + assertEquals( + video.toFile(), + FileUtil.locateVideoFile(temporaryDirectory.resolve("42").toString())); + } + + @Test + void ignoresNonMp4AndPartialDownloads() throws Exception { + Files.createFile(temporaryDirectory.resolve("42.webm")); + Files.createFile(temporaryDirectory.resolve("42.webm.part")); + Files.createFile(temporaryDirectory.resolve("42.info.json")); + + assertNull(FileUtil.locateVideoFile(temporaryDirectory.resolve("42").toString())); + } + + @Test + void removesAllContentsButKeepsDownloadDirectory() throws Exception { + Path nestedDirectory = Files.createDirectory(temporaryDirectory.resolve("nested")); + Files.createFile(nestedDirectory.resolve("video.mp4")); + Files.createFile(temporaryDirectory.resolve("other.webm")); + + FileUtil.cleanDirectory(temporaryDirectory); + + assertTrue(Files.isDirectory(temporaryDirectory)); + try (var files = Files.list(temporaryDirectory)) { + assertEquals(0, files.count()); + } + } +} From c41eee08cb875d1f51963ec6f27fa3b6a6576f15 Mon Sep 17 00:00:00 2001 From: SegoCode <35817798+SegoCode@users.noreply.github.com> Date: Fri, 7 Aug 2026 11:27:26 +0200 Subject: [PATCH 63/72] Update LICENSE --- LICENSE | 302 ++++++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 227 insertions(+), 75 deletions(-) diff --git a/LICENSE b/LICENSE index 6ab3d8e..1b2ebe6 100644 --- a/LICENSE +++ b/LICENSE @@ -1,75 +1,227 @@ -Copyright (c) SegoCode -All rights reserved. - -Section 1 - Definitions - -1.1 "NonCommercial" pertains to any use, distribution, or modification of the - licensed material that does not primarily aim to achieve commercial - advantage or generate monetary compensation. This includes, but is not - limited to, activities such as distributing the licensed material as part - of an application or product that is sold, using the licensed material in - advertising, and creating products or services with the licensed material - that are subsequently sold. - -1.2 "Adapted Material" refers to any work derived from or based upon the licensed - material. This includes, but is not limited to, translations, alterations, - rearrangements, transformations, source code modifications, compiled code - alterations, architectural redesigns, or the integration of the licensed - material into other software projects. - -1.3 "NonAdapted Material" refers to exact copies of the licensed material, either - in source code or binary form, which are reproduced without any changes, - modifications, or transformations. - -Section 2 - License Conditions - -2.1 Distribution and usage in source or binary forms are permitted solely for - NonCommercial purposes for both NonAdapted Material and Adapted Material - excluding NonAdapted Material cases in 2.4 section. - -2.2 Redistributions for any NonAdapted Material or Adapted Material, either - in source code or binary form, must include the original copyright notice, - this license, the disclaimer, and comply with the requirements specified - herein. For binary form redistributions, these documents must be included - in any provided documentation or materials or a clearly accessible link - to this license ensuring that recipients can easily review the license terms. - -2.3 For any Adapted Material, whether in source code or binary form, and for any - instance of the licensed material utilized in applications accessible over - the internet that run on a server, the source code must be made accessible - through a public repository, a downloadable archive, or an equivalent - method, ensuring that users have the capability to access, review, and - download the code, and must clearly credit the original work and author. - -2.4 For any NonAdapted Material that are offered on download sites, marketplaces, or - software distribution platforms, are permitted under the condition that any - economic benefit derived from such distribution is strictly indirect, including - but not limited to advertisements or link redirectors. Direct sales or charges - for access to the licensed material are not permitted under this license. Upon - the original author distributing the material on the same platform, all alternative - downloads and any associated revenue-generating mechanisms must be discontinued - immediately. Acceptance of this license constitutes agreement that the copyright - holder may request the immediate cessation of such downloads and activities on - such platforms, and the distributor must comply with such request without delay. - -2.5 Distributing the licensed material alongside unrelated or harmful software, such as adware, - malware, or spyware or implying endorsement or association with the original author - or recognized entities without permission, is prohibited. - -Section 3 - Updates and Revisions - -3.1 The copyright holder retains the right to amend, update, or otherwise modify this - license at any time without prior notice. Users should periodically review the - license terms to stay informed of any changes. - -3.2 Continued use of the licensed material after such changes signifies acceptance of the - revised license terms. It is the user's responsibility to ensure ongoing compliance - with the most current version of the license. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +# Source Available PolyForm Noncommercial + GNU AGPL-3.0 License + + + +## Acceptance + +In order to get any license under these terms, you must agree +to them as both strict obligations and conditions to all +your licenses. + +## Copyright License + +The licensor grants you a copyright license for the +software to do everything you might do with the software +that would otherwise infringe the licensor's copyright +in it for any permitted purpose. However, you may +only distribute the software according to [Distribution +License](#distribution-license) and make changes or new works +based on the software according to [Changes and New Works +License](#changes-and-new-works-license), subject to +[Source Availability Obligations](#source-availability-obligations). + +## Distribution License + +The licensor grants you an additional copyright license +to distribute copies of the software. Your license +to distribute covers distributing the software with +changes and new works permitted by [Changes and New Works +License](#changes-and-new-works-license), subject to +[Source Availability Obligations](#source-availability-obligations). + +## Notices + +You must ensure that anyone who gets a copy of any part of +the software from you also gets a copy of these terms or the +URL for them above, as well as copies of any plain-text lines +beginning with `Required Notice:` that the licensor provided +with the software. For example: + +> Required Notice: Copyright [SegoCode] ([https://github.com/SegoCode/webdl]) + +## Changes and New Works License + +The licensor grants you an additional copyright license to +make changes and new works based on the software for any +permitted purpose, subject to +[Source Availability Obligations](#source-availability-obligations). + +## Patent License + +The licensor grants you a patent license for the software that +covers patent claims the licensor can license, or becomes able +to license, that you would infringe by using the software. + +## Noncommercial Purposes + +Any noncommercial purpose is a permitted purpose. + +## Personal Uses + +Personal use for research, experiment, and testing for +the benefit of public knowledge, personal study, private +entertainment, hobby projects, amateur pursuits, or religious +observance, without any anticipated commercial application, +is use for a permitted purpose. + +## Noncommercial Organizations + +Use by any charitable organization, educational institution, +public research organization, public safety or health +organization, environmental protection organization, +or government institution is use for a permitted purpose +regardless of the source of funding or obligations resulting +from the funding. + +## Fair Use + +You may have "fair use" rights for the software under the +law. These terms do not limit them. + +## No Other Rights + +These terms do not allow you to sublicense or transfer any of +your licenses to anyone else, or prevent the licensor from +granting licenses to anyone else. These terms do not imply +any other licenses. + +## Patent Defense + +If you make any written claim that the software infringes or +contributes to infringement of any patent, your patent license +for the software granted under these terms ends immediately. If +your company makes such a claim, your patent license ends +immediately for work on behalf of your company. + +## Source Availability Obligations + +These obligations apply in addition to all other terms of this +license and are conditions of every license granted hereunder. + +### Definitions + +“Source Code” means the preferred form of the work for making +modifications to it. + +“Object Code” means any non-source form of a work. + +“Corresponding Source” means all the Source Code needed to +generate, install, and (for an executable work) run the Object +Code and to modify the work, including scripts to control those +activities. However, it does not include the work’s System +Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities +but which are not part of the work. The Corresponding Source for +a work in Source Code form is that same work. + +“Convey” means any kind of propagation that enables other parties +to make or receive copies. Mere interaction with a user through a +computer network, with no transfer of a copy, is not conveying. + +“Make Available as a Network Service” means enabling third parties +to interact with the functionality of the software or a modified +version remotely through a computer network, or offering a service +whose value entirely or primarily derives from the software or a +modified version, or that accomplishes for users the primary purpose +of the software or a modified version. + +### Obligation when Conveying + +If you Convey the software or any modified version (whether in +Source Code or Object Code form), you must: + +- license the entire work, as a whole, under these terms to anyone + who comes into possession of a copy; +- provide the Corresponding Source of the work under these same + terms, either accompanying the Object Code on a durable physical + medium customarily used for software interchange, or offering + equivalent access to the Corresponding Source from a network + server at no charge, with clear directions next to the Object + Code stating where to find it, and keeping that access available + for as long as needed to satisfy these requirements; and +- ensure that any interactive user interfaces display Appropriate + Legal Notices (copyright notice, absence of warranty, and how to + view a copy of these terms). + +### Obligation for Network Interaction and Services + +If you modify the software and Make Available as a Network Service +a version that supports remote interaction, or if you Make Available +as a Network Service the software or any modified version, you must +prominently offer all users interacting with it remotely an +opportunity to receive the Corresponding Source of your version by +providing access to that Corresponding Source from a network server +at no charge, through some standard or customary means of +facilitating copying of software. This obligation applies even if +you do not Convey copies. + +### Same License and Notices + +Any Corresponding Source you provide under these obligations must +itself be licensed under these terms and must include all Required +Notices and a copy of (or URL to) these terms. + +## Limited Control over Third-Party Hosting + +The licensor may at any time, by any reasonable means (including +direct notification to the third-party site, public announcement on +the licensor’s website, repository, social media, or any other public +communication), make known that distribution of the software or any +modified version on a particular third-party website, platform, +repository, mirror, or download service is not authorized under these +terms. + +Once the licensor has made such communication in a manner reasonably +capable of coming to the attention of the public or of the operators +of the site, any person who has uploaded, instructed the upload of, +hosts, or continues to distribute the software or a modified version +on that site must promptly cease the distribution and remove all +copies under their control. + +Failure to cease distribution and remove the copies after the +licensor’s communication has become reasonably known constitutes a +violation of these terms. + +This obligation applies only to copies that the person controls or +that they caused to be placed on the site. It does not impose liability +for actions of independent third parties beyond the person’s control. + +## Violations + +The first time you are notified in writing that you have +violated any of these terms, or done anything with the software +not covered by your licenses, your licenses can nonetheless +continue if you come into full compliance with these terms, +and take practical steps to correct past violations, within +32 days of receiving notice. Otherwise, all your licenses +end immediately. + +## No Liability + +***As far as the law allows, the software comes as is, without +any warranty or condition, and the licensor will not be liable +to you for any damages arising out of these terms or the use +or nature of the software, under any kind of legal claim.*** + +## Definitions + +The **licensor** is the individual or entity offering these +terms, and the **software** is the software the licensor makes +available under these terms. + +**You** refers to the individual or entity agreeing to these +terms. + +**Your company** is any legal entity, sole proprietorship, +or other kind of organization that you work for, plus all +organizations that have control over, are under the control of, +or are under common control with that organization. **Control** +means ownership of substantially all the assets of an entity, +or the power to direct its management and policies by vote, +contract, or otherwise. Control can be direct or indirect. + +**Your licenses** are all the licenses granted to you for the +software under these terms. + +**Use** means anything you do with the software requiring one +of your licenses. From 831aba8116f9d8cbe61e0e4432e166906c73345e Mon Sep 17 00:00:00 2001 From: SegoCode <35817798+SegoCode@users.noreply.github.com> Date: Fri, 7 Aug 2026 11:27:28 +0200 Subject: [PATCH 64/72] Delete demoPanel.png --- media/demoPanel.png | Bin 42605 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 media/demoPanel.png diff --git a/media/demoPanel.png b/media/demoPanel.png deleted file mode 100644 index e84ee6fbfb439f192f3484f2bce8d84c7e776057..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 42605 zcmcG$byOU|w=POT0tpTY!GaAI+})BexCM8&0KsK&2{yO|han-j2KRyB?h@P?TnD#1 zB=`L8T5p|y-nsAfYSt7zHM@50+Pk`Ie_wUzCk07NG-5O)BqU5}DWEbE(lcWuBxK7M zPZ3v!*{OX*nSwVRbzIJxO51B0)l;K#~T2P<2b+TX6S!y>W+nbhx)?%L9_4 zK-GHsggQ`#9p~qFKTS&jw^bNwh?;hle4*j2VJDA7=alBND;hPWDqS){Zv2l*xIaFb zPyn8Yz1YAYaSYKl<@Fr6y-!{k96F$#VMJX&^*NSwb=`D_=$)rnKu_Pc9|FnY7s*TnCk)JF62n13s^l42ZseH$R*r_+uIA zM9EFW?4Jl!ep1?M>-^G!Bs3E)rWdjQnt+qQHfbG%xNG3b(gNHyII#2gBLC`7^AXTi z>+dI&yQ*WnMHJ3-|6Sj6WRbHv+{!VA>qz{&qMnW(?|iM#Akkv>uP26`^=!R&)h(#n z|5ZcR%Qnt>R=a*jM)-K0fZ}EJ-gxmpJG`BcEJy1eO{^`w)y(Fx3niqyTObccdB#7T z@%*p&k-6)4a@X6*o85r3U-sKoM2iuDLm5*Lk(uLtNR1~s+E&{CwlZISw15+_)fv$F zqWL})`|{ydB>Yz{+3;Y&)DlI?v@3_FbJXDd&PbjG$z8NJA6V; z{QWsHqCvl171ndw$w@+)Xv>NHx9y+K%!F}!O(TvHlfF^{0)7t$5ucEUxt%it0M}YW z1<_=`-14|QXPYiqaB%4}XB#hqLJu~^t$jS)7x(KKiXw|ahDYG?7B^~U8_PDSLmzqq zL?iE8Mx8&i>K7Ik+3jtOV380D0XV)$;dGIIFxV|&E6CP79iEpcy~q^zykpN#+f{h@ zwH?&WQ$lpJTas3J_+tAI@6mYh*oTaw%ln?tnB~@1BvD`Ameht|^61R4dLz~O$XD6Q zH7&BJ0aA8B(l;$uYkVF@8XtbFC^!2*T);{QhCX}Jm+dAV3^aznxq7|lWmH()68>n& zkzIw%X1Ddv+w*gIk`jTbyfu1ir~Tc)EzQ)3f`T^guK<+oRix}sfyj&lOGrNIq2Y1Y>UW^At$bqTpYWU%KnfpDBB+lHYOleIhdFC za!bnL&Y>0flx}X}-_IU>9WQBFDTV4&8nrP=6$ zAdiKY#}WhR_yB$>sG)(Z1^GFcDy2tUDWr!-B&6z8qZuJG*`4h z(fc+e&*@awdJ@IB8M&L5$$k4!aUk5jPOwMta`T2$#31IfEgh9Nof`*N@78M^M6_#5 zQz%-qQ6m3vyBbnC8CfC#P-izCfYa&U>I8S@3!N22KODhz8v6>hU4Yc5N-O{ zWzJsh$lBTN-*QMUo~Nnp`uc{rlkd+wG#wwzW$p{8IOJ&AP{dt{cP?Iq_%=1UMf{SS zL9d;!T>;>Z=1&Z_G+kwe7BmhON@kP)sq}a|N!lZQ7k*q6m3%yBLmt9^y^nDGR_@(_ zdRbf}u^_@w@D6dQYJMTDBB3mhBL%)$P@c14(TEw35*~INZ^X!{k^odqPlM9)ws8!+ z6VWCUwyEDvu)2UaW*HtIi_`TR(`FaC!h%`)wMwb8+9I`PNW`2HyOl;0_r@HEtY7_< zN~kqgG52v(uc)vG-Q8BFyrk}s-8}_FdYjeYrZ#7l4vP>9d8!*~l{m<%o`j{FI!V`L z?~^uA=vO_s%JgZGSR9WgZYG01AskFYbV*=*OFZijAB#&#J-Wi9=s_WZHRa(zX>I0* zsa;HMOmc2*+IV(md0o9o2&q)NWHrqUXZ)g|37Z{Tej0JVFOy9cHBH9Md0x6a0kf-Z zi9pTTRUkP7R2UvH5~*@wIa^|hsvQwxHFV(N0}+yb>-C~hn0=v%B=*CCE|k?`W$%vF zCoySrf&;HYdbmXE=(pI_84#%9!&I83L$%eK#^{Lcd|)!7%kO<;hdn5IJjJVXi1}+U zLuy*vz@>N{FIHUI#f+7@j0eRKMP6;ivqdg3RWw)YbSTfx+r%LJSY?9j2F6)iSS49Q zi%kU>%WywDb}x$7G&TLin=yAY`%0yjrukuV)-xCnH{<@Ov*mu=*ab~br}NlX*hR9p zll7vw=9+<}`KG@XZHO2wRW=?f8uUR$t?mH2;e@(F{PuCKU_XnLr~LgvubD)M`d4{* z3e3H0=fD%R2XD|kPc#iA>l67S=PV+42d+~Tn_xHF$)q2W7kyo0KOageN!;UuE!R_= z-J45|*6Gb{I-keMDtO0}@jObu>XVd2-3Y;1Es%zlTkoPilErpf?l)TVes6!!T|@xNw;-Jad-H_?AHwS29#(p4<{qrW0<+n#>V zNA;3n_0535H$7#!mHqQ zrh3qwlc&I?o>3@LeO7Ct3E)UyxV~<20RjifSv+SsZYcm(SH;UZMRGC*ig>Q*$@eF< zqCDz%muuFbXE;jzidw!`zj}M|`VZ^OQu)ggX#s%kU!mw48;1|26+5aHs}tHsU5Tea zprnCkuV#-@w4JblL*el}$N(n7K_xdI>+)IWKr3JarF}+`$J-*oeX=IqdnyxjxMUy* z@EUMI7iW(XS5|u-Eo^k1y*PTRY;beEwX#$A*bxvjc_&`%N?RHUv?leZYk}dggH!12 zp7*5ZIsR-v7Q#zP>4@b+bf?@=Mct$fZM@e~PLl$Y^z|K)ySLARS)yw4wy2p02DH*r zDjfw#2l`cF3u>u3Eo{t(PU`a4@%hnz(z|(dX)C=QtKPixw49k8NUu%_4<_wh)Rd!^ zaUvp}U3#~t<@GJjamswNSdS+y>Pw%77R=0H0>IESpn+R(GWD7ey~(vr=~$|gC7)sU zDP4C;JO1Q~GDk~s&#)jnI}J_S4tS03^E`%wpNi5XA%}y^>jFf*_5pUu^^usaYnjog zN$$weaXACknW*9_IO1ex(a*!*pA-F(>xQS{B5z`!Z7Hh?WSg8+cTV)WfHZ8Qe|bnF zjyBD+$jPxV+tgtuFL`uR!O^o`J;+OQXKe_0;0JDdOlD?>W3`=r6C9K|EMM59X<4i) z0;LpL1XlR5bCTb{&X*JcfH1x4y9|PZOk&)3<2=(7Ref~P1m>89zL(H+uxc9UEeF}Y z=xGouLy>}7=Jno&&RxmL{8f#4p}|#A=EM*%TBP}o1!9x zTlh6;ZUT3is%{lSy1xYTyaOC)G8rujZLu#d*Z6hcuP2H-n)Gma^!1lN>Dktefrfg9 zmC(gmP3OI+XGJoOldA z1?-nf3Mot&LX}S2hu>OfqKQ3Y@bL0NzGX5|t1T{`mggGjDfQAWGWfl&Z9(#5 z3Ca(An_k4-MpR2$Rd4)rxgHTQgMey6ZSHs1LSZ5Nh$0_4mYU_`0Gr6TULo= zmuov2C^n@ju(D#w1y`p?59e!(k5H?RrhBIkuzUs!k?<`PUh#7~%jMN# z6+6Qo&UP4m=T5xcizWp>t{xx9Zxv;N!3xm((|nM`5)@uL{NQu{?Y)DY@~w)U@LLu- z;`^gRf@Ys(d3@Z_t4a&7F|(_3F7$4-=>;GzGW`K2I&cN=ZAKZd{hcbhnNK%ek|z2U z+~WycWmH_eTB$T30_PWRd1j%4ccSmsatNG2i5gnod4=c?R1Lkx$ez2LcFbfe@<4y8 zA;_-_wU!Ij-}%Rv>BJFiGjFbx?XFyRc-6!PA{t4he@{>1hK zEUV|nF=!m4Zv;d7NT8Qz6t`&Dz=MtNad0ry=Tmo?g3ahmEmt#hY;_VfZIXF^G-p9q z{jduf&wSmLT)z0h-{;3@t((ETma<*i4mg>x{z}|blW){vE<*^|c=4T{%)GlZiiSal zlg_M!O|p*V&Ws zPxDJl&R>np^c2WQIDc?v#V@c4e9P~1&2uP!6Pv(f-V_Jr7(TRO(R9@D@IMyB#5l*S zHws?eJBa75DZq)W+m@Jff@~FG!6iY6g>9<~tB2`&XuJiK-~RHT{v}#a(l3XBcQnJN zO9YwuB`uD_3+GvpEJPM(>mBh98MgkvsNW-=@VOhQ7POp)xEOQeQiHYyfYi=CfiKyW z_vI*W^Lq?hdWVkvD>cHVF-ofY~Yi(N_j8HC#_j^^1u->g*(2|ff1%StAQqdy3U(u`auwBv) zkDNO5x&so|c2^%gOailrV8@Vuu2|;Ql9H~fGc!Z7psZ^g5$p97sMl2({-#KOt51*RPSU8;!ZuE1b)PcoM3=GQ7i$x=7G z;oXIuieMX|igCBJCbztDI+=u8T3N2dh>;aUUjPu1Mh6cv3Y7$8hT_?u=g$@%}xl-p$Ss{&Q)l#;bx z*BZV_fv&7(p!Vv{w#5zMdVE|NpV}*>DC}8^vuKiIT6F(a@06JpZ^r&=-fJMy!Xy5R z##Rvf}OXTG&<&84us$zZ>V)$QXE-Yb@!182+7bP&LR+ z2X*&6J0&OQDKWu0M@+@fm9MaK_BWMxEcw(9y$>db=WL>qp3BvXnsnb0?W`u*>ioKU zic3_;Quz6+SV~$#^YO&ZX|3=ZMjQTn{T*VJ!E}$)LExf;O7nJ>_GR)+Ewpwu5vl%q zjLui{BC`C!c4xnGy9Z6>MS*R77M0!Iwy)4xDV4CDD|Ba*N-KOxXj}K{@m8I>eh9m1 z{VM1yq@?e$vlmM)8wxhl+Bh0%JZzer?w<31He|;O`teM0Sd%clnZ;TZ_qk*@8m>A_ z%TeWnt}e+cN`)|bSy4rHk$%Q)cWwO zpiwg#SJ9t^6+>37i|~j^6|AJOb;(9d^~X88&Ma4tp^&(ssr6Dz&Ol1$c1)aRbGU+u z5dto>CQj=QEK|aX|5lkU5Y1h73A;SCSL;^fLK&{r_>;f>ZN{4CC4D!`0M}U!J~=Zs zhHO=u`kIf*4ATQDy6jL$;Lv_O^@6cm(IkF@OZn{mc>{5KKN0s&yO;+SDj=;zYN)x4;_17oxY0f)7OJHT>Dp5Wh#>Hlvv;2AA)jTpf6#%!^c5h5CwOYd7a-C#UBW( zc@OO(b>g>s!pmDOTRmDeVG3QkRR(#{I@*SIFZo6T5}5AbCrxusjl1+l-9q=1wM4wV zQa3AWE5l%d>`IpRzsEuMbhNv*;j5C*`z8o#OX}95+u|I?^@Yzq7|veOT`0ro02LmN zqoXjBf`4f_XSLUvf{Oyz=WXxDd@hm-u=jomdPRYs9{8R*AEb{B@Plk$N009^?UUN6 zH8+p&fc#AixDo3I-3zYNDA#IQH}L*7>Rk>W@7<^5NvZlLZT>i&JLQ#R8nG=t79C-W zvMq=1xjJ-i?*y45gfB$*$$eDL;6IZ%HB@&IE9){5gB!$unOd&(*_vX&oBM^DRgI)f zANWZwW+afyD6DNELQ?p+mt+%vuJ}^%X8am9py(~^X)m@DuqF|W9hLmnJJZ48$j`k9 z(&#eQjvav~KlU=YanU)$Y?b_mWb-9sQmPtj|V7cG><-gjY<}-j$5qEWeuxTr8LyeBe(V0H$@8{L{)qS$RVLfyt_Aw- z!F|xfx@8zU`$>8E$qeXbNbx+=J?}7?@CtE8j^p=3&>*hiR2g}~gy*euW=qiXiTTm? z+eq1l-n#&kO%dqv+{2!?Rh%V}-*@_2c`&h6YI!?Da73f~jJ3fx8+kGbuI-uq%Jn~uN6N`*+u?GTiC-yfoy^5|JpUaTK5&tG0PIrw*N zD|SWPm-mHY;UMyMt6kaMygqtvCu=k&gZ&fFX>DfjPFMq_B`ad)lzHC>2`YFkgLUkP zgqfs_FceLFlvEBpTyJ8YXQCbs_z-#KlN#<4|QcN;g6|p zBqWHVe3|ofH1T5Z0u|Hu)-IjWzlmfdq-h~o8}%IizK3+C*?Mw?#-Ty_v6i%2WfXV< zlO~P4@u41ojEgJCaK=hMB(j6?R~QNh?9H5v3*+VhiKXizwZ`bZh0D_zTkOB+ zfuD7>lPsUTkX|Vj^0em$CBT@5%ZpGNWvWi_uP{QU^j-Nlw%7BWDq%wJ_>kED17vu_ zKiem^`m&uMLZkli_mlOta}|rnJUJqkH)WE6z_*Ge8U@V(&K{2RPaG%-xcLSaO8-H2$2!0|2BsN_@BW&;HqO872l+PIvXOz%H)jQv-}&m zRwO_|uj!p0%HIb3o&XtQ4L$z#{5>6gJKO*H=9%9Aof7{CbNavR&3_|Ec^YfY9b!gP z3~XbIut0yt_Wnr7ZkaE@e>akx-wH?f!lMl3$91aozoNWoCAFQR$U|ND@%6VMReS|m(@HS$vw%ocYn(!C z44%(^Nb@V^mCtlL(v5vyuItrMM069FA%7#SD)@p?| z8NMhdy5|!Vgah-%AM!=ml7BpTY9G?i-4iO%?(nvMRpc3lva-6B{h<$;6%K|uLIqWF z!hsQ_Cw1)`#Ot4vJ5GhV9q^dKo(3cU+qTX;k`7@%P<~Uu6fi%Y`G3HA?4WMxhat=c zCmt8S-Ow8oG9fiQKkL}EfCC{-L;;JK0tp1a{w_GdPPjfI^P~>h2+yzoQ9h#Z{?M@! z?fYMG`0xKCuIQ=e`9EL&?-0cQ??>}LPM&?I=XX$LE&X#%5T4B$1EGqd1H`dWC&Wb1 zu+r0;MnpHY$i8HwaPZ*R0*~Y7(G9&9QtWue-oZv~Bn_YmjnE{LMuGDqeP8-4UA9t9 zte;2I4B@r!IPKuP1p`~bI%g6DC#?zn61 zQn%oj)LnTk=x8Gu{%$_gTlwPcwJwBf0LHh9+Q^oe&T6v2ra($jRE^JoaIqXeGa0^1g)XH%TQk(J^4T(>{e5=}SE+l2!%PuvCCmkeU-XmSq zZEbgMTz7U3Q3>2uH(53TWIwoR{${=SO4v z<As%Hr+v&!HLTq_V4V@9|~K> zPYHGFwDm8LiBwB#-=pA{-T2pOV-pN$6)vP~@1ENCqR=8|@k$rvrK&^Q4+tsgxC^99 z*4l50>KikQ`FNYZ%Qexv8kt))4Mrh;vAY8+(@R}84@N58`AnHXS32kx#WhGh9;e|q zXZZ;5Ip{ahd&D+dA>ce~#VrJ~moumJv@So&+Hz%XL9zS2F7tG;k#jrsfd=JIL{fbQ zHDG@#V^Pj}h<7>_iHBSH~bD5}o+#3mjDdd8b*R}(*`nxRz#?E zaFTRYdAbW1_b(PXLDk}d6#eQcNkEbJFW2pf4k9Vst;H`nmpS~$K)%lQ)-GI)LGA)~ zMfD$VpnBcg?KB@C$t9{yr-t@l5Q`1*Z}xYn*qxI7Kl7Lm50(91!JF=1JIF)i;)*uU zh$RFTISKL$7d&^eOt#@hYR0OpwIA`Xb}O;R_7YO)KT3$}IEKGqf2c0ad{k7=_WYKO zKPs<(@b>8aa9Mx`!hW9hM;7p`YS~wEwF9+z7W*mHOpIDH^_+5(L8kYw6!1G*dISO- zBFcc_CM20k%kd}0;(#R}kxM@N#RkVl3qd9ZGOl}i*+Bq*?f}M;YVe{wxv1j3= zd!R+USax_Sna7b7(MwSuj;U`$VTO93y=mJ19TQgTQxVF_INBt#EXjR3Gdpx$WWH4v zCyhOp$z@vZvgQFjA{L@m+iX?m3MTW9L#i^2^tqRO70|;^>1jTWv$HubGg?}=#L+E1 zd!H$Vf1k~$FZDSo?z%%w3(DMzFkT5+xN@p4#j4Q}U<8-z{LweDXdx7)Q}{@PH8~_B z{cGnD-KNbpQ9+sm3JhbAHN;{sjGZ~iTnMtqe= zq3w2O0dSJJiKFLk6qNBedSz*lavt_Da9B-t$qZ?|howUH#`nh6pWnV!@vt-7PIv5M!?@1+-Kap8fq^i(A79rcOmj6)9$s885r{+h)+v$|tKXC3a1P}_oc>)=-gjqO(K>ZEK_|euk2v! z@uXAkm-P1SOYOu;bu9$2@2NKxs+HTqdk85<`0;9GF4ac14u*RWDS;Tf;A z^d5Z^$@B10r`4wjR3`QIvj%gn{F5*7^6JX4lqVjfWulXazO$dH>-&bqDmb#@uSGM2 zb&WQ72laOCR?WkQoZR%yjCYvAi})rT008BoWWr>d242bzZKXufk8Akf&CneiD9v*- zP>Mdwp8sAJx7MKk1gqAaM*f_3ahMZPoubsWInWTwRCO7ov0g@yV6QG?dVl?8)QsBg zy)Vnkr}3TMa@QI@h?TZ*@`vN00InM)l@f5KV44DX4FpDzWYxr;o6gQYQZVro@U@_{ zrtP-j^O8?0^nKltvO>z+c}@Cte9rMd3P3DO%i=QUdz03)A3rSKRZWbpmQOI8 zU?+4^rwD+*(Vu4Z%Y>Djhsx+%-MV)xg7exhjLax%g|7CH!Mz5$sY0alIGU^>PGmTXzHirKj~wKi?jv;QU+>Q; zu9?3JaymoIrEOjD8O(@8d`|eBj*`RL$A~1;!uA|4eg&;q-~0iuC+13+gC9S(+r%kt z>`n9%GGf;(eP#pEs{G+e>QQL77IN)w z6*RTnGZbIP;i!2!Dw8EOO7^C?=ru=wO4VADxoXLvg3hNOyRMD(1du^m6Qo!i4+3#Yp$Vl?$1%l%BxS0mtT<9f)+at z=2ivd{&?Y97;Q7vK2)TmR#>Xk+5(29 zy-~5R0^I<2O@tbjMkgC&2s2w11N%N>UhOp2+^szB?InQUo1z@J33(V@ttm(}KrrFq@kyhcT z1(k4&{cZKe`atK~XOTL&)XKBE5?|-8>EyY07$}b02T^2);({Y>I-D9hk~p5F1?DnMQ$?cO{b1p=17KX zg>7j8S5j?5Zfx6I#kVMIQI{yChlQVzr+4}p(tROuJNA~G+}iqyqeEF86K*mH+`l=y zE=z{+1rSI*|MAUzX}*;+G~#@7LaUawtwI7Z0V3EU8UycYrvJ>a22+I!R=5S2JyG7H z4PU|cZfhlBfoqvJtvscA>9FlCgShb*{P05L5l<7lgw*CESk{Ob5s7R|m2!rP8cx5% zO$kla`MMYvI&}J>O-Qww;KzjO!5;{Y3)xGK1p3pjoZ)Jm`KPKB4G|sRToku5KY%o5 zZ{XV!WPL2Q+H$A&gN>23#RD1jZy2o8=*D>m*jfWs$Y{hX- z1bCi&Z#IC&6{QlHiKFA=Xu&KWr9y1LLpEc1EC1$D=#Sa>`16g3(21EUt!-m?Mn>0= z5Bpo0Ccwj|l1{_=o=@X+Ijg$6FtiuXw|KO+evJ8{x`^Qlt=-D;%9>Y7a?Te{@eTMEDxloAZ>G=zS;^Vyra3NWO3K;tQ%b#z zZzhR%JrN9;ncpFA5FA~UF%-thP*>Ef~8UA4)T zvCTj3S%MBPReshU{?SNon#iI-*os5R8Q;P^(6P^Sw#rvC&Osc<%y_Epy2?fCCMb!g z9StwhLSi#*7IzH`SOo|eAQ}8AhX{$R@j#`Y?9$=JL*)C{@tQobJa%33BGJMDd;Ak_gMr2p1-jTb*7e!L5X!638JF(35l0u`~S3rL3Mf6PKvfRodO z8R?&pG;re(Q*h*`lR9YJZ-+%CrqV}yZz4t5WRO%qs*%xe@#$ymdCPP z$y3{uCIZ}%o8gDdR<9rD4L*g_C@2b}h?17v#)4)|p#y=Q%{ltQd0HxkMGT`Ew@t#f zS|DEHpkkW4TcI5hv-%i<=Lp0nvT(kgqjR4?!vg7%wB+Uy}6<3iWtCH>G zR;vcYNc+Cc_}RyKMp;uz5wV4X~b9dA>_jx*Y$)4WOs+M06F;ZCY8{*hA%J9bA| zSVoHvqzPE|$a1jUsV}VFY)Oi)-JT7#V5w2XjU}*7-`QbS!HI=9eg5zQvn+wLSt%%x zX8C#ni5>+`424a8XnG3B+ARLHC(luIfBZ^ePDIG30C=!QaB#b>0wpqvdv4^ew&mjO zJ-OCX8JAjiauvb_D}Xa8pkC33{f<3IGHkR|kya)}O!${QN(zd1;K@%Zds;EY$3y`LD3>xbj7MRZDH@EmJeb{3IW@6P5#KvC1%O#pcb67ybW65IUfOht`Bdhe zv$f`T7e7Ja>&_$5^Q}j&Q*$w8~?r7H~noa{D41OA1SsXzLP#=0GF5TNZ@!0D7mA99pe zpZ6A^Mr)z@x$B(Gxk2Eaekhqwzlq}mn}s9lmbJGqZ*QzS&J&S=iSmlxpJr6SG73d- zRvQ~(Pc2dNI{4yTlSM}#Q84|be*B%1wY}eZ@X8+Cq2eVK>5hz}8yZS0(nim+@=IA@ zHpp$mJ0KAZc3BP%rK(59FpYzhRtaEeBjyDT5wQqL+7Vf}p`*Ug`9wjzwD2Yt+jsUM z%()fJhS@l;6}$aO6u>H+lv`3Xwxen*i7GZM!eju3jf#j705?PhFg zm*jLlo1USFs51=%(!M;PxGIKO7ysky=3?RIRW7v0L|Hv4`Z zvcj3zhulUwMEbq#%w>yWw;;vM8aCN(HtQg>ok@0_Mn*-KbI8n**OW_63|M zv9&0kdtWM$yP!Vwp_WW)cznL~7?>q3pU_v{{H1vAHGb!M69lw~oq+2#)LC62%D;xCl3BIoiFki)eO8{$!T5YTM*Fxv8Qz ziQzI+Q|x&Fj#feO?e`~f9pJb9q%b4dMWQ|Q!hjzwCP`Mf@eV3Xo#72n18<_ z7AlA9I?fszixKVn&F%hVrG#vF0ZXs{*ruh0-pb3q<1$3VMVN>%@Va5}C~j=B)J}qs zPryrtgqXV52k?m{XDl3rA@7RwwR$6cNJV^>nP|rJAhfq%XXx4G$eiF@xPt4vrw$0a z1@3rXnfR~|om;FkEdMa=alIToap;1u=_B~x#2RLg8*8J-U9c|hdWG)VfO5y|C$VrdQuLo?CM z)Oo+XZb~`b@m$JKGvW8K7dN<|r8ix`9J00Am>|8~dQqS&1Dq(`1otQ!!D8N%2ZOa| z-2th5$`Zh~_;0#Hvn&~l2jfVX7LJ*EzQ1QWkmfrDyb@^C*nYe+TTwAZoW)<+okt*( z@@E^@v9<0CvA7gp2=4g_srVBbsU#LM_@cm{`oCk-CzGw(8YMjMuYOaUyIx;O;TByl z*fFAdqPPpg&b-%$Op9w`4C+c~% zsSw$+R~S-hyc7Z{8nV?K#e{$nn2>oHn!SxrU`(|Nt#C{2vHNlE=2sfdF?4OP*MZOI zyh@-pinq9eGONVY=ozx+t=S^bc~d5+>$}>olZNx${t_2$1>amPZk|sCFnA6o44hxJ zGQWq*%h~a^?1miox!cuway0>!lgG#cvK_C8;kAyp<%Lr;^SzWfP*ug<|8wwmM#F`^ zwy$~}8a(s-kAc1cx3A21xjC&c zTdL=w&Y;|le(XNz?e{&|)BZqkZ`As(ml3mDwAAfn29?OHS14GsAF@y}A1R3m1n&Kr zxB#Uci1Z*!F5Gv#t95+#>$gH z9WtKUuQ>VAxBL}dw+F=sB)Z-vY0t^}Jh}z@Bv8NE9kb$fBXsDESjEBS9sg$A+^}sC z9RSU9d1EPR&yjb6V@v)?1c<8IBF0~GBS(Y)zd(gCrB}MgT?C?yKp;@W$%m)= zc4*u8@D0no*XEs>)+yt~(z&`(K%yE@uE}mtG*dRJmAG$N=?fnbi@@Ej2!upn1;+94 zLpkW|xqi=Oqc%g-W%L#lg}pjYU?mV?|`eU+diR`k}eak_b=C)K?-h6EkjiW1v!)OyoUh- zX#U?l1eF7BCO5P9O96z|&mdhKd-)>$5k``@EA;~-c8E+TAnS)u1Ufc{V`9>`;OqKv zk!=>=OW@wu^mYvlAa!(ub-RBviSJ@(v%Mb_pyp;=5rb^H2+?f%Q2W67gE6#Aq-Nd~ zLjV!O+`e6ctrEyncuFL&ZFEX7_b4L}1zSyAI^eM{LSL$!nrSfkad4Af*1)hsbAn9$@vW0@%Zi%Gr zu%Pj5uQw0XQDzVI8%|S@nSsEHKi!KW9$OD@qJOhd5aKjF1@CY8V;844361_7hp!}N zv^oF$bJgMCRj>ey}1#m5%&?y2x?kA`e<#A_Cz!C|Zg4Fh!aH{<#BAPJq8aBY-3 zT3x*LPWTk|PRUyWM_Xb|~I2taG`Q0+BES;cxiT+o*$kuCJ7 zjNS?PIc&IBB+7t%jI_-j+Pc0SMP~6z=J@!)-HX*R?m}Z@0AQ{`(<3;Sjk-eEdqw8g z4xbM}AHQ*}N7eJFp&)`FI*_`ZMy#-&miusp0bxP}WXw`(Ymp`LND};wS!4IB{Nf(a z1~(p3iKahwR09Tc&Owr^1|3BoZXj~o8zlE^$h?(PGmvy)@2VySaOc?{{6x zE4@rbXBI;Tqj#5baw` zmGkggTePbcq0nCj@>rf}7#v=%6(>_XmqqMrt?aG4|1T~8Iq&Yw{?c0yA7yfuIg9`T zD$;(bZg;~3dx;^bZX*#qjCXP&?xhyBZji&RtrtfZJ?-QT*`uQgu8Y-l!`FS5VC=Qf z?w+TAG?qrTU@*GaI!ES7RBkX#8kA7AuOLd90R~gnWA}P37~GJ2mt~p$4z={-TP?G< z8T1l7^ujj=zx+$%5UM)o^{aleh=fbz)=tBr+VJOTc`SzDQ)^$bLx&O+%wk)Dpa{TI zqR1$1E-xKaehwq)wwO$U=kIVRej9ePQD0D{U~f-kPvhi)w%&zK)HS&oh*Z2WM8bpQ zaRqwm{G+dsHiT?-_S_PRBdsmy{MKu}ziHC6GlU{iZYi)?upQkFg}#NAzbR0^WyGpNrV|=(os(NV=4=HKrW}zH;}a7dYf;ZIu!M%EC#P#`i#8lt6V3E&b3O$a+Mh^0QrmvNe5AG^`XCq5 zZldN6;U&Z%MI6MD3T5OR)IB;YgB6;L*rkdn0Y94c^i*^h<9ofly_=hx7n)K+5Zwy1 z_^U3%Uv&$J*MZ5vT&vviZ7zg_ym6~)lmP!Vj5_uw(?r3P!H-(8va$w!T-n&zFn>k! z9`oymEZ)DQKD@sIeiN_jBBmLkVW+owyek+eQLs^ebi_}MG(!UuWXw2HR2f_>ssiv4 zcE|82cw`hB`aLoV$%;QHgh&HB((KbewS4-$EYQIg@u~V5nH&+qLIL`Pk0V6*{gDxv zXBER}6h~x5pt~eA$2ap4^Xslo*&`XS4*GWlC9PtrmSm9-4Y%??Y`p)WCnDZ3ax66bznFUusHWCvT{MUaf>KmK=~zIzNbd+LB2Bt< z5v4;YfzU!wK~w~k7NnQZdzUUKp!D7Y(mSD-0LlI1KIh(d?>Xb1G44I*y~o&NkG)gY z`q%vD{N^{mIoC?bW&Il#*IG)?odHk6fOPMi4;8*95hi{GSQC=WlxXMwKaFHUAm=Y6 zOn%>~&D2&R1*3;luB9Zd-aRa_cfeS9yjq)gPCwZtFas>n)p`C`)!h%X5+YjQUX81N zXE2>O!SE&o5L{MKU-)cx=nHj$3&Fvl1w3W#Rz~T&hT1QwOIdLSMOXQG*Y2 zzDhqYE)@ypAR+$tCQkm$mm)k97bP^_yaLaG`R;Z3LfY`EX-!fP@yI3%uB3Al$87qN9K$6ZQ}>W$Uc(mRCefO6=57 zSTJJj8PGElGyR4Y0&&H70dy_?FipU0gv%fsY?w*ZvF(A9b;9S_S6ET~&(r6wxu%WwsP+ zbE97kt+%RA&ae5Zt%}s(^lMI5fcN<0jhZaBVlTFt@k>ox(cgMiCpGGt36Ad6p(<&` zNUv4#)ANidhHB0$aj}x~f;6;dv1M%}KLcT@&h0DG`;DzZH2e97qX2WTJ&PN@`pe-C z!P9=@8y3-@m+SJ2N5vVbOXC7V+(yMGHIv*IY)Ab@PbD5a&@Gxv3-X+-(xUKa2-+x> zL?v)XUOLbqdEPU+uSIfOc3^K5IxhR^VY@S5n!|@5qzm9OG)`LYJ!0UivSy+MTfv9q zvlI#GlCbjfz{*g|A3Sce>E-`XK=EyzHDkvC>F{D9{LFE4rEMBL_OekBIzROavQqti zy&yB!yK4i{hCdEOAN_w`&fHvjisG0XkZqNuXL32^gV`JnDnfjkv0ZAb-vm#XMM zcK7oocO)AfxOv-3?!!2bVU*!rso9vLHTk_kf;D#XM{W+*cMIuuj#lf1UQ;?hj){c# zPn3$a21#Zb|DQ4vtve(7Mq!^Sx|Sl91Fuh$bT2I_QJ?t1V4EXjMix)Gf9j{CtgRw8x_9Ng+U7y4s+{{#$+quL0(ym!j#)Gxq zdnF})ck?4xx5L8`ATT_^%xx;l4i}8;^Jg8>Aiq8@4Y!BZ`rwOv@c8M@ z%C7OTj=OKa{0L&H+c{m~(#}T3!Y0FNj|X*gT$0Cs@FsRJlQ1w$?M6?fW7Qe)RcX{h z@7HIZi3ysII&-YY$aiVIG=q=VzOs*iTQ&sEj$TqgiMfu=37ZFF=;M|*BM1xgyyopo zp1;+eT@Pt-v8#P?2#gWtJEdD&^ke+>#eBQ8P;S3UT_!)$)|31*FLNJQYLgpiCTz~t zs_+Jh`jD)zWM5HSz{JNv>uOzi0)G^Sr%!dz)Hok*&Ii3qdN`ZZkSJts($Z6w#gYKC zu@9-ScVg)DTu8=0*9=W%T9g-mAY8(1U99!hGc2`(d6lh zCYQ|UON!T>m5GCQXPZtgT<6+c@*N*(xT5*2_|fc=kIa5@`Immqps04Jtm~6%92@kv zL)43Pg?Bd-){V!5qJ_qLf1r@E3E`nQCW-AZuYoBZ1u<0#&cIy4R`ljr(M{1ynh$iE zMJI3V)?|lp4ebUm=Et9CE5Un*c3C9NspZvWUEE=rHa5QQ%>y0x;alp3F|k5{O4qy& zas^q81O=g!i;OkZR8tRyyK3h%lj4Rwjdi}wr++$P4&9w)o`vuJ)Z`MGbQ0mtYdaB& zJ(ewV7l&ZJ4{D-=c-W+nV)1jcncX}38eNBP(!an9KW?0tt?iY-;TfH7$hO6M0e7W3 znCPF#`nX#!Y@E&sE^J{uYgbm4pE*c;I&%jJH=FIuzjCkAEpqC+mS0EOsXj@`Gc$K} zN7ZQ6bnU^xL4?K%ruW=!v<3aT$Kj3b{gI0b!#k75dUZYrUER>V`Kp3Rh^-VvN`IU& z8SN8M@-xNwJtSG!_BEtL3m&3EZSJSP(5bw5o#&FmWmdcoKaB!Ck6N{Vz(s<|A7w=Ic3-X$yT8NierZVegtaMeQ*WgAf$_%6mmEkpxpJUGUG+$bRkNNv$-Zni4!D#)<#}9hBjwk)F zVf=)dc$njVZ1ad6AzR-gbV;ve`=hXs!~zpjT3J(9EaJn^IR)`3rx^W$ zg2&bLkg+?JmVImEi=7E>#`EvoOP%U%_t$q6$tfB53nq#RTz^!TYs%KS4R4?+&nEjA zf&k-FP%o07ed$5eR<1!*FAPseOOg?BRv;{zdluiepI=-&SQ{;Ut4eC~ z$jr>F?{-hI$Ho|9zHB*|VNyrTZa|Bkmyd7l1;5tdaklwsiFdnd`SJd3Qwo@A;6saF zMNLAet4Z!2_xsArGEYwOYUqeo{5Ac4-TMBDEz7P;CD~_R594f zHMUa*E)$`2xYidWl;w6|bwHpZlpJ3<%y<9pO!Xp5KOiZv6>>|OT41fuVu!omK_W-hY*WB3T3 z_y)h+Nix2{%)Yk=kvzLLmE^tUB~Df$FLVK76MQh0t6F`QJ%9#}^KsD?hOe5SLr`-$G?`oBXXBEG;}n2>IN(iw8#buOEA@YV2wbYAJB z$dlJ^Kgx-xdg>a6nQ>7NAP{B-q3pEUs@#o6!rt^o0$GFr+W2tuEsk0~3y+Cj$sAg% z!No>&T{nw|J%m8!?ha$5f0nN9Pa%|}J`Ehn9a>_j=@CW1o>Z)Aw@twV2dkc_*&&U^ z3<#@AY{Nsin8cXO;f>?@E63plpkOh%%~@WWbFk{?OHfuZxc}@pxDGj@9|tf6GIvWT zJK=Qu1LF8#Rr!e-4;_!bU--YiA&b~{Ifdf8U!!xA8$>v3|CS z;eo;BCv7(xkj=!KS&h`y>@?XvqyU>mGU?*GdxNxmjYunRw#@1DI3@gKTEg#Q;{ z^C14$z&=6P9}%A$`kwZGeVKF7QhRi%xEHCqQ70Hsq}(G1|Nio4P$!eiZUXB|EnQXK{&(-eH|wq?r$ zXh_Xk-;~o>q=Og8IRb6#lV}joE_0jHaEuTVgk2RSz1lVLf*JMz%RKBE%3-YM&YdF~gjwCG6%7E0O%??1iL$(x%^!b!cf;ljRsFoe zLV4#u3N^!So6k)V2t*AuT1?t=^SSa#%CzTIIywO8pXULHb$)YN=&)}v1+dXJNOv=c zT98R}CBv}t0W|-=M}F|fk58UF8LLY)tJ|l)y5@M$*=Hr|y?cjSQgWFG0{JCW#S%d@ z3ej-0HVf|z#iy>;-9xNg-4O>?clGceb$5ukNz&2Gr&TK_OoeQ~U5BO5WLWdqr?$i(xBwY@@eiK(;Rd+X z&8DgyeL((D(LJkB16(3`kcg+#=fLhX9{KFA*=~CS*aug~5dbbt#OuY;mx`wb#J}NQ zcaN;Gu`yj9kSP_i3JVLfvjK3=hNDmhy1J1tIwHR)8%uso3epJCn<9phHMTnBDF8ha zYCOy(9dTMO`tYa62kYa*#d;n^bv`;(iMz@6S^4>dKX0kS3ueB7&DvWZ7hmQfy049R z1Xr|juT9T)k{|D=Myu=(z_DXZOW@c^K&Lh)R>nq+YjPSN$Dwj8v45Vp4hoh4Fq-3{@sUg4n z;?6>*iB8;|C=1@A;^;6fVqbQJJdOcAk^-_sS(!*ck>|9maiybXH8bS$5Z#NuF4Gz! zMPMy7G1U_Db^x11jNIN-5VR&yrb65-(C7)DDR6cCCiV+(sG7XlHyanQ*HKJEL7g|%R$}8D<8z*9?Q9$lhtBnn zIXPLYmV5YeBYqsvjrIx2N?Cj`7uP2y6d)tF_>5m@56Hc$zXx2^vVcxq4LF+DRU(v> z2G-;j6ohIUf(Q4dId49%fkst~H#BVISAwj5?q|IBN;U|`$JG?EJWQfFz_pe@ZijrX zb@-s7b*wAqy|$aJ<5WA1qc7N-8!HFa3v*IqG?%jl>qtaSH%I3Y;{`ZjW!2bC$)mZt zz(AjIL}{5}p_wf1Kpw=nKXcF6d2;^(ez3&3a3RZkWSEBx`=K+9$EZitEhtT*V+4HO z2sb(&=vPG+q0=bNCEIgzb9=dU8v~qDsQ6V7v)h5&Ypn_-VuQPYdXiK#iu6p6A~0=t zo_SKD_+HHLGGuA2gqQOYlN5UD`sqfsrJudQr|NJ;a^UisiO(h{-2uE}9(y#S^T#VP zv7!_L3nV>;smGQ|i;Ro}OpB{* zA+C*NsfqZ>Ym&a6B12Bosm-WQ*CYP!Tg?k?;849t@PaVdJ9Ilqa?Q8Z&?Ded90B3s z;bOjm=OCtP`qb5={$0g@D^}JYh0ruLJq(6#jIC3)kyKZ$t*E!X>>!0CEc`$GpYtpk z`?z;R$0$VMMxQW--^{aHz0^sC==ssWvjgtd`)ULGsi^#p{ivrh4DuOFc~o@C2_PE? z7DaOK-dFSYR1lUNH2Ni2Fj$W&+o6RBiWGt|1XcvR*W{j+8dy6p-)QYC)xLR1k7))qf5S4gsSd%=A;I1q!Gvmbd12*4HnGyWPhn6Rs-gs*$6L_Dv)0SS*Zty2Mq zC#D{93lP37G635@{3@7411c$B>WA1}R{@uk+>g#QN}aXAdUFo2=Ga*5?Sn}aObcA? zJ7LIY{yG)=ByXtz=h;icK{WMyolriLGS77#O=V4zdC)sDviPL#-{;p>wiBUHNqqL3{7N>wfPu9FG*G=M|^ zHFd6IySu!Qo|LvnVfho^iXC>J>5!3pj^@{qcrXR8ckZ28TF#4uZeLo0z(V7!Fqajd zRm^;O#moA^Oel?1K^(hOqm}EVGlh(gK z!X9-N2QY_3S2S3rc1{-U_h%WIYA<3 zJ_8%v5pBEi=G+DSGYXX@UhUDUEuXT%k-)LzDjojSjcj+K`N8&I;4ty+4Rp#nm)x(b zr)LWC9!Ef5UtgB-1^*2`;NZXoY;A4(^dLR!ASPr8O~D)ivOfFZzi*jRK`yixeBA&P zVSjD(isDH5aVOfIB$?^Dr0=2d>M&R~lj1i}Qh~r=w*a9i;(EBblzR$_u)L@<$^8@V zttVMmveD?d0_g>=e~vauWHs`C{X0sZ!e65z-C0X4g~xuwpJ$rMn}_M37tw!O$7uv8x+Br znhCDJz1avEL>Q00Xni;rp}D_44nL_;?O`Ki4Yps()&&TRLZM(But-ues`{NSX%zz( z6A(|h{dGXOg+}QjDxAjs8od@DhM9XVr+;NI9SJYz5ziY3Bm(3|x5Wv1#A>q;9UWbQ zYa6$t;4Zm_gaz{cYwCGJv~pOgj)tTVk$om7voBw^c?3`=&Sx?wAYS8NX8~3^WD}j6 zE;KhcCseh14A;Qb@85Ugs&i6QTiN~?yrU_ZuGy!@jN+4#E9K>kij3ql-g+#;bqJU- zAL37tG9k2qhy-5imFyS zurKMD`!K((!U9PS6u2(D0HE)0((#&3M}@lCzdcXoxrt9-*J6PD{slr|NNx`X+_i_r z@t=gl5fCg`?YvsCu7V;}XaYG7gi|LtD}b|RO6+wp*yF#@3nDKnI+~LR2sc2?ky`*Z z-%HA{16dLQ;0lQ71&+0`Q>(uK02T;d3nR?@26p0?{Xz&AD zqf-P~ajeQ74%YU$2%JwI_ge`p!VnNBati<~#4p2(|8yar$R{3Qn6BiV^>_FT5|eCi z&rIM!OcRMe81C0|2kZ!tEs*hVgWN$(D1gm<_;o^0G4+^40Ti!40f+#|{sxlNGO7G0 zr}T3`K>$>4_c<`waw`PpM#Kzwmt!yCIH4D7JazI2tOzgxEs&|#fu{hp9RrwSJya;( z&jLwE1_TJL3SnP;X#fo;78(9OUl~BP7u4Y5oz=GiHZBPOki>#5?ALS`BKdoG;)*&R zLq3BvW>rnfP7AWZGW<~k0HGup2jpcS$j|q{u|F(<7r}W1nPkOq{~cyaw9*vG!Liek zwhi)ihckZ(z}+P(e>JcHL_9tn0qF)7ON54h@@+5jUk(57IS9PiecS_Z?#IAXk%=_m zS>5d1hT{F0L|Gq2azNa_!uTPuo9D+!K6BT+APP6Ae9v0}MF3;~!V_`5I+&Am9nIFc3+1CrW_UHHDS|~X3z*81r7a%a{<&Ct_%5I<4^~%fn=lthDva|Cr z;;>|eEHPVElCtY~Tmr1+XO$$qzr(Wn8UM4_8z2f9AYXo6?+3LN&dHFty3xxKRBYh! zAArDo?<|He3;1Bwy{%4hU>iU%rLBdiKzc3!sL2KCXQYQ?*??ZKmaEa5$jt67b#0!9 zT-E{*4am(0R*;%D@;&-3+7w$YmzxorisTh8<{CAW5g~=Yhxs>^+DE z;P(4O0YDSbpl=3o)vmZ$Z660pX+ASg&Cx6^fZ1K9MSs9CiGjh`AP%z1kwN`EP%5MC z&$8!U-@EjE8Wh6m%Acv1m#~5;KAKHe8~boOIj#LA?@DklNlR_)B_E z=Nv-SMEDGZiZ+5>wQk>Tz0j`;y2>j$IGByLU&d)jaT890y+0l2UeT0$Xm$qj^VY8? zysUW7AB4#y7#-$UE88%T4G9@-q&fw1wgZbkoFMmqzAJR0q`d|^aJp$O=RMnC2P7)# zu?G3QS;Af~wn7)VzRI;HbH5U>C38741^Vnl>M(x7vqw*cGTK%Mhw_6&S>%=q^zzFu zgi^mLUcup8rsq;-io=2ubabV@Uk^{a4)hwp_Q5_|m@cdWs!-)VzE9KiFS^c+auVnG zUC#Rz5=9O84}(C*dPAcq;dwXDZ!H3y%brW5qrf$cKq3dQ6ALIsMC^k1KIylJu`(yDXQ*=ov|U;ysuMu%1v|=S zO9><*GHTYfESwaO8OyUHp?tqcdXu=pJkPS?(j6fAAMela`$wLfoFvMY+GiL7`!*}H zt=CPXl)EaSx>kagsNmJZ18I~pR2^fHpzNdFadKzU%56#IuNner}x>i^WHPCpPD z0B<8I3x62_pk)1pAcV@EbhV-Zd>Vt4h+F=LPZBW>kag@txpZR_NIuMZK$dEK1!yU# zq40qW@eS79i8PAyEg_&@yizB+QKAYYq5i)zPH}xb{!imu5bma?raa&WHh|Cub`Wea z&~UW(_Vz-P@>~J*gi81UVz<4$4e$V>AlU6fL2MO1 zvd0IRE32!2D|$!Abuu>J&oDZEJsll$b3x8mUw~}nx7k?lF}22>8LtiG{EfJCkTESF zEOMTwS|XfK3WA;vtE_jtA8vWwOaVLTLrJk;LBu`InXgO99xz>1mg*kppjcsib^Z!#fPOJAw-EK& zv?U55+wGs*9j#wAL3L)7s|r#O7=H5mkK0E7uQvAnou;9-&k7=Z%GyIRTNAWp$`reS z85H7?z0NRMZ=CoU!|Az*Q%eV=U*>wYdBYpDD*M7Gzd#jK4~2E)f_6FPK6bL}v$IH@ zMn}4;R!`s&c`vOmxs9eZHLvTDWUk3>rfj%C3Y`E>2RzaQJvZ8>80kSXgt zyHH|TD)5@8NvvveS9TiY!t=0%d?JstZvqw=oOI>4^+&2u_Jq~1m`zl3%`#$ z{xmgw+YJD67PGV0(T7LJ3U7cb7MvJiiG7o5BQ>L{>Qn$}AVyFVKIYn@CO(LynHNd0 zD`J*bN%gm@;GW2pvNuqZajs>a>Y6JWzmBou@nB(dMIe%bjZ)h}m?8?&g-zgZ^{Fol z@2%FE<}9&S9Y}63fr`=OAD{&lzc+}e4+Bp@=V!rK^Ju`fN@OCJciH^~z5|;mtG%UP0ur)$v_4<=$1E zuP9uN!Vz$kMi0om)>hl}tV8)9V1?#u48b=*}Qqh!>0;f+X1@Zk?z>Lr# z-p5^KxG!XGKCt1$wGc{hPtwu&@eKP#uk1#APu z{#sXVM3j_S_m!sP?tz1qazdy06m|)*x7?ozovLT4V+h|~NUVIKs%l&^Hx6w;fK(A$ zkST1t=c)z3YrL5B}_6Ed{M=>T4H-|z7EBs$kt9`r6ifvN`Dn*wwZ{{v#GtE)8; zD>|ThbaC9)?Ai!O=*gb+Ud{JA#kn_yp1(+b^N~lB-;xXfLpJ0INQ*tMaj1&jIp;Ck z%)#MdxQ)<;&(UsfLy~TM++HxUFS1 zD%P=#f&u~gDSl?t(PjqIZ@aJAG(ds|9plO=FE|j{m5uJn%TF~b%%sjfO*Bv0SESs&YW1N7j381?5d*s#$1O z2a+b}GJFm(xe5j`|848)-|)6;LfN^1Ux?wLAdAjU3;^|C6`Jol+hQpIq!nU9Ypv2L znfgaT$AChwRFmYFQSWu>odytzltgKJWR5O5&ackFKoor8UsPn8CP4hVK-=i;fuQ4f z22oxwp$pRmy)apRAYE-W`2d^S6>5|CKLE{4`25L4XrWvgxgN?q3T*g$W8Cy1&vIs6UAP z11@JsNuxjhQseKV>o?A0+_{1NQ2`DMV9VM>T=I-px%Q>{VkU;T9B4 z0;JNijt=ak1n5xpkJfFpqZp#dP0#?XdW*US!zf%9dArP_3~fI`R%^hLm-TTDG{y|1>OTTIBj zbpCNqRDXswRSuho^LMZpsSHGX?Hjyb(Gxe29tryO2uOkprUP6-T30;KOrfChwQ`1n z<77KK6Ld6E$wdIyn3~ub0(Q;K$!TlYj-qKYAlNk#g|5frM0%+{gaDGg7D^WsPC#DZ z2vF~qH3zE1M#BoIe1Qxmy&c49Occ0?8{*J>Q!DcoOy0N}bx%r0k(|LCX7S?o8RwDY z8k#0R)Yw8wc!5WNt_g^lhzh2k=z?WXhgpUCx7Frn-Ns1wj+O#<>?lt(sqtv~=Avx4 z`l(+tm=)bR+sm@%=LRRY03oQwTQfI#To5mI3OWz7dF7)bao{G+tc3 z1{-#N&_9gU=>dTEXc!B;Vf#=p@gfJ{L!c|PMr0kJNP?v!fz^X(0L^`>t*yPalN3b5 zN@$MI8v82e{zU3kAPvYcGCN~Xal=Yb%To$U5H;?!I-(&1oI%Hc*h*xq02O5T#&H3c z)bkBfZ{+UombuJz%vUkMNG@+16kuMUpice7!t%KknuL<1x$3`0i;#J&`3wgD z6PM5{I2z8*G^aN_Q1P-#mq*Xk6tjq)WHF^_y;(}e{nF%@CiUy!>Zm>)zKj1fL+)id zKc2E$Zbj+Z{i4CDiTP4o|S+y&Sf zFgx*}ET6;`s$Ci3b=ww%%so&~ig0kcOM?>D*+3SI5(IJ+e?*Mfll%`_>i-Td{cjrZ z{~v$ah#>-SW$7#`NOe*5tdkrVWiPuQ$CRx96J1+d8yh=Tv3}lvW9>Vh^ytWutpVJ- zknA`%0Vx;YM3lgxJcIA*fPMxhT&U$vHYF^>LDOtVD)D?4-Rj&T@ejGX)#^BM34+ld zWFtI#_U!4iXE7+$a0NfOF~H{StgXKhV+5e}+dDg;=MJWXHXF&<&?fz)f1YV)57c1m zfGUiinIX3S9fMWCqm7ML51Eo{SLnm`HPW>~r2wV`27w~MDl<}v<>uOzNlE^0;sp^Q z4ikWHf}EC_m9+%K37P4DE1{XD9a5<2R_DJWn=2NEt z1MRB4o%dI_;2l0b9v&V(K8GFW8ctwLW>aY~h5S&^Yt~J};j#kz9+1#Ppim5=8ejIu z@_@P0>Y*p7}FFzZovc5ZD-RPN-NXy4&4v z%m&kEtoEH5s$v*`I-d~~5d!QCC#|FhCu>K}3k*z22MOCFJ~~EaR{gi*oQQUk^Zq-> zQzF7SxMo!I{A5vM;|o{;YIt_8yAyY?YmAji@XKud{kCG4+YN2vPAB5Hq7v}QVAwO$&_P@{QzG9SUIj_Z3Lv5)tguOyVYtj8|m zr<+P!x{Z&IsegP;<5G{^T}--!Z8&bNU(}2dSx)T(lGfImphhe>`BG!Cx*0yf{)y7a z@nJ)TTOS)r7Uw|DWws?gwGvnLT-aC&-WDl%HDGhBZ9a}Vzt-2(RKlfc;6k$_;S{)d zYdUeLkHC^uaI~2grnCMh^NJ$5MU!S6;;6&e40x>|#?^{^T38%v=UM2z*j1?m%8-@J zD;gK4JL1Ik#OsS!a;_+mn?KWznpC{MRW+PHYM5X*wA!6FS|Wbx(BGMKfg@gGodNW> z_LAU<<}`|T>g)!`mG4OAtZybwPc`T;DoU(ZTc;l6MEg$n=*Qv9)GG%zNzvx-^#xA5 z8rasw!97f6;P-3l8VCyRlscNY2EJ8wsiVkxjsQ(+JnTMsAz?>v7HW#0Arnmja#z0+ z`E+*DsVkJckiS}jQ3UL!9d_x3r}mP;~<1|6BLp%)seXQd5%k5U`0 zJOb*ixc)XX(mli3s)6w)>L`w7Nk*? zFEKM8kUr|RB#!S`s4utjYy#eX`)CVzN0z`4b`+u_Hv%F+x?jYagG+vPWw9&p&V zx4kNSx7kwCYIf1$aBkVUydKywxekU8_(Mt-Pv2BS*lOppulI46iWrbNf)ZF2B^19z28I}6IiyYQov zG*1d~3nMg&BPVfFYm^*UlU(rzPUfded#6exHSX<99G~zzJ4)on_{Ax?6OGv+I}cg! zVbTWZXd-;wPduP!HL@No^6AdRcB{E&gZOstEHP+)fggO9Du~rGF!n+7A0)^*I@}*A z-RG(VQAGJh!&eoNsdl7ze?)sr9_zQ({KFbNM;#(%yx5AK`Jjq_)%2Ug_i(O~Y)@)y zK)6fd2z`GRJ7s3zY!Y`6w86l6yRPF=bb<^%CWD|5Obj zn&aH`53UBeJKjDWqX>EF`PT%WpNE&lz1Ond(3lYj^)lP-1|~vQOEKxtJ(=Bwq|Um` zlYuwY>%sUu?C&Hdw`MwnPe&8kiv%SRQVj7P7DoM(?Oz1^W8t{a)Gqt>=sM@!m2 zoZl+6Ktib26I&-3I|F_gv_#?Z0cV2WUXH;~2OC^NrLd@QB>^r+fspf=7Dv_Zwyfkb zS!rIAvzQ?0lgOBn0qoPB>bk|jL*a4*=e}b=u%5+UPVLV4KO#T3G zEf`05%M?`Z(2$h47u;I!rIGaJz#L!6g};ey``BugNSkZ7$JASKfcN|H_Z#{RDT|N1 ztmjz5CL}Y=T`_ zdIFfyfn+vZnPGr`FfZ@_azN;L8_~(7h7m`&oh<66Ntwznw0&L@#`P^&1W(l8~p#<^v{Olhc;fpA6Km zGvU{dauOHS@Y7f`JX?O!2SA^fAYe2ebjbJ)h;t}qq<<6leI%?&}h1ha^LUS)wpHOW8#$Y^G2^es^pwMcphV z*VC(ORQ6-Acch?n8}H|uXp}Nn*-}I4Vr@L^{#*)$1Wc)RHmR!Gz3|ZdGpRL&AVlVEb_<0N+C1b%EO$`D9SV< z_s6HDw62cK-568dXh9cQPQN73_)qL^$MZ65kaGlVQ4bC14-@}!iWSXM+TU#(3LW7a zeF@Zcj+L?qb(fi`7sE(Q;Kk6&U8JCStj+R!r5t*^B( zU#`2B5gz=Giix=?RPU0c?P!|4;x%M5IzyF(@dJDbhLR&;WvpS9K50XVBj$UmzvfbZ zt*c%Bl}o=7DzHM%Z@ad~QEKkzwcj&iKLqFxxK@~F!@-xZx)EdRZm*+xNhL z$D)PU6eUh1Qh9dOjS2sX6{kxJxWl^W2K_HH!ERtvAyws5%8f&O6DM!pR~N0>K56={ z5V@N_3+u%t&wKFI7P(~FTV0eq&QY?Tsy`Vyr$D}J>99$s6Iaz%l~;S%gtd-uA$}}9 zB>8E^e^E(3mC4ip-u!-gHyt2i3dY4*zm{ zX^>fePD+ci#6Fd@CM)K>^kN8Qkhe)1^~c!-jA-hz$56+$zK%V3b$)z^3Wu8UU3+IY zwsWa+Lo2akE3s^0lpF#1g)f=6XZ=i!O?OTX&IbXbTkPc6+lhBF#uw}E(pqdDB<4E$ z@*)tSd4qQyY=Jw59x*V>tR8tX_?Er=!BM;(g;FBFxpAU2%W<`?e)gd7<;Hqz5LFz@ zj|JfL@+hy1k^vHOm*7z%R4vC_ht6vq{WhC<>RY6)Mh|C;rSPqTqR3wza(|bgV=TlU zU~9mFgV3%cPW1X?$7M!WCkoZ=nBkQty`n*c3+859r7S7ayMsLv1zsYErw_SWR$`S9 zahF=PVF3ZlbRkMgX{7h0#zh@UEq=c%wPAuQquh4EBQXfN&<6XT?`tYVeT|q->K5&C zc{5tY-2yD3T_+=6B@5K>&}%M>`(tFSK|Rj?qLS4a=Av>UVy%}L-WGrdF0{1Eyc0fO zs7Td{zB^N(Y<-E!qVp3kGWy81p-4Vk5bHNb z_gmv;h8gWCt+hBRoUzp~uZnY0;t>*O^I7}HHMDJrpsBCT1Mim2s!s*}?@G7m%$Z!W z{PnS+1vy92u%D)Gtzh%V?E5O^nK4>nb%yXV8C%U@*FJ3JOS6l15>sN#>-kHGsd2yN zW3D#6mp1T&I+II#FiRDgC)PbzpQX!tr<&u~(?j>vbatU3K=f4P`YA%`nzz%gvhBC7 z-RD1S#bv*5d*a`;F9=NZdOC7f{~SC|#SzN0^8G^?g`ZcK!D8)%XGf-BU4!dLUFW4O zu4?u%xl~>%X+JTq##oy3-9~?IAgSzrsZcT$$UfQhcAejY$FRe>P zUYCVTHMs6~FLaoXdmbVCE!OR3ojc!Ap^4YI{GQ(y{ET*%y-la6as%RE{wA|e?|hn4 zskA^6Voix#YPG-%*2QbA6oI@Mg2)VE(c#B)FRy^PhrqitU9GY0ioJAsVxL|+ee3G& zq5I=%u$_VF?a@_6ep!N!9SR8`igTu|QW_WI$IXz9@9b5B;E!2@mEO~THKRp5v00;B z*D`@^HRW@lujtOM4(DB14$inFtJ-V4Q(3+u_0*dp=;nai;Nd$}jgSP=xma5BxB=(K zBEmK$DPdVDK3oV@#B4gQWx;i+>L#P|OA#|eHjj;@02)R?g7yAqWz=F^-Q}rmO!$9S#ZsNn?8H=0WI57^HQ}!MdK{T<_`;0gNrV8_c+1Qb+)ZMF8F1_DsM5oNZ-8|(^ z-AYVvS&ylE_st|sWjO^Nl=$eh>Tfb|T&9T8@9h!2??DWYk;szr7U{V(?+ zvMu86?5J56=^-}o_V!?_2REbRCKWI7_eiW_zZE>M$LrfQ7ETBP5eATC`uAQ08^ozpqiyklt@Hsu4 z5r#U}kpQHmjQ9VNVo^wYA+dFY`&?LBpgU-t9wT_ZoO*buJy}@6_J*ROyMlt*XGlVyw|<0z-vX>uBIhtU zOdwkb`|XPz^)iqZ$MwcF#1zkcKfG5c2OCeA#nx>!z$RL&`pR8j_2gWFN@r%bvq9g={J7y4Fgxsul%PdhnrYUcp;nUuq(z6+qA%>A}E z)?Qe>zl|;M6jeB;iGYM_nd;~y*{P0YYo>!e7^`FTpWVcfPM!WPXy+1Lh~8X9o(_KJ zO`~-wl;6Z81z;ymnmO&)o)w>THk>^2MNbrBYj3WYQ%HMY+k9vH)2)9FpYF?N*5t^! zty8ml&h(NcdhWiCJ0`3^vAz{7NSex|7_O0<9NK-PhmABaY(NR9D0I`|Qe$lH?`ZnWnIGSGXz?*ezVbV#euvieUY(0O2;^G%b=vFn(F)HvSZKfeBwZxc zbUYuZ*U&rg+wk!^y^bmLk9t;1m&3@aW)kzSv=gca5AEJM-ur6)05p1k^ z&M824zNN$MfOOp}DW@X!@h7U>H)+Llmf1E^xmDajAH`t<%!Q-!p`sZ{dY#vjwR!5sA(Bt2l^?_p;mB@KGDnQrbwqDC zzG@WQ@=$$MvzA#vwczyPg$3suPEO8?SL=;2-W&VsOm8;!C#hd?KBtw;XMuZUnCd;=jJ)@h z!ycvL_L_!&68eSin7Ui(m?k$?TA>e5Px|b18gJA7ma-7d{7^<+HBrY!SwPku;of9a zZg}3|^(c>+dQ+1jw@`K?TGwf{Vt8_|P);J_PV1S5!ob4E&!_JXY@Pb++C=uesYL%g zJGzB$o>_aLqpBX%A0N#V9vjWqDK34>dIgo%Z)Aj7qo-Z3w01r+nR#8pf{E^N{TBL% z3U1N95-(oQ+9f69^>s=F^*P{VtTfh8kZ_~U@hHRa6GiUZR_T5n6(;0fO?w00o)DMI z79SPnbRq<0U2L>-sXVB6h#Q-5tAsAMz8U29E%{ZL-xBXSB86cQ9`tSGnGu|P`X$X) zyt~J|KT*dj&Wcufx;dk%DwOH4RNv@A(2ck;gkA0&A}1&1(*Qij7_?dSYBcv)grg50 zcz=!|ZB2WvX5Jjv5-!DUUosY(ryOxg&3?U9_|^A@`#g^%8%)Xpum?QeEV=cf-= zdc+rt8ak?5Z<7fgg;f}w5~BIf+E7!gAy%l081gEPUU!gjga8%V#eOpmT!(5)zj!&H+ymZBpA zA_ubVSA6#r!c-GRb!o_XLX9gY#zS!of6`;Ap{v!S95)=reInjGRPCqWRl9(9D?|22 zc_!Z|dfxpk9yPyFKkYe9&Qj@ART*_I^Q|k@JlDS8sDL1Gv1cBc&TVXjk$$)bH}P1|}ebq0H=UpJy7wd|?OkgReZLI$F+i_X+R| zmLSHmbj>!hJO6Z%LEMU(EVWzqVMixUVVv}xyG6Xw>G72%EA_S!Yy8|7=%><@LtEN0 z;U&stfiRIzqhD1jQlJk%#ffv?cFKz}k(%&Qdij0%Vsz872Yb;+K~a@@KeReueYr<= zmGor|jpMccg%FjE6lgg|tN1A0FV%>`8~tGvx$jf&l$r}3jQws$`W{ru%kGWWE*zM} zxh9@tV@hS5XNEja$=iJhe;DX>(9J@TC$_{b57Y4Ti;R0H@_Q0BYM;G)m%ah(v-@ke zSa*Q+T9$AeMGhM)6}_r6c!&Wt?T7ax($%NJGPX4yzxQIjGxy_(%Q30oeI4i(f~p}^ zcAac<4Z@ADBK!HiR@vUDKj(=-simjn9#Xtf9V;;XP#66tacZ4j^||_fbma8{VB4aP zKbPniYTzkdZ?gt<%qD4kuX_a9-=P*LwZF?^|C<->Nek)ta!;y;pGr>;JRI=!zqNLr zQB5uF9*^fJ*cC*ofCuTsASfVBcqR1Sn-GeC2!d$nC5VV1A|;?fBuEWCM0zLz=?X-N z5Gf%@N4oS7!rk%Q4|m;l)?K&Fhpc?qvuF0q-ZTH-%=0|sN2hfNd|5IW=94g4fV3UX z+UOS+JJ{qa2PDVp8F_j7R~l4idDqqOR*IlBZe$Q+tjm!TR{Gft=|Z@I+l5h{a(2fB zqslq&dtIYkgCykInfDdEFB6Ro_9=Xh=T-9Od#U4vjOpZ6N~b3mrK5xQyMiYWqE%C# zepWlq8g!h8L0uif!mt@rzVW8K$O>Atq}TqvMIXYu8s3eW)axonFGO;Se{BwLyip_K z1}`IW2b zv9WaVEsy8p2B)h_4attjPzie-!VxjhR8&Ms^s_HMIJnsKVbsg${9JK%J}>SJ3fAI! zT!LIuG6?!^KVWf?;opPIiC=s*)ngZzHDFJ=MITJ-Z{CfbE)q*H9z2R^e*Qs>AgtT& zKUbL%IQG`y7?{(A-Abfy?AvW4gCmt3USC301QI>a`)!@=^DeeNtWG>SW5E@7F=Guw zG>5_myCoUMqyh4l3g`Mvp%@m3M*i*_B&A~CgyDlj_|1e;_tlAKkl19#tPeFl4=@2} zhK(Jv9-n~3TY9d~7DU>{$0zXf@ND&m$J=D)w0CquI+H*XGeEoS-Bd-?!dZ3}?F0$V zBiltoUB2go>ECA>Msb^X}$B?A)kvI|_vZTW2-ZavQS7KVe*%c%_LyWa7^Lptk1}d~RBL1*}0){8C zDw=Fh|K)oP8gdDA-mhFa%i%HO@}$(^0lvFQjL5p5Ax#V9%M-o%`uIL`eH~w?Qa$E-sIb*9XE<8`+dR`k3f%!5LPG zE%HX#5IG(Q0+R)A@S)54(@#}kk;}hh|LM;F;Wfug9(OuV-~<;V7VD zQtBWw>rz zX6Jrrq0b1E5I`CuQg1ORK!ZdHma09V*+{JIqg|WIvf_S$-^G3YM! z;I+Xr%ou8KkJ!*KW8%KLsmjav@$TJEh~0>@^Tpt`FREjfK0asV?+Vl&On$xEQyTPx zQoro(JB0qwqKt7h!T;4l-ufZoACmY*vNb%^({yKO~BO?}oiIErd1IgMJv)DzWt&1rt`^`O*>v>`lK59);C_H0zrk;3KUL%U{tu8VW8Vr z9iR$A4%HKBpNNZ2OS(^_r!<~s#9X8i<8IT>-e=wCdqc2cctrY(n2dLMI6 zWPyKqmB?T&)fm-^L;jI{F6uJbr>Lw&{{`Ly=~&nbFI}~$1>BEE9I73lQEF9tBSu^N zr^+bJi1J#%H9&UY*KE*@q=k|ol8u>|o1v9Ga_im)>CspjWpfsR#@(YrLX%YyVX8CH z*wg*%;X;$2bzmN7UnVEb8A9~u>kEF&KB@IKw321#Brc&PejZ|dxu#WYI(~N#P21sdC+{}}YC?r`3&k~{ArM4_nskM*a&CYUAejV!wC zPD1TtYS8|?+QD22;a1=@L%ZHL>0fqoH)^KWE!bb%1z3_&N2qq4layd8v zpMmlERPp_{F2gQOSaf}cHPVlSnjaCsunaq1e}8&ZFrVV;PJ;46ArhSxXXPx%E>#7HQ&{P zuDx8}#&F$H0VSf)D|S%6aqz-?9N2TIa9c~f$&~RX_;u*j6ha%19`~aEJ}*+twE@10tR1qR>dh`U@Tj3!4j z%PTJIkriqOQHF9ZCEpt*WSmLQQxWQS+vnjI(8HV!q07@VYR&tjS!mh=$Bj^|p&~IV z@r)F!#0ik&?M;eH{K<`#EM<-{P!5J}^6-bhaeWUJjZ>ur-kh2zij*qk7;iWCIs*8Q z1J)K=^$Y%}TyQNaRJ~^6A%D?AP36+-$4rWVVT%H3R3%<7Z+4qoWK*7?&3pQKQL(Xk zvnxrniNlsBlG!d)UZXah@)s!8Ig@ae8SrIO9CX=Elj3dWxj@?8dAG?JApu>%#aHmct!F2SUkjLnjYZDYf?4Z8fgH_4Aua z*hf8CI)PdSBRDE_rTsL$nGrqcNh@QIymyh$3$+&1v_aX`!H@v64>0S>XqojsSw!ra zT!$AaIqM+4*&Qj}{Y*(8*fg$L;*%&urgfUz453J8ELw(qsZ=ecgQvvvP@)WM8#PEkBrH_DF z+?gumZ|$D^6{pxIu>i-*`QeoK^IML9jS~eM-dXd#W3N&738%B{OhpV&tR~kvXR2`S ztrWF?PKcQmgbI6x_9$z?jM?e~%QpN%7(NS|gA=rs9@4}xf}Jpg%R4-4B@KvZ=u0CE z{oaGIr7ZKwU}4Z_^g2a|I(`Xg5IX{`rm|PTE-f zO%0}}S(={234x?y7L5|U9JEsY9XUJ~n+d^PnGE-w?+r9JlJbd^Dw%q(c{DeSt6pIZ z-bOtd!a1`~{6gLvo+2kM86@4Rr>$;NR=GO>LP<+n2M(^3F8v!Fz~{-C#BxQ%c{bY& zMt>_=bF6tgPvJ4r9xHz%Ugwb&V?~%vra40nj?3OFz(Rvk>O4YziifYFpUlRe34A(t z^fl-|Knwe^RwlswU+CnYNg*a84y&+BZPO0}0{($OrjmPDcA-a<{8zfkG&N?CPOoj? zfVNpudasHGH;rrZ{w@>@CWdW%zT}dUBkt-twb20huY*8fT=Lu%B}y3Q zm%gi_2Wy)}#r;!27&;KBBG1C|fHc(=FMxzAWg21j89m70JAi#P2=p58Z`(nHguise zT64ISXJtMxFyqj~ka$kVBqt}U9V{42BK3Y>C}L@O=n=F#Tseos`N_!0Y{cq*nlBYp zFtZ$-T7UTpNH1R+8_;K9jE)gGFIShsG-gemY}bNR0OR<;dJ4-^XA0imi(H;b^&SAK zel%rZcXzkDyBqlZ{vE|6FxB3x%(jgMYqDv61*%Y2S0@LcdPhf}^p&{Sh*w-zVMr^b zOklBH2md8Z`uPHfSH*m; Date: Fri, 7 Aug 2026 11:27:30 +0200 Subject: [PATCH 65/72] Update README.md --- README.md | 41 +++++++++++++++++++++++++---------------- 1 file changed, 25 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 2f64978..863c29f 100644 --- a/README.md +++ b/README.md @@ -1,34 +1,47 @@ # webdl - +

- ## About -Telegram bot in Java for downloading social media videos using yt-dlp +[![Top language](https://img.shields.io/github/languages/top/SegoCode/webdl?style=flat-square)](https://github.com/SegoCode/webdl) +[![Repository size](https://img.shields.io/github/repo-size/SegoCode/webdl?style=flat-square&label=repo%20size)](https://github.com/SegoCode/webdl) +[![Commit activity per year](https://img.shields.io/github/commit-activity/y/SegoCode/webdl?style=flat-square&label=commits)](https://github.com/SegoCode/webdl/graphs/commit-activity) +[![Commits since tagged version](https://img.shields.io/github/commits-since/SegoCode/webdl/latest?style=flat-square&label=commits%20since%20tag)](https://github.com/SegoCode/webdl/releases) +[![GitHub downloads](https://img.shields.io/github/downloads/SegoCode/webdl/total?style=flat-square&label=downloads)](https://github.com/SegoCode/webdl/releases) +[![Licencia: PolyForm Noncommercial + GNU AGPL-3.0](https://img.shields.io/badge/License-PolyForm%20Noncommercial%20%2B%20GNU%20AGPL--3.0-blue?style=flat-square)](https://github.com/SegoCode/webdl/blob/main/LICENSE) +[![Bitcoin BTC](https://img.shields.io/badge/buy_me_a_coffee-BTC-F7931A?style=flat-square&logo=bitcoin&logoColor=white)](https://github.com/SegoCode/SegoCode/discussions/2) + + +Telegram bot in Java for downloading social media videos using [yt-dlp](https://github.com/yt-dlp/yt-dlp). Send a video URL, get the file back as a Telegram video message. ## Features - Non-blocking message queue processing with virtual threads -- Dynamic interaction with Telegram messages (send, delete, edit) - -- Web panel with usage statistics on port 8080 +- Dynamic interaction with Telegram messages (send and delete) - Automatic retry on download failures ## Quick Start & Information -Webdl accepts a video URL, downloads it using [yt-dlp](https://github.com/yt-dlp/yt-dlp), and sends it back to the user as a video message. +Requires Java 21, Maven, and [yt-dlp](https://github.com/yt-dlp/yt-dlp) available on `PATH`. Set `BOT_TOKEN` to your Telegram bot token. + +> [!TIP] +> Prefer Docker if you want yt-dlp and the runtime bundled without a local Maven setup. + +> [!IMPORTANT] +> `BOT_TOKEN` must be set or the bot will fail to start. ### From source -``` +```shell git clone https://github.com/SegoCode/webdl cd webdl/code mvn clean package -DskipTests @@ -37,7 +50,7 @@ java -jar target/webdl.jar ### Docker -``` +```shell cd webdl/code mvn clean package -DskipTests docker build -t webdl-image . @@ -45,8 +58,6 @@ docker run -d \ --name webdl \ --restart unless-stopped \ -e BOT_TOKEN=your-bot-token \ - -p 8080:8080 \ - -v /mnt/drive/data/webdl:/downloads \ webdl-image ``` @@ -58,18 +69,16 @@ code/src/main/java/org/segocode/webdl/ ├── bot/ │ ├── Webdlbot.java # Telegram long-polling bot │ ├── constants/Messages.java # User-facing message strings -│ ├── model/{User,DataRootContainer}.java # EclipseStore persistence │ ├── service/{MessageService,VideoService}.java │ └── util/MessageUtil.java -├── panel/ -│ ├── PanelApplication.java # Javalin web server bootstrap -│ └── AdminController.java # Admin panel route handler └── system/ ├── command/CommandExecutor.java # yt-dlp subprocess with retry └── util/FileUtil.java ``` +## Download +[Latest release](https://github.com/SegoCode/webdl/releases/latest) ---

From 9dd3d3ee93f03be3b07d7131ef5efa69fcaf0019 Mon Sep 17 00:00:00 2001 From: SegoCode <35817798+SegoCode@users.noreply.github.com> Date: Fri, 7 Aug 2026 11:27:32 +0200 Subject: [PATCH 66/72] Create readme_example.md --- readme_example.md | 69 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 readme_example.md diff --git a/readme_example.md b/readme_example.md new file mode 100644 index 0000000..df84490 --- /dev/null +++ b/readme_example.md @@ -0,0 +1,69 @@ +# webdl + + + +

+ About • + Features • + Quick Start & Information +

+ + +## About +Telegram bot in Java for downloading social media videos using yt-dlp + +## Features + +- Non-blocking message queue processing with virtual threads + +- Dynamic interaction with Telegram messages (send, delete, edit) + +- Automatic retry on download failures + +## Quick Start & Information + +Webdl accepts a video URL, downloads it using [yt-dlp](https://github.com/yt-dlp/yt-dlp), and sends it back to the user as a video message. + +### From source + +``` +git clone https://github.com/SegoCode/webdl +cd webdl/code +mvn clean package -DskipTests +java -jar target/webdl.jar +``` + +### Docker + +``` +cd webdl/code +mvn clean package -DskipTests +docker build -t webdl-image . +docker run -d \ + --name webdl \ + --restart unless-stopped \ + -e BOT_TOKEN=your-bot-token \ + webdl-image +``` + +### Project structure + +``` +code/src/main/java/org/segocode/webdl/ +├── Main.java # Entry point +├── bot/ +│ ├── Webdlbot.java # Telegram long-polling bot +│ ├── constants/Messages.java # User-facing message strings +│ ├── service/{MessageService,VideoService}.java +│ └── util/MessageUtil.java +└── system/ + ├── command/CommandExecutor.java # yt-dlp subprocess with retry + └── util/FileUtil.java +``` + + + +--- +

+ +

From 76310106dcfdd02fc6b391e76f9ce98e117cc1cc Mon Sep 17 00:00:00 2001 From: SegoCode <35817798+SegoCode@users.noreply.github.com> Date: Fri, 7 Aug 2026 11:33:26 +0200 Subject: [PATCH 67/72] Update README.md --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index 863c29f..2acd73c 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,6 @@ [![Repository size](https://img.shields.io/github/repo-size/SegoCode/webdl?style=flat-square&label=repo%20size)](https://github.com/SegoCode/webdl) [![Commit activity per year](https://img.shields.io/github/commit-activity/y/SegoCode/webdl?style=flat-square&label=commits)](https://github.com/SegoCode/webdl/graphs/commit-activity) [![Commits since tagged version](https://img.shields.io/github/commits-since/SegoCode/webdl/latest?style=flat-square&label=commits%20since%20tag)](https://github.com/SegoCode/webdl/releases) -[![GitHub downloads](https://img.shields.io/github/downloads/SegoCode/webdl/total?style=flat-square&label=downloads)](https://github.com/SegoCode/webdl/releases) [![Licencia: PolyForm Noncommercial + GNU AGPL-3.0](https://img.shields.io/badge/License-PolyForm%20Noncommercial%20%2B%20GNU%20AGPL--3.0-blue?style=flat-square)](https://github.com/SegoCode/webdl/blob/main/LICENSE) [![Bitcoin BTC](https://img.shields.io/badge/buy_me_a_coffee-BTC-F7931A?style=flat-square&logo=bitcoin&logoColor=white)](https://github.com/SegoCode/SegoCode/discussions/2) From 9e958109adc65158a202b4a2da9563edd9e4b7b6 Mon Sep 17 00:00:00 2001 From: SegoCode <35817798+SegoCode@users.noreply.github.com> Date: Fri, 7 Aug 2026 11:34:27 +0200 Subject: [PATCH 68/72] Update README.md --- README.md | 25 ------------------------- 1 file changed, 25 deletions(-) diff --git a/README.md b/README.md index 2acd73c..dfd34c6 100644 --- a/README.md +++ b/README.md @@ -32,12 +32,6 @@ Telegram bot in Java for downloading social media videos using [yt-dlp](https:// Requires Java 21, Maven, and [yt-dlp](https://github.com/yt-dlp/yt-dlp) available on `PATH`. Set `BOT_TOKEN` to your Telegram bot token. -> [!TIP] -> Prefer Docker if you want yt-dlp and the runtime bundled without a local Maven setup. - -> [!IMPORTANT] -> `BOT_TOKEN` must be set or the bot will fail to start. - ### From source ```shell @@ -60,25 +54,6 @@ docker run -d \ webdl-image ``` -### Project structure - -``` -code/src/main/java/org/segocode/webdl/ -├── Main.java # Entry point -├── bot/ -│ ├── Webdlbot.java # Telegram long-polling bot -│ ├── constants/Messages.java # User-facing message strings -│ ├── service/{MessageService,VideoService}.java -│ └── util/MessageUtil.java -└── system/ - ├── command/CommandExecutor.java # yt-dlp subprocess with retry - └── util/FileUtil.java -``` - -## Download - -[Latest release](https://github.com/SegoCode/webdl/releases/latest) - ---

From de8c330e415ca1b877a64e493cfc698f48471a44 Mon Sep 17 00:00:00 2001 From: SegoCode <35817798+SegoCode@users.noreply.github.com> Date: Fri, 7 Aug 2026 11:34:46 +0200 Subject: [PATCH 69/72] Delete readme_example.md --- readme_example.md | 69 ----------------------------------------------- 1 file changed, 69 deletions(-) delete mode 100644 readme_example.md diff --git a/readme_example.md b/readme_example.md deleted file mode 100644 index df84490..0000000 --- a/readme_example.md +++ /dev/null @@ -1,69 +0,0 @@ -# webdl - - - -

- About • - Features • - Quick Start & Information -

- - -## About -Telegram bot in Java for downloading social media videos using yt-dlp - -## Features - -- Non-blocking message queue processing with virtual threads - -- Dynamic interaction with Telegram messages (send, delete, edit) - -- Automatic retry on download failures - -## Quick Start & Information - -Webdl accepts a video URL, downloads it using [yt-dlp](https://github.com/yt-dlp/yt-dlp), and sends it back to the user as a video message. - -### From source - -``` -git clone https://github.com/SegoCode/webdl -cd webdl/code -mvn clean package -DskipTests -java -jar target/webdl.jar -``` - -### Docker - -``` -cd webdl/code -mvn clean package -DskipTests -docker build -t webdl-image . -docker run -d \ - --name webdl \ - --restart unless-stopped \ - -e BOT_TOKEN=your-bot-token \ - webdl-image -``` - -### Project structure - -``` -code/src/main/java/org/segocode/webdl/ -├── Main.java # Entry point -├── bot/ -│ ├── Webdlbot.java # Telegram long-polling bot -│ ├── constants/Messages.java # User-facing message strings -│ ├── service/{MessageService,VideoService}.java -│ └── util/MessageUtil.java -└── system/ - ├── command/CommandExecutor.java # yt-dlp subprocess with retry - └── util/FileUtil.java -``` - - - ---- -

- -

From 4fabed017c936c731812c9578c7ccbba3aa37ba8 Mon Sep 17 00:00:00 2001 From: SegoCode <35817798+SegoCode@users.noreply.github.com> Date: Fri, 7 Aug 2026 11:37:16 +0200 Subject: [PATCH 70/72] Update java-verify.yml --- .github/workflows/java-verify.yml | 38 ++++--------------------------- 1 file changed, 5 insertions(+), 33 deletions(-) diff --git a/.github/workflows/java-verify.yml b/.github/workflows/java-verify.yml index bd20429..ce90609 100644 --- a/.github/workflows/java-verify.yml +++ b/.github/workflows/java-verify.yml @@ -2,19 +2,6 @@ name: Java verify on: pull_request: - workflow_dispatch: - workflow_call: - inputs: - working-directory: - description: Directory containing the Maven project - required: false - type: string - default: '' - java-version: - description: Java version used to build and test the project - required: false - type: string - default: '21' permissions: contents: read @@ -27,35 +14,20 @@ jobs: verify: name: Compile and test runs-on: ubuntu-latest + defaults: + run: + working-directory: code steps: - name: Check out repository uses: actions/checkout@v5 - - name: Find Maven project - id: project - env: - CONFIGURED_DIRECTORY: ${{ inputs.working-directory }} - run: | - if [ -n "$CONFIGURED_DIRECTORY" ]; then - directory="$CONFIGURED_DIRECTORY" - elif [ -f pom.xml ]; then - directory="." - elif [ -f code/pom.xml ]; then - directory="code" - else - echo "No pom.xml found" >&2 - exit 1 - fi - echo "directory=$directory" >> "$GITHUB_OUTPUT" - - name: Set up Java uses: actions/setup-java@v5 with: distribution: temurin - java-version: ${{ inputs.java-version || '21' }} + java-version: '21' cache: maven - cache-dependency-path: ${{ steps.project.outputs.directory }}/pom.xml + cache-dependency-path: code/pom.xml - name: Verify Maven project - working-directory: ${{ steps.project.outputs.directory }} run: mvn --batch-mode --no-transfer-progress verify From bd5af992a8dedf4d12431159a1adc369740816db Mon Sep 17 00:00:00 2001 From: SegoCode <35817798+SegoCode@users.noreply.github.com> Date: Fri, 7 Aug 2026 11:41:54 +0200 Subject: [PATCH 71/72] Add files via upload --- media/logo.png | Bin 0 -> 56032 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 media/logo.png diff --git a/media/logo.png b/media/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..6c10d8091100341eb1d235ec8337e8457be3983f GIT binary patch literal 56032 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 Date: Fri, 7 Aug 2026 11:48:56 +0200 Subject: [PATCH 72/72] Fix formatting and update links in README.md --- README.md | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index dfd34c6..8c97ee1 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,17 @@ -# webdl +

AboutFeatures • - Quick Start & Information • - Download + Quick Start & Information

## About [![Top language](https://img.shields.io/github/languages/top/SegoCode/webdl?style=flat-square)](https://github.com/SegoCode/webdl) [![Repository size](https://img.shields.io/github/repo-size/SegoCode/webdl?style=flat-square&label=repo%20size)](https://github.com/SegoCode/webdl) [![Commit activity per year](https://img.shields.io/github/commit-activity/y/SegoCode/webdl?style=flat-square&label=commits)](https://github.com/SegoCode/webdl/graphs/commit-activity) -[![Commits since tagged version](https://img.shields.io/github/commits-since/SegoCode/webdl/latest?style=flat-square&label=commits%20since%20tag)](https://github.com/SegoCode/webdl/releases) [![Licencia: PolyForm Noncommercial + GNU AGPL-3.0](https://img.shields.io/badge/License-PolyForm%20Noncommercial%20%2B%20GNU%20AGPL--3.0-blue?style=flat-square)](https://github.com/SegoCode/webdl/blob/main/LICENSE) [![Bitcoin BTC](https://img.shields.io/badge/buy_me_a_coffee-BTC-F7931A?style=flat-square&logo=bitcoin&logoColor=white)](https://github.com/SegoCode/SegoCode/discussions/2)

AboutFeatures • - Quick Start & Information + Quick Start & Information • + Download