Compare commits
3 Commits
fix/client
...
394df6d391
| Author | SHA1 | Date | |
|---|---|---|---|
|
394df6d391
|
|||
|
b282b12497
|
|||
|
|
562bd28cff
|
44
configure.ac
44
configure.ac
@@ -71,10 +71,6 @@ AC_ARG_ENABLE([omemo-qrcode],
|
||||
[AS_HELP_STRING([--enable-omemo-qrcode], [enable ability to display omemo qr code])])
|
||||
AC_ARG_ENABLE([coverage],
|
||||
[AS_HELP_STRING([--enable-coverage], [enable code coverage analysis])])
|
||||
AC_ARG_ENABLE([sanitizers],
|
||||
[AS_HELP_STRING([--enable-sanitizers], [enable AddressSanitizer + UndefinedBehaviorSanitizer + unsigned-integer-overflow])])
|
||||
AC_ARG_ENABLE([hardening],
|
||||
[AS_HELP_STRING([--enable-hardening], [enable extra hardening warnings (e.g. -Wnull-dereference) that may produce false positives under -O2])])
|
||||
|
||||
m4_include([m4/ax_valgrind_check.m4])
|
||||
AX_VALGRIND_DFLT([drd], [off])
|
||||
@@ -399,7 +395,7 @@ AC_SUBST([FORKPTY_LIB])
|
||||
|
||||
## Default parameters
|
||||
AM_CFLAGS="$AM_CFLAGS -Wall -Wextra -Wformat=2 -Wno-format-zero-length"
|
||||
AM_CFLAGS="$AM_CFLAGS -Wno-deprecated-declarations -Wno-unused-parameter -Wno-missing-field-initializers -Wno-cast-function-type"
|
||||
AM_CFLAGS="$AM_CFLAGS -Wno-deprecated-declarations -Wno-unused-parameter -Wno-missing-field-initializers -Wno-sign-compare -Wno-cast-function-type"
|
||||
AM_CFLAGS="$AM_CFLAGS -Wpointer-arith"
|
||||
AM_CFLAGS="$AM_CFLAGS -Wimplicit-function-declaration"
|
||||
AM_CFLAGS="$AM_CFLAGS -Wundef"
|
||||
@@ -410,8 +406,7 @@ AM_CFLAGS="$AM_CFLAGS -std=gnu99 -ggdb3"
|
||||
# GCC-specific warnings (not supported by clang) — test each one
|
||||
saved_CFLAGS="$CFLAGS"
|
||||
for _flag in -Wlogical-op -Wduplicated-cond -Wduplicated-branches \
|
||||
-Wstringop-overflow -Warray-bounds=2 -Walloc-zero \
|
||||
-Wsign-compare; do
|
||||
-Wstringop-overflow -Warray-bounds=2 -Walloc-zero; do
|
||||
AC_MSG_CHECKING([whether $CC supports $_flag])
|
||||
CFLAGS="$saved_CFLAGS $_flag -Werror"
|
||||
AC_COMPILE_IFELSE([AC_LANG_PROGRAM()],
|
||||
@@ -438,46 +433,11 @@ AS_IF([test "x$enable_coverage" = xyes],
|
||||
AM_LDFLAGS="$AM_LDFLAGS --coverage"
|
||||
AC_MSG_NOTICE([Code coverage analysis enabled])])
|
||||
|
||||
AS_IF([test "x$enable_sanitizers" = xyes],
|
||||
[SAN_FLAGS="-fsanitize=address,undefined"
|
||||
saved_CFLAGS="$CFLAGS"
|
||||
CFLAGS="$saved_CFLAGS -fsanitize=unsigned-integer-overflow -Werror"
|
||||
AC_MSG_CHECKING([whether $CC supports -fsanitize=unsigned-integer-overflow])
|
||||
AC_COMPILE_IFELSE([AC_LANG_PROGRAM()],
|
||||
[AC_MSG_RESULT([yes]); SAN_FLAGS="$SAN_FLAGS,unsigned-integer-overflow"],
|
||||
[AC_MSG_RESULT([no])])
|
||||
CFLAGS="$saved_CFLAGS"
|
||||
AM_CFLAGS="$AM_CFLAGS $SAN_FLAGS -fno-sanitize-recover=all -fno-omit-frame-pointer"
|
||||
AM_LDFLAGS="$AM_LDFLAGS -fsanitize=address,undefined"
|
||||
AC_MSG_NOTICE([Sanitizers enabled: $SAN_FLAGS])])
|
||||
|
||||
AS_IF([test "x$enable_hardening" = xyes],
|
||||
[saved_CFLAGS="$CFLAGS"
|
||||
for _flag in -Wnull-dereference; do
|
||||
AC_MSG_CHECKING([whether $CC supports $_flag])
|
||||
CFLAGS="$saved_CFLAGS $_flag -Werror"
|
||||
AC_COMPILE_IFELSE([AC_LANG_PROGRAM()],
|
||||
[AC_MSG_RESULT([yes]); AM_CFLAGS="$AM_CFLAGS $_flag"],
|
||||
[AC_MSG_RESULT([no])])
|
||||
done
|
||||
CFLAGS="$saved_CFLAGS"
|
||||
AC_MSG_NOTICE([Extra hardening warnings enabled])])
|
||||
|
||||
# Skip _FORTIFY_SOURCE=2 if env already set one (Pikaur/makepkg) or under coverage -O0 (FORTIFY needs -O>0).
|
||||
AS_IF([test "x$enable_coverage" != xyes],
|
||||
[AS_CASE([" ${CPPFLAGS} ${CFLAGS} "],
|
||||
[*_FORTIFY_SOURCE=*], [AC_MSG_NOTICE([_FORTIFY_SOURCE preset from environment, leaving as-is])],
|
||||
[AM_CFLAGS="$AM_CFLAGS -D_FORTIFY_SOURCE=2"])])
|
||||
|
||||
AS_IF([test "x$PACKAGE_STATUS" = xdevelopment],
|
||||
[AM_CFLAGS="$AM_CFLAGS -Wunused -Werror"])
|
||||
AS_IF([test "x$PLATFORM" = xosx],
|
||||
[AM_CFLAGS="$AM_CFLAGS -Qunused-arguments"])
|
||||
|
||||
# Treat glib/gio headers as system to keep their macros out of our -W* set.
|
||||
glib_CFLAGS=$(echo "$glib_CFLAGS" | sed 's/-I/-isystem /g')
|
||||
gio_CFLAGS=$(echo "$gio_CFLAGS" | sed 's/-I/-isystem /g')
|
||||
|
||||
AM_CFLAGS="$AM_CFLAGS $PTHREAD_CFLAGS $glib_CFLAGS $gio_CFLAGS $curl_CFLAGS ${SQLITE_CFLAGS}"
|
||||
AM_CFLAGS="$AM_CFLAGS $libnotify_CFLAGS ${GTK_CFLAGS} $python_CFLAGS"
|
||||
AM_CFLAGS="$AM_CFLAGS -DTHEMES_PATH=\"\\\"$THEMES_PATH\\\"\" -DICONS_PATH=\"\\\"$ICONS_PATH\\\"\" -DGLOBAL_PYTHON_PLUGINS_PATH=\"\\\"$GLOBAL_PYTHON_PLUGINS_PATH\\\"\" -DGLOBAL_C_PLUGINS_PATH=\"\\\"$GLOBAL_C_PLUGINS_PATH\\\"\""
|
||||
|
||||
@@ -186,22 +186,6 @@ ai_json_escape(const gchar* str)
|
||||
return g_string_free(result, FALSE);
|
||||
}
|
||||
|
||||
/* Keys emitted as fixed payload fields; custom settings must not duplicate them */
|
||||
static gboolean
|
||||
_is_reserved_payload_key(const gchar* key)
|
||||
{
|
||||
return g_strcmp0(key, "model") == 0 || g_strcmp0(key, "messages") == 0 || g_strcmp0(key, "stream") == 0;
|
||||
}
|
||||
|
||||
/* RFC 8259 scalars (number/true/false/null) may be emitted bare in a JSON payload */
|
||||
static gboolean
|
||||
_is_json_bare_token(const gchar* value)
|
||||
{
|
||||
if (g_strcmp0(value, "true") == 0 || g_strcmp0(value, "false") == 0 || g_strcmp0(value, "null") == 0)
|
||||
return TRUE;
|
||||
return g_regex_match_simple("^-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?$", value, 0, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a 4-hex-digit sequence at @p to a 16-bit code point.
|
||||
* Returns -1 on invalid hex.
|
||||
@@ -408,7 +392,6 @@ ai_provider_new(const gchar* name, const gchar* api_url)
|
||||
provider->models = NULL;
|
||||
provider->models_fresh = FALSE;
|
||||
provider->ref_count = 1;
|
||||
pthread_mutex_init(&provider->settings_lock, NULL);
|
||||
return provider;
|
||||
}
|
||||
|
||||
@@ -436,7 +419,6 @@ ai_provider_unref(AIProvider* provider)
|
||||
g_free(provider->project_id);
|
||||
g_free(provider->default_model);
|
||||
g_hash_table_destroy(provider->settings);
|
||||
pthread_mutex_destroy(&provider->settings_lock);
|
||||
|
||||
GList* curr = provider->models;
|
||||
while (curr) {
|
||||
@@ -747,44 +729,31 @@ ai_get_provider_default_model(const gchar* provider_name)
|
||||
return provider->default_model;
|
||||
}
|
||||
|
||||
gboolean
|
||||
void
|
||||
ai_set_provider_setting(const gchar* provider_name, const gchar* setting, const gchar* value)
|
||||
{
|
||||
if (!provider_name || !setting)
|
||||
return FALSE;
|
||||
|
||||
if (_is_reserved_payload_key(setting)) {
|
||||
log_warning("Setting '%s' for provider '%s' rejected: reserved payload key", setting, provider_name);
|
||||
return FALSE;
|
||||
}
|
||||
return;
|
||||
|
||||
AIProvider* provider = ai_get_provider(provider_name);
|
||||
if (!provider) {
|
||||
log_warning("Provider '%s' not found for setting '%s'", provider_name, setting);
|
||||
return FALSE;
|
||||
return;
|
||||
}
|
||||
|
||||
/* settings is iterated by the request thread; mutate under settings_lock */
|
||||
pthread_mutex_lock(&provider->settings_lock);
|
||||
if (!provider->settings) {
|
||||
provider->settings = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free);
|
||||
}
|
||||
|
||||
if (value) {
|
||||
g_hash_table_insert(provider->settings, g_strdup(setting), g_strdup(value));
|
||||
} else {
|
||||
g_hash_table_remove(provider->settings, setting);
|
||||
}
|
||||
pthread_mutex_unlock(&provider->settings_lock);
|
||||
|
||||
if (value) {
|
||||
prefs_ai_set_setting(provider_name, setting, value);
|
||||
log_info("Setting '%s' for provider '%s' set to: %s", setting, provider_name, value);
|
||||
} else {
|
||||
g_hash_table_remove(provider->settings, setting);
|
||||
prefs_ai_remove_setting(provider_name, setting);
|
||||
log_info("Setting '%s' removed for provider '%s'", setting, provider_name);
|
||||
}
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
gchar*
|
||||
@@ -797,10 +766,8 @@ ai_get_provider_setting(const gchar* provider_name, const gchar* setting)
|
||||
if (!provider || !provider->settings)
|
||||
return NULL;
|
||||
|
||||
pthread_mutex_lock(&provider->settings_lock);
|
||||
gchar* value = g_strdup(g_hash_table_lookup(provider->settings, setting));
|
||||
pthread_mutex_unlock(&provider->settings_lock);
|
||||
return value;
|
||||
gchar* value = g_hash_table_lookup(provider->settings, setting);
|
||||
return g_strdup(value);
|
||||
}
|
||||
|
||||
/* ========================================================================
|
||||
@@ -1468,19 +1435,13 @@ ai_session_switch(AISession* session, const gchar* provider_name,
|
||||
* This is the thread-safe variant used by _ai_request_thread() which holds
|
||||
* the session lock while calling it.
|
||||
*
|
||||
* Custom provider settings are merged into the payload as additional JSON
|
||||
* key-value pairs alongside "model" and "messages": scalar values
|
||||
* (number/true/false/null) are emitted bare, anything else as a JSON string;
|
||||
* reserved keys (model/messages/stream) are skipped.
|
||||
*
|
||||
* @param provider The AI provider (for custom settings)
|
||||
* @param model The model identifier
|
||||
* @param history GList of AIMessage* (must be held under session lock)
|
||||
* @param prompt The new user prompt to append
|
||||
* @return Newly allocated JSON string (caller must free)
|
||||
*/
|
||||
static gchar*
|
||||
_build_json_payload_from_list(AIProvider* provider, const gchar* model, GList* history, const gchar* prompt)
|
||||
_build_json_payload_from_list(const gchar* model, GList* history, const gchar* prompt)
|
||||
{
|
||||
GString* messages_json = g_string_new("");
|
||||
GList* curr = history;
|
||||
@@ -1507,50 +1468,11 @@ _build_json_payload_from_list(AIProvider* provider, const gchar* model, GList* h
|
||||
g_string_append_printf(messages_json, "{\"role\":\"user\",\"content\":\"%s\"}", escaped_prompt);
|
||||
|
||||
auto_gchar gchar* escaped_model = ai_json_escape(model);
|
||||
|
||||
/* Build custom settings JSON fragment; settings is mutated on the main
|
||||
* thread (/ai set custom), so iterate under settings_lock */
|
||||
GString* custom_json = g_string_new("");
|
||||
if (provider) {
|
||||
pthread_mutex_lock(&provider->settings_lock);
|
||||
if (provider->settings) {
|
||||
GHashTableIter iter;
|
||||
gpointer key, value;
|
||||
g_hash_table_iter_init(&iter, provider->settings);
|
||||
while (g_hash_table_iter_next(&iter, &key, &value)) {
|
||||
if (_is_reserved_payload_key((gchar*)key)) {
|
||||
continue; /* fixed payload fields stay authoritative */
|
||||
}
|
||||
auto_gchar gchar* escaped_key = ai_json_escape((gchar*)key);
|
||||
if (custom_json->len > 0) {
|
||||
g_string_append_c(custom_json, ',');
|
||||
}
|
||||
if (_is_json_bare_token((gchar*)value)) {
|
||||
g_string_append_printf(custom_json, "\"%s\": %s", escaped_key, (gchar*)value);
|
||||
} else {
|
||||
/* non-scalar values must be quoted, else the payload is invalid JSON */
|
||||
auto_gchar gchar* escaped_val = ai_json_escape((gchar*)value);
|
||||
g_string_append_printf(custom_json, "\"%s\": \"%s\"", escaped_key, escaped_val);
|
||||
}
|
||||
}
|
||||
}
|
||||
pthread_mutex_unlock(&provider->settings_lock);
|
||||
}
|
||||
|
||||
/* Assemble final payload: model, messages, custom settings, then fixed keys */
|
||||
gchar* json_payload;
|
||||
if (custom_json->len > 0) {
|
||||
json_payload = g_strdup_printf(
|
||||
"{\"model\":\"%s\",\"messages\":[%s],%s,\"stream\":false}",
|
||||
escaped_model, messages_json->str, custom_json->str);
|
||||
} else {
|
||||
json_payload = g_strdup_printf(
|
||||
"{\"model\":\"%s\",\"messages\":[%s],\"stream\":false}",
|
||||
escaped_model, messages_json->str);
|
||||
}
|
||||
gchar* json_payload = g_strdup_printf(
|
||||
"{\"model\":\"%s\",\"input\":[%s],\"stream\":false,\"store\":false}",
|
||||
escaped_model, messages_json->str);
|
||||
|
||||
g_string_free(messages_json, TRUE);
|
||||
g_string_free(custom_json, TRUE);
|
||||
return json_payload;
|
||||
}
|
||||
|
||||
@@ -1560,7 +1482,12 @@ ai_parse_response(const gchar* response_json)
|
||||
if (!response_json || strlen(response_json) == 0)
|
||||
return NULL;
|
||||
|
||||
/* OpenAI /v1/chat/completions: {"choices":[{"message":{"content":"..."}}]} */
|
||||
/* Perplexity /v1/agent: {"output":[{"content":[{"text":"...","type":"output_text"}]}]}
|
||||
* Legacy OpenAI: {"choices":[{"message":{"content":"..."}}]} */
|
||||
gchar* text = _extract_json_string(response_json, "text");
|
||||
if (text)
|
||||
return text;
|
||||
|
||||
return _extract_json_string(response_json, "content");
|
||||
}
|
||||
|
||||
@@ -1639,7 +1566,7 @@ _ai_request_thread(gpointer data)
|
||||
/* Build JSON payload from history + prompt (under lock), then add user message to history */
|
||||
log_debug("[AI-THREAD] Building JSON payload...");
|
||||
pthread_mutex_lock(&session->lock);
|
||||
auto_gchar gchar* json_payload = _build_json_payload_from_list(local_provider, local_model, session->history, prompt);
|
||||
auto_gchar gchar* json_payload = _build_json_payload_from_list(local_model, session->history, prompt);
|
||||
|
||||
/* Add user message to history now (it's in JSON but not yet in history) */
|
||||
AIMessage* user_msg = g_new0(AIMessage, 1);
|
||||
@@ -1660,7 +1587,7 @@ _ai_request_thread(gpointer data)
|
||||
|
||||
/* Build request URL */
|
||||
const gchar* api_url = local_provider->api_url;
|
||||
auto_gchar gchar* request_url = g_strdup_printf("%s%sv1/chat/completions", api_url, g_str_has_suffix(api_url, "/") ? "" : "/");
|
||||
auto_gchar gchar* request_url = g_strdup_printf("%s%sv1/responses", api_url, g_str_has_suffix(api_url, "/") ? "" : "/");
|
||||
log_debug("[AI-THREAD] API URL: %s", api_url);
|
||||
log_debug("[AI-THREAD] API Request URL: %s", request_url);
|
||||
log_debug("[AI-THREAD] Model: %s", local_model);
|
||||
|
||||
@@ -17,15 +17,14 @@ typedef struct ai_message_t
|
||||
*/
|
||||
typedef struct ai_provider_t
|
||||
{
|
||||
gchar* name; /* Provider name (e.g., "openai", "perplexity") */
|
||||
gchar* api_url; /* API endpoint URL */
|
||||
gchar* project_id; /* Optional project ID (for some providers) */
|
||||
gchar* default_model; /* Default model for this provider */
|
||||
pthread_mutex_t settings_lock; /* Protects settings (iterated by the request thread) */
|
||||
GHashTable* settings; /* Extensible per-provider settings (e.g., temperature=0.7) */
|
||||
GList* models; /* Cached models (gchar*) */
|
||||
gboolean models_fresh; /* Whether models cache is current */
|
||||
gint32 ref_count; /* Reference count (atomic) */
|
||||
gchar* name; /* Provider name (e.g., "openai", "perplexity") */
|
||||
gchar* api_url; /* API endpoint URL */
|
||||
gchar* project_id; /* Optional project ID (for some providers) */
|
||||
gchar* default_model; /* Default model for this provider */
|
||||
GHashTable* settings; /* Extensible per-provider settings (e.g., tools=enabled, search=disabled) */
|
||||
GList* models; /* Cached models (gchar*) */
|
||||
gboolean models_fresh; /* Whether models cache is current */
|
||||
gint32 ref_count; /* Reference count (atomic) */
|
||||
} AIProvider;
|
||||
|
||||
/**
|
||||
@@ -143,13 +142,11 @@ const gchar* ai_get_provider_default_model(const gchar* provider_name);
|
||||
|
||||
/**
|
||||
* Set a custom setting for a provider.
|
||||
* Reserved payload keys (model, messages, stream) are rejected.
|
||||
* @param provider_name The provider name
|
||||
* @param setting The setting key (e.g., "temperature")
|
||||
* @param value The setting value, or NULL to remove
|
||||
* @return TRUE on success, FALSE on unknown provider or reserved key
|
||||
* @param setting The setting key (e.g., "tools", "search")
|
||||
* @param value The setting value
|
||||
*/
|
||||
gboolean ai_set_provider_setting(const gchar* provider_name, const gchar* setting, const gchar* value);
|
||||
void ai_set_provider_setting(const gchar* provider_name, const gchar* setting, const gchar* value);
|
||||
|
||||
/**
|
||||
* Get a custom setting for a provider.
|
||||
@@ -187,9 +184,9 @@ gboolean ai_models_are_fresh(const gchar* provider_name);
|
||||
void ai_parse_models_from_json(AIProvider* provider, const gchar* json);
|
||||
|
||||
/**
|
||||
* Extract assistant content from an OpenAI chat-completions response body
|
||||
* ({"choices":[{"message":{"content":"..."}}]}). Decodes the \" and \n
|
||||
* escape sequences only.
|
||||
* Extract assistant content from an LLM response body. Tries Perplexity
|
||||
* /v1/responses "text" first, falls back to OpenAI "content". Decodes
|
||||
* the \" and \n escape sequences only.
|
||||
* @param response_json The JSON body from the provider
|
||||
* @return Newly allocated content string, or NULL if not found (caller frees)
|
||||
*/
|
||||
|
||||
@@ -47,7 +47,6 @@ static char* _notify_autocomplete(ProfWin* window, const char* const input, gboo
|
||||
static char* _theme_autocomplete(ProfWin* window, const char* const input, gboolean previous);
|
||||
static char* _spellcheck_autocomplete(ProfWin* window, const char* const input, gboolean previous);
|
||||
static char* _autoaway_autocomplete(ProfWin* window, const char* const input, gboolean previous);
|
||||
static char* _autoping_autocomplete(ProfWin* window, const char* const input, gboolean previous);
|
||||
static char* _autoconnect_autocomplete(ProfWin* window, const char* const input, gboolean previous);
|
||||
static char* _account_autocomplete(ProfWin* window, const char* const input, gboolean previous);
|
||||
static char* _who_autocomplete(ProfWin* window, const char* const input, gboolean previous);
|
||||
@@ -1057,7 +1056,6 @@ cmd_ac_init(void)
|
||||
|
||||
autocomplete_add(autoping_ac, "set");
|
||||
autocomplete_add(autoping_ac, "timeout");
|
||||
autocomplete_add(autoping_ac, "warning");
|
||||
|
||||
autocomplete_add(plugins_ac, "install");
|
||||
autocomplete_add(plugins_ac, "update");
|
||||
@@ -1409,7 +1407,6 @@ cmd_ac_init(void)
|
||||
g_hash_table_insert(ac_funcs, "/alias", _alias_autocomplete);
|
||||
g_hash_table_insert(ac_funcs, "/autoaway", _autoaway_autocomplete);
|
||||
g_hash_table_insert(ac_funcs, "/autoconnect", _autoconnect_autocomplete);
|
||||
g_hash_table_insert(ac_funcs, "/autoping", _autoping_autocomplete);
|
||||
g_hash_table_insert(ac_funcs, "/avatar", _avatar_autocomplete);
|
||||
g_hash_table_insert(ac_funcs, "/ban", _ban_autocomplete);
|
||||
g_hash_table_insert(ac_funcs, "/blocked", _blocked_autocomplete);
|
||||
@@ -1937,6 +1934,7 @@ _cmd_ac_complete_params(ProfWin* window, const char* const input, gboolean previ
|
||||
{ "/prefs", prefs_ac },
|
||||
{ "/disco", disco_ac },
|
||||
{ "/room", room_ac },
|
||||
{ "/autoping", autoping_ac },
|
||||
{ "/mainwin", winpos_ac },
|
||||
{ "/inputwin", winpos_ac },
|
||||
};
|
||||
@@ -4266,20 +4264,6 @@ _executable_autocomplete(ProfWin* window, const char* const input, gboolean prev
|
||||
return result;
|
||||
}
|
||||
|
||||
static char*
|
||||
_autoping_autocomplete(ProfWin* window, const char* const input, gboolean previous)
|
||||
{
|
||||
char* result = NULL;
|
||||
|
||||
result = autocomplete_param_with_ac(input, "/autoping", autoping_ac, TRUE, previous);
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
|
||||
result = autocomplete_param_with_func(input, "/autoping warning", prefs_autocomplete_boolean_choice, previous, NULL);
|
||||
return result;
|
||||
}
|
||||
|
||||
static char*
|
||||
_lastactivity_autocomplete(ProfWin* window, const char* const input, gboolean previous)
|
||||
{
|
||||
|
||||
@@ -1964,14 +1964,12 @@ static const struct cmd_t command_defs[] = {
|
||||
CMD_TAG_CONNECTION)
|
||||
CMD_SYN(
|
||||
"/autoping set <seconds>",
|
||||
"/autoping timeout <seconds>",
|
||||
"/autoping warning on|off")
|
||||
"/autoping timeout <seconds>")
|
||||
CMD_DESC(
|
||||
"Set the interval between sending ping requests to the server to ensure the connection is kept alive.")
|
||||
CMD_ARGS(
|
||||
{ "set <seconds>", "Number of seconds between sending pings, a value of 0 disables autoping." },
|
||||
{ "timeout <seconds>", "Seconds to wait for autoping responses, after which the connection is considered broken." },
|
||||
{ "warning on|off", "Enable or disable autoping availability warning." })
|
||||
{ "timeout <seconds>", "Seconds to wait for autoping responses, after which the connection is considered broken." })
|
||||
},
|
||||
|
||||
{ CMD_PREAMBLE("/ping",
|
||||
@@ -2640,8 +2638,7 @@ static const struct cmd_t command_defs[] = {
|
||||
CMD_DESC(
|
||||
"Spawn external editor to edit message. "
|
||||
"After editing the inputline may appear empty. Press enter to send the text anyways. "
|
||||
"Use /executable to set your favourite editor. "
|
||||
"Ctrl-Z in the editor aborts the session and returns to Profanity." )
|
||||
"Use /executable to set your favourite editor." )
|
||||
},
|
||||
|
||||
{ CMD_PREAMBLE("/correct-editor",
|
||||
@@ -2833,7 +2830,7 @@ static const struct cmd_t command_defs[] = {
|
||||
{ "set provider <name> <url>", "Add or update a provider with custom API endpoint" },
|
||||
{ "set token <provider> <token>", "Set API token for a provider (e.g., openai, perplexity)" },
|
||||
{ "set default-model <provider> <model>", "Set default model for a provider" },
|
||||
{ "set custom <provider> <setting> <value>", "Set provider-level setting (e.g., temperature, max_tokens)" },
|
||||
{ "set custom <provider> <setting> <value>", "Set provider-level setting (e.g., tools, search)" },
|
||||
{ "remove provider <name>", "Remove a custom provider" },
|
||||
{ "providers", "List configured providers with full details" },
|
||||
{ "start <provider> [<model>]", "Start new AI chat (space-separated, uses defaults if omitted)" },
|
||||
@@ -2846,7 +2843,7 @@ static const struct cmd_t command_defs[] = {
|
||||
"/ai set token perplexity pplx-xxx",
|
||||
"/ai set provider custom https://my-api.com/v1/chat/completions",
|
||||
"/ai set default-model perplexity sonar",
|
||||
"/ai set custom perplexity temperature 0.7",
|
||||
"/ai set custom perplexity tools enabled",
|
||||
"/ai remove provider custom",
|
||||
"/ai start",
|
||||
"/ai start perplexity",
|
||||
|
||||
@@ -6374,8 +6374,6 @@ cmd_autoping(ProfWin* window, const char* const command, gchar** args)
|
||||
cons_bad_cmd_usage(command);
|
||||
}
|
||||
|
||||
} else if (g_strcmp0(cmd, "warning") == 0) {
|
||||
_cmd_set_boolean_preference(value, "Autoping availability warning", PREF_AUTOPING_WARNING);
|
||||
} else {
|
||||
cons_bad_cmd_usage(command);
|
||||
}
|
||||
@@ -10065,14 +10063,8 @@ cmd_vcard_remove(ProfWin* window, const char* const command, gchar** args)
|
||||
}
|
||||
|
||||
if (args[1]) {
|
||||
int index;
|
||||
auto_gchar gchar* err_msg = NULL;
|
||||
if (!strtoi_range(args[1], &index, 0, INT_MAX, &err_msg)) {
|
||||
cons_show_error("%s", err_msg);
|
||||
return TRUE;
|
||||
}
|
||||
vcard_user_remove_element((unsigned int)index);
|
||||
cons_show("Removed element at index %d", index);
|
||||
vcard_user_remove_element(atoi(args[1]));
|
||||
cons_show("Removed element at index %d", atoi(args[1]));
|
||||
vcardwin_update();
|
||||
} else {
|
||||
cons_bad_cmd_usage(command);
|
||||
@@ -10787,11 +10779,8 @@ cmd_ai_set(ProfWin* window, const char* const command, gchar** args)
|
||||
cons_bad_cmd_usage(command);
|
||||
return TRUE;
|
||||
}
|
||||
if (ai_set_provider_setting(args[2], args[3], args[4])) {
|
||||
cons_show("Setting '%s' for provider '%s' set to: %s", args[3], args[2], args[4]);
|
||||
} else {
|
||||
cons_show_error("Cannot set '%s' for provider '%s': unknown provider or reserved key (model, messages, stream).", args[3], args[2]);
|
||||
}
|
||||
ai_set_provider_setting(args[2], args[3], args[4]);
|
||||
cons_show("Setting '%s' for provider '%s' set to: %s", args[3], args[2], args[4]);
|
||||
cons_show("");
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
40
src/common.c
40
src/common.c
@@ -294,7 +294,7 @@ str_replace(const char* string, const char* substr,
|
||||
wr = newstr;
|
||||
|
||||
while ((tok = strstr(head, substr))) {
|
||||
size_t l = g_diff_to_gsize(tok, head);
|
||||
size_t l = tok - head;
|
||||
memcpy(wr, head, l);
|
||||
wr += l;
|
||||
memcpy(wr, replacement, len_replacement);
|
||||
@@ -309,16 +309,6 @@ str_replace(const char* string, const char* substr,
|
||||
return newstr;
|
||||
}
|
||||
|
||||
gsize
|
||||
g_diff_to_gsize(const void* end, const void* start)
|
||||
{
|
||||
if (end < start) {
|
||||
log_error("g_diff_to_gsize: end < start (%p < %p)", end, start);
|
||||
return 0;
|
||||
}
|
||||
return (gsize)((const char*)end - (const char*)start);
|
||||
}
|
||||
|
||||
gboolean
|
||||
strtoi_range(const char* str, int* saveptr, int min, int max, gchar** err_msg)
|
||||
{
|
||||
@@ -369,9 +359,9 @@ string_matches_one_of(const char* what, const char* is, gboolean is_can_be_null,
|
||||
char errmsg[256] = { 0 };
|
||||
size_t sz = 0;
|
||||
int s = snprintf(errmsg, sizeof(errmsg) - sz, "%s must be one of:", what);
|
||||
if (s < 0 || (size_t)s + sz >= sizeof(errmsg))
|
||||
if (s < 0 || s + sz >= sizeof(errmsg))
|
||||
return ret;
|
||||
sz += (size_t)s;
|
||||
sz += s;
|
||||
|
||||
cur = first;
|
||||
va_start(ap, first);
|
||||
@@ -385,12 +375,12 @@ string_matches_one_of(const char* what, const char* is, gboolean is_can_be_null,
|
||||
errmsg[sz] = '\0';
|
||||
s = snprintf(errmsg + sz, sizeof(errmsg) - sz, " or '%s'.", cur);
|
||||
}
|
||||
if (s < 0 || (size_t)s + sz >= sizeof(errmsg)) {
|
||||
if (s < 0 || s + sz >= sizeof(errmsg)) {
|
||||
log_debug("Error message too long or some other error occurred (%d).", s);
|
||||
s = -1;
|
||||
break;
|
||||
}
|
||||
sz += (size_t)s;
|
||||
sz += s;
|
||||
cur = next;
|
||||
}
|
||||
va_end(ap);
|
||||
@@ -441,7 +431,7 @@ str_xml_sanitize(const char* const str)
|
||||
return NULL;
|
||||
}
|
||||
|
||||
GString* sanitized = g_string_new_len(NULL, (gssize)strlen(str));
|
||||
GString* sanitized = g_string_new_len(NULL, strlen(str));
|
||||
const char* curr = str;
|
||||
|
||||
while (*curr != '\0') {
|
||||
@@ -700,7 +690,7 @@ get_file_paths_recursive(const char* path, GSList** contents)
|
||||
}
|
||||
|
||||
gchar*
|
||||
get_random_string(size_t length)
|
||||
get_random_string(int length)
|
||||
{
|
||||
GRand* prng;
|
||||
gchar* rand;
|
||||
@@ -711,7 +701,7 @@ get_random_string(size_t length)
|
||||
|
||||
prng = g_rand_new();
|
||||
|
||||
for (size_t i = 0; i < length; i++) {
|
||||
for (int i = 0; i < length; i++) {
|
||||
rand[i] = alphabet[g_rand_int_range(prng, 0, endrange)];
|
||||
}
|
||||
g_rand_free(prng);
|
||||
@@ -758,17 +748,17 @@ call_external(gchar** argv)
|
||||
*
|
||||
* This function constructs an argument vector (argv) based on the provided template string, replacing placeholders ("%u" and "%p") with the provided URL and filename, respectively.
|
||||
*
|
||||
* @param template_fmt The template string with placeholders.
|
||||
* @param url The URL to replace "%u" (or NULL to skip).
|
||||
* @param filename The filename to replace "%p" (or NULL to skip).
|
||||
* @return The constructed argument vector (argv) as a null-terminated array of strings.
|
||||
* @param template The template string with placeholders.
|
||||
* @param url The URL to replace "%u" (or NULL to skip).
|
||||
* @param filename The filename to replace "%p" (or NULL to skip).
|
||||
* @return The constructed argument vector (argv) as a null-terminated array of strings.
|
||||
*
|
||||
* @note Remember to free the returned argument vector using `auto_gcharv` or `g_strfreev()`.
|
||||
*/
|
||||
gchar**
|
||||
format_call_external_argv(const char* template_fmt, const char* url, const char* filename)
|
||||
format_call_external_argv(const char* template, const char* url, const char* filename)
|
||||
{
|
||||
gchar** argv = g_strsplit(template_fmt, " ", 0);
|
||||
gchar** argv = g_strsplit(template, " ", 0);
|
||||
|
||||
guint num_args = 0;
|
||||
while (argv[num_args]) {
|
||||
@@ -777,7 +767,7 @@ format_call_external_argv(const char* template_fmt, const char* url, const char*
|
||||
argv[num_args] = g_strdup(url);
|
||||
} else if (0 == g_strcmp0(argv[num_args], "%p") && filename != NULL) {
|
||||
g_free(argv[num_args]);
|
||||
argv[num_args] = g_strdup(filename);
|
||||
argv[num_args] = strdup(filename);
|
||||
}
|
||||
num_args++;
|
||||
}
|
||||
|
||||
@@ -158,7 +158,6 @@ gboolean create_dir(const char* name);
|
||||
gboolean copy_file(const char* const src, const char* const target, const gboolean overwrite_existing);
|
||||
char* str_replace(const char* string, const char* substr, const char* replacement);
|
||||
gboolean strtoi_range(const char* str, int* saveptr, int min, int max, char** err_msg);
|
||||
gsize g_diff_to_gsize(const void* end, const void* start);
|
||||
int utf8_display_len(const char* const str);
|
||||
gchar* str_xml_sanitize(const char* const str);
|
||||
|
||||
@@ -179,10 +178,10 @@ int is_regular_file(const char* path);
|
||||
int is_dir(const char* path);
|
||||
void get_file_paths_recursive(const char* directory, GSList** contents);
|
||||
|
||||
gchar* get_random_string(size_t length);
|
||||
gchar* get_random_string(int length);
|
||||
|
||||
gboolean call_external(gchar** argv);
|
||||
gchar** format_call_external_argv(const char* template_fmt, const char* url, const char* filename);
|
||||
gchar** format_call_external_argv(const char* template, const char* url, const char* filename);
|
||||
|
||||
gchar* unique_filename_from_url(const char* url, const char* path);
|
||||
gchar* get_expanded_path(const char* path);
|
||||
|
||||
@@ -43,8 +43,8 @@ _sanitize_account_name(const char* const name)
|
||||
gchar* sanitized = g_strdup(name);
|
||||
gchar* p = sanitized;
|
||||
while (*p) {
|
||||
// GKeyFile group header forbids these characters.
|
||||
if (*p == '[' || *p == ']' || *p == '\n' || *p == '\r') {
|
||||
// GKeyFile special characters: [ ] = # \n \r
|
||||
if (*p == '[' || *p == ']' || *p == '=' || *p == '#' || *p == '\n' || *p == '\r') {
|
||||
*p = '_';
|
||||
}
|
||||
p++;
|
||||
@@ -522,7 +522,7 @@ accounts_set_script_start(const char* const account_name, const char* const valu
|
||||
void
|
||||
accounts_set_client(const char* const account_name, const char* const value)
|
||||
{
|
||||
_accounts_set_string_option(account_name, "client.account_name", value);
|
||||
_accounts_set_string_option(account_name, "client.name", value);
|
||||
}
|
||||
|
||||
void
|
||||
@@ -576,7 +576,7 @@ accounts_clear_script_start(const char* const account_name)
|
||||
void
|
||||
accounts_clear_client(const char* const account_name)
|
||||
{
|
||||
_accounts_clear_string_option(account_name, "client.account_name");
|
||||
_accounts_clear_string_option(account_name, "client.name");
|
||||
}
|
||||
|
||||
void
|
||||
|
||||
@@ -2290,7 +2290,6 @@ _get_group(preference_t pref)
|
||||
case PREF_TRAY:
|
||||
case PREF_TRAY_READ:
|
||||
case PREF_ADV_NOTIFY_DISCO_OR_VERSION:
|
||||
case PREF_AUTOPING_WARNING:
|
||||
return PREF_GROUP_NOTIFICATIONS;
|
||||
case PREF_DBLOG:
|
||||
case PREF_CHLOG:
|
||||
@@ -2647,8 +2646,6 @@ _get_key(preference_t pref)
|
||||
return "enabled";
|
||||
case PREF_SPELLCHECK_LANG:
|
||||
return "lang";
|
||||
case PREF_AUTOPING_WARNING:
|
||||
return "autoping.warning";
|
||||
default:
|
||||
return NULL;
|
||||
}
|
||||
@@ -2702,7 +2699,6 @@ _get_default_boolean(preference_t pref)
|
||||
case PREF_MOOD:
|
||||
case PREF_STROPHE_SM_ENABLED:
|
||||
case PREF_STROPHE_SM_RESEND:
|
||||
case PREF_AUTOPING_WARNING:
|
||||
return TRUE;
|
||||
case PREF_SPELLCHECK_ENABLE:
|
||||
case PREF_PGP_PUBKEY_AUTOIMPORT:
|
||||
|
||||
@@ -169,7 +169,6 @@ typedef enum {
|
||||
PREF_FORCE_ENCRYPTION_MODE,
|
||||
PREF_SPELLCHECK_ENABLE,
|
||||
PREF_SPELLCHECK_LANG,
|
||||
PREF_AUTOPING_WARNING
|
||||
} preference_t;
|
||||
|
||||
typedef struct prof_alias_t
|
||||
|
||||
@@ -282,7 +282,7 @@ _export_one_contact(const char* contact)
|
||||
gchar* key = _make_dedup_key(pl->stanza_id, pl->timestamp_str, pl->from_jid, pl->message);
|
||||
g_hash_table_add(seen_keys, key);
|
||||
}
|
||||
int existing_count = (int)g_slist_length(existing);
|
||||
int existing_count = g_slist_length(existing);
|
||||
|
||||
// 2. Query SQLite for this contact — get ALL messages via direct SQL
|
||||
GSList* sqlite_lines = db_sqlite_get_all_chat(contact);
|
||||
@@ -581,7 +581,7 @@ log_database_import_from_flatfile(const gchar* const contact_jid)
|
||||
// Wrap in a transaction for atomicity and performance
|
||||
int contact_imported = 0;
|
||||
int contact_skipped = 0;
|
||||
int total_lines = (int)g_slist_length(ff_lines);
|
||||
int total_lines = g_slist_length(ff_lines);
|
||||
|
||||
db_sqlite_begin_transaction();
|
||||
gboolean import_ok = TRUE;
|
||||
|
||||
@@ -88,7 +88,7 @@ _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, g_diff_to_gsize(close, bracket + 1));
|
||||
auto_gchar gchar* meta_str = g_strndup(bracket + 1, close - bracket - 1);
|
||||
char** parts = ff_split_meta(meta_str);
|
||||
|
||||
char* stanza_id = NULL;
|
||||
@@ -111,9 +111,9 @@ _ff_cache_line_ids(ff_contact_state_t* state, const char* line)
|
||||
const char* sender_start = close + 2;
|
||||
const char* colonspace = ff_find_unescaped_colonspace(sender_start);
|
||||
if (colonspace) {
|
||||
const char* slash = memchr(sender_start, '/', g_diff_to_gsize(colonspace, sender_start));
|
||||
const char* slash = memchr(sender_start, '/', colonspace - sender_start);
|
||||
const char* jid_end = slash ? slash : colonspace;
|
||||
from_jid = g_strndup(sender_start, g_diff_to_gsize(jid_end, sender_start));
|
||||
from_jid = g_strndup(sender_start, jid_end - sender_start);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -140,7 +140,7 @@ _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, g_diff_to_gsize(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",
|
||||
@@ -771,7 +771,7 @@ _flatfile_get_previous_chat(const gchar* const contact_barejid, const gchar* sta
|
||||
|
||||
// For "last N messages": back up from read_to using index
|
||||
if (!from_start) {
|
||||
size_t margin_entries = (MESSAGES_TO_RETRIEVE * 3) / FF_INDEX_STEP + 2;
|
||||
int margin_entries = (MESSAGES_TO_RETRIEVE * 3) / FF_INDEX_STEP + 2;
|
||||
|
||||
// Find index entry closest to (but before) read_to
|
||||
size_t entry_idx = 0;
|
||||
@@ -781,7 +781,7 @@ _flatfile_get_previous_chat(const gchar* const contact_barejid, const gchar* sta
|
||||
entry_idx = i;
|
||||
}
|
||||
|
||||
size_t start_entry = entry_idx > margin_entries
|
||||
size_t start_entry = entry_idx > (size_t)margin_entries
|
||||
? entry_idx - margin_entries
|
||||
: 0;
|
||||
off_t backed = state->entries[start_entry].byte_offset;
|
||||
|
||||
@@ -553,7 +553,7 @@ ff_split_meta(const char* meta)
|
||||
continue;
|
||||
}
|
||||
if (*p == '|' || *p == '\0') {
|
||||
g_ptr_array_add(arr, g_strndup(start, g_diff_to_gsize(p, start)));
|
||||
g_ptr_array_add(arr, g_strndup(start, p - start));
|
||||
if (*p == '\0')
|
||||
break;
|
||||
start = p + 1;
|
||||
@@ -710,7 +710,7 @@ ff_parse_line(const char* line)
|
||||
|
||||
if (bracket_start && first_space && first_space < bracket_start) {
|
||||
// Standard format with metadata: {timestamp} [{meta}] {sender}: {msg}
|
||||
result->timestamp_str = g_strndup(work, g_diff_to_gsize(first_space, work));
|
||||
result->timestamp_str = g_strndup(work, first_space - work);
|
||||
|
||||
// Parse metadata section [...]
|
||||
const char* bracket_end = ff_find_unescaped_char(bracket_start + 1, ']');
|
||||
@@ -721,7 +721,7 @@ ff_parse_line(const char* line)
|
||||
return NULL;
|
||||
}
|
||||
|
||||
auto_gchar gchar* meta_content = g_strndup(bracket_start + 1, g_diff_to_gsize(bracket_end, bracket_start + 1));
|
||||
auto_gchar gchar* meta_content = g_strndup(bracket_start + 1, bracket_end - bracket_start - 1);
|
||||
char** parts = ff_split_meta(meta_content);
|
||||
if (parts) {
|
||||
_ff_parse_meta_parts(parts, result);
|
||||
@@ -736,12 +736,12 @@ ff_parse_line(const char* line)
|
||||
// Find first *unescaped* ': ' which separates sender from message.
|
||||
const char* colon = ff_find_unescaped_colonspace(after_meta);
|
||||
if (colon) {
|
||||
auto_gchar gchar* raw_sender = g_strndup(after_meta, g_diff_to_gsize(colon, after_meta));
|
||||
auto_gchar gchar* raw_sender = g_strndup(after_meta, colon - after_meta);
|
||||
|
||||
// Split sender into jid/resource, then unescape resource
|
||||
char* slash = strchr(raw_sender, '/');
|
||||
if (slash) {
|
||||
result->from_jid = g_strndup(raw_sender, g_diff_to_gsize(slash, raw_sender));
|
||||
result->from_jid = g_strndup(raw_sender, slash - raw_sender);
|
||||
result->from_resource = ff_unescape_sender_resource(slash + 1);
|
||||
} else {
|
||||
result->from_jid = g_strdup(raw_sender);
|
||||
@@ -755,7 +755,7 @@ ff_parse_line(const char* line)
|
||||
}
|
||||
} else if (first_space) {
|
||||
// Legacy/simple format without metadata: {timestamp} - {sender}: {msg}
|
||||
result->timestamp_str = g_strndup(work, g_diff_to_gsize(first_space, work));
|
||||
result->timestamp_str = g_strndup(work, first_space - work);
|
||||
result->type = g_strdup("chat");
|
||||
result->enc = g_strdup("none");
|
||||
|
||||
@@ -767,10 +767,10 @@ ff_parse_line(const char* line)
|
||||
|
||||
char* colon = strstr(rest, ": ");
|
||||
if (colon) {
|
||||
char* sender = g_strndup(rest, g_diff_to_gsize(colon, rest));
|
||||
char* sender = g_strndup(rest, colon - rest);
|
||||
char* slash = strchr(sender, '/');
|
||||
if (slash) {
|
||||
result->from_jid = g_strndup(sender, g_diff_to_gsize(slash, sender));
|
||||
result->from_jid = g_strndup(sender, slash - sender);
|
||||
result->from_resource = g_strdup(slash + 1);
|
||||
} else {
|
||||
result->from_jid = g_strdup(sender);
|
||||
|
||||
@@ -10,8 +10,6 @@
|
||||
#ifndef EVENT_COMMON_H
|
||||
#define EVENT_COMMON_H
|
||||
|
||||
#include "glib.h"
|
||||
|
||||
void ev_disconnect_cleanup(void);
|
||||
void ev_inc_connection_counter(void);
|
||||
void ev_reset_connection_counter(void);
|
||||
|
||||
6
src/gitversion.h.in
Normal file
6
src/gitversion.h.in
Normal file
@@ -0,0 +1,6 @@
|
||||
#ifndef PROF_GIT_BRANCH
|
||||
#define PROF_GIT_BRANCH @PROF_GIT_BRANCH@
|
||||
#endif
|
||||
#ifndef PROF_GIT_REVISION
|
||||
#define PROF_GIT_REVISION @PROF_GIT_REVISION@
|
||||
#endif
|
||||
@@ -260,12 +260,7 @@ omemo_on_connect(ProfAccount* account)
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_omemo_finalize_identity_load(account)) {
|
||||
omemo_ctx.loaded = FALSE;
|
||||
log_error("[OMEMO] failed to load OMEMO state from disk for %s", account->jid);
|
||||
cons_show_error("OMEMO: could not load encryption state from disk; OMEMO will be unavailable this session.");
|
||||
return;
|
||||
}
|
||||
_omemo_finalize_identity_load(account);
|
||||
|
||||
wins_omemo_trust_changed(NULL);
|
||||
}
|
||||
@@ -1283,12 +1278,8 @@ omemo_is_trusted_identity(const char* const jid, const char* const fingerprint)
|
||||
.device_id = GPOINTER_TO_UINT(device_id),
|
||||
};
|
||||
|
||||
size_t fingerprint_len = 0;
|
||||
size_t fingerprint_len;
|
||||
unsigned char* fingerprint_raw = _omemo_fingerprint_decode(fingerprint, &fingerprint_len);
|
||||
if (!fingerprint_raw) {
|
||||
log_error("[OMEMO] omemo_is_trusted_identity: failed to decode fingerprint for %s", jid);
|
||||
return FALSE;
|
||||
}
|
||||
unsigned char djb_type[] = { '\x05' };
|
||||
signal_buffer* buffer = signal_buffer_create(djb_type, 1);
|
||||
buffer = signal_buffer_append(buffer, fingerprint_raw, fingerprint_len);
|
||||
@@ -1472,10 +1463,6 @@ _omemo_fingerprint(ec_public_key* identity, gboolean formatted)
|
||||
static unsigned char*
|
||||
_omemo_fingerprint_decode(const char* const fingerprint, size_t* len)
|
||||
{
|
||||
if (!fingerprint) {
|
||||
*len = 0;
|
||||
return NULL;
|
||||
}
|
||||
unsigned char* output = malloc(strlen(fingerprint) / 2 + 1);
|
||||
if (!output) {
|
||||
*len = 0;
|
||||
@@ -1542,11 +1529,6 @@ omemo_trust(const char* const jid, const char* const fingerprint_formatted)
|
||||
};
|
||||
|
||||
unsigned char* fingerprint_raw = _omemo_fingerprint_decode(fingerprint_formatted, &len);
|
||||
if (!fingerprint_raw) {
|
||||
log_error("[OMEMO] omemo_trust: failed to decode fingerprint for %s", jid);
|
||||
cons_show_error("Failed to trust device: could not decode fingerprint.");
|
||||
return;
|
||||
}
|
||||
unsigned char djb_type[] = { '\x05' };
|
||||
signal_buffer* buffer = signal_buffer_create(djb_type, 1);
|
||||
buffer = signal_buffer_append(buffer, fingerprint_raw, len);
|
||||
@@ -1564,10 +1546,8 @@ omemo_untrust(const char* const jid, const char* const fingerprint_formatted)
|
||||
{
|
||||
size_t len;
|
||||
unsigned char* identity = _omemo_fingerprint_decode(fingerprint_formatted, &len);
|
||||
if (!identity) {
|
||||
log_error("[OMEMO] omemo_untrust: failed to decode fingerprint for %s", jid);
|
||||
if (!identity)
|
||||
return;
|
||||
}
|
||||
|
||||
GHashTableIter iter;
|
||||
gpointer key, value;
|
||||
|
||||
@@ -278,8 +278,7 @@ otr_on_message_recv(const char* const barejid, const char* const resource, const
|
||||
if (strstr(message, OTRL_MESSAGE_TAG_V2) && strstr(message, OTRL_MESSAGE_TAG_V1)) {
|
||||
tag_length = 32;
|
||||
}
|
||||
// Move the rest of the message (with NUL) over the tag.
|
||||
memmove(whitespace_base, whitespace_base + tag_length, strlen(whitespace_base + tag_length) + 1);
|
||||
memmove(whitespace_base, whitespace_base + tag_length, tag_length);
|
||||
char* otr_query_message = otr_start_query();
|
||||
cons_show("OTR Whitespace pattern detected. Attempting to start OTR session…");
|
||||
free(message_send_chat_otr(barejid, otr_query_message, FALSE, NULL));
|
||||
|
||||
@@ -519,15 +519,13 @@ p_gpg_sign(const gchar* const str, const gchar* const fp)
|
||||
}
|
||||
|
||||
gchar*
|
||||
p_gpg_encrypt(const gchar* const barejid, const gchar* const message, const gchar* const fp, gchar** err)
|
||||
p_gpg_encrypt(const gchar* const barejid, const gchar* const message, const gchar* const fp)
|
||||
{
|
||||
ProfPGPPubKeyId* pubkeyid = g_hash_table_lookup(pubkeys, barejid);
|
||||
if (!pubkeyid) {
|
||||
*err = g_strdup("No PGP key found for recipient");
|
||||
return NULL;
|
||||
}
|
||||
if (!pubkeyid->id) {
|
||||
*err = g_strdup("No key ID found for recipient");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
@@ -540,7 +538,6 @@ p_gpg_encrypt(const gchar* const barejid, const gchar* const message, const gcha
|
||||
gpgme_ctx_t ctx;
|
||||
gpgme_error_t error = gpgme_new(&ctx);
|
||||
if (error) {
|
||||
*err = g_strdup_printf("Failed to create GPGME context: %s %s", gpgme_strsource(error), gpgme_strerror(error));
|
||||
log_error("GPG: Failed to create gpgme context. %s %s", gpgme_strsource(error), gpgme_strerror(error));
|
||||
return NULL;
|
||||
}
|
||||
@@ -548,7 +545,6 @@ p_gpg_encrypt(const gchar* const barejid, const gchar* const message, const gcha
|
||||
gpgme_key_t receiver_key;
|
||||
error = gpgme_get_key(ctx, pubkeyid->id, &receiver_key, 0);
|
||||
if (error || receiver_key == NULL) {
|
||||
*err = g_strdup_printf("Failed to get receiver key '%s': %s %s", pubkeyid->id, gpgme_strsource(error), gpgme_strerror(error));
|
||||
log_error("GPG: Failed to get receiver_key. %s %s", gpgme_strsource(error), gpgme_strerror(error));
|
||||
gpgme_release(ctx);
|
||||
return NULL;
|
||||
@@ -558,10 +554,8 @@ p_gpg_encrypt(const gchar* const barejid, const gchar* const message, const gcha
|
||||
gpgme_key_t sender_key = NULL;
|
||||
error = gpgme_get_key(ctx, fp, &sender_key, 0);
|
||||
if (error || sender_key == NULL) {
|
||||
*err = g_strdup_printf("Failed to get sender key '%s': %s %s", fp, gpgme_strsource(error), gpgme_strerror(error));
|
||||
log_error("GPG: Failed to get sender_key. %s %s", gpgme_strsource(error), gpgme_strerror(error));
|
||||
gpgme_release(ctx);
|
||||
gpgme_key_unref(receiver_key);
|
||||
return NULL;
|
||||
}
|
||||
keys[1] = sender_key;
|
||||
@@ -580,8 +574,7 @@ p_gpg_encrypt(const gchar* const barejid, const gchar* const message, const gcha
|
||||
gpgme_key_unref(sender_key);
|
||||
|
||||
if (error) {
|
||||
*err = g_strdup_printf("Encryption failed: (%s) %s", gpgme_strsource(error), gpgme_strerror(error));
|
||||
log_error("GPG: Failed to encrypt message. (%s) %s", gpgme_strsource(error), gpgme_strerror(error));
|
||||
log_error("GPG: Failed to encrypt message. %s %s", gpgme_strsource(error), gpgme_strerror(error));
|
||||
return NULL;
|
||||
}
|
||||
|
||||
|
||||
@@ -10,8 +10,6 @@
|
||||
#ifndef PGP_GPG_H
|
||||
#define PGP_GPG_H
|
||||
|
||||
#include "glib.h"
|
||||
|
||||
typedef struct pgp_key_t
|
||||
{
|
||||
gchar* id;
|
||||
@@ -42,7 +40,7 @@ gboolean p_gpg_available(const gchar* const barejid);
|
||||
const gchar* p_gpg_libver(void);
|
||||
gchar* p_gpg_sign(const gchar* const str, const gchar* const fp);
|
||||
void p_gpg_verify(const gchar* const barejid, const gchar* const sign);
|
||||
gchar* p_gpg_encrypt(const gchar* const barejid, const gchar* const message, const gchar* const fp, gchar** err);
|
||||
gchar* p_gpg_encrypt(const gchar* const barejid, const gchar* const message, const gchar* const fp);
|
||||
gchar* p_gpg_decrypt(const gchar* const cipher);
|
||||
void p_gpg_free_decrypted(gchar* decrypted);
|
||||
gchar* p_gpg_autocomplete_key(const gchar* const search_str, gboolean previous, void* context);
|
||||
|
||||
@@ -70,38 +70,6 @@ static void _connect_default(const char* const account);
|
||||
pthread_mutex_t lock;
|
||||
static gboolean force_quit = FALSE;
|
||||
|
||||
static volatile sig_atomic_t suspend_requested = FALSE;
|
||||
static volatile sig_atomic_t cont_requested = FALSE;
|
||||
|
||||
static void
|
||||
_handle_suspend_signals(void)
|
||||
{
|
||||
if (cont_requested) {
|
||||
cont_requested = FALSE;
|
||||
if (!editor_is_active()) {
|
||||
clearok(stdscr, TRUE);
|
||||
ui_resize();
|
||||
}
|
||||
}
|
||||
|
||||
if (editor_check_stopped()) {
|
||||
log_info("[Editor] stopped; aborting back to profanity");
|
||||
suspend_requested = FALSE;
|
||||
editor_emergency_kill();
|
||||
} else if (suspend_requested) {
|
||||
suspend_requested = FALSE;
|
||||
if (editor_is_active()) {
|
||||
log_info("[Editor] SIGTSTP from vim group; deferring to next poll");
|
||||
} else {
|
||||
log_info("[Suspend] dropping to shell");
|
||||
if (!isendwin()) {
|
||||
endwin();
|
||||
}
|
||||
kill(0, SIGSTOP);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
prof_run(gchar* log_level, gchar* account_name, gchar* config_file, gchar* log_file, gchar* theme_name, gchar** commands)
|
||||
{
|
||||
@@ -125,7 +93,6 @@ prof_run(gchar* log_level, gchar* account_name, gchar* config_file, gchar* log_f
|
||||
int waittime = 0;
|
||||
while (cont && !force_quit) {
|
||||
log_stderr_handler();
|
||||
_handle_suspend_signals();
|
||||
session_check_autoaway();
|
||||
|
||||
line = commands ? *commands : inp_readline();
|
||||
@@ -138,7 +105,7 @@ prof_run(gchar* log_level, gchar* account_name, gchar* config_file, gchar* log_f
|
||||
* so we can be sure there's runtime left after executing
|
||||
* the last command.
|
||||
*/
|
||||
min_runtime += (unsigned int)waittime;
|
||||
min_runtime += waittime;
|
||||
} else {
|
||||
log_error("%s", err_msg);
|
||||
g_free(err_msg);
|
||||
@@ -215,18 +182,6 @@ sigterm_handler(int sig)
|
||||
force_quit = TRUE;
|
||||
}
|
||||
|
||||
static void
|
||||
sigtstp_handler(int sig)
|
||||
{
|
||||
suspend_requested = TRUE;
|
||||
}
|
||||
|
||||
static void
|
||||
sigcont_handler(int sig)
|
||||
{
|
||||
cont_requested = TRUE;
|
||||
}
|
||||
|
||||
static void
|
||||
_init(char* log_level, char* config_file, char* log_file, char* theme_name)
|
||||
{
|
||||
@@ -234,8 +189,7 @@ _init(char* log_level, char* config_file, char* log_file, char* theme_name)
|
||||
// ignore SIGPIPE
|
||||
signal(SIGPIPE, SIG_IGN);
|
||||
signal(SIGINT, SIG_IGN);
|
||||
signal(SIGTSTP, sigtstp_handler);
|
||||
signal(SIGCONT, sigcont_handler);
|
||||
signal(SIGTSTP, SIG_IGN);
|
||||
signal(SIGWINCH, ui_sigwinch_handler);
|
||||
signal(SIGTERM, sigterm_handler);
|
||||
signal(SIGHUP, sigterm_handler);
|
||||
|
||||
@@ -10,10 +10,8 @@
|
||||
#ifndef BOOKMARK_IGNORE_H
|
||||
#define BOOKMARK_IGNORE_H
|
||||
|
||||
#include "xmpp/xmpp.h"
|
||||
|
||||
void bookmark_ignore_on_connect(const char* const barejid);
|
||||
void bookmark_ignore_on_disconnect();
|
||||
void bookmark_ignore_on_disconnect(void);
|
||||
gboolean bookmark_ignored(Bookmark* bookmark);
|
||||
gchar** bookmark_ignore_list(gsize* len);
|
||||
void bookmark_ignore_add(const char* const barejid);
|
||||
|
||||
@@ -32,15 +32,9 @@ typedef struct EditorContext
|
||||
void* user_data;
|
||||
} EditorContext;
|
||||
|
||||
static gboolean editor_active = FALSE; // re-entrancy guard
|
||||
static pid_t editor_pid = 0;
|
||||
|
||||
static void
|
||||
_editor_exit_cb(GPid pid, gint status, gpointer data)
|
||||
{
|
||||
editor_active = FALSE;
|
||||
editor_pid = 0;
|
||||
|
||||
EditorContext* ctx = data;
|
||||
gchar* contents = NULL;
|
||||
GError* error = NULL;
|
||||
@@ -58,10 +52,8 @@ _editor_exit_cb(GPid pid, gint status, gpointer data)
|
||||
cons_show_error("Could not read edited content: %s", error->message);
|
||||
g_error_free(error);
|
||||
}
|
||||
} else if (WIFEXITED(status)) {
|
||||
cons_show_error("Editor exited with status %d", WEXITSTATUS(status));
|
||||
} else if (WIFSIGNALED(status)) {
|
||||
cons_show("Editor session cancelled.");
|
||||
} else {
|
||||
cons_show_error("Editor exited with error status %d", WEXITSTATUS(status));
|
||||
}
|
||||
|
||||
if (remove(ctx->filename) != 0) {
|
||||
@@ -78,11 +70,6 @@ _editor_exit_cb(GPid pid, gint status, gpointer data)
|
||||
gboolean
|
||||
launch_editor(gchar* initial_content, void (*callback)(gchar* content, void* data), void* user_data)
|
||||
{
|
||||
if (editor_active) {
|
||||
cons_show_error("An editor session is already active.");
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
auto_gchar gchar* filename = NULL;
|
||||
auto_gerror GError* glib_error = NULL;
|
||||
const char* jid = connection_get_barejid();
|
||||
@@ -131,7 +118,6 @@ launch_editor(gchar* initial_content, void (*callback)(gchar* content, void* dat
|
||||
ctx->callback = callback;
|
||||
ctx->user_data = user_data;
|
||||
|
||||
editor_active = TRUE;
|
||||
ui_suspend();
|
||||
|
||||
// Force GLib to install its SIGCHLD handler before forking. Creating a
|
||||
@@ -143,7 +129,6 @@ launch_editor(gchar* initial_content, void (*callback)(gchar* content, void* dat
|
||||
pid_t pid = fork();
|
||||
if (pid == -1) {
|
||||
log_error("[Editor] Failed to fork: %s", strerror(errno));
|
||||
editor_active = FALSE;
|
||||
ui_resume();
|
||||
ui_resize();
|
||||
cons_show_error("Failed to start editor: %s", strerror(errno));
|
||||
@@ -154,8 +139,7 @@ launch_editor(gchar* initial_content, void (*callback)(gchar* content, void* dat
|
||||
} else if (pid == 0) {
|
||||
// Child process: Inherits TTY from parent
|
||||
|
||||
// SIGTSTP=SIG_DFL lets vim's :stop / Ctrl-Z work; profanity catches
|
||||
// the STOPPED state via editor_check_stopped() and drops to the shell.
|
||||
// Reset signal handlers that profanity sets so the editor doesn't inherit them
|
||||
signal(SIGINT, SIG_DFL);
|
||||
signal(SIGTSTP, SIG_DFL);
|
||||
signal(SIGPIPE, SIG_DFL);
|
||||
@@ -167,39 +151,7 @@ launch_editor(gchar* initial_content, void (*callback)(gchar* content, void* dat
|
||||
}
|
||||
|
||||
// Parent process: Watch the child asynchronously
|
||||
editor_pid = pid;
|
||||
g_child_watch_add((GPid)pid, _editor_exit_cb, ctx);
|
||||
g_strfreev(editor_argv);
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
void
|
||||
editor_emergency_kill(void)
|
||||
{
|
||||
if (!editor_active || editor_pid <= 0) {
|
||||
return;
|
||||
}
|
||||
log_warning("[Editor] aborting editor pid %d", editor_pid);
|
||||
kill(editor_pid, SIGCONT); // wake if STOPPED; no-op otherwise
|
||||
kill(editor_pid, SIGTERM);
|
||||
// _editor_exit_cb will restore the UI when the child reaps.
|
||||
}
|
||||
|
||||
gboolean
|
||||
editor_is_active(void)
|
||||
{
|
||||
return editor_active;
|
||||
}
|
||||
|
||||
gboolean
|
||||
editor_check_stopped(void)
|
||||
{
|
||||
// Poll for STOPPED without reaping (g_child_watch handles exit via its
|
||||
// own WNOHANG). Lets prof_run drop the group to the shell on Ctrl-Z.
|
||||
if (!editor_active || editor_pid <= 0) {
|
||||
return FALSE;
|
||||
}
|
||||
int status;
|
||||
pid_t r = waitpid(editor_pid, &status, WNOHANG | WUNTRACED);
|
||||
return (r == editor_pid && WIFSTOPPED(status));
|
||||
}
|
||||
|
||||
@@ -16,13 +16,4 @@
|
||||
|
||||
gboolean launch_editor(gchar* initial_content, void (*callback)(gchar* content, void* data), void* user_data);
|
||||
|
||||
void editor_emergency_kill(void); // SIGCONT+SIGTERM the editor child
|
||||
|
||||
// TRUE if editor child is STOPPED (Ctrl-Z / :stop / gdb-attach). Does not
|
||||
// reap; g_child_watch keeps handling exit.
|
||||
gboolean editor_check_stopped(void);
|
||||
|
||||
// Editor-session flag accessor. Used by prof_run on SIGCONT resume.
|
||||
gboolean editor_is_active(void);
|
||||
|
||||
#endif
|
||||
|
||||
@@ -399,6 +399,8 @@ chatwin_outgoing_msg(ProfChatWin* chatwin, const char* const message, const char
|
||||
win_print_outgoing((ProfWin*)chatwin, enc_char, id, replace_id, display_message);
|
||||
}
|
||||
|
||||
plugins_post_chat_message_display(myjid->barejid, myjid->resourcepart, display_message);
|
||||
|
||||
// Save last id and message for LMC
|
||||
// Note: if the same message is to be corrected several times, the id of the original message is used in each case
|
||||
// https://xmpp.org/extensions/xep-0308.html#rules
|
||||
@@ -432,6 +434,7 @@ chatwin_outgoing_carbon(ProfChatWin* chatwin, ProfMessage* message)
|
||||
|
||||
win_print_outgoing(window, enc_char, message->id, message->replace_id, message->plain);
|
||||
|
||||
plugins_post_chat_message_display(message->from_jid->barejid, message->from_jid->resourcepart, message->plain);
|
||||
message->plain = old_plain;
|
||||
|
||||
int num = wins_get_num(window);
|
||||
@@ -558,6 +561,7 @@ _chatwin_history(ProfChatWin* chatwin, const char* const contact_barejid)
|
||||
if (plugin_msg)
|
||||
msg->plain = plugin_msg;
|
||||
win_print_history((ProfWin*)chatwin, msg);
|
||||
plugins_post_chat_message_display(msg->from_jid->barejid, msg->from_jid->resourcepart, msg->plain);
|
||||
msg->plain = old_plain;
|
||||
curr = g_slist_next(curr);
|
||||
}
|
||||
@@ -597,6 +601,7 @@ chatwin_db_history(ProfChatWin* chatwin, const gchar* start_time, const gchar* e
|
||||
} else {
|
||||
win_print_history((ProfWin*)chatwin, msg);
|
||||
}
|
||||
plugins_post_chat_message_display(msg->from_jid->barejid, msg->from_jid->resourcepart, msg->plain);
|
||||
msg->plain = old_plain;
|
||||
curr = g_slist_next(curr);
|
||||
}
|
||||
|
||||
@@ -1703,11 +1703,6 @@ cons_notify_setting(void)
|
||||
else
|
||||
cons_show("Subscription requests (/notify sub) : OFF");
|
||||
|
||||
if (prefs_get_boolean(PREF_AUTOPING_WARNING))
|
||||
cons_show("Autoping warning (/autoping warning): ON");
|
||||
else
|
||||
cons_show("Autoping warning (/autoping warning): OFF");
|
||||
|
||||
gint remind_period = prefs_get_notify_remind();
|
||||
if (remind_period == 0) {
|
||||
cons_show("Reminder period (/notify remind) : OFF");
|
||||
@@ -2008,26 +2003,21 @@ cons_autoping_setting(void)
|
||||
{
|
||||
gint autoping_interval = prefs_get_autoping();
|
||||
if (autoping_interval == 0) {
|
||||
cons_show("Autoping interval (/autoping) : OFF");
|
||||
cons_show("Autoping interval (/autoping) : OFF");
|
||||
} else if (autoping_interval == 1) {
|
||||
cons_show("Autoping interval (/autoping) : 1 second");
|
||||
cons_show("Autoping interval (/autoping) : 1 second");
|
||||
} else {
|
||||
cons_show("Autoping interval (/autoping) : %d seconds", autoping_interval);
|
||||
cons_show("Autoping interval (/autoping) : %d seconds", autoping_interval);
|
||||
}
|
||||
|
||||
gint autoping_timeout = prefs_get_autoping_timeout();
|
||||
if (autoping_timeout == 0) {
|
||||
cons_show("Autoping timeout (/autoping) : OFF");
|
||||
cons_show("Autoping timeout (/autoping) : OFF");
|
||||
} else if (autoping_timeout == 1) {
|
||||
cons_show("Autoping timeout (/autoping) : 1 second");
|
||||
cons_show("Autoping timeout (/autoping) : 1 second");
|
||||
} else {
|
||||
cons_show("Autoping timeout (/autoping) : %d seconds", autoping_timeout);
|
||||
cons_show("Autoping timeout (/autoping) : %d seconds", autoping_timeout);
|
||||
}
|
||||
|
||||
if (prefs_get_boolean(PREF_AUTOPING_WARNING))
|
||||
cons_show("Autoping warning (/autoping warning): ON");
|
||||
else
|
||||
cons_show("Autoping warning (/autoping warning): OFF");
|
||||
}
|
||||
|
||||
void
|
||||
|
||||
@@ -64,8 +64,10 @@ static Display* display;
|
||||
|
||||
static void _ui_draw_term_title(void);
|
||||
|
||||
// Fail-safe gate: authoritative flag (doesn't depend on isendwin staying
|
||||
// TRUE), plus the regular endwin check for shutdown.
|
||||
// Skip drawing while an external editor is active (ui_suspended) or after the
|
||||
// screen has been closed (endwin). The flag is authoritative and does not rely
|
||||
// on isendwin() staying TRUE, which a stray doupdate() during the session could
|
||||
// otherwise clear.
|
||||
static gboolean
|
||||
_ui_redraw_suspended(void)
|
||||
{
|
||||
@@ -146,7 +148,7 @@ ui_update(void)
|
||||
title_bar_update_virtual();
|
||||
status_bar_draw();
|
||||
inp_put_back();
|
||||
prof_doupdate();
|
||||
doupdate();
|
||||
|
||||
if (perform_resize) {
|
||||
perform_resize = FALSE;
|
||||
@@ -187,10 +189,9 @@ ui_reset_idle_time(void)
|
||||
void
|
||||
ui_suspend(void)
|
||||
{
|
||||
inp_suspend();
|
||||
// Flush before flipping the flag (prof_doupdate skips while suspended).
|
||||
prof_doupdate();
|
||||
ui_suspended = TRUE;
|
||||
inp_suspend();
|
||||
doupdate();
|
||||
endwin();
|
||||
}
|
||||
|
||||
@@ -198,7 +199,6 @@ void
|
||||
ui_resume(void)
|
||||
{
|
||||
ui_suspended = FALSE;
|
||||
clearok(stdscr, TRUE); // ncurses can't see vim's screen clobber
|
||||
refresh();
|
||||
inp_resume();
|
||||
}
|
||||
@@ -219,14 +219,6 @@ ui_flash(void)
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
prof_doupdate(void)
|
||||
{
|
||||
if (!_ui_redraw_suspended()) {
|
||||
doupdate();
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
ui_resize(void)
|
||||
{
|
||||
|
||||
@@ -182,11 +182,7 @@ inp_readline(void)
|
||||
{
|
||||
// UI is suspended
|
||||
if (is_suspended || isendwin()) {
|
||||
// Mirror the normal-path unlock so workers aren't starved
|
||||
// for the whole editor session.
|
||||
pthread_mutex_unlock(&lock);
|
||||
g_usleep(100000); // 100ms
|
||||
pthread_mutex_lock(&lock);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
@@ -315,13 +311,14 @@ inp_get_line(void)
|
||||
werase(inp_win);
|
||||
wmove(inp_win, 0, 0);
|
||||
_inp_win_update_virtual();
|
||||
prof_doupdate();
|
||||
// Don't write to the terminal while suspended (external editor active).
|
||||
if (!is_suspended && !isendwin()) {
|
||||
doupdate();
|
||||
}
|
||||
char* line = NULL;
|
||||
while (!line) {
|
||||
line = inp_readline();
|
||||
ui_update();
|
||||
while (g_main_context_iteration(NULL, FALSE)) // non-blocking; bounded by ready sources
|
||||
;
|
||||
}
|
||||
status_bar_clear_prompt();
|
||||
return line;
|
||||
@@ -344,14 +341,15 @@ inp_get_password(void)
|
||||
werase(inp_win);
|
||||
wmove(inp_win, 0, 0);
|
||||
_inp_win_update_virtual();
|
||||
prof_doupdate();
|
||||
// Don't write to the terminal while suspended (external editor active).
|
||||
if (!is_suspended && !isendwin()) {
|
||||
doupdate();
|
||||
}
|
||||
char* password = NULL;
|
||||
get_password = TRUE;
|
||||
while (!password) {
|
||||
password = inp_readline();
|
||||
ui_update();
|
||||
while (g_main_context_iteration(NULL, FALSE)) // non-blocking; bounded by ready sources
|
||||
;
|
||||
}
|
||||
get_password = FALSE;
|
||||
status_bar_clear_prompt();
|
||||
@@ -467,7 +465,9 @@ _inp_write(char* line, int offset)
|
||||
_inp_win_handle_scroll();
|
||||
|
||||
_inp_win_update_virtual();
|
||||
prof_doupdate();
|
||||
if (!isendwin()) {
|
||||
doupdate();
|
||||
}
|
||||
}
|
||||
|
||||
static int
|
||||
|
||||
@@ -10,8 +10,6 @@
|
||||
#ifndef UI_TITLEBAR_H
|
||||
#define UI_TITLEBAR_H
|
||||
|
||||
#include "glib.h"
|
||||
|
||||
void create_title_bar(void);
|
||||
void free_title_bar(void);
|
||||
void title_bar_update_virtual(void);
|
||||
|
||||
@@ -43,9 +43,6 @@ void ui_load_colours(void);
|
||||
void ui_update(void);
|
||||
void ui_redraw(void);
|
||||
void ui_resize(void);
|
||||
// doupdate() wrapper that skips when the UI is suspended. Use this instead
|
||||
// of raw doupdate() to avoid the per-site whitelist class of bug.
|
||||
void prof_doupdate(void);
|
||||
void ui_focus_win(ProfWin* window);
|
||||
void ui_sigwinch_handler(int sig);
|
||||
void ui_handle_otr_error(const char* const barejid, const char* const message);
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
#include "ai/ai_client.h"
|
||||
|
||||
static const int PAD_MIN_HEIGHT = 100;
|
||||
static const int PAD_DEAD_SPACE_LIMIT = 2000; // reclaim once the pad holds this much dead space (cursor past live buffer); size-independent, so it never fires while scrolling
|
||||
static const int PAD_THRESHOLD = 3000;
|
||||
static gboolean _in_redraw = FALSE;
|
||||
|
||||
static void
|
||||
@@ -56,10 +56,9 @@ _win_ensure_pad_capacity(ProfWin* window, WINDOW* win, int lines_needed)
|
||||
int cur_height = getmaxy(win);
|
||||
int cur_width = getmaxx(win);
|
||||
if (lines_needed >= cur_height - 1) {
|
||||
// redraw to reclaim dead pad space (cursor far past live buffer, e.g. a long
|
||||
// append-only session); dead space never accrues while scrolling, only here
|
||||
int dead = window ? getcury(win) - window->layout->buffer->lines : 0;
|
||||
if (window && dead > PAD_DEAD_SPACE_LIMIT && !_in_redraw) {
|
||||
// If we are getting too large, trigger a redraw to clean up old lines
|
||||
// but only if we are not already in a redraw process.
|
||||
if (window && cur_height >= PAD_THRESHOLD && !_in_redraw) {
|
||||
win_redraw(window);
|
||||
} else {
|
||||
// resize to required lines + some buffer for next messages
|
||||
@@ -68,20 +67,6 @@ _win_ensure_pad_capacity(ProfWin* window, WINDOW* win, int lines_needed)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// upper-bound estimate of rows a message body occupies (hard newlines + soft-wrap), to reserve pad space before printing so a tall message isn't clipped (a clipped height desyncs the scroll offset)
|
||||
static int
|
||||
_win_estimated_lines(WINDOW* win, const char* const message, int indent, int pad_indent)
|
||||
{
|
||||
int usable = MAX(1, getmaxx(win) - (indent + pad_indent));
|
||||
int newlines = 0;
|
||||
for (const char* p = message; *p; p++) {
|
||||
if (*p == '\n') {
|
||||
newlines++;
|
||||
}
|
||||
}
|
||||
return newlines + utf8_display_len(message) / usable + 2;
|
||||
}
|
||||
static const char* LOADING_MESSAGE = "Loading older messages…";
|
||||
static const char* CONS_WIN_TITLE = "CProof. Type /help for help information.";
|
||||
static const char* XML_WIN_TITLE = "XML Console";
|
||||
@@ -2016,7 +2001,7 @@ _win_print_internal(ProfWin* window, const char* show_char, int pad_indent, GDat
|
||||
}
|
||||
}
|
||||
|
||||
_win_ensure_pad_capacity(window, window->layout->win, getcury(window->layout->win) + _win_estimated_lines(window->layout->win, message + offset, indent, pad_indent));
|
||||
_win_ensure_pad_capacity(window, window->layout->win, getcury(window->layout->win));
|
||||
|
||||
if (prefs_get_boolean(PREF_WRAP)) {
|
||||
_win_print_wrapped(window->layout->win, message + offset, indent, pad_indent);
|
||||
@@ -2187,10 +2172,9 @@ win_redraw(ProfWin* window)
|
||||
unsigned int size = buffer_size(window->layout->buffer);
|
||||
_in_redraw = TRUE;
|
||||
|
||||
// size the pad to fit the whole buffer (plus headroom) and erase it
|
||||
// shrink pad back to minimum size and erase it
|
||||
int cols = getmaxx(window->layout->win);
|
||||
int needed = window->layout->buffer->lines + 100;
|
||||
wresize(window->layout->win, MAX(needed, PAD_MIN_HEIGHT), cols);
|
||||
wresize(window->layout->win, PAD_MIN_HEIGHT, cols);
|
||||
werase(window->layout->win);
|
||||
|
||||
for (unsigned int i = 0; i < size; i++) {
|
||||
|
||||
@@ -10,8 +10,6 @@
|
||||
#ifndef XMPP_BLOCKING_H
|
||||
#define XMPP_BLOCKING_H
|
||||
|
||||
#include <strophe.h>
|
||||
|
||||
void blocking_request(void);
|
||||
int blocked_set_handler(xmpp_stanza_t* stanza);
|
||||
int reporting_set_handler(xmpp_stanza_t* stanza);
|
||||
|
||||
@@ -99,7 +99,6 @@ static void _disco_items_result_handler(xmpp_stanza_t* const stanza);
|
||||
static void _last_activity_get_handler(xmpp_stanza_t* const stanza);
|
||||
static void _version_get_handler(xmpp_stanza_t* const stanza);
|
||||
static void _ping_get_handler(xmpp_stanza_t* const stanza);
|
||||
static void _disco_autoping_warning_message(GHashTable* features);
|
||||
|
||||
static int _version_result_id_handler(xmpp_stanza_t* const stanza, void* const userdata);
|
||||
static int _disco_info_response_id_handler(xmpp_stanza_t* const stanza, void* const userdata);
|
||||
@@ -2430,12 +2429,6 @@ _disco_info_response_id_handler_onconnect(xmpp_stanza_t* const stanza, void* con
|
||||
}
|
||||
child = xmpp_stanza_get_next(child);
|
||||
}
|
||||
|
||||
// Prevent repetitions by avoiding checks of disco items (from connection_set_disco_items)
|
||||
const char* domain = connection_get_domain();
|
||||
if (from && domain && g_ascii_strcasecmp(from, domain) == 0) {
|
||||
_disco_autoping_warning_message(features);
|
||||
}
|
||||
}
|
||||
|
||||
connection_features_received(from);
|
||||
@@ -2443,21 +2436,6 @@ _disco_info_response_id_handler_onconnect(xmpp_stanza_t* const stanza, void* con
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void
|
||||
_disco_autoping_warning_message(GHashTable* features)
|
||||
{
|
||||
gboolean server_supports_ping = g_hash_table_contains(features, "urn:xmpp:ping");
|
||||
gboolean user_prefers_warning = prefs_get_boolean(PREF_AUTOPING_WARNING);
|
||||
gboolean is_autoping_enabled = prefs_get_autoping() != 0;
|
||||
|
||||
if (!is_autoping_enabled && server_supports_ping && user_prefers_warning) {
|
||||
cons_show("This server supports XEP-0199: XMPP Ping (better keepalive detection),\n"
|
||||
"but autoping feature is currently disabled in settings.\n"
|
||||
"Consider enabling it (e.g., `/autoping set 30`) for improved connection stability.\n"
|
||||
"Use `/autoping warning off` to disable this message.");
|
||||
}
|
||||
}
|
||||
|
||||
static int
|
||||
_http_upload_response_id_handler(xmpp_stanza_t* const stanza, void* const userdata)
|
||||
{
|
||||
|
||||
@@ -10,8 +10,6 @@
|
||||
#ifndef XMPP_IQ_H
|
||||
#define XMPP_IQ_H
|
||||
|
||||
#include <strophe.h>
|
||||
|
||||
typedef int (*ProfIqCallback)(xmpp_stanza_t* const stanza, void* const userdata);
|
||||
typedef void (*ProfIqFreeCallback)(void* userdata);
|
||||
|
||||
|
||||
@@ -117,7 +117,7 @@ jid_is_valid(const gchar* const str)
|
||||
|
||||
// Localpart validation
|
||||
if (at) {
|
||||
size_t local_len = g_diff_to_gsize(at, str);
|
||||
size_t local_len = at - str;
|
||||
if (local_len == 0 || local_len > JID_MAX_PART_LEN) {
|
||||
return FALSE;
|
||||
}
|
||||
@@ -135,7 +135,7 @@ jid_is_valid(const gchar* const str)
|
||||
|
||||
// Resourcepart validation if present
|
||||
if (slash) {
|
||||
domain_len = g_diff_to_gsize(slash, domain_start);
|
||||
domain_len = slash - domain_start;
|
||||
size_t resource_len = strlen(slash + 1);
|
||||
if (resource_len > JID_MAX_PART_LEN) {
|
||||
return FALSE;
|
||||
|
||||
@@ -65,7 +65,6 @@ static void _handle_pubsub(xmpp_stanza_t* const stanza, xmpp_stanza_t* const eve
|
||||
static gboolean _handle_form(xmpp_stanza_t* const stanza);
|
||||
static gboolean _handle_jingle_message(xmpp_stanza_t* const stanza);
|
||||
static gboolean _should_ignore_based_on_silence(xmpp_stanza_t* const stanza);
|
||||
static gboolean _stanza_id_by_trusted(const char* by);
|
||||
#ifdef HAVE_OMEMO
|
||||
static void _receive_omemo(xmpp_stanza_t* const stanza, ProfMessage* message);
|
||||
#endif
|
||||
@@ -467,15 +466,12 @@ message_send_chat_pgp(const char* const barejid, const char* const msg, gboolean
|
||||
auto_char char* jid = chat_session_get_jid(barejid);
|
||||
char* id = connection_create_stanza_id();
|
||||
|
||||
ProfWin* current = wins_get_current();
|
||||
|
||||
xmpp_stanza_t* message = NULL;
|
||||
#ifdef HAVE_LIBGPGME
|
||||
ProfAccount* account = accounts_get_account(session_get_account_name());
|
||||
if (account->pgp_keyid) {
|
||||
auto_jid Jid* jidp = jid_create(jid);
|
||||
auto_gchar gchar* err = NULL;
|
||||
auto_gchar gchar* encrypted = p_gpg_encrypt(jidp->barejid, msg, account->pgp_keyid, &err);
|
||||
auto_gchar gchar* encrypted = p_gpg_encrypt(jidp->barejid, msg, account->pgp_keyid);
|
||||
if (encrypted) {
|
||||
message = xmpp_message_new(ctx, STANZA_TYPE_CHAT, jid, id);
|
||||
xmpp_message_set_body(message, "This message is encrypted (XEP-0027).");
|
||||
@@ -489,31 +485,18 @@ message_send_chat_pgp(const char* const barejid, const char* const msg, gboolean
|
||||
xmpp_stanza_add_child(message, x);
|
||||
xmpp_stanza_release(x);
|
||||
} else {
|
||||
if (current) {
|
||||
win_println(current, THEME_ERROR, "-", "Unable to encrypt message for %s: %s.", jid, err);
|
||||
}
|
||||
log_error("Message not encrypted for %s: %s.", jid, err);
|
||||
account_free(account);
|
||||
free(id);
|
||||
return NULL;
|
||||
message = xmpp_message_new(ctx, STANZA_TYPE_CHAT, jid, id);
|
||||
xmpp_message_set_body(message, msg);
|
||||
}
|
||||
} else {
|
||||
if (current) {
|
||||
win_println(current, THEME_ERROR, "-", "No PGP key configured for this account.");
|
||||
}
|
||||
log_error("No PGP key configured, message not sent.");
|
||||
account_free(account);
|
||||
free(id);
|
||||
return NULL;
|
||||
message = xmpp_message_new(ctx, STANZA_TYPE_CHAT, jid, id);
|
||||
xmpp_message_set_body(message, msg);
|
||||
}
|
||||
account_free(account);
|
||||
#else
|
||||
if (current) {
|
||||
win_println(current, THEME_ERROR, "-", "LibGPGME not available, message not sent.");
|
||||
}
|
||||
log_error("LibGPGME not available, message not sent.");
|
||||
free(id);
|
||||
return NULL;
|
||||
// ?
|
||||
message = xmpp_message_new(ctx, STANZA_TYPE_CHAT, jid, id);
|
||||
xmpp_message_set_body(message, msg);
|
||||
#endif
|
||||
|
||||
if (state) {
|
||||
@@ -562,10 +545,7 @@ message_send_chat_ox(const char* const barejid, const char* const msg, gboolean
|
||||
xmpp_stanza_to_text(signcrypt, &c, &s);
|
||||
char* signcrypt_e = p_ox_gpg_signcrypt(account->jid, barejid, c);
|
||||
if (signcrypt_e == NULL) {
|
||||
ProfWin* current = wins_get_current();
|
||||
if (current) {
|
||||
win_println(current, THEME_ERROR, "-", "Unable to send OX message. Check log file and profanity-ox-setup man page for details.");
|
||||
}
|
||||
cons_show("Unable to send OX message. Check log file and profanity-ox-setup man page for details.");
|
||||
log_error("Message not signcrypted.");
|
||||
return NULL;
|
||||
}
|
||||
@@ -1108,7 +1088,7 @@ _handle_groupchat(xmpp_stanza_t* const stanza)
|
||||
xmpp_stanza_t* stanzaidst = xmpp_stanza_get_child_by_name_and_ns(stanza, STANZA_NAME_STANZA_ID, STANZA_NS_STABLE_ID);
|
||||
if (stanzaidst) {
|
||||
const char* by = xmpp_stanza_get_attribute(stanzaidst, "by");
|
||||
if (by && g_strcmp0(by, from_jid->barejid) == 0 && _stanza_id_by_trusted(by)) {
|
||||
if (by && (g_strcmp0(by, from_jid->barejid) == 0)) {
|
||||
stanzaid = (char*)xmpp_stanza_get_attribute(stanzaidst, STANZA_ATTR_ID);
|
||||
if (stanzaid) {
|
||||
message->stanzaid = strdup(stanzaid);
|
||||
@@ -1417,7 +1397,7 @@ _handle_chat(xmpp_stanza_t* const stanza, gboolean is_mam, gboolean is_carbon, c
|
||||
xmpp_stanza_t* stanzaidst = xmpp_stanza_get_child_by_name_and_ns(stanza, STANZA_NAME_STANZA_ID, STANZA_NS_STABLE_ID);
|
||||
if (stanzaidst) {
|
||||
const char* by = xmpp_stanza_get_attribute(stanzaidst, "by");
|
||||
if (by && equals_our_barejid(by) && _stanza_id_by_trusted(by)) {
|
||||
if (by && equals_our_barejid(by)) {
|
||||
stanzaid = (char*)xmpp_stanza_get_attribute(stanzaidst, STANZA_ATTR_ID);
|
||||
if (stanzaid) {
|
||||
message->stanzaid = strdup(stanzaid);
|
||||
@@ -1755,22 +1735,3 @@ _should_ignore_based_on_silence(xmpp_stanza_t* const stanza)
|
||||
}
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
// XEP-0359 §6 disco-gate; falls back to domain since that's what disco is run against.
|
||||
static gboolean
|
||||
_stanza_id_by_trusted(const char* by)
|
||||
{
|
||||
if (!by) {
|
||||
return FALSE;
|
||||
}
|
||||
GHashTable* features = connection_get_features(by);
|
||||
if (features && g_hash_table_contains(features, STANZA_NS_STABLE_ID)) {
|
||||
return TRUE;
|
||||
}
|
||||
const char* at = strchr(by, '@');
|
||||
if (!at) {
|
||||
return FALSE;
|
||||
}
|
||||
features = connection_get_features(at + 1);
|
||||
return features && g_hash_table_contains(features, STANZA_NS_STABLE_ID);
|
||||
}
|
||||
|
||||
@@ -707,7 +707,7 @@ muc_autocomplete(ProfWin* window, const char* const input, gboolean previous)
|
||||
} else {
|
||||
search_str = last_space + 1;
|
||||
if (!chat_room->autocomplete_prefix) {
|
||||
chat_room->autocomplete_prefix = g_strndup(input, g_diff_to_gsize(search_str, input));
|
||||
chat_room->autocomplete_prefix = g_strndup(input, search_str - input);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,8 +7,6 @@
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later WITH OpenSSL-exception
|
||||
*/
|
||||
|
||||
#include "glib.h"
|
||||
|
||||
/*!
|
||||
* \page OX OX Implementation
|
||||
*
|
||||
|
||||
@@ -621,20 +621,15 @@ _available_handler(xmpp_stanza_t* const stanza)
|
||||
}
|
||||
|
||||
if (g_strcmp0(xmpp_presence->jid->barejid, my_jid->barejid) == 0) {
|
||||
// Copy what we read before connection_add_available_resource() takes ownership.
|
||||
Resource* resource_for_roster = resource_copy(resource);
|
||||
auto_gchar gchar* resource_name = g_strdup(resource->name);
|
||||
auto_gchar gchar* resource_status = resource->status ? g_strdup(resource->status) : NULL;
|
||||
int resource_priority = resource->priority;
|
||||
resource_presence_t resource_presence = resource->presence;
|
||||
connection_add_available_resource(resource);
|
||||
Resource* resource_for_roster = resource_copy(resource);
|
||||
sv_ev_contact_online(xmpp_presence->jid->barejid, resource_for_roster, xmpp_presence->last_activity, pgpsig);
|
||||
const char* account_name = session_get_account_name();
|
||||
int max_sessions = accounts_get_max_sessions(account_name);
|
||||
if (max_sessions > 0) {
|
||||
auto_gchar gchar* cur_resource = accounts_get_resource(account_name);
|
||||
int res_count = connection_count_available_resources();
|
||||
if (res_count > max_sessions && g_strcmp0(cur_resource, resource_name)) {
|
||||
if (res_count > max_sessions && g_strcmp0(cur_resource, resource->name)) {
|
||||
ProfWin* console = wins_get_console();
|
||||
ProfWin* current_window = wins_get_current();
|
||||
auto_gchar gchar* message = g_strdup_printf("Max sessions alarm! (%d/%d devices in use)", res_count, max_sessions);
|
||||
@@ -644,14 +639,14 @@ _available_handler(xmpp_stanza_t* const stanza)
|
||||
}
|
||||
notify(message, 10000, "Security alert");
|
||||
|
||||
const char* resource_presence_str = string_from_resource_presence(resource_presence);
|
||||
win_print(console, THEME_DEFAULT, "|", "New device info: \n %s (%d), %s", resource_name, resource_priority, resource_presence_str);
|
||||
const char* resource_presence = string_from_resource_presence(resource->presence);
|
||||
win_print(console, THEME_DEFAULT, "|", "New device info: \n %s (%d), %s", resource->name, resource->priority, resource_presence);
|
||||
|
||||
if (resource_status) {
|
||||
win_append(console, THEME_DEFAULT, ", \"%s\"", resource_status);
|
||||
if (resource->status) {
|
||||
win_append(console, THEME_DEFAULT, ", \"%s\"", resource->status);
|
||||
}
|
||||
win_appendln(console, THEME_DEFAULT, "");
|
||||
auto_jid Jid* jidp = jid_create_from_bare_and_resource(my_jid->barejid, resource_name);
|
||||
auto_jid Jid* jidp = jid_create_from_bare_and_resource(my_jid->barejid, resource->name);
|
||||
EntityCapabilities* caps = caps_lookup(jidp->fulljid);
|
||||
|
||||
if (caps) {
|
||||
|
||||
@@ -10,9 +10,6 @@
|
||||
#ifndef XMPP_ROSTER_H
|
||||
#define XMPP_ROSTER_H
|
||||
|
||||
#include "glib.h"
|
||||
#include <strophe.h>
|
||||
|
||||
void roster_request(void);
|
||||
void roster_set_handler(xmpp_stanza_t* const stanza);
|
||||
void roster_result_handler(xmpp_stanza_t* const stanza);
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
|
||||
#include "ui/win_types.h"
|
||||
#include "xmpp/vcard.h"
|
||||
#include <strophe.h>
|
||||
|
||||
vCard* vcard_new();
|
||||
void vcard_free(vCard* vcard);
|
||||
|
||||
@@ -150,14 +150,6 @@ main(int argc, char* argv[])
|
||||
PROF_FUNC_TEST(autoping_sends_ping_after_interval),
|
||||
PROF_FUNC_TEST(autoping_server_not_supporting_ping),
|
||||
|
||||
/* Autoping availability warning - negative cases wait ~3s */
|
||||
PROF_FUNC_TEST(autoping_warning_shown_when_disabled),
|
||||
PROF_FUNC_TEST(autoping_warning_not_shown_when_server_unsupported),
|
||||
PROF_FUNC_TEST(autoping_warning_not_shown_when_autoping_enabled),
|
||||
PROF_FUNC_TEST(autoping_warning_not_shown_when_user_disabled),
|
||||
PROF_FUNC_TEST(autoping_warning_not_shown_for_user_jid),
|
||||
PROF_FUNC_TEST(autoping_warning_not_shown_for_subdomain_service),
|
||||
|
||||
/* Service Discovery - XEP-0030 */
|
||||
PROF_FUNC_TEST(disco_info_shows_identity),
|
||||
PROF_FUNC_TEST(disco_info_shows_features),
|
||||
@@ -261,10 +253,6 @@ main(int argc, char* argv[])
|
||||
PROF_FUNC_TEST(message_receive_console),
|
||||
PROF_FUNC_TEST(message_receive_chatwin),
|
||||
|
||||
/* XEP-0359 disco gate for stanza-id trust */
|
||||
PROF_FUNC_TEST(stanza_id_dedup_fires_when_server_announces_sid0),
|
||||
PROF_FUNC_TEST(stanza_id_not_trusted_when_server_does_not_announce_sid0),
|
||||
|
||||
#ifdef HAVE_SQLITE
|
||||
/* MUC (groupchat) database — export/import of type="muc" messages */
|
||||
PROF_FUNC_TEST(muc_export_sqlite_to_flatfile),
|
||||
|
||||
@@ -119,9 +119,10 @@ _create_dir(const char* name)
|
||||
gboolean
|
||||
_mkdir_recursive(const char* dir)
|
||||
{
|
||||
int i;
|
||||
gboolean result = TRUE;
|
||||
|
||||
for (size_t i = 1; i <= strlen(dir); i++) {
|
||||
for (i = 1; i <= strlen(dir); i++) {
|
||||
if (dir[i] == '/' || dir[i] == '\0') {
|
||||
gchar* next_dir = g_strndup(dir, i);
|
||||
result = _create_dir(next_dir);
|
||||
|
||||
@@ -112,144 +112,3 @@ autoping_server_not_supporting_ping(void** state)
|
||||
// Should show error about ping not being supported
|
||||
assert_true(prof_output_regex("Server ping not supported"));
|
||||
}
|
||||
|
||||
/*
|
||||
* Autoping availability warning.
|
||||
*
|
||||
* The warning fires from the on-connect disco handler, so the three inputs
|
||||
* (server ping support, autoping interval, warning preference) must all be
|
||||
* set before prof_connect().
|
||||
*/
|
||||
|
||||
/* Stable substring of the warning text emitted by iq.c. */
|
||||
#define AUTOPING_WARNING_MATCH "This server supports XEP-0199"
|
||||
|
||||
static void
|
||||
_stub_server_disco(gboolean with_ping)
|
||||
{
|
||||
if (with_ping) {
|
||||
stbbr_for_query("http://jabber.org/protocol/disco#info",
|
||||
"<iq to='stabber@localhost/profanity' type='result' from='localhost'>"
|
||||
"<query xmlns='http://jabber.org/protocol/disco#info'>"
|
||||
"<identity category='server' type='im' name='Stabber'/>"
|
||||
"<feature var='urn:xmpp:ping'/>"
|
||||
"</query>"
|
||||
"</iq>"
|
||||
);
|
||||
} else {
|
||||
stbbr_for_query("http://jabber.org/protocol/disco#info",
|
||||
"<iq to='stabber@localhost/profanity' type='result' from='localhost'>"
|
||||
"<query xmlns='http://jabber.org/protocol/disco#info'>"
|
||||
"<identity category='server' type='im' name='Stabber'/>"
|
||||
"</query>"
|
||||
"</iq>"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
autoping_warning_shown_when_disabled(void** state)
|
||||
{
|
||||
// All conditions met: server supports ping, autoping off, warning on (default).
|
||||
_stub_server_disco(TRUE);
|
||||
|
||||
prof_input("/autoping set 0");
|
||||
assert_true(prof_output_exact("Autoping disabled."));
|
||||
|
||||
prof_connect();
|
||||
|
||||
assert_true(prof_output_regex(AUTOPING_WARNING_MATCH));
|
||||
}
|
||||
|
||||
void
|
||||
autoping_warning_not_shown_when_server_unsupported(void** state)
|
||||
{
|
||||
// Server does not advertise urn:xmpp:ping -> no warning even with autoping off.
|
||||
_stub_server_disco(FALSE);
|
||||
|
||||
prof_input("/autoping set 0");
|
||||
assert_true(prof_output_exact("Autoping disabled."));
|
||||
|
||||
prof_connect();
|
||||
|
||||
prof_timeout(NEGATIVE_ASSERT_TIMEOUT);
|
||||
assert_false(prof_output_regex(AUTOPING_WARNING_MATCH));
|
||||
}
|
||||
|
||||
void
|
||||
autoping_warning_not_shown_when_autoping_enabled(void** state)
|
||||
{
|
||||
// Warning is suppressed while autoping is enabled.
|
||||
_stub_server_disco(TRUE);
|
||||
|
||||
prof_input("/autoping set 30");
|
||||
assert_true(prof_output_exact("Autoping interval set to 30 seconds."));
|
||||
|
||||
prof_connect();
|
||||
|
||||
prof_timeout(NEGATIVE_ASSERT_TIMEOUT);
|
||||
assert_false(prof_output_regex(AUTOPING_WARNING_MATCH));
|
||||
}
|
||||
|
||||
void
|
||||
autoping_warning_not_shown_when_user_disabled(void** state)
|
||||
{
|
||||
// User opted out -> no warning.
|
||||
_stub_server_disco(TRUE);
|
||||
|
||||
prof_input("/autoping set 0");
|
||||
assert_true(prof_output_exact("Autoping disabled."));
|
||||
|
||||
prof_input("/autoping warning off");
|
||||
assert_true(prof_output_exact("Autoping availability warning disabled."));
|
||||
|
||||
prof_connect();
|
||||
|
||||
prof_timeout(NEGATIVE_ASSERT_TIMEOUT);
|
||||
assert_false(prof_output_regex(AUTOPING_WARNING_MATCH));
|
||||
}
|
||||
|
||||
void
|
||||
autoping_warning_not_shown_for_user_jid(void** state)
|
||||
{
|
||||
// disco#info from a user JID (with '@') should not trigger the warning,
|
||||
// only server responses (no '@') should.
|
||||
stbbr_for_query("http://jabber.org/protocol/disco#info",
|
||||
"<iq to='stabber@localhost/profanity' type='result' from='buddy1@localhost'>"
|
||||
"<query xmlns='http://jabber.org/protocol/disco#info'>"
|
||||
"<identity category='person' type='chat' name='Buddy1'/>"
|
||||
"<feature var='urn:xmpp:ping'/>"
|
||||
"</query>"
|
||||
"</iq>"
|
||||
);
|
||||
|
||||
prof_input("/autoping set 0");
|
||||
assert_true(prof_output_exact("Autoping disabled."));
|
||||
|
||||
prof_connect();
|
||||
|
||||
prof_timeout(NEGATIVE_ASSERT_TIMEOUT);
|
||||
assert_false(prof_output_regex(AUTOPING_WARNING_MATCH));
|
||||
}
|
||||
|
||||
void
|
||||
autoping_warning_not_shown_for_subdomain_service(void** state)
|
||||
{
|
||||
// disco#info from a subdomain service should not trigger the warning, only the server's own domain should
|
||||
stbbr_for_query("http://jabber.org/protocol/disco#info",
|
||||
"<iq to='stabber@localhost/profanity' type='result' from='conf.localhost'>"
|
||||
"<query xmlns='http://jabber.org/protocol/disco#info'>"
|
||||
"<identity category='conference' type='text' name='Conference Service'/>"
|
||||
"<feature var='urn:xmpp:ping'/>"
|
||||
"</query>"
|
||||
"</iq>"
|
||||
);
|
||||
|
||||
prof_input("/autoping set 0");
|
||||
assert_true(prof_output_exact("Autoping disabled."));
|
||||
|
||||
prof_connect();
|
||||
|
||||
prof_timeout(NEGATIVE_ASSERT_TIMEOUT);
|
||||
assert_false(prof_output_regex(AUTOPING_WARNING_MATCH));
|
||||
}
|
||||
|
||||
@@ -4,9 +4,3 @@ void autoping_timeout_set(void** state);
|
||||
void autoping_timeout_zero_disables(void** state);
|
||||
void autoping_sends_ping_after_interval(void** state);
|
||||
void autoping_server_not_supporting_ping(void** state);
|
||||
void autoping_warning_shown_when_disabled(void** state);
|
||||
void autoping_warning_not_shown_when_server_unsupported(void** state);
|
||||
void autoping_warning_not_shown_when_autoping_enabled(void** state);
|
||||
void autoping_warning_not_shown_when_user_disabled(void** state);
|
||||
void autoping_warning_not_shown_for_user_jid(void** state);
|
||||
void autoping_warning_not_shown_for_subdomain_service(void** state);
|
||||
|
||||
@@ -56,70 +56,3 @@ message_receive_chatwin(void **state)
|
||||
|
||||
assert_true(prof_output_regex("someuser@chatserv.org/laptop: .+How are you?"));
|
||||
}
|
||||
|
||||
// XEP-0359 disco gate: server announces urn:xmpp:sid:0 -> stanza-id trusted -> replay flagged as duplicate.
|
||||
void
|
||||
stanza_id_dedup_fires_when_server_announces_sid0(void **state)
|
||||
{
|
||||
stbbr_for_query("http://jabber.org/protocol/disco#info",
|
||||
"<iq type='result' to='stabber@localhost/profanity' from='localhost'>"
|
||||
"<query xmlns='http://jabber.org/protocol/disco#info'>"
|
||||
"<identity category='server' type='im' name='TestServer'/>"
|
||||
"<feature var='urn:xmpp:sid:0'/>"
|
||||
"</query>"
|
||||
"</iq>"
|
||||
);
|
||||
|
||||
prof_connect();
|
||||
|
||||
stbbr_send(
|
||||
"<message id='m-trust-1' to='stabber@localhost' from='someuser@chatserv.org/laptop' type='chat'>"
|
||||
"<body>first</body>"
|
||||
"<stanza-id xmlns='urn:xmpp:sid:0' by='stabber@localhost' id='archive-id-42'/>"
|
||||
"</message>"
|
||||
);
|
||||
assert_true(prof_output_exact("<< chat message: someuser@chatserv.org/laptop (win 2)"));
|
||||
|
||||
stbbr_send(
|
||||
"<message id='m-trust-2' to='stabber@localhost' from='someuser@chatserv.org/laptop' type='chat'>"
|
||||
"<body>replay</body>"
|
||||
"<stanza-id xmlns='urn:xmpp:sid:0' by='stabber@localhost' id='archive-id-42'/>"
|
||||
"</message>"
|
||||
);
|
||||
assert_true(prof_output_exact("Got a message with duplicate (server-generated) stanza-id from someuser@chatserv.org/laptop."));
|
||||
}
|
||||
|
||||
// XEP-0359 disco gate: server does NOT announce urn:xmpp:sid:0 -> stanza-id untrusted -> no replay error.
|
||||
void
|
||||
stanza_id_not_trusted_when_server_does_not_announce_sid0(void **state)
|
||||
{
|
||||
stbbr_for_query("http://jabber.org/protocol/disco#info",
|
||||
"<iq type='result' to='stabber@localhost/profanity' from='localhost'>"
|
||||
"<query xmlns='http://jabber.org/protocol/disco#info'>"
|
||||
"<identity category='server' type='im' name='TestServer'/>"
|
||||
"<feature var='urn:xmpp:ping'/>"
|
||||
"</query>"
|
||||
"</iq>"
|
||||
);
|
||||
|
||||
prof_connect();
|
||||
|
||||
stbbr_send(
|
||||
"<message id='m-untrust-1' to='stabber@localhost' from='someuser@chatserv.org/laptop' type='chat'>"
|
||||
"<body>first</body>"
|
||||
"<stanza-id xmlns='urn:xmpp:sid:0' by='stabber@localhost' id='archive-id-77'/>"
|
||||
"</message>"
|
||||
);
|
||||
assert_true(prof_output_exact("<< chat message: someuser@chatserv.org/laptop (win 2)"));
|
||||
|
||||
stbbr_send(
|
||||
"<message id='m-untrust-2' to='stabber@localhost' from='someuser@chatserv.org/laptop' type='chat'>"
|
||||
"<body>replay</body>"
|
||||
"<stanza-id xmlns='urn:xmpp:sid:0' by='stabber@localhost' id='archive-id-77'/>"
|
||||
"</message>"
|
||||
);
|
||||
|
||||
prof_timeout(2);
|
||||
assert_false(prof_output_exact("Got a message with duplicate (server-generated) stanza-id"));
|
||||
prof_timeout_reset();
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
void message_send(void **state);
|
||||
void message_receive_console(void **state);
|
||||
void message_receive_chatwin(void **state);
|
||||
void stanza_id_dedup_fires_when_server_announces_sid0(void **state);
|
||||
void stanza_id_not_trusted_when_server_does_not_announce_sid0(void **state);
|
||||
|
||||
@@ -614,24 +614,6 @@ test_ai_set_provider_setting(void** state)
|
||||
assert_null(ai_get_provider_setting("p_set_settings", "nonexistent_setting"));
|
||||
}
|
||||
|
||||
void
|
||||
test_ai_set_provider_setting_reserved_key(void** state)
|
||||
{
|
||||
ai_add_provider("p_reserved_keys", "https://example.test/rk/");
|
||||
|
||||
/* Reserved payload keys are rejected (would duplicate fixed fields) */
|
||||
assert_false(ai_set_provider_setting("p_reserved_keys", "model", "x"));
|
||||
assert_false(ai_set_provider_setting("p_reserved_keys", "messages", "[]"));
|
||||
assert_false(ai_set_provider_setting("p_reserved_keys", "stream", "true"));
|
||||
assert_null(ai_get_provider_setting("p_reserved_keys", "model"));
|
||||
|
||||
/* Ordinary keys still work */
|
||||
assert_true(ai_set_provider_setting("p_reserved_keys", "temperature", "0.7"));
|
||||
auto_gchar gchar* temp = ai_get_provider_setting("p_reserved_keys", "temperature");
|
||||
assert_non_null(temp);
|
||||
assert_string_equal("0.7", temp);
|
||||
}
|
||||
|
||||
void
|
||||
test_ai_get_provider_setting(void** state)
|
||||
{
|
||||
@@ -937,22 +919,22 @@ test_ai_parse_response_openai_content(void** state)
|
||||
}
|
||||
|
||||
void
|
||||
test_ai_parse_response_legacy_text_format_unsupported(void** state)
|
||||
test_ai_parse_response_perplexity_text(void** state)
|
||||
{
|
||||
/* Legacy Perplexity /v1/agent format: "content" is an array, not a string.
|
||||
* Dropped with the chat-completions alignment, so parsing yields NULL. */
|
||||
const gchar* json = "{\"output\":[{\"content\":[{\"text\":\"Search result\",\"type\":\"output_text\"}]}]}";
|
||||
assert_null(ai_parse_response(json));
|
||||
auto_gchar gchar* out = ai_parse_response(json);
|
||||
assert_non_null(out);
|
||||
assert_string_equal("Search result", out);
|
||||
}
|
||||
|
||||
void
|
||||
test_ai_parse_response_content_preferred_over_text(void** state)
|
||||
test_ai_parse_response_text_preferred_over_content(void** state)
|
||||
{
|
||||
/* Chat-completions "content" is extracted; a stray "text" field is ignored. */
|
||||
/* When both formats are present, "text" wins (Perplexity path tried first). */
|
||||
const gchar* json = "{\"text\":\"from text field\",\"content\":\"from content field\"}";
|
||||
auto_gchar gchar* out = ai_parse_response(json);
|
||||
assert_non_null(out);
|
||||
assert_string_equal("from content field", out);
|
||||
assert_string_equal("from text field", out);
|
||||
}
|
||||
|
||||
void
|
||||
|
||||
@@ -43,7 +43,6 @@ void test_ai_providers_find_case_insensitive(void** state);
|
||||
void test_ai_set_provider_default_model(void** state);
|
||||
void test_ai_get_provider_default_model(void** state);
|
||||
void test_ai_set_provider_setting(void** state);
|
||||
void test_ai_set_provider_setting_reserved_key(void** state);
|
||||
void test_ai_get_provider_setting(void** state);
|
||||
/* Model caching tests */
|
||||
void test_ai_models_are_fresh_initial(void** state);
|
||||
@@ -65,8 +64,8 @@ int ai_client_teardown_with_prefs(void** state);
|
||||
|
||||
/* Chat response parser tests (ai_parse_response) */
|
||||
void test_ai_parse_response_openai_content(void** state);
|
||||
void test_ai_parse_response_legacy_text_format_unsupported(void** state);
|
||||
void test_ai_parse_response_content_preferred_over_text(void** state);
|
||||
void test_ai_parse_response_perplexity_text(void** state);
|
||||
void test_ai_parse_response_text_preferred_over_content(void** state);
|
||||
void test_ai_parse_response_escaped_quote(void** state);
|
||||
void test_ai_parse_response_newline_escape(void** state);
|
||||
void test_ai_parse_response_empty_content(void** state);
|
||||
|
||||
@@ -85,10 +85,6 @@ void
|
||||
ui_resize(void)
|
||||
{
|
||||
}
|
||||
void
|
||||
prof_doupdate(void)
|
||||
{
|
||||
}
|
||||
|
||||
void
|
||||
ui_focus_win(ProfWin* win)
|
||||
|
||||
@@ -752,7 +752,6 @@ main(int argc, char* argv[])
|
||||
cmocka_unit_test_setup_teardown(test_ai_set_provider_default_model, ai_client_setup, ai_client_teardown),
|
||||
cmocka_unit_test_setup_teardown(test_ai_get_provider_default_model, ai_client_setup, ai_client_teardown),
|
||||
cmocka_unit_test_setup_teardown(test_ai_set_provider_setting, ai_client_setup, ai_client_teardown),
|
||||
cmocka_unit_test_setup_teardown(test_ai_set_provider_setting_reserved_key, ai_client_setup, ai_client_teardown),
|
||||
cmocka_unit_test_setup_teardown(test_ai_get_provider_setting, ai_client_setup, ai_client_teardown),
|
||||
/* Model caching tests */
|
||||
cmocka_unit_test_setup_teardown(test_ai_models_are_fresh_initial, ai_client_setup, ai_client_teardown),
|
||||
@@ -775,8 +774,8 @@ main(int argc, char* argv[])
|
||||
cmocka_unit_test(test_ai_json_escape_backslash_quote),
|
||||
/* Chat response parser */
|
||||
cmocka_unit_test(test_ai_parse_response_openai_content),
|
||||
cmocka_unit_test(test_ai_parse_response_legacy_text_format_unsupported),
|
||||
cmocka_unit_test(test_ai_parse_response_content_preferred_over_text),
|
||||
cmocka_unit_test(test_ai_parse_response_perplexity_text),
|
||||
cmocka_unit_test(test_ai_parse_response_text_preferred_over_content),
|
||||
cmocka_unit_test(test_ai_parse_response_escaped_quote),
|
||||
cmocka_unit_test(test_ai_parse_response_newline_escape),
|
||||
cmocka_unit_test(test_ai_parse_response_empty_content),
|
||||
|
||||
Reference in New Issue
Block a user