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
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
-------------
+6
View File
@@ -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
$<TARGET_OBJECTS:network>
$<TARGET_OBJECTS:xmpp>
$<TARGET_OBJECTS:bridge>
$<TARGET_OBJECTS:scheduler>
$<TARGET_OBJECTS:irc>
$<TARGET_OBJECTS:identd>)
@@ -230,6 +235,7 @@ add_executable(test_suite ${source_tests}
$<TARGET_OBJECTS:network>
$<TARGET_OBJECTS:xmpp>
$<TARGET_OBJECTS:bridge>
$<TARGET_OBJECTS:scheduler>
$<TARGET_OBJECTS:irc>
$<TARGET_OBJECTS:identd>)
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
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
-----------------
+9
View File
@@ -2,6 +2,7 @@
#ifdef USE_DATABASE
#include <database/select_query.hpp>
#include <database/delete_query.hpp>
#include <database/save.hpp>
#include <database/database.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);
}
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;
+1
View File
@@ -153,6 +153,7 @@ class Database
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_full_roster();
static void delete_old_muclines(const long& seconds);
static void close();
static void open(const std::string& filename);
+3
View File
@@ -14,6 +14,7 @@
#include <csignal>
#include <identd/identd_server.hpp>
#include <scheduler/scheduler.hpp>
// A flag set by the SIGINT signal handler.
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)
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();
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");
}
+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);
}