diff --git a/src/main/java/com/annimon/ffmpegbot/MainBotHandler.java b/src/main/java/com/annimon/ffmpegbot/MainBotHandler.java index fa427a3..baa9283 100644 --- a/src/main/java/com/annimon/ffmpegbot/MainBotHandler.java +++ b/src/main/java/com/annimon/ffmpegbot/MainBotHandler.java @@ -40,7 +40,7 @@ public class MainBotHandler extends BotHandler { commands.registerBundle(mediaProcessingBundle); commands.registerBundle(new InputParametersBundle(sessions)); commands.registerBundle(new YtDlpCommandBundle(sessions)); - commands.registerBundle(new AdminCommandBundle()); + commands.registerBundle(new AdminCommandBundle(sessions)); commands.register(new HelpCommand()); } diff --git a/src/main/java/com/annimon/ffmpegbot/commands/HelpCommand.java b/src/main/java/com/annimon/ffmpegbot/commands/HelpCommand.java index 156c2ef..9b53410 100644 --- a/src/main/java/com/annimon/ffmpegbot/commands/HelpCommand.java +++ b/src/main/java/com/annimon/ffmpegbot/commands/HelpCommand.java @@ -40,7 +40,7 @@ public class HelpCommand implements TextCommand { yt-dlp /dl link [format] — download a media using yt-dlp link — a link to download (it must be supported by yt-dlp) - format — (optional) a download format. Can be "audio", "240", "360", "480", "720" or "1080" + format — (optional) a download format. Can be "best", "audio", "240", "360", "480", "720" or "1080" """.stripIndent()).enableHtml().callAsync(ctx.sender); } } diff --git a/src/main/java/com/annimon/ffmpegbot/commands/admin/AdminCommandBundle.java b/src/main/java/com/annimon/ffmpegbot/commands/admin/AdminCommandBundle.java index a7a38c6..0e336ac 100644 --- a/src/main/java/com/annimon/ffmpegbot/commands/admin/AdminCommandBundle.java +++ b/src/main/java/com/annimon/ffmpegbot/commands/admin/AdminCommandBundle.java @@ -1,5 +1,6 @@ package com.annimon.ffmpegbot.commands.admin; +import com.annimon.ffmpegbot.session.Sessions; import com.annimon.tgbotsmodule.commands.CommandBundle; import com.annimon.tgbotsmodule.commands.CommandRegistry; import com.annimon.tgbotsmodule.commands.authority.For; @@ -7,9 +8,17 @@ import org.jetbrains.annotations.NotNull; public class AdminCommandBundle implements CommandBundle { + private final Sessions sessions; + + public AdminCommandBundle(Sessions sessions) { + this.sessions = sessions; + } + @Override public void register(@NotNull CommandRegistry commands) { commands.register(new RunCommand()); commands.register(new ClearCommand()); + commands.register(new SessionsCommand(sessions)); + commands.register(new SessionsClearCommand(sessions)); } } diff --git a/src/main/java/com/annimon/ffmpegbot/commands/admin/SessionsClearCommand.java b/src/main/java/com/annimon/ffmpegbot/commands/admin/SessionsClearCommand.java new file mode 100644 index 0000000..3bdb6dc --- /dev/null +++ b/src/main/java/com/annimon/ffmpegbot/commands/admin/SessionsClearCommand.java @@ -0,0 +1,38 @@ +package com.annimon.ffmpegbot.commands.admin; + +import com.annimon.ffmpegbot.Permissions; +import com.annimon.ffmpegbot.session.Sessions; +import com.annimon.tgbotsmodule.commands.CallbackQueryCommand; +import com.annimon.tgbotsmodule.commands.authority.For; +import com.annimon.tgbotsmodule.commands.context.CallbackQueryContext; +import org.jetbrains.annotations.NotNull; +import java.util.EnumSet; + +public class SessionsClearCommand implements CallbackQueryCommand { + static final String CALLBACK_NAME = "sessions_clear"; + private final Sessions sessions; + + public SessionsClearCommand(Sessions sessions) { + this.sessions = sessions; + } + + @Override + public String command() { + return CALLBACK_NAME; + } + + @SuppressWarnings("unchecked") + @Override + public EnumSet authority() { + return Permissions.SUPERUSERS; + } + + @Override + public void accept(@NotNull CallbackQueryContext ctx) { + final int size = sessions.getSize(); + sessions.clear(); + ctx.editMessage("%d sessions cleared".formatted(size)) + .setReplyMarkup(null) + .callAsync(ctx.sender); + } +} diff --git a/src/main/java/com/annimon/ffmpegbot/commands/admin/SessionsCommand.java b/src/main/java/com/annimon/ffmpegbot/commands/admin/SessionsCommand.java new file mode 100644 index 0000000..265b540 --- /dev/null +++ b/src/main/java/com/annimon/ffmpegbot/commands/admin/SessionsCommand.java @@ -0,0 +1,84 @@ +package com.annimon.ffmpegbot.commands.admin; + +import com.annimon.ffmpegbot.Permissions; +import com.annimon.ffmpegbot.TextUtils; +import com.annimon.ffmpegbot.session.MediaSession; +import com.annimon.ffmpegbot.session.Session; +import com.annimon.ffmpegbot.session.Sessions; +import com.annimon.ffmpegbot.session.YtDlpSession; +import com.annimon.tgbotsmodule.commands.TextCommand; +import com.annimon.tgbotsmodule.commands.authority.For; +import com.annimon.tgbotsmodule.commands.context.MessageContext; +import org.jetbrains.annotations.NotNull; +import org.telegram.telegrambots.meta.api.objects.replykeyboard.buttons.InlineKeyboardButton; +import java.util.Comparator; +import java.util.EnumSet; +import java.util.Objects; +import java.util.function.Function; +import java.util.stream.Collectors; + +public class SessionsCommand implements TextCommand { + private final Sessions sessions; + + public SessionsCommand(Sessions sessions) { + this.sessions = sessions; + } + + @Override + public String command() { + return "/sessions"; + } + + @SuppressWarnings("unchecked") + @Override + public EnumSet authority() { + return Permissions.SUPERUSERS; + } + + @Override + public void accept(@NotNull MessageContext ctx) { + final int size = sessions.getSize(); + if (size == 0) { + ctx.replyToMessage("No sessions found").callAsync(ctx.sender); + return; + } + + + if (ctx.argument(1).equals("clear")) { + sessions.clear(); + ctx.replyToMessage("%d sessions cleared".formatted(size)).callAsync(ctx.sender); + } else { + final var sessionsInfo = sessions.sessions() + .sorted(Comparator.comparing(Session::getInstant).reversed()) + .map(this::formatSession) + .limit(30) + .collect(Collectors.joining("\n")); + ctx.replyToMessage(("%d active sessions:\n%s").formatted(size, sessionsInfo)) + .disableWebPagePreview() + .setSingleRowInlineKeyboard(InlineKeyboardButton.builder() + .text("Clear").callbackData(SessionsClearCommand.CALLBACK_NAME) + .build()) + .callAsync(ctx.sender); + } + } + + private String formatSession(Session session) { + long chatId = session.getChatId(); + if (session instanceof MediaSession ms) { + return String.join(" ", str(chatId), str(ms.getOriginalFilename()), + str(ms.getFileSize(), TextUtils::readableFileSize), + str(ms.getDuration(), TextUtils::readableDuration)); + } else if (session instanceof YtDlpSession ys) { + return String.join(" ", str(chatId), str(ys.getUrl())); + } + return str(chatId); + } + + private String str(Object obj) { + return obj == null ? "" : Objects.toString(obj); + } + + private String str(T obj, Function func) { + return obj == null ? "" : str(func.apply(obj)); + } +} diff --git a/src/main/java/com/annimon/ffmpegbot/session/MediaSession.java b/src/main/java/com/annimon/ffmpegbot/session/MediaSession.java index e8b9134..055fffa 100644 --- a/src/main/java/com/annimon/ffmpegbot/session/MediaSession.java +++ b/src/main/java/com/annimon/ffmpegbot/session/MediaSession.java @@ -54,14 +54,26 @@ public final class MediaSession extends Session { this.originalFilename = originalFilename; } + public String getOriginalFilename() { + return originalFilename; + } + public void setFileSize(Long fileSize) { this.fileSize = fileSize; } + public Long getFileSize() { + return fileSize; + } + public void setDuration(Integer duration) { this.duration = duration; } + public Integer getDuration() { + return duration; + } + public void setResolution(Integer width, Integer height) { if (width == null && height == null) { this.resolution = null; diff --git a/src/main/java/com/annimon/ffmpegbot/session/Session.java b/src/main/java/com/annimon/ffmpegbot/session/Session.java index 2468897..9ad952e 100644 --- a/src/main/java/com/annimon/ffmpegbot/session/Session.java +++ b/src/main/java/com/annimon/ffmpegbot/session/Session.java @@ -2,6 +2,7 @@ package com.annimon.ffmpegbot.session; import com.annimon.ffmpegbot.parameters.InputParameters; +import java.time.Instant; import java.util.StringJoiner; public abstract sealed class Session permits MediaSession, YtDlpSession { @@ -10,6 +11,8 @@ public abstract sealed class Session permits MediaSession, YtDlpSession { protected int messageId; // Parameters protected final InputParameters inputParams = new InputParameters(); + // Meta + private final Instant instant = Instant.now(); public long getChatId() { return chatId; @@ -31,5 +34,9 @@ public abstract sealed class Session permits MediaSession, YtDlpSession { return inputParams; } + public Instant getInstant() { + return instant; + } + abstract StringJoiner describe(); } diff --git a/src/main/java/com/annimon/ffmpegbot/session/Sessions.java b/src/main/java/com/annimon/ffmpegbot/session/Sessions.java index 4d172d1..214d87a 100644 --- a/src/main/java/com/annimon/ffmpegbot/session/Sessions.java +++ b/src/main/java/com/annimon/ffmpegbot/session/Sessions.java @@ -2,6 +2,7 @@ package com.annimon.ffmpegbot.session; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Stream; public class Sessions { private final Map sessions; @@ -10,6 +11,18 @@ public class Sessions { sessions = new ConcurrentHashMap<>(); } + public void clear() { + sessions.clear(); + } + + public int getSize() { + return sessions.size(); + } + + public Stream sessions() { + return sessions.values().stream(); + } + public Session get(long chatId, long messageId) { return sessions.get(mapKey(chatId, messageId)); }