Compare commits

..

4 Commits

Author SHA1 Message Date
129eddadd7 fix(command): update logging command argument constraints
All checks were successful
CI Code / Check spelling (pull_request) Successful in 17s
CI Code / Check coding style (pull_request) Successful in 32s
CI Code / Code Coverage (pull_request) Successful in 2m45s
CI Code / Linux (debian) (pull_request) Successful in 4m39s
CI Code / Linux (ubuntu) (pull_request) Successful in 4m53s
CI Code / Linux (arch) (pull_request) Successful in 5m42s
2026-05-16 15:25:23 +00:00
88958c0a37 chore(config): add .kilo/ and .roo/ to gitignore
All checks were successful
CI Code / Check spelling (pull_request) Successful in 17s
CI Code / Check coding style (pull_request) Successful in 32s
CI Code / Code Coverage (pull_request) Successful in 2m43s
CI Code / Linux (debian) (pull_request) Successful in 4m39s
CI Code / Linux (ubuntu) (pull_request) Successful in 4m57s
CI Code / Linux (arch) (pull_request) Successful in 5m39s
2026-05-16 15:08:02 +00:00
df5b39fa7c fix(history): address PR #116 review feedback
All checks were successful
CI Code / Check spelling (pull_request) Successful in 17s
CI Code / Check coding style (pull_request) Successful in 31s
CI Code / Code Coverage (pull_request) Successful in 2m42s
CI Code / Linux (debian) (pull_request) Successful in 4m43s
CI Code / Linux (ubuntu) (pull_request) Successful in 4m58s
CI Code / Linux (arch) (pull_request) Successful in 5m55s
- console: drop orphaned `/logging chat` reference from
  cons_privacy_setting() — the subcommand was removed, PREF_CHLOG now
  tracks PREF_HISTORY implicitly
- console: delete cons_logging_setting() and remove its call from
  cons_show_log_prefs() — every line in it referenced /logging chat or
  /logging group, both now invalid after the deprecation below
- command: deprecate /logging command. It now prints a single notice
  pointing to /history; CMD_PREAMBLE trimmed (min_args=0, no
  setting_func, syntax/args/examples dropped); subcommand 'group'
  removed from autocomplete

The SQLite history-load gap for /correction off is split out into a
follow-up — out of scope for this PR.
2026-05-16 17:53:22 +03:00
102c32c744 feat(history): nodb backlog (#114)
All checks were successful
CI Code / Check spelling (pull_request) Successful in 16s
CI Code / Check coding style (pull_request) Successful in 33s
CI Code / Code Coverage (pull_request) Successful in 2m44s
CI Code / Linux (debian) (pull_request) Successful in 4m45s
CI Code / Linux (ubuntu) (pull_request) Successful in 4m58s
CI Code / Linux (arch) (pull_request) Successful in 5m48s
Implements all open items from the nodb backlog:

statusbar
- PREF_STATUSBAR_SHOW_DBBACKEND (default ON) gates the [sqlite] /
  [flatfile] indicator; toggle via "/statusbar show|hide dbbackend"

/history off|on
- "/history off" now stops persistence as well as hiding history on
  open: sets PREF_DBLOG=off + PREF_CHLOG=false in addition to
  PREF_HISTORY=false
- "/history on" restores persistence (re-enabling PREF_DBLOG=on if
  it was off) and PREF_CHLOG, in addition to PREF_HISTORY=true

/logging
- Removed the "/logging chat on|off" subcommand — chat-log
  persistence is now controlled exclusively by /history; the
  command kept only "/logging group on|off" for MUC logs
- All "use '/logging chat on' to enable" hints replaced with
  "/history on"

/privacy logging
- Single-pass validation across {on, off, redact, flatfile}
- Backend-switching values (on, flatfile) now take effect
  immediately when connected (via log_database_switch_backend),
  matching "/history switch" behaviour; off/redact only flip
  pref bits and keep the live backend open
- Updated help text to reflect runtime switching

/correction off
- win_print_outgoing and win_print_outgoing_with_receipt now gate
  _win_correct on PREF_CORRECTION_ALLOW, matching incoming-msg
  paths. Previously a peer's correction reflected via XEP-0280
  carbons (or the user's own /correct invocation) was applied
  in-buffer regardless of the pref

style
- Two local g_strndup allocations in database_flatfile.c switched
  from char* with manual g_free to auto_gchar gchar*
2026-05-16 08:44:24 +00:00
8 changed files with 404 additions and 175 deletions

View File

@@ -186,65 +186,14 @@ ai_json_escape(const gchar* str)
return g_string_free(result, FALSE);
}
/**
* Convert a 4-hex-digit sequence at @p to a 16-bit code point.
* Returns -1 on invalid hex.
*/
static int
_hex4_to_int(const gchar* p)
{
int val = 0;
for (int i = 0; i < 4; i++) {
char c = p[i];
val <<= 4;
if (c >= '0' && c <= '9')
val |= c - '0';
else if (c >= 'a' && c <= 'f')
val |= c - 'a' + 10;
else if (c >= 'A' && c <= 'F')
val |= c - 'A' + 10;
else
return -1;
}
return val;
}
/**
* Write a UTF-8 encoding of @cp to @w, advancing @w.
* Returns the updated pointer.
*/
static gchar*
_write_utf8(gchar* w, unsigned int cp)
{
if (cp <= 0x7F) {
*w++ = (char)cp;
} else if (cp <= 0x7FF) {
*w++ = (char)(0xC0 | (cp >> 6));
*w++ = (char)(0x80 | (cp & 0x3F));
} else if (cp <= 0xFFFF) {
*w++ = (char)(0xE0 | (cp >> 12));
*w++ = (char)(0x80 | ((cp >> 6) & 0x3F));
*w++ = (char)(0x80 | (cp & 0x3F));
} else if (cp <= 0x10FFFF) {
*w++ = (char)(0xF0 | (cp >> 18));
*w++ = (char)(0x80 | ((cp >> 12) & 0x3F));
*w++ = (char)(0x80 | ((cp >> 6) & 0x3F));
*w++ = (char)(0x80 | (cp & 0x3F));
}
/* Invalid code point: silently skip */
return w;
}
/* Decode JSON string escapes in [start, end) into a freshly allocated buffer.
* Handles \" \\ \/ \n \t \r \b \f and \uXXXX (including surrogate pairs). */
* Handles \" \\ \/ \n \t \r \b \f. \uXXXX sequences are passed through verbatim
* since the rest of the AI pipeline already deals in UTF-8 byte streams. */
static gchar*
_json_unescape_substring(const gchar* start, const gchar* end)
{
/* Estimate upper bound: each \uXXXX becomes up to 6 UTF-8 bytes. */
gsize max_len = (end - start) * 6 + 1;
gchar* out = g_new0(gchar, max_len);
gchar* out = g_new0(gchar, (end - start) + 1);
gchar* w = out;
for (const gchar* in = start; in < end;) {
if (*in == '\\' && in + 1 < end) {
switch (in[1]) {
@@ -280,64 +229,6 @@ _json_unescape_substring(const gchar* start, const gchar* end)
*w++ = '\f';
in += 2;
break;
case 'u':
/* Handle \uXXXX or surrogate pair \uHHHH\uLLLL */
if (in + 6 > end) {
/* Not enough characters — emit literal */
*w++ = *in++;
break;
}
if (in[2] != '\\' || in[3] != 'u') {
/* Single \uXXXX */
int cp = _hex4_to_int(in + 2);
if (cp < 0) {
/* Invalid hex — emit literal backslash */
*w++ = *in++;
} else if (cp < 0xD800 || cp > 0xDFFF) {
/* Not a surrogate — encode directly */
w = _write_utf8(w, (unsigned int)cp);
in += 6;
} else {
/* High or low surrogate — expect pair */
unsigned int high = (unsigned int)cp;
if ((high >= 0xD800 && high <= 0xDBFF) && in + 12 <= end && in[6] == '\\' && in[7] == 'u') {
/* High surrogate followed by \u */
int low = _hex4_to_int(in + 8);
if (low >= 0xDC00 && low <= 0xDFFF) {
/* Valid surrogate pair */
unsigned int cp2 = (0x10000 + ((high & 0x3FF) << 10)) | (low & 0x3FF);
w = _write_utf8(w, cp2);
in += 12;
} else {
/* Invalid low surrogate — emit replacement */
w = _write_utf8(w, 0xFFFD);
in += 6;
}
} else {
/* Orphan high surrogate or lone low surrogate */
w = _write_utf8(w, 0xFFFD);
in += 6;
}
}
} else {
/* \uHHHH\uLLLL — surrogate pair */
int high = _hex4_to_int(in + 2);
if (high < 0) {
*w++ = *in++;
break;
}
int low = _hex4_to_int(in + 8);
if ((high >= 0xD800 && high <= 0xDBFF) && (low >= 0xDC00 && low <= 0xDFFF)) {
unsigned int cp2 = (0x10000 + ((unsigned int)(high & 0x3FF) << 10)) | (unsigned int)(low & 0x3FF);
w = _write_utf8(w, cp2);
in += 12;
} else {
/* Invalid pair — emit replacement for high */
w = _write_utf8(w, 0xFFFD);
in += 6;
}
}
break;
default:
*w++ = *in++;
break;
@@ -347,9 +238,7 @@ _json_unescape_substring(const gchar* start, const gchar* end)
}
}
*w = '\0';
/* Realloc to exact size */
return g_realloc(out, w - out + 1);
return out;
}
/* Locate "field":"...value..." in json and return the unescaped value.

View File

@@ -1,5 +1,5 @@
/*
* chatlog.c
* log.c
* vim: expandtab:ts=4:sts=4:sw=4
*
* Copyright (C) 2012 - 2019 James Booth <boothj5@gmail.com>
@@ -36,81 +36,442 @@
#include "config.h"
#include "chatlog.h"
#include <assert.h>
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
/*
* Background message-logging to per-day plain-text files under
* $XDG_DATA_HOME/profanity/chatlogs/ is disabled. These files were never
* read back by the application — history replay reads from the database
* (log_database_get_previous_chat) — so this subsystem only ever wrote to
* disk for users to read with external tools.
*
* All public entry points are kept as no-ops to avoid touching the ~20
* call sites in event/, otr/ and profanity.c until the API itself is
* removed.
*/
#include "glib.h"
#include "glib/gstdio.h"
#include "log.h"
#include "chatlog.h"
#include "common.h"
#include "config/files.h"
#include "config/preferences.h"
#include "xmpp/xmpp.h"
#include "xmpp/muc.h"
static GHashTable* logs;
static GHashTable* groupchat_logs;
struct dated_chat_log
{
gchar* filename;
GDateTime* date;
};
static gboolean _log_roll_needed(struct dated_chat_log* dated_log);
static struct dated_chat_log* _create_chatlog(const char* const other, const char* const login);
static struct dated_chat_log* _create_groupchat_log(const char* const room, const char* const login);
static void _free_chat_log(struct dated_chat_log* dated_log);
static gboolean _key_equals(void* key1, void* key2);
static void _chat_log_chat(const char* const login, const char* const other, const gchar* const msg,
chat_log_direction_t direction, GDateTime* timestamp, const char* const resourcepart);
static void _groupchat_log_chat(const gchar* const login, const gchar* const room, const gchar* const nick,
const gchar* const msg);
void
_chatlog_close(void)
{
g_hash_table_destroy(logs);
g_hash_table_destroy(groupchat_logs);
}
void
chatlog_init(void)
{
log_info("Initialising chat logs");
prof_add_shutdown_routine(_chatlog_close);
logs = g_hash_table_new_full(g_str_hash, (GEqualFunc)_key_equals, free,
(GDestroyNotify)_free_chat_log);
groupchat_logs = g_hash_table_new_full(g_str_hash, (GEqualFunc)_key_equals, free,
(GDestroyNotify)_free_chat_log);
}
void
chat_log_msg_out(const char* const barejid, const char* const msg, const char* const resource)
{
if (prefs_get_boolean(PREF_CHLOG)) {
_chat_log_chat(connection_get_barejid(), barejid, msg, PROF_OUT_LOG, NULL, resource);
}
}
void
chat_log_otr_msg_out(const char* const barejid, const char* const msg, const char* const resource)
{
if (prefs_get_boolean(PREF_CHLOG)) {
const char* mybarejid = connection_get_barejid();
auto_gchar gchar* pref_otr_log = prefs_get_string(PREF_OTR_LOG);
if (strcmp(pref_otr_log, "on") == 0) {
_chat_log_chat(mybarejid, barejid, msg, PROF_OUT_LOG, NULL, resource);
} else if (strcmp(pref_otr_log, "redact") == 0) {
_chat_log_chat(mybarejid, barejid, "[redacted]", PROF_OUT_LOG, NULL, resource);
}
}
}
void
chat_log_pgp_msg_out(const char* const barejid, const char* const msg, const char* const resource)
{
if (prefs_get_boolean(PREF_CHLOG)) {
const char* mybarejid = connection_get_barejid();
auto_gchar gchar* pref_pgp_log = prefs_get_string(PREF_PGP_LOG);
if (strcmp(pref_pgp_log, "on") == 0) {
_chat_log_chat(mybarejid, barejid, msg, PROF_OUT_LOG, NULL, resource);
} else if (strcmp(pref_pgp_log, "redact") == 0) {
_chat_log_chat(mybarejid, barejid, "[redacted]", PROF_OUT_LOG, NULL, resource);
}
}
}
void
chat_log_omemo_msg_out(const char* const barejid, const char* const msg, const char* const resource)
{
}
void
chat_log_msg_in(ProfMessage* message)
{
if (prefs_get_boolean(PREF_CHLOG)) {
const char* mybarejid = connection_get_barejid();
auto_gchar gchar* pref_omemo_log = prefs_get_string(PREF_OMEMO_LOG);
if (strcmp(pref_omemo_log, "on") == 0) {
_chat_log_chat(mybarejid, barejid, msg, PROF_OUT_LOG, NULL, resource);
} else if (strcmp(pref_omemo_log, "redact") == 0) {
_chat_log_chat(mybarejid, barejid, "[redacted]", PROF_OUT_LOG, NULL, resource);
}
}
}
void
chat_log_otr_msg_in(ProfMessage* message)
{
if (prefs_get_boolean(PREF_CHLOG)) {
const char* mybarejid = connection_get_barejid();
auto_gchar gchar* pref_otr_log = prefs_get_string(PREF_OTR_LOG);
if (message->enc == PROF_MSG_ENC_NONE || (strcmp(pref_otr_log, "on") == 0)) {
if (message->type == PROF_MSG_TYPE_MUCPM) {
_chat_log_chat(mybarejid, message->from_jid->barejid, message->plain, PROF_IN_LOG, message->timestamp, message->from_jid->resourcepart);
} else {
_chat_log_chat(mybarejid, message->from_jid->barejid, message->plain, PROF_IN_LOG, message->timestamp, NULL);
}
} else if (strcmp(pref_otr_log, "redact") == 0) {
if (message->type == PROF_MSG_TYPE_MUCPM) {
_chat_log_chat(mybarejid, message->from_jid->barejid, "[redacted]", PROF_IN_LOG, message->timestamp, message->from_jid->resourcepart);
} else {
_chat_log_chat(mybarejid, message->from_jid->barejid, "[redacted]", PROF_IN_LOG, message->timestamp, NULL);
}
}
}
}
void
chat_log_pgp_msg_in(ProfMessage* message)
{
if (prefs_get_boolean(PREF_CHLOG)) {
const char* mybarejid = connection_get_barejid();
auto_gchar gchar* pref_pgp_log = prefs_get_string(PREF_PGP_LOG);
if (strcmp(pref_pgp_log, "on") == 0) {
if (message->type == PROF_MSG_TYPE_MUCPM) {
_chat_log_chat(mybarejid, message->from_jid->barejid, message->plain, PROF_IN_LOG, message->timestamp, message->from_jid->resourcepart);
} else {
_chat_log_chat(mybarejid, message->from_jid->barejid, message->plain, PROF_IN_LOG, message->timestamp, NULL);
}
} else if (strcmp(pref_pgp_log, "redact") == 0) {
if (message->type == PROF_MSG_TYPE_MUCPM) {
_chat_log_chat(mybarejid, message->from_jid->barejid, "[redacted]", PROF_IN_LOG, message->timestamp, message->from_jid->resourcepart);
} else {
_chat_log_chat(mybarejid, message->from_jid->barejid, "[redacted]", PROF_IN_LOG, message->timestamp, NULL);
}
}
}
}
void
chat_log_omemo_msg_in(ProfMessage* message)
{
if (prefs_get_boolean(PREF_CHLOG)) {
const char* mybarejid = connection_get_barejid();
auto_gchar gchar* pref_omemo_log = prefs_get_string(PREF_OMEMO_LOG);
if (strcmp(pref_omemo_log, "on") == 0) {
if (message->type == PROF_MSG_TYPE_MUCPM) {
_chat_log_chat(mybarejid, message->from_jid->barejid, message->plain, PROF_IN_LOG, message->timestamp, message->from_jid->resourcepart);
} else {
_chat_log_chat(mybarejid, message->from_jid->barejid, message->plain, PROF_IN_LOG, message->timestamp, NULL);
}
} else if (strcmp(pref_omemo_log, "redact") == 0) {
if (message->type == PROF_MSG_TYPE_MUCPM) {
_chat_log_chat(mybarejid, message->from_jid->barejid, "[redacted]", PROF_IN_LOG, message->timestamp, message->from_jid->resourcepart);
} else {
_chat_log_chat(mybarejid, message->from_jid->barejid, "[redacted]", PROF_IN_LOG, message->timestamp, NULL);
}
}
}
}
void
chat_log_msg_in(ProfMessage* message)
{
if (prefs_get_boolean(PREF_CHLOG)) {
const char* mybarejid = connection_get_barejid();
if (message->type == PROF_MSG_TYPE_MUCPM) {
_chat_log_chat(mybarejid, message->from_jid->barejid, message->plain, PROF_IN_LOG, message->timestamp, message->from_jid->resourcepart);
} else {
_chat_log_chat(mybarejid, message->from_jid->barejid, message->plain, PROF_IN_LOG, message->timestamp, NULL);
}
}
}
static void
_chat_log_chat(const char* const login, const char* const other, const char* msg,
chat_log_direction_t direction, GDateTime* timestamp, const char* const resourcepart)
{
auto_gchar gchar* pref_dblog = prefs_get_string(PREF_DBLOG);
if (g_strcmp0(pref_dblog, "redact") == 0) {
msg = "[REDACTED]";
}
char* other_name;
GString* other_str = NULL;
if (resourcepart) {
other_str = g_string_new(other);
g_string_append(other_str, "_");
g_string_append(other_str, resourcepart);
other_name = other_str->str;
} else {
other_name = (char*)other;
}
struct dated_chat_log* dated_log = g_hash_table_lookup(logs, other_name);
// no log for user
if (dated_log == NULL) {
dated_log = _create_chatlog(other_name, login);
g_hash_table_insert(logs, strdup(other_name), dated_log);
// log entry exists but file removed
} else if (!g_file_test(dated_log->filename, G_FILE_TEST_EXISTS)) {
dated_log = _create_chatlog(other_name, login);
g_hash_table_replace(logs, strdup(other_name), dated_log);
// log file needs rolling
} else if (_log_roll_needed(dated_log)) {
dated_log = _create_chatlog(other_name, login);
g_hash_table_replace(logs, strdup(other_name), dated_log);
}
if (resourcepart) {
g_string_free(other_str, TRUE);
}
if (timestamp == NULL) {
timestamp = g_date_time_new_now_local();
} else {
g_date_time_ref(timestamp);
}
auto_gchar gchar* date_fmt = g_date_time_format_iso8601(timestamp);
FILE* chatlogp = fopen(dated_log->filename, "a");
g_chmod(dated_log->filename, S_IRUSR | S_IWUSR);
if (chatlogp) {
if (direction == PROF_IN_LOG) {
if (strncmp(msg, "/me ", 4) == 0) {
if (resourcepart) {
fprintf(chatlogp, "%s - *%s %s\n", date_fmt, resourcepart, msg + 4);
} else {
fprintf(chatlogp, "%s - *%s %s\n", date_fmt, other, msg + 4);
}
} else {
if (resourcepart) {
fprintf(chatlogp, "%s - %s: %s\n", date_fmt, resourcepart, msg);
} else {
fprintf(chatlogp, "%s - %s: %s\n", date_fmt, other, msg);
}
}
} else {
if (strncmp(msg, "/me ", 4) == 0) {
fprintf(chatlogp, "%s - *me %s\n", date_fmt, msg + 4);
} else {
fprintf(chatlogp, "%s - me: %s\n", date_fmt, msg);
}
}
fflush(chatlogp);
int result = fclose(chatlogp);
if (result == EOF) {
log_error("Error closing file %s, errno = %d", dated_log->filename, errno);
}
}
g_date_time_unref(timestamp);
}
void
groupchat_log_msg_out(const gchar* const room, const gchar* const msg)
{
if (prefs_get_boolean(PREF_GRLOG)) {
const char* const mynick = muc_nick(room);
_groupchat_log_chat(connection_get_barejid(), room, mynick, msg);
}
}
void
groupchat_log_msg_in(const gchar* const room, const gchar* const nick, const gchar* const msg)
{
if (prefs_get_boolean(PREF_GRLOG)) {
_groupchat_log_chat(connection_get_barejid(), room, nick, msg);
}
}
void
groupchat_log_omemo_msg_out(const gchar* const room, const gchar* const msg)
{
if (prefs_get_boolean(PREF_CHLOG)) {
const char* mybarejid = connection_get_barejid();
auto_gchar gchar* pref_omemo_log = prefs_get_string(PREF_OMEMO_LOG);
const char* const mynick = muc_nick(room);
if (strcmp(pref_omemo_log, "on") == 0) {
_groupchat_log_chat(mybarejid, room, mynick, msg);
} else if (strcmp(pref_omemo_log, "redact") == 0) {
_groupchat_log_chat(mybarejid, room, mynick, "[redacted]");
}
}
}
void
groupchat_log_omemo_msg_in(const gchar* const room, const gchar* const nick, const gchar* const msg)
{
if (prefs_get_boolean(PREF_CHLOG)) {
const char* mybarejid = connection_get_barejid();
auto_gchar gchar* pref_omemo_log = prefs_get_string(PREF_OMEMO_LOG);
if (strcmp(pref_omemo_log, "on") == 0) {
_groupchat_log_chat(mybarejid, room, nick, msg);
} else if (strcmp(pref_omemo_log, "redact") == 0) {
_groupchat_log_chat(mybarejid, room, nick, "[redacted]");
}
}
}
void
_groupchat_log_chat(const gchar* const login, const gchar* const room, const gchar* const nick,
const gchar* const msg)
{
struct dated_chat_log* dated_log = g_hash_table_lookup(groupchat_logs, room);
// no log for room
if (dated_log == NULL) {
dated_log = _create_groupchat_log(room, login);
g_hash_table_insert(groupchat_logs, strdup(room), dated_log);
// log exists but needs rolling
} else if (_log_roll_needed(dated_log)) {
dated_log = _create_groupchat_log(room, login);
g_hash_table_replace(logs, strdup(room), dated_log);
}
GDateTime* dt_tmp = g_date_time_new_now_local();
auto_gchar gchar* date_fmt = g_date_time_format_iso8601(dt_tmp);
FILE* grpchatlogp = fopen(dated_log->filename, "a");
g_chmod(dated_log->filename, S_IRUSR | S_IWUSR);
if (grpchatlogp) {
if (strncmp(msg, "/me ", 4) == 0) {
fprintf(grpchatlogp, "%s - *%s %s\n", date_fmt, nick, msg + 4);
} else {
fprintf(grpchatlogp, "%s - %s: %s\n", date_fmt, nick, msg);
}
fflush(grpchatlogp);
int result = fclose(grpchatlogp);
if (result == EOF) {
log_error("Error closing file %s, errno = %d", dated_log->filename, errno);
}
}
g_date_time_unref(dt_tmp);
}
static char*
_get_log_filename(const char* const other, const char* const login, GDateTime* dt, gboolean is_room)
{
auto_gchar gchar* chatlogs_dir = files_file_in_account_data_path(DIR_CHATLOGS, login, is_room ? "rooms" : NULL);
auto_gchar gchar* logfile_name = g_date_time_format(dt, "%Y_%m_%d.log");
auto_char char* other_ = str_replace(other, "@", "_at_");
auto_gchar gchar* logs_path = g_strdup_printf("%s/%s", chatlogs_dir, other_);
gchar* logfile_path = NULL;
if (create_dir(logs_path)) {
logfile_path = g_strdup_printf("%s/%s", logs_path, logfile_name);
}
return logfile_path;
}
static struct dated_chat_log*
_create_chatlog(const char* const other, const char* const login)
{
GDateTime* now = g_date_time_new_now_local();
auto_char char* filename = _get_log_filename(other, login, now, FALSE);
struct dated_chat_log* new_log = malloc(sizeof(struct dated_chat_log));
new_log->filename = strdup(filename);
new_log->date = now;
return new_log;
}
static struct dated_chat_log*
_create_groupchat_log(const char* const room, const char* const login)
{
GDateTime* now = g_date_time_new_now_local();
auto_char char* filename = _get_log_filename(room, login, now, TRUE);
struct dated_chat_log* new_log = malloc(sizeof(struct dated_chat_log));
new_log->filename = strdup(filename);
new_log->date = now;
return new_log;
}
static gboolean
_log_roll_needed(struct dated_chat_log* dated_log)
{
gboolean result = FALSE;
GDateTime* now = g_date_time_new_now_local();
if (g_date_time_get_day_of_year(dated_log->date) != g_date_time_get_day_of_year(now)) {
result = TRUE;
}
g_date_time_unref(now);
return result;
}
static void
_free_chat_log(struct dated_chat_log* dated_log)
{
if (dated_log) {
if (dated_log->filename) {
g_free(dated_log->filename);
dated_log->filename = NULL;
}
if (dated_log->date) {
g_date_time_unref(dated_log->date);
dated_log->date = NULL;
}
free(dated_log);
}
}
static gboolean
_key_equals(void* key1, void* key2)
{
gchar* str1 = (gchar*)key1;
gchar* str2 = (gchar*)key2;
return (g_strcmp0(str1, str2) == 0);
}

View File

@@ -10932,16 +10932,23 @@ cmd_ai_start(ProfWin* window, const char* const command, gchar** args)
if (g_strv_length(args) >= 2) {
provider_name = args[1];
}
if (!provider_name) {
cons_show_error("Please specify provider name.");
return TRUE;
}
if (g_strv_length(args) >= 3) {
model = args[2];
}
// Resolve defaults
auto_gchar gchar* owned_provider_name = NULL;
if (!provider_name) {
// Use default provider from preferences
auto_gchar gchar* default_provider = prefs_get_string(PREF_AI_PROVIDER);
if (default_provider && strlen(default_provider) > 0) {
provider_name = default_provider;
}
}
if (!provider_name) {
provider_name = "openai"; // Fallback
}
AIProvider* provider = ai_get_provider(provider_name);
if (!provider) {
cons_show_error("Provider '%s' not found. Use '/ai set provider %s <url>' to add it.",

View File

@@ -61,22 +61,6 @@ integrity_issue_free(integrity_issue_t* issue)
}
}
gboolean
log_database_can_recover_messages(void)
{
if (!active_db_backend) {
return FALSE;
}
auto_gchar gchar* pref_dblog = prefs_get_string(PREF_DBLOG);
if (g_strcmp0(pref_dblog, "off") == 0) {
return FALSE;
}
if (g_strcmp0(pref_dblog, "redact") == 0) {
return FALSE;
}
return TRUE;
}
gboolean
log_database_init(ProfAccount* account)
{

View File

@@ -109,9 +109,6 @@ ProfMessage* log_database_get_limits_info(const gchar* const contact_barejid, gb
void log_database_close(void);
GSList* log_database_verify_integrity(const gchar* const contact_barejid);
// FALSE if no backend, or PREF_DBLOG is "off" / "redact".
gboolean log_database_can_recover_messages(void);
// Cross-backend export/import (requires HAVE_SQLITE)
#ifdef HAVE_SQLITE
int log_database_export_to_flatfile(const gchar* const contact_jid);

View File

@@ -301,6 +301,12 @@ cl_ev_send_ai_msg(ProfAiWin* aiwin, const char* const message, const char* const
return;
}
/* Reset paged flag before printing user message.
* If the user scrolled up to view history, paged=1 would suppress
* the message in _win_printf(). Reset it here so the message displays. */
aiwin->window.layout->paged = 0;
aiwin->window.layout->unread_msg = 0;
// Display user message in AI window.
win_print_outgoing(&aiwin->window, ">>", id, NULL, message);

View File

@@ -46,7 +46,6 @@
#include "ui/ui.h"
#include "ui/titlebar.h"
#include "ui/inputwin.h"
#include "ui/win_types.h"
#include "ui/window_list.h"
#include "ui/window.h"
#include "ui/screen.h"
@@ -253,10 +252,6 @@ _title_bar_draw(void)
_show_muc_privacy(mucwin);
_show_attention(current, mucwin->has_attention);
_show_scrolled(current);
} else if (current && current->type == WIN_AI) {
ProfAiWin* aiwin = (ProfAiWin*)current;
assert(aiwin->memcheck == PROFAIWIN_MEMCHECK);
_show_scrolled(current);
}
_show_self_presence();

View File

@@ -848,14 +848,6 @@ win_page_down(ProfWin* window, int scroll_size)
window->layout->paged = 0;
window->layout->unread_msg = 0;
}
// Non-chat windows have no DB to fetch from, so WIN_SCROLL_REACHED_BOTTOM
// never fires; reset paged once the buffer's last line is on screen.
if (window->type != WIN_CHAT
&& (total_rows - *page_start) <= page_space + 1) {
window->layout->paged = 0;
window->layout->unread_msg = 0;
}
}
void
@@ -1809,12 +1801,10 @@ static void
_win_printf(ProfWin* window, const char* show_char, int pad_indent, GDateTime* timestamp, int flags, theme_item_t theme_item, const char* const display_from, const char* const from_jid, const char* const message_id, const char* const message, ...)
{
/* Prevent printing and buffer update when user is viewing message history [SCROLLING]*/
if (window->layout->paged && wins_is_current(window)) {
window->layout->unread_msg++;
// Skip buffer only when DB can replay the message on scroll-down.
if (window->type == WIN_CHAT && log_database_can_recover_messages()) {
return;
}
return;
}
if (timestamp == NULL) {