From 61fbc2ccd7f36f99da65ec4184ee0eec6621a789 Mon Sep 17 00:00:00 2001 From: Michael Vetter Date: Tue, 21 Jul 2026 13:22:08 +0200 Subject: [PATCH 01/18] fix: Only autocomplete open windows for /win For some reason why changed this in below mentioned commit. I don't see any reason why. Let's restore the correct behaviour. Revert: 65aff65596561424c5d85a78114a54c1f98b4529 Signed-off-by: Michael Vetter --- src/command/cmd_ac.c | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/command/cmd_ac.c b/src/command/cmd_ac.c index 29c119ac1..cd8d51b88 100644 --- a/src/command/cmd_ac.c +++ b/src/command/cmd_ac.c @@ -3605,14 +3605,6 @@ static char* _win_autocomplete(ProfWin* window, const char* const input, gboolean previous) { char* result = autocomplete_param_with_func(input, "/win", win_autocomplete, previous, NULL); - - if (result) { - return result; - } - - char* unquoted = strip_arg_quotes(input); - result = autocomplete_param_with_func(unquoted, "/win", roster_contact_autocomplete, previous, NULL); - free(unquoted); return result; } From 5ba21d6d72d5cc84302a0249893f86c024f67402 Mon Sep 17 00:00:00 2001 From: Michael Vetter Date: Tue, 21 Jul 2026 13:41:20 +0200 Subject: [PATCH 02/18] chore(scripts): Add --issue option to changelog-helper.py Enable the changelog generator to extract issue numbers from 'Fixes:' tags. Signed-off-by: Michael Vetter --- scripts/changelog-helper.py | 56 +++++++++++++++++++++++++++++-------- 1 file changed, 44 insertions(+), 12 deletions(-) diff --git a/scripts/changelog-helper.py b/scripts/changelog-helper.py index 9a97c3ef8..ff9550680 100755 --- a/scripts/changelog-helper.py +++ b/scripts/changelog-helper.py @@ -44,13 +44,31 @@ def get_last_tag(): return output[0] if output else None def get_commits(revision_range): - """Get list of (hash, subject) tuples.""" - lines = git_run(["log", revision_range, "--format=%H %s"]) + """Get list of (hash, subject, body) tuples.""" + try: + result = subprocess.run( + ["git", "log", revision_range, "--format=%H%n%s%n%b%n@@@COMMIT-DELIMITER@@@"], + capture_output=True, text=True, check=True + ) + raw_output = result.stdout.strip() + except subprocess.CalledProcessError: + return [] + commits = [] - for line in lines: - parts = line.split(' ', 1) - if len(parts) == 2: - commits.append(parts) + if not raw_output: + return commits + + raw_commits = raw_output.split("@@@COMMIT-DELIMITER@@@") + for raw_c in raw_commits: + raw_c = raw_c.strip() + if not raw_c: + continue + parts = raw_c.split('\n', 2) + if len(parts) >= 2: + c_hash = parts[0].strip() + c_subject = parts[1].strip() + c_body = parts[2].strip() if len(parts) == 3 else "" + commits.append((c_hash, c_subject, c_body)) return commits def get_pr_mappings(revision_range): @@ -97,14 +115,15 @@ def format_description(description): def main(): parser = argparse.ArgumentParser(description="Generate a sorted changelog from git commits.") parser.add_argument("--pr", action="store_true", help="Append PR number to each commit.") + parser.add_argument("--issue", action="store_true", help="Append issue number from Fixes: to each commit.") args = parser.parse_args() last_tag = get_last_tag() if not last_tag: print("No tags found in the repository.", file=sys.stderr) - + revision_range = f"{last_tag}..HEAD" if last_tag else "HEAD" - + commits = get_commits(revision_range) if not commits: print(f"No commits found since {last_tag if last_tag else 'the beginning'}.") @@ -114,25 +133,38 @@ def main(): # Conventional Commit regex: type(scope): description commit_re = re.compile(r'^(\w+)(?:\(([^)]+)\))?:\s*(.*)$') + # Regex to extract issue/PR number from Fixes: tags in body + fixes_re = re.compile(r'^[Ff]ixes:\s*(?:https?://\S+/(?:issues|pull)/|#)(\d+)', re.MULTILINE) grouped = defaultdict(list) others = [] - for c_hash, c_subject in commits: + for c_hash, c_subject, c_body in commits: # Skip merge commits in the output if c_subject.startswith(("Merge pull request", "Merge branch")): continue - pr_suffix = f" (#{pr_map[c_hash]})" if c_hash in pr_map else "" + suffix = "" + assoc_numbers = [] + if args.pr and c_hash in pr_map: + assoc_numbers.append(pr_map[c_hash]) + if args.issue: + fixes_numbers = fixes_re.findall(c_body) + for num in fixes_numbers: + if num not in assoc_numbers: + assoc_numbers.append(num) + if assoc_numbers: + suffix = f" (#" + ", #".join(assoc_numbers) + ")" + match = commit_re.match(c_subject) if match: ctype = match.group(1).lower() ctype = CORRECTIONS.get(ctype, ctype) description = format_description(match.group(3)) - grouped[ctype].append(f"{description}{pr_suffix}") + grouped[ctype].append(f"{description}{suffix}") else: - others.append(f"{c_subject}{pr_suffix}") + others.append(f"{c_subject}{suffix}") # Output sections in ordered priority all_types = TYPE_ORDER + sorted([t for t in grouped if t not in TYPE_ORDER]) From 4e01164c51ae309a10fd350af5996dc80f953aaf Mon Sep 17 00:00:00 2001 From: Michael Vetter Date: Tue, 21 Jul 2026 13:47:08 +0200 Subject: [PATCH 03/18] refactor: Remove unused function get_nick_from_full_jid() Was removed in d9f5a2bd and replaced with the resourcepart of the struct. Ref: d9f5a2bda65ff9b5a147a11caed1031be93809d6 Signed-off-by: Michael Vetter --- src/xmpp/jid.c | 20 -------------------- src/xmpp/jid.h | 2 -- 2 files changed, 22 deletions(-) diff --git a/src/xmpp/jid.c b/src/xmpp/jid.c index 42cad031f..081db7754 100644 --- a/src/xmpp/jid.c +++ b/src/xmpp/jid.c @@ -231,26 +231,6 @@ create_fulljid(const gchar* const barejid, const gchar* const resource) } } -/* - * Get the nickname part of the full JID, e.g. - * Full JID = "test@conference.server/person" - * returns "person" - */ -gchar* -get_nick_from_full_jid(const gchar* const full_room_jid) -{ - auto_gcharv gchar** tokens = g_strsplit(full_room_jid, "/", 0); - gchar* nick_part = NULL; - - if (tokens) { - if (tokens[0] && tokens[1]) { - nick_part = strdup(tokens[1]); - } - } - - return nick_part; -} - /* * get the fulljid, fall back to the barejid */ diff --git a/src/xmpp/jid.h b/src/xmpp/jid.h index 6d7a70a80..fecb0adc2 100644 --- a/src/xmpp/jid.h +++ b/src/xmpp/jid.h @@ -37,8 +37,6 @@ void jid_auto_destroy(Jid** str); gboolean jid_is_valid_room_form(Jid* jid); gchar* create_fulljid(const gchar* const barejid, const gchar* const resource); -gchar* get_nick_from_full_jid(const gchar* const full_room_jid); - const gchar* jid_fulljid_or_barejid(Jid* jid); gchar* jid_random_resource(void); From d38aa6c64a0793da3b0c0456880b9b6b9e3c699c Mon Sep 17 00:00:00 2001 From: Michael Vetter Date: Tue, 21 Jul 2026 13:55:42 +0200 Subject: [PATCH 04/18] refactor: use FREE_SET_NULL and GFREE_SET_NULL macros Replace manual deallocation and nullification blocks with `FREE_SET_NULL` and `GFREE_SET_NULL` macros. Signed-off-by: Michael Vetter --- src/chatlog.c | 5 +---- src/config/account.c | 3 +-- src/config/cafile.c | 3 +-- src/log.c | 3 +-- src/ui/chatwin.c | 16 ++++------------ src/ui/statusbar.c | 6 ++---- 6 files changed, 10 insertions(+), 26 deletions(-) diff --git a/src/chatlog.c b/src/chatlog.c index 5e83ff49a..38e7a11ca 100644 --- a/src/chatlog.c +++ b/src/chatlog.c @@ -417,10 +417,7 @@ static void _free_chat_log(struct dated_chat_log* dated_log) { if (dated_log) { - if (dated_log->filename) { - g_free(dated_log->filename); - dated_log->filename = NULL; - } + GFREE_SET_NULL(dated_log->filename); if (dated_log->date) { g_date_time_unref(dated_log->date); dated_log->date = NULL; diff --git a/src/config/account.c b/src/config/account.c index 76f96bfb2..8bb9137a3 100644 --- a/src/config/account.c +++ b/src/config/account.c @@ -160,8 +160,7 @@ account_eval_password(ProfAccount* account) g_strstrip(account->password); if (account->password[0] == '\0') { log_error("Empty password returned by `eval_password` command."); - g_free(account->password); - account->password = NULL; + GFREE_SET_NULL(account->password); return FALSE; } return TRUE; diff --git a/src/config/cafile.c b/src/config/cafile.c index 8a43ba26b..a315a3cb3 100644 --- a/src/config/cafile.c +++ b/src/config/cafile.c @@ -74,8 +74,7 @@ cafile_get_name(void) * of servers you're trying to connect to are in your OS trust-store */ log_debug("[CAfile] file %s not created yet", cafile); - g_free(cafile); - cafile = NULL; + GFREE_SET_NULL(cafile); } return cafile; } diff --git a/src/log.c b/src/log.c index 3eeeef371..757074e74 100644 --- a/src/log.c +++ b/src/log.c @@ -194,8 +194,7 @@ log_get_filter(void) void log_close(void) { - g_free(mainlogfile); - mainlogfile = NULL; + GFREE_SET_NULL(mainlogfile); if (logp) { fclose(logp); } diff --git a/src/ui/chatwin.c b/src/ui/chatwin.c index 47ead9b49..142f48e3d 100644 --- a/src/ui/chatwin.c +++ b/src/ui/chatwin.c @@ -14,6 +14,7 @@ #include #include +#include "common.h" #include "xmpp/chat_session.h" #include "window_list.h" #include "xmpp/roster_list.h" @@ -504,10 +505,7 @@ chatwin_set_enctext(ProfChatWin* chatwin, const char* const enctext) void chatwin_unset_enctext(ProfChatWin* chatwin) { - if (chatwin->enctext) { - free(chatwin->enctext); - chatwin->enctext = NULL; - } + FREE_SET_NULL(chatwin->enctext); } void @@ -522,10 +520,7 @@ chatwin_set_incoming_char(ProfChatWin* chatwin, const char* const ch) void chatwin_unset_incoming_char(ProfChatWin* chatwin) { - if (chatwin->incoming_char) { - free(chatwin->incoming_char); - chatwin->incoming_char = NULL; - } + FREE_SET_NULL(chatwin->incoming_char); } void @@ -540,10 +535,7 @@ chatwin_set_outgoing_char(ProfChatWin* chatwin, const char* const ch) void chatwin_unset_outgoing_char(ProfChatWin* chatwin) { - if (chatwin->outgoing_char) { - free(chatwin->outgoing_char); - chatwin->outgoing_char = NULL; - } + FREE_SET_NULL(chatwin->outgoing_char); } static void diff --git a/src/ui/statusbar.c b/src/ui/statusbar.c index 82f774bbd..ecc80cecd 100644 --- a/src/ui/statusbar.c +++ b/src/ui/statusbar.c @@ -24,6 +24,7 @@ #include "config/theme.h" #include "config/preferences.h" +#include "common.h" #include "ui/ui.h" #include "ui/statusbar.h" #include "ui/inputwin.h" @@ -471,10 +472,7 @@ _status_bar_draw_time(guint pos) return pos; } - if (statusbar->time) { - g_free(statusbar->time); - statusbar->time = NULL; - } + GFREE_SET_NULL(statusbar->time); GDateTime* datetime = g_date_time_new_now(tz); statusbar->time = g_date_time_format(datetime, time_pref); From 6f3e3f362fbfd699e06144056786387fa4470d67 Mon Sep 17 00:00:00 2001 From: Michael Vetter Date: Tue, 21 Jul 2026 14:18:01 +0200 Subject: [PATCH 05/18] chore: Add donation text to about Signed-off-by: Michael Vetter --- src/ui/console.c | 1 + 1 file changed, 1 insertion(+) diff --git a/src/ui/console.c b/src/ui/console.c index 24ef80129..53a478cfd 100644 --- a/src/ui/console.c +++ b/src/ui/console.c @@ -342,6 +342,7 @@ cons_about(void) win_println(console, THEME_DEFAULT, "-", "This is free software; you are free to change and redistribute it."); win_println(console, THEME_DEFAULT, "-", "There is NO WARRANTY, to the extent permitted by law."); win_println(console, THEME_DEFAULT, "-", ""); + win_println(console, THEME_DEFAULT, "-", "Please consider supporting our development: "); win_println(console, THEME_DEFAULT, "-", "Type '/help' to show complete help."); win_println(console, THEME_DEFAULT, "-", ""); From da86383460e82840768d3bc51e8f644979574de7 Mon Sep 17 00:00:00 2001 From: Michael Vetter Date: Tue, 21 Jul 2026 14:18:27 +0200 Subject: [PATCH 06/18] chore: Add monero to README.md Signed-off-by: Michael Vetter --- README.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index c7789ffbe..8de4143ad 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,12 @@ Our [user guide](https://profanity-im.github.io/userguide.html) contains an [ins We would highly appreciate if you support us via [GitHub Sponsors](https://github.com/sponsors/jubalh/). Especially if you make feature requests or need help using Profanity. Sponsoring enables us to spend time on Profanity. -An alternative way to support us would be to ask for our IBAN or use Bitcoin: `bc1qx265eat7hfasqkqmk9qf38delydnrnuvzhzy0x`. +An alternative way to support us would be: +* to ask for our IBAN +* Bitcoin: `bc1qx265eat7hfasqkqmk9qf38delydnrnuvzhzy0x` +* Monero (XMR): `85Mz3CXUrom2LJSJRBqcbWj5rg7PfkkVrUzAW5MyGzZiTFxbXjkGckAiLFfze6bjAuLTmPoZ4VNgXZVKHhPWgeWg2nWeU69` + +Also see [https://profanity-im.github.io/donate](https://profanity-im.github.io/donate). Thank you! <3 From ce3ec07bde53eacad1cc2dd30b810f63f5d24082 Mon Sep 17 00:00:00 2001 From: Michael Vetter Date: Tue, 21 Jul 2026 15:33:28 +0200 Subject: [PATCH 07/18] refactor: Guard /roster add against invalid JIDs Signed-off-by: Michael Vetter --- src/command/cmd_funcs.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/command/cmd_funcs.c b/src/command/cmd_funcs.c index 826599511..3d1700232 100644 --- a/src/command/cmd_funcs.c +++ b/src/command/cmd_funcs.c @@ -2864,6 +2864,8 @@ cmd_roster(ProfWin* window, const char* const command, gchar** args) gchar* jid = args[1]; if (jid == NULL) { cons_bad_cmd_usage(command); + } else if (!jid_is_valid_user_jid(jid)) { + cons_show_error("Not a valid JID: %s", jid); } else { gchar* name = args[2]; roster_send_add_new(jid, name); From c4081eb1b3141ac8fa7c7a2b9b61c9cd7ce92258 Mon Sep 17 00:00:00 2001 From: Michael Vetter Date: Tue, 21 Jul 2026 15:44:58 +0200 Subject: [PATCH 08/18] refactor: Check the JID in bookmark command in a better way Signed-off-by: Michael Vetter --- src/command/cmd_funcs.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/command/cmd_funcs.c b/src/command/cmd_funcs.c index 3d1700232..97b2ac8d3 100644 --- a/src/command/cmd_funcs.c +++ b/src/command/cmd_funcs.c @@ -4747,7 +4747,8 @@ cmd_bookmark(ProfWin* window, const char* const command, gchar** args) cons_alert(NULL); return TRUE; } - if (strchr(jid, '@') == NULL) { + auto_jid Jid* jidp = jid_create(jid); + if (!jid_is_valid_user_jid(jid) || jidp->resourcepart != NULL) { cons_show("Invalid room, must be of the form room@domain.tld"); cons_show(""); cons_alert(NULL); From b1eb8797c505c03c08952cb008b03d80b2158d6a Mon Sep 17 00:00:00 2001 From: Michael Vetter Date: Tue, 21 Jul 2026 16:06:52 +0200 Subject: [PATCH 09/18] refactor: Add JID validation to /sub and /blocked Signed-off-by: Michael Vetter --- src/command/cmd_funcs.c | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/command/cmd_funcs.c b/src/command/cmd_funcs.c index 97b2ac8d3..56a134a57 100644 --- a/src/command/cmd_funcs.c +++ b/src/command/cmd_funcs.c @@ -1188,6 +1188,11 @@ cmd_sub(ProfWin* window, const char* const command, gchar** args) jid = chatwin->barejid; } + if (!jid_is_valid_user_jid(jid)) { + cons_show_error("Not a valid JID: %s", jid); + return TRUE; + } + auto_jid Jid* jidp = jid_create(jid); if (!jidp) { cons_bad_cmd_usage(command); @@ -3013,6 +3018,11 @@ cmd_blocked(ProfWin* window, const char* const command, gchar** args) return TRUE; } + if (!jid_is_valid(jid)) { + cons_show_error("Not a valid JID: %s", jid); + return TRUE; + } + gboolean res = blocked_add(jid, br, NULL); if (!res) { cons_show("User %s already blocked.", jid); @@ -3027,6 +3037,11 @@ cmd_blocked(ProfWin* window, const char* const command, gchar** args) return TRUE; } + if (!jid_is_valid(args[1])) { + cons_show_error("Not a valid JID: %s", args[1]); + return TRUE; + } + gboolean res = blocked_remove(args[1]); if (!res) { cons_show("User %s is not currently blocked.", args[1]); @@ -3047,6 +3062,11 @@ cmd_blocked(ProfWin* window, const char* const command, gchar** args) return TRUE; } + if (!jid_is_valid(jid)) { + cons_show_error("Not a valid JID: %s", jid); + return TRUE; + } + if (argn >= 3) { msg = args[2]; } From f6cab920dc73f54229c028a31f1c61429ab93d9b Mon Sep 17 00:00:00 2001 From: Michael Vetter Date: Tue, 21 Jul 2026 16:09:42 +0200 Subject: [PATCH 10/18] chore: Heavily extend the profrc.example file Describe all options and their values. Hilight the default. Our users asked often for a config file that they can edit. We were reluctant and told them to use the commands inside Profanity because they will also do checks on the input. However I think it's time to give powerusers what they asked for. Signed-off-by: Michael Vetter --- CONTRIBUTING.md | 2 +- profrc.example | 619 ++++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 595 insertions(+), 26 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c96027705..312e6c825 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -144,7 +144,7 @@ We will then pull from your repository and merge manually. * When fixing a bug, describe it and how your patch fixes it. * When fixing a reported issue add an `Fixes https://github.com/profanity-im/profanity/issues/23` in the commit body. * When adding a new feature add a description of the feature and how it should be used (workflow). -* If your patch adds a new configuration option add this to the `profrc.example` file. +* If your patch adds or changes a configuration option update the `profrc.example` file. * If your patch adds a new theming option add this to the `theme_template` file. * Each patch or pull request should only contain related modifications. * Run the tests and code formatters before submitting (c.f. Chapter 'Check everything' of this README). diff --git a/profrc.example b/profrc.example index c8909acbc..ecb909b58 100644 --- a/profrc.example +++ b/profrc.example @@ -1,62 +1,631 @@ +# ============================================================================== +# [ui] - User Interface +# ============================================================================== [ui] +# Show splash screen on startup. +# Possible values: true, false (Default: true) splash=true -intype=true -beep=false -statuses.muc=all + +# Theme name to load from the themes folder. +# Possible values: default, aqua, gruvbox, solarized-dark, etc. (Default: default) theme=default -history=true -titlebar=true -mouse=false + +# Show active conversation/window title in terminal emulator title. +# Possible values: true, false (Default: true) +wintitle.show=true + +# Set terminal window title to "Goodbye" on exit. +# Possible values: true, false (Default: true) +wintitle.goodbye=true + +# Beep on message notifications. +# Possible values: true, false (Default: false) +beep=false + +# Flash terminal/window manager on new messages. +# Possible values: true, false (Default: false) flash=false + +# Use interactive text input system in chat windows. +# Possible values: true, false (Default: true) +intype=true + +# Use interactive text input system in console window. +# Possible values: true, false (Default: true) +intype.console=true + +# Save and reload scrollback history for windows. +# Possible values: true, false (Default: true) +history=true + +# Clear saved scrollback history files on clean exit. +# Possible values: true, false (Default: false) +clear.persist_history=false + +# Check for Profanity updates on startup. +# Possible values: true, false (Default: false) vercheck=false -statuses.console=all -statuses.chat=all -color.nick=true +# Guard against accidentally sending slash commands as raw text. +# Possible values: true, false (Default: false) +slashguard=false + +# Wrap text lines in chat/MUC window output. +# Possible values: true, false (Default: true) +wrap=true + +# Enable mood publishing and display support (XEP-0107). +# Possible values: true, false (Default: true) +mood=true + +# Milliseconds to wait/block input during UI lag protection. +# Possible values: Integer in milliseconds (Default: 1000) +inpblock=1000 + +# Use dynamic typing blocks. +# Possible values: true, false (Default: true) +inpblock.dynamic=true + +# Warn when sending unencrypted messages to encrypted contacts. +# Possible values: true, false (Default: true) +enc.warn=true + +# Show TLS certificate information upon successful connection. +# Possible values: true, false (Default: true) +tls.show=true + +# Default UI logging verbosity. +# Possible values: all, none (Default: all) +console.chat=all +console.muc=all +console.private=all + +# Color contact nicknames in chat windows. +# Possible values: true, false, theme (Default: false) +color.nick=false + +# Color your own nickname in windows. +# Possible values: true, false (Default: true) +color.nick.own=true + +# Use nick colors in the roster view. +# Possible values: true, false (Default: false) +color.roster.nick=false + +# Use nick colors in the MUC occupants list view. +# Possible values: true, false (Default: false) +color.occupants.nick=false + +# Enable the roster view panel on the side. +# Possible values: true, false (Default: true) +roster=true + +# Width of the roster panel in characters (0 for automatic/default). +# Possible values: Integer (Default: 0) +roster.size=0 + +# Show offline contacts in the roster. +# Possible values: true, false (Default: true) +roster.offline=true + +# Show empty groups in the roster. +# Possible values: true, false (Default: true) +roster.empty=true + +# Show connected resources/clients for roster contacts. +# Possible values: true, false (Default: false) +roster.resource=false + +# Show presence icon/indicator next to roster names. +# Possible values: true, false (Default: false) +roster.presence=false + +# Show status messages directly in the roster. +# Possible values: true, false (Default: false) +roster.status=false + +# Group/Sort roster. +# Possible values: presence, name (Default: presence) +roster.by=presence + +# Sort contacts order in roster. +# Possible values: presence, name (Default: presence) +roster.order=presence + +# Order unread items position. +# Possible values: after, before, none (Default: after) +roster.unread=after + +# Unread items counting format. +# Possible values: unread, all, none (Default: unread) +roster.count=unread + +# Show unread badge count even when it is 0. +# Possible values: true, false (Default: true) +roster.count.zero=true + +# Show presence priority for contact resources. +# Possible values: true, false (Default: true) +roster.priority=true + +# Wrap long roster entries. +# Possible values: true, false (Default: false) +roster.wrap=false + +# Join multiple resources into a comma-separated list. +# Possible values: true, false (Default: true) +roster.resource.join=true + +# Show the standard roster contact list. +# Possible values: true, false (Default: true) +roster.contacts=true + +# Show unsubscribed contacts in the roster. +# Possible values: true, false (Default: true) +roster.unsubscribed=true + +# Show chat rooms list in the roster panel. +# Possible values: true, false (Default: true) +roster.rooms=true + +# Position of the room list in the roster panel. +# Possible values: last, first (Default: last) +roster.rooms.pos=last + +# Sort room list in the roster panel. +# Possible values: name, none (Default: none) +roster.rooms.by=none + +# Sort room list order in the roster panel. +# Possible values: name (Default: name) +roster.rooms.order=name + +# Order unread rooms position. +# Possible values: after, before, none (Default: after) +roster.rooms.unread=after + +# Display format for room titles in the roster. +# Possible values: name, jid (Default: name) +roster.rooms.title=name + +# Placement of private MUC occupant chats in the roster. +# Possible values: room, roster, none (Default: room) +roster.private=room + +# Show occupants list in active MUC chat windows. +# Possible values: true, false (Default: true) +occupants=true + +# Width of occupants panel in characters (0 for automatic/default). +# Possible values: Integer (Default: 0) +occupants.size=0 + +# Show occupant real JIDs when room privileges permit. +# Possible values: true, false (Default: false) +occupants.jid=false + +# Show offline occupants in MUC windows. +# Possible values: true, false (Default: false) +occupants.offline=false + +# Wrap occupant nicknames in occupants panel. +# Possible values: true, false (Default: false) +occupants.wrap=false + +# Show MUC room privilege and role modifications. +# Possible values: true, false (Default: true) +privileges=true + +# Show contact presence updates inside active conversation windows. +# Possible values: true, false (Default: true) +presence=true + +# Resource display toggles. +# Possible values: true, false (Default: true) +resource.title=true +resource.message=true + +# Show the connected account JID/name in the statusbar. +# Possible values: true, false (Default: false) +statusbar.show.name=false + +# Show window numbers in the statusbar. +# Possible values: true, false (Default: true) +statusbar.show.number=true + +# Show read/unread indicators in the statusbar. +# Possible values: true, false (Default: true) +statusbar.show.read=true + +# Display format for own information. +# Possible values: fulljid, barejid, nick, none (Default: fulljid) +statusbar.self=fulljid + +# Display format for active 1:1 chat windows. +# Possible values: user, barejid, fulljid (Default: user) +statusbar.chat=user + +# Display format for active room windows. +# Possible values: name, jid (Default: name) +statusbar.room.title=name + +# Multi-window tab display mode. +# Possible values: default, titles, numbers (Default: default) +statusbar.tabmode=default + +# Limit the number of statusbar tabs shown (0 for unlimited). +# Possible values: Integer (Default: 0) +statusbar.tabs=0 + +# Limit the character length of statusbar tab titles (0 for unlimited). +# Possible values: Integer (Default: 0) +statusbar.tablen=0 + +# Room title layout shown in the titlebar. +# Possible values: name, jid (Default: name) +titlebar.muc.title=name + +# --- Custom prefix characters for list views --- +# Example values: +# roster.header.char=> +# roster.contact.char=* +# roster.resource.char=- +# roster.private.char=p +# roster.rooms.char=# +# roster.rooms.private.char=x +# occupants.char=+ +# occupants.header.char=@ +# correction.char=* + +# --- Window placements/positions --- +# Integer coordinate/order of UI panes (1-4) +titlebar.position=1 +mainwin.position=2 +statusbar.position=3 +inputwin.position=4 + + +# ============================================================================== +# [connection] - XMPP Connectivity, Features & Extensions +# ============================================================================== [connection] +# The default account profile to log in with (profile defined in account storage). +# Possible values: account profile name, e.g., me@server.org (Default: NULL) +account=me@server.org + +# Default global account profile. +# Possible values: profile JID (Default: NULL) +defaccount= + +# Keep-alive ping interval in seconds (0 to disable). +# Possible values: Integer (Default: 60) autoping=60 + +# Keep-alive ping response timeout limit in seconds. +# Possible values: Integer (Default: 0 - libstrophe default) +autoping.timeout=0 + +# MUC room ping check interval/timeout. +# Possible values: Integer in seconds (Default: 0 - disabled) +muc.ping.interval=0 +muc.ping.timeout=0 + +# Reconnection attempt interval in seconds (0 to disable). +# Possible values: Integer (Default: 5) reconnect=5 -account=me@server.org +# Enable Message Carbons (XEP-0280) to sync chats on other devices. +# Possible values: true, false (Default: true) +carbons=true + +# Automatically send delivery receipts to requestors (XEP-0184). +# Possible values: true, false (Default: true) +receipts.send=true + +# Automatically request delivery receipts on outgoing messages. +# Possible values: true, false (Default: false) +receipts.request=false + +# Reveal local OS name via Software Version query responses (XEP-0092). +# Possible values: true, false (Default: true) +reveal.os=true + +# Custom directory/file path to trusted CA TLS certificate bundle. +# Possible values: Path to CA file/directory (Default: NULL) +tls.certpath= + +# Support Last Message Correction (XEP-0308). +# Possible values: true, false (Default: true) +correction.allow=true + +# Support Message Archive Management queries (XEP-0313). +# Possible values: true, false (Default: true) +mam=true + +# Silently discard all incoming messages from non-contacts. +# Possible values: true, false (Default: false) +silence.incoming.nonroster=false + +# libstrophe parser logging verbosity. +# Possible values: Integer, e.g., 0, 1, 2, 3 (Default: 0 - disabled) +strophe.verbosity=0 + +# Enable Stream Management (XEP-0198). +# Possible values: true, false (Default: true) +strophe.sm.enabled=true + +# Automatically resend unacknowledged messages upon recovery. +# Possible values: true, false (Default: true) +strophe.sm.resend=true + + +# ============================================================================== +# [chatstates] - Chat State Notifications (XEP-0085) +# ============================================================================== [chatstates] +# Send chat state notifications (active, typing, paused, gone). +# Possible values: true, false (Default: true) enabled=true + +# Send state notifications in group/MUC chats. +# Possible values: true, false (Default: false) outtype=false + +# Idle minutes before sending "gone" state notification. +# Possible values: Integer in minutes (Default: 10) gone=10 + +# ============================================================================== +# [notifications] - Desktop, Tray, and UI Notification settings +# ============================================================================== [notifications] +# Notification reminder interval in seconds (0 to disable). +# Possible values: Integer (Default: 60) remind=60 + +# Notify on incoming chat room invitation. +# Possible values: true, false (Default: true) invite=true + +# Notify on incoming subscription requests. +# Possible values: true, false (Default: true) sub=true + +# Notify on incoming 1:1 messages. +# Possible values: true, false (Default: true) message=true -room=mention + +# Notify even if the active, focused window receives the message. +# Possible values: true, false (Default: true) message.current=true + +# Include message content text inside notifications. +# Possible values: true, false (Default: true) +message.text=true + +# Notify on any incoming MUC messages. +# Possible values: true, false (Default: false) +room=false + +# Notify only when your nickname is mentioned in a MUC. +# Possible values: true, false (Default: true) +room.mention=true + +# Enable strict nickname casing match for mentions. +# Possible values: true, false (Default: false) +room.mention.casesensitive=false + +# Require nickname mention to be isolated whole words. +# Possible values: true, false (Default: true) +room.mention.wholeword=true + +# Notify when a keyword from the trigger list is matched in MUC. +# Possible values: true, false (Default: false) +room.trigger=false + +# Notify if you receive groupchat messages while being offline. +# Possible values: true, false (Default: true) +room.offline=true + +# Notify even if active focused MUC window receives a message. +# Possible values: true, false (Default: true) room.current=true + +# Include room message content text inside notifications. +# Possible values: true, false (Default: true) +room.text=true + +# Show typing notifications from contacts. +# Possible values: true, false (Default: true) typing=true + +# Show typing notifications even in focused active chat window. +# Possible values: true, false (Default: false) typing.current=false -message.text=true -room.text=true -room.offline=true -[alias] -colour=/color -topic=/subject -friends=/who online friends -bob=/msg bob@server.org hey wassup? +# Enable system tray icon notification support. +# Possible values: true, false (Default: false) +tray=false + +# Clear tray icon status when the conversation is marked read. +# Possible values: true, false (Default: true) +tray.read=true + +# Limit system tray notification display duration (0 to disable). +# Possible values: Integer in seconds (Default: 0) +tray.timer=0 + +# Notify on capabilities/disco changes. +# Possible values: true, false (Default: false) +adv.notify.discoversion=false + +# ============================================================================== +# [logging] - Logging +# ============================================================================== [logging] +# Enable file logging of 1:1 chat conversations. +# Possible values: true, false (Default: false) chlog=true + +# Enable file logging of MUC (group) chat conversations. +# Possible values: true, false (Default: false) grlog=true + +# Save message history in internal SQLite database. +# Possible values: on, off (Default: on) +dblog=on + +# Maximum log file size in bytes before auto-rotation. +# Possible values: Integer, minimum 64 (Default: 0 - disable limit, Example: 1048580) maxsize=1048580 + +# Rotate logs when maxsize limit is reached. +# Possible values: true, false (Default: true) rotate=true + +# Share logs across accounts. +# Possible values: true, false (Default: true) shared=true -[otr] -warn=true -log=redact -policy=manual +# ============================================================================== +# [presence] - User Auto-Away Status Customization +# ============================================================================== [presence] +# Behavior for auto-away. +# Possible values: away, xa (extended away), off (Default: off) autoaway.mode=away -autoaway.time=15 -autoaway.message=Away from computer + +# Check keyboard/screen idle states for autoaway. +# Possible values: true, false (Default: true) autoaway.check=true + +# Idle minutes before switching to "away". +# Possible values: Integer in minutes (Default: 15) +autoaway.awaytime=15 + +# Status message when automatically set to "away". +# Possible values: String (Default: "Away from computer") +autoaway.awaymessage=Away from computer + +# Idle minutes before switching to "xa" (extended away). +# Possible values: Integer in minutes (Default: 45) +autoaway.xatime=45 + +# Status message when automatically set to "xa" (extended away). +# Possible values: String (Default: "Extended away") +autoaway.xamessage=Extended away + +# Enable Last Activity tracking (XEP-0012). +# Possible values: true, false (Default: true) +lastactivity=true + + +# ============================================================================== +# [otr] - Off-the-Record (OTR) Encryption +# ============================================================================== +[otr] +# OTR conversation logging behavior. +# Possible values: on (unencrypted/decrypted), redact (metadata only), off (Default: on) +log=redact + +# OTR usage policy. +# Possible values: manual, opportunistic, always (Default: manual) +policy=manual + +# Encrypt OTR files. +# Possible values: true, false (Default: false) +sendfile=false + + +# ============================================================================== +# [pgp] - Legacy OpenPGP Encryption (XEP-0027) +# ============================================================================== +[pgp] +# Legacy PGP conversation logging behavior. +# Possible values: on, redact, off (Default: on) +log=on + +# Encrypt PGP files. +# Possible values: true, false (Default: false) +sendfile=false + +# Automatically import received PGP keys. +# Possible values: true, false (Default: false) +pgp.pubkey.autoimport=false + + +# ============================================================================== +# [omemo] - OMEMO Encryption (XEP-0384) +# ============================================================================== +[omemo] +# OMEMO conversation logging behavior. +# Possible values: on, redact, off (Default: on) +log=on + +# OMEMO usage policy. +# Possible values: manual, automatic, always (Default: automatic) +policy=automatic + +# Security device/key verification trust policy. +# Possible values: manual, first-use, blind (Default: manual) +trustmode=manual + + +# ============================================================================== +# [ox] - OpenPGP for XMPP Encryption (XEP-0373) +# ============================================================================== +[ox] +# OX conversation logging behavior. +# Possible values: on, redact, off (Default: on) +log=on + +# Automatically encrypt files sent during an OX session. +# Possible values: true, false (Default: true if OMEMO enabled, else false) +encryptfile=true + + +# ============================================================================== +# [spellcheck] - Interactive Spell Checking +# ============================================================================== +[spellcheck] +# Enable live terminal typing spell check checking. +# Possible values: true, false (Default: false) +enabled=false + +# Dictionary locale used for spell checks. +# Possible values: Language code, e.g., en_US, en_GB, de_DE (Default: en_US) +lang=en_US + + +# ============================================================================== +# [executables] - Executables +# ============================================================================== +[executables] +# Command used to open clicked or selected links. +# Possible values: Shell command with %u placeholder (Default: "xdg-open %u") +url.open.cmd=xdg-open %u + +# Command used to download/save incoming files/URLs. +# Possible values: Shell command with placeholders, or empty for built-in downloader. (Default: empty) +url.save.cmd= + +# Command used to display contact avatar image files. +# Possible values: Shell command with %p placeholder (Default: "xdg-open %p") +avatar.cmd=xdg-open %p + +# Command used to open vCard profile photos. +# Possible values: Shell command with %p placeholder (Default: "xdg-open %p") +vcard.photo.cmd=xdg-open %p + + +# ============================================================================== +# [alias] - Custom Command Shortcuts +# ============================================================================== +[alias] +colour=/color +topic=/subject +friends=/who online friends +bob=/msg bob@server.org hey wassup? From 1f72e7e5d7773497ba619025d2766f65b0177301 Mon Sep 17 00:00:00 2001 From: Michael Vetter Date: Tue, 21 Jul 2026 21:14:51 +0200 Subject: [PATCH 11/18] refactor: make MAM timestamp conversion robust Replace fragile manual string building of timezone with more robust glib solution. Previously it was assumed a specific length and timezone offset suffix (+00:00). This broke if the server used a different timezone offset representation (like 'Z' or +0000). Signed-off-by: Michael Vetter --- src/xmpp/iq.c | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/src/xmpp/iq.c b/src/xmpp/iq.c index 5d62361ef..04810657f 100644 --- a/src/xmpp/iq.c +++ b/src/xmpp/iq.c @@ -17,6 +17,7 @@ #include #include "profanity.h" +#include "common.h" #include "log.h" #include "config/preferences.h" #include "event/server_events.h" @@ -3034,20 +3035,20 @@ _mam_rsm_id_handler(xmpp_stanza_t* const stanza, void* const userdata) buffer_remove_entry(window->layout->buffer, 0); - auto_char char* start_str = NULL; + auto_gchar gchar* start_str = NULL; if (data->start_datestr) { - start_str = strdup(data->start_datestr); - // Convert to iso8601 - if (start_str && strlen(start_str) >= 3) { - start_str[strlen(start_str) - 3] = '\0'; + GDateTime* dt = g_date_time_new_from_iso8601(data->start_datestr, NULL); + if (dt) { + start_str = prof_date_time_format_iso8601(dt); + g_date_time_unref(dt); } } - auto_char char* end_str = NULL; + auto_gchar gchar* end_str = NULL; if (data->end_datestr) { - end_str = strdup(data->end_datestr); - // Convert to iso8601 - if (end_str && strlen(end_str) >= 3) { - end_str[strlen(end_str) - 3] = '\0'; + GDateTime* dt = g_date_time_new_from_iso8601(data->end_datestr, NULL); + if (dt) { + end_str = prof_date_time_format_iso8601(dt); + g_date_time_unref(dt); } } From b3a1b967ce6fd54af6d827acc9a896ab39bf82a5 Mon Sep 17 00:00:00 2001 From: Michael Vetter Date: Tue, 21 Jul 2026 21:26:02 +0200 Subject: [PATCH 12/18] refactor: modernise timestamp and ISO8601 conversions Replace all occurrences of legacy GTimeVal timestamp formatting and parsing with the modern GDateTime. Signed-off-by: Michael Vetter --- src/config/accounts.c | 6 ++---- src/event/server_events.c | 11 ++++------- src/plugins/plugins.c | 9 +-------- src/xmpp/stanza.c | 24 ++++++++++++------------ 4 files changed, 19 insertions(+), 31 deletions(-) diff --git a/src/config/accounts.c b/src/config/accounts.c index 3938f4a3f..f0f7e4e32 100644 --- a/src/config/accounts.c +++ b/src/config/accounts.c @@ -824,12 +824,10 @@ accounts_set_last_activity(const char* const account_name) auto_gchar gchar* sanitized_account_name = _sanitize_account_name(account_name); if (_accounts_has_group(sanitized_account_name)) { GDateTime* nowdt = g_date_time_new_now_utc(); - GTimeVal nowtv; - gboolean res = g_date_time_to_timeval(nowdt, &nowtv); + auto_gchar gchar* timestr = prof_date_time_format_iso8601(nowdt); g_date_time_unref(nowdt); - if (res) { - auto_char char* timestr = g_time_val_to_iso8601(&nowtv); + if (timestr) { g_key_file_set_string(accounts, sanitized_account_name, "last.activity", timestr); _accounts_save(account_name); } diff --git a/src/event/server_events.c b/src/event/server_events.c index 6ff8b153f..03d73ecf3 100644 --- a/src/event/server_events.c +++ b/src/event/server_events.c @@ -159,12 +159,10 @@ sv_ev_roster_received(void) int diff_secs = 0; if (prefs_get_boolean(PREF_LASTACTIVITY) && last_activity_str) { - GTimeVal lasttv; GDateTime* nowdt = g_date_time_new_now_utc(); - gboolean res = g_time_val_from_iso8601(last_activity_str, &lasttv); + GDateTime* lastdt = g_date_time_new_from_iso8601(last_activity_str, NULL); - if (res) { - GDateTime* lastdt = g_date_time_new_from_timeval_utc(&lasttv); + if (lastdt) { GTimeSpan diff_micros = g_date_time_difference(nowdt, lastdt); diff_secs = (diff_micros / 1000) / 1000; @@ -268,10 +266,9 @@ sv_ev_room_history(ProfMessage* message) int msg_is_new = 0; if (last_activity) { - GTimeVal lasttv; + GDateTime* lastdt = g_date_time_new_from_iso8601(last_activity, NULL); - if (g_time_val_from_iso8601(last_activity, &lasttv)) { - GDateTime* lastdt = g_date_time_new_from_timeval_utc(&lasttv); + if (lastdt) { GDateTime* msgdt = message->timestamp; GTimeSpan time_diff = g_date_time_difference(msgdt, lastdt); diff --git a/src/plugins/plugins.c b/src/plugins/plugins.c index ed6428380..fd9065d09 100644 --- a/src/plugins/plugins.c +++ b/src/plugins/plugins.c @@ -658,12 +658,7 @@ void plugins_on_room_history_message(const char* const barejid, const char* const nick, const char* const message, GDateTime* timestamp) { - char* timestamp_str = NULL; - GTimeVal timestamp_tv; - gboolean res = g_date_time_to_timeval(timestamp, ×tamp_tv); - if (res) { - timestamp_str = g_time_val_to_iso8601(×tamp_tv); - } + auto_gchar gchar* timestamp_str = prof_date_time_format_iso8601(timestamp); GList* values = g_hash_table_get_values(plugins); GList* curr = values; @@ -673,8 +668,6 @@ plugins_on_room_history_message(const char* const barejid, const char* const nic curr = g_list_next(curr); } g_list_free(values); - - g_free(timestamp_str); } char* diff --git a/src/xmpp/stanza.c b/src/xmpp/stanza.c index c14e067dc..90087ab86 100644 --- a/src/xmpp/stanza.c +++ b/src/xmpp/stanza.c @@ -1133,19 +1133,19 @@ stanza_get_delay(xmpp_stanza_t* const stanza) static GDateTime* _stanza_get_delay_timestamp_xep0203(xmpp_stanza_t* const delay_stanza) { - GTimeVal utc_stamp; const char* xmlns = xmpp_stanza_get_attribute(delay_stanza, STANZA_ATTR_XMLNS); if (xmlns && (g_strcmp0(xmlns, "urn:xmpp:delay") == 0)) { const char* stamp = xmpp_stanza_get_attribute(delay_stanza, STANZA_ATTR_STAMP); - if (stamp && (g_time_val_from_iso8601(stamp, &utc_stamp))) { - + if (stamp) { GDateTime* datetime = g_date_time_new_from_iso8601(stamp, NULL); - GDateTime* local_datetime = g_date_time_to_local(datetime); - g_date_time_unref(datetime); + if (datetime) { + GDateTime* local_datetime = g_date_time_to_local(datetime); + g_date_time_unref(datetime); - return local_datetime; + return local_datetime; + } } } @@ -1155,18 +1155,18 @@ _stanza_get_delay_timestamp_xep0203(xmpp_stanza_t* const delay_stanza) static GDateTime* _stanza_get_delay_timestamp_xep0091(xmpp_stanza_t* const x_stanza) { - GTimeVal utc_stamp; const char* xmlns = xmpp_stanza_get_attribute(x_stanza, STANZA_ATTR_XMLNS); if (xmlns && (g_strcmp0(xmlns, "jabber:x:delay") == 0)) { const char* stamp = xmpp_stanza_get_attribute(x_stanza, STANZA_ATTR_STAMP); - if (stamp && (g_time_val_from_iso8601(stamp, &utc_stamp))) { - + if (stamp) { GDateTime* datetime = g_date_time_new_from_iso8601(stamp, NULL); - GDateTime* local_datetime = g_date_time_to_local(datetime); - g_date_time_unref(datetime); + if (datetime) { + GDateTime* local_datetime = g_date_time_to_local(datetime); + g_date_time_unref(datetime); - return local_datetime; + return local_datetime; + } } } From 41f6ea00d9a6b464e51e3d274f3b04aa1e6a81ad Mon Sep 17 00:00:00 2001 From: Michael Vetter Date: Tue, 21 Jul 2026 21:34:01 +0200 Subject: [PATCH 13/18] refactor: Format openpgp timestamp thread safely Replace the not thread safe localtime and strftime manual date formatting with modern GLib GDateTime. Using localtime inside multithreaded environments can lead to race conditions. Signed-off-by: Michael Vetter --- src/xmpp/message.c | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/xmpp/message.c b/src/xmpp/message.c index f01d22497..d739486a2 100644 --- a/src/xmpp/message.c +++ b/src/xmpp/message.c @@ -1742,10 +1742,9 @@ message_is_sent_by_us(const ProfMessage* const message, bool checkOID) static xmpp_stanza_t* _ox_openpgp_signcrypt(xmpp_ctx_t* ctx, const char* const to, const char* const text) { - time_t now = time(NULL); - struct tm* tm = localtime(&now); - char buf[255]; - strftime(buf, sizeof(buf), "%FT%T%z", tm); + GDateTime* nowdt = g_date_time_new_now_local(); + auto_gchar gchar* buf = g_date_time_format(nowdt, "%FT%T%z"); + g_date_time_unref(nowdt); // build rpad int randnr = (rand() % 100) + 1; From 7f95565499d588544527a2d789e760d83873929f Mon Sep 17 00:00:00 2001 From: Michael Vetter Date: Tue, 21 Jul 2026 21:35:11 +0200 Subject: [PATCH 14/18] refactor: Replace obsolete gettimeofday with g_get_real_time Replace legacy gettimeofday and struct timeval API with standard GLib g_get_real_time. gettimeofday is marked as obsolete in POSIX.1-2008. Using g_get_real_time is more modern, portable, and thread safe. Signed-off-by: Michael Vetter --- src/omemo/omemo.c | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/omemo/omemo.c b/src/omemo/omemo.c index cca8d1410..c3d1335f4 100644 --- a/src/omemo/omemo.c +++ b/src/omemo/omemo.c @@ -2010,9 +2010,7 @@ static void _generate_signed_pre_key(void) { session_signed_pre_key* signed_pre_key; - struct timeval tv; - gettimeofday(&tv, NULL); - unsigned long long timestamp = (unsigned long long)(tv.tv_sec) * 1000 + (unsigned long long)(tv.tv_usec) / 1000; + unsigned long long timestamp = g_get_real_time() / 1000; omemo_ctx.signed_pre_key_id = 1; signal_protocol_key_helper_generate_signed_pre_key(&signed_pre_key, omemo_ctx.identity_key_pair, omemo_ctx.signed_pre_key_id, timestamp, omemo_ctx.signal); From 1872b76e015d3770c3fea1f1972d06492d2fc4ee Mon Sep 17 00:00:00 2001 From: Michael Vetter Date: Tue, 21 Jul 2026 21:59:49 +0200 Subject: [PATCH 15/18] refactor(pgp): Modernise string concatenation with g_strconcat Replace unsafe alloca and manual string concatenation with modern GLib g_strconcat. Signed-off-by: Michael Vetter --- src/pgp/ox.c | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/pgp/ox.c b/src/pgp/ox.c index 4d698a7d4..aa6374216 100644 --- a/src/pgp/ox.c +++ b/src/pgp/ox.c @@ -203,13 +203,8 @@ p_ox_gpg_signcrypt(const char* const sender_barejid, const char* const recipient gpgme_set_offline(ctx, 1); gpgme_set_keylist_mode(ctx, GPGME_KEYLIST_MODE_LOCAL); - char* xmpp_jid_me = alloca((strlen(sender_barejid) + 6) * sizeof(char)); - char* xmpp_jid_recipient = alloca((strlen(recipient_barejid) + 6) * sizeof(char)); - - strcpy(xmpp_jid_me, "xmpp:"); - strcpy(xmpp_jid_recipient, "xmpp:"); - strcat(xmpp_jid_me, sender_barejid); - strcat(xmpp_jid_recipient, recipient_barejid); + gchar* xmpp_jid_me = g_strconcat("xmpp:", sender_barejid, NULL); + gchar* xmpp_jid_recipient = g_strconcat("xmpp:", recipient_barejid, NULL); gpgme_signers_clear(ctx); @@ -269,6 +264,8 @@ p_ox_gpg_signcrypt(const char* const sender_barejid, const char* const recipient result = g_base64_encode((unsigned char*)cipher_str, len); cleanup: + g_free(xmpp_jid_me); + g_free(xmpp_jid_recipient); if (cipher_str) gpgme_free(cipher_str); if (plain) From 72336fe41bb1af5063720f17fc7abed504d1e191 Mon Sep 17 00:00:00 2001 From: Michael Vetter Date: Tue, 21 Jul 2026 22:11:47 +0200 Subject: [PATCH 16/18] refactor(xmpp): DRY up delay timestamp parsing in stanza.c Move code from duplicate helper functions _stanza_get_delay_timestamp_xep0203 and _stanza_get_delay_timestamp_xep0091 into parameterized helper _stanza_get_delay_timestamp that accepts the expected XML namespace. Signed-off-by: Michael Vetter --- src/xmpp/stanza.c | 33 ++++++--------------------------- src/xmpp/stanza.h | 2 ++ 2 files changed, 8 insertions(+), 27 deletions(-) diff --git a/src/xmpp/stanza.c b/src/xmpp/stanza.c index 90087ab86..33f5beca3 100644 --- a/src/xmpp/stanza.c +++ b/src/xmpp/stanza.c @@ -1131,11 +1131,11 @@ stanza_get_delay(xmpp_stanza_t* const stanza) } static GDateTime* -_stanza_get_delay_timestamp_xep0203(xmpp_stanza_t* const delay_stanza) +_stanza_get_delay_timestamp(xmpp_stanza_t* const delay_stanza, const char* const expected_xmlns) { const char* xmlns = xmpp_stanza_get_attribute(delay_stanza, STANZA_ATTR_XMLNS); - if (xmlns && (g_strcmp0(xmlns, "urn:xmpp:delay") == 0)) { + if (xmlns && (g_strcmp0(xmlns, expected_xmlns) == 0)) { const char* stamp = xmpp_stanza_get_attribute(delay_stanza, STANZA_ATTR_STAMP); if (stamp) { @@ -1152,27 +1152,6 @@ _stanza_get_delay_timestamp_xep0203(xmpp_stanza_t* const delay_stanza) return NULL; } -static GDateTime* -_stanza_get_delay_timestamp_xep0091(xmpp_stanza_t* const x_stanza) -{ - const char* xmlns = xmpp_stanza_get_attribute(x_stanza, STANZA_ATTR_XMLNS); - - if (xmlns && (g_strcmp0(xmlns, "jabber:x:delay") == 0)) { - const char* stamp = xmpp_stanza_get_attribute(x_stanza, STANZA_ATTR_STAMP); - if (stamp) { - GDateTime* datetime = g_date_time_new_from_iso8601(stamp, NULL); - if (datetime) { - GDateTime* local_datetime = g_date_time_to_local(datetime); - g_date_time_unref(datetime); - - return local_datetime; - } - } - } - - return NULL; -} - GDateTime* stanza_get_delay_from(xmpp_stanza_t* const stanza, gchar* from) { @@ -1186,7 +1165,7 @@ stanza_get_delay_from(xmpp_stanza_t* const stanza, gchar* from) } if (delay) { - return _stanza_get_delay_timestamp_xep0203(delay); + return _stanza_get_delay_timestamp(delay, STANZA_NS_DELAY); } // otherwise check for XEP-0091 legacy delayed delivery @@ -1198,7 +1177,7 @@ stanza_get_delay_from(xmpp_stanza_t* const stanza, gchar* from) } if (delay) { - return _stanza_get_delay_timestamp_xep0091(delay); + return _stanza_get_delay_timestamp(delay, STANZA_NS_X_DELAY); } return NULL; @@ -1216,7 +1195,7 @@ stanza_get_oldest_delay(xmpp_stanza_t* const stanza) child_name = xmpp_stanza_get_name(child); if (child_name && g_strcmp0(child_name, STANZA_NAME_DELAY) == 0) { - GDateTime* tmp = _stanza_get_delay_timestamp_xep0203(child); + GDateTime* tmp = _stanza_get_delay_timestamp(child, STANZA_NS_DELAY); if (oldest == NULL) { oldest = tmp; @@ -1229,7 +1208,7 @@ stanza_get_oldest_delay(xmpp_stanza_t* const stanza) } if (child_name && g_strcmp0(child_name, STANZA_NAME_X) == 0) { - GDateTime* tmp = _stanza_get_delay_timestamp_xep0091(child); + GDateTime* tmp = _stanza_get_delay_timestamp(child, STANZA_NS_X_DELAY); if (oldest == NULL) { oldest = tmp; diff --git a/src/xmpp/stanza.h b/src/xmpp/stanza.h index 466f61c8a..8d1130bee 100644 --- a/src/xmpp/stanza.h +++ b/src/xmpp/stanza.h @@ -203,6 +203,8 @@ #define STANZA_NS_RECEIPTS "urn:xmpp:receipts" #define STANZA_NS_SIGNED "jabber:x:signed" #define STANZA_NS_ENCRYPTED "jabber:x:encrypted" +#define STANZA_NS_DELAY "urn:xmpp:delay" +#define STANZA_NS_X_DELAY "jabber:x:delay" // XEP-0373: OpenPGP for XMPP #define STANZA_NS_OPENPGP_0 "urn:xmpp:openpgp:0" #define STANZA_NS_OPENPGP_0_PUBLIC_KEYS "urn:xmpp:openpgp:0:public-keys" From b601f7b3651ad342cfac98a7b4ca965a2c2a7757 Mon Sep 17 00:00:00 2001 From: Michael Vetter Date: Tue, 21 Jul 2026 22:19:15 +0200 Subject: [PATCH 17/18] refactor(database): Consolidate database limit and count functions Merge log_database_get_limits_info and log_database_get_limits_info_muc into a parameterized _db_get_limits_info helper. Extract the duplicate logic from log_database_get_chat_count and log_database_get_muc_count into _db_get_int_result. Signed-off-by: Michael Vetter --- src/database.c | 99 +++++++++++++++++--------------------------------- 1 file changed, 33 insertions(+), 66 deletions(-) diff --git a/src/database.c b/src/database.c index 41443e4f6..d378c19da 100644 --- a/src/database.c +++ b/src/database.c @@ -32,6 +32,7 @@ static sqlite3* g_chatlog_database; static gboolean _add_to_db(ProfMessage* message, char* type, const Jid* const from_jid, const Jid* const to_jid); static char* _get_db_filename(ProfAccount* account); static prof_msg_type_t _get_message_type_type(const char* const type); +static const char* _get_message_type_str(prof_msg_type_t type); static prof_enc_t _get_message_enc_type(const char* const encstr); static int _get_db_version(void); static gboolean _migrate_to_v2(void); @@ -256,31 +257,31 @@ log_database_add_outgoing_muc_pm(const char* const id, const char* const barejid _log_database_add_outgoing("mucpm", id, barejid, message, replace_id, enc); } -// Get info (timestamp and stanza_id) of the first or last message in db (personal chats) -ProfMessage* -log_database_get_limits_info(const gchar* const contact_barejid, gboolean is_last) +static ProfMessage* +_db_get_limits_info(prof_msg_type_t type, const char* const jid, gboolean is_last) { sqlite3_stmt* stmt = NULL; const Jid* myjid = connection_get_jid(); if (!myjid->str) return NULL; + const char* type_str = _get_message_type_str(type); const char* order = is_last ? "DESC" : "ASC"; auto_sqlite char* query = sqlite3_mprintf("SELECT `archive_id`, `timestamp` FROM `ChatLogs` WHERE " - "`type` = 'chat' AND (" + "`type` = %Q AND (" "(`from_jid` = %Q AND `to_jid` = %Q) OR " "(`from_jid` = %Q AND `to_jid` = %Q)) " "ORDER BY `timestamp` %s LIMIT 1;", - contact_barejid, myjid->barejid, myjid->barejid, contact_barejid, order); + type_str, jid, myjid->barejid, myjid->barejid, jid, order); if (!query) { - log_error("Could not allocate memory for SQL query in log_database_get_limits_info()"); + log_error("Could not allocate memory for SQL query in _db_get_limits_info()"); return NULL; } int rc = sqlite3_prepare_v2(g_chatlog_database, query, -1, &stmt, NULL); if (rc != SQLITE_OK) { - log_error("Unknown SQLite error in log_database_get_last_info()."); + log_error("Unknown SQLite error in _db_get_limits_info()."); return NULL; } @@ -301,49 +302,18 @@ log_database_get_limits_info(const gchar* const contact_barejid, gboolean is_las return msg; } +// Get info (timestamp and stanza_id) of the first or last message in db (personal chats) +ProfMessage* +log_database_get_limits_info(const gchar* const contact_barejid, gboolean is_last) +{ + return _db_get_limits_info(PROF_MSG_TYPE_CHAT, contact_barejid, is_last); +} + // Get info (timestamp and stanza_id) of the first or last message in db (MUCs) ProfMessage* log_database_get_limits_info_muc(const gchar* const room_jid, gboolean is_last) { - sqlite3_stmt* stmt = NULL; - const Jid* myjid = connection_get_jid(); - if (!myjid->str) - return NULL; - - const char* order = is_last ? "DESC" : "ASC"; - auto_sqlite char* query = sqlite3_mprintf("SELECT `archive_id`, `timestamp` FROM `ChatLogs` WHERE " - "`type` = 'muc' AND (" - "(`from_jid` = %Q AND `to_jid` = %Q) OR " - "(`from_jid` = %Q AND `to_jid` = %Q)) " - "ORDER BY `timestamp` %s LIMIT 1;", - room_jid, myjid->barejid, myjid->barejid, room_jid, order); - - if (!query) { - log_error("Could not allocate memory for SQL query in log_database_get_limits_info_muc()"); - return NULL; - } - - int rc = sqlite3_prepare_v2(g_chatlog_database, query, -1, &stmt, NULL); - if (rc != SQLITE_OK) { - log_error("Unknown SQLite error in log_database_get_limits_info_muc()."); - return NULL; - } - - ProfMessage* msg = message_init(); - - if (sqlite3_step(stmt) == SQLITE_ROW) { - char* archive_id = (char*)sqlite3_column_text(stmt, 0); - char* date = (char*)sqlite3_column_text(stmt, 1); - - msg->stanzaid = _db_strdup(archive_id); - msg->timestamp = g_date_time_new_from_iso8601(date, NULL); - } else { - message_free(msg); - msg = NULL; - } - sqlite3_finalize(stmt); - - return msg; + return _db_get_limits_info(PROF_MSG_TYPE_MUC, room_jid, is_last); } // Query previous chats, constraints start_time and end_time. If end_time is @@ -482,10 +452,24 @@ log_database_get_previous_muc(const gchar* const room_jid, const char* start_tim return history; } +static int +_db_get_int_result(const char* query) +{ + sqlite3_stmt* stmt = NULL; + int count = 0; + int rc = sqlite3_prepare_v2(g_chatlog_database, query, -1, &stmt, NULL); + if (rc == SQLITE_OK) { + if (sqlite3_step(stmt) == SQLITE_ROW) { + count = sqlite3_column_int(stmt, 0); + } + } + sqlite3_finalize(stmt); + return count; +} + int log_database_get_chat_count(const gchar* const contact_barejid, const char* start_time, const char* end_time) { - sqlite3_stmt* stmt = NULL; const Jid* myjid = connection_get_jid(); if (!myjid->str) return 0; @@ -497,21 +481,12 @@ log_database_get_chat_count(const gchar* const contact_barejid, const char* star "AND `timestamp` >= %Q AND `timestamp` <= %Q;", contact_barejid, myjid->barejid, myjid->barejid, contact_barejid, start_time, end_time); - int count = 0; - int rc = sqlite3_prepare_v2(g_chatlog_database, query, -1, &stmt, NULL); - if (rc == SQLITE_OK) { - if (sqlite3_step(stmt) == SQLITE_ROW) { - count = sqlite3_column_int(stmt, 0); - } - } - sqlite3_finalize(stmt); - return count; + return _db_get_int_result(query); } int log_database_get_muc_count(const gchar* const room_jid, const char* start_time, const char* end_time) { - sqlite3_stmt* stmt = NULL; const Jid* myjid = connection_get_jid(); if (!myjid->str) return 0; @@ -522,15 +497,7 @@ log_database_get_muc_count(const gchar* const room_jid, const char* start_time, "AND `timestamp` >= %Q AND `timestamp` <= %Q;", room_jid, room_jid, start_time, end_time); - int count = 0; - int rc = sqlite3_prepare_v2(g_chatlog_database, query, -1, &stmt, NULL); - if (rc == SQLITE_OK) { - if (sqlite3_step(stmt) == SQLITE_ROW) { - count = sqlite3_column_int(stmt, 0); - } - } - sqlite3_finalize(stmt); - return count; + return _db_get_int_result(query); } static const char* From 31e6f4b2d41df0dfb33ea2f82a08d0a80b4678a0 Mon Sep 17 00:00:00 2001 From: Michael Vetter Date: Tue, 21 Jul 2026 22:24:45 +0200 Subject: [PATCH 18/18] refactor(database): Consolidate SQLite history parsing functions Consolidate SQLite message log parsing loops from chat and MUC history queries. Signed-off-by: Michael Vetter --- src/database.c | 81 ++++++++++++++++++++------------------------------ 1 file changed, 33 insertions(+), 48 deletions(-) diff --git a/src/database.c b/src/database.c index d378c19da..b9b39eef6 100644 --- a/src/database.c +++ b/src/database.c @@ -316,6 +316,37 @@ log_database_get_limits_info_muc(const gchar* const room_jid, gboolean is_last) return _db_get_limits_info(PROF_MSG_TYPE_MUC, room_jid, is_last); } +static GSList* +_db_parse_history_messages(sqlite3_stmt* stmt) +{ + GSList* history = NULL; + + while (sqlite3_step(stmt) == SQLITE_ROW) { + char* message = (char*)sqlite3_column_text(stmt, 0); + char* date = (char*)sqlite3_column_text(stmt, 1); + char* from_jid = (char*)sqlite3_column_text(stmt, 2); + char* from_resource = (char*)sqlite3_column_text(stmt, 3); + char* to_jid = (char*)sqlite3_column_text(stmt, 4); + char* to_resource = (char*)sqlite3_column_text(stmt, 5); + char* type = (char*)sqlite3_column_text(stmt, 6); + char* encryption = (char*)sqlite3_column_text(stmt, 7); + char* id = (char*)sqlite3_column_text(stmt, 8); + + ProfMessage* msg = message_init(); + msg->id = id ? strdup(id) : NULL; + msg->from_jid = jid_create_from_bare_and_resource(from_jid, from_resource); + msg->to_jid = jid_create_from_bare_and_resource(to_jid, to_resource); + msg->plain = strdup(message ?: ""); + msg->timestamp = g_date_time_new_from_iso8601(date, NULL); + msg->type = _get_message_type_type(type); + msg->enc = _get_message_enc_type(encryption); + + history = g_slist_append(history, msg); + } + + return history; +} + // Query previous chats, constraints start_time and end_time. If end_time is // null the current time is used. from_start gets first few messages if true // otherwise the last ones. Flip flips the order of the results @@ -355,30 +386,7 @@ log_database_get_previous_chat(const gchar* const contact_barejid, const char* s return NULL; } - GSList* history = NULL; - - while (sqlite3_step(stmt) == SQLITE_ROW) { - char* message = (char*)sqlite3_column_text(stmt, 0); - char* date = (char*)sqlite3_column_text(stmt, 1); - char* from_jid = (char*)sqlite3_column_text(stmt, 2); - char* from_resource = (char*)sqlite3_column_text(stmt, 3); - char* to_jid = (char*)sqlite3_column_text(stmt, 4); - char* to_resource = (char*)sqlite3_column_text(stmt, 5); - char* type = (char*)sqlite3_column_text(stmt, 6); - char* encryption = (char*)sqlite3_column_text(stmt, 7); - char* id = (char*)sqlite3_column_text(stmt, 8); - - ProfMessage* msg = message_init(); - msg->id = id ? strdup(id) : NULL; - msg->from_jid = jid_create_from_bare_and_resource(from_jid, from_resource); - msg->to_jid = jid_create_from_bare_and_resource(to_jid, to_resource); - msg->plain = strdup(message ?: ""); - msg->timestamp = g_date_time_new_from_iso8601(date, NULL); - msg->type = _get_message_type_type(type); - msg->enc = _get_message_enc_type(encryption); - - history = g_slist_append(history, msg); - } + GSList* history = _db_parse_history_messages(stmt); sqlite3_finalize(stmt); return history; @@ -423,30 +431,7 @@ log_database_get_previous_muc(const gchar* const room_jid, const char* start_tim return NULL; } - GSList* history = NULL; - - while (sqlite3_step(stmt) == SQLITE_ROW) { - char* message = (char*)sqlite3_column_text(stmt, 0); - char* date = (char*)sqlite3_column_text(stmt, 1); - char* from_jid = (char*)sqlite3_column_text(stmt, 2); - char* from_resource = (char*)sqlite3_column_text(stmt, 3); - char* to_jid = (char*)sqlite3_column_text(stmt, 4); - char* to_resource = (char*)sqlite3_column_text(stmt, 5); - char* type = (char*)sqlite3_column_text(stmt, 6); - char* encryption = (char*)sqlite3_column_text(stmt, 7); - char* id = (char*)sqlite3_column_text(stmt, 8); - - ProfMessage* msg = message_init(); - msg->id = id ? strdup(id) : NULL; - msg->from_jid = jid_create_from_bare_and_resource(from_jid, from_resource); - msg->to_jid = jid_create_from_bare_and_resource(to_jid, to_resource); - msg->plain = strdup(message ?: ""); - msg->timestamp = g_date_time_new_from_iso8601(date, NULL); - msg->type = _get_message_type_type(type); - msg->enc = _get_message_enc_type(encryption); - - history = g_slist_append(history, msg); - } + GSList* history = _db_parse_history_messages(stmt); sqlite3_finalize(stmt); return history;