add archive_expires_after option

This commit is contained in:
Luca Matei Pintilie
2026-01-10 18:59:34 +01:00
committed by mathieui
parent 4de88b5160
commit 6ed185a8a5
11 changed files with 249 additions and 0 deletions
+2
View File
@@ -22,6 +22,8 @@ For admins
- Options default_persistence_allowed, persist_user_denylist, and - Options default_persistence_allowed, persist_user_denylist, and
persist_user_allowlist have been added to granualy control who can set the persist_user_allowlist have been added to granualy control who can set the
persist option persist option
- Option archive_expires_after has been added to delete older messages from the
database
For packagers For packagers
------------- -------------
+6
View File
@@ -176,6 +176,10 @@ file(GLOB source_bridge
src/bridge/*.[hc]pp) src/bridge/*.[hc]pp)
add_library(bridge OBJECT ${source_bridge}) add_library(bridge OBJECT ${source_bridge})
file(GLOB source_scheduler
src/scheduler/*.[hc]pp)
add_library(scheduler OBJECT ${source_scheduler})
file(GLOB source_config file(GLOB source_config
src/config/*.[hc]pp) src/config/*.[hc]pp)
add_library(config OBJECT ${source_config}) add_library(config OBJECT ${source_config})
@@ -217,6 +221,7 @@ add_executable(${PROJECT_NAME} src/main.cpp
$<TARGET_OBJECTS:network> $<TARGET_OBJECTS:network>
$<TARGET_OBJECTS:xmpp> $<TARGET_OBJECTS:xmpp>
$<TARGET_OBJECTS:bridge> $<TARGET_OBJECTS:bridge>
$<TARGET_OBJECTS:scheduler>
$<TARGET_OBJECTS:irc> $<TARGET_OBJECTS:irc>
$<TARGET_OBJECTS:identd>) $<TARGET_OBJECTS:identd>)
@@ -230,6 +235,7 @@ add_executable(test_suite ${source_tests}
$<TARGET_OBJECTS:network> $<TARGET_OBJECTS:network>
$<TARGET_OBJECTS:xmpp> $<TARGET_OBJECTS:xmpp>
$<TARGET_OBJECTS:bridge> $<TARGET_OBJECTS:bridge>
$<TARGET_OBJECTS:scheduler>
$<TARGET_OBJECTS:irc> $<TARGET_OBJECTS:irc>
$<TARGET_OBJECTS:identd>) $<TARGET_OBJECTS:identd>)
set_target_properties(test_suite PROPERTIES EXCLUDE_FROM_ALL TRUE) set_target_properties(test_suite PROPERTIES EXCLUDE_FROM_ALL TRUE)
+22
View File
@@ -266,6 +266,28 @@ default_persistence_allowed
If a given XMPP user or domain is not in either denylist or allowlist this 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. 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 TLS configuration
----------------- -----------------
+9
View File
@@ -2,6 +2,7 @@
#ifdef USE_DATABASE #ifdef USE_DATABASE
#include <database/select_query.hpp> #include <database/select_query.hpp>
#include <database/delete_query.hpp>
#include <database/save.hpp> #include <database/save.hpp>
#include <database/database.hpp> #include <database/database.hpp>
#include <utils/get_first_non_empty.hpp> #include <utils/get_first_non_empty.hpp>
@@ -317,6 +318,14 @@ std::vector<Database::RosterItem> Database::get_full_roster()
return query.execute(*Database::db); 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() void Database::close()
{ {
Database::db = nullptr; Database::db = nullptr;
+1
View File
@@ -153,6 +153,7 @@ class Database
static void delete_roster_item(const std::string& local, const std::string& remote); static void delete_roster_item(const std::string& local, const std::string& remote);
static std::vector<Database::RosterItem> get_contact_list(const std::string& local); static std::vector<Database::RosterItem> get_contact_list(const std::string& local);
static std::vector<Database::RosterItem> get_full_roster(); static std::vector<Database::RosterItem> get_full_roster();
static void delete_old_muclines(const long& seconds);
static void close(); static void close();
static void open(const std::string& filename); static void open(const std::string& filename);
+3
View File
@@ -14,6 +14,7 @@
#include <csignal> #include <csignal>
#include <identd/identd_server.hpp> #include <identd/identd_server.hpp>
#include <scheduler/scheduler.hpp>
// A flag set by the SIGINT signal handler. // A flag set by the SIGINT signal handler.
static std::atomic<bool> stop(false); static std::atomic<bool> stop(false);
@@ -107,6 +108,7 @@ static int main_loop(std::string hostname, std::string password)
if (Config::get_int("identd_port", 113) != 0) if (Config::get_int("identd_port", 113) != 0)
identd = std::make_unique<IdentdServer>(*xmpp_component, p, static_cast<uint16_t>(Config::get_int("identd_port", 113))); identd = std::make_unique<IdentdServer>(*xmpp_component, p, static_cast<uint16_t>(Config::get_int("identd_port", 113)));
Scheduler::start_scheduler();
auto timeout = TimedEventsManager::instance().get_timeout(); auto timeout = TimedEventsManager::instance().get_timeout();
while (p->poll(timeout) != -1) while (p->poll(timeout) != -1)
{ {
@@ -130,6 +132,7 @@ static int main_loop(std::string hostname, std::string password)
#endif #endif
if (identd) if (identd)
identd->shutdown(); identd->shutdown();
Scheduler::stop_scheduler();
// Cancel the timer for a potential reconnection // Cancel the timer for a potential reconnection
TimedEventsManager::instance().cancel("XMPP reconnection"); TimedEventsManager::instance().cancel("XMPP reconnection");
} }
+68
View File
@@ -0,0 +1,68 @@
#include "biboumi.h"
#include <config/config.hpp>
#include <scheduler/scheduler.hpp>
#include <utils/parse_duration.hpp>
#include <utils/timed_events.hpp>
#ifdef USE_DATABASE
#include <database/database.hpp>
#endif
static const std::unordered_map<
std::string, std::pair<std::chrono::seconds, std::function<void()>>>
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<void()> 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<long> 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<long>(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<std::chrono::seconds>(
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
+10
View File
@@ -0,0 +1,10 @@
#pragma once
class Scheduler {
public:
static void start_scheduler();
static void stop_scheduler();
// Jobs
static void cleanup_old_messages();
};
+36
View File
@@ -0,0 +1,36 @@
#include <cstdio>
#include <optional>
#include <stdexcept>
#include <string>
#include <unordered_map>
#include <utils/parse_duration.hpp>
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<std::string, long> multipliers = {
{"d", day}, {"w", day * 7}, {"mon", day * 31}, {"y", day * 365.2425},
{"s", 1}, {"min", 60}, {"h", 3600}, {"ho", 3600},
};
std::optional<long> 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
+8
View File
@@ -0,0 +1,8 @@
#pragma once
#include <optional>
#include <string>
namespace utils {
std::optional<long> parse_duration(const std::string &duration);
} // namespace utils
+84
View File
@@ -0,0 +1,84 @@
#include "catch2/catch_test_macros.hpp"
#include <optional>
#include <utils/parse_duration.hpp>
TEST_CASE("parse_duration - 2d")
{
const long expected = (60 * 60 * 24) * 2;
const std::optional<long> output = utils::parse_duration("2d");
CHECK(expected == output);
}
TEST_CASE("parse_duration - 5w")
{
const long expected = ((60 * 60 * 24) * 7) * 5;
const std::optional<long> output = utils::parse_duration("5w");
CHECK(expected == output);
}
TEST_CASE("parse_duration - asdf")
{
const std::optional<long> expected;
const std::optional<long> output = utils::parse_duration("asdf");
CHECK(expected == output);
}
TEST_CASE("parse_duration - 1s")
{
const long expected = 1;
const std::optional<long> output = utils::parse_duration("1s");
CHECK(expected == output);
}
TEST_CASE("parse_duration - 2mon")
{
const long expected = ((60 * 60 * 24) * 31) * 2;
const std::optional<long> 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<long> output = utils::parse_duration("5y");
CHECK(expected == output);
}
TEST_CASE("parse_duration - 5")
{
const std::optional<long> expected;
const std::optional<long> output = utils::parse_duration("5");
CHECK(expected == output);
}
TEST_CASE("parse_duration - <empty>")
{
const std::optional<long> expected;
const std::optional<long> output = utils::parse_duration("");
CHECK(expected == output);
}
TEST_CASE("parse_duration - 5blorbs")
{
const std::optional<long> expected;
const std::optional<long> output = utils::parse_duration("5blorbs");
CHECK(expected == output);
}
TEST_CASE("parse_duration - 5y2d")
{
const std::optional<long> expected;
const std::optional<long> output = utils::parse_duration("5y2d");
CHECK(expected == output);
}