feat: add flat-file database backend for message history

Add pluggable storage backend abstraction (vtable) to the database layer,
allowing selection between SQLite (default) and a new flat-file backend
that stores messages as human-readable plain text files.

New files:
- database_sqlite.c: extracted SQLite backend from database.c
- database_flatfile.c: plain text backend with tolerant parser,
  LMC correction chains, UTF-8/BOM/CRLF handling, integrity checks

Commands:
- /privacy logging flatfile — switch to flat-file backend
- /history verify [<jid>] — check integrity of stored history

Fixes:
- Add missing 'off' guard in flatfile backend
- Enable CHLOG/HISTORY prefs when switching to flatfile mode

Logs stored in ~/.local/share/profanity/flatlog/{account}/{contact}/{date}.log
Format: {ISO8601} [{type}|{enc}|id:{id}|corrects:{id}] {sender}: {message}

Updated: CHANGELOG, CONTRIBUTING.md, profrc.example, man page, cmd_defs,
Makefile.am, test stubs, functional test support (PROF_FLATFILE=1)
This commit is contained in:
2026-02-17 17:31:50 +03:00
parent 9e1b95a814
commit 71230f7ced
14 changed files with 2353 additions and 644 deletions

View File

@@ -6686,6 +6686,11 @@ cmd_privacy(ProfWin* window, const char* const command, gchar** args)
} else if (g_strcmp0(arg, "redact") == 0) {
cons_show("Messages are going to be redacted.");
prefs_set_string(PREF_DBLOG, arg);
} else if (g_strcmp0(arg, "flatfile") == 0) {
cons_show("Using flat-file backend for message logging. Takes effect on next connection.");
prefs_set_string(PREF_DBLOG, arg);
prefs_set_boolean(PREF_CHLOG, TRUE);
prefs_set_boolean(PREF_HISTORY, TRUE);
} else {
cons_bad_cmd_usage(command);
return TRUE;
@@ -6730,6 +6735,58 @@ cmd_history(ProfWin* window, const char* const command, gchar** args)
return TRUE;
}
if (g_strcmp0(args[0], "verify") == 0) {
const gchar* contact_jid = args[1]; // may be NULL (verify all)
cons_show("Verifying history integrity...");
GSList* issues = log_database_verify_integrity(contact_jid);
int errors = 0, warnings = 0, infos = 0;
for (GSList* l = issues; l; l = l->next) {
integrity_issue_t* issue = l->data;
const char* level_str;
switch (issue->level) {
case INTEGRITY_ERROR:
level_str = "ERROR";
errors++;
break;
case INTEGRITY_WARNING:
level_str = "WARN";
warnings++;
break;
case INTEGRITY_INFO:
level_str = "INFO";
infos++;
break;
default:
level_str = "???";
break;
}
if (issue->line > 0) {
if (issue->level == INTEGRITY_ERROR) {
cons_show_error("[%s] %s:%d — %s", level_str, issue->file, issue->line, issue->message);
} else {
cons_show("[%s] %s:%d — %s", level_str, issue->file, issue->line, issue->message);
}
} else {
if (issue->level == INTEGRITY_ERROR) {
cons_show_error("[%s] %s — %s", level_str, issue->file, issue->message);
} else {
cons_show("[%s] %s — %s", level_str, issue->file, issue->message);
}
}
}
if (!issues) {
cons_show("Verification complete: no issues found.");
} else {
cons_show("Verification complete: %d error(s), %d warning(s), %d info(s).",
errors, warnings, infos);
g_slist_free_full(issues, (GDestroyNotify)integrity_issue_free);
}
return TRUE;
}
_cmd_set_boolean_preference(args[0], "Chat history", PREF_HISTORY);
// if set to on, set chlog (/logging chat on)