Compare commits
59 Commits
feat/no-db
...
fix/ai-fol
| Author | SHA1 | Date | |
|---|---|---|---|
|
43d557f2dc
|
|||
|
0847394d34
|
|||
|
5104de40d3
|
|||
|
659b9d77df
|
|||
|
de2d21a0ac
|
|||
|
65c6012677
|
|||
| befdd0ba10 | |||
| d676f3b087 | |||
|
30ad6e2a6e
|
|||
|
7cf82c99f0
|
|||
|
f93f7cdf2c
|
|||
|
a9f0153c0f
|
|||
|
5a074b280d
|
|||
|
fba6a040ee
|
|||
|
89a224b017
|
|||
|
b28b719d11
|
|||
|
b21d537e80
|
|||
|
2f1637ca8e
|
|||
|
277c82fb5d
|
|||
|
1ecaceb59f
|
|||
|
18fb7fa733
|
|||
|
a75a06b4a2
|
|||
|
9f4621883a
|
|||
|
6fe8c370df
|
|||
|
b1dbe714e8
|
|||
|
107f7778e2
|
|||
|
b634eed5af
|
|||
|
010b2062a4
|
|||
|
ecb07fd00c
|
|||
|
a898cb212d
|
|||
|
07d267b5bc
|
|||
|
acae543057
|
|||
|
731b55fa19
|
|||
|
e229ed1281
|
|||
|
f4221e27ac
|
|||
|
cdcc45b7c6
|
|||
|
f133d81a05
|
|||
|
a16237d16b
|
|||
|
cfe6ea46e2
|
|||
|
2fc7f3d672
|
|||
|
fe0a46da58
|
|||
|
cead417e78
|
|||
|
00f11eb704
|
|||
|
4913a3d5a4
|
|||
|
634fb7d7eb
|
|||
|
461c0c32dd
|
|||
|
002a6ed15b
|
|||
|
dc75f16221
|
|||
|
da0bf43d73
|
|||
|
9e1f9b666e
|
|||
|
93ad7379e2
|
|||
|
caa0b3ccba
|
|||
|
c9a5239117
|
|||
|
25e0459979
|
|||
|
bccd3ecded
|
|||
|
cff05ca802
|
|||
|
9c8ad57b59
|
|||
|
1f1770bd58
|
|||
|
e43e8378b0
|
2
.gitignore
vendored
2
.gitignore
vendored
@@ -17,8 +17,6 @@ compile_commands.json
|
||||
.project
|
||||
.settings/
|
||||
.vscode/
|
||||
.kilo/
|
||||
.roo/
|
||||
*.plist/
|
||||
|
||||
# autotools
|
||||
|
||||
@@ -1419,10 +1419,10 @@ _ai_request_thread(gpointer data)
|
||||
* list modifications). We must snapshot all fields atomically.
|
||||
* ===================================================================== */
|
||||
pthread_mutex_lock(&session->lock);
|
||||
auto_gchar gchar* local_provider_name = g_strdup(session->provider_name);
|
||||
gchar* local_provider_name = g_strdup(session->provider_name);
|
||||
AIProvider* local_provider = ai_provider_ref(session->provider);
|
||||
auto_gchar gchar* local_model = g_strdup(session->model);
|
||||
auto_gchar gchar* local_api_key = g_strdup(session->api_key ? session->api_key : "");
|
||||
gchar* local_model = g_strdup(session->model);
|
||||
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,8 +1434,6 @@ _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;
|
||||
}
|
||||
@@ -1446,8 +1444,6 @@ _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;
|
||||
}
|
||||
@@ -1496,7 +1492,6 @@ _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));
|
||||
@@ -1505,14 +1500,17 @@ _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.size, response.data ? response.data : "NULL");
|
||||
/* 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 {
|
||||
log_debug("[AI-THREAD] Raw API response (%zu bytes): %s", response.size, response_data);
|
||||
/* 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;
|
||||
auto_gchar gchar* content = ai_parse_response(response_data);
|
||||
if (content) {
|
||||
/* Add assistant response to history (under lock) */
|
||||
@@ -1533,9 +1531,12 @@ _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,7 +1132,6 @@ 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");
|
||||
@@ -1147,6 +1146,9 @@ 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");
|
||||
|
||||
@@ -4077,6 +4079,11 @@ _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|dbbackend",
|
||||
"/statusbar hide name|number|read|dbbackend",
|
||||
"/statusbar show name|number|read",
|
||||
"/statusbar hide name|number|read",
|
||||
"/statusbar maxtabs <value>",
|
||||
"/statusbar tablen <value>",
|
||||
"/statusbar tabmode default|dynamic|actlist",
|
||||
@@ -1308,7 +1308,6 @@ 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." },
|
||||
@@ -1660,14 +1659,23 @@ static const struct cmd_t command_defs[] = {
|
||||
},
|
||||
|
||||
{ CMD_PREAMBLE("/logging",
|
||||
parse_args, 0, 0, NULL)
|
||||
parse_args, 2, 3, &cons_logging_setting)
|
||||
CMD_MAINFUNC(cmd_logging)
|
||||
CMD_TAGS(
|
||||
CMD_TAG_CHAT)
|
||||
CMD_SYN(
|
||||
"/logging")
|
||||
"/logging chat|group on|off")
|
||||
CMD_DESC(
|
||||
"Deprecated. Use '/history' instead.")
|
||||
"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")
|
||||
},
|
||||
|
||||
{ CMD_PREAMBLE("/states",
|
||||
@@ -1880,16 +1888,15 @@ static const struct cmd_t command_defs[] = {
|
||||
"/history export [<jid>]",
|
||||
"/history import [<jid>]")
|
||||
CMD_DESC(
|
||||
"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. "
|
||||
"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. "
|
||||
"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 persistence and showing chat history." },
|
||||
{ "on|off", "Enable or disable 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." },
|
||||
@@ -2734,7 +2741,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 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." },
|
||||
{ "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." },
|
||||
{ "os on|off", "Choose whether to include the OS name if a user asks for software information (XEP-0092)." }
|
||||
)
|
||||
CMD_EXAMPLES(
|
||||
|
||||
@@ -6002,12 +6002,6 @@ 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;
|
||||
}
|
||||
@@ -6041,12 +6035,6 @@ 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;
|
||||
}
|
||||
@@ -6680,48 +6668,36 @@ cmd_privacy(ProfWin* window, const char* const command, gchar** args)
|
||||
}
|
||||
|
||||
if (g_strcmp0(args[0], "logging") == 0) {
|
||||
const gchar* arg = args[1];
|
||||
gchar* arg = args[1];
|
||||
if (arg == NULL) {
|
||||
cons_bad_cmd_usage(command);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
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) {
|
||||
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 {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -6731,7 +6707,26 @@ cmd_privacy(ProfWin* window, const char* const command, gchar** args)
|
||||
gboolean
|
||||
cmd_logging(ProfWin* window, const char* const command, gchar** args)
|
||||
{
|
||||
cons_show("The '/logging' command is deprecated, use '/history' instead.");
|
||||
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);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
@@ -6878,20 +6873,9 @@ cmd_history(ProfWin* window, const char* const command, gchar** args)
|
||||
|
||||
_cmd_set_boolean_preference(args[0], "Chat history", PREF_HISTORY);
|
||||
|
||||
// /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 set to on, set chlog (/logging chat on)
|
||||
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;
|
||||
@@ -7308,7 +7292,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 '/history on' to enable.");
|
||||
cons_show("Chat logging is currently disabled, use '/logging chat on' to enable.");
|
||||
}
|
||||
} else if (g_strcmp0(choice, "off") == 0) {
|
||||
prefs_set_string(PREF_PGP_LOG, "off");
|
||||
@@ -7317,7 +7301,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 '/history on' to enable.");
|
||||
cons_show("Chat logging is currently disabled, use '/logging chat on' to enable.");
|
||||
}
|
||||
} else {
|
||||
cons_bad_cmd_usage(command);
|
||||
@@ -7804,7 +7788,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 '/history on' to enable.");
|
||||
cons_show("Chat logging is currently disabled, use '/logging chat on' to enable.");
|
||||
}
|
||||
} else if (g_strcmp0(choice, "off") == 0) {
|
||||
prefs_set_string(PREF_OX_LOG, "off");
|
||||
@@ -7813,7 +7797,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 '/history on' to enable.");
|
||||
cons_show("Chat logging is currently disabled, use '/logging chat on' to enable.");
|
||||
}
|
||||
} else {
|
||||
cons_bad_cmd_usage(command);
|
||||
@@ -7853,7 +7837,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 '/history on' to enable.");
|
||||
cons_show("Chat logging is currently disabled, use '/logging chat on' to enable.");
|
||||
}
|
||||
} else if (g_strcmp0(choice, "off") == 0) {
|
||||
prefs_set_string(PREF_OTR_LOG, "off");
|
||||
@@ -7862,7 +7846,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 '/history on' to enable.");
|
||||
cons_show("Chat logging is currently disabled, use '/logging chat on' to enable.");
|
||||
}
|
||||
} else {
|
||||
cons_bad_cmd_usage(command);
|
||||
@@ -8532,8 +8516,7 @@ _cmd_execute_default(ProfWin* window, const char* inp)
|
||||
case WIN_AI:
|
||||
{
|
||||
ProfAiWin* aiwin = (ProfAiWin*)window;
|
||||
auto_gchar gchar* stanza_id = connection_create_stanza_id();
|
||||
cl_ev_send_ai_msg(aiwin, inp, stanza_id);
|
||||
cl_ev_send_ai_msg(aiwin, inp, connection_create_stanza_id());
|
||||
break;
|
||||
}
|
||||
default:
|
||||
@@ -8799,7 +8782,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 '/history on' to enable.");
|
||||
cons_show("Chat logging is currently disabled, use '/logging chat on' to enable.");
|
||||
}
|
||||
} else if (g_strcmp0(choice, "off") == 0) {
|
||||
prefs_set_string(PREF_OMEMO_LOG, "off");
|
||||
@@ -8808,7 +8791,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 '/history on' to enable.");
|
||||
cons_show("Chat logging is currently disabled, use '/logging chat on' to enable.");
|
||||
}
|
||||
} else {
|
||||
cons_bad_cmd_usage(command);
|
||||
@@ -10834,6 +10817,7 @@ 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;
|
||||
}
|
||||
|
||||
@@ -10997,9 +10981,6 @@ 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,7 +2208,6 @@ _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:
|
||||
@@ -2536,8 +2535,6 @@ _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:
|
||||
@@ -2631,7 +2628,6 @@ _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,7 +162,6 @@ 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,
|
||||
|
||||
@@ -88,8 +88,9 @@ _ff_cache_line_ids(ff_contact_state_t* state, const char* line)
|
||||
return;
|
||||
|
||||
// Split metadata on unescaped '|'
|
||||
auto_gchar gchar* meta_str = g_strndup(bracket + 1, close - bracket - 1);
|
||||
char* 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;
|
||||
@@ -140,13 +141,15 @@ _ff_maybe_index_line(ff_contact_state_t* state, const char* buf, off_t pos)
|
||||
if (!space)
|
||||
return;
|
||||
|
||||
auto_gchar gchar* ts_str = g_strndup(buf, space - buf);
|
||||
char* 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,13 +211,9 @@ _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)) {
|
||||
// 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);
|
||||
*issues = g_slist_prepend(*issues,
|
||||
_issue_new(INTEGRITY_WARNING, basename, lineno,
|
||||
g_strdup_printf("Duplicate stanza-id \"%s\"", pl->stanza_id)));
|
||||
} else {
|
||||
g_hash_table_insert(seen_stanza_ids, g_strdup(pl->stanza_id), GINT_TO_POINTER(lineno));
|
||||
}
|
||||
@@ -306,13 +302,7 @@ _verify_contact_dir(const char* cdir_path, GSList** issues)
|
||||
return;
|
||||
}
|
||||
|
||||
// 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);
|
||||
const char* basename = "history.log";
|
||||
|
||||
_check_permissions(filepath, basename, issues);
|
||||
|
||||
|
||||
@@ -466,36 +466,23 @@ _sqlite_verify_integrity(const gchar* const contact_barejid)
|
||||
sqlite3_finalize(stmt);
|
||||
}
|
||||
|
||||
// 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.
|
||||
// Check timestamp ordering for a specific contact or all
|
||||
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`, 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` "
|
||||
"SELECT A.`id`, A.`timestamp`, B.`id`, B.`timestamp` FROM `ChatLogs` A "
|
||||
"JOIN `ChatLogs` B ON B.`id` = A.`id` + 1 "
|
||||
"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`, 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` "
|
||||
"SELECT A.`id`, A.`timestamp`, B.`id`, B.`timestamp` FROM `ChatLogs` A "
|
||||
"JOIN `ChatLogs` B ON B.`id` = A.`id` + 1 "
|
||||
"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");
|
||||
}
|
||||
|
||||
@@ -503,19 +490,15 @@ _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);
|
||||
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);
|
||||
int id_b = sqlite3_column_int(stmt, 2);
|
||||
const char* ts_b = (const char*)sqlite3_column_text(stmt, 3);
|
||||
|
||||
integrity_issue_t* issue = g_malloc0(sizeof(integrity_issue_t));
|
||||
issue->level = INTEGRITY_WARNING;
|
||||
issue->file = g_strdup_printf("%s↔%s/chatlog.db",
|
||||
from_a ? from_a : "?", to_a ? to_a : "?");
|
||||
issue->file = g_strdup("chatlog.db");
|
||||
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);
|
||||
@@ -523,7 +506,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`, A.`from_jid`, A.`to_jid` FROM `ChatLogs` A "
|
||||
"SELECT A.`id`, A.`replaces_db_id` 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");
|
||||
@@ -532,13 +515,10 @@ _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_printf("%s↔%s/chatlog.db",
|
||||
from ? from : "?", to ? to : "?");
|
||||
issue->file = g_strdup("chatlog.db");
|
||||
issue->line = id;
|
||||
issue->message = g_strdup_printf("Broken LMC reference: row %d references non-existent row %d",
|
||||
id, replaces_id);
|
||||
|
||||
@@ -1884,12 +1884,6 @@ 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();
|
||||
@@ -1948,12 +1942,27 @@ 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);
|
||||
}
|
||||
@@ -2847,6 +2856,11 @@ 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,8 +609,6 @@ _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;
|
||||
|
||||
|
||||
@@ -330,6 +330,7 @@ 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);
|
||||
|
||||
@@ -1600,7 +1600,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 (!prefs_get_boolean(PREF_CORRECTION_ALLOW) || !_win_correct(window, message, id, replace_id, myjid)) {
|
||||
if (!_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 +1747,7 @@ win_print_outgoing_with_receipt(ProfWin* window, const char* show_char, const ch
|
||||
receipt->received = FALSE;
|
||||
|
||||
const char* myjid = connection_get_fulljid();
|
||||
if (prefs_get_boolean(PREF_CORRECTION_ALLOW) && _win_correct(window, message, id, replace_id, myjid)) {
|
||||
if (_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);
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
#include "glib.h"
|
||||
#include "prof_cmocka.h"
|
||||
#include "common.h"
|
||||
#include "ai/ai_client.h"
|
||||
@@ -704,6 +703,7 @@ 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,6 +776,7 @@ 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();
|
||||
}
|
||||
|
||||
@@ -800,6 +801,7 @@ 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();
|
||||
}
|
||||
|
||||
@@ -819,6 +821,7 @@ 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();
|
||||
}
|
||||
|
||||
@@ -834,6 +837,7 @@ 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();
|
||||
}
|
||||
|
||||
@@ -857,6 +861,7 @@ test_ai_parse_models_escaped_quotes(void** state)
|
||||
GList* models = provider->models;
|
||||
assert_non_null(models);
|
||||
|
||||
ai_provider_unref(provider);
|
||||
ai_client_shutdown();
|
||||
}
|
||||
|
||||
@@ -880,6 +885,7 @@ 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();
|
||||
}
|
||||
|
||||
@@ -1136,7 +1142,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_full(g_str_hash, g_str_equal, g_free, NULL);
|
||||
GHashTable* seen = g_hash_table_new(g_str_hash, g_str_equal);
|
||||
|
||||
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 '/history on' to enable.");
|
||||
expect_cons_show("Chat logging is currently disabled, use '/logging chat 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 '/history on' to enable.");
|
||||
expect_cons_show("Chat logging is currently disabled, use '/logging chat on' to enable.");
|
||||
|
||||
gboolean result = cmd_otr_log(NULL, CMD_OTR, args);
|
||||
assert_true(result);
|
||||
|
||||
Reference in New Issue
Block a user