Compare commits
10 Commits
fix/ai-fol
...
fix/ai-jso
| Author | SHA1 | Date | |
|---|---|---|---|
|
53cdf488b6
|
|||
|
4776c1f1ec
|
|||
|
a3a45ad477
|
|||
|
2952466abd
|
|||
|
06b80bc89a
|
|||
|
f9e0ba9630
|
|||
|
7469f31c78
|
|||
|
5e329c77e1
|
|||
|
9e5dfb14f8
|
|||
|
1aaa382d5e
|
2
.gitignore
vendored
2
.gitignore
vendored
@@ -17,6 +17,8 @@ compile_commands.json
|
||||
.project
|
||||
.settings/
|
||||
.vscode/
|
||||
.kilo/
|
||||
.roo/
|
||||
*.plist/
|
||||
|
||||
# autotools
|
||||
|
||||
@@ -186,14 +186,65 @@ 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. \uXXXX sequences are passed through verbatim
|
||||
* since the rest of the AI pipeline already deals in UTF-8 byte streams. */
|
||||
* Handles \" \\ \/ \n \t \r \b \f and \uXXXX (including surrogate pairs). */
|
||||
static gchar*
|
||||
_json_unescape_substring(const gchar* start, const gchar* end)
|
||||
{
|
||||
gchar* out = g_new0(gchar, (end - start) + 1);
|
||||
/* 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* w = out;
|
||||
|
||||
for (const gchar* in = start; in < end;) {
|
||||
if (*in == '\\' && in + 1 < end) {
|
||||
switch (in[1]) {
|
||||
@@ -229,6 +280,64 @@ _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;
|
||||
@@ -238,7 +347,9 @@ _json_unescape_substring(const gchar* start, const gchar* end)
|
||||
}
|
||||
}
|
||||
*w = '\0';
|
||||
return out;
|
||||
|
||||
/* Realloc to exact size */
|
||||
return g_realloc(out, w - out + 1);
|
||||
}
|
||||
|
||||
/* Locate "field":"...value..." in json and return the unescaped value.
|
||||
@@ -1419,10 +1530,10 @@ _ai_request_thread(gpointer data)
|
||||
* list modifications). We must snapshot all fields atomically.
|
||||
* ===================================================================== */
|
||||
pthread_mutex_lock(&session->lock);
|
||||
gchar* local_provider_name = g_strdup(session->provider_name);
|
||||
auto_gchar gchar* local_provider_name = g_strdup(session->provider_name);
|
||||
AIProvider* local_provider = ai_provider_ref(session->provider);
|
||||
gchar* local_model = g_strdup(session->model);
|
||||
gchar* local_api_key = g_strdup(session->api_key ? session->api_key : "");
|
||||
auto_gchar gchar* local_model = g_strdup(session->model);
|
||||
auto_gchar gchar* local_api_key = g_strdup(session->api_key ? session->api_key : "");
|
||||
pthread_mutex_unlock(&session->lock);
|
||||
|
||||
log_debug("[AI-THREAD] Session: %s/%s", local_provider_name, local_model);
|
||||
@@ -1434,6 +1545,8 @@ _ai_request_thread(gpointer data)
|
||||
local_provider_name, local_provider_name);
|
||||
log_error("AI request failed for %s/%s: %s", local_provider_name, local_model, error_msg);
|
||||
_aiwin_display_error(user_data, error_msg);
|
||||
ai_provider_unref(local_provider);
|
||||
ai_session_unref(session);
|
||||
g_free(args);
|
||||
return NULL;
|
||||
}
|
||||
@@ -1444,6 +1557,8 @@ _ai_request_thread(gpointer data)
|
||||
log_error("AI request failed for %s/%s: Failed to initialize curl",
|
||||
local_provider_name, local_model);
|
||||
_aiwin_display_error(user_data, "Failed to initialize curl.");
|
||||
ai_provider_unref(local_provider);
|
||||
ai_session_unref(session);
|
||||
g_free(args);
|
||||
return NULL;
|
||||
}
|
||||
@@ -1492,6 +1607,7 @@ _ai_request_thread(gpointer data)
|
||||
long http_code = 0;
|
||||
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code);
|
||||
log_debug("[AI-THREAD] HTTP response code: %ld", http_code);
|
||||
auto_gchar gchar* response_data = g_steal_pointer(&response.data);
|
||||
|
||||
if (res != CURLE_OK) {
|
||||
auto_gchar gchar* error_msg = g_strdup(curl_easy_strerror(res));
|
||||
@@ -1500,17 +1616,14 @@ _ai_request_thread(gpointer data)
|
||||
} else if (http_code >= 400) {
|
||||
/* Handle HTTP errors */
|
||||
log_debug("[AI-THREAD] HTTP error response body (%zu bytes): %s",
|
||||
response.size, response.data ? response.data : "NULL");
|
||||
response.size, response_data);
|
||||
/* Try to extract the actual error message from the JSON response */
|
||||
auto_gchar gchar* parsed_error = ai_parse_error_message(response.data);
|
||||
auto_gchar gchar* error_msg = parsed_error ? g_strdup_printf("HTTP %ld: %s", http_code, parsed_error) : g_strdup_printf("HTTP %ld: %s", http_code, response.data && strlen(response.data) > 0 ? response.data : "Unknown error");
|
||||
auto_gchar gchar* parsed_error = ai_parse_error_message(response_data);
|
||||
auto_gchar gchar* error_msg = parsed_error ? g_strdup_printf("HTTP %ld: %s", http_code, parsed_error) : g_strdup_printf("HTTP %ld: %s", http_code, response_data && strlen(response_data) > 0 ? response_data : "Unknown error");
|
||||
log_error("AI request failed for %s/%s: %s", local_provider_name, local_model, error_msg);
|
||||
_aiwin_display_error(user_data, error_msg);
|
||||
} else {
|
||||
/* Parse response - transfer ownership to auto_gchar for cleanup */
|
||||
log_debug("[AI-THREAD] Raw API response (%zu bytes): %s", response.size, response.data ? response.data : "NULL");
|
||||
auto_gchar gchar* response_data = response.data;
|
||||
response.data = NULL;
|
||||
log_debug("[AI-THREAD] Raw API response (%zu bytes): %s", response.size, response_data);
|
||||
auto_gchar gchar* content = ai_parse_response(response_data);
|
||||
if (content) {
|
||||
/* Add assistant response to history (under lock) */
|
||||
@@ -1531,12 +1644,9 @@ _ai_request_thread(gpointer data)
|
||||
curl_slist_free_all(headers);
|
||||
curl_easy_cleanup(curl);
|
||||
|
||||
/* Cleanup local copies */
|
||||
g_free(local_provider_name);
|
||||
ai_provider_unref(local_provider);
|
||||
g_free(local_model);
|
||||
g_free(local_api_key);
|
||||
|
||||
ai_session_unref(session);
|
||||
g_free(args);
|
||||
|
||||
return NULL;
|
||||
|
||||
@@ -1132,6 +1132,7 @@ cmd_ac_init(void)
|
||||
autocomplete_add(statusbar_show_ac, "name");
|
||||
autocomplete_add(statusbar_show_ac, "number");
|
||||
autocomplete_add(statusbar_show_ac, "read");
|
||||
autocomplete_add(statusbar_show_ac, "dbbackend");
|
||||
|
||||
autocomplete_add(statusbar_tabmode_ac, "actlist");
|
||||
autocomplete_add(statusbar_tabmode_ac, "dynamic");
|
||||
@@ -1146,9 +1147,6 @@ cmd_ac_init(void)
|
||||
autocomplete_add(status_state_ac, "xa");
|
||||
autocomplete_add(status_state_ac, "dnd");
|
||||
|
||||
autocomplete_add(logging_ac, "chat");
|
||||
autocomplete_add(logging_ac, "group");
|
||||
|
||||
autocomplete_add(privacy_ac, "logging");
|
||||
autocomplete_add(privacy_ac, "os");
|
||||
|
||||
@@ -4079,11 +4077,6 @@ _logging_autocomplete(ProfWin* window, const char* const input, gboolean previou
|
||||
return result;
|
||||
}
|
||||
|
||||
result = autocomplete_param_with_func(input, "/logging chat", prefs_autocomplete_boolean_choice, previous, NULL);
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
|
||||
result = autocomplete_param_with_ac(input, "/logging group", logging_group_ac, TRUE, previous);
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -1289,8 +1289,8 @@ static const struct cmd_t command_defs[] = {
|
||||
CMD_TAGS(
|
||||
CMD_TAG_UI)
|
||||
CMD_SYN(
|
||||
"/statusbar show name|number|read",
|
||||
"/statusbar hide name|number|read",
|
||||
"/statusbar show name|number|read|dbbackend",
|
||||
"/statusbar hide name|number|read|dbbackend",
|
||||
"/statusbar maxtabs <value>",
|
||||
"/statusbar tablen <value>",
|
||||
"/statusbar tabmode default|dynamic|actlist",
|
||||
@@ -1308,6 +1308,7 @@ static const struct cmd_t command_defs[] = {
|
||||
{ "show|hide name", "Show or hide names in tabs." },
|
||||
{ "show|hide number", "Show or hide numbers in tabs." },
|
||||
{ "show|hide read", "Show or hide inactive tabs." },
|
||||
{ "show|hide dbbackend", "Show or hide the database backend indicator [sqlite] or [flatfile]." },
|
||||
{ "self user|barejid|fulljid", "Show account user name, barejid, fulljid as status bar title." },
|
||||
{ "self off", "Disable showing self as status bar title." },
|
||||
{ "chat user|jid", "Show users name, or fulljid. Change needs a redraw/restart to take effect." },
|
||||
@@ -1659,23 +1660,14 @@ static const struct cmd_t command_defs[] = {
|
||||
},
|
||||
|
||||
{ CMD_PREAMBLE("/logging",
|
||||
parse_args, 2, 3, &cons_logging_setting)
|
||||
parse_args, 0, 0, NULL)
|
||||
CMD_MAINFUNC(cmd_logging)
|
||||
CMD_TAGS(
|
||||
CMD_TAG_CHAT)
|
||||
CMD_SYN(
|
||||
"/logging chat|group on|off")
|
||||
"/logging")
|
||||
CMD_DESC(
|
||||
"Configure chat logging. "
|
||||
"Switch logging on or off. "
|
||||
"Chat logging will be enabled if /history is set to on. "
|
||||
"When disabling this option, /history will also be disabled. ")
|
||||
CMD_ARGS(
|
||||
{ "chat on|off", "Enable/Disable regular chat logging." },
|
||||
{ "group on|off", "Enable/Disable groupchat (room) logging." })
|
||||
CMD_EXAMPLES(
|
||||
"/logging chat on",
|
||||
"/logging group off")
|
||||
"Deprecated. Use '/history' instead.")
|
||||
},
|
||||
|
||||
{ CMD_PREAMBLE("/states",
|
||||
@@ -1888,15 +1880,16 @@ static const struct cmd_t command_defs[] = {
|
||||
"/history export [<jid>]",
|
||||
"/history import [<jid>]")
|
||||
CMD_DESC(
|
||||
"Switch chat history on or off, /logging chat will automatically be enabled when this setting is on. "
|
||||
"When history is enabled, previous messages are shown in chat windows. "
|
||||
"Enable or disable chat history. "
|
||||
"When on, messages are persisted via the active database backend and shown in chat windows. "
|
||||
"When off, persistence stops and old history is hidden on chat-window open. "
|
||||
"Use 'backend' to show the active database backend. "
|
||||
"Use 'switch' to change the active database backend at runtime without reconnecting. "
|
||||
"Use 'verify' to check integrity of stored message history. "
|
||||
"Use 'export' to copy messages from SQLite to flat-file format, or 'import' to copy from flat-file to SQLite. "
|
||||
"Both export and import merge with existing data (duplicates are skipped).")
|
||||
CMD_ARGS(
|
||||
{ "on|off", "Enable or disable showing chat history." },
|
||||
{ "on|off", "Enable or disable persistence and showing chat history." },
|
||||
{ "backend", "Show the name of the active database backend." },
|
||||
{ "switch sqlite|flatfile", "Switch the active database backend at runtime." },
|
||||
{ "verify [<jid>]", "Verify integrity of message history. Optionally specify a JID to check only one contact." },
|
||||
@@ -2741,7 +2734,7 @@ static const struct cmd_t command_defs[] = {
|
||||
"clientid to set the client identification name "
|
||||
"session_alarm to configure an alarm when more clients log in.")
|
||||
CMD_ARGS(
|
||||
{ "logging on|redact|off|flatfile", "Switch chat logging. 'on' uses SQLite database (default). 'flatfile' stores messages as plain text files in ~/.local/share/profanity/flatlog/ that can be manually edited with any text editor. 'off' disables logging entirely. 'redact' stores messages with content replaced by '[redacted]'. Note: 'off' might have unintended consequences, such as not being able to decrypt OMEMO encrypted messages received later via MAM. The 'flatfile' setting takes effect on next connection." },
|
||||
{ "logging on|redact|off|flatfile", "Switch chat logging backend. 'on' uses the SQLite database (default). 'flatfile' stores messages as plain-text files under ~/.local/share/profanity/flatlog/ that can be edited with any text editor. 'off' disables logging entirely. 'redact' keeps logging but replaces every message body with '[REDACTED]'. Note: 'off' may have side effects such as not being able to decrypt OMEMO messages received later via MAM. Backend changes ('on' / 'flatfile') take effect immediately when connected, otherwise on next connect." },
|
||||
{ "os on|off", "Choose whether to include the OS name if a user asks for software information (XEP-0092)." }
|
||||
)
|
||||
CMD_EXAMPLES(
|
||||
|
||||
@@ -6002,6 +6002,12 @@ cmd_statusbar(ProfWin* window, const char* const command, gchar** args)
|
||||
ui_resize();
|
||||
return TRUE;
|
||||
}
|
||||
if (g_strcmp0(args[1], "dbbackend") == 0) {
|
||||
prefs_set_boolean(PREF_STATUSBAR_SHOW_DBBACKEND, TRUE);
|
||||
cons_show("Enabled showing database backend indicator.");
|
||||
ui_resize();
|
||||
return TRUE;
|
||||
}
|
||||
cons_bad_cmd_usage(command);
|
||||
return TRUE;
|
||||
}
|
||||
@@ -6035,6 +6041,12 @@ cmd_statusbar(ProfWin* window, const char* const command, gchar** args)
|
||||
ui_resize();
|
||||
return TRUE;
|
||||
}
|
||||
if (g_strcmp0(args[1], "dbbackend") == 0) {
|
||||
prefs_set_boolean(PREF_STATUSBAR_SHOW_DBBACKEND, FALSE);
|
||||
cons_show("Disabled showing database backend indicator.");
|
||||
ui_resize();
|
||||
return TRUE;
|
||||
}
|
||||
cons_bad_cmd_usage(command);
|
||||
return TRUE;
|
||||
}
|
||||
@@ -6668,36 +6680,48 @@ cmd_privacy(ProfWin* window, const char* const command, gchar** args)
|
||||
}
|
||||
|
||||
if (g_strcmp0(args[0], "logging") == 0) {
|
||||
gchar* arg = args[1];
|
||||
const gchar* arg = args[1];
|
||||
if (arg == NULL) {
|
||||
cons_bad_cmd_usage(command);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
if (g_strcmp0(arg, "on") == 0) {
|
||||
cons_show("Logging enabled.");
|
||||
prefs_set_string(PREF_DBLOG, arg);
|
||||
prefs_set_boolean(PREF_CHLOG, TRUE);
|
||||
prefs_set_boolean(PREF_HISTORY, TRUE);
|
||||
} else if (g_strcmp0(arg, "off") == 0) {
|
||||
cons_show("Logging disabled.");
|
||||
prefs_set_string(PREF_DBLOG, arg);
|
||||
prefs_set_boolean(PREF_CHLOG, FALSE);
|
||||
prefs_set_boolean(PREF_HISTORY, FALSE);
|
||||
} 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) {
|
||||
auto_gchar gchar* ff_path = files_get_data_path(DIR_FLATLOG);
|
||||
cons_show("Using flat-file backend for message logging. Takes effect on next connection.");
|
||||
cons_show("Flatfile directory: %s", ff_path);
|
||||
prefs_set_string(PREF_DBLOG, arg);
|
||||
prefs_set_boolean(PREF_CHLOG, TRUE);
|
||||
prefs_set_boolean(PREF_HISTORY, TRUE);
|
||||
} else {
|
||||
const gboolean is_on = (g_strcmp0(arg, "on") == 0);
|
||||
const gboolean is_off = (g_strcmp0(arg, "off") == 0);
|
||||
const gboolean is_redact = (g_strcmp0(arg, "redact") == 0);
|
||||
const gboolean is_flatfile = (g_strcmp0(arg, "flatfile") == 0);
|
||||
|
||||
if (!is_on && !is_off && !is_redact && !is_flatfile) {
|
||||
cons_bad_cmd_usage(command);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
prefs_set_string(PREF_DBLOG, arg);
|
||||
prefs_set_boolean(PREF_CHLOG, !is_off);
|
||||
prefs_set_boolean(PREF_HISTORY, !is_off);
|
||||
|
||||
// For backend-switching values (on = sqlite, flatfile), apply the
|
||||
// change immediately when connected so the user does not have to
|
||||
// reconnect to see the new backend take effect. off/redact change
|
||||
// write semantics but keep the currently-open backend alive.
|
||||
if ((is_on || is_flatfile)
|
||||
&& active_db_backend
|
||||
&& connection_get_status() == JABBER_CONNECTED
|
||||
&& g_strcmp0(active_db_backend->name, is_on ? "sqlite" : "flatfile") != 0) {
|
||||
log_database_switch_backend(is_on ? "on" : "flatfile");
|
||||
}
|
||||
|
||||
if (is_on) {
|
||||
cons_show("Database logging enabled (sqlite).");
|
||||
} else if (is_off) {
|
||||
cons_show("Database logging disabled.");
|
||||
} else if (is_redact) {
|
||||
cons_show("Database logging set to redact (message bodies replaced with [REDACTED]).");
|
||||
} else if (is_flatfile) {
|
||||
auto_gchar gchar* ff_path = files_get_data_path(DIR_FLATLOG);
|
||||
cons_show("Database logging set to flat-file backend.");
|
||||
cons_show("Flatfile directory: %s", ff_path);
|
||||
}
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
@@ -6707,26 +6731,7 @@ cmd_privacy(ProfWin* window, const char* const command, gchar** args)
|
||||
gboolean
|
||||
cmd_logging(ProfWin* window, const char* const command, gchar** args)
|
||||
{
|
||||
if (args[0] == NULL) {
|
||||
cons_logging_setting();
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
if (strcmp(args[0], "chat") == 0 && args[1] != NULL) {
|
||||
_cmd_set_boolean_preference(args[1], "Chat logging", PREF_CHLOG);
|
||||
|
||||
// if set to off, disable history
|
||||
if (strcmp(args[1], "off") == 0) {
|
||||
prefs_set_boolean(PREF_HISTORY, FALSE);
|
||||
}
|
||||
|
||||
return TRUE;
|
||||
} else if (g_strcmp0(args[0], "group") == 0 && args[1] != NULL) {
|
||||
_cmd_set_boolean_preference(args[1], "Groupchat logging", PREF_GRLOG);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
cons_bad_cmd_usage(command);
|
||||
cons_show("The '/logging' command is deprecated, use '/history' instead.");
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
@@ -6873,9 +6878,20 @@ cmd_history(ProfWin* window, const char* const command, gchar** args)
|
||||
|
||||
_cmd_set_boolean_preference(args[0], "Chat history", PREF_HISTORY);
|
||||
|
||||
// if set to on, set chlog (/logging chat on)
|
||||
// /history off should stop persisting messages too, not just hide
|
||||
// existing history on chat-window open. /history on resumes
|
||||
// persistence (default backend) and parallel text chatlogs.
|
||||
if (strcmp(args[0], "on") == 0) {
|
||||
prefs_set_boolean(PREF_CHLOG, TRUE);
|
||||
auto_gchar gchar* dblog = prefs_get_string(PREF_DBLOG);
|
||||
if (g_strcmp0(dblog, "off") == 0) {
|
||||
prefs_set_string(PREF_DBLOG, "on");
|
||||
cons_show("Database logging re-enabled (sqlite).");
|
||||
}
|
||||
} else if (strcmp(args[0], "off") == 0) {
|
||||
prefs_set_boolean(PREF_CHLOG, FALSE);
|
||||
prefs_set_string(PREF_DBLOG, "off");
|
||||
cons_show("Database logging disabled.");
|
||||
}
|
||||
|
||||
return TRUE;
|
||||
@@ -7292,7 +7308,7 @@ cmd_pgp(ProfWin* window, const char* const command, gchar** args)
|
||||
prefs_set_string(PREF_PGP_LOG, "on");
|
||||
cons_show("PGP messages will be logged as plaintext.");
|
||||
if (!prefs_get_boolean(PREF_CHLOG)) {
|
||||
cons_show("Chat logging is currently disabled, use '/logging chat on' to enable.");
|
||||
cons_show("Chat logging is currently disabled, use '/history on' to enable.");
|
||||
}
|
||||
} else if (g_strcmp0(choice, "off") == 0) {
|
||||
prefs_set_string(PREF_PGP_LOG, "off");
|
||||
@@ -7301,7 +7317,7 @@ cmd_pgp(ProfWin* window, const char* const command, gchar** args)
|
||||
prefs_set_string(PREF_PGP_LOG, "redact");
|
||||
cons_show("PGP messages will be logged as '[redacted]'.");
|
||||
if (!prefs_get_boolean(PREF_CHLOG)) {
|
||||
cons_show("Chat logging is currently disabled, use '/logging chat on' to enable.");
|
||||
cons_show("Chat logging is currently disabled, use '/history on' to enable.");
|
||||
}
|
||||
} else {
|
||||
cons_bad_cmd_usage(command);
|
||||
@@ -7788,7 +7804,7 @@ cmd_ox_log(ProfWin* window, const char* const command, gchar** args)
|
||||
prefs_set_string(PREF_OX_LOG, "on");
|
||||
cons_show("OX messages will be logged as plaintext.");
|
||||
if (!prefs_get_boolean(PREF_CHLOG)) {
|
||||
cons_show("Chat logging is currently disabled, use '/logging chat on' to enable.");
|
||||
cons_show("Chat logging is currently disabled, use '/history on' to enable.");
|
||||
}
|
||||
} else if (g_strcmp0(choice, "off") == 0) {
|
||||
prefs_set_string(PREF_OX_LOG, "off");
|
||||
@@ -7797,7 +7813,7 @@ cmd_ox_log(ProfWin* window, const char* const command, gchar** args)
|
||||
prefs_set_string(PREF_OX_LOG, "redact");
|
||||
cons_show("OX messages will be logged as '[redacted]'.");
|
||||
if (!prefs_get_boolean(PREF_CHLOG)) {
|
||||
cons_show("Chat logging is currently disabled, use '/logging chat on' to enable.");
|
||||
cons_show("Chat logging is currently disabled, use '/history on' to enable.");
|
||||
}
|
||||
} else {
|
||||
cons_bad_cmd_usage(command);
|
||||
@@ -7837,7 +7853,7 @@ cmd_otr_log(ProfWin* window, const char* const command, gchar** args)
|
||||
prefs_set_string(PREF_OTR_LOG, "on");
|
||||
cons_show("OTR messages will be logged as plaintext.");
|
||||
if (!prefs_get_boolean(PREF_CHLOG)) {
|
||||
cons_show("Chat logging is currently disabled, use '/logging chat on' to enable.");
|
||||
cons_show("Chat logging is currently disabled, use '/history on' to enable.");
|
||||
}
|
||||
} else if (g_strcmp0(choice, "off") == 0) {
|
||||
prefs_set_string(PREF_OTR_LOG, "off");
|
||||
@@ -7846,7 +7862,7 @@ cmd_otr_log(ProfWin* window, const char* const command, gchar** args)
|
||||
prefs_set_string(PREF_OTR_LOG, "redact");
|
||||
cons_show("OTR messages will be logged as '[redacted]'.");
|
||||
if (!prefs_get_boolean(PREF_CHLOG)) {
|
||||
cons_show("Chat logging is currently disabled, use '/logging chat on' to enable.");
|
||||
cons_show("Chat logging is currently disabled, use '/history on' to enable.");
|
||||
}
|
||||
} else {
|
||||
cons_bad_cmd_usage(command);
|
||||
@@ -8516,7 +8532,8 @@ _cmd_execute_default(ProfWin* window, const char* inp)
|
||||
case WIN_AI:
|
||||
{
|
||||
ProfAiWin* aiwin = (ProfAiWin*)window;
|
||||
cl_ev_send_ai_msg(aiwin, inp, connection_create_stanza_id());
|
||||
auto_gchar gchar* stanza_id = connection_create_stanza_id();
|
||||
cl_ev_send_ai_msg(aiwin, inp, stanza_id);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
@@ -8782,7 +8799,7 @@ cmd_omemo_log(ProfWin* window, const char* const command, gchar** args)
|
||||
prefs_set_string(PREF_OMEMO_LOG, "on");
|
||||
cons_show("OMEMO messages will be logged as plaintext.");
|
||||
if (!prefs_get_boolean(PREF_CHLOG)) {
|
||||
cons_show("Chat logging is currently disabled, use '/logging chat on' to enable.");
|
||||
cons_show("Chat logging is currently disabled, use '/history on' to enable.");
|
||||
}
|
||||
} else if (g_strcmp0(choice, "off") == 0) {
|
||||
prefs_set_string(PREF_OMEMO_LOG, "off");
|
||||
@@ -8791,7 +8808,7 @@ cmd_omemo_log(ProfWin* window, const char* const command, gchar** args)
|
||||
prefs_set_string(PREF_OMEMO_LOG, "redact");
|
||||
cons_show("OMEMO messages will be logged as '[redacted]'.");
|
||||
if (!prefs_get_boolean(PREF_CHLOG)) {
|
||||
cons_show("Chat logging is currently disabled, use '/logging chat on' to enable.");
|
||||
cons_show("Chat logging is currently disabled, use '/history on' to enable.");
|
||||
}
|
||||
} else {
|
||||
cons_bad_cmd_usage(command);
|
||||
@@ -10817,7 +10834,6 @@ cmd_ai(ProfWin* window, const char* const command, gchar** args)
|
||||
|
||||
cons_show("Use '/ai start' to begin a chat (uses default provider/model).");
|
||||
cons_show("Use '/ai models <provider>' to fetch available models.");
|
||||
cons_show("Available models: https://models.litellm.ai/");
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
@@ -10916,21 +10932,14 @@ cmd_ai_start(ProfWin* window, const char* const command, gchar** args)
|
||||
if (g_strv_length(args) >= 2) {
|
||||
provider_name = args[1];
|
||||
}
|
||||
if (g_strv_length(args) >= 3) {
|
||||
model = args[2];
|
||||
|
||||
if (!provider_name) {
|
||||
cons_show_error("Please specify provider name.");
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
// 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
|
||||
if (g_strv_length(args) >= 3) {
|
||||
model = args[2];
|
||||
}
|
||||
|
||||
AIProvider* provider = ai_get_provider(provider_name);
|
||||
@@ -10981,6 +10990,9 @@ cmd_ai_start(ProfWin* window, const char* const command, gchar** args)
|
||||
}
|
||||
log_debug("[AI-CMD] wins_new_ai() returned successfully, window type: %d", ai_win->type);
|
||||
|
||||
// Release the reference held by cmd_ai_start, since the window now holds one
|
||||
ai_session_unref(session);
|
||||
|
||||
// Add welcome message to the AI window
|
||||
log_debug("[AI-CMD] Adding welcome messages...");
|
||||
win_println(ai_win, THEME_DEFAULT, "-", "AI Chat: %s/%s", provider_name, model);
|
||||
|
||||
@@ -2208,6 +2208,7 @@ _get_group(preference_t pref)
|
||||
case PREF_STATUSBAR_SHOW_NAME:
|
||||
case PREF_STATUSBAR_SHOW_NUMBER:
|
||||
case PREF_STATUSBAR_SHOW_READ:
|
||||
case PREF_STATUSBAR_SHOW_DBBACKEND:
|
||||
case PREF_STATUSBAR_SELF:
|
||||
case PREF_STATUSBAR_CHAT:
|
||||
case PREF_STATUSBAR_ROOM_TITLE:
|
||||
@@ -2535,6 +2536,8 @@ _get_key(preference_t pref)
|
||||
return "statusbar.show.number";
|
||||
case PREF_STATUSBAR_SHOW_READ:
|
||||
return "statusbar.show.read";
|
||||
case PREF_STATUSBAR_SHOW_DBBACKEND:
|
||||
return "statusbar.show.dbbackend";
|
||||
case PREF_STATUSBAR_SELF:
|
||||
return "statusbar.self";
|
||||
case PREF_STATUSBAR_CHAT:
|
||||
@@ -2628,6 +2631,7 @@ _get_default_boolean(preference_t pref)
|
||||
case PREF_ROOM_LIST_CACHE:
|
||||
case PREF_STATUSBAR_SHOW_NUMBER:
|
||||
case PREF_STATUSBAR_SHOW_READ:
|
||||
case PREF_STATUSBAR_SHOW_DBBACKEND:
|
||||
case PREF_REVEAL_OS:
|
||||
case PREF_CORRECTION_ALLOW:
|
||||
case PREF_RECEIPTS_SEND:
|
||||
|
||||
@@ -162,6 +162,7 @@ typedef enum {
|
||||
PREF_STATUSBAR_SHOW_NAME,
|
||||
PREF_STATUSBAR_SHOW_NUMBER,
|
||||
PREF_STATUSBAR_SHOW_READ,
|
||||
PREF_STATUSBAR_SHOW_DBBACKEND,
|
||||
PREF_STATUSBAR_SELF,
|
||||
PREF_STATUSBAR_CHAT,
|
||||
PREF_STATUSBAR_ROOM_TITLE,
|
||||
|
||||
@@ -61,6 +61,22 @@ 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)
|
||||
{
|
||||
|
||||
@@ -109,6 +109,9 @@ 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);
|
||||
|
||||
@@ -88,9 +88,8 @@ _ff_cache_line_ids(ff_contact_state_t* state, const char* line)
|
||||
return;
|
||||
|
||||
// Split metadata on unescaped '|'
|
||||
char* meta_str = g_strndup(bracket + 1, close - bracket - 1);
|
||||
auto_gchar gchar* meta_str = g_strndup(bracket + 1, close - bracket - 1);
|
||||
char** parts = ff_split_meta(meta_str);
|
||||
g_free(meta_str);
|
||||
|
||||
char* stanza_id = NULL;
|
||||
char* archive_id = NULL;
|
||||
@@ -141,15 +140,13 @@ _ff_maybe_index_line(ff_contact_state_t* state, const char* buf, off_t pos)
|
||||
if (!space)
|
||||
return;
|
||||
|
||||
char* ts_str = g_strndup(buf, space - buf);
|
||||
auto_gchar gchar* ts_str = g_strndup(buf, space - buf);
|
||||
GDateTime* dt = g_date_time_new_from_iso8601(ts_str, NULL);
|
||||
if (!dt) {
|
||||
log_warning("flatfile: unparsable timestamp in %s at offset %ld: %s",
|
||||
state->filepath, (long)pos, ts_str);
|
||||
g_free(ts_str);
|
||||
return;
|
||||
}
|
||||
g_free(ts_str);
|
||||
|
||||
if (state->n_entries >= state->cap_entries) {
|
||||
state->cap_entries = state->cap_entries ? state->cap_entries * 2 : 64;
|
||||
|
||||
@@ -211,9 +211,13 @@ _first_pass(FILE* fp, const char* basename,
|
||||
|
||||
if (pl->stanza_id && pl->stanza_id[0] != '\0') {
|
||||
if (g_hash_table_contains(seen_stanza_ids, pl->stanza_id)) {
|
||||
*issues = g_slist_prepend(*issues,
|
||||
_issue_new(INTEGRITY_WARNING, basename, lineno,
|
||||
g_strdup_printf("Duplicate stanza-id \"%s\"", pl->stanza_id)));
|
||||
// Older clients (Pidgin, Adium, even early Profanity)
|
||||
// sometimes reuse client-generated stanza-ids across
|
||||
// distinct messages, so a collision is not a real
|
||||
// integrity defect — log it for diagnostics but don't
|
||||
// surface it through /history verify.
|
||||
log_debug("flatfile verify: duplicate stanza-id \"%s\" at %s:%d",
|
||||
pl->stanza_id, basename, lineno);
|
||||
} else {
|
||||
g_hash_table_insert(seen_stanza_ids, g_strdup(pl->stanza_id), GINT_TO_POINTER(lineno));
|
||||
}
|
||||
@@ -302,7 +306,13 @@ _verify_contact_dir(const char* cdir_path, GSList** issues)
|
||||
return;
|
||||
}
|
||||
|
||||
const char* basename = "history.log";
|
||||
// Reconstruct the contact JID from the directory name so issue reports
|
||||
// identify which contact the problem belongs to (every contact has its
|
||||
// own history.log, so the bare filename is ambiguous).
|
||||
auto_gchar gchar* dir_name = g_path_get_basename(cdir_path);
|
||||
auto_gcharv gchar** parts = g_strsplit(dir_name, "_at_", -1);
|
||||
auto_gchar gchar* contact_jid = g_strjoinv("@", parts);
|
||||
auto_gchar gchar* basename = g_strdup_printf("%s/history.log", contact_jid);
|
||||
|
||||
_check_permissions(filepath, basename, issues);
|
||||
|
||||
|
||||
@@ -466,23 +466,36 @@ _sqlite_verify_integrity(const gchar* const contact_barejid)
|
||||
sqlite3_finalize(stmt);
|
||||
}
|
||||
|
||||
// Check timestamp ordering for a specific contact or all
|
||||
// Check timestamp ordering, restricted to consecutive rows belonging
|
||||
// to the same conversation pair so adjacency does not span unrelated
|
||||
// contacts. Adjacent rows by id from different conversations are
|
||||
// legitimately out-of-order and should not be reported.
|
||||
const Jid* myjid = connection_get_jid();
|
||||
if (myjid && myjid->barejid) {
|
||||
auto_sqlite char* query = NULL;
|
||||
if (contact_barejid) {
|
||||
query = sqlite3_mprintf(
|
||||
"SELECT A.`id`, A.`timestamp`, B.`id`, B.`timestamp` FROM `ChatLogs` A "
|
||||
"JOIN `ChatLogs` B ON B.`id` = A.`id` + 1 "
|
||||
"SELECT A.`id`, A.`timestamp`, A.`from_jid`, A.`to_jid`, B.`id`, B.`timestamp` "
|
||||
"FROM `ChatLogs` A "
|
||||
"JOIN `ChatLogs` B ON B.`from_jid` = A.`from_jid` AND B.`to_jid` = A.`to_jid` "
|
||||
" AND B.`id` > A.`id` "
|
||||
"WHERE A.`timestamp` > B.`timestamp` "
|
||||
"AND ((A.`from_jid` = %Q AND A.`to_jid` = %Q) OR (A.`from_jid` = %Q AND A.`to_jid` = %Q)) "
|
||||
"AND NOT EXISTS (SELECT 1 FROM `ChatLogs` C "
|
||||
" WHERE C.`from_jid` = A.`from_jid` AND C.`to_jid` = A.`to_jid` "
|
||||
" AND C.`id` > A.`id` AND C.`id` < B.`id`) "
|
||||
"LIMIT 50",
|
||||
contact_barejid, myjid->barejid, myjid->barejid, contact_barejid);
|
||||
} else {
|
||||
query = sqlite3_mprintf(
|
||||
"SELECT A.`id`, A.`timestamp`, B.`id`, B.`timestamp` FROM `ChatLogs` A "
|
||||
"JOIN `ChatLogs` B ON B.`id` = A.`id` + 1 "
|
||||
"SELECT A.`id`, A.`timestamp`, A.`from_jid`, A.`to_jid`, B.`id`, B.`timestamp` "
|
||||
"FROM `ChatLogs` A "
|
||||
"JOIN `ChatLogs` B ON B.`from_jid` = A.`from_jid` AND B.`to_jid` = A.`to_jid` "
|
||||
" AND B.`id` > A.`id` "
|
||||
"WHERE A.`timestamp` > B.`timestamp` "
|
||||
"AND NOT EXISTS (SELECT 1 FROM `ChatLogs` C "
|
||||
" WHERE C.`from_jid` = A.`from_jid` AND C.`to_jid` = A.`to_jid` "
|
||||
" AND C.`id` > A.`id` AND C.`id` < B.`id`) "
|
||||
"LIMIT 50");
|
||||
}
|
||||
|
||||
@@ -490,15 +503,19 @@ _sqlite_verify_integrity(const gchar* const contact_barejid)
|
||||
while (sqlite3_step(stmt) == SQLITE_ROW) {
|
||||
int id_a = sqlite3_column_int(stmt, 0);
|
||||
const char* ts_a = (const char*)sqlite3_column_text(stmt, 1);
|
||||
int id_b = sqlite3_column_int(stmt, 2);
|
||||
const char* ts_b = (const char*)sqlite3_column_text(stmt, 3);
|
||||
const char* from_a = (const char*)sqlite3_column_text(stmt, 2);
|
||||
const char* to_a = (const char*)sqlite3_column_text(stmt, 3);
|
||||
int id_b = sqlite3_column_int(stmt, 4);
|
||||
const char* ts_b = (const char*)sqlite3_column_text(stmt, 5);
|
||||
|
||||
integrity_issue_t* issue = g_malloc0(sizeof(integrity_issue_t));
|
||||
issue->level = INTEGRITY_WARNING;
|
||||
issue->file = g_strdup("chatlog.db");
|
||||
issue->file = g_strdup_printf("%s↔%s/chatlog.db",
|
||||
from_a ? from_a : "?", to_a ? to_a : "?");
|
||||
issue->line = id_a;
|
||||
issue->message = g_strdup_printf("Timestamp out of order: row %d (%s) > row %d (%s)",
|
||||
id_a, ts_a ? ts_a : "NULL", id_b, ts_b ? ts_b : "NULL");
|
||||
id_a, ts_a ? ts_a : "NULL",
|
||||
id_b, ts_b ? ts_b : "NULL");
|
||||
issues = g_slist_append(issues, issue);
|
||||
}
|
||||
sqlite3_finalize(stmt);
|
||||
@@ -506,7 +523,7 @@ _sqlite_verify_integrity(const gchar* const contact_barejid)
|
||||
|
||||
// Check broken LMC references
|
||||
auto_sqlite char* lmc_query = sqlite3_mprintf(
|
||||
"SELECT A.`id`, A.`replaces_db_id` FROM `ChatLogs` A "
|
||||
"SELECT A.`id`, A.`replaces_db_id`, A.`from_jid`, A.`to_jid` FROM `ChatLogs` A "
|
||||
"WHERE A.`replaces_db_id` IS NOT NULL "
|
||||
"AND NOT EXISTS (SELECT 1 FROM `ChatLogs` B WHERE B.`id` = A.`replaces_db_id`) "
|
||||
"LIMIT 50");
|
||||
@@ -515,10 +532,13 @@ _sqlite_verify_integrity(const gchar* const contact_barejid)
|
||||
while (sqlite3_step(stmt) == SQLITE_ROW) {
|
||||
int id = sqlite3_column_int(stmt, 0);
|
||||
int replaces_id = sqlite3_column_int(stmt, 1);
|
||||
const char* from = (const char*)sqlite3_column_text(stmt, 2);
|
||||
const char* to = (const char*)sqlite3_column_text(stmt, 3);
|
||||
|
||||
integrity_issue_t* issue = g_malloc0(sizeof(integrity_issue_t));
|
||||
issue->level = INTEGRITY_ERROR;
|
||||
issue->file = g_strdup("chatlog.db");
|
||||
issue->file = g_strdup_printf("%s↔%s/chatlog.db",
|
||||
from ? from : "?", to ? to : "?");
|
||||
issue->line = id;
|
||||
issue->message = g_strdup_printf("Broken LMC reference: row %d references non-existent row %d",
|
||||
id, replaces_id);
|
||||
|
||||
@@ -301,12 +301,6 @@ 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);
|
||||
|
||||
|
||||
@@ -1884,6 +1884,12 @@ cons_statusbar_setting(void)
|
||||
cons_show("Show tab with no actions (/statusbar) : OFF");
|
||||
}
|
||||
|
||||
if (prefs_get_boolean(PREF_STATUSBAR_SHOW_DBBACKEND)) {
|
||||
cons_show("Show database backend (/statusbar) : ON");
|
||||
} else {
|
||||
cons_show("Show database backend (/statusbar) : OFF");
|
||||
}
|
||||
|
||||
cons_show("Max tabs (/statusbar) : %d", prefs_get_statusbartabs());
|
||||
|
||||
gint pref_len = prefs_get_statusbartablen();
|
||||
@@ -1942,27 +1948,12 @@ cons_log_setting(void)
|
||||
cons_show("Log level (/log level) : %s", level);
|
||||
}
|
||||
|
||||
void
|
||||
cons_logging_setting(void)
|
||||
{
|
||||
if (prefs_get_boolean(PREF_CHLOG))
|
||||
cons_show("Chat logging (/logging chat) : ON");
|
||||
else
|
||||
cons_show("Chat logging (/logging chat) : OFF");
|
||||
|
||||
if (prefs_get_boolean(PREF_GRLOG))
|
||||
cons_show("Groupchat logging (/logging group) : ON");
|
||||
else
|
||||
cons_show("Groupchat logging (/logging group) : OFF");
|
||||
}
|
||||
|
||||
void
|
||||
cons_show_log_prefs(void)
|
||||
{
|
||||
cons_show("Logging preferences:");
|
||||
cons_show("");
|
||||
cons_log_setting();
|
||||
cons_logging_setting();
|
||||
|
||||
cons_alert(NULL);
|
||||
}
|
||||
@@ -2856,11 +2847,6 @@ cons_privacy_setting(void)
|
||||
{
|
||||
cons_show("Database logging : %s", prefs_get_string(PREF_DBLOG));
|
||||
|
||||
if (prefs_get_boolean(PREF_CHLOG)) {
|
||||
cons_show("Chat logging (/logging chat) : ON");
|
||||
} else {
|
||||
cons_show("Chat logging (/logging chat) : OFF");
|
||||
}
|
||||
if (prefs_get_boolean(PREF_HISTORY)) {
|
||||
cons_show("Chat history (/history) : ON");
|
||||
} else {
|
||||
|
||||
@@ -609,6 +609,8 @@ _status_bar_draw_maintext(int pos)
|
||||
static int
|
||||
_status_bar_draw_dbbackend(int pos)
|
||||
{
|
||||
if (!prefs_get_boolean(PREF_STATUSBAR_SHOW_DBBACKEND))
|
||||
return pos;
|
||||
if (!active_db_backend || !active_db_backend->name)
|
||||
return pos;
|
||||
|
||||
|
||||
@@ -46,6 +46,7 @@
|
||||
#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"
|
||||
@@ -252,6 +253,10 @@ _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();
|
||||
|
||||
@@ -330,7 +330,6 @@ void cons_history_setting(void);
|
||||
void cons_carbons_setting(void);
|
||||
void cons_receipts_setting(void);
|
||||
void cons_log_setting(void);
|
||||
void cons_logging_setting(void);
|
||||
void cons_autoaway_setting(void);
|
||||
void cons_reconnect_setting(void);
|
||||
void cons_autoping_setting(void);
|
||||
|
||||
@@ -848,6 +848,14 @@ 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
|
||||
@@ -1600,7 +1608,7 @@ win_print_outgoing(ProfWin* window, const char* show_char, const char* const id,
|
||||
GDateTime* timestamp = g_date_time_new_now_local();
|
||||
|
||||
const char* myjid = connection_get_fulljid();
|
||||
if (!_win_correct(window, message, id, replace_id, myjid)) {
|
||||
if (!prefs_get_boolean(PREF_CORRECTION_ALLOW) || !_win_correct(window, message, id, replace_id, myjid)) {
|
||||
auto_gchar gchar* outgoing_str = prefs_get_string(PREF_OUTGOING_STAMP);
|
||||
_win_printf(window, show_char, 0, timestamp, 0, THEME_TEXT_ME, outgoing_str, myjid, id, "%s", message);
|
||||
}
|
||||
@@ -1747,7 +1755,7 @@ win_print_outgoing_with_receipt(ProfWin* window, const char* show_char, const ch
|
||||
receipt->received = FALSE;
|
||||
|
||||
const char* myjid = connection_get_fulljid();
|
||||
if (_win_correct(window, message, id, replace_id, myjid)) {
|
||||
if (prefs_get_boolean(PREF_CORRECTION_ALLOW) && _win_correct(window, message, id, replace_id, myjid)) {
|
||||
free(receipt); // TODO: probably we should use this in _win_correct()
|
||||
} else {
|
||||
int y_start_pos = getcury(window->layout->win);
|
||||
@@ -1801,10 +1809,12 @@ 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++;
|
||||
return;
|
||||
// Skip buffer only when DB can replay the message on scroll-down.
|
||||
if (window->type == WIN_CHAT && log_database_can_recover_messages()) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (timestamp == NULL) {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
#include "glib.h"
|
||||
#include "prof_cmocka.h"
|
||||
#include "common.h"
|
||||
#include "ai/ai_client.h"
|
||||
@@ -703,7 +704,6 @@ test_ai_parse_models_openai_format(void** state)
|
||||
models = g_list_next(models);
|
||||
assert_string_equal("google/gemini-3-flash-preview", models->data);
|
||||
|
||||
ai_provider_unref(provider);
|
||||
ai_client_shutdown();
|
||||
}
|
||||
|
||||
@@ -776,7 +776,6 @@ test_ai_parse_models_perplexity_format(void** state)
|
||||
models = g_list_last(models);
|
||||
assert_string_equal("xai/grok-4.3", models->data);
|
||||
|
||||
ai_provider_unref(provider);
|
||||
ai_client_shutdown();
|
||||
}
|
||||
|
||||
@@ -801,7 +800,6 @@ test_ai_parse_models_array_format(void** state)
|
||||
models = g_list_next(models);
|
||||
assert_string_equal("model3", models->data);
|
||||
|
||||
ai_provider_unref(provider);
|
||||
ai_client_shutdown();
|
||||
}
|
||||
|
||||
@@ -821,7 +819,6 @@ test_ai_parse_models_empty_json(void** state)
|
||||
assert_null(provider->models);
|
||||
assert_int_equal(0, g_list_length(provider->models));
|
||||
|
||||
ai_provider_unref(provider);
|
||||
ai_client_shutdown();
|
||||
}
|
||||
|
||||
@@ -837,7 +834,6 @@ test_ai_parse_models_null_handling(void** state)
|
||||
ai_parse_models_from_json(provider, NULL);
|
||||
assert_null(provider->models);
|
||||
|
||||
ai_provider_unref(provider);
|
||||
ai_client_shutdown();
|
||||
}
|
||||
|
||||
@@ -861,7 +857,6 @@ test_ai_parse_models_escaped_quotes(void** state)
|
||||
GList* models = provider->models;
|
||||
assert_non_null(models);
|
||||
|
||||
ai_provider_unref(provider);
|
||||
ai_client_shutdown();
|
||||
}
|
||||
|
||||
@@ -885,7 +880,6 @@ test_ai_parse_models_with_whitespace(void** state)
|
||||
GList* models = provider->models;
|
||||
assert_string_equal("model-with-spaces", models->data);
|
||||
|
||||
ai_provider_unref(provider);
|
||||
ai_client_shutdown();
|
||||
}
|
||||
|
||||
@@ -1142,7 +1136,7 @@ test_ai_providers_find_cycles_through_many(void** state)
|
||||
ai_add_provider("alpine", "https://al.example/");
|
||||
|
||||
/* Three providers share prefix "alp": alpha, alphabet, alpine. */
|
||||
GHashTable* seen = g_hash_table_new(g_str_hash, g_str_equal);
|
||||
GHashTable* seen = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL);
|
||||
|
||||
for (int i = 0; i < 3; i++) {
|
||||
auto_gchar gchar* match = ai_providers_find("alp", FALSE, NULL);
|
||||
|
||||
@@ -69,7 +69,7 @@ cmd_otr_log_on_shows_warning_when_chlog_disabled(void** state)
|
||||
prefs_set_boolean(PREF_CHLOG, FALSE);
|
||||
|
||||
expect_cons_show("OTR messages will be logged as plaintext.");
|
||||
expect_cons_show("Chat logging is currently disabled, use '/logging chat on' to enable.");
|
||||
expect_cons_show("Chat logging is currently disabled, use '/history on' to enable.");
|
||||
|
||||
gboolean result = cmd_otr_log(NULL, CMD_OTR, args);
|
||||
assert_true(result);
|
||||
@@ -115,7 +115,7 @@ cmd_otr_log_redact_shows_warning_when_chlog_disabled(void** state)
|
||||
prefs_set_boolean(PREF_CHLOG, FALSE);
|
||||
|
||||
expect_cons_show("OTR messages will be logged as '[redacted]'.");
|
||||
expect_cons_show("Chat logging is currently disabled, use '/logging chat on' to enable.");
|
||||
expect_cons_show("Chat logging is currently disabled, use '/history on' to enable.");
|
||||
|
||||
gboolean result = cmd_otr_log(NULL, CMD_OTR, args);
|
||||
assert_true(result);
|
||||
|
||||
Reference in New Issue
Block a user