diff --git a/pom.xml b/pom.xml index 1c11ddd..1ad4d8d 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ botdarr release botdarr - Discord/Slack/Telegram bot for interfacing with radarr, sonarr + Discord/Slack/Telegram/Matrix bot for interfacing with radarr, sonarr, lidarr 1.8 @@ -91,7 +91,7 @@ com.github.pengrad java-telegram-bot-api - 5.5.0 + 6.6.1 com.j2html diff --git a/src/main/java/com/botdarr/clients/ChatClient.java b/src/main/java/com/botdarr/clients/ChatClient.java index 07d46c8..d48d04b 100644 --- a/src/main/java/com/botdarr/clients/ChatClient.java +++ b/src/main/java/com/botdarr/clients/ChatClient.java @@ -4,4 +4,5 @@ public interface ChatClient { void sendToConfiguredChannels(List chatClientResponses); + void cleanup(); } diff --git a/src/main/java/com/botdarr/clients/ChatClientBootstrap.java b/src/main/java/com/botdarr/clients/ChatClientBootstrap.java index 49a478f..f091475 100644 --- a/src/main/java/com/botdarr/clients/ChatClientBootstrap.java +++ b/src/main/java/com/botdarr/clients/ChatClientBootstrap.java @@ -60,6 +60,7 @@ protected void initScheduling(ChatClient chatC //make sure to always cache before doing any notifications scheduler.initApiCaching(apis); scheduler.initApiNotifications(apis, chatClient, responseBuilder); + scheduler.initCleanup(chatClient); } protected void runAndProcessCommands(String prefix, diff --git a/src/main/java/com/botdarr/clients/discord/DiscordChatClient.java b/src/main/java/com/botdarr/clients/discord/DiscordChatClient.java index bca9b0c..6743988 100644 --- a/src/main/java/com/botdarr/clients/discord/DiscordChatClient.java +++ b/src/main/java/com/botdarr/clients/discord/DiscordChatClient.java @@ -27,6 +27,11 @@ public void sendToConfiguredChannels(List chatClientResponses) }, null); } + @Override + public void cleanup() { + // nothing to cleanup + } + public void sendMessage(DiscordResponse chatClientResponse, String channelName) { sendMessages(channel -> channel.sendMessage(chatClientResponse.getMessage()).queue(), channelName); } diff --git a/src/main/java/com/botdarr/clients/matrix/MatrixChatClient.java b/src/main/java/com/botdarr/clients/matrix/MatrixChatClient.java index 86fc417..d451796 100644 --- a/src/main/java/com/botdarr/clients/matrix/MatrixChatClient.java +++ b/src/main/java/com/botdarr/clients/matrix/MatrixChatClient.java @@ -57,6 +57,11 @@ public void sendToConfiguredChannels(List chatClientResponses) { } } + @Override + public void cleanup() { + // nothing to cleanup + } + public void sendMessage(MatrixResponse response, String roomId) { sendMatrixResponse(response, roomId); } diff --git a/src/main/java/com/botdarr/clients/slack/SlackChatClient.java b/src/main/java/com/botdarr/clients/slack/SlackChatClient.java index 6bf8f63..0cda6f5 100644 --- a/src/main/java/com/botdarr/clients/slack/SlackChatClient.java +++ b/src/main/java/com/botdarr/clients/slack/SlackChatClient.java @@ -152,6 +152,11 @@ public void sendToConfiguredChannels(List chatClientResponses) { sendMessage(chatClientResponses, null); } + @Override + public void cleanup() { + // nothing to cleanup + } + private interface MessageSender { void send(String channel); } diff --git a/src/main/java/com/botdarr/clients/telegram/TelegramBootstrap.java b/src/main/java/com/botdarr/clients/telegram/TelegramBootstrap.java index f761892..c6d6e01 100644 --- a/src/main/java/com/botdarr/clients/telegram/TelegramBootstrap.java +++ b/src/main/java/com/botdarr/clients/telegram/TelegramBootstrap.java @@ -40,8 +40,10 @@ public void init() throws Exception { chat = message.chat(); } else if (update.callbackQuery() != null && !Strings.isEmpty(update.callbackQuery().data())) { // interactive commands with callback data + TelegramCallbackManager telegramCallbackManager = new TelegramCallbackManager(); + String callback = telegramCallbackManager.getCallback(Integer.parseInt(update.callbackQuery().data())); ObjectMapper mapper = new ObjectMapper(); - TelegramCallbackData callbackData = mapper.readValue(update.callbackQuery().data(), TelegramCallbackData.class); + TelegramCallbackData callbackData = mapper.readValue(callback, TelegramCallbackData.class); text = callbackData.getCommand(); chat = update.callbackQuery().message().chat(); } else { diff --git a/src/main/java/com/botdarr/clients/telegram/TelegramCallbackManager.java b/src/main/java/com/botdarr/clients/telegram/TelegramCallbackManager.java new file mode 100644 index 0000000..1f632bd --- /dev/null +++ b/src/main/java/com/botdarr/clients/telegram/TelegramCallbackManager.java @@ -0,0 +1,106 @@ +package com.botdarr.clients.telegram; + +import com.botdarr.database.DatabaseHelper; +import com.botdarr.utilities.DateWrapper; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.apache.logging.log4j.util.Strings; + +import java.sql.*; +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.List; + +public class TelegramCallbackManager { + private final DateWrapper dateWrapper; + + public TelegramCallbackManager(DateWrapper dateWrapper) { + this.dateWrapper = dateWrapper; + } + + public TelegramCallbackManager() { + this.dateWrapper = new DateWrapper(); + } + + private void deleteCallback(Connection conn, int id) throws SQLException { + PreparedStatement statement = conn.prepareStatement("delete from telegram_callbacks where id = ?"); + statement.setInt(1, id); + statement.executeUpdate(); + } + + public int saveCallback(String callback) { + String url = databaseHelper.getJdbcUrl(); + try (Connection conn = DriverManager.getConnection(url)) { + PreparedStatement statement = conn.prepareStatement("insert into telegram_callbacks (callback, createdDt) values (?, ?)"); + statement.setString(1, callback); + statement.setDate(2, Date.valueOf(this.dateWrapper.getNow())); + statement.executeUpdate(); + try (ResultSet rs = statement.getGeneratedKeys()) { + if (rs.next()) { + return rs.getInt(1); + } + } + } catch (Exception e) { + LOGGER.error("Error trying to save telegram callback", e); + throw new RuntimeException(e); + } + throw new RuntimeException("Could not save telegram callback"); + } + + public String getCallback(int id) { + String url = databaseHelper.getJdbcUrl(); + try (Connection conn = DriverManager.getConnection(url)) { + PreparedStatement statement = conn.prepareStatement("select callback from telegram_callbacks where id = ?"); + statement.setInt(1, id); + String callback = null; + try (ResultSet rs = statement.executeQuery()) { + if (rs.next()) { + callback = rs.getString("callback"); + } + } + if (!Strings.isEmpty(callback)) { + // delete the callback entry + deleteCallback(conn, id); + return callback; + } + throw new TelegramCallbackMissing(); + } catch (TelegramCallbackMissing e) { + throw e; + } catch (Exception e) { + LOGGER.error("Error trying to get telegram callback", e); + throw new RuntimeException(e); + } + } + + public void deleteOldCallbacks() { + LOGGER.info("Checking for telegram callbacks to delete"); + String url = databaseHelper.getJdbcUrl(); + try (Connection conn = DriverManager.getConnection(url)) { + // if callbacks haven't been used in 10 days, delete them + PreparedStatement statement = conn.prepareStatement("SELECT id FROM telegram_callbacks WHERE createdDt < ?"); + LocalDate tenDaysAgo = this.dateWrapper.getNow().minusDays(10); + statement.setDate(1, Date.valueOf(tenDaysAgo)); + List rowsToDelete = new ArrayList<>(); + try (ResultSet rs = statement.executeQuery()) { + if (rs.next()) { + rowsToDelete.add(rs.getInt("id")); + } + } + LOGGER.info("Found telegram callbacks to delete, count=" + rowsToDelete.size()); + for (int rowIdToDelete : rowsToDelete) { + LOGGER.info("Deleting telegram callback, id=" + rowIdToDelete); + deleteCallback(conn, rowIdToDelete); + } + } catch (Exception e) { + LOGGER.error("Error trying to delete old telegram callback", e); + throw new RuntimeException(e); + } + } + + public static class TelegramCallbackMissing extends RuntimeException { + + } + + private final DatabaseHelper databaseHelper = new DatabaseHelper(); + Logger LOGGER = LogManager.getLogger(); +} diff --git a/src/main/java/com/botdarr/clients/telegram/TelegramChatClient.java b/src/main/java/com/botdarr/clients/telegram/TelegramChatClient.java index a9738f6..f24bdbf 100644 --- a/src/main/java/com/botdarr/clients/telegram/TelegramChatClient.java +++ b/src/main/java/com/botdarr/clients/telegram/TelegramChatClient.java @@ -96,6 +96,12 @@ public void sendToConfiguredChannels(List chatClientResponses) sendMessage(chatClientResponses, null); } + @Override + public void cleanup() { + // remove any 10 day old callbacks + new TelegramCallbackManager().deleteOldCallbacks(); + } + private interface MessageSender { void send(Chat chatChannel); } diff --git a/src/main/java/com/botdarr/clients/telegram/TelegramResponseBuilder.java b/src/main/java/com/botdarr/clients/telegram/TelegramResponseBuilder.java index e11ff5f..4a032ac 100644 --- a/src/main/java/com/botdarr/clients/telegram/TelegramResponseBuilder.java +++ b/src/main/java/com/botdarr/clients/telegram/TelegramResponseBuilder.java @@ -300,7 +300,7 @@ public TelegramResponse build(NewMusicArtistResponse newMusicArtistResponse) { List domContents = new ArrayList<>(); String artistDetail = " (" + lookupArtist.getDisambiguation() + ")"; domContents.add(b(lookupArtist.getArtistName() + (Strings.isEmpty(lookupArtist.getDisambiguation()) ? "" : artistDetail))); - domContents.add(a(lookupArtist.getRemoteImage())); + domContents.add(Strings.isEmpty(lookupArtist.getRemoteImage()) ? code("No image found!") : a(lookupArtist.getRemoteImage())); return getAddResponse(domContents, LidarrCommands.getAddArtistCommandStr(lookupArtist.getArtistName(), lookupArtist.getForeignArtistId())); } @@ -338,11 +338,11 @@ private TelegramResponse getAddResponse(List domContents, String com try { ObjectMapper objectMapper = new ObjectMapper(); String json = objectMapper.writeValueAsString(new TelegramCallbackData(command)); - return new TelegramResponse(domContents, new InlineKeyboardMarkup( - new InlineKeyboardButton[]{ - new InlineKeyboardButton("Add").callbackData(json), - } - )); + TelegramCallbackManager telegramCallbackManager = new TelegramCallbackManager(); + int id = telegramCallbackManager.saveCallback(json); + return new TelegramResponse(domContents, + // callback data can never be larger than 64 bytes + new InlineKeyboardMarkup(new InlineKeyboardButton("Add").callbackData(String.valueOf(id)))); } catch (JsonProcessingException e) { throw new RuntimeException(e); } diff --git a/src/main/java/com/botdarr/scheduling/Scheduler.java b/src/main/java/com/botdarr/scheduling/Scheduler.java index e51e513..7799f39 100644 --- a/src/main/java/com/botdarr/scheduling/Scheduler.java +++ b/src/main/java/com/botdarr/scheduling/Scheduler.java @@ -26,6 +26,11 @@ public static Scheduler getScheduler() { return instance; } + public void initCleanup(ChatClient chatClient) { + if (cleanupFuture == null) { + cleanupFuture = Executors.newScheduledThreadPool(1).scheduleWithFixedDelay(chatClient::cleanup, 0, 5 , TimeUnit.MINUTES); + } + } public void initApiNotifications(List apis, ChatClient chatClient, ChatClientResponseBuilder responseBuilder) { if (notificationFuture == null) { @@ -74,6 +79,7 @@ public void executeCommand(Callable callable) { private ScheduledFuture notificationFuture; private ScheduledFuture cacheFuture; + private ScheduledFuture cleanupFuture; private ExecutorService commandThreadPool; private static volatile Scheduler instance; private static final Logger LOGGER = LogManager.getLogger(); diff --git a/src/main/java/com/botdarr/utilities/DateWrapper.java b/src/main/java/com/botdarr/utilities/DateWrapper.java new file mode 100644 index 0000000..765e4e0 --- /dev/null +++ b/src/main/java/com/botdarr/utilities/DateWrapper.java @@ -0,0 +1,9 @@ +package com.botdarr.utilities; + +import java.time.LocalDate; + +public class DateWrapper { + public LocalDate getNow() { + return LocalDate.now(); + } +} diff --git a/src/main/resources/upgrade/V3__addTelegramCallbackTable.sql b/src/main/resources/upgrade/V3__addTelegramCallbackTable.sql new file mode 100644 index 0000000..5fe7d6b --- /dev/null +++ b/src/main/resources/upgrade/V3__addTelegramCallbackTable.sql @@ -0,0 +1 @@ +create table telegram_callbacks (id integer primary key not null, callback text not null, createdDt date not null); \ No newline at end of file diff --git a/src/main/resources/version.txt b/src/main/resources/version.txt index 3b285e7..7ba0f8a 100644 --- a/src/main/resources/version.txt +++ b/src/main/resources/version.txt @@ -1 +1 @@ -5.6.6 \ No newline at end of file +5.6.7 \ No newline at end of file diff --git a/src/test/java/com/botdarr/api/ApiRequestsTests.java b/src/test/java/com/botdarr/api/ApiRequestsTests.java index 2708f46..71c96d6 100644 --- a/src/test/java/com/botdarr/api/ApiRequestsTests.java +++ b/src/test/java/com/botdarr/api/ApiRequestsTests.java @@ -3,6 +3,7 @@ import com.botdarr.Config; import com.botdarr.database.DatabaseBootstrap; import com.botdarr.database.DatabaseHelper; +import com.botdarr.database.MockedDatabase; import mockit.*; import org.junit.Assert; import org.junit.Before; @@ -243,19 +244,6 @@ public void validate() { private final Throwable expectedThrowable; } - private static class MockedDatabase extends MockUp { - private MockedDatabase(File temporaryDatabase) { - this.temporaryDatabase = temporaryDatabase; - } - - @Mock - public File getDatabaseFile() { - return temporaryDatabase; - } - - private final File temporaryDatabase; - } - @Rule public TemporaryFolder temporaryFolder = new TemporaryFolder(); } diff --git a/src/test/java/com/botdarr/clients/telegram/TelegramCallbackManagerTests.java b/src/test/java/com/botdarr/clients/telegram/TelegramCallbackManagerTests.java new file mode 100644 index 0000000..cbef79a --- /dev/null +++ b/src/test/java/com/botdarr/clients/telegram/TelegramCallbackManagerTests.java @@ -0,0 +1,64 @@ +package com.botdarr.clients.telegram; + +import com.botdarr.database.DatabaseBootstrap; +import com.botdarr.database.MockedDatabase; +import com.botdarr.utilities.DateWrapper; +import org.junit.*; +import org.junit.rules.TemporaryFolder; + +import java.io.IOException; +import java.time.LocalDate; + +public class TelegramCallbackManagerTests { + @Before + public void beforeTests() throws IOException { + //create temporary database + new MockedDatabase(temporaryFolder.newFile()); + DatabaseBootstrap.init(); + } + + @Test + public void saveCallback_savesAndGetsCallbackDataSuccessfully() { + TelegramCallbackManager telegramCallbackManager = new TelegramCallbackManager(); + String callback = "!movie find new test"; + int result = telegramCallbackManager.saveCallback(callback); + Assert.assertTrue(result > 0); + String actualCallback = telegramCallbackManager.getCallback(result); + Assert.assertEquals(callback, actualCallback); + } + + @Test(expected = TelegramCallbackManager.TelegramCallbackMissing.class) + public void deleteCallbacks_removes10DayOldCallbacks() { + LocalDate fakeNow = LocalDate.now(); + fakeNow = fakeNow.minusDays(11); + + // insert callbacks that are 11 days old + TelegramCallbackManager telegramCallbackManager = new TelegramCallbackManager(new FakeDateWrapper(fakeNow)); + String callback = "!movie find new test"; + int result = telegramCallbackManager.saveCallback(callback); + Assert.assertTrue(result > 0); + + // trigger the callback delete logic + telegramCallbackManager = new TelegramCallbackManager(); + telegramCallbackManager.deleteOldCallbacks(); + + // this should throw an exception now + telegramCallbackManager.getCallback(result); + } + + private static class FakeDateWrapper extends DateWrapper { + private LocalDate now; + + public FakeDateWrapper(LocalDate now) { + this.now = now; + } + + @Override + public LocalDate getNow() { + return this.now; + } + } + + @Rule + public TemporaryFolder temporaryFolder = new TemporaryFolder(); +} diff --git a/src/test/java/com/botdarr/database/MockedDatabase.java b/src/test/java/com/botdarr/database/MockedDatabase.java new file mode 100644 index 0000000..1ec43be --- /dev/null +++ b/src/test/java/com/botdarr/database/MockedDatabase.java @@ -0,0 +1,19 @@ +package com.botdarr.database; + +import mockit.Mock; +import mockit.MockUp; + +import java.io.File; + +public class MockedDatabase extends MockUp { + public MockedDatabase(File temporaryDatabase) { + this.temporaryDatabase = temporaryDatabase; + } + + @Mock + public File getDatabaseFile() { + return temporaryDatabase; + } + + private final File temporaryDatabase; +} \ No newline at end of file