From 6ed185a8a53c97c8c778588ee7e18aad13c860f2 Mon Sep 17 00:00:00 2001 From: Luca Matei Pintilie Date: Wed, 3 Dec 2025 18:29:40 +0100 Subject: [PATCH] add archive_expires_after option --- CHANGELOG.rst | 2 + CMakeLists.txt | 6 +++ doc/admin.rst | 22 ++++++++++ src/database/database.cpp | 9 ++++ src/database/database.hpp | 1 + src/main.cpp | 3 ++ src/scheduler/scheduler.cpp | 68 +++++++++++++++++++++++++++++ src/scheduler/scheduler.hpp | 10 +++++ src/utils/parse_duration.cpp | 36 ++++++++++++++++ src/utils/parse_duration.hpp | 8 ++++ tests/parse_duration.cpp | 84 ++++++++++++++++++++++++++++++++++++ 11 files changed, 249 insertions(+) create mode 100644 src/scheduler/scheduler.cpp create mode 100644 src/scheduler/scheduler.hpp create mode 100644 src/utils/parse_duration.cpp create mode 100644 src/utils/parse_duration.hpp create mode 100644 tests/parse_duration.cpp diff --git a/CHANGELOG.rst b/CHANGELOG.rst index c6101fc..5b31bac 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -22,6 +22,8 @@ For admins - Options default_persistence_allowed, persist_user_denylist, and persist_user_allowlist have been added to granualy control who can set the persist option +- Option archive_expires_after has been added to delete older messages from the + database For packagers ------------- diff --git a/CMakeLists.txt b/CMakeLists.txt index c2f4188..96854c0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -176,6 +176,10 @@ file(GLOB source_bridge src/bridge/*.[hc]pp) add_library(bridge OBJECT ${source_bridge}) +file(GLOB source_scheduler + src/scheduler/*.[hc]pp) +add_library(scheduler OBJECT ${source_scheduler}) + file(GLOB source_config src/config/*.[hc]pp) add_library(config OBJECT ${source_config}) @@ -217,6 +221,7 @@ add_executable(${PROJECT_NAME} src/main.cpp $ $ $ + $ $ $) @@ -230,6 +235,7 @@ add_executable(test_suite ${source_tests} $ $ $ + $ $ $) set_target_properties(test_suite PROPERTIES EXCLUDE_FROM_ALL TRUE) diff --git a/doc/admin.rst b/doc/admin.rst index effe6ac..2a59f56 100644 --- a/doc/admin.rst +++ b/doc/admin.rst @@ -266,6 +266,28 @@ default_persistence_allowed If a given XMPP user or domain is not in either denylist or allowlist this option decides if the user is allowed to persist or not. +archive_expires_after +~~~~~~~~~~~~~~~~~~~~~ + +Messages in the archive can expire after some time, by default never. This +can be changed by this setting + +.. code-block:: ini + + # one day + archive_expires_after=1d + # one week + archive_expires_after=1w + # two months + archive_expires_after=2m + # one year + archive_expires_after=1y + # 1h + archive_expires_after=1h + # keep messages forever, the default + archive_expires_after=0 + + TLS configuration ----------------- diff --git a/src/database/database.cpp b/src/database/database.cpp index 861abcb..9f2657a 100644 --- a/src/database/database.cpp +++ b/src/database/database.cpp @@ -2,6 +2,7 @@ #ifdef USE_DATABASE #include +#include #include #include #include @@ -317,6 +318,14 @@ std::vector Database::get_full_roster() return query.execute(*Database::db); } +void Database::delete_old_muclines(const long& seconds) +{ + auto query = DeleteQuery(Database::muc_log_lines.get_name()); + query << " WHERE " << Database::Date{} << "<" << seconds; + + query.execute(*Database::db); +} + void Database::close() { Database::db = nullptr; diff --git a/src/database/database.hpp b/src/database/database.hpp index de1df49..12b3ef5 100644 --- a/src/database/database.hpp +++ b/src/database/database.hpp @@ -153,6 +153,7 @@ class Database static void delete_roster_item(const std::string& local, const std::string& remote); static std::vector get_contact_list(const std::string& local); static std::vector get_full_roster(); + static void delete_old_muclines(const long& seconds); static void close(); static void open(const std::string& filename); diff --git a/src/main.cpp b/src/main.cpp index 20bf9fd..9365499 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -14,6 +14,7 @@ #include #include +#include // A flag set by the SIGINT signal handler. static std::atomic stop(false); @@ -107,6 +108,7 @@ static int main_loop(std::string hostname, std::string password) if (Config::get_int("identd_port", 113) != 0) identd = std::make_unique(*xmpp_component, p, static_cast(Config::get_int("identd_port", 113))); + Scheduler::start_scheduler(); auto timeout = TimedEventsManager::instance().get_timeout(); while (p->poll(timeout) != -1) { @@ -130,6 +132,7 @@ static int main_loop(std::string hostname, std::string password) #endif if (identd) identd->shutdown(); + Scheduler::stop_scheduler(); // Cancel the timer for a potential reconnection TimedEventsManager::instance().cancel("XMPP reconnection"); } diff --git a/src/scheduler/scheduler.cpp b/src/scheduler/scheduler.cpp new file mode 100644 index 0000000..3050396 --- /dev/null +++ b/src/scheduler/scheduler.cpp @@ -0,0 +1,68 @@ +#include "biboumi.h" +#include +#include +#include +#include + +#ifdef USE_DATABASE +#include +#endif + +static const std::unordered_map< + std::string, std::pair>> + events = { +#ifdef USE_DATABASE + {"cleanup_old_messages", + {60s * 10 /* 10min */, &Scheduler::cleanup_old_messages}}, +#endif +}; + +void Scheduler::start_scheduler() { + for (const auto &job : events) { + const std::string name = job.first; + const std::chrono::seconds interval = job.second.first; + const std::function job_function = job.second.second; + // Run it once at startup + job_function(); + // Then every interval + TimedEventsManager::instance().add_event( + TimedEvent(interval, job_function, name)); + } +} + +void Scheduler::stop_scheduler() { + for (const auto &job : events) { + const std::string name = job.first; + TimedEventsManager::instance().cancel(name); + } +} + +#ifdef USE_DATABASE +void Scheduler::cleanup_old_messages() { + // Get the config value + const std::string archive_expires_after = + Config::get("archive_expires_after", "0"); + const std::optional seconds = + utils::parse_duration(archive_expires_after); + // If it's actually set and relevant, start cleanup + if (seconds.has_value() && seconds.value() != 0) { + // Calculate age of messages to be deleted + log_debug("[cleanup_old_messages] Deleting messages older than ", + archive_expires_after, " (", seconds.value(), " seconds)"); + + // Convert the config value to a duration + const auto duration = std::chrono::duration(seconds.value()); + // Get timestamp now relative to unix epoch + const auto now = std::chrono::system_clock::now(); + // Older than this get deleted + const auto ago = now - duration; + // Convert to seconds since epoch + const auto since_epoch = std::chrono::duration_cast( + ago.time_since_epoch()); + Database::delete_old_muclines(since_epoch.count()); + } else + log_debug("[cleanup_old_messages] Not deleting anything. " + "archive_expires_after is ", + archive_expires_after); +} +#endif diff --git a/src/scheduler/scheduler.hpp b/src/scheduler/scheduler.hpp new file mode 100644 index 0000000..b35aeaa --- /dev/null +++ b/src/scheduler/scheduler.hpp @@ -0,0 +1,10 @@ +#pragma once + +class Scheduler { +public: + static void start_scheduler(); + static void stop_scheduler(); + + // Jobs + static void cleanup_old_messages(); +}; diff --git a/src/utils/parse_duration.cpp b/src/utils/parse_duration.cpp new file mode 100644 index 0000000..371c4c4 --- /dev/null +++ b/src/utils/parse_duration.cpp @@ -0,0 +1,36 @@ +#include +#include +#include +#include +#include +#include + +namespace utils { + +// Strongly inspired from prosody +// https://hg.prosody.im/trunk/file/0ff11f2e87cd/util/human/io.lua#l214 +static const long day = 86400; + +static const std::unordered_map multipliers = { + {"d", day}, {"w", day * 7}, {"mon", day * 31}, {"y", day * 365.2425}, + {"s", 1}, {"min", 60}, {"h", 3600}, {"ho", 3600}, +}; + +std::optional parse_duration(const std::string &duration) { + std::size_t ptr; + long number; + try { + number = std::stol(duration, &ptr, 10); + } catch (std::invalid_argument &) { + return {}; + } + const std::string m = duration.substr(ptr); + + if (!multipliers.contains(m)) + return {}; + + const long multiplier = multipliers.at(m); + + return multiplier * number; +} +} // namespace utils diff --git a/src/utils/parse_duration.hpp b/src/utils/parse_duration.hpp new file mode 100644 index 0000000..b0f9b21 --- /dev/null +++ b/src/utils/parse_duration.hpp @@ -0,0 +1,8 @@ +#pragma once + +#include +#include + +namespace utils { +std::optional parse_duration(const std::string &duration); +} // namespace utils diff --git a/tests/parse_duration.cpp b/tests/parse_duration.cpp new file mode 100644 index 0000000..45e9348 --- /dev/null +++ b/tests/parse_duration.cpp @@ -0,0 +1,84 @@ +#include "catch2/catch_test_macros.hpp" + +#include +#include + +TEST_CASE("parse_duration - 2d") +{ + const long expected = (60 * 60 * 24) * 2; + const std::optional output = utils::parse_duration("2d"); + + CHECK(expected == output); +} + +TEST_CASE("parse_duration - 5w") +{ + const long expected = ((60 * 60 * 24) * 7) * 5; + const std::optional output = utils::parse_duration("5w"); + + CHECK(expected == output); +} + +TEST_CASE("parse_duration - asdf") +{ + const std::optional expected; + const std::optional output = utils::parse_duration("asdf"); + + CHECK(expected == output); +} + +TEST_CASE("parse_duration - 1s") +{ + const long expected = 1; + const std::optional output = utils::parse_duration("1s"); + + CHECK(expected == output); +} + +TEST_CASE("parse_duration - 2mon") +{ + const long expected = ((60 * 60 * 24) * 31) * 2; + const std::optional output = utils::parse_duration("2mon"); + + CHECK(expected == output); +} + +TEST_CASE("parse_duration - 5y") +{ + const long expected = (long)((60 * 60 * 24) * 365.2425) * 5; + const std::optional output = utils::parse_duration("5y"); + + CHECK(expected == output); +} + +TEST_CASE("parse_duration - 5") +{ + const std::optional expected; + const std::optional output = utils::parse_duration("5"); + + CHECK(expected == output); +} + +TEST_CASE("parse_duration - ") +{ + const std::optional expected; + const std::optional output = utils::parse_duration(""); + + CHECK(expected == output); +} + +TEST_CASE("parse_duration - 5blorbs") +{ + const std::optional expected; + const std::optional output = utils::parse_duration("5blorbs"); + + CHECK(expected == output); +} + +TEST_CASE("parse_duration - 5y2d") +{ + const std::optional expected; + const std::optional output = utils::parse_duration("5y2d"); + + CHECK(expected == output); +}