Merge branch 'master' into merge/upstream-full
Some checks failed
CI Code / Check spelling (pull_request) Successful in 17s
CI Code / Check coding style (pull_request) Successful in 31s
CI Code / Linux (debian) (pull_request) Failing after 39s
CI Code / Linux (ubuntu) (pull_request) Failing after 42s
CI Code / Linux (arch) (pull_request) Failing after 4m59s
CI Code / Code Coverage (pull_request) Failing after 4m48s

Resolve conflicts after master gained the flat-file database backend,
AI client, and other cproof-fork features:

- CHANGELOG: keep cproof-fork unreleased section atop full upstream
  history (0.17.0 / 0.16.0 / 0.15.1)
- src/command/cmd_ac.c: keep both upstream spellcheck AC and cproof
  /history switch|verify|export|import autocompletion
- src/config/preferences.c: merge upstream [spellcheck] group with the
  cproof [ai]/[ai/<provider>] groups
- src/database.c: take master (thin dispatcher); upstream SQLite
  improvements (v3 migration, auto_gchar) belong in database_sqlite.c
  and will land in a follow-up commit
- src/tools/autocomplete.c: take upstream unescape of search_str
- src/ui/statusbar.c: keep guint signatures (per project preference)
  and adapt _status_bar_draw_dbbackend to guint as well
- src/xmpp/jid.c: combine the new jid_is_valid/jid_is_valid_user_jid
  with the "(null)" legacy-placeholder guard in
  jid_create_from_bare_and_resource; use auto_gchar
- tests/functionaltests/proftest.c: keep unsigned-safe pre-check on
  output_len before subtracting OUTPUT_BUF_SIZE
- tests/unittests/unittests.c: keep upstream subdirectory include
  layout plus cproof-only test_ai_client/database_export/database_stress
- tests/unittests/test_jid.c: drop orphan top-level copy (the
  restructured version lives in tests/unittests/xmpp/test_jid.c)

Upstream is included up to 1ac05754b (release 0.18.0 era). Follow-up:
pull 1ac05754b..upstream/master (esp. 42a849d16 db migration fix and
4749645c7 receipt-request restore), and adapt the SQLite improvements
into database_sqlite.c.
This commit is contained in:
2026-05-18 11:42:48 +03:00
76 changed files with 19454 additions and 1185 deletions

View File

@@ -31,6 +31,7 @@
#define DIR_OMEMO "omemo"
#define DIR_PLUGINS "plugins"
#define DIR_DATABASE "database"
#define DIR_FLATLOG "flatlog"
#define DIR_DOWNLOADS "downloads"
#define DIR_EDITOR "editor"
#define DIR_CERTS "certs"

View File

@@ -42,6 +42,9 @@
#define PREF_GROUP_PLUGINS "plugins"
#define PREF_GROUP_EXECUTABLES "executables"
#define PREF_GROUP_SPELLCHECK "spellcheck"
#define PREF_GROUP_AI "ai"
#define PREF_GROUP_AI_PREFIX "ai/" /* per-provider subsection prefix: [ai/<provider>] */
#define PREF_AI_SETTING_PREFIX "setting." /* per-provider custom setting key prefix */
#define INPBLOCK_DEFAULT 1000
@@ -218,6 +221,69 @@ _prefs_load(void)
g_key_file_remove_key(prefs, PREF_GROUP_UI, "titlebar.muc.title.jid", NULL);
g_key_file_remove_key(prefs, PREF_GROUP_UI, "titlebar.muc.title.name", NULL);
}
/* Migrate legacy flat [ai] layout
* openai_url=..., openai=<token>, openai_models=..., openai_default_model=...
* into per-provider subsections
* [ai/openai] url=..., key=..., models=..., default_model=...
* Idempotent: once the flat keys are gone, this is a no-op. */
if (g_key_file_has_group(prefs, PREF_GROUP_AI)) {
gsize key_count = 0;
auto_gcharv gchar** all_keys = g_key_file_get_keys(prefs, PREF_GROUP_AI, &key_count, NULL);
/* Collect provider names from <name>_url keys first so we can migrate
* the whole bundle (url + token + models + default_model) atomically
* per provider, without misclassifying unrelated flat keys. */
GHashTable* to_migrate = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL);
for (gsize i = 0; i < key_count; i++) {
const gchar* k = all_keys[i];
if (g_str_has_suffix(k, "_url") && !g_str_has_suffix(k, "_models_url")) {
gsize klen = strlen(k);
if (klen > 4) {
g_hash_table_add(to_migrate, g_strndup(k, klen - 4));
}
}
}
GList* provider_names = g_hash_table_get_keys(to_migrate);
for (GList* curr = provider_names; curr; curr = g_list_next(curr)) {
const gchar* name = (const gchar*)curr->data;
auto_gchar gchar* group = g_strconcat(PREF_GROUP_AI_PREFIX, name, NULL);
const struct
{
const gchar* flat_suffix;
const gchar* new_key;
} map[] = {
{ "_url", "url" },
{ "_models", "models" },
{ "_default_model", "default_model" },
{ NULL, NULL },
};
for (int j = 0; map[j].flat_suffix; j++) {
auto_gchar gchar* flat_key = g_strconcat(name, map[j].flat_suffix, NULL);
if (g_key_file_has_key(prefs, PREF_GROUP_AI, flat_key, NULL)) {
auto_gchar gchar* val = g_key_file_get_string(prefs, PREF_GROUP_AI, flat_key, NULL);
if (val) {
g_key_file_set_string(prefs, group, map[j].new_key, val);
}
g_key_file_remove_key(prefs, PREF_GROUP_AI, flat_key, NULL);
}
}
/* Token was stored as the bare provider name in [ai]. */
if (g_key_file_has_key(prefs, PREF_GROUP_AI, name, NULL)) {
auto_gchar gchar* val = g_key_file_get_string(prefs, PREF_GROUP_AI, name, NULL);
if (val) {
g_key_file_set_string(prefs, group, "key", val);
}
g_key_file_remove_key(prefs, PREF_GROUP_AI, name, NULL);
}
}
g_list_free(provider_names);
g_hash_table_destroy(to_migrate);
}
_save_prefs();
boolean_choice_ac = autocomplete_new();
@@ -1753,6 +1819,366 @@ _save_prefs(void)
save_keyfile(&prefs_prof_keyfile);
}
/* ========================================================================
* AI provider preferences
*
* Each provider lives in its own [ai/<name>] subsection of the keyfile with:
* url=...
* key=<api token>
* default_model=...
* models=model1;model2;...
* setting.<custom>=value (e.g. setting.tools=enabled)
* The bare [ai] section keeps only the global "defaults_initialized" sentinel.
* Legacy flat-key layouts are migrated on first load in _prefs_load().
* ======================================================================== */
static gchar*
_ai_section(const char* const provider)
{
return g_strconcat(PREF_GROUP_AI_PREFIX, provider, NULL);
}
static gboolean
_ai_set_string(const char* const provider, const char* const key, const char* const value)
{
if (!provider || !key || !value || !prefs)
return FALSE;
auto_gchar gchar* group = _ai_section(provider);
g_key_file_set_string(prefs, group, key, value);
_save_prefs();
return TRUE;
}
static char*
_ai_get_string(const char* const provider, const char* const key)
{
if (!provider || !key || !prefs)
return NULL;
auto_gchar gchar* group = _ai_section(provider);
if (!g_key_file_has_key(prefs, group, key, NULL))
return NULL;
return g_key_file_get_string(prefs, group, key, NULL);
}
static gboolean
_ai_remove_string(const char* const provider, const char* const key)
{
if (!provider || !key || !prefs)
return FALSE;
auto_gchar gchar* group = _ai_section(provider);
if (!g_key_file_has_key(prefs, group, key, NULL))
return FALSE;
g_key_file_remove_key(prefs, group, key, NULL);
_save_prefs();
return TRUE;
}
/* --- Token --- */
gboolean
prefs_ai_set_token(const char* const provider, const char* const token)
{
if (!provider || !prefs)
return FALSE;
if (!token)
return prefs_ai_remove_token(provider);
return _ai_set_string(provider, "key", token);
}
gboolean
prefs_ai_remove_token(const char* const provider)
{
return _ai_remove_string(provider, "key");
}
char*
prefs_ai_get_token(const char* const provider)
{
return _ai_get_string(provider, "key");
}
GList*
prefs_ai_list_tokens(void)
{
if (!prefs)
return NULL;
GList* result = NULL;
GList* providers = prefs_ai_list_providers();
for (GList* curr = providers; curr; curr = g_list_next(curr)) {
const gchar* name = (const gchar*)curr->data;
auto_gchar gchar* tok = prefs_ai_get_token(name);
if (tok && tok[0] != '\0') {
result = g_list_append(result, g_strdup(name));
}
}
prefs_free_ai_providers(providers);
return result;
}
void
prefs_free_ai_tokens(GList* tokens)
{
if (tokens) {
g_list_free_full(tokens, g_free);
}
}
/* --- Models cache --- */
gboolean
prefs_ai_set_models(const char* const provider, const gchar* const* models, gint count)
{
if (!provider || count <= 0 || !prefs)
return FALSE;
GString* model_str = g_string_new("");
for (int i = 0; i < count; i++) {
if (i > 0)
g_string_append_c(model_str, ';');
g_string_append(model_str, models[i]);
}
gboolean ok = _ai_set_string(provider, "models", model_str->str);
g_string_free(model_str, TRUE);
return ok;
}
gboolean
prefs_ai_remove_models(const char* const provider)
{
return _ai_remove_string(provider, "models");
}
GList*
prefs_ai_get_models(const char* const provider)
{
auto_gchar gchar* model_str = _ai_get_string(provider, "models");
if (!model_str || model_str[0] == '\0')
return NULL;
GList* result = NULL;
gchar** models = g_strsplit(model_str, ";", -1);
for (int i = 0; models[i] != NULL; i++) {
result = g_list_append(result, g_strdup(models[i]));
}
g_strfreev(models);
return result;
}
void
prefs_free_ai_models(GList* models)
{
if (models) {
g_list_free_full(models, g_free);
}
}
/* --- Default model --- */
gboolean
prefs_ai_set_default_model(const char* const provider, const char* const model)
{
if (!provider || !model || !prefs)
return FALSE;
if (model[0] == '\0')
return prefs_ai_remove_default_model(provider);
return _ai_set_string(provider, "default_model", model);
}
gboolean
prefs_ai_remove_default_model(const char* const provider)
{
return _ai_remove_string(provider, "default_model");
}
char*
prefs_ai_get_default_model(const char* const provider)
{
return _ai_get_string(provider, "default_model");
}
/* --- Provider URL --- */
gboolean
prefs_ai_set_provider(const char* const provider, const char* const url)
{
if (!provider || !url)
return FALSE;
return _ai_set_string(provider, "url", url);
}
gboolean
prefs_ai_remove_provider(const char* const provider)
{
/* Provider presence is identified by the url key; removing it is what
* makes prefs_ai_list_providers stop returning this name. The remaining
* sub-keys are wiped through prefs_ai_remove_section(). */
return _ai_remove_string(provider, "url");
}
char*
prefs_ai_get_provider_url(const char* const provider)
{
return _ai_get_string(provider, "url");
}
gboolean
prefs_ai_remove_section(const char* const provider)
{
if (!provider || !prefs)
return FALSE;
auto_gchar gchar* group = _ai_section(provider);
if (!g_key_file_has_group(prefs, group))
return FALSE;
g_key_file_remove_group(prefs, group, NULL);
_save_prefs();
return TRUE;
}
GList*
prefs_ai_list_providers(void)
{
if (!prefs)
return NULL;
GList* result = NULL;
gsize len = 0;
auto_gcharv gchar** groups = g_key_file_get_groups(prefs, &len);
if (!groups)
return NULL;
gsize prefix_len = strlen(PREF_GROUP_AI_PREFIX);
for (gsize i = 0; i < len; i++) {
const gchar* g = groups[i];
if (g_str_has_prefix(g, PREF_GROUP_AI_PREFIX) && strlen(g) > prefix_len) {
/* Only count groups that actually identify a provider (have url). */
if (g_key_file_has_key(prefs, g, "url", NULL)) {
result = g_list_append(result, g_strdup(g + prefix_len));
}
}
}
return result;
}
void
prefs_free_ai_providers(GList* providers)
{
if (providers) {
g_list_free_full(providers, g_free);
}
}
GList*
prefs_ai_get_providers(void)
{
GList* configured = prefs_ai_list_providers();
GList* result = NULL;
for (GList* curr = configured; curr; curr = g_list_next(curr)) {
const gchar* name = (const gchar*)curr->data;
auto_gchar gchar* url = prefs_ai_get_provider_url(name);
if (url && strlen(url) > 0) {
result = g_list_append(result, g_strdup(name));
result = g_list_append(result, g_strdup(url));
}
}
prefs_free_ai_providers(configured);
/* Seed openai/perplexity into the in-memory result on very first init
* only. In production the "defaults_initialized" sentinel in [ai] is what
* tells us we've already seeded — a user who removes every provider must
* not get them silently re-added on the next start. When prefs is NULL
* (e.g. unit tests that bring up ai_client without prefs_load) we still
* seed the in-memory list so behaviour matches a fresh first run, but
* skip the persistence write. */
gboolean already_seeded = prefs && g_key_file_get_boolean(prefs, PREF_GROUP_AI, "defaults_initialized", NULL);
if (!already_seeded && !result) {
if (prefs) {
prefs_ai_set_provider("openai", "https://api.openai.com/");
prefs_ai_set_provider("perplexity", "https://api.perplexity.ai/");
}
result = g_list_append(result, g_strdup("openai"));
result = g_list_append(result, g_strdup("https://api.openai.com/"));
result = g_list_append(result, g_strdup("perplexity"));
result = g_list_append(result, g_strdup("https://api.perplexity.ai/"));
}
if (prefs && !already_seeded) {
g_key_file_set_boolean(prefs, PREF_GROUP_AI, "defaults_initialized", TRUE);
_save_prefs();
}
return result;
}
/* --- Custom per-provider settings (setting.<name>) --- */
gboolean
prefs_ai_set_setting(const char* const provider, const char* const setting, const char* const value)
{
if (!provider || !setting)
return FALSE;
auto_gchar gchar* key = g_strconcat(PREF_AI_SETTING_PREFIX, setting, NULL);
if (!value)
return _ai_remove_string(provider, key);
return _ai_set_string(provider, key, value);
}
gboolean
prefs_ai_remove_setting(const char* const provider, const char* const setting)
{
if (!provider || !setting)
return FALSE;
auto_gchar gchar* key = g_strconcat(PREF_AI_SETTING_PREFIX, setting, NULL);
return _ai_remove_string(provider, key);
}
char*
prefs_ai_get_setting(const char* const provider, const char* const setting)
{
if (!provider || !setting)
return NULL;
auto_gchar gchar* key = g_strconcat(PREF_AI_SETTING_PREFIX, setting, NULL);
return _ai_get_string(provider, key);
}
GList*
prefs_ai_list_settings(const char* const provider)
{
if (!provider || !prefs)
return NULL;
auto_gchar gchar* group = _ai_section(provider);
if (!g_key_file_has_group(prefs, group))
return NULL;
GList* result = NULL;
gsize len = 0;
auto_gcharv gchar** keys = g_key_file_get_keys(prefs, group, &len, NULL);
gsize prefix_len = strlen(PREF_AI_SETTING_PREFIX);
for (gsize i = 0; i < len; i++) {
if (g_str_has_prefix(keys[i], PREF_AI_SETTING_PREFIX) && strlen(keys[i]) > prefix_len) {
result = g_list_append(result, g_strdup(keys[i] + prefix_len));
}
}
return result;
}
void
prefs_free_ai_settings(GList* settings)
{
if (settings) {
g_list_free_full(settings, g_free);
}
}
// get the preference group for a specific preference
// for example the PREF_BEEP setting ("beep" in .profrc, see _get_key) belongs
// to the [ui] section.
@@ -1829,6 +2255,7 @@ _get_group(preference_t pref)
case PREF_STATUSBAR_SHOW_NAME:
case PREF_STATUSBAR_SHOW_NUMBER:
case PREF_STATUSBAR_SHOW_READ:
case PREF_STATUSBAR_SHOW_DBBACKEND:
case PREF_STATUSBAR_SELF:
case PREF_STATUSBAR_CHAT:
case PREF_STATUSBAR_ROOM_TITLE:
@@ -1915,6 +2342,9 @@ _get_group(preference_t pref)
case PREF_SPELLCHECK_ENABLE:
case PREF_SPELLCHECK_LANG:
return PREF_GROUP_SPELLCHECK;
case PREF_AI_PROVIDER:
case PREF_AI_API_KEY:
return PREF_GROUP_AI;
default:
return NULL;
}
@@ -2156,6 +2586,8 @@ _get_key(preference_t pref)
return "statusbar.show.number";
case PREF_STATUSBAR_SHOW_READ:
return "statusbar.show.read";
case PREF_STATUSBAR_SHOW_DBBACKEND:
return "statusbar.show.dbbackend";
case PREF_STATUSBAR_SELF:
return "statusbar.self";
case PREF_STATUSBAR_CHAT:
@@ -2192,6 +2624,10 @@ _get_key(preference_t pref)
return "stamp.incoming";
case PREF_OX_LOG:
return "log";
case PREF_AI_PROVIDER:
return "provider";
case PREF_AI_API_KEY:
return "api_key";
case PREF_MOOD:
return "mood";
case PREF_VCARD_PHOTO_CMD:
@@ -2249,6 +2685,7 @@ _get_default_boolean(preference_t pref)
case PREF_ROOM_LIST_CACHE:
case PREF_STATUSBAR_SHOW_NUMBER:
case PREF_STATUSBAR_SHOW_READ:
case PREF_STATUSBAR_SHOW_DBBACKEND:
case PREF_REVEAL_OS:
case PREF_CORRECTION_ALLOW:
case PREF_RECEIPTS_SEND:
@@ -2376,6 +2813,10 @@ _get_default_string(preference_t pref)
return "resend-to-confirm";
case PREF_SPELLCHECK_LANG:
return "en_US";
case PREF_AI_PROVIDER:
return "openai";
case PREF_AI_API_KEY:
return "";
default:
return NULL;
}

View File

@@ -136,6 +136,7 @@ typedef enum {
PREF_STATUSBAR_SHOW_NAME,
PREF_STATUSBAR_SHOW_NUMBER,
PREF_STATUSBAR_SHOW_READ,
PREF_STATUSBAR_SHOW_DBBACKEND,
PREF_STATUSBAR_SELF,
PREF_STATUSBAR_CHAT,
PREF_STATUSBAR_ROOM_TITLE,
@@ -157,6 +158,9 @@ typedef enum {
PREF_OX_LOG,
PREF_MOOD,
PREF_STROPHE_VERBOSITY,
PREF_AI_PROVIDER,
PREF_AI_API_KEY,
PREF_AI_DEFAULT_MODEL,
PREF_STROPHE_SM_ENABLED,
PREF_STROPHE_SM_RESEND,
PREF_VCARD_PHOTO_CMD,
@@ -333,4 +337,42 @@ gboolean prefs_get_room_notify(const char* const roomjid);
gboolean prefs_get_room_notify_mention(const char* const roomjid);
gboolean prefs_get_room_notify_trigger(const char* const roomjid);
/* AI token management */
gboolean prefs_ai_set_token(const char* const provider, const char* const token);
gboolean prefs_ai_remove_token(const char* const provider);
char* prefs_ai_get_token(const char* const provider);
GList* prefs_ai_list_tokens(void);
void prefs_free_ai_tokens(GList* tokens);
/* AI provider management */
gboolean prefs_ai_set_provider(const char* const provider, const char* const url);
gboolean prefs_ai_remove_provider(const char* const provider);
char* prefs_ai_get_provider_url(const char* const provider);
GList* prefs_ai_list_providers(void);
void prefs_free_ai_providers(GList* providers);
GList* prefs_ai_get_providers(void);
/* Wipe the whole [ai/<provider>] subsection (url, key, models, default_model,
* custom settings) in one call. */
gboolean prefs_ai_remove_section(const char* const provider);
/* AI model cache management */
gboolean prefs_ai_set_models(const char* const provider, const gchar* const* models, gint count);
gboolean prefs_ai_remove_models(const char* const provider);
GList* prefs_ai_get_models(const char* const provider);
void prefs_free_ai_models(GList* models);
/* AI default model per provider */
gboolean prefs_ai_set_default_model(const char* const provider, const char* const model);
gboolean prefs_ai_remove_default_model(const char* const provider);
char* prefs_ai_get_default_model(const char* const provider);
/* AI per-provider custom settings (e.g. tools, search) — persisted as
* setting.<name>=<value> inside [ai/<provider>]. Pass NULL value to remove. */
gboolean prefs_ai_set_setting(const char* const provider, const char* const setting, const char* const value);
gboolean prefs_ai_remove_setting(const char* const provider, const char* const setting);
char* prefs_ai_get_setting(const char* const provider, const char* const setting);
GList* prefs_ai_list_settings(const char* const provider);
void prefs_free_ai_settings(GList* settings);
#endif