Compare commits

...

426 Commits

Author SHA1 Message Date
bb3b36e162 fix: address config injection and SIGCHLD handling
- Sanitize account names to prevent GKeyFile config injection by
  blocking special characters like #, \n, and \r.
- Ensure GLib's SIGCHLD handler is initialized before forking
  external editors to prevent zombie processes.
- Add missing error check for OMEMO random key generation.
- Replace hardcoded console tab ID with a named constant.
- Log invalid JIDs safely to prevent potential null pointer issues.
2026-05-22 18:27:54 +00:00
adab585c09 fix(review): address PR #105 review follow-ups
All checks were successful
CI Code / Check coding style (pull_request) Successful in 31s
CI Code / Check spelling (pull_request) Successful in 18s
CI Code / Linux (debian) (pull_request) Successful in 4m45s
CI Code / Linux (ubuntu) (pull_request) Successful in 4m56s
CI Code / Linux (arch) (pull_request) Successful in 5m39s
CI Code / Code Coverage (pull_request) Successful in 6m41s
- src/log.c: log_stderr_init no longer closes STDERR_FILENO immediately
  after dup2(). The previous 'close(dup_fd)' closed fd 2 because dup2
  returns newfd on success; the in-app stderr capture pipe was silently
  dead, dropping libstrophe/openssl error output. Close the original
  stderr_pipe[1] instead and remember that the write end now lives at
  STDERR_FILENO so _log_stderr_close stays correct.

- src/database_sqlite.c: NULL-check sqlite3_mprintf result before
  passing it to sqlite3_exec in the DbVersion bootstrap path.

- src/tools/editor.c: drop three orphan #includes (<fcntl.h>,
  <pthread.h>, <readline/readline.h>) left behind when 9b03e3a50
  removed the async-editor path.

- src/xmpp/jid.c: match the g_new0 allocation in jid_create with
  g_free in jid_destroy. Same behaviour on glibc but stops being a
  foot-gun under custom glib allocators.

Verified with ci-build.sh in Debian docker: all 4 configs pass
(644/0, 605/0, 605/0, 644/0 unit + 130/0 functional).
2026-05-21 15:40:25 +03:00
9b03e3a508 refactor: address PR #105 review (#1222, #1226)
All checks were successful
CI Code / Check spelling (pull_request) Successful in 17s
CI Code / Check coding style (pull_request) Successful in 30s
CI Code / Code Coverage (pull_request) Successful in 2m36s
CI Code / Linux (debian) (pull_request) Successful in 4m39s
CI Code / Linux (ubuntu) (pull_request) Successful in 4m48s
CI Code / Linux (arch) (pull_request) Successful in 5m36s
#1222 (src/xmpp/roster_list.c): remove dead 'resource = NULL' inside
roster_update_presence() — assigning to a value parameter never
affects the caller. Document the ownership contract: the function
consumes 'resource' on every return path and callers must null
their own pointer afterwards. The existing caller in
roster_process_pending_presence already does that, and
resource_destroy() is NULL-safe.

#1226 (src/tools/editor.{c,h}): remove get_message_from_editor[_async]
and the surrounding async-editor infrastructure (editor_thread,
editor_task global, editor_process polling, EditorTask typedef).
The upstream callback-based launch_editor API has replaced every
call site (cmd_funcs.c, inputwin.c). Also drop the now-unused
'background_mode' global (only the obsolete async path ever set it
to TRUE) and the editor_process() call in profanity.c main loop.

Verified with ci-build.sh in Debian docker — all 4 build configs
pass (644/0, 605/0, 605/0, 644/0 unit + 130/0 functional each).
2026-05-21 12:50:19 +03:00
14380c8373 tests: add ui_is_suspended stub (upstream 3326ce5c6)
All checks were successful
CI Code / Check spelling (pull_request) Successful in 18s
CI Code / Check coding style (pull_request) Successful in 31s
CI Code / Code Coverage (pull_request) Successful in 2m28s
CI Code / Linux (ubuntu) (pull_request) Successful in 4m20s
CI Code / Linux (debian) (pull_request) Successful in 6m45s
CI Code / Linux (arch) (pull_request) Successful in 9m36s
Add stub for ui_is_suspended() in tests/unittests/ui/stub_ui.c so
the unit-test binary links cleanly against any code path that calls
it from non-UI translation units.
2026-05-21 12:31:15 +03:00
025051505b fix(receipts): request receipts when capabilities unknown (upstream 4749645c7)
When the receipts-request preference is on but we have no cached
EntityCapabilities for the recipient (new session, bare-jid only,
disco not finished), the previous check via caps_jid_has_feature()
defaulted to 'not supported' and silently suppressed the receipt.
Per XEP-0184 we should send the receipt request unless we have
positive knowledge that the peer does not advertise the feature.

Restore that behaviour: look up caps with caps_lookup(); only flip
request_receipt off when caps are present AND the feature is
missing from the list. If caps are NULL, leave the request enabled
(falls back to the user preference).
2026-05-21 12:30:56 +03:00
fbdef0b9d4 fix(database): port v3 migration into database_sqlite.c and harden init (upstream 42a849d16)
The SQLite-backend code now lives in database_sqlite.c (master split
src/database.c into a thin backend dispatcher); the upstream changes
that PR 105 had folded into the monolithic database.c were not
ported across when we took master's dispatcher version. This brings
forward the v3 schema work and folds in upstream 42a849d16
(fix(database): resolve migration failure and improve init safety):

- latest_version bumped 2 -> 3; ChatLogs.archive_id gains UNIQUE
  constraint; _migrate_to_v3 dedupes existing rows via the
  ChatLogs_v3_migration intermediate table
- DbVersion bookkeeping uses DELETE+INSERT instead of UPDATE so
  multiple rows do not trip the unique-constraint failure reported
  upstream (#2105)
- Initial INSERT INTO DbVersion uses latest_version via
  sqlite3_mprintf, so brand-new databases are stamped at the current
  schema and skip redundant migrations
- server_events.c now checks log_database_init's return so a broken
  DB init is logged instead of silently leaving the handle half-open

(644/0, 605/0, 605/0, 644/0 unit + 130/0 functional each).
2026-05-21 12:30:33 +03:00
dfdca61073 Merge branch 'master' into merge/upstream-full
All checks were successful
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) Successful in 4m48s
CI Code / Linux (ubuntu) (pull_request) Successful in 4m56s
CI Code / Code Coverage (pull_request) Successful in 7m0s
CI Code / Linux (arch) (pull_request) Successful in 10m5s
Bring in master commits since the previous merge:
- 4776c1f1e fix(ai): properly decode \uXXXX JSON escape sequences as UTF-8
- 53cdf488b fix(ai): resolve UAF and require provider argument instead of defaults
- 3b673150b chore(chatlog): disable background message file logging (stage 1)

Conflict in src/chatlog.c: master stubs out all chat_log_* function
bodies (project policy — disable plain-text per-day chatlogs), while
the upstream-merged version in this branch still had upstream's full
implementation. Taking master's version entirely keeps the no-op
chatlog API surface that the ~20 call sites in event/, otr/ and
profanity.c depend on.
2026-05-21 11:58:44 +03:00
49797acbde test(receipts): update caps ver hash for send_receipt_request fixture
Functional test stbbr fixture used the old Profanity caps verification
hash (hAkb1xZdJV9BQpgGNw8zG5Xsals=) for buddy1's simulated presence
and the corresponding disco#info reply. The current build computes
JNkIRQChhYM8+Co3IypmMtMJnOE= for the same identity+features set, so
align the fixture with what the running client now produces.
2026-05-21 11:50:11 +03:00
3b673150b4 chore(chatlog): disable background message file logging (stage 1)
All checks were successful
CI Code / Check spelling (pull_request) Successful in 16s
CI Code / Check coding style (pull_request) Successful in 30s
CI Code / Code Coverage (pull_request) Successful in 2m39s
CI Code / Linux (ubuntu) (pull_request) Successful in 4m58s
CI Code / Linux (arch) (pull_request) Successful in 5m56s
CI Code / Linux (debian) (pull_request) Successful in 7m2s
CI Code / Check spelling (push) Successful in 16s
CI Code / Check coding style (push) Successful in 31s
CI Code / Code Coverage (push) Successful in 2m41s
CI Code / Linux (ubuntu) (push) Successful in 5m3s
CI Code / Linux (arch) (push) Successful in 6m2s
CI Code / Linux (debian) (push) Successful in 7m12s
First stage of removing the chatlog subsystem that writes plain-text
per-day message logs to $XDG_DATA_HOME/profanity/chatlogs/. These files
were never read back by Profanity (history replay reads from the
database via log_database_get_previous_chat), so the feature only ever
wrote to disk for users to inspect with external tools.

This change reduces chatlog.c to no-op stubs, preserving all public
signatures so the ~20 call sites in event/, otr/ and profanity.c
continue to compile and link unchanged. Subsequent stages will remove
the call sites, the related preferences (PREF_CHLOG, PREF_GRLOG and the
per-encryption PREF_*_LOG) and the now-dead commands (/logging,
/otr log, /pgp log, /omemo log, /ox log).

Database-based history (PREF_DBLOG, /history) and the debug log
(log.c, /log) are unaffected.
2026-05-20 13:35:51 +03:00
53cdf488b6 fix(ai): resolve UAF and require provider argument instead of defaults
All checks were successful
CI Code / Check spelling (pull_request) Successful in 18s
CI Code / Check coding style (pull_request) Successful in 50s
CI Code / Linux (debian) (pull_request) Successful in 4m57s
CI Code / Linux (ubuntu) (pull_request) Successful in 5m12s
CI Code / Code Coverage (pull_request) Successful in 6m48s
CI Code / Linux (arch) (pull_request) Successful in 11m9s
CI Code / Check spelling (push) Successful in 16s
CI Code / Check coding style (push) Successful in 33s
CI Code / Code Coverage (push) Successful in 2m41s
CI Code / Linux (debian) (push) Successful in 4m40s
CI Code / Linux (ubuntu) (push) Successful in 4m53s
CI Code / Linux (arch) (push) Successful in 10m41s
This enforces explicit provider specification.
Automatic fallback to preferences or "openai" is removed.
Users must now pass the provider name explicitly.

default provider functionality was improperly removed and implemented (some parts still remain intact), leading to UAF.
2026-05-19 16:42:22 +00:00
4776c1f1ec fix(ai): properly decode \uXXXX JSON escape sequences as UTF-8
Add helper functions to parse hex digits and encode UTF-8 characters. Update buffer allocation to account for UTF-8 expansion and implement full surrogate pair support for characters outside the BMP. Previously, \uXXXX sequences were passed through verbatim; they are now correctly decoded into proper UTF-8 strings.
2026-05-19 16:40:21 +00:00
c0f8f1a0f1 fix(merge): drop leftover <<<<<<< HEAD marker in jid_is_valid
Some checks failed
CI Code / Check spelling (pull_request) Successful in 17s
CI Code / Check coding style (pull_request) Successful in 32s
CI Code / Linux (debian) (pull_request) Failing after 4m54s
CI Code / Linux (ubuntu) (pull_request) Failing after 5m6s
CI Code / Linux (arch) (pull_request) Failing after 5m49s
CI Code / Code Coverage (pull_request) Failing after 6m22s
My earlier merge-resolution Edit on src/xmpp/jid.c only covered the
=======/>>>>>>> half of the hunk and left an orphan <<<<<<< HEAD on
line 82 of jid_is_valid. CI caught it. Removed.
2026-05-18 12:01:32 +03:00
508de43da9 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.
2026-05-18 11:42:48 +03:00
a3a45ad477 fix(ui): preserve messages in non-chat windows while scrolled
All checks were successful
CI Code / Check spelling (push) Successful in 17s
CI Code / Check coding style (push) Successful in 31s
CI Code / Code Coverage (push) Successful in 2m43s
CI Code / Linux (debian) (push) Successful in 4m33s
CI Code / Linux (ubuntu) (push) Successful in 4m48s
CI Code / Linux (arch) (push) Successful in 5m36s
_win_printf dropped buffer append and render for any window in paged
state, but only WIN_CHAT can recover lost messages via chatwin_db_history()
on scroll-down. WIN_MUC, WIN_PRIVATE and WIN_AI have no such fallback, so
incoming and outgoing messages were silently lost when the user was viewing
history.

Introduce log_database_can_recover_messages() to check whether the DB
backend can replay messages (returns FALSE when PREF_DBLOG is "off",
"redact", or no backend is active). Gate the WIN_CHAT early return in
_win_printf() on this check: when recovery is impossible, fall through
and append to the buffer so messages remain visible on scroll-down.

Add a buffer-bottom reset in win_page_down() for non-chat windows.
WIN_SCROLL_REACHED_BOTTOM is only set on the is_chat DB branch, so
non-chat windows never clear paged on their own; reset paged and
unread_msg when the last line of the buffer reaches the screen.

Add missing scroll rendering for AI windows in the title bar draw
function, ensuring AI windows display their scrolled state consistently
with other window types.

Remove the manual paged/unread_msg reset before printing the user
message in cl_ev_send_ai_msg() -- it was a local workaround for the
same drop and is no longer needed.
2026-05-16 15:47:49 +00:00
2952466abd feat(history): consolidate logging controls and add dbbackend statusbar indicator
All checks were successful
CI Code / Check spelling (push) Successful in 18s
CI Code / Check coding style (push) Successful in 33s
CI Code / Code Coverage (push) Successful in 2m42s
CI Code / Linux (debian) (push) Successful in 4m40s
CI Code / Linux (ubuntu) (push) Successful in 4m53s
CI Code / Linux (arch) (push) Successful in 5m38s
Deprecate /logging command in favor of /history for chat logging control.
/history off now stops persistence (sets PREF_DBLOG=off + PREF_CHLOG=false)
in addition to hiding history on open. /history on restores persistence
(re-enabling PREF_DBLOG=on if it was off) and PREF_CHLOG.

statusbar
- PREF_STATUSBAR_SHOW_DBBACKEND (default ON) gates the [sqlite] /
  [flatfile] indicator; toggle via "/statusbar show|hide dbbackend"

/history off|on
- "/history off" now stops persistence as well as hiding history on
  open: sets PREF_DBLOG=off + PREF_CHLOG=false in addition to
  PREF_HISTORY=false
- "/history on" restores persistence (re-enabling PREF_DBLOG=on if
  it was off) and PREF_CHLOG, in addition to PREF_HISTORY=true

/logging
- Deprecated /logging command. It now prints a single notice
  pointing to /history; CMD_PREAMBLE trimmed (min_args=0, no
  setting_func, syntax/args/examples dropped); subcommand 'group'
  removed from autocomplete
- All "use '/logging chat on' to enable" hints replaced with
  "/history on" across /omemo-log, /pgp-log, /otr-log, and /ox-log

/privacy logging
- Single-pass validation across {on, off, redact, flatfile}
- Backend-switching values (on, flatfile) now take effect
  immediately when connected (via log_database_switch_backend),
  matching "/history switch" behaviour; off/redact only flip
  pref bits and keep the live backend open

/correction off
- win_print_outgoing and win_print_outgoing_with_receipt now gate
  _win_correct on PREF_CORRECTION_ALLOW, matching incoming-msg
  paths. Previously a peer's correction reflected via XEP-0280
  carbons (or the user's own /correct invocation) was applied
  in-buffer regardless of the pref

console
- Drop orphaned /logging chat reference from cons_privacy_setting()
- Delete cons_logging_setting() and remove its call from
  cons_show_log_prefs()

autocomplete
- Add dbbackend to statusbar_show_ac
- Remove logging_ac entries for chat/group subcommands
- Remove _logging_autocomplete() param handler for chat subcommand

database_flatfile
- Two local g_strndup allocations switched from char* with manual
  g_free to auto_gchar gchar* for automatic cleanup

tests
- Update test_cmd_otr.c to expect new /history-on warning messages

@author: jabber.developer2 <jabber.developer2@jabber.space>
2026-05-16 15:33:42 +00:00
06b80bc89a fix(ai): fix memory leak of local provider in error paths
All checks were successful
CI Code / Check spelling (pull_request) Successful in 17s
CI Code / Check coding style (pull_request) Successful in 31s
CI Code / Code Coverage (pull_request) Successful in 2m44s
CI Code / Linux (debian) (pull_request) Successful in 4m42s
CI Code / Linux (ubuntu) (pull_request) Successful in 4m56s
CI Code / Linux (arch) (pull_request) Successful in 5m49s
CI Code / Check spelling (push) Successful in 16s
CI Code / Check coding style (push) Successful in 31s
CI Code / Code Coverage (push) Successful in 2m42s
CI Code / Linux (debian) (push) Successful in 4m45s
CI Code / Linux (ubuntu) (push) Successful in 4m57s
CI Code / Linux (arch) (push) Successful in 5m50s
Add missing unref calls for local_provider in _ai_request_thread
error handling to prevent memory leaks.
2026-05-15 14:14:11 +00:00
f9e0ba9630 fix(ai): fix memory leaks in ai session handling
All checks were successful
CI Code / Check spelling (pull_request) Successful in 19s
CI Code / Check coding style (pull_request) Successful in 38s
CI Code / Code Coverage (pull_request) Successful in 2m55s
CI Code / Linux (debian) (pull_request) Successful in 4m39s
CI Code / Linux (ubuntu) (pull_request) Successful in 4m51s
CI Code / Linux (arch) (pull_request) Successful in 5m34s
Add missing `ai_session_unref` calls to prevent memory leaks.

- In `_ai_request_thread`, release session on error paths and
  after successful processing.
- In `cmd_ai_start`, release reference after passing ownership
  to the AI window.
2026-05-15 11:58:25 +00:00
7469f31c78 fix(ai): fix memory leaks in _ai_request_thread
All checks were successful
CI Code / Check spelling (pull_request) Successful in 17s
CI Code / Check coding style (pull_request) Successful in 34s
CI Code / Code Coverage (pull_request) Successful in 2m43s
CI Code / Linux (debian) (pull_request) Successful in 4m45s
CI Code / Linux (ubuntu) (pull_request) Successful in 4m55s
CI Code / Linux (arch) (pull_request) Successful in 5m45s
Replace manual g_free calls with auto_gchar for local_provider_name,
local_model, local_api_key, and response_data to ensure automatic
cleanup and prevent memory leaks.
2026-05-15 11:29:10 +00:00
5e329c77e1 fix(ai): fix memory leak in AI message stanza ID
All checks were successful
CI Code / Check spelling (pull_request) Successful in 17s
CI Code / Check coding style (pull_request) Successful in 32s
CI Code / Code Coverage (pull_request) Successful in 2m43s
CI Code / Linux (debian) (pull_request) Successful in 4m41s
CI Code / Linux (ubuntu) (pull_request) Successful in 4m55s
CI Code / Linux (arch) (pull_request) Successful in 5m44s
Store stanza ID in an auto_gchar variable to ensure automatic memory cleanup.
2026-05-15 08:55:56 +00:00
9e5dfb14f8 feat(ai): add AI client with multi-provider chat support
All checks were successful
CI Code / Check spelling (push) Successful in 18s
CI Code / Check coding style (push) Successful in 29s
CI Code / Code Coverage (push) Successful in 2m44s
CI Code / Linux (debian) (push) Successful in 4m46s
CI Code / Linux (ubuntu) (push) Successful in 4m59s
CI Code / Linux (arch) (push) Successful in 5m56s
Add an AI client module that integrates with OpenAI-compatible API
providers (OpenAI, Perplexity, and custom endpoints) to provide
AI-assisted chat within CProof. Users can start sessions with /ai start,
send prompts, receive responses in a dedicated AI window, switch between
providers and models, and manage API keys — all with tab-completion.

Providers are configured via /ai set commands with per-provider API keys,
endpoints, default models, and custom settings. Two default providers
(openai, perplexity) are seeded on first use. Provider state persists in
[ai/<name>] sections of the preferences keyfile with automatic migration
from the previous flat-key format.

The /ai command integrates into the existing command system with 8
subcommands covering provider management, session lifecycle, model
fetching, and conversation clearing. Autocomplete uses the standard
flat prefix-matching chain for reliable tab-completion at every nesting
level. A new ProfAiWin window type is added to the window system.

Architecture:

Async design: HTTP requests run on a background thread (pthread) to avoid blocking the ncurses UI loop; results are displayed on the main thread via direct function calls
Thread safety: AIProvider and AISession use atomic ref-counting and mutex-protected session state; the request thread snapshots all session data before making the HTTP call
Window validation: wins_ai_exists() prevents use-after-free when the user closes the AI window during an in-flight HTTP request (~60s)
Privacy: store:false is sent with every request to prevent providers from persisting conversations or using them for training
Response size limit: 10MB cap with immediate curl abort via CURL_WRITEFUNC_ERROR to prevent OOM
JSON parsing uses unified helpers for both chat responses and error
envelopes with consistent escape decoding. The response parser tries
Perplexity /v1/responses "text" field first, then falls back to OpenAI
"content". Error parsing extracts provider error.message from the
standard envelope format. Model parsing handles multiple API response
formats (OpenAI list, Perplexity, array) including edge cases.

Tests include 470+ lines of unit tests covering provider management,
session lifecycle, JSON parsing (multiple formats), autocomplete cycling,
and error handling, plus functional tests for /ai command dispatch.
A stub_ai.c module isolates unit tests from UI dependencies.
2026-05-15 02:21:54 +00:00
1aaa382d5e fix(verify): per-contact context in output, demote duplicate stanza-id to debug
All checks were successful
CI Code / Check spelling (pull_request) Successful in 19s
CI Code / Check coding style (pull_request) Successful in 31s
CI Code / Code Coverage (pull_request) Successful in 2m39s
CI Code / Linux (debian) (pull_request) Successful in 4m39s
CI Code / Linux (ubuntu) (pull_request) Successful in 4m51s
CI Code / Linux (arch) (pull_request) Successful in 5m40s
CI Code / Check spelling (push) Successful in 20s
CI Code / Check coding style (push) Successful in 36s
CI Code / Code Coverage (push) Successful in 2m42s
CI Code / Linux (debian) (push) Successful in 4m43s
CI Code / Linux (ubuntu) (push) Successful in 4m58s
CI Code / Linux (arch) (push) Successful in 5m44s
2026-05-06 17:02:27 +03:00
3f36c303c2 feat(history): flat-file backend with bidirectional SQLite migration
All checks were successful
CI Code / Check spelling (push) Successful in 21s
CI Code / Check coding style (push) Successful in 31s
CI Code / Code Coverage (push) Successful in 2m21s
CI Code / Linux (ubuntu) (push) Successful in 4m30s
CI Code / Linux (debian) (push) Successful in 6m43s
CI Code / Linux (arch) (push) Successful in 10m8s
A flat-file alternative to the SQLite chatlog backend with runtime
switching, full migration tooling, integrity verification, and a
synthetic load harness. SQLite remains the default; both backends share
one dispatch layer (db_backend_t vtable) so callers don't change.

Storage layout
- Per-contact append-only `flatlog/<account>/<contact>/history.log`
  under XDG_DATA_HOME, one line per message
- Single-line file header with embedded format-version marker
  (FLATFILE_FORMAT_VERSION); reader warns on missing or mismatched
  marker, writer and checker stay in sync via preprocessor
  stringification
- Deterministic key=value metadata (`id`, `aid`, `corrects`, `to`,
  `to_res`, `read`) plus escaped body \u2014 `\|`, `\]`, `\\`, `\n`, `\r`
  literals prevent log injection
- Sparse byte-offset index (FF_INDEX_STEP=500) per contact for
  O(log n) time-range lookups; rebuilt on inode / size / mtime
  change, extended in-place when the file just grew
- Per-contact GHashTable caches for archive_id presence and
  stanza_id \u2192 from_jid mapping (O(1) MAM dedup, O(1) LMC sender
  validation)

Hardening
- Path-traversal protection: JID directory name normalisation
  (`@` \u2192 `_at_`, slashes and `..` rejected at construction); every
  per-contact path is anchored under the account's flatlog/
  directory and validated before open
- Symlink-attack protection: every fopen / open uses O_NOFOLLOW; on
  ELOOP the operation aborts with an error rather than following
- Filesystem permissions: log files created with mode 0600,
  directories with mode 0700; both enforced at creation, verified
  on each open and reported on drift by `/history verify`
- Atomic crash-safe export: write to a temp file via mkstemp (mode
  0600, random suffix, no name collisions between concurrent
  exports), fsync, then rename \u2014 partial state never replaces the
  live file
- Concurrency: advisory flock(LOCK_EX) held for the duration of
  every write, including append from live messages and full rewrite
  from export, so two profanity processes can't interleave bytes
  on the same log
- DoS / abuse guards:
    * FF_MAX_LINE_LEN = 10 MB \u2014 lines longer than this are rejected
      at read with a warning; the parser will not allocate
      unbounded memory for a single record
    * FF_MAX_LMC_DEPTH = 100 \u2014 `corrects:` chain walk stops at this
      depth and emits a warning, preventing a malicious correction
      cycle from spinning the apply pass
    * FF_VERSION_SCAN_MAX = 16 \u2014 header version probe never reads
      past 16 leading comment lines, even on garbage input
    * Empty / inverted byte-range early-return in page-up read path
      so a malformed time filter cannot cause an unbounded scan
    * Zero-entry index guard so a file whose every line failed to
      parse cannot cause a NULL deref on later page-up
- LMC sender validation: an incoming correction whose sender does
  not match the original message's sender is rejected at write
  time and surfaced via cons_show_error; a cycle in the apply pass
  is broken via a visited-set
- jid_create_from_bare_and_resource treats NULL, empty string, and
  the literal "(null)" as no resource and returns a bare jid;
  similar normalisation for barejid eliminates the legacy
  "user@host/(null)" artefact that leaked into stored fulljids
  whenever g_strdup_printf("%s", NULL) ran inside create_fulljid

Commands
- `/history switch sqlite|flatfile` \u2014 runtime backend swap, closes
  the old backend and opens the new one without reconnecting
- `/history export [<jid>]` \u2014 SQLite -> flat-file, merging with any
  existing flatlog (dedup keyed on a SHA-256 hash mixing stanza_id,
  timestamp, from_jid, body \u2014 robust against id reuse by older
  clients)
- `/history import [<jid>]` \u2014 flat-file -> SQLite, same merge
  semantics, runs inside a single SQLite transaction with rollback
  on per-contact failure
- `/history verify [<jid>]` \u2014 integrity check; emits a structured
  list of issues (ERROR / WARNING / INFO) per file:
    * file-level: missing log, wrong permissions (\u2260 0600), UTF-8
      BOM present, CRLF line endings, empty file
    * line-level: invalid UTF-8 (with byte offset), embedded
      control characters, unparsable lines, timestamps out of
      order, duplicate `id:` and `aid:` (tracked separately so a
      stanza/archive id collision isn't double-reported)
    * cross-line: broken `corrects:` references whose target id is
      not present in the file
- `/history backend` \u2014 show currently active backend
- Active backend indicator `[sqlite]` / `[flatfile]` in the status
  bar next to the JID
- Roster-JID autocomplete for verify / export / import
- export and import open a SQLite handle on demand when the
  flatfile backend is currently active, so migration works
  regardless of which backend is live

Tests
- Unit: database_export (parser round-trip, escape/unescape, dedup
  key stability, JID normalisation), database_stress (14 cases
  exercising rapid writes, large messages, deep LMC chains, MAM
  dedup, concurrent contacts)
- Functional: history persistence across reconnects, export /
  import round-trip with content equality, MUC migration,
  timestamp normalisation across timezones
- Bench harness P1\u2013P5 (synthetic load: bulk insert, time-range
  read, page-up scroll, MAM ingest, mixed workload) and failure
  modes F1\u2013F17 (page-up cursor and forward-iteration symmetry,
  oversized lines, MAM dedup, LMC depth and cycles, BOM/CRLF,
  missing log, empty file, mtime+inode flip, broken corrects, etc.)
- All bench tests integrate with the existing make targets and
  emit CSV rows for baseline comparison

Author: jabber.developer2 <jabber.developer2@jabber.space>
Reviewed-by: jabber.developer <jabber.developer@jabber.space>
2026-05-05 19:26:07 +00:00
a7ad9ac0a4 Revert build: enable -Wconversion/-Wsign-compare and add --enable-sanitizers
All checks were successful
CI Code / Check spelling (pull_request) Successful in 21s
CI Code / Check coding style (pull_request) Successful in 35s
CI Code / Code Coverage (pull_request) Successful in 2m50s
CI Code / Linux (debian) (pull_request) Successful in 4m19s
CI Code / Linux (ubuntu) (pull_request) Successful in 4m32s
CI Code / Linux (arch) (pull_request) Successful in 5m4s
This reverts the configure.ac

Tracked as a TODO under issue #112.
2026-04-28 18:49:18 +03:00
145792ca7d Merge remote-tracking branch 'origin/master' into merge/upstream-full
# Conflicts:
#	configure.ac
2026-04-28 18:40:32 +03:00
60da899bd9 fix: address open PR #105 review comments (leaks + null safety)
All checks were successful
CI Code / Check spelling (pull_request) Successful in 20s
CI Code / Check coding style (pull_request) Successful in 38s
CI Code / Linux (ubuntu) (pull_request) Successful in 4m36s
CI Code / Linux (arch) (pull_request) Successful in 4m42s
CI Code / Code Coverage (pull_request) Successful in 7m7s
CI Code / Linux (debian) (pull_request) Successful in 7m28s
- ui/window.c win_show_subwin (#1144): unconditional newpad without
  freeing the previous pad leaks the ncurses subwin if the function
  is invoked twice without an intervening win_hide_subwin. Added a
  delwin guard.
- omemo/omemo.c omemo_on_disconnect (#1126): hash tables and
  libsignal handles are destroyed unconditionally even when a full
  init never ran (e.g. shutdown after a failed load), and neither
  g_hash_table_destroy nor signal_*_destroy are documented as
  NULL-safe. NULL-check each handle before tearing it down.
- xmpp/roster_list.c (#1140 / #1141 / #1160): null the caller's
  pointer immediately after resource_destroy() so a stale reference
  cannot trigger a double-free; resource_destroy itself only zeroes
  the struct fields it owns and cannot reach back to the caller.
2026-04-28 17:19:20 +03:00
1e372f6fa8 fix(subprojects): point libstrophe.wrap at libstrophe-gh-mirror
All checks were successful
CI Code / Check spelling (pull_request) Successful in 22s
CI Code / Check coding style (pull_request) Successful in 36s
CI Code / Code Coverage (pull_request) Successful in 2m57s
CI Code / Linux (debian) (pull_request) Successful in 4m15s
CI Code / Linux (ubuntu) (pull_request) Successful in 4m40s
CI Code / Linux (arch) (pull_request) Successful in 10m13s
devs/libstrophe-gh is a static fork; -mirror is the live one.
2026-04-28 10:35:03 +03:00
9feff00ead refactor: clean narrowing conversions and harden unsigned arithmetic
Cleanup of the conversion-safety warnings exposed by enabling
-Wconversion / -Wsign-compare in the previous commit, plus guard
clauses at the few places where unsigned arithmetic could actually
misbehave.

Build:
- configure.ac drops -Wno-error=conversion and
  -Wno-error=float-conversion. Only -Wno-error=sign-conversion and
  -Wno-error=sign-compare remain, gating the ~230 sign warnings
  inherited from upstream that will be cleaned up in follow-ups.

Type / conversion fixes (no behaviour change):
- Length-like locals in command/cmd_ac.c, command/cmd_funcs.c,
  pgp/gpg.c, tools/autocomplete.c, tools/parser.c and ui/mucwin.c
  switched from int to size_t / glong (matching strlen /
  g_utf8_strlen return type) so we no longer need an (int) cast and
  loop counters / array sizes stay in their natural unsigned domain.
- g_timer_elapsed / GTimeSpan -> int casts in session.c, iq.c,
  core.c, server_events.c, window.c.
- _win_print_wrapped: indent parameter and local curx/maxx switched
  from size_t to int to match _win_indent / getcurx / getmaxx.
- Port casts (int -> unsigned short) at the libstrophe boundary in
  connection.c and session.c, each preceded by
  g_assert(port >= 0 && port <= UINT16_MAX) so the truncation is
  documented at the call-site.
- curl_off_t / fread size_t results cast at usage in http_upload.c,
  http_download.c, omemo/crypto.c.
- strtoul results cast to uint32_t in xmpp/omemo.c and omemo/omemo.c
  where device/prekey IDs are genuinely 32-bit.
- config/color.c: fg/bg/palette indices switched to `short`
  end-to-end (find_col, color_hash, find_closest_col,
  _color_pair_cache_get, cache.pairs), so the ncurses init_pair
  boundary needs at most one (short)i cast for the cache index. Also
  TODO-noted: init_extended_pair is needed for >15-bit palettes.
- xmpp/avatar.c: float arithmetic explicitly casts its int operands.
- tests/functionaltests/proftest.c: read() result handling uses
  size_t for the accumulator, _read_output returns ssize_t, and the
  buffer-shift check happens before space subtraction so the
  expression cannot underflow.

Real-risk guard clauses (the part that actually fixes bugs):
- src/ui/statusbar.c _tabs_width: `end > opened_tabs - 1` rewritten
  as `end < opened_tabs` so opened_tabs == 0 no longer underflows.
- src/ui/statusbar.c _status_bar_draw_extended_tabs: the mirror
  comparison rewritten as `end >= opened_tabs`.
- src/ui/statusbar.c status_bar_draw: replaced
  `MAX(0, getmaxx - (int)_tabs_width)` with an explicit precheck
  before subtraction.
- src/omemo/omemo.c prekey selection: prekey_index is now uint32_t
  and randomized into an unsigned buffer, so modulo with prekeys_len
  cannot yield a negative index for g_list_nth_data.
- src/omemo/crypto.c omemo_decrypt_func: PKCS#5/PKCS#7 unpadding
  reads `plaintext[plaintext_len - 1]`, which would underflow on a
  malformed empty ciphertext and read past the heap buffer. Reject
  plaintext_len == 0 before the padding peek and validate the
  padding byte against the buffer length before the unpad loop.
  Initialise plaintext = NULL so the early `goto out` cannot free
  uninitialised memory.
- src/ui/inputwin.c (4 mbrlen sites) and src/ui/window.c
  _win_print_wrapped: mbrlen() returns 0 for the null wide
  character. The existing checks rejected (size_t)-1 / -2 but
  treated 0 as a valid step, so the surrounding loops would either
  advance by SIZE_MAX (i += ch_len - 1) or spin in place
  (word_pos += 0 forever). Add `|| ch_len == 0` to each guard;
  inside the spell-check word-emission loop also fall back to a
  one-byte advance.
- Defensive `len > 0 ? len - 1 : 0` prechecks at the strlen-based
  g_strndup / loop sites in ui/console.c, plugins/c_api.c and
  plugins/python_plugins.c.
2026-04-28 10:24:52 +03:00
5459e78e82 build: enable -Wconversion/-Wsign-compare and add --enable-sanitizers
Turn on the stricter conversion-safety warnings and wire in an optional
sanitizer build mode. Nothing is promoted to a hard error yet: the
warnings become part of the build output and can be cleaned up
incrementally without blocking development.

- Drop -Wno-sign-compare and add -Wsign-compare + -Wconversion to the
  GCC-specific warnings loop (probed for compiler support).
- Add --enable-sanitizers flag that enables ASan + UBSan +
  -fsanitize=unsigned-integer-overflow with -fno-sanitize-recover=all.
  Off by default; intended for a dedicated CI job.
- Under PACKAGE_STATUS=development (-Werror), opt out of turning the
  new conversion/sign-compare warnings into hard errors via
  -Wno-error=sign-conversion/-conversion/-float-conversion/-sign-compare.
  Existing codebase has ~240 sign-conversion, ~87 other narrowing and a
  handful of sign-compare warnings (mostly inherited from upstream);
  they stay visible but don't block development builds.
- Route glib/gio CFLAGS through -isystem so macros like
  GPOINTER_TO_UINT in glibconfig.h don't generate noise.
2026-04-24 16:01:17 +03:00
c815c39cdd ci: fix Valgrind startup failure on Arch via DEBUGINFOD_URLS
All checks were successful
CI Code / Check spelling (pull_request) Successful in 22s
CI Code / Check coding style (pull_request) Successful in 33s
CI Code / Linux (debian) (pull_request) Successful in 6m54s
CI Code / Linux (ubuntu) (pull_request) Successful in 7m4s
CI Code / Code Coverage (pull_request) Successful in 6m49s
CI Code / Linux (arch) (pull_request) Successful in 8m46s
Arch Linux ships a stripped ld-linux-x86-64.so.2, so Valgrind cannot
find the memcmp/memcpy/strlen symbols it must redirect and exits at
startup with "Fatal error at startup: a function redirection which is
mandatory for this platform-tool combination cannot be set up".

debuginfod is already installed in the image but was never wired up.
Set DEBUGINFOD_URLS to the official Arch debuginfod server so Valgrind
fetches the missing glibc debug symbols on demand and caches them.
2026-04-24 15:41:44 +03:00
4401e817d0 refactor: address PR #105 review feedback
Some checks failed
CI Code / Check spelling (pull_request) Successful in 21s
CI Code / Check coding style (pull_request) Successful in 35s
CI Code / Linux (arch) (pull_request) Failing after 2m52s
CI Code / Code Coverage (pull_request) Successful in 2m56s
CI Code / Linux (debian) (pull_request) Successful in 6m48s
CI Code / Linux (ubuntu) (pull_request) Successful in 7m11s
Apply fixes, refactors and test additions requested by reviewer on PR #105.

Fixes:
- database: warn and notify user on duplicate archive_id instead of
  silently debug-logging it (R05).
- database: add missing '[' in "[DB Migration]" log prefix (R06).
- xmpp/resource: NULL-out name/status after g_free to avoid double-free
  via roster_list.c cleanup path (R09, R23).
- common: widen strtoi_range internal storage from int to long so that
  values in (INT_MAX, LONG_MAX] are rejected as out-of-range instead of
  being silently truncated on 64-bit platforms (R25).

Refactors:
- xmpp/message: extract _receive_omemo helper, removing three copies of
  the OMEMO receive block in groupchat / MUC-PM / chat handlers (R04).
- omemo: flatten deeply nested device-list processing via guard-clause
  continues (R11).
- tools/autocomplete: merge two nested ifs into a single && condition
  (R13).
- ui/titlebar: extract _show_trust_indicator and inline _wprintw_withattr
  wrapper, collapsing three near-identical trust-indicator blocks (R22).
- config/tlscerts: drop _checked_g_strdup wrapper; g_strdup is
  NULL-safe per glib documentation (R19).
- ui/inputwin: use auto_gchar for spellcheck word instead of manual
  g_free (R20).
- tools/editor: drop outdated "Deprecated synchronous" comment that
  no longer matches the callback-based implementation (R28).

Tests:
- tests/command/cmd_ac: rename segfaults_when_empty ->
  no_segfault_when_empty; expand cycling coverage to three files plus
  backward SHIFT-TAB traversal (R16, R17).
- tests/common: add strtoi_range overflow/underflow and strtol-parsing
  consistency tests (R25).
- tests/xmpp/jid: add test for '@' inside resourcepart per RFC 6122
  section 2.4 (R26).

Misc:
- xmpp/omemo: change omemo_error_to_string return type from char* to
  gchar* for glib consistency (R01).
- subprojects/libstrophe: point wrap-git at our fork at
  git.jabber.space/devs/libstrophe-gh (R24).
- RELEASE_GUIDE: drop "Updating website" section referring to an
  upstream site that is not ours (R18).
2026-04-24 15:13:38 +03:00
0feacbc9da ci: simulate Pikaur flag duplication in Arch Linux CI
All checks were successful
CI Code / Check spelling (pull_request) Successful in 17s
CI Code / Check coding style (pull_request) Successful in 30s
CI Code / Code Coverage (pull_request) Successful in 3m1s
CI Code / Linux (ubuntu) (pull_request) Successful in 4m31s
CI Code / Linux (debian) (pull_request) Successful in 7m36s
CI Code / Linux (arch) (pull_request) Successful in 9m52s
CI Code / Check spelling (push) Successful in 18s
CI Code / Check coding style (push) Successful in 32s
CI Code / Linux (debian) (push) Successful in 4m51s
CI Code / Linux (arch) (push) Successful in 5m35s
CI Code / Linux (ubuntu) (push) Successful in 6m36s
CI Code / Code Coverage (push) Successful in 7m21s
Inject system flags from /etc/makepkg.conf into the CI environment to
detect build collisions caused by Pikaur's configuration bug.

Pikaur's cascading logic causes flags from /etc/makepkg.conf to be
merged into the build environment. This creates collisions with flags
defined in the project's Makefile.am (e.g., duplicate -D_FORTIFY_SOURCE
definitions), which can cause builds to fail for users.

By exporting these flags in the CI environment, we ensure that any
code change that is sensitive to flag duplication will trigger a
failure in our Arch Linux CI matrix, preventing broken builds from
reaching users.

Implementation details:
- Detects Arch Linux via /etc/os-release.
- Uses a sed-based flattener to handle multi-line variables and
  trailing backslashes in makepkg.conf.
- Exports the flags to the shell environment so that 'configure'
  and 'make' inherit them naturally, maintaining parity with a
  real Pikaur session.
2026-04-21 10:17:44 +00:00
a5fe32b245 Merge remote-tracking branch 'upstream/master' into merge/upstream-full
# Conflicts:
#	CONTRIBUTING.md
#	src/profanity.c
#	src/tools/editor.c
2026-04-18 12:26:52 +03:00
Michael Vetter
3af4e9acb8 Merge pull request #2153 from bkmgit/doc-spellcheck
doc: Explain how to enable spell checking
2026-04-16 13:22:17 +02:00
Michael Vetter
064149cf0e Merge pull request #2155 from bkmgit/update-release-guide
docs: XEP list does not need to be updated manually on a release
2026-04-16 13:21:45 +02:00
Benson Muite
5b75bc26c8 docs: XEP list does not need to be updated manually on a release
Updated tooling builds supported XEP list from profanity.doap file
The profanity.doap file is a git submodule in the website repository.
It is used as an input to create the supported XEPs webpage.
3cd439d172

Fixes: #2154
Signed-off-by: Benson Muite <benson_muite@emailplus.org>
2026-04-16 09:35:23 +03:00
Benson Muite
31bb9ab514 docs: Explain how to enable spell checking
Fixes: #2152
Signed-off-by: Benson Muite <benson_muite@emailplus.org>
2026-04-16 09:29:43 +03:00
0722dc9e36 build(pikaur): Fix failure due to duplicated flag and warnings
All checks were successful
CI Code / Check spelling (pull_request) Successful in 20s
CI Code / Check coding style (pull_request) Successful in 35s
CI Code / Linux (ubuntu) (pull_request) Successful in 6m42s
CI Code / Linux (debian) (pull_request) Successful in 6m47s
CI Code / Code Coverage (pull_request) Successful in 7m3s
CI Code / Linux (arch) (pull_request) Successful in 9m33s
CI Code / Check spelling (push) Successful in 20s
CI Code / Check coding style (push) Successful in 39s
CI Code / Code Coverage (push) Successful in 2m53s
CI Code / Linux (debian) (push) Successful in 4m8s
CI Code / Linux (ubuntu) (push) Successful in 4m35s
CI Code / Linux (arch) (push) Successful in 10m28s
2026-04-13 19:52:24 +00:00
Michael Vetter
49240083b0 Merge pull request #2151 from profanity-im/issue
chore: Explain how to get a backtrace
2026-04-13 21:29:09 +02:00
Michael Vetter
dbc739bcc5 chore: Mention omemo backend in issue template as well
Signed-off-by: Michael Vetter <jubalh@iodoru.org>
2026-04-13 21:27:37 +02:00
Michael Vetter
741777d2c7 chore: Explain how to add a backtrace in issue template
Signed-off-by: Michael Vetter <jubalh@iodoru.org>
2026-04-13 21:27:32 +02:00
Michael Vetter
c071d6cf84 Merge pull request #2150 from profanity-im/fix/2148
fix: resolve issues with async editor
2026-04-12 22:55:57 +02:00
Michael Vetter
b17ed21b16 fix: Fix connect with empty resource
We recently made the jid validation and creation functions more strict.
So this is a sideffect of it. We need to use `jid_fulljid_or_barejid`
instead of jidp->fulljid here since it now treats just the barejid not
as a fulljid.

Ref: 09757da5df
Ref: f251cc8bb3
Ref: fa64b135df
Signed-off-by: Michael Vetter <jubalh@iodoru.org>
2026-04-12 19:25:53 +02:00
Michael Vetter
dd76f9087f fix: resolve issues with async editor
If we build Profanity without GTK we dont iterate the context so we will
never know when the editor exited. Calling `g_main_context_iteration()`
or finally switching to GMainLoop fixes this.
So far calling `tray_update()` does this for us in GTK builds.
So this went unnoticed.

The editor will inherit ignored signal handlers (SIGINT, SIGTSTP, SIGPIPE)
from us. Neovim uses libuv which seems to check SIGINT on startup to
determine whether the environment is interactive or not.
We now reset these signals to SIG_DFL in the child process before execvp.

Profanity uses readline which still competed for terminal input with the
editor. We only took care about other Profanity UI processes in our
previous commit. This led to misordered characters in the editor.

In my tests with vim everything worked fine and these bugs were
discovered when a user used neovim.

Fixes: https://github.com/profanity-im/profanity/issues/2148
Ref: 36ec2b0ae1
Signed-off-by: Michael Vetter <jubalh@iodoru.org>
2026-04-11 23:45:36 +02:00
4319172758 Merge upstream/master into merge/upstream-full
All checks were successful
CI Code / Check coding style (pull_request) Successful in 32s
CI Code / Check spelling (pull_request) Successful in 15s
CI Code / Linux (debian) (pull_request) Successful in 6m43s
CI Code / Linux (ubuntu) (pull_request) Successful in 6m49s
CI Code / Linux (arch) (pull_request) Successful in 8m52s
CI Code / Code Coverage (pull_request) Successful in 7m19s
Merge profanity-im/profanity master (373 commits) into cproof fork.

Source changes (manual merge):
- src/command/: cmd_defs, cmd_funcs, cmd_ac — upstream g_new0, auto_gchar,
  launch_editor callback; keep our XEP-0308 LMC, force-encryption, CWE-134
- src/config/: preferences, tlscerts, account — upstream UNIQUE dedup,
  dynamic pad capacity; keep our db_history_result_t, scroll logic
- src/database.*: upstream g_new0 memory mgmt; keep our return types,
  add null-check fix for msg->timestamp
- src/omemo/, src/pgp/: upstream _gpgme_key_get_email, g_new0;
  keep our replace_id in omemo_on_message_send
- src/tools/: editor, http_upload, bookmark_ignore — upstream launch_editor
  callback API
- src/ui/: console, window, chatwin, mucwin, inputwin, buffer —
  upstream win_warn_needed/sent dedup, PAD_MIN_HEIGHT dynamic pads;
  keep our y_start_pos scroll, _truncate_datetime_suffix
- src/xmpp/: message, stanza, presence, roster_list, iq, session —
  upstream connection_get_available_resources; keep our LMC stanza logic
- src/common.c: fix format-security (cons_show)

Build system:
- Makefile.am: updated test paths to subdirectory structure, added
  test_cmd_ac, test_forced_encryption
- Restored autotools files deleted by upstream meson migration:
  bootstrap.sh, autogen.sh, m4/ax_valgrind_check.m4, configure-debug

Tests:
- Unit tests: upstream unittests.c base + our forced_encryption tests
  (482 passed, 0 failed across all 4 configurations)
- Functional tests: kept our version (93 passed, 0 failed)
  upstream version requires stbbr_for_xmlns not yet in our stabber fork
- Updated test stubs: stub_xmpp.c, stub_omemo.c from upstream

Compile fixes:
- src/common.c: cons_show(errmsg) -> cons_show("%s", errmsg)
- src/database.c: null-check before msg->timestamp access
- src/ui/console.c: size_t -> (int) cast for format width
- src/config/tlscerts.c: %d -> %zu for size_t
2026-04-11 13:44:50 +03:00
Michael Vetter
89fce43c07 Start new cycle
Signed-off-by: Michael Vetter <jubalh@iodoru.org>
2026-04-10 18:30:57 +02:00
Michael Vetter
56443dccd4 Release 0.18.0
Signed-off-by: Michael Vetter <jubalh@iodoru.org>
2026-04-10 18:29:41 +02:00
Michael Vetter
1ac05754be Merge pull request #2147 from balejk/omemo-heartbeats-fix
omemo: ignore key contents if there is no payload
2026-04-09 14:08:06 +02:00
Karel Balej
1458b09e27 fix(omemo): ignore key contents if there is no payload
For key transport messages, Monal at least doesn't append the
authentication tag to the plaintext key contents as that only makes
sense if the key was used to encrypt something. This causes the key
length check to fail and show the

	OMEMO message received but decryption failed.

error to the user which is confusing because there is no user-originated
message involved.

Skip the length check for key transport messages as profanity only uses
these to advance the ratchet and makes no use of the decrypted contents.

Signed-off-by: Karel Balej <balejk@matfyz.cz>
2026-04-09 09:52:10 +02:00
Michael Vetter
95eb1ef8e7 Merge pull request #2146 from profanity-im/wip
Misc improvements
2026-04-03 10:21:15 +02:00
Michael Vetter
4be8ec47fe docs: Add section about sanitizers to contributing.md
Signed-off-by: Michael Vetter <jubalh@iodoru.org>
2026-04-03 10:19:40 +02:00
Michael Vetter
27cb926061 ci: remove ASan from Valgrind check
ASan and Valgrind both intercept memory allocations and management at
runtime. Running them simultaneously might lead to conflicts in memory
tracking.

This change ensures that during the Valgrind phase of the build matrix,
only Valgrind is responsible for memory analysis, avoiding redundant
overhead and ensuring more reliable results.

Signed-off-by: Michael Vetter <jubalh@iodoru.org>
2026-04-03 10:03:03 +02:00
Michael Vetter
8af80a7ad5 ci: Run functional tests in CI
Signed-off-by: Michael Vetter <jubalh@iodoru.org>
2026-04-03 09:31:30 +02:00
Michael Vetter
6a7262f2b0 ci: Add CodeQL
Signed-off-by: Michael Vetter <jubalh@iodoru.org>
2026-04-03 09:22:04 +02:00
Michael Vetter
35bf9df0a9 refactor: Make Launching the editor more robust
Signed-off-by: Michael Vetter <jubalh@iodoru.org>
2026-04-03 08:40:14 +02:00
Michael Vetter
a643fd87cc docs: Remove autotools leftover from CONTRIBUTING.md
Signed-off-by: Michael Vetter <jubalh@iodoru.org>
2026-04-03 08:33:59 +02:00
Michael Vetter
2ad3bff8f0 docs: Add eval_password example
And show how to escape whitespaces.

Signed-off-by: Michael Vetter <jubalh@iodoru.org>
2026-04-03 08:32:31 +02:00
Michael Vetter
c0f96e9b60 Merge pull request #2145 from profanity-im/fix/2143
fix: restore TTY access for eval_password commands
2026-04-02 23:18:07 +02:00
Michael Vetter
196d3c3783 cleanup: Cleanup gitignore
Remove IDE specific entries, they belong in the global conf of the user.
Remove autotools specific files.

Ref: bc777c56b2
Signed-off-by: Michael Vetter <jubalh@iodoru.org>
2026-04-02 23:16:51 +02:00
Michael Vetter
c3718e3ae7 cleanup: Remove unused variable
This was forgotten in a refactor commit.

Ref: c43c9567df
Signed-off-by: Michael Vetter <jubalh@iodoru.org>
2026-04-02 22:47:26 +02:00
Michael Vetter
36ec2b0ae1 feat: implement asynchronous external editor support
Move from blocking fork/wait logic to nonblocking fork and
g_child_watch_add.

This ensures that the Profanity main loop continues to run while an
external editor is open. So we don't loose connection and react to
pings.

We change editor handling also in vcard and muc subject editing.
In the new implementation we are launching the editor and passing a
callback which we will use once the editor exited.

We use the recently added ui_susped() and ui_resume().

To not clutter the UI we need to check whether Profanity UI is suspended
and omit drawing in this case.

Fixes: https://github.com/profanity-im/profanity/issues/1888
Ref: 9b112904a9bc7250dc013d901187ca8622580d98
Signed-off-by: Michael Vetter <jubalh@iodoru.org>
2026-04-02 22:47:26 +02:00
Michael Vetter
fc66ddc2c1 refactor: Use helper functions in vcard related code
There was way too much repetition here.
This is in preparation for future changes regarding the editor.

Signed-off-by: Michael Vetter <jubalh@iodoru.org>
2026-04-02 22:47:26 +02:00
Michael Vetter
8675f7be0c fix: Fix removal of entries in account file
Commit 81f92f1fe introduced a selective update mechanism to
_accounts_save() to prevent overwrites when running multiple instances
by only modifying the specific account being saved.

Commit 5c484c fixed a problem with that commit regarding empty accounts.

It turns there was another bug introduced:
The new implementation only set the keys that existed in memory.
So removal of keys was not possible anymore.

Fixes: 81f92f1fed
Ref: 5c484c26ed
Signed-off-by: Michael Vetter <jubalh@iodoru.org>
2026-04-02 22:47:22 +02:00
Michael Vetter
c9a3823f13 fix: restore TTY access for eval_password commands
Replace g_spawn_command_line_sync with g_spawn_sync with the
G_SPAWN_CHILD_INHERITS_STDIN flag.
This is actually needed to so that interactive commands can access the
terminal.
Otherwise they cannot ask the user for the passphrase.

Ref: f5787fb31f
Fixes: https://github.com/profanity-im/profanity/issues/2143
Signed-off-by: Michael Vetter <jubalh@iodoru.org>
2026-04-02 16:38:34 +02:00
Michael Vetter
c49ee8d415 Merge pull request #2144 from profanity-im/fix/2124
fix: Fix crash when loading MAM for a contact with empty db
2026-04-02 16:12:27 +02:00
Michael Vetter
14c0c38acd fix: Fix crash when loading MAM for a contact with empty db
I think this bug happens when a user scrolls up a window, triggering
iq_mam_request_older() to load older history, in the case when the
database is empty for that contact.

The initial MAM fetch is triggered when the window is opened which
should get the latest history.

Fixes: https://github.com/profanity-im/profanity/issues/2142
Signed-off-by: Michael Vetter <jubalh@iodoru.org>
2026-04-02 13:37:49 +02:00
Michael Vetter
b56498adcb Merge pull request #2139 from profanity-im/fix/1844
Improve autocompleter and command parsing in relation to quoting
2026-04-02 12:31:58 +02:00
Michael Vetter
923dfea71e Merge pull request #2136 from profanity-im/db-dup
refactor(database): enforce unique archive_id for reliable deduplication
2026-04-02 12:31:26 +02:00
Michael Vetter
37463c0e43 Merge pull request #2141 from paulfertser/allow_libstrophe_as_subproject
chore: support building libstrophe as a subproject
2026-04-02 12:22:02 +02:00
Michael Vetter
dfe79038d4 Merge pull request #2140 from profanity-im/fix/1420
fix: handle X11 connection loss gracefully
2026-04-02 12:21:09 +02:00
Paul Fertser
5670ccb2b1 chore: support building libstrophe as a subproject
Allow building the latest libstrophe from upstream Git automatically when
-Dforce_fallback_for=libstrophe is passed.

Default to find and link against system-provided libstrophe stays the same.

Signed-off-by: Paul Fertser <fercerpav@gmail.com>
2026-04-02 13:07:04 +03:00
Michael Vetter
8eac31e5e1 fix: handle X11 connection loss gracefully
Use XSetIOErrorExitHandler on X11 to detect connection loss and
disable GTK features (tray, clipboard, notifications).

Signed-off-by: Michael Vetter <jubalh@iodoru.org>
Signed-off-by: Paul Fertser <fercerpav@gmail.com>
Co-authored-by: Paul Fertser <fercerpav@gmail.com>
2026-04-01 23:14:46 +02:00
Michael Vetter
c43c9567df refactor(database): use ON CONFLICT for deduplication
Replace 'INSERT OR IGNORE' with 'INSERT ... ON CONFLICT(`archive_id`) DO
NOTHING RETURNING id'. This is only available since sqlite 3.35.0.
So deduplication only happens for `archive_id` and we don't silently
ignore other errors or constraints (like not null).

We can now detect if an insertion was skipped due to duplication by
checking the result of 'RETURNING id'.

We don't print out when we don't insert duplicated messages since this
will happen often and will be too noisy. So we match the behaviour of
what Dino is doing.

Signed-off-by: Michael Vetter <jubalh@iodoru.org>
2026-03-31 15:27:33 +02:00
Michael Vetter
df1f1e5ef1 fix: ensure consistent unescaping and space handling
Unescape any character following a backslash. This fixes autocompletion
for names with escaped spaces.
For example: `/msg Thor\ Odinson hello`

Signed-off-by: Michael Vetter <jubalh@iodoru.org>
2026-03-30 18:33:59 +02:00
Michael Vetter
3997f34e5a Merge pull request #2137 from profanity-im/omemo-prekey
fix(omemo): standardize PreKey management to prevent decryption failures
2026-03-30 10:54:56 +02:00
Michael Vetter
bda5b0f844 fix(tools): implement backslash escaping for contact names
Implement backslash based escaping in the command parser and autocompletion
logic to handle contact names containing double quotes.
This stops the parser from splitting nicknames into multiple tokens, which
previously caused "Invalid usage" errors.

The parser now sees \ as an escape character for quotes and spaces in
_parse_args_helper, count_tokens and get_start. Autocomplete results are
escaped when they contain spaces, and search prefixes are unescaped before
matching. strip_arg_quotes() has also been updated to handle unescaping.

Fixes: https://github.com/profanity-im/profanity/issues/1844
Signed-off-by: Michael Vetter <jubalh@iodoru.org>
2026-03-29 00:15:23 +01:00
Michael Vetter
9b2f96fb45 chore: Update information on XEP-0077
Signed-off-by: Michael Vetter <jubalh@iodoru.org>
2026-03-28 00:49:22 +01:00
Michael Vetter
91e536b02d chore: Fix release years in doap file
Signed-off-by: Michael Vetter <jubalh@iodoru.org>
2026-03-28 00:49:19 +01:00
Michael Vetter
816617672e ci: Rename and sort Ci jobs
Signed-off-by: Michael Vetter <jubalh@iodoru.org>
2026-03-28 00:11:16 +01:00
Michael Vetter
247c44be30 fix(xmpp): verify 'by' attribute of stanza-id and MAM result
Implement security checks to ensure the 'archive_id' (XEP-0359) used
for database deduplication originates from a trusted source.

XEP-0359 Section 3.1: "The 'by' attribute MUST be the XMPP
address (JID) of the entity assigning the unique and stable stanza ID."
Furthermore, Section 4 (Security Considerations) specifies: "A client
SHOULD only trust <stanza-id/> elements from its own server or from
a MUC service it is joined to."

XEP-0313 Section 4.1.2: "The 'by' attribute of the <result/> element is
the JID of the archive being queried." and "If the 'by' attribute is not
present, the recipient MUST assume that the results are from their own
personal archive."

Let _handle_chat verify <stanza-id/> 'by' attribute matches our bare JID.
Let _handle_groupchat verify <stanza-id/> 'by' attribute matches the MUC
s bare JID.
Let _handle_mam verify the <result/> 'by' attribute matches the outer
message 'from' (archive JID). If 'by' is missing then 'from' matches our
own bare JID (personal archive).

Signed-off-by: Michael Vetter <jubalh@iodoru.org>
2026-03-27 23:57:59 +01:00
Michael Vetter
64fcdfaa0f refactor(database): enforce unique archive_id for reliable deduplication
Implement a UNIQUE constraint on the archive_id column (XEP-0359 stanza-id)
to prevent duplicate messages in the chat log. This addresses problems where
the same message could be stored multiple times if received via both MAM
and regular.

Signed-off-by: Michael Vetter <jubalh@iodoru.org>
2026-03-27 23:25:54 +01:00
Michael Vetter
68085f014f fix(omemo): standardize PreKey management to prevent decryption failures
Standardize how PreKeys are handled during OMEMO message reception to
align with XEP-0384 best practices.

Previously Profanity replaced a consumed PreKey by generating a new
one with the same ID. This behavior could lead to decryption failures if
multiple senders attempted to use the same PreKey ID before the client
could successfully update its bundle on the server.

Remove consumed PreKeys from the local store.
New PreKeys are generated in batches (100) when the local supply runs
low (< 10).
Republish OMEMO bundle after replenishment.

Signed-off-by: Michael Vetter <jubalh@iodoru.org>
2026-03-27 23:21:03 +01:00
Michael Vetter
259caea9c8 Merge pull request #2134 from profanity-im/feat/noautotools
feat: Remove autotools and adapt docu and scripts
2026-03-27 22:42:24 +01:00
Michael Vetter
ca156d58a2 Merge pull request #2135 from GunniBusch/feat/add-macos-homebrew-ci
ci: add macOS Homebrew CI
2026-03-27 22:40:42 +01:00
Leon Adomaitis
189f682518 ci: add macOS Homebrew CI
Signed-off-by: Leon Adomaitis <leon@adomaitis.de>
2026-03-27 22:24:36 +01:00
Michael Vetter
1d646a1afe ci: Let Debian build only with libomemo-c
So we can test if it works if libsignal isn't even installed on the
system.

Ref: 9a501e6ecd
Signed-off-by: Michael Vetter <jubalh@iodoru.org>
2026-03-27 21:58:38 +01:00
Michael Vetter
e169aa1426 ci: Set compiler path for Cygwin
Signed-off-by: Michael Vetter <jubalh@iodoru.org>
2026-03-27 21:58:34 +01:00
Michael Vetter
f5b7cdecb5 ci: Add libasan and libubsan to Fedora image
Signed-off-by: Michael Vetter <jubalh@iodoru.org>
2026-03-27 21:48:02 +01:00
Michael Vetter
c695b3a7b0 ci: Require gtk3-devel instead of gtk2-devel
Ref: 01c9a51c70
Signed-off-by: Michael Vetter <jubalh@iodoru.org>
2026-03-27 21:39:56 +01:00
Michael Vetter
bc777c56b2 feat: Remove autotools and adapt docu and scripts
We will only use Meson from now on.

Signed-off-by: Michael Vetter <jubalh@iodoru.org>
2026-03-27 21:33:44 +01:00
Michael Vetter
84e6eeae92 Merge pull request #2132 from profanity-im/feat/spellcheck
Add spellchecking
2026-03-27 14:32:01 +01:00
Michael Vetter
5e8987274f chore: Update RELEASE_GUIDE.md
Signed-off-by: Michael Vetter <jubalh@iodoru.org>
2026-03-27 10:01:02 +01:00
Michael Vetter
0dbc18cd7e Merge pull request #2133 from GunniBusch/fix/omemo-libary
fix(omemo): add missing includes for libomemo-c builds
2026-03-27 08:12:56 +01:00
Leon Adomaitis
9a501e6ecd fix(omemo): add missing includes for libomemo-c builds
Signed-off-by: Leon Adomaitis <leon@adomaitis.de>
2026-03-27 02:34:26 +01:00
Michael Vetter
ed2fdf88ac feat: Add /spellcheck list command
To list all available dictionaries.

Signed-off-by: Michael Vetter <jubalh@iodoru.org>
2026-03-26 23:34:13 +01:00
Michael Vetter
4922366366 feat: add spellcheck highlighting support
Introduce optional spellcheck highlighting in the input window using
the Enchant-2 library.

```
/spellcheck on
/spellcheck lang en_US
```

New theme color `input.misspelled`.

Fixes: https://github.com/profanity-im/profanity/issues/183
Signed-off-by: Michael Vetter <jubalh@iodoru.org>
2026-03-26 23:34:06 +01:00
Michael Vetter
88b80c33ef chore: Fix old spelling mistakes
Signed-off-by: Michael Vetter <jubalh@iodoru.org>
2026-03-26 12:45:07 +01:00
Michael Vetter
9be249e63c Start new cycle
Signed-off-by: Michael Vetter <jubalh@iodoru.org>
2026-03-26 12:21:43 +01:00
Michael Vetter
d99db450c5 Release 0.17.0
Signed-off-by: Michael Vetter <jubalh@iodoru.org>
2026-03-26 12:17:31 +01:00
Michael Vetter
24f7d16390 chore: Properly exclude directories
Signed-off-by: Michael Vetter <jubalh@iodoru.org>
2026-03-26 12:17:31 +01:00
Michael Vetter
25d2cb0120 fix: Check for necessary pointers in omemo_receive_message
Static analyzer hilighted this.

Ref: e39ffd981
Signed-off-by: Michael Vetter <jubalh@iodoru.org>
2026-03-26 11:41:20 +01:00
Michael Vetter
64b77ab493 tests: Check for binary before starting functional tests
Signed-off-by: Michael Vetter <jubalh@iodoru.org>
2026-03-26 11:28:26 +01:00
Michael Vetter
6ce6383b26 chore: Make meson doublecheck only available if the script exists
We won't have it in the distribution tarballs.

Signed-off-by: Michael Vetter <jubalh@iodoru.org>
2026-03-26 10:25:25 +01:00
Michael Vetter
b83003ff76 Merge pull request #2131 from profanity-im/fix/dl
Improve (encrypted) file downloads
2026-03-26 10:21:17 +01:00
Michael Vetter
63af72773c feat: Implement color coded status messages for file transfers
Introduce meaningful color feedback for background tasks like file
downloads and uploads. Progress is displayed in a neutral color,
while successful completions turn green and failures turn red.

We reuse the existing THEME colors for now.

Add new UI flags ENTRY_COMPLETED and ENTRY_ERROR to
the buffer entry system. So we don't misuse delivery receipts anymore.

Fixes: https://github.com/profanity-im/profanity/issues/1758
Signed-off-by: Michael Vetter <jubalh@iodoru.org>
2026-03-25 17:40:10 +01:00
Michael Vetter
a9ef000328 fix: Improve status reporting and filename handling for /url save
Use the original aesgcm:// URL when downloading OMEMO downloads.  Then
in the final message use the decrypted file destination instead of the
internal temporary path.

Use the unique download ID instead of the URL for message updates to
have proper progress reporting.

Unify the final transfer status to "done" across both downloads and
uploads.

Fixes: https://github.com/profanity-im/profanity/issues/1939
Signed-off-by: Michael Vetter <jubalh@iodoru.org>
2026-03-24 22:37:54 +01:00
Michael Vetter
050096c21e Merge pull request #2130 from profanity-im/fix/omemo-false-cannot-decrypt-msg
Fix incorrect omemo decryption error for Key Transport Messages
2026-03-22 17:59:09 +01:00
Michael Vetter
a824b26008 refactor: improve key transport message handling and deduplicate errors
Centralize heartbeat detection and have a function for error to string
mapping. So we don't need the same code in multiple handlers.

Signed-off-by: Michael Vetter <jubalh@iodoru.org>
2026-03-22 17:42:08 +01:00
Michael Vetter
e4bfda2f8a fix: Incorrect omemo decryption error for Key Transport Messages
Correctly handle OMEMO stanzas that contain a <header> but no <payload>.
These "Key Transport Messages" (heartbeats) are used by some clients
for session maintenance and establishing trust.

Ref: a2726b6a7 made the <payload> element mandatory in the parser.
Ref: 4d49c2b74 the bug became user-visible.

Fixes: https://github.com/profanity-im/profanity/issues/2129
Signed-off-by: Michael Vetter <jubalh@iodoru.org>
2026-03-22 17:41:57 +01:00
Michael Vetter
a85a33e397 chore: Add linter file to be ignored upon export 2026-03-21 01:10:48 +01:00
Michael Vetter
287b466e3c Merge pull request #2128 from profanity-im/tooling
Improve tooling for development and maintenance
2026-03-21 01:07:05 +01:00
Michael Vetter
a8c7850a97 chore: Unify build configuration script into one
Earlier ci/meson-build.sh and ci/ci-build.sh.
The latter we then renamed to the more descriptive build-configuration-matrix.sh.

Both scripts are doing the same thing but for different build systems.
So lets merge them together.

Signed-off-by: Michael Vetter <jubalh@iodoru.org>
2026-03-21 01:05:16 +01:00
Michael Vetter
beef714a30 ci: Add linter to make sure all commits are DCO signed
Mentioned in the CONTRIBUTING.md as well.
Let's enforce it.

Signed-off-by: Michael Vetter <jubalh@iodoru.org>
2026-03-21 01:05:16 +01:00
Michael Vetter
9407eb958d ci: Add linter to make sure we use Conventional Commits
This will make it easier for us to write Changelogs for example.
See https://www.conventionalcommits.org/en/v1.0.0.
Also mentioned in the CONTRIBUTING.md

Signed-off-by: Michael Vetter <jubalh@iodoru.org>
2026-03-21 01:05:11 +01:00
Michael Vetter
d808b88b55 chore: Add README explaining the purpose and usage of helper scripts
Signed-off-by: Michael Vetter <jubalh@iodoru.org>
2026-03-21 01:02:48 +01:00
Michael Vetter
a786ba3564 chore: Add changelog-helper script
Since we now use Conventional Commits we can create our Changelog
semi-automatically.

Signed-off-by: Michael Vetter <jubalh@iodoru.org>
2026-03-21 01:02:36 +01:00
Michael Vetter
4347af833a chore: Use more descriptive names for helper scripts
Signed-off-by: Michael Vetter <jubalh@iodoru.org>
2026-03-21 00:18:52 +01:00
Michael Vetter
53a980cdd3 Merge pull request #2122 from profanity-im/fix-2045
feat(ui): implement dynamic pad resizing
2026-03-21 00:00:06 +01:00
Michael Vetter
98fc8b6110 Merge pull request #2127 from profanity-im/fix/2084
fix: allow adding own JID to roster
2026-03-20 23:59:04 +01:00
Michael Vetter
4c731f5241 fix: allow adding own JID to roster
Enable adding oneself to the roster.

Self subscriptions are implicit according to XEP-0060 / XEP-0163.
So we adapt the /sub command to handle this gracefully by just printing
an informative message.

Profanitys logic didn't handle own presence/when adding to roster
correctly. This got fixed now.

Fixes: https://github.com/profanity-im/profanity/issues/2084

Signed-off-by: Michael Vetter <jubalh@iodoru.org>
2026-03-20 23:48:00 +01:00
Michael Vetter
bbdd5e176a Merge pull request #2126 from profanity-im/fix/2125
fix(ui): Fix custom outgoing stamp and fix stamp command help
2026-03-20 22:15:33 +01:00
Michael Vetter
9035af95cb feat: Add command autocompletion for /stamp
Ref: 2c003dd2
Signed-off-by: Michael Vetter <jubalh@iodoru.org>
2026-03-20 16:23:46 +01:00
Michael Vetter
b0a89530f2 fix(ui): Fix custom outgoing stamp and fix stamp command help
Replace the hardcoded "me" stamp for outgoing messages that are
sometimes used with the value from PREF_OUTGOING_STAMP.

It was never implemented in all code paths. Like with receipts for
example.

Additionally fix the confusing description and variable names.

Ref: 2c003dd2 (Add /stamp feature)
Ref: 3a4cd7da (Broke the variables)
Fixes: https://github.com/profanity-im/profanity/issues/2125
Signed-off-by: Michael Vetter <jubalh@iodoru.org>
2026-03-20 16:23:41 +01:00
Michael Vetter
c3fbb89a7a Merge pull request #2123 from profanity-im/ci
ci: Cleanup and improve CI jobs
2026-03-20 15:09:31 +01:00
Michael Vetter
540d9e686c ci: Don't run functional tests in CI yet
Disable the functional tests in the case of meson as well.
They don't seem to work in the CI or take too long.
This needs further investigation. For now let's run them locally and
before each release.

Signed-off-by: Michael Vetter <jubalh@iodoru.org>
2026-03-20 14:22:26 +01:00
Michael Vetter
c7834e022b chore: Require DCO from now on
Explain the how and why in CONTRIBUTING.md

Signed-off-by: Michael Vetter <jubalh@iodoru.org>
2026-03-20 14:16:46 +01:00
Michael Vetter
3b988f6790 build: add .gitattributes to exclude development files from exports
This will result in smaller and cleaner tarballs for distributions to use.

Additionally it is a logical step towards moving fully to the Meson build system.
While the last release provided two tarballs (one generated with
Autotools and one with Meson), the next release will only provide a
Meson-generated tarball.

These attributes are respected by `git archive` and `meson dist` which
means they will take effect for the Meson generated tarballs and the
GitHub autogenerated tarballs as well.

As a consequence of switching to `meson dist`, pre-generated files like
`configure` will no longer be included in the tarballs. Users and
distributions that still wish to use the Autotools build system from
these tarballs will now need to run `./bootstrap.sh` or `autoreconf -fi`
themselves, which requires automake, autoconf, and libtool to be
installed.

Since many distributions are slowly dropping the usage of vendor
generated tarballs and are moving to checking out git tags this should
not be a problem.
Arch Linux is for example doing
`source=("git+https://github.com/user/project.git#tag=v1.2.3")` and
Debian has the `git-buildpackage` too. So both of them require require
autoreconf already.

Autotools support might be removed entirely in a future
release since I find them much easier to edit and maintain.
2026-03-20 14:03:52 +01:00
Michael Vetter
24335c657c ci: Sort jobs and names in a better way
Instead of:

```
CI / Check coding style
CI / Check spelling
CI / debian | Autotools | unit | signal
CI / debian | Meson | unit+func | libomemo
CI / debian | Meson | unit+func | signal
CI / fedora | Autotools | unit | signal
CI / ubuntu | Autotools | unit | signal
Cygwin / cygwin-build
```

We will sort them into "Style", "Linux" and "Cygwin".
2026-03-20 13:14:02 +01:00
Michael Vetter
551aea683a ci: Only print log file if we find it 2026-03-20 12:47:39 +01:00
Michael Vetter
2d7426fb2f fix: Fix rare memleak
Triggered by unit tests. In Profanity itself the situation is probably
rarely happening. We would have to disconnect/exit after receiving
presence but before the roster arrives.
2026-03-20 12:47:39 +01:00
Michael Vetter
4417ec453a chore: Update valgrind suppression file
Locally valgrind is quiet. But on CI valgrind complains. This is due to
an older valgrind version which doesn't handle AVX2-optimized strings
properly. So we need to add it to the suppression file.
2026-03-20 12:47:36 +01:00
Michael Vetter
3596e4c263 ci: Run functional tests only in the case of meson
Remove stabber from Dockerfiles were we only build with autotools.
2026-03-20 12:45:40 +01:00
Michael Vetter
f778e3e093 build(autotools): Add --enable-functional-tests switch 2026-03-20 12:45:40 +01:00
Michael Vetter
45ddbe94e8 ci: Make CI more informative
Have a better overview, we are interested in which tests are run, which
distro, which build system and which omemo backend.

debian | Autotools | unit+func | signal
debian | Meson | unit+func | signal
debian | Meson | unit+func | libomemo
fedora | Autotools | unit+func | signal
ubuntu | Autotools | unit+func | signal
2026-03-20 12:34:39 +01:00
Michael Vetter
9904c05841 Merge pull request #2124 from profanity-im/fix-2083
Check connection state before accepting command.
2026-03-20 10:44:32 +01:00
Steffen Jaeckel
613df86450 Check connection state before accepting command.
Prevent `/register` and `/reconnect` if connection is not in correct state.

Fixes: https://github.com/profanity-im/profanity/issues/2083
Signed-off-by: Steffen Jaeckel <s@jaeckel.eu>
2026-03-20 09:23:41 +01:00
Michael Vetter
8548dfca96 ci: Move CI related files into own directory 2026-03-20 08:36:17 +01:00
Michael Vetter
180ee651a5 ci: Remove OpenBSD
There once was OpenBSD CI from sr.ht sponsored by wstrm.
It's not available since a log time. So let's drop it.
2026-03-20 08:23:18 +01:00
Michael Vetter
b53ec23778 ci: Remove Brewfile
a4cbf3e4 removed macOS from the CI.
We couldn't get it to build and no community member stepped up.
So let's remove this file now as well.
2026-03-20 08:21:52 +01:00
Michael Vetter
3e16ae1def feat(ui): implement dynamic pad resizing
The ncurses pad used for rendering window history had a fixed height of 100
lines, which caused "Ncurses Overflow!" errors and broke scrolling once
the content exceeded this limit.

This commit replaces the fixed limit with a dynamic resizing strategy.
We use a minimum height of 100 lines.
If a pad reaches its threshold of 3000 lines, a full redraw is triggered
to reclaim space from discarded messages and reset the pad height.
Pads are shrunk back to the minimum height during every redraw to
minimize memory usage for inactive windows.

Message history should always be smoothly scrollable now.

d7e46d64fe set it from 1000 to 10000.
f27fa98717 set it from 1000 to 100.
Probably also relevant is 4fe2c423b1.

Close: https://github.com/profanity-im/profanity/pull/2074
Fixes: https://github.com/profanity-im/profanity/issues/2045
2026-03-19 22:10:36 +01:00
Michael Vetter
b219ce9e60 Merge pull request #2121 from profanity-im/ci-buildwithlibomemo
ci: Build with libomemo-c for the "Linux Meson" job
2026-03-19 20:24:33 +01:00
Michael Vetter
e252f54436 ci: Build with libomemo-c for the "Linux Meson" job
The rest will stay with libsignal-protocol-c. So we test both.
Let's add the dependency to all Dockerfiles already.
2026-03-19 20:11:26 +01:00
Michael Vetter
046eb13e2e Merge pull request #2117 from profanity-im/xep0377
Improve support for XEP-0377
2026-03-19 19:53:22 +01:00
Michael Vetter
456a266fa9 Merge pull request #2120 from profanity-im/feature/libomemo-support
Add support for libomemo-c as OMEMO backend
2026-03-19 19:52:35 +01:00
Michael Vetter
27b5f0fbda build(meson): add support for libomemo-c as OMEMO backend
Add the ability to choose between libsignal-protocol-c (default)
and libomemo-c when building with OMEMO support enabled in Meson.

Close: https://github.com/profanity-im/profanity/pull/2020
2026-03-19 19:01:50 +01:00
Michael Vetter
82db360fb9 feat: Improve XEP-0377 support with report-origin and third-party elements
Add support for <report-origin/> and <third-party/>.

When a server forwards a report to an administrator, the original reporter
(report-origin) and the offender (third-party) are preserved even
if the 'from' address is modified during forwarding.

Also display this information in case we receive a report.
2026-03-19 13:31:13 +01:00
Michael Vetter
d395cfb6b9 Merge pull request #2118 from profanity-im/fix-2011
fix: missing plugins_post_chat_message_display calls
2026-03-19 12:59:18 +01:00
Michael Vetter
1d94316937 fix: missing plugins_post_chat_message_display calls
We need the post-display hooks in DB, log, outgoing messages, and carbons as well.
Also add pre-display hook to carbons for consistency.

Fixes: https://github.com/profanity-im/profanity/issues/2011
2026-03-19 11:46:31 +01:00
Michael Vetter
030c0b7999 docs: Improve help for /blocked 2026-03-19 11:07:51 +01:00
Michael Vetter
93918a20d1 feat: Display incoming reports (XEP-0377)
Servers might forward incoming reports to admins. So we need to display
them so they can react upon it.
2026-03-19 10:57:41 +01:00
Michael Vetter
d616e160cd Merge pull request #2116 from profanity-im/sanitzexml
feat: Sanitize illegal XML characters from outgoing messages
2026-03-19 10:40:19 +01:00
Michael Vetter
446f28ac31 fix: Use <body> tag for spam reporting (XEP-0377)
Update `blocked_add` to use the `<body>` tag with the `jabber:client`
namespace when reporting spam, as specified in XEP-0377 Example 5.
Other report types continue to use the `<text>` tag.

Fixes: https://github.com/profanity-im/profanity/issues/1971
2026-03-19 10:36:59 +01:00
Michael Vetter
189050f3f4 feat: Sanitize illegal XML characters from outgoing messages
Filter out control characters (U+0000 to U+001F) from outgoing
messages, as they are illegal in XML 1.0 (except for \t, \n, and \r).
This prevents XMPP servers from closing the connection when such
characters are accidentally or intentionally included in a message.

Fixes: https://github.com/profanity-im/profanity/issues/1437
2026-03-19 10:28:49 +01:00
Michael Vetter
1f146d1499 Merge pull request #2113 from profanity-im/omemo-noise
Reduce OMEMO noise
2026-03-19 09:36:53 +01:00
Michael Vetter
6a569626fe Merge pull request #2115 from profanity-im/fix-1986
Properly support UTF-8 characters in autocompletion
2026-03-19 09:13:02 +01:00
Michael Vetter
f6b9903c31 tests: Add test for tab autocompletion of /msg
Test both latin and UTF-8.
2026-03-19 08:55:20 +01:00
Michael Vetter
ee0d325648 tests: add prof_send_raw
Allow sending byte sequences to the Profanity PTY without a newline.
Now we can simulate control characters, specifically the `TAB` key
(`\t`), to trigger autocompletion.
2026-03-19 08:50:58 +01:00
Michael Vetter
e9c6b3b3d8 feat(tools): support UTF-8 characters in autocompletion
Autocompletion failed for nicknames using non Latin scripts. We used
`g_str_to_ascii`, which replaces characters it cannot transliterate with
'?', leading search failures and false matches between different
scripts.

Now we do:
Use `g_utf8_casefold` for case-insensitive UTF-8 comparison. This
ensures that like 'Σ' correctly match 'σ' across all Unicode scripts,
providing correct results for non English nicknames.

If we don't fine anything typing a base ASCII character matches an
accented one (typing `e` matches `è`). This pass uses `g_str_to_ascii`
followed by `g_ascii_strdown` for comparison. It is now restricted to
run if the search string itself is valid ASCII, preventing the
"everything matches '?'" bug in non Latin scripts.

Autocomplete items are sorted using `strcmp`. `g_utf8_collate` provides
linguistical ordering for a specific language. But its behavior is
locale dependent and undefined when comparing strings from different
scripts. `strcmp` does byte-wise ordering that correctly follows Unicode
code order for UTF-8 strings.
2026-03-19 08:41:50 +01:00
Michael Vetter
b7c8f8235c feat(omemo): suppress "new device" alerts for already known devices
Modify omemo_set_device_list to check against the persistent
known_devices.txt file before notifying the user. This prevents
false-positive security alerts on every restart for devices
that have already been registered and trusted.

Check against the persistent known_devices.txt file before notifying
the user. This ensures that "New OMEMO device" alerts are only shown
for devices that are truly new to this client.

Ref: ce9b5140d
2026-03-18 22:19:18 +01:00
Michael Vetter
4253da99c5 feat(omemo): suppress redundant session already exists messages
Removing the UI notification in omemo_start_device_session to reduce
noise during /omemo start.
The information is still available in the debug logs.

And I think for existing sessions its just too much without value.

Ref: b20e3f1a
2026-03-18 22:19:13 +01:00
Michael Vetter
321ff57150 feat(omemo): suppress repetitive missing device ID warnings
When sending OMEMO messages, we print a warning message for every
participant without a device ID on every single send:

```
Can't find a OMEMO device id for my@jid.org
```

This clutters the UI due to the high volume of repetitive information.

This is solved by tracking suppressed warnings within the window
structure. A warning for a specific JID and type is only shown once
per window context.

Suppression state is stored in the base ProfWin structure and managed
via helper functions (win_warn_needed, win_warn_sent). The state is
lazily initialized to save resources and automatically cleaned up when
the window is closed.

Warnings are explicitly reset when an OMEMO session is started or
ended to ensure users see fresh alerts when toggling encryption.
2026-03-18 21:48:37 +01:00
Michael Vetter
9de455ceea Merge pull request #2112 from profanity-im/fix-2110
Fix 2110
2026-03-18 17:16:07 +01:00
Steffen Jaeckel
2240879956 Fix potential double free.
Change `log_database_get_previous_chat()` to not take ownership of
`end_time`, so it's clear that whoever passes it in, has to free it.

Fixes: https://github.com/profanity-im/profanity/issues/2110
Signed-off-by: Steffen Jaeckel <s@jaeckel.eu>
2026-03-18 16:47:49 +01:00
Steffen Jaeckel
349de02b8c Introduce prof_date_time_format_iso8601().
Instead of repeating the same pattern over and over, introduce a helper
function that either outputs a formatted timestamp if one is given as
argument or returns the current time if no argument is given.

Signed-off-by: Steffen Jaeckel <s@jaeckel.eu>
2026-03-18 16:47:49 +01:00
Steffen Jaeckel
f55f7611c2 The buffer API should use unsigned types.
Either there are buffer entries or there are none. No need to have negative
values in the API.

Signed-off-by: Steffen Jaeckel <s@jaeckel.eu>
2026-03-18 16:47:49 +01:00
Michael Vetter
73ccd6bc9b Merge pull request #2109 from profanity-im/more-fixes
More fixes
2026-03-16 10:55:56 +01:00
Steffen Jaeckel
d9a8c579b6 Optimize random data generation.
In the process of trying to bring down the time of sending an OMEMO
encrypted message I noticed that a lot of time was spent in generating the
random key and IV (~0.54s on my machine).
By changing this to a single call to `gcry_random_bytes_secure()` I have
been able to slash the time by 50% to ~0.27s.

Signed-off-by: Steffen Jaeckel <s@jaeckel.eu>
2026-03-16 10:43:28 +01:00
Steffen Jaeckel
cbe6dac448 Flush OMEMO store only after encrypting for all recipients.
Instead of flushing the OMEMO store to disk after each operation, do it
only once after all recipients are processed.

A user reported a long delay between hitting enter on the keyboard and the
message being sent on the wire in an OMEMO-enabled group chat. This patch
hopefully improves this situation.

Signed-off-by: Steffen Jaeckel <s@jaeckel.eu>
2026-03-16 10:43:28 +01:00
Steffen Jaeckel
ff65db0b10 Fix potential segfault.
It's not guaranteed that `log_database_get_limits_info()` returns a
non-NULL value or that `timestamp` is non-NULL.

Signed-off-by: Steffen Jaeckel <s@jaeckel.eu>
2026-03-16 10:43:28 +01:00
f84ed1bf6a perf(functests): speedup — profrc pre-baking, pty-close shutdown, parallel port pools
All checks were successful
CI Code / Check spelling (pull_request) Successful in 18s
CI Code / Check coding style (pull_request) Successful in 32s
CI Code / Code Coverage (pull_request) Successful in 2m46s
CI Code / Linux (debian) (pull_request) Successful in 4m30s
CI Code / Linux (ubuntu) (pull_request) Successful in 4m29s
CI Code / Linux (arch) (pull_request) Successful in 4m44s
CI Code / Check spelling (push) Successful in 18s
CI Code / Check coding style (push) Successful in 33s
CI Code / Code Coverage (push) Successful in 3m4s
CI Code / Linux (ubuntu) (push) Successful in 4m29s
CI Code / Linux (debian) (push) Successful in 4m33s
CI Code / Linux (arch) (push) Successful in 4m48s
Replace 16 interactive UI setup commands with pre-written profrc file
containing [ui] and [notifications] sections (~1800ms saved per test).

Replace sleep(1) + blocking waitpid with close(pty fd) → SIGHUP →
polling waitpid(WNOHANG) → SIGTERM/SIGKILL fallback chain (~4900ms
saved per test).

Remove post-stbbr_start() and post-stbbr_stop() sleeps — bind+listen
completes synchronously before stbbr_start() returns, and
pthread_join() in stbbr_stop() guarantees socket cleanup (~200ms saved).

Add PORTS_PER_GROUP=50 isolated port ranges per test group to enable
safe parallel execution of 4 groups without port conflicts.
2026-03-12 18:20:35 +03:00
Michael Vetter
ab3f58b3a3 docs: Add information about footers in git commit messages 2026-03-09 13:32:05 +01:00
Michael Vetter
acba9d9da7 Merge pull request #2108 from profanity-im/omemo-ux-fix
fix: Dont OMEMO trust check so often
2026-03-09 13:22:19 +01:00
Michael Vetter
833090aac5 chore: Add copyright header to C files who had none 2026-03-09 13:14:44 +01:00
Michael Vetter
798edce22a chore: Move to SPDX license header 2026-03-09 12:55:37 +01:00
Michael Vetter
04ba2700d0 docs: Sort entries in theme_template 2026-03-09 12:19:25 +01:00
Michael Vetter
7235b019fc docs: Update theme_template with missing options
Synchronize theme_template with the current codebase by adding all
themeable preferences and color attributes that were previously
undocumented.
Remove 'wins.autotidy' option which is no longer used.
2026-03-09 12:17:23 +01:00
Michael Vetter
562b7c2ab9 docs: Add Conventional Commit Structure 2026-03-09 12:08:26 +01:00
Michael Vetter
aaf146bd4f chore: Add XEP comparison script
The idea is that we keep our DOAP file updated.
Then we always know which version of a XEP we implemented.
By running this script we can then find out if a new version of a XEP
came out.
And we can check whether we need to update our code to adhere to the
latest version or whether we can just version bump.
2026-03-09 11:51:49 +01:00
Michael Vetter
624585572e fix: Dont OMEMO trust check so often
a26cdf386b / #2104 added OMEMO trust status to the
title bar, but it was being re-calculated on UI refresh.

Update only on certain operations (like `/omemo trust`, new device
discoveries and even incoming OMEMO messages for example).
2026-03-09 11:38:51 +01:00
Michael Vetter
205634534e Merge pull request #2107 from profanity-im/fix/2106
fix: Fix not saving first created account
2026-03-09 10:37:31 +01:00
Michael Vetter
5c484c26ed fix: Fix not saving first created account
Commit 81f92f1fe introduced a selective update mechanism to
_accounts_save() to prevent overwrites when running multiple instances
by only modifying the specific account being saved.

There was one mistake: the logic for adding a new
account was tied to the final iteration of a loop over
existing accounts.

When starting with an empty accounts file, the
loop never executes though.

We can call g_key_file_set_value() directly and don't need the loop
because this function will only modify one entry or add it if it doesn't
exist.
2026-03-09 09:57:48 +01:00
Michael Vetter
0b46c32a71 chore: updat clang-format action to 4.16.0 2026-03-09 09:17:24 +01:00
Michael Vetter
a26cdf386b Merge pull request #2104 from profanity-im/omemo-ux
Improve OMEMO UX
2026-03-09 09:15:24 +01:00
Michael Vetter
e39ffd981f refactor: Refactor OMEMO error handling and add non-null attributes
Add __attribute__((nonnull) hints to omemo_receive_message and
omemo_on_message_recv declarations to catch NULL arguments at
compiletime.
So we can set the error state unconditionally in the functions.
2026-03-09 08:48:41 +01:00
Michael Vetter
3e2673aad2 cleanup: Cleanup types 2026-03-09 07:52:21 +01:00
Michael Vetter
4d49c2b746 feat: Provide descriptive fallback messages for OMEMO decryption failures
If an incoming OMEMO message failed to decrypt (due to missing session
keys or untrusted identities), Profanity would fall back to displaying
the raw XMPP body. This usually contained a generic string like "This
message is encrypted with OMEMO,".

We wrote the detailed reason in the debug logs but the user only saw the
fallback message and probably wondered *why* the message wasn't
displayed properly.

I'm unsure if we should display the fallback message as well though.
2026-03-09 07:49:03 +01:00
Michael Vetter
b20e3f1a0c feat: Improve feedback during OMEMO session Initiation
After typing `/omemo start` a user was not informed of what exactly
happens.
Profanity is fetching the device list and the key bundle for each
device. This happens asynchronously. It could be that it takes some time
and the user already starts typing a message, believing OMEMO is
working.

Now we print:
* Initiating OMEMO session..
* Fetching device list and key bundles
* OMEMO session ready

So the user understands much better the current situation.
2026-03-09 07:49:03 +01:00
Michael Vetter
5a3083f273 feat: Show active and trust status in /omemo fingerprint
The /omemo fingerprint command provided a list of all keys ever
seen for a contact but did not indicate which keys were currently "active"
(present in the latest server device list). This made it difficult for users
to identify which fingerprints actually required verification to fix
encryption issues.

Users can now easily distinguish between devices currently in use and
historical keys that no longer matter.

When encryption fails users can immediately see which "active" key is
"untrusted," allowing them to verify the correct fingerprint fast.

Function `omemo_is_device_active` will check if a fingerprint belongs
to a device currently announced by the contacts server.
2026-03-09 07:49:03 +01:00
Michael Vetter
99c03f8c3b feat: Provide detailed encryption failure messages for OMEMO
When OMEMO encryption failed due to untrusted devices, the
user was presented with a generic error message. This made it difficult
to identify which specific fingerprints were causing the issue, forcing
users to manually hunt for untrusted keys using /omemo fingerprint.

New function `omemo_get_jid_untrusted_fingerprints` to specifically
identify unverified identities for a contact.
`omemo_on_message_send` will now catch encryption failures and
explicitly list the untrusted fingerprints in the chat window.
2026-03-09 07:49:03 +01:00
Michael Vetter
ce9b5140d5 feat: Notify users when new OMEMO devices or fingerprints are discovered
Previously, Profanity silently updated OMEMO device lists and identity
caches in the background. This often led to confusion when a contact
added a new device, as encryption or decryption would eventually fail
due to untrusted keys without any prior warning to the user.

The `notifying` flag will distinguish between initial data loading and
realtime discovery.

We now print a message both when a contact publishes a new device ID or
when a new fingerprint is discovered.
2026-03-09 07:49:03 +01:00
Michael Vetter
ded2f6afc0 feat: Add OMEMO trust status indicators to the titlebar
Previously the titlebar only showed [OMEMO], regardless of whether the
active session contained untrusted devices. This forced users to manually
run /omemo fingerprint to check the security status of their conversation,
making it difficult to know at a glance if a session was truly secure.

New function `omemo_is_jid_trusted()` to check if all active devices for a JID are trusted.
Update chat and MUC titlebar to display [trusted] or [untrusted] tags when an OMEMO session is active.
Not sure if MUC makes sense here?
2026-03-09 07:49:03 +01:00
Michael Vetter
73b44eff67 chore: Add 0.16.0 to release in doap file 2026-03-09 07:30:39 +01:00
Michael Vetter
7dedd26440 docs: Update implemented XEPs 2026-03-09 07:29:00 +01:00
Michael Vetter
7663c334ed Merge pull request #2105 from profanity-im/fix/2103
fix: Sanitize account names
2026-03-08 22:30:29 +01:00
Michael Vetter
bfc9eb259c fix: Sanitize account names
GKeyFile parses ini-like config files. So the characters '[', ']',
and '=' cannot be used.
We will replace them with '_' now.

This bug was found when a user wanted to connect to an IPv6 address (user@[ipv6:address]).

One problem could be that we get duplicate account names with they would
use one of those 3 characters. But I consider this very unlikely.

Fix https://github.com/profanity-im/profanity/issues/2103
2026-03-08 21:45:35 +01:00
9ec01fa8cc fix: CWE-134 format string audit and compiler hardening
All checks were successful
CI Code / Check spelling (push) Successful in 20s
CI Code / Check coding style (push) Successful in 36s
CI Code / Code Coverage (push) Successful in 5m38s
CI Code / Linux (ubuntu) (push) Successful in 7m1s
CI Code / Linux (debian) (push) Successful in 7m5s
CI Code / Linux (arch) (push) Successful in 7m19s
Security:
Fix CWE-134 in iq.c: user-controlled string passed as format argument
Add G_GNUC_PRINTF annotations to all variadic printf-like wrappers
in ui.h, log.h and http_common.h
Compiler flags (configure.ac):

Replace basic -Wformat/-Wformat-nonliteral with -Wformat=2
Add -Wextra, -Wnull-dereference, -Wpointer-arith,
-Wimplicit-function-declaration, -Wundef, -Wfloat-equal,
-Wredundant-decls, -Walloc-zero
Add -fstack-protector-strong, -fno-common, -D_FORTIFY_SOURCE=2
Add GCC-specific flags via AC_COMPILE_IFELSE: -Wlogical-op,
-Wduplicated-cond, -Wduplicated-branches, -Wstringop-overflow,
-Warray-bounds=2
Suppress noisy -Wextra sub-warnings: -Wno-unused-parameter,
-Wno-missing-field-initializers, -Wno-sign-compare,
-Wno-cast-function-type
Remove AM_CFLAGS/CFLAGS duplication
Bug fixes found by new warnings:

chatlog.c: non-MUCPM redact path passed resourcepart instead of NULL
rosterwin.c: merge duplicated if/else branches into single condition
omemo.c: redundant else-if in omemo_automatic_start; remove
unnecessary scope block and goto, use early return
console.c: pointer compared to integer 0 instead of NULL
stanza.c: increase pri_str/idle_str buffers from 10 to 12 bytes
(INT_MIN = -2147483648 needs 12 bytes including NUL)
vcard.c: NULL guard for filename before g_file_set_contents
api.c: broken log_warning() calls with extra format argument
Format mismatch fixes:

chatwin.c: Jid* → char* for %s
connection.c: %x → %lx for long flags
cmd_funcs.c: %d → %zu for size_t; cast gpointer to char* for %s
cmd_defs.c: %d → %u for g_list_length() return (guint)
iq.c: barejid → fulljid for from_jid
console.c, mucwin.c, privwin.c, account.c, omemo.c, presence.c:
gpointer → (char*) casts for %s
Const-correctness and cleanup:

database.c: const for type, query, sort variables
form.c/xmpp.h: const for form_set_value parameter
files.c: refactor to early return, eliminating NULL logfile path
muc.c/muc.h: remove meaningless top-level const on return type
common.c: const for URL string literal
Remove stale declarations: cons_show_desktop_prefs (ui.h),
connection_set_priority (connection.h),
omemo_devicelist_configure_and_request (omemo.h)
test_common.c: add currb NULL check to silence -Wnull-dereference
Tooling (check-cwe134.sh):

Reduce from 5 checks to 2 (checks 1-3 redundant with -Wformat=2)
Check 1: verify known wrappers have G_GNUC_PRINTF attribute
Check 2: auto-detect unannotated variadic printf-like functions
Match both const char* and const gchar* in variadic patterns

Author: jabber.developer2 <jabber.developer2@jabber.space>
2026-03-07 11:55:50 +01:00
Michael Vetter
185e943adf Merge pull request #2102 from profanity-im/msgjid
Improve JID validation and fix a bug when loading database messages
2026-03-06 15:18:45 +01:00
Michael Vetter
40963c702e fix: database return NULL if no history limits are found
log_database_get_limits_info should return NULL when a contact has no
history in the database.

We hit a bug with `/msg adsf@asd` before.
2026-03-06 15:05:57 +01:00
Michael Vetter
f251cc8bb3 feat: handle and allow JIDs with trailing slashes
Make jid_is_valid() allow JIDs ending with a slash (`user@domain/`).
In these cases, the parser now treats the input as a Bare JID with no
resource part, rather than rejecting it as invalid.

We will then just get `user@domain`.
2026-03-06 15:05:52 +01:00
Michael Vetter
35a2f2990a fix: Correct handling of create_fulljid without resource
If resource was NULL/"" we otherwise had a trailing slash.
2026-03-06 13:09:49 +01:00
Michael Vetter
fa64b135df refactor: Centralize validation within jid_is_valid 2026-03-06 13:08:06 +01:00
Michael Vetter
d6705362a2 fix: Only allow /msg with valid JIDs
Only allow to message someone who has a nick in roster or when we pass a
user JID.
Otherwise notify the user.
2026-03-06 12:36:53 +01:00
Michael Vetter
5bcf859e92 feat: Add jid_is_valid_user_jid() to check for valid user JIDs
This one will need a localpart as well.
It should be used when we want to message a user for example.
While jid_is_valid() also returns TRUE for domain/server JIDs.
2026-03-06 12:35:30 +01:00
Michael Vetter
09757da5df feat: Improve validation of JIDs
Add new `jid_is_valid()` function and improve the adherence to XMPP RFCs
in jid_create().

Add unit tests for them as well.
2026-03-06 12:11:30 +01:00
Michael Vetter
a001a6c4b7 Merge pull request #2101 from profanity-im/functionaltests
Reenable functional tests
2026-03-06 11:13:56 +01:00
Michael Vetter
778a9b3d3d tests: Don't build functional test with analyzer and increase timeouts 2026-03-06 11:01:18 +01:00
Michael Vetter
285a5086ca docs: Add note about sanitizers overhead 2026-03-06 09:53:49 +01:00
Michael Vetter
f3e3a64ec2 docs: Add a section explaining how to write functional tests 2026-03-06 09:45:51 +01:00
Michael Vetter
cea4c961e0 docs: Add section explaining how to run functional tests 2026-03-06 09:35:58 +01:00
Michael Vetter
5bc6604648 tests: remove apparently unused code 2026-03-06 09:02:09 +01:00
Michael Vetter
cf7b5f808e tests: fix functional presence tests 2026-03-05 22:46:33 +01:00
Michael Vetter
23fe2a4661 tests: fix functional test message_receive_chatwin 2026-03-05 22:22:24 +01:00
Michael Vetter
2692f70473 tests: fix functional test message_send 2026-03-05 22:14:57 +01:00
Michael Vetter
82c9284616 tests: fix functional test resets_to_barejid_after_presence_received 2026-03-05 22:00:09 +01:00
Michael Vetter
e03b165741 tests: fix functional test sends_room_join 2026-03-05 21:31:25 +01:00
Michael Vetter
e0fbe40757 tests: fix functional test send_receipt_on_request 2026-03-05 20:24:16 +01:00
Michael Vetter
421bb2e261 feat: Only request receipts when supported
Only request delivery receipts if the recipient is known to support them
via entity capabilities
As mentioned in the recommendations of XEP-0184.
2026-03-05 20:21:47 +01:00
Michael Vetter
b3d61587e1 tests: fix functional test receive_carbon 2026-03-05 19:45:58 +01:00
1508f27e73 Merge branch 'ci/separate-build-step'
All checks were successful
CI Code / Check spelling (push) Successful in 21s
CI Code / Check coding style (push) Successful in 37s
CI Code / Code Coverage (push) Successful in 5m35s
CI Code / Linux (debian) (push) Successful in 6m56s
CI Code / Linux (ubuntu) (push) Successful in 7m2s
CI Code / Linux (arch) (push) Successful in 7m24s
2026-03-05 18:36:13 +01:00
Michael Vetter
897e192544 ests: fix functional test rooms_query 2026-03-05 13:51:28 +01:00
Michael Vetter
fbd31f61a6 tests: fix functional test connect_shows_presence_updates 2026-03-05 13:19:37 +01:00
Michael Vetter
3f989775a6 tests: fix functional test ping_responds_to_server_request 2026-03-05 13:12:02 +01:00
Michael Vetter
cb89451b3a tests: fix functional test ping_jid 2026-03-05 13:11:59 +01:00
Michael Vetter
d2d5f1489f tests: Fix passing of special characters to prof_expect()
We were passing our raw strings to the regex engine. We need to escape
them with g_regex_escape_string() first.
2026-03-05 13:11:59 +01:00
Michael Vetter
edd36f784f tests: fix functional test test_ping 2026-03-05 13:11:54 +01:00
Michael Vetter
d7142479ae tests: try two places for profanity binary 2026-03-05 12:33:03 +01:00
Michael Vetter
9cb11bdec2 tests: Reenable functional tests
They were disabled in 171b6e73c9.

Enable the functional test suite and replaces the
dependency on libexpect with a custom solution.

The native solution allows for specific optimizations like automatic
ANSI code stripping, which is essential for reliable pattern matching in
an ncurses interface.

Custom PTY management via libutil provides full control over the process
lifecycle, resolving issues where tests would hang indefinitely.
We use forkpty from libutil.

Addiiotnally this commit implements automatic ANSI escape sequence
filtering to improve matching consistency.
This will get rid of the colors in matches. It's still something that we
need to think about more since basically we will not test out
color/theme system this way.

We also add non-blocking poll() and SIGKILL based teardown logic to
ensure clean test termination.

We added the functional tests to both autotools and meson.
They are only build when tests are enabled and stabber + libutil are
present.
Meson will print this out nicely in the summary.

Regards https://github.com/profanity-im/profanity/issues/789
2026-03-04 22:32:38 +01:00
Michael Vetter
11bc6ea22b fix: Fix usage error in /time command
When targeting a specific preference (`/time console off`)
the loop would continue to iterate through the remaining
categories after finding and applying the setting.

Because subsequent items would not match, it always resulted in
"invalid usage" printed as well after the success message.
2026-03-04 22:23:31 +01:00
Michael Vetter
99e7057a36 fix: Increase max arguments for /connect to 9
The /connect command supports several optional parameters
server, port, tls, and auth.

This was implemented in ac410445af but
this change in the command definitions probably were forgotten.
2026-03-04 22:20:31 +01:00
9e1b95a814 test(disco): remove disco_items_error_handling test
All checks were successful
CI Code / Check spelling (pull_request) Successful in 22s
CI Code / Check coding style (pull_request) Successful in 40s
CI Code / Linux (debian) (pull_request) Successful in 6m59s
CI Code / Linux (arch) (pull_request) Successful in 7m21s
CI Code / Linux (ubuntu) (pull_request) Successful in 7m13s
CI Code / Code Coverage (pull_request) Successful in 10m59s
CI Code / Check spelling (push) Successful in 20s
CI Code / Check coding style (push) Successful in 36s
CI Code / Code Coverage (push) Successful in 5m43s
CI Code / Linux (ubuntu) (push) Successful in 7m9s
CI Code / Linux (debian) (push) Successful in 10m5s
CI Code / Linux (arch) (push) Successful in 12m0s
This test requires the fix from fix/xep-0030-disco-items-error-handling
branch. Moved there along with the source code fix.
2026-03-03 16:57:35 +01:00
24e1dac354 test(disco): add comprehensive XEP-0030 functional tests
Add 7 new tests for /disco command:
- disco_items_to_jid: query items to specific JID
- disco_info_empty_result: handle empty disco#info response
- disco_info_multiple_identities: multiple identity elements
- disco_info_without_name: identity without optional name attr
- disco_items_without_name: items without optional name attr
- disco_info_service_unavailable: error handling for info
- disco_items_error_handling: error handling for items (XEP-0030 §7)

The disco_items_error_handling test documents a bug where disco#items
errors are silently ignored (unlike disco#info which handles them).
This violates XEP-0030 Section 7 which requires error feedback to user.
2026-03-03 16:57:35 +01:00
f20a4da160 Add functional tests for /disco command (XEP-0030)
- Add 8 tests for disco info and disco items commands
- Fix XEP-0030 compliance bug: show message for empty disco#items results
- Tests cover: identity display, features, server/jid queries, error handling,
  items display, empty results, and connection requirement
2026-03-03 16:57:35 +01:00
663a959f9c test: add functional tests for autoping (XEP-0199)
- autoping_set_interval: verify /autoping set command
- autoping_set_zero_disables: verify disabling autoping
- autoping_timeout_set: verify /autoping timeout command
- autoping_timeout_zero_disables: verify disabling timeout
- autoping_sends_ping_after_interval: verify automatic ping IQ
- autoping_server_not_supporting_ping: verify error handling

Fast tests (command parsing) in Group 1, slow tests (timer-based) in Group 3.
2026-03-03 16:57:35 +01:00
Michael Vetter
7088c2c95e Merge pull request #2100 from profanity-im/fix/2098
Fix file autocompletion bugs and restore cycling
2026-03-01 01:44:50 +01:00
Michael Vetter
8dbd1e3253 fix: Fix file autocompletion bugs and restore cycling
Fix several issues in cmd_ac_complete_filepath:
* Prevent a segfault when input is empty.
* Fix a double free where acstring was managed by both auto_gchar and GArray.
* Fix a memory leak when reassigning inpcp during quote stripping.
* Restore the ability to cycle through files on repeated TAB presses by
  caching the last input and skipping updates if the input is already a
  known completion.
* Preserve user input style (e.g. ~, ./) in autocompletion strings to
  ensure matches are correctly displayed and filtered.

Bug got introduced when "cleaning" code with new compiler flags and
sanitizers: aec8e48268.
Add unit tests so this doesn't happen again.

Before that commit we didn't use the static variable but used
autocomplete_update instead. Now we avoid redundat updates and preserve
the state across tab presses. We should look at this again later.

Fix https://github.com/profanity-im/profanity/issues/2098
2026-03-01 01:26:14 +01:00
Michael Vetter
612cc0e59e Merge pull request #2099 from profanity-im/clean-unittests
Cleanup unittests and improve documentation for new contibutors
2026-02-28 15:15:13 +01:00
Michael Vetter
aadf912e0e docs: Expand the build section in CONTRIBUTING.md
Usually we point to the website. But honestly it's cumbersome for
developers to update the website. Let's decide later whether we remove
the section there or how we keep it in sync.

But for now let's just add a detailed build section here.
Especially since now we ship autotools *and* meson.

Also mention the convention that we use `build_run` as the meson build
dir. The quality script will depend on this.
2026-02-28 14:52:36 +01:00
Michael Vetter
91911b3e2f ci: Update clang-format to version 21
Update the gh workflow to pin clang-format to version 21.

Mention the version we use in CONTRIBUTING.md.
And hint to the workflow file if we want to change it.
2026-02-28 14:52:36 +01:00
Michael Vetter
109347eea5 docs: Explain how to turn clang-format of for some blocks of code 2026-02-28 14:52:36 +01:00
Michael Vetter
b0abb64950 refactor: Fix clang-format expansion of table
By removing the trailing comma from the array initialization we hint
clang-format to maintain the compact layout.
2026-02-28 14:52:36 +01:00
Michael Vetter
80e36bd1e2 tests: Apply coding style to unit tests 2026-02-28 14:52:36 +01:00
Michael Vetter
174848499d feat: Add unified quality-check script and git hook support
Move the `make doublecheck` functionality into a build system agnostic
script.

`scripts/quality-check.sh` can now be used to check for spelling via
codespell, formatting clang-format and run the unit tests.

`make doublecheck` and `meson compile doublecheck` will call this
script.

Sometimes we have issues with different versions of clang-format locally
vs our CI. In this case SKIP_FORMAT env variable can be set.
2026-02-28 14:52:27 +01:00
Michael Vetter
b29326969c docs: Explain how to write unit tests 2026-02-28 12:56:13 +01:00
Michael Vetter
e81435be06 tests: standardize naming convention
Use a behavior-driven naming convention for unit tests.

Functions are now named using the pattern: [unit]__[verb]__[scenario]
The __ works as a semantic separator.

Examples:
* jid_create__returns__null_from_null()
* cmd_connect__shows__usage_when_no_server_value

Benefits:
* Easy to find all tests associated with a specific function using the
  mandatory prefix.
* Test output in CI now explicitly describes the unit, the expected
  outcome, and the scenario being tested.

Also disabled keyhandlers tests due to missing code in src/ui.
2026-02-28 12:41:33 +01:00
Michael Vetter
a7408a970e cleanup: Map test files according to structure in src
For example cmd_account() is located in src/command/cmd_funcs.c.
The unit test was located in tests/unittests/test_cmd_account.c.
Let's move it to a subdirectory like tests/unittests/command/test_cmd_account.c
to correpsond to the same structure.
2026-02-28 11:36:47 +01:00
Michael Vetter
d8c16640c5 Merge pull request #2097 from profanity-im/fix/731-pseudo-join
fix: Ignore self-presence for untracked MUC rooms
2026-02-28 11:32:24 +01:00
Michael Vetter
e18b71576e Merge pull request #2096 from profanity-im/fix/1997-floatingpoint
fix: Fix Floating Point Exception in OMEMO session building
2026-02-28 11:07:35 +01:00
Michael Vetter
9fb45b543b fix: Ignore self-presence for untracked MUC rooms
Only handle self-presence for rooms we are actively managing.

Earlier we blindly react to any self-presence
stanza by calling 'ui_room_join'.

The join could have been initiated by another client or an external
Gateway. In the case of #731 this was Slack.
I remember that more people reported this kind of bug in our MUC.

We then would open a window and the nick would be (null) because we
don't have any nickname for that room set.

So only react on self-presence if we manage the MUC.
Other XMPP clients do the same.

Fix https://github.com/profanity-im/profanity/issues/731
2026-02-28 10:42:03 +01:00
Michael Vetter
45c551b8ac Merge pull request #2095 from profanity-im/morefixes
Add more compiler flags
2026-02-28 10:34:33 +01:00
Michael Vetter
bc13a726b6 fix: Fix Floating Point Exception in OMEMO session building
When an OMEMO bundle is received with an empty <prekeys/> element,
`prekeys_list` remains empty. In omemo_start_device_session(),
performing a modulo operation on the length of an empty list
(0) triggered a SIGFPE (Floating point exception).

torsocks likely affected the network environment where such empty
or incomplete bundles could be intercepted.

Fix https://github.com/profanity-im/profanity/issues/1997
2026-02-28 10:28:26 +01:00
Michael Vetter
09c9307493 ci: build with ASan and UBSan
Should help us find the following bugs early.

ASan:
* Out-of-bounds accesses
* Use-after-free
* Memory leaks
* Double free / Invalid free

UBSan:
* Undefined Behavior
* Signed integer overflow
* Null pointer dereference
* Pointer misalignment
* Division by zero
* Bit-shifting out of bounds

This needs works during runtime. But let's add it here so that at least
basic --version run is test and as a reminder for developer to add it.
I will also adjust documentation later on.
2026-02-28 10:10:04 +01:00
Michael Vetter
ab4adf19d0 refactor: Refactor form_set_value to use glib list management
Simplify how field values are updated and ensure memory is correctly
managed.

The previous implementation relied on manual manipulation of the
GSList internal `data` member and only handled cases where a field
had zero or one existing value.

The function now correctly handles fields that may
already contain multiple values by using 'g_slist_free_full' to
clear the entire list and its contents before setting the new
value.
2026-02-28 10:02:26 +01:00
Michael Vetter
434a887834 Fix -Wanalyzer-deref-before-check warning in get_message_from_editor
Let's add an explicit check.
2026-02-28 09:55:33 +01:00
Michael Vetter
c24cdcd671 fix: Fix -Wanalyzer-deref-before-check warning in cmd_ac_complete
Adding an explicit NULL check for 'input' at the start of
cmd_ac_complete to prevent a potential crash and resolve
a GCC static analyzer warning.

Quirk Explanation:
The analyzer found  a "deref-before-check" warning for:
`if ((strncmp(input, "/", 1) == 0) && (!strchr(input, ' ')))`

The analyzer interpreted 'strchr(input, ' ')' as a point where the
validity of 'input' is being questioned (a "check"), even though it is
actually checking the return value. This is due to GCC's internal model
of certain standard string functions having a 'nonnull' attribute or
being categorized as pointer "interrogations" in its state machine.

Because 'strncmp' dereferences 'input' earlier in the same line, the
analyzer saw a logical contradiction: "You treat it as safe in strncmp,
but then you call a function (strchr) that 'requires/checks' it to be
safe, implying you weren't sure."
2026-02-28 09:44:27 +01:00
Michael Vetter
4b5c253b89 build: Add -Wpointer-arith flag
Even though we are using with GNU extensions I will add this flag
since I prefer the explicit writing style that we have to use wich
this flag enabled.
2026-02-28 09:29:44 +01:00
Michael Vetter
38624f26a5 Merge pull request #2094 from profanity-im/flags
Let's build with -fanalyzer in debug mode.
These commits are related to issues found by gccs static analyzer.

Also bulid with -fstack-protector-strong compiler flag.
This flag adds a canary to stack frames, causing the program to terminate immediately if stack corruption is detected.

This helps identify memory safety bugs earlier during development by
turning silent corruption into an immediate crash.
2026-02-27 23:15:44 +01:00
Michael Vetter
36b15ec6d5 tests: update unit tests
Not really a best practise. They should have been run before each commit
and updated accordingly. Next time..
2026-02-27 23:01:14 +01:00
Michael Vetter
02cde29b65 cleanup: use g_new() instead of malloc in prof_add_shutdown_routine() 2026-02-27 22:29:45 +01:00
Michael Vetter
051f986774 cleanup: make _connection_handler() safer
Check for the memory before using it.
2026-02-27 22:29:45 +01:00
Michael Vetter
10ef8505f8 cleanup: Cleanup log modul
* _rotate_log_file(): guard against NULL mainlogfile before g_strdup/strlen
* log_stderr_init(): move dup2 after malloc checks to avoid fd leak on
  allocation failure.
  Also remove explicit close(STDERR_FILENO) before dup2
  since dup2 closes the target atomically. Close the fd returned by
  dup2 immediately.
2026-02-27 22:29:45 +01:00
Michael Vetter
35030b7a7e cleanup: use g_new0 instead of malloc in buffer_get_entry_by_id() 2026-02-27 22:29:45 +01:00
Michael Vetter
7876645cb0 cleanup: be a bit more defensive in 2026-02-27 22:29:45 +01:00
Michael Vetter
7b0232c11c wleanup: be a bit more defensive in server_events.c 2026-02-27 22:29:45 +01:00
Michael Vetter
286e563fbe fix: fix redundant error reporting in http download
We were sequentially checking for errors from 'curl_easy_perform',
'ftell', and 'fclose'. Each time overwriting the last one, resulting in
a leak. This commit ensures that 'err' is only set if it is currently
NULL, preserving the first and most specific error encountered.
2026-02-27 22:29:45 +01:00
Michael Vetter
f5787fb31f cleanup: refactor account_eval_password to use glib
Move from popen() to g_spawn_command_line_sync().
Which is nicer and gets rid of a resource leak that was present here.
2026-02-27 22:29:45 +01:00
Michael Vetter
5d31f376dd cleanup: check for strdup() success in stanza_create_http_upload_request() 2026-02-27 22:29:45 +01:00
Michael Vetter
0218ba26c8 fix: fix memory leak and potential crash in iq_id_handler_add
`userdata` was not freed if a handler could not be registered.

This occurs when the 'id' parameter is NULL. In such cases, the
handler cannot be stored in the 'id_handlers' table, but ownership
of 'userdata' has already been transferred to this function. This
commit ensures 'free_func' is called if 'id' is NULL since we always
need to have an ID.
2026-02-27 22:29:45 +01:00
Michael Vetter
e15f659277 fix: Fix NULL dereference and memory leak in _mam_rsm_id_handler
Add NULL checks before calling 'strlen' and also ensures that the string
is long enough before attempting to offset it by 3.
2026-02-27 22:29:45 +01:00
Michael Vetter
776bc262d0 refactor: change vcard_print() checks
data is allocated via g_new0() so cannot be NULL here.
And it was used before the check already so that didn't make any sense
in the first place.
2026-02-27 22:29:45 +01:00
Michael Vetter
1ec536d343 fix: fix api_get_current_occupants() memory problems
Fix a potential NULL pointer dereference. And free the list properly.
2026-02-27 22:29:45 +01:00
Michael Vetter
2406413e14 fix: fix NULL dereference and memory leaks in OX logic
recp elements were accessed without verifying the success of
_ox_key_lookup().

Also fix several memory leaks.

gpgme_data_new() followed 'gpgme_data_new_from_mem' was redundant.
2026-02-27 22:29:42 +01:00
Michael Vetter
16080ab985 cleanup: Fix potential null pointer dereference 2026-02-27 19:45:57 +01:00
Michael Vetter
f74501e97e cleanup: add a defensive check in cmd_process_input() 2026-02-27 19:36:47 +01:00
Michael Vetter
d8a7b234bb fix: segfault when using /command help
Usually we use `/help command`. Additionally there is a shortcut
`/comamnd?`. 54fea4afcf tried to introduce
to have `/command help`. Which doesn't really work since most commands
get this as parameters. So it never worked. Additionally it introduced
at segfault because it tried to set *question_mark to NULL even we can
also get there without the `?` but by having the `help` as parameter.
2026-02-27 19:33:28 +01:00
Michael Vetter
373ec4a7e3 refactor: replace malloc with g_new0 in many occasions 2026-02-27 19:26:42 +01:00
Michael Vetter
de5949e8eb refactor: partly move plugins to glib
That's not very nice yet but we have to start somewhere.
2026-02-27 19:26:42 +01:00
Michael Vetter
20154048e7 refactor: Move pgp module to gchar 2026-02-27 19:26:42 +01:00
Michael Vetter
45dc50eccf fix: initialize OMEMO pointers and add NULL checks 2026-02-27 19:26:42 +01:00
Michael Vetter
55be1de29c cleanup: use g_new0 and g_strdup for alias allocation
And move whole preferences module to gchar.
2026-02-27 19:26:42 +01:00
Michael Vetter
aec8e48268 refactor: modernize cmd_ac_complete_filepath and simplify path handling 2026-02-27 19:26:42 +01:00
Michael Vetter
65ca3c448b fix: handle potential NULL from malloc in OMEMO fingerprint decoding 2026-02-27 19:26:42 +01:00
Michael Vetter
4ee35028e5 cleanup: use g_malloc and auto_gchar in _win_print_wrapped 2026-02-27 19:26:42 +01:00
Michael Vetter
ba5614fb0e cleanup: use auto_FILE and handle fopen failure in command_docgen 2026-02-27 19:26:42 +01:00
Michael Vetter
156b78adfa cleanup: fix potential NULL dereference and leaks in cmd_sendfile
* Add NULL checks for malloc and strdup calls in cmd_sendfile
* Ensure file handles and descriptors are closed on all error paths
2026-02-27 00:02:59 +01:00
Michael Vetter
5711671c99 refactor: roster export uses GString and g_file_set_contents now
Modernize the /export command implementation by adopting GLib-based
patterns for string manipulation and file handling.

* Replace _writecsv with _append_csv_escaped to efficiently handle CSV
  quote doubling directly within a GString buffer
* Refactor cmd_export to build the CSV content in memory using GString,
  minimizing system calls
* Use g_file_set_contents for atomic writing
2026-02-27 00:02:59 +01:00
Michael Vetter
9f3d78a994 refactor: Make _writecsv safer and with better performance
* Cache strlen() result
* Check for malloc() failure to prevent null pointer dereference
* Ensure the output buffer is null-terminated for safe use with cons_show()
* Simplify the quote escaping logic for better readability.
2026-02-27 00:02:52 +01:00
Michael Vetter
3d2a82ad4a cleanup: Fix potential NULL dereference in cmd_omemo_(un)trust
Replace malloc() with g_malloc() to handle allocation failure and
ensure enough memory is allocated for the null terminator.
2026-02-26 19:47:13 +01:00
Michael Vetter
d29fcdbed3 refactor: Use p_contact_new instead of malloc in p_contact_new() 2026-02-26 19:29:43 +01:00
Michael Vetter
48205fc6de cleanup: Initialize waittime to 0
This fixes a -fanalyzer uninitialized value warning.
2026-02-26 19:21:45 +01:00
Michael Vetter
caf84ca731 refactor: use g_new in message_pubsub_event_handler_add() 2026-02-26 19:06:44 +01:00
Michael Vetter
f98e33be81 refactor: make Jid use glib functions 2026-02-26 19:06:44 +01:00
Michael Vetter
abffffb499 refactor: make Resource use glib functions 2026-02-26 19:06:44 +01:00
Michael Vetter
8a36ca6d97 refactor: start to standardize on gchar
Make get_random_string() return a gchar*.

This is part of the effort to migrate string declarations and
allocations from char to gchar, replacing standard C allocators
(malloc/calloc/strdup) with their glib equivalents
(g_malloc/g_new0/g_strdup).

The primary goal is to prevent mismatched allocator bugs. While char and
gchar are binary compatible, mixing their respective deallocators is
dangerous:

* Freeing malloc'd memory with g_free() (or vice versa) can lead to
  memory corruption, double-frees, or crashes.
* This is seems to be the case especiall on non Linux platforms or when
  using hardened memory allocators where the GLib slice allocator or
  wrappers may differ from the system heap.

By standardizing on gchar, we ensure that our automatic cleanup macros
(like auto_gchar) always invoke the correct deallocator (g_free).
2026-02-26 19:06:44 +01:00
Michael Vetter
e1fd2a1cf6 refactor: replace calloc with g_new0 for struct allocations
Migrate structure allocations from calloc to glibs g_new0 macro to
improve type safety and memory robustness.

* Type Safety: The macro takes the type name directly, ensuring the
  allocated size always matches the pointer type.
* Static Analysis: It guarantees a non-NULL return by aborting on
  failure, which silences -fanalyzer warnings regarding potential NULL
  pointer dereferences.
* Readability: Removes redundant sizeof() calls and is the glib way
2026-02-26 19:06:39 +01:00
Michael Vetter
f1d4ed41d6 refactor: Use g_malloc0 instead of calloc in get_random_string()
It automatically aborts the program on allocation failure.
Also eliminates the need for repetitive manual NULL checks.
2026-02-26 16:29:50 +01:00
Michael Vetter
ec45a19c72 build: enable -fanalyzer for static analysis
Enable gccs static analyzer to detect memory bugs at compile-time. This
validates our __attribute__((__cleanup__)) macros and improves security
when parsing external XMPP data with zero runtime overhead.
2026-02-26 16:28:00 +01:00
Michael Vetter
606a10208a build: Enable _FORTIFY_SOURCE and -Og optimization
Add security hardening via _FORTIFY_SOURCE=2 and switch to -Og to
provide the necessary optimization for these checks while remaining
debugger friendly.

The goal is to catch many buffer overflows and format string errors
at compile-time or runtime.

-Og may occasionally optimize out very short-lived
local variables, making them invisible in the debugger.
So i'm not sure this is a very good idea. But I hope that we will find
bugs earlier and don't even need to debug that often. We might revert
this change later though in case we run into problems too often.

I'm not aware of any other downsides.
2026-02-26 08:50:12 +01:00
Michael Vetter
e95e8b5c6f build: Add -fstack-protector-strong compiler flag
Enable stack protection to detect buffer overflows and harden the binary.
This flag adds a canary to stack frames, causing the
program to terminate immediately if stack corruption is detected.

This helps identify memory safety bugs earlier during development by
turning silent corruption into an immediate crash, and provides
protection against stack-smashing exploits at runtime with negligible
performance overhead.

We don't add this generally because we wan't distributions/users to
decide if they want it. But it can help us find problems early on during
development.
2026-02-26 08:42:24 +01:00
Michael Vetter
8fd3c05983 Merge pull request #2093 from profanity-im/improve-unittests
Improve unittests
2026-02-25 12:50:50 +01:00
Michael Vetter
549f28fa0d tests: Add test for release_is_new()
Also refactor the function so we pass it the current version
and don't rely on the define inside of it.

This function only allows version format in the style of "x.y.z".
2026-02-25 12:40:32 +01:00
Michael Vetter
ea7a59808a refactor: optimize prof_occurrences()
Optimize prof_occurrences by using pointer arithmetic and
g_utf8_next_char instead of calls to g_utf8_offset_to_pointer.

Improve list construction efficiency in prof_occurrences by
using g_slist_prepend and g_slist_reverse instead of appends.

Also fix a slight oversight that could lead to trouble:
When empty search strings (e.g. empty nicks) are passed
it caused matches at every position.

Update the test case to test for empty nicks.
2026-02-25 12:40:32 +01:00
Michael Vetter
ca8cc8f651 tests: Add test for get_mentions() 2026-02-25 12:40:32 +01:00
Michael Vetter
0c15cd1d93 tests: Add test for valid_tls_policy_option()
See last commit with comment regarding cons_show().

In this test here we can see that if we pass NULL cons_show() isn't
called. Making it a bit different that other invalid test cases.
2026-02-25 12:40:32 +01:00
Michael Vetter
b7d53adba6 tests: Add tests for string_matches_one_of()
Writing this test I'm unsure whether a function like
string_matches_one_of() should actually call cons_show().
Let's just note that for later.
2026-02-25 12:40:28 +01:00
Michael Vetter
2b650c8daf build: Set -Qunused-arguments depending on compiler not os
This got introduced in configure.ac in
4ca6296fb7. I'll leave it there as is for
now but let's use the proper way in meson.

See discussion in https://github.com/profanity-im/profanity/pull/2090
2026-02-24 19:07:52 +01:00
Michael Vetter
e6bad112aa Merge pull request #2090 from botantony/fix-function-declaration
fix: define `prefs_changes_print` outside of `prefs_changes`
2026-02-24 19:01:25 +01:00
botantony
309c0a64a7 fix: define prefs_changes_print outside of prefs_changes
This causes build failures on macOS runners in Homebrew, see:
https://github.com/Homebrew/homebrew-core/pull/268970
https://github.com/Homebrew/homebrew-core/actions/runs/22305120219/job/64538952484?pr=268970

I didn't run `make format` because it modified other unrelated files
(guess they were not formatted before) but I assume this PR follow the
style guides

Signed-off-by: botantony <antonsm21@gmail.com>
2026-02-24 14:54:31 +01:00
Michael Vetter
a7be9f1280 tests: Add tests for strtoi_range() 2026-02-23 20:05:32 +01:00
Michael Vetter
5a7bba8093 tests: Add test for string_to_verbosity() 2026-02-23 18:37:14 +01:00
Michael Vetter
74bf33e727 tests: Add test for get_expanded_path() 2026-02-23 18:37:10 +01:00
Michael Vetter
1be326911c tests: Improve autocomplete unit tests
It seems like we didn't even check whether the operations succeed or
not.

* `clear_empty()`
  Add assertion to confirm a newly created autocomplete object is empty.
* `find_after_create()`:
  Add assertion to ensure `autocomplete_complete()` returns NULL when no
  entries are present in th object.
2026-02-23 18:28:49 +01:00
Michael Vetter
657de5f22f Merge pull request #2089 from profanity-im/cleanup
Cleanup
2026-02-23 11:10:38 +01:00
Michael Vetter
2b6bb1ae3e docs: Mention generated html pages in release guide 2026-02-23 10:50:24 +01:00
Michael Vetter
64a72249cb Start new cycle 2026-02-23 10:44:08 +01:00
Michael Vetter
5ff84d98d8 Release 0.16.0 2026-02-23 10:30:36 +01:00
Michael Vetter
a1fe714487 build: enable more warnings in debug mode 2026-02-20 16:50:03 +01:00
Michael Vetter
6221d1046a cleanup: Cast device ids in omemo.c
Seems like in signal address.device_id is int32 for some reason.
2026-02-20 16:50:03 +01:00
Michael Vetter
10a68437ad cleanup: Move loop var from int to size_t in omemo.c 2026-02-20 16:50:03 +01:00
Michael Vetter
7eb64b2779 cleanup: Cast g_hash_table_lookup return to function pointer
Explicitly cast the void pointer returned by g_hash_table_lookup
to the expected function pointer signature.

Make `-Wpedantic` happy.
2026-02-20 16:49:58 +01:00
Michael Vetter
110b359ee1 cleanup: Cast to get rid of warnings 2026-02-20 16:45:01 +01:00
Michael Vetter
1d0e6580d1 cleanup: Move loop var from int to size_t in cmd_funcs.c 2026-02-20 16:45:01 +01:00
Michael Vetter
ddd43f5e60 cleanup: Move more functions from int to guint in statusbar.c 2026-02-20 16:45:01 +01:00
Michael Vetter
8970b5e61b cleanup: Move some variables from int to guint in statusbar.c 2026-02-20 16:45:01 +01:00
Michael Vetter
1120170ef7 cleanup: Adapt type and cast to get ride of warnings 2026-02-20 16:44:54 +01:00
Michael Vetter
289fed262f cleanup: cast option to curl_easy_setopt
Also found via LTO mismatch.
2026-02-20 16:28:56 +01:00
Michael Vetter
759e5830a6 fix: Correct function signature for unittests
Found as LTO type-mismatch.
2026-02-20 16:25:47 +01:00
Michael Vetter
f1acf702a9 chore: Update copyright year 2026-02-20 16:13:31 +01:00
Michael Vetter
6e61383e97 cleanup: Adapt loop counter to proper type
Fixing a couple of -Wsign-compare warnings.
2026-02-20 16:02:27 +01:00
Michael Vetter
3e0c9e79e4 cleanup: Correct comparison in cons_show_wins()
g_strstr_len() actually returns a gchar* to the position. So it is not
like in the case of strlen() where it returns the number of bytes.
2026-02-20 16:02:17 +01:00
Michael Vetter
61fa8d5b66 cleanup: Initialize optional fields in profModule
Let's initialize them with NULL to make the compiler happy.
2026-02-20 16:02:13 +01:00
Michael Vetter
70ed25c418 cleanup: Initialize GOptionEntry entries correctly
The `arg_description` field was forgotten in 034a98587.
2026-02-20 16:02:08 +01:00
Michael Vetter
15fcda7950 cleanup: Fix uninitialized field in color_distance()
`name` of the struct `color_def` was not initialized but is also not
needed in this function.
2026-02-20 16:02:03 +01:00
Michael Vetter
12a181b5d5 cleanup: Wrap release handler in iq.c
GDestroyNotify takes a void* and returns void.

Wrap xmpp_stanza_release to get rid of the incompatible function type
warning.
2026-02-20 16:01:58 +01:00
Michael Vetter
b6a70aa47e cleanup: Fix cast function type warnings in pgp.c
The gpgme_set_passphrase_cb function expects a function
pointer of type gpgme_passphrase_cb_t.

The gpgme_passphrase_cb_t typedef specifies a function that returns
gpgme_error_t so return GPG_ERR_NO_ERROR instead of 0.
2026-02-20 16:01:52 +01:00
Michael Vetter
b8eddb4e3c cleanup: Make muc_nick() return const char*
Fix `type qualifiers ignored on function return type`.
2026-02-20 16:01:46 +01:00
Steffen Jaeckel
4b610579ae Fix Git version strings for out-of-tree builds.
Signed-off-by: Steffen Jaeckel <s@jaeckel.eu>
2026-02-09 10:24:56 +01:00
Michael Vetter
0ad4e38aa1 readme: remove sponsors section
Noone wants to sponsor us anyways :)
2026-02-05 16:48:50 +01:00
Michael Vetter
13a0214ed6 Merge pull request #2086 from profanity-im/meson-build
Fix https://github.com/profanity-im/profanity/issues/1002.
Was already started in 2021 in https://github.com/profanity-im/profanity/pull/1619 but then abandoned it.

I want to take this opportunity to change a couple of other things regarding building:
* The `--plugins` switch is gone. Use `--python-plugins` and `--c-plugins` instead.
* We don't autoenable depending on dependencies being present. Every feature needs to be enabled explicitly. This helps with having deterministic builds.
* Instead of setting `PACKAGE_STATUS="development/release"` in the configure.ac file we will use the default meson `buildtype`.

Autotools will probably stay a while. The next release will contain both with a note for packagers to try to use meson and give us feedback.

Meson/Ninja is significantly faster and the syntax is much easier to read.
The faster build times eventually will also help when waiting for CI results.

There are some things missing still like our `make dist`, `make doublecheck` and such.

Usage example:
```
meson setup build --buildtype=release -Domemo=enabled -Dpgp=enabled
meson compile -C build
```

The changes described above are only on meson. Autotools still behaves the same. The plan is to keep it as is and remove it once we know everything works in meson.
2026-02-05 16:08:15 +01:00
Michael Vetter
4223105746 ci: add ci job for building with meson
This probably could be nicer. But since we will drop one in the future
it's not really a priority to make this pretty.
2026-02-05 15:36:54 +01:00
Michael Vetter
57d18d44c8 build: only check for gpgme via pkg-config
Most likely we don't need to check for gpgme-config on most modern
systems.
2026-02-05 15:33:44 +01:00
Michael Vetter
f6eced0ad0 build: simplify build configuration and remove redundancy
Use disabler() for optional dependencies.
Extract repeated build type checks into is_debug/is_release variables.
Consolidate compiler flags into single add_project_arguments() calls.
Simplify dependency list building (Meson auto-ignores disabled deps).
Streamline platform checks using 'in' operator.
Remove redundant variables (xscrnsaver_found, gtk_version, config_h_inc).
Simplify ncurses and header detection with clearer fallback chains.
Consolidate install_data() calls for files in same directory.
2026-02-05 15:09:41 +01:00
Michael Vetter
01c9a51c70 build: only support gtk3 for icon and clipboard
gtk2 is pretty old now. Let's just focus on gtk3.
2026-02-05 14:36:28 +01:00
Michael Vetter
75579c6bf6 build: simplify adding dependencies
We don't actually need to check whether they were found.
Meson will just ignore the empty ones.
2026-02-05 14:31:28 +01:00
Michael Vetter
d91ff7eb20 build: features need to be enabled
For meson we dont just check for the presence of a dependency and
then auto enable it.
Users must enable features explicit. This helps with having
deterministic results.

Also remove the general `plugins` switch which was used to
enable/disable both python and c plugins. Users can just use
those switches.
2026-02-05 14:19:10 +01:00
Michael Vetter
886080dc9b build: simplify readline detection 2026-02-05 13:35:55 +01:00
Michael Vetter
af0dfcce70 build: fixup release/debug mistake 2026-02-05 13:32:27 +01:00
Michael Vetter
d8211584f6 build: only check for ncurses{w}
In out autotools build we check for all kinds of curses and their
support for wide character.
Let's focus on more modern systems until someone complains.
2026-02-05 13:28:59 +01:00
Michael Vetter
275afd40d5 build: remove specific libstrophe build check
For autotools this was added in:
75bb00368f

Regarding:
https://github.com/profanity-im/profanity/issues/1334

I think libstrophe is rather common now and should be treated
like all the other dependencies anyways.
So I'll remove this from the meson files at least. Since my
goal is to have a cleaner build system.
2026-02-05 13:10:52 +01:00
Michael Vetter
e29c418452 build: simplify platform detection 2026-02-05 13:03:26 +01:00
Michael Vetter
32ca4a2e30 build: add -Wno-unused-parameter 2026-02-05 13:02:17 +01:00
Michael Vetter
49ef85a58d build: introduce variable to check for xscreensaver
`.found()` does not work on lists. And since this feature needs
several deps it is a list.
2026-02-05 12:17:04 +01:00
Michael Vetter
87c4b7ec53 build: readd compiler flags that got lost in conversion 2026-02-05 12:16:42 +01:00
Michael Vetter
3f76676e9a build: use default meson buildtype
In autotools we had `package_status` which we set to either
`release` or `development`.
When converting from autotools to meson we used that mechanism
as well. But actually meson has a default way of handling this
with the option `buildtype`.

The following values are possible:
debug, debugoptimized, release, plain

So our `package_status = development` will now be
`if get_option('buildtype') == 'debug' or get_option('buildtype') ==
'debugoptimized'`. Probably we could also use
`if get_option('buildtype') == 'release'. But like this the
`plain` will be really plain.

Usage:
```
meson setup build --buildtype=debug
meson compile -C build
```
2026-02-05 12:16:34 +01:00
Michael Vetter
fbfa1f13e5 Add meson build system 2026-02-05 09:13:40 +01:00
Michael Vetter
f5ecceac91 Merge pull request #2066 from ritesh006/ci/cygwin-build
ci: add Cygwin build workflow
2026-01-24 20:09:18 +01:00
Ritesh Kudkelwar
f21ce24cd7 ci: add Cygwin build workflow
Add a GitHub Actions workflow to build with Cygwin on Windows.
Fixes issues by forcing Cygwin bash for all steps, handling temporary script paths,
stripping CRLF line endings, and enabling igncr.
2026-01-24 19:53:18 +05:30
Michael Vetter
fce4da4883 Merge pull request #2082 from balejk/sigterm
exit gracefully on SIGTERM and SIGHUP
2026-01-22 14:00:44 +01:00
Karel Balej
7c83c2608d exit gracefully on SIGTERM and SIGHUP
Make profanity exit gracefully when it receives either SIGTERM or
SIGHUP. This means that various cleanups will happen as if the user
issued the /quit command, whereas before these signals would terminate
profanity immediately leaving for instance the connection to the server
unterminated.

The main motivation for this change is so that profanity will exit
gracefully when the user closes the terminal that it's running in, which
is a handy shortcut (but can also happen on accident) and won't now lead
to the connection slowly timing out on the server instead. Similarly for
SIGTERM: profanity now for instance needs not be manually closed before
shutting down the computer to avoid the above as it will instead receive
this signal from the init process.
2026-01-22 13:37:56 +01:00
Michael Vetter
e8e9c29e4f Merge pull request #2075 from mcalierno/fix/apple-silicon-homebrew
Update compiler flags for homebrew packages on Apple silicon
2025-12-03 12:06:06 +01:00
Steffen Jaeckel
b17716570b Merge pull request #2080 from profanity-im/some-fixes
Some fixes
2025-11-26 22:07:03 +01:00
Steffen Jaeckel
01c781fcf6 Don't publish keys if the server doesn't support pubsub.
Related-to: https://github.com/profanity-im/profanity/issues/2078
Signed-off-by: Steffen Jaeckel <s@jaeckel.eu>
2025-11-23 12:47:39 +01:00
Steffen Jaeckel
1d508d592c Fix memory leaks when OMEMO keys have not been generated yet.
Signed-off-by: Steffen Jaeckel <s@jaeckel.eu>
2025-11-23 12:47:39 +01:00
Steffen Jaeckel
fa1c4dddc9 Fix invalid free after /omemo gen
Signed-off-by: Steffen Jaeckel <s@jaeckel.eu>
2025-11-23 12:47:39 +01:00
Steffen Jaeckel
0830d82916 Minor fixes of accounts.
Signed-off-by: Steffen Jaeckel <s@jaeckel.eu>
2025-11-23 12:47:39 +01:00
Steffen Jaeckel
f19acc2156 Fix reconnect when no account has been set up yet.
e.g. if one connects with an account for the first time and the server
returns a `see-other-host` error.

Signed-off-by: Steffen Jaeckel <s@jaeckel.eu>
2025-11-23 12:47:39 +01:00
Steffen Jaeckel
c61c72f2f0 When renaming an account, copy all existing keys.
No need to have a fixed list of keys, we can simply copy all existing ones.

Signed-off-by: Steffen Jaeckel <s@jaeckel.eu>
2025-11-23 12:47:39 +01:00
Steffen Jaeckel
bb964b2f1b Consistency please.
Signed-off-by: Steffen Jaeckel <s@jaeckel.eu>
2025-11-23 12:47:39 +01:00
Steffen Jaeckel
7c04b17aff Simplify accounts_get_account().
* No need to call `g_key_file_has_key()` before calling a getter.
* Add helper to convert `gcharv` to `glist`.

Signed-off-by: Steffen Jaeckel <s@jaeckel.eu>
2025-11-23 12:47:39 +01:00
Steffen Jaeckel
81f92f1fed Fix overwriting new accounts when running multiple instances.
If one is running multiple instances of profanity, the behavior of the
accounts module was to constantly overwrite the accounts file with the
version that was on-disk of the first instance of profanity started.

This is changed now in order to only write what we modified, we keep a
copy of the accounts file and when "saving" we re-read accounts from disk
and only update the values of the modified account.

This is not 100% fool proof if one modifies the same account from two
different instances, but still better than before.

Signed-off-by: Steffen Jaeckel <s@jaeckel.eu>
2025-11-23 12:47:39 +01:00
Steffen Jaeckel
038f345a4e Minor improve theme
* Less `GString`.
* Don't `g_free()` a `strdup()`'ed string.
* Don't lookup the `console` window X times, but only once.

Signed-off-by: Steffen Jaeckel <s@jaeckel.eu>
2025-11-23 12:47:39 +01:00
Steffen Jaeckel
54fea4afcf Minor improvements.
* Add new TLS policy `direct` as a replacement for `legacy`.
* Document that `/[command]?` prints the help of a command.
* Add option to get help via `/command help`.
* Fix `my-prof.supp` generation and tests for out-of-source builds.

Signed-off-by: Steffen Jaeckel <s@jaeckel.eu>
2025-11-23 12:47:39 +01:00
Steffen Jaeckel
a50cd3a885 Refactor tlscerts.
* use `gchar` instead of `char`.
* improve situations when strings must be duplicated or can pass ownership.
* encapsulate the X.509 name details into a struct.
* prevent memory leaks if a name detail is contained multiple times.

Signed-off-by: Steffen Jaeckel <s@jaeckel.eu>
2025-11-23 12:47:39 +01:00
Steffen Jaeckel
62fd40c510 Use gboolean consistently.
Signed-off-by: Steffen Jaeckel <s@jaeckel.eu>
2025-11-23 12:47:39 +01:00
Steffen Jaeckel
b0221f6146 Simplify conf_string_list_{add,remove}() implementations.
* add can simply do a `memcpy()`.
* in remove we don't have to put the array in a list in order to put it
  back into an array again. Also we don't have to `strdup()` each entry,
  which leads to even less allocations.

Signed-off-by: Steffen Jaeckel <s@jaeckel.eu>
2025-11-23 12:47:39 +01:00
Steffen Jaeckel
1ede01ed8c Free in reverse order.
... as much as possible ... subject and issuer details excluded.

Signed-off-by: Steffen Jaeckel <s@jaeckel.eu>
2025-11-23 12:47:39 +01:00
Steffen Jaeckel
a07cff8a99 Use the stronger certificate fingerprint.
If a cert has a SHA256 use that one and only use SHA1 as fallback.

Closes: https://github.com/profanity-im/profanity/issues/2068
Signed-off-by: Steffen Jaeckel <s@jaeckel.eu>
2025-11-23 12:47:06 +01:00
Steffen Jaeckel
0acea8d0d2 Retrieve and save new fingerprints from libstrophe.
This also reads the certificate SHA256 and pubkey fingerprint from
libstrophe, but doesn't store it persistently yet.

Related-to: https://github.com/profanity-im/profanity/issues/2068
Signed-off-by: Steffen Jaeckel <s@jaeckel.eu>
2025-11-23 12:46:51 +01:00
Steffen Jaeckel
4dcaa839fa Prepare to use SHA256 fingerprints of certs.
First let's make clear we're currently using SHA1 & untangle the tlscerts
API from fingerprint specific details.

Related-to: https://github.com/profanity-im/profanity/issues/2068
Signed-off-by: Steffen Jaeckel <s@jaeckel.eu>
2025-11-23 12:46:29 +01:00
Steffen Jaeckel
7f5ae430af Fix more memleaks
Signed-off-by: Steffen Jaeckel <s@jaeckel.eu>
2025-11-18 12:16:29 +01:00
Steffen Jaeckel
24c3b5d531 Add new command /changes
With that command one can see the modifications of the runtime
configuration vs. the saved configuration.

Signed-off-by: Steffen Jaeckel <s@jaeckel.eu>
2025-11-18 12:16:29 +01:00
Steffen Jaeckel
79ff9bad7d Fix OMEMO startup
When setting up OMEMO for the first time via `/omemo gen` one had
to reconnect in order to make OMEMO work. This is fixed now.

Fixes: 5b6b5130 ("Fix OMEMO keyfile loading")
Signed-off-by: Steffen Jaeckel <s@jaeckel.eu>
2025-11-18 12:16:29 +01:00
Steffen Jaeckel
1aeb19acb0 Print PID in debug logs.
So one can easily see if there are two instances running, if they are
logging to the same file.

Signed-off-by: Steffen Jaeckel <s@jaeckel.eu>
2025-11-18 12:16:29 +01:00
Steffen Jaeckel
2fb56b8536 Fix some more untyped APIs
Signed-off-by: Steffen Jaeckel <s@jaeckel.eu>
2025-11-18 12:16:29 +01:00
Steffen Jaeckel
48ac88de08 Fix GError handling
Several users have reported segfaults when starting up profanity which
has OMEMO support, but OMEMO is not set up yet.

@StefanKropp has been able to reproduce this and tracked it down to
`_load_identity()` calling `omemo_known_devices_keyfile_save()`.
The latter then calls `save_keyfile()` which calls
`g_key_file_save_to_file()`. This can then fail if one of the first two
strings is NULL and won't set the `error` on return. In its error handling
`save_keyfile()` unconditionally dereferences `error` which leads to the
segfault.

Fix this and also go through the entire codebase and verify that the usage
of `GError` is done correctly.

Signed-off-by: Steffen Jaeckel <s@jaeckel.eu>
2025-11-18 12:16:29 +01:00
Steffen Jaeckel
5da079bbb2 Merge pull request #2072 from aryansri05/fix-otr-whitespace-detection
Fix OTR whitespace tag detection to prevent false positives
2025-11-03 22:15:58 +01:00
aryansri05
cff26a97af Fix OTR whitespace tag detection to prevent false positives
The previous implementation used strstr() which would match OTR tag patterns anywhere in the message. This caused normal messages containing these patterns to incorrectly trigger OTR session initialization.

Changed to use strncmp() to verify that V1/V2 tags immediately follow the base tag, ensuring only legitimate OTR whitespace tags trigger session establishment.

Fixes #1957
2025-11-03 22:02:14 +01:00
Michael
5ac6ab71a2 Use PKG_CHECK_MODULES to check for libgcrypt
On macOS running ./configure --enable-omemo was failing to find gcrypt despite being installed.
2025-10-26 11:49:32 +00:00
Michael
34e8b1b2f0 Update compiler flags for homebrew packages on Apple silicon
Add /opt/homebrew/{include,lib} flags for apple silicon macs, which are not included as default search paths by Clang/GCC. (Homebrew uses /usr/local/{include,lib} on intel macs)
2025-10-25 13:42:27 +01:00
Michael Vetter
58d130a0a9 ci: disable arch
```
 #11 0.137 --2025-09-18 10:41:06--  https://aur.archlinux.org/cgit/aur.git/snapshot/libstrophe-git.tar.gz
```

This currently blocks us at https://github.com/profanity-im/profanity/pull/2066.
2025-09-18 12:48:28 +02:00
Michael Vetter
6ce69428cb Merge pull request #2063 from jubalh/terminology-messages
docs: explain the different kinds of messages
2025-09-13 14:04:14 +02:00
Michael Vetter
2ac4e170a1 Merge pull request #2067 from mcalierno/bugfix/fix-mac-build
Add types to function prototypes
2025-09-13 13:50:38 +02:00
Michael
a704887f14 Add types to function prototypes
Build fail under macOS, Clang 16.0.0.
Add parameters to the following prototypes
- bookmark_ignore_on_connect()
- cons_show_qrcode()
2025-09-13 12:36:05 +01:00
Michael Vetter
58312ed0d8 Merge pull request #2062 from ritesh006/fix/cygwin-pthread
build: replace ACX_PTHREAD with AX_PTHREAD
2025-09-12 23:27:51 +02:00
ritesh006
26513840a8 build: replace ACX_PTHREAD with AX_PTHREAD
On Cygwin, ./configure failed with:

    ./configure: line XXXX: syntax error near unexpected token ACX_PTHREAD([], [AC_MSG_ERROR([pthread is required])])'

Root cause: ACX_PTHREAD is a legacy macro not provided by modern
Autoconf; the maintained macro is AX_PTHREAD (from autoconf-archive).

Changes:
- Replace ACX_PTHREAD with AX_PTHREAD in configure.ac.
- Rely on system autoconf-archive; do not vendor m4/ax_pthread.m4.
- Keep AC_CONFIG_MACRO_DIR([m4]) (harmless even if empty).

Result:
- autoreconf -fi && ./configure && make succeeds on Cygwin x86_64.
- Linux builds continue to work.

Fixes: #2059
2025-09-12 20:30:45 +05:30
Michael Vetter
000ef280dd docs: explain the different kinds of messages 2025-09-12 09:30:41 +02:00
Michael Vetter
44f9037e16 docs: fix typo 2025-09-05 11:47:59 +02:00
Michael Vetter
15a277eb9e Start new cycle 2025-08-22 09:31:58 +02:00
Michael Vetter
c01b531fe2 Release 0.15.1 2025-08-22 09:23:01 +02:00
Michael Vetter
66c7a6b7e4 Merge pull request #2060 from profanity-im/fix-1911
If config keyfile does not exist, create it.
2025-08-21 15:23:23 +02:00
Steffen Jaeckel
b12521ca21 If config keyfile does not exist, create it.
Fixes #1911
Alternative to #2056

Signed-off-by: Steffen Jaeckel <s@jaeckel.eu>
2025-08-21 11:05:54 +02:00
Michael Vetter
26dbdb31c6 Add feature request template 2025-08-21 09:26:17 +02:00
Michael Vetter
41c47f432d Delete old file 2025-08-21 09:24:45 +02:00
Michael Vetter
5dcbd84f75 Update issue templates
According to the new way: https://docs.github.com/en/communities/using-templates-to-encourage-useful-issues-and-pull-requests/about-issue-and-pull-request-templates
2025-08-21 09:23:45 +02:00
Michael Vetter
f1e12a33cf Rename issue template
Seems like GH changed how the templates work.
2025-08-21 09:19:31 +02:00
Michael Vetter
bbc62fb2d9 Merge pull request #2053 from profanity-im/some-improvements
Some improvements
2025-08-20 18:21:46 +02:00
Steffen Jaeckel
7f48452d84 Don't use memchr() on strings potentially shorter than 4 bytes.
When running profanity under Valgrind with slashguard enabled, the
following error could occur:

```
[...]
==4021347== Invalid read of size 1
==4021347==    at 0x4851F49: memchr (vg_replace_strmem.c:986)
==4021347==    by 0x45CEAD: _inp_slashguard_check (inputwin.c:183)
==4021347==    by 0x45CEAD: inp_readline (inputwin.c:225)
==4021347==    by 0x431184: prof_run (profanity.c:121)
==4021347==    by 0x42C609: main (main.c:176)
==4021347==  Address 0xe850883 is 0 bytes after a block of size 3 alloc'd
==4021347==    at 0x48477C4: malloc (vg_replace_malloc.c:446)
[...]
```

`memchr()` requires the complete memory that shall be searched to be
accessible. Using `strchr()` could work for shorter strings, but we only
want to search in the first 4 chars.

Instead of somehow working around those limitations, simply search manually
in the first 4 bytes.

Fixes: 3c56b289 ("Add slashguard feature")
Signed-off-by: Steffen Jaeckel <s@jaeckel.eu>
2025-08-20 16:25:39 +02:00
Steffen Jaeckel
65f5883fee Introduce tests/prof_cmocka.h
As 9f2abc75 accidentally got the ordering of some of the includes wrong,
I decided to propose my initial solution again.

Additional to that, I've opened a MR against CMocka to solve this on
their side, since I believe that the current way this is done is not
sustainable [0].

[0] https://gitlab.com/cmocka/cmocka/-/merge_requests/91

Fixes: 9f2abc75 ("Fix tests with gcc15 (uintptr_t)")
Signed-off-by: Steffen Jaeckel <s@jaeckel.eu>
2025-08-06 15:52:33 +02:00
Steffen Jaeckel
9d335729a0 Tidy up some code
* less allocations
* less duplicate code

Signed-off-by: Steffen Jaeckel <s@jaeckel.eu>
2025-08-06 12:45:13 +02:00
Steffen Jaeckel
40aafd06e7 Refactor slashguard
Fixes #2054
Fixes: 95c2199c ("Some more memory improvements")

Signed-off-by: Steffen Jaeckel <s@jaeckel.eu>
2025-08-06 12:45:13 +02:00
Steffen Jaeckel
e0f107f75e Fix memory leak.
Signed-off-by: Steffen Jaeckel <s@jaeckel.eu>
2025-08-06 12:45:13 +02:00
Steffen Jaeckel
3299dd8fc6 Trampoline Python unref again.
In the past `Py_XDECREF()` was a macro. Preserve compat to ancient Python
versions by having a trampoline which calls `Py_XDECREF()`.

Fixes: #2043
Fixes: c0da36c4 ("Rage-cleanup.")

Signed-off-by: Steffen Jaeckel <s@jaeckel.eu>
2025-08-06 12:45:13 +02:00
Steffen Jaeckel
3370f8a7f1 Separate entries visually in my-prof.supp
Signed-off-by: Steffen Jaeckel <s@jaeckel.eu>
2025-08-06 12:45:13 +02:00
Michael Vetter
bda6dabbd9 Merge pull request #2055 from andreasstieger/gcc15
Fix tests with gcc15 (uintptr_t)
2025-08-06 11:45:41 +02:00
Michael Vetter
12bb3195ee Merge pull request #2058 from mdosch/fix-aasgard-typo
Fix typo in examples.
2025-08-04 08:45:07 +02:00
Martin Dosch
5696bf77c7 Fix typo in examples. 2025-08-03 23:17:42 +02:00
Andreas Stieger
9f2abc75ad Fix tests with gcc15 (uintptr_t)
fixes: error: ‘uintptr_t’ undeclared, defined in header ‘<stdint.h>
2025-07-28 18:38:26 +02:00
Michael Vetter
edda887ae6 ci: enable arch
This reverts commit 7164d71992.
See fix 606eaac31d.
2025-07-23 09:08:57 +02:00
Michael Vetter
e7d79d2614 Merge pull request #2050 from killerdevildog/fix-gpgme-2.0.0-compatibility
Fix GPGME >= 2.0.0 compatibility issue #2048
2025-07-23 09:00:17 +02:00
Quaylyn Rimer
606eaac31d Fix GPGME >= 2.0.0 compatibility issue #2048
Replace deprecated gpgme_key_get_string_attr with modern API

This commit fixes the build failure against GPGME >= 2.0.0 where
gpgme_key_get_string_attr and GPGME_ATTR_EMAIL were removed.

Changes made:
- Replaced direct call to gpgme_key_get_string_attr(key, GPGME_ATTR_EMAIL, NULL, 0)
- Added new helper function _gpgme_key_get_email() with backwards compatibility
- Function uses conditional compilation to support both old and new GPGME versions

Backwards Compatibility:
- GPGME < 2.0.0: Uses gpgme_key_get_string_attr() if GPGME_ATTR_EMAIL is available
- GPGME >= 2.0.0: Uses modern key->uids->email API

Forward Compatibility:
- Code compiles successfully with GPGME 2.0.0+ where deprecated functions are removed
- Uses modern GPGME API that iterates through key user IDs to find email addresses

Testing:
- Tested with GPGME 1.18.0 (backwards compatibility confirmed)
- Tested compilation compatibility for both old and new GPGME versions
- Verified the exact error from issue #2048 is resolved
- Confirmed no regression in functionality

Fixes: #2048
Resolves compilation error: 'gpgme_key_get_string_attr' undeclared
Resolves compilation error: 'GPGME_ATTR_EMAIL' undeclared
2025-07-22 20:27:43 -06:00
Michael Vetter
359a086287 Merge pull request #2041 from profanity-im/some-improvements
Some improvements
2025-06-24 09:25:52 +02:00
Michael Vetter
7164d71992 ci: disable arch temporarily
Will need to fix this soon. Lets temp remove arch to get results for the
other systems.

Build fail on Arch:
```
 src/pgp/gpg.c: In function ‘p_gpg_decrypt’:
src/pgp/gpg.c:659:36: error: implicit declaration of function ‘gpgme_key_get_string_attr’ [-Wimplicit-function-declaration]
  659 |                 const char* addr = gpgme_key_get_string_attr(key, GPGME_ATTR_EMAIL, NULL, 0);
      |                                    ^~~~~~~~~~~~~~~~~~~~~~~~~
src/pgp/gpg.c:659:67: error: ‘GPGME_ATTR_EMAIL’ undeclared (first use in this function)
  659 |                 const char* addr = gpgme_key_get_string_attr(key, GPGME_ATTR_EMAIL, NULL, 0);
      |                                                                   ^~~~~~~~~~~~~~~~
src/pgp/gpg.c:659:67: note: each undeclared identifier is reported only once for each function it appears in
make[1]: *** [Makefile:2085: src/pgp/gpg.o] Error 1
make[1]: *** Waiting for unfinished jobs....
make: *** [Makefile:1314: all] Error 2
```
2025-06-24 09:05:39 +02:00
Steffen Jaeckel
dd8da16c96 Use correct free function.
Fixes: 75d76638 ("Free wins summary list")

Signed-off-by: Steffen Jaeckel <s@jaeckel.eu>
2025-06-23 12:43:04 +02:00
Steffen Jaeckel
f7cddd11c4 Improve const correctness
Signed-off-by: Steffen Jaeckel <s@jaeckel.eu>
2025-04-28 16:42:27 +02:00
Steffen Jaeckel
4eaa291f70 Re-factor cmd_presence()
Similar to 9c2b3f6579, but no bugs were fixed. Maybe some were introduced
but only time will tell.

Signed-off-by: Steffen Jaeckel <s@jaeckel.eu>
2025-04-28 16:40:05 +02:00
Steffen Jaeckel
29129bf888 Return error on /time <invalid-section>
Fixes: 9c2b3f6579 ("Re-factor `cmd_time()`")

Signed-off-by: Steffen Jaeckel <s@jaeckel.eu>
2025-04-28 16:40:05 +02:00
Steffen Jaeckel
4194298dbd Less GString
Signed-off-by: Steffen Jaeckel <s@jaeckel.eu>
2025-04-28 16:40:05 +02:00
Steffen Jaeckel
5810f49c97 url save: fix location printed
Print the final storage location when successfully decrypting a file with
`aesgcm://` source.

Signed-off-by: Steffen Jaeckel <s@jaeckel.eu>
2025-04-23 11:19:04 +02:00
351 changed files with 31121 additions and 11307 deletions

View File

@@ -1,49 +0,0 @@
image: openbsd/7.0
packages:
- gcc-11.2.0p0
- cmake
- gmake
- cmocka
- libtool
- automake-1.16.3
- pkgconf
- readline
- python-3.8.12
- autoconf-2.69p3
- autoconf-archive
- curl
- gpgme
- glib2
- gtk+2
- libotr
- libassuan
- libgpg-error
- libgcrypt
- libsignal-protocol-c
- sqlite3
sources:
- https://github.com/strophe/libstrophe.git#0.11.0
- https://github.com/profanity-im/profanity
environment:
LANG: en_US.UTF-8
tasks:
- symlink: |
doas ln -sf /usr/local/bin/python3 /usr/local/bin/python
doas ln -sf /usr/local/bin/python3-config /usr/local/bin/python-config
doas ln -sf /usr/local/bin/pydoc3 /usr/local/bin/pydoc
- build: |
export AUTOCONF_VERSION=2.69
export AUTOMAKE_VERSION=1.16
cd ~/libstrophe
./bootstrap.sh
./configure
make
doas make install
cd ~/profanity
./ci-build.sh

23
.commitlintrc.json Normal file
View File

@@ -0,0 +1,23 @@
{
"extends": ["@commitlint/config-conventional"],
"rules": {
"type-enum": [
2,
"always",
[
"feat",
"fix",
"docs",
"style",
"refactor",
"perf",
"tests",
"build",
"ci",
"chore",
"cleanup"
]
],
"subject-case": [0]
}
}

11
.gitattributes vendored Normal file
View File

@@ -0,0 +1,11 @@
# Pattern Attribute
.clang-format export-ignore
.codespellrc export-ignore
.commitlintrc.json export-ignore
.git-blame-ignore-revs export-ignore
.github/ export-ignore
CONTRIBUTING.md export-ignore
RELEASE_GUIDE.md export-ignore
ci/ export-ignore
prof.supp export-ignore
scripts/ export-ignore

View File

@@ -1,44 +0,0 @@
---
name: Bug report
about: Create a report
title: ''
labels: bug
assignees: ''
---
<!--- Provide a general summary of the issue in the Title above -->
<!--- More than 50 issues open? Please don't file any new feature requests -->
<!--- Help us reduce the work first :-) -->
## Expected Behavior
<!--- If you're describing a bug, tell us what should happen -->
<!--- If you're suggesting a change/improvement, tell us how it should work -->
## Current Behavior
<!--- If describing a bug, tell us what happens instead of the expected behavior -->
<!--- If suggesting a change/improvement, explain the difference from current behavior -->
## Possible Solution
<!--- Not obligatory, but suggest a fix/reason for the bug, -->
<!--- or ideas how to implement the addition or change -->
## Steps to Reproduce (for bugs)
<!--- Describe, in detail, what needs to happen to reproduce this bug -->
<!--- Give us a screenshot (if it's helpful for this particular bug) -->
1.
2.
3.
4.
## Context
<!--- How has this issue affected you? What are you trying to accomplish? -->
## Environment
* Give us the version and build information output generated by `profanity -v`
* If you could not yet build profanity, mention the revision you try to build from
* Operating System/Distribution
* glib version
* libstrophe version
* Some bugs might be due to specific implementation in the server. `/serversoftware example.domain` can be helpful

View File

@@ -0,0 +1,20 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: feature
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

View File

@@ -1,3 +1,12 @@
---
name: Bug report
about: Create a report
title: ''
labels: bug
assignees: ''
---
<!--- Provide a general summary of the issue in the Title above -->
<!--- More than 50 issues open? Please don't file any new feature requests -->
@@ -31,4 +40,3 @@
* glib version
* libstrophe version
* Some bugs might be due to specific implementation in the server. `/serversoftware example.domain` can be helpful

View File

@@ -30,7 +30,7 @@ jobs:
steps:
- uses: actions/checkout@v4
- name: Build
run: docker build -f Dockerfile.${{ matrix.flavor }} -t profanity .
run: docker build -f ci/Dockerfile.${{ matrix.flavor }} -t profanity .
- name: Run tests
run: docker run profanity ./ci-build.sh
@@ -45,10 +45,10 @@ jobs:
# if this check fails, you have to update the number of auto types known and the list of auto types in the check below
- name: Check auto types are up-to-date
run: |
[[ "$(find src -type f -name '*.[ch]' -exec awk '/^#define auto_[\W]*/ {print $2}' '{}' \; | sort -u | wc -l)" == "8" ]] || exit -1
[[ "$(find src -type f -name '*.[ch]' -exec awk '/^#define auto_[\W]*/ {print $2}' '{}' \; | sort -u | wc -l)" == "9" ]] || exit -1
- name: Check auto types are initialized
run: |
grep -P 'auto_(char|gchar|gcharv|guchar|jid|sqlite|gfd|FILE)[\w *]*;$' -r src && exit -1 || true
grep -P 'auto_(char|gchar|gcharv|guchar|gerror|jid|sqlite|gfd|FILE)[\w *]*;$' -r src && exit -1 || true
- name: Check CWE-134 format string vulnerabilities
run: ./check-cwe134.sh
@@ -108,6 +108,6 @@ jobs:
steps:
- uses: actions/checkout@v4
- name: Build
run: docker build -f Dockerfile.arch -t profanity-cov .
run: docker build -f ci/Dockerfile.arch -t profanity-cov .
- name: Run coverage
run: docker run profanity-cov ./ci-build.sh --coverage-only

23
.gitignore vendored
View File

@@ -17,6 +17,8 @@ compile_commands.json
.project
.settings/
.vscode/
.kilo/
.roo/
*.plist/
# autotools
@@ -114,3 +116,24 @@ coverage/
*.gcda
*.gcov
coverage.info
# Bench harness build artefacts (sources are committed; binaries/CSVs aren't)
tests/bench/gen_history
tests/bench/bench_runner
tests/bench/bench_long_messages
tests/bench/bench_failure_modes
tests/bench/bench_export_import
tests/bench/*.o
tests/bench/.deps/
tests/bench/current.csv
tests/bench/full.csv
tests/bench/quick.csv
tests/bench/longmsg.csv
tests/bench/fail.csv
tests/bench/multi.csv
tests/bench/lmc.csv
tests/bench/ooo.csv
tests/bench/pipe.csv
tests/bench/pipeline.csv
tests/bench/maxpipeline.csv
tests/bench/imp.csv

View File

@@ -1,22 +0,0 @@
brew 'autoconf'
brew 'autoconf-archive'
brew 'automake'
brew 'check'
brew 'cmocka'
brew 'curl'
brew 'expat'
brew 'glib'
brew 'gnutls'
brew 'gpgme'
brew 'gtk+'
brew 'libffi'
brew 'libotr'
brew 'libsignal-protocol-c'
brew 'libstrophe'
brew 'libtool'
brew 'ncurses'
brew 'openssl'
brew 'ossp-uuid'
brew 'pkg-config'
brew 'readline'
brew 'sqlite'

278
CHANGELOG
View File

@@ -1,3 +1,281 @@
cproof fork (unreleased)
========================
Changes:
- Add flat-file database backend as alternative to SQLite for message history.
Stores messages as human-readable plain text files. Configure with `/privacy logging flatfile`.
Files are stored in ~/.local/share/profanity/flatlog/.
- Add `/history verify [<jid>]` command to check integrity of stored message
history (works with both SQLite and flat-file backends).
- Add vtable-based database backend abstraction allowing pluggable storage.
- Add `make check-functional-flatfile` target to run functional tests with
flat-file backend.
- Add AI client with multi-provider chat support (OpenAI, local, etc.).
- Consolidate logging controls; show database backend in the statusbar.
0.17.0 (2026-03-26)
===================
Special thanks to our sponsor Matthew Fennell!
3 people contributed to this release: @botantony, @sjaeckel and @jubalh.
The last release (0.16.0) was the first that could be compiled with the Meson build system.
It provided a tarball for autotools and a separate one for meson. The autotools tarball was done
so that distributions don't need dependencies on automake, autoconf and libtool.
Several distributions are dropping vendor tarballs and use git checkouts or autogenerated tarballs nowadays.
Meaning they need to run the above mentioned tools in the case of the autotools build anyways.
So this release will only be shipped with tarballs generated with meson. Profanity can still be compiled with both autotools or meson for easier adjustments. Autotols users just need to generate configure first. The next release will only use Meson. So we encourage everybody to build with Meson arleady and report any bugs they encounter.
We finally re-enabled functional tests (along with our existing unit tests) again!
It would be appreciated if we could find people who want to help us expand the unit and functional tests so we are less likely to introduce regressions when we rewrite existing code.
Features:
- Add OMEMO trust status indicators to the titlebar (#2104)
- Add command autocompletion for /stamp (#2126)
- Add jid_is_valid_user_jid() to check for valid user JIDs (#2102)
- Add unified quality-check script and git hook support (#2099)
- Display incoming reports (XEP-0377) (#2117)
- Handle and allow JIDs with trailing slashes (#2102)
- Implement color coded status messages for file transfers (#2131)
- Implement dynamic pad resizing (#2122)
- Improve XEP-0377 support with report-origin and third-party elements (#2117)
- Improve feedback during OMEMO session Initiation (#2104)
- Improve validation of JIDs (#2102)
- Notify users when new OMEMO devices or fingerprints are discovered (#2104)
- Only request receipts when supported (#2101)
- Provide descriptive fallback messages for OMEMO decryption failures (#2104)
- Provide detailed encryption failure messages for OMEMO (#2104)
- Sanitize illegal XML characters from outgoing messages (#2116)
- Show active and trust status in /omemo fingerprint (#2104)
- Support UTF-8 characters in autocompletion (#2115)
- Suppress "new device" alerts for already known devices (#2113)
- Suppress redundant `session already exists` messages (#2113)
- Suppress repetitive missing device ID warnings (#2113)
- Introduce `prof_date_time_format_iso8601()`. (#2112)
- Optimize random data generation. (#2109)
Bug Fixes:
- Allow adding own JID to roster (#2127)
- Check for necessary pointers in omemo_receive_message
- Correct handling of create_fulljid without resource (#2102)
- Database return NULL if no history limits are found (#2102)
- Define `prefs_changes_print` outside of `prefs_changes` (#2090)
- Dont OMEMO trust check so often (#2108)
- Fix -Wanalyzer-deref-before-check warning in cmd_ac_complete (#2095)
- Fix Floating Point Exception in OMEMO session building (#2096)
- Fix NULL dereference and memory leak in _mam_rsm_id_handler (#2094)
- Fix NULL dereference and memory leaks in OX logic (#2094)
- Fix api_get_current_occupants() memory problems (#2094)
- Fix custom outgoing stamp and fix stamp command help (#2126)
- Fix file autocompletion bugs and restore cycling (#2100)
- Fix memory leak and potential crash in iq_id_handler_add (#2094)
- Fix not saving first created account (#2107)
- Fix rare memleak (#2123)
- Fix redundant error reporting in http download (#2094)
- Fix usage error in /time command (#2101)
- Handle potential NULL from malloc in OMEMO fingerprint decoding (#2094)
- Ignore self-presence for untracked MUC rooms (#2097)
- Improve status reporting and filename handling for /url save (#2131)
- Incorrect omemo decryption error for Key Transport Messages (#2130)
- Increase max arguments for /connect to 9 (#2101)
- Initialize OMEMO pointers and add NULL checks (#2094)
- Missing plugins_post_chat_message_display calls (#2118)
- Only allow /msg with valid JIDs (#2102)
- Sanitize account names (#2105)
- Segfault when using /command help (#2094)
- Use <body> tag for spam reporting (XEP-0377) (#2117)
- Fix -Wanalyzer-deref-before-check warning in get_message_from_editor (#2095)
- Fix potential double free. (#2112)
- Fix potential segfault. (#2109)
- Flush OMEMO store only after encrypting for all recipients. (#2109)
- Check connection state before accepting command. (#2124)
Documentation:
- Add Conventional Commit Structure
- Add a section explaining how to write functional tests (#2101)
- Add information about footers in git commit messages
- Add note about sanitizers overhead (#2101)
- Add section explaining how to run functional tests (#2101)
- Expand the build section in CONTRIBUTING.md (#2099)
- Explain how to turn clang-format of for some blocks of code (#2099)
- Explain how to write unit tests (#2099)
- Improve help for /blocked (#2117)
- Mention generated html pages in release guide
- Sort entries in theme_template
- Update implemented XEPs
- Update theme_template with missing options
Cleanup:
- Adapt loop counter to proper type (#2089)
- Adapt type and cast to get ride of warnings (#2089)
- Add a defensive check in cmd_process_input() (#2094)
- Be a bit more defensive in (#2094)
- Be a bit more defensive in server_events.c (#2094)
- Cast device ids in omemo.c (#2089)
- Cast g_hash_table_lookup return to function pointer (#2089)
- Cast to get rid of warnings (#2089)
- Check for strdup() success in stanza_create_http_upload_request() (#2094)
- Cleanup log module (#2094)
- Cleanup types (#2104)
- Correct comparison in cons_show_wins() (#2089)
- Fix cast function type warnings in pgp.c (#2089)
- Fix potential NULL dereference and leaks in cmd_sendfile (#2094)
- Fix potential NULL dereference in cmd_omemo_(un)trust (#2094)
- Fix potential null pointer dereference (#2094)
- Fix uninitialized field in color_distance() (#2089)
- Initialize GOptionEntry entries correctly (#2089)
- Initialize optional fields in profModule (#2089)
- Initialize waittime to 0 (#2094)
- Make _connection_handler() safer (#2094)
- Make muc_nick() return const char* (#2089)
- Map test files according to structure in src (#2099)
- Move loop var from int to size_t in cmd_funcs.c (#2089)
- Move loop var from int to size_t in omemo.c (#2089)
- Move more functions from int to guint in statusbar.c (#2089)
- Move some variables from int to guint in statusbar.c (#2089)
- Refactor account_eval_password to use glib (#2094)
- Use auto_FILE and handle fopen failure in command_docgen (#2094)
- Use g_malloc and auto_gchar in _win_print_wrapped (#2094)
- Use g_new() instead of malloc in prof_add_shutdown_routine() (#2094)
- Use g_new0 and g_strdup for alias allocation (#2094)
- Use g_new0 instead of malloc in buffer_get_entry_by_id() (#2094)
- Wrap release handler in iq.c (#2089)
- The buffer API should use `unsigned` types. (#2112)
Tests:
- Add prof_send_raw (#2115)
- Add test for get_expanded_path() (#2093)
- Add test for get_mentions() (#2093)
- Add test for release_is_new() (#2093)
- Add test for string_to_verbosity() (#2093)
- Add test for tab autocompletion of /msg (#2115)
- Add test for valid_tls_policy_option() (#2093)
- Add tests for string_matches_one_of() (#2093)
- Add tests for strtoi_range() (#2093)
- Apply coding style to unit tests (#2099)
- Check for binary before starting functional tests
- Don't build functional test with analyzer and increase timeouts (#2101)
- Fix functional presence tests (#2101)
- Fix functional test connect_shows_presence_updates (#2101)
- Fix functional test message_receive_chatwin (#2101)
- Fix functional test message_send (#2101)
- Fix functional test ping_jid (#2101)
- Fix functional test ping_responds_to_server_request (#2101)
- Fix functional test receive_carbon (#2101)
- Fix functional test resets_to_barejid_after_presence_received (#2101)
- Fix functional test rooms_query (#2101)
- Fix functional test send_receipt_on_request (#2101)
- Fix functional test sends_room_join (#2101)
- Fix functional test test_ping (#2101)
- Fix passing of special characters to prof_expect() (#2101)
- Improve autocomplete unit tests (#2093)
- Re-enable functional tests (#2101)
- Remove apparently unused code (#2101)
- Standardize naming convention (#2099)
- Try two places for profanity binary (#2101)
- Update unit tests (#2094)
Build System:
- Add --enable-functional-tests switch (#2123)
- Add -Wpointer-arith flag (#2095)
- Add -fstack-protector-strong compiler flag (#2094)
- Add .gitattributes to exclude development files from exports (#2123)
- Add support for libomemo-c as OMEMO backend (#2120)
- Enable -fanalyzer for static analysis (#2094)
- Enable _FORTIFY_SOURCE and -Og optimization (#2094)
- Enable more warnings in debug mode (#2089)
- Set -Qunused-arguments depending on compiler not os
Refactorings:
- Centralize validation within jid_is_valid (#2102)
- Change vcard_print() checks (#2094)
- Fix clang-format expansion of table (#2099)
- Improve key transport message handling and deduplicate errors (#2130)
- Make Jid use glib functions (#2094)
- Make Resource use glib functions (#2094)
- Make _writecsv safer and with better performance (#2094)
- Modernize cmd_ac_complete_filepath and simplify path handling (#2094)
- Move pgp module to gchar (#2094)
- Optimize prof_occurrences() (#2093)
- Partly move plugins to glib (#2094)
- Refactor OMEMO error handling and add non-null attributes (#2104)
- Refactor form_set_value to use glib list management (#2095)
- Replace calloc with g_new0 for struct allocations (#2094)
- Replace malloc with g_new0 in many occasions (#2094)
- Roster export uses GString and g_file_set_contents now (#2094)
- Start to standardize on gchar (#2094)
- Use g_malloc0 instead of calloc in get_random_string() (#2094)
- Use g_new in message_pubsub_event_handler_add() (#2094)
- Use p_contact_new instead of malloc in p_contact_new() (#2094)
Chores:
- Add 0.16.0 to release in doap file
- Add README explaining the purpose and usage of helper scripts (#2128)
- Add XEP comparison script
- Add changelog-helper script (#2128)
- Add copyright header to C files who had none
- Add linter file to be ignored upon export
- Make meson doublecheck only available if the script exists
- Move to SPDX license header
- Require DCO from now on (#2123)
- Unify build configuration script into one (#2128)
- Update clang-format action to 4.16.0
- Update valgrind suppression file (#2123)
- Use more descriptive names for helper scripts (#2128)
0.16.0 (2026-02-23)
===================
5 people contributed to this release: @balejk, @mcalierno, @ritesh006, @sjaeckel and @jubalh.
Thanks a lot to our sponsors: Matthew Fennell, Martin Dosch and one anonymous sponsor.
If you want to support us too: https://profanity-im.github.io/donate.html
This release depends on libstrophe >= 0.12.3.
Changes:
- Add new command /changes (#2080)
With that command one can see the modifications of the runtime
configuration vs. the saved configuration.
- Explain the different kinds of messages in the manpage (#2063)
- Fix OTR detection (#1957, #2072)
- Fix OMEMO startup (79ff9ba)
- Fix overwriting new accounts when running multiple instances (#2080)
- Fix reconnect when no account has been set up yet (#2080)
- Don't publish keys if the server doesn't support pubsub (#2078, #2080)
- Fix compilation on Apple silicon macs (#2075)
- Handle SIGTERM and SIGHUP (#2082)
- Cleanup (#2067, #2080)
- Add cygwin CI (#2066)
- Replace ACX_PTHREAD with AX_PTHREAD (#2062)
- Add meson build system (#2086)
Mainly for testing purposes. Details will be announced in a later release.
0.15.1 (2025-08-22)
===================
5 people contributed to this release: @andreasstieger, @killerdevildog, @mdosch,
@sjaeckel and @jubalh.
Thanks a lot to our sponsors: Matthew Fennell, Martin Dosch and one anonymous sponsor.
If you want to support us too: https://profanity-im.github.io/donate.html
This release depends on libstrophe >= 0.12.3.
Changes:
- Add `iso8601` as valid time format
`/time all set iso8601` instead of manual specification
- Fix ignoring of roster pushes (#2035)
- Print location of decrypted files (#2041)
- Fix GPGME >= 2.0.0 compatibility issue (#2048)
- Fix tests with gcc15 and uintptr_t (#2055)
- Reduce noise in log files (#1911, #2060)
- Cleanup, code improvement and memory fixes (#2033, #2041, #2053)
- Improve documentation (#2040, 18f157b, #2058)
0.15.0 (2025-03-27)
===================

View File

@@ -123,6 +123,20 @@ Test your changes with the following tools to find mistakes.
Run `make check` to run the unit tests with your current configuration or `./ci-build.sh` to check with different switches passed to configure.
### flat-file backend tests
To run functional tests with the flat-file database backend (instead of SQLite):
```bash
make check-functional-flatfile
```
Or manually for a single group:
```bash
PROF_FLATFILE=1 PROF_TEST_GROUP=1 ./tests/functionaltests/functionaltests 1
```
### valgrind
We provide a suppressions file `prof.supp`. It is a combination of the suppressions for shipped with glib2, python and custom rules.

View File

@@ -3,6 +3,10 @@ core_sources = \
src/log.c src/common.c \
src/chatlog.c src/chatlog.h \
src/database.h src/database.c \
src/database_flatfile.c src/database_flatfile.h \
src/database_flatfile_parser.c \
src/database_flatfile_verify.c \
src/database_export.c \
src/log.h src/profanity.c src/common.h \
src/profanity.h src/xmpp/chat_session.c \
src/xmpp/chat_session.h src/xmpp/muc.c src/xmpp/muc.h src/xmpp/jid.h src/xmpp/jid.c \
@@ -33,6 +37,7 @@ core_sources = \
src/ui/window_list.c src/ui/window_list.h \
src/ui/rosterwin.c src/ui/occupantswin.c \
src/ui/buffer.c src/ui/buffer.h \
src/ui/aiwin.c \
src/ui/chatwin.c \
src/ui/mucwin.c \
src/ui/privwin.c \
@@ -74,7 +79,8 @@ core_sources = \
src/plugins/themes.c src/plugins/themes.h \
src/plugins/settings.c src/plugins/settings.h \
src/plugins/disco.c src/plugins/disco.h \
src/ui/tray.h src/ui/tray.c
src/ui/tray.h src/ui/tray.c \
src/ai/ai_client.h src/ai/ai_client.c
unittest_sources = \
src/xmpp/contact.c src/xmpp/contact.h src/common.c \
@@ -85,6 +91,7 @@ unittest_sources = \
src/xmpp/chat_state.h src/xmpp/chat_state.c \
src/xmpp/roster_list.c src/xmpp/roster_list.h \
src/xmpp/xmpp.h src/xmpp/form.c \
src/ai/ai_client.h src/ai/ai_client.c \
src/ui/ui.h \
src/otr/otr.h \
src/pgp/gpg.h \
@@ -93,6 +100,8 @@ unittest_sources = \
src/omemo/crypto.h \
src/omemo/store.h \
src/xmpp/vcard.h src/xmpp/vcard_funcs.h \
src/database_flatfile.h \
src/database_flatfile_parser.c \
src/command/cmd_defs.h src/command/cmd_defs.c \
src/command/cmd_funcs.h src/command/cmd_funcs.c \
src/command/cmd_ac.h src/command/cmd_ac.c \
@@ -132,6 +141,7 @@ unittest_sources = \
tests/unittests/xmpp/stub_message.c \
tests/unittests/ui/stub_ui.c tests/unittests/ui/stub_ui.h \
tests/unittests/ui/stub_vcardwin.c \
tests/unittests/ui/stub_ai.c \
tests/unittests/log/stub_log.c \
tests/unittests/chatlog/stub_chatlog.c \
tests/unittests/database/stub_database.c \
@@ -142,32 +152,36 @@ unittest_sources = \
tests/unittests/tools/stub_aesgcm_download.c \
tests/unittests/tools/stub_plugin_download.c \
tests/unittests/helpers.c tests/unittests/helpers.h \
tests/unittests/test_form.c tests/unittests/test_form.h \
tests/unittests/xmpp/test_form.c tests/unittests/xmpp/test_form.h \
tests/unittests/test_common.c tests/unittests/test_common.h \
tests/unittests/test_autocomplete.c tests/unittests/test_autocomplete.h \
tests/unittests/test_jid.c tests/unittests/test_jid.h \
tests/unittests/test_parser.c tests/unittests/test_parser.h \
tests/unittests/test_roster_list.c tests/unittests/test_roster_list.h \
tests/unittests/test_chat_session.c tests/unittests/test_chat_session.h \
tests/unittests/test_contact.c tests/unittests/test_contact.h \
tests/unittests/test_preferences.c tests/unittests/test_preferences.h \
tests/unittests/test_server_events.c tests/unittests/test_server_events.h \
tests/unittests/test_muc.c tests/unittests/test_muc.h \
tests/unittests/test_cmd_presence.c tests/unittests/test_cmd_presence.h \
tests/unittests/test_cmd_alias.c tests/unittests/test_cmd_alias.h \
tests/unittests/test_cmd_connect.c tests/unittests/test_cmd_connect.h \
tests/unittests/test_cmd_rooms.c tests/unittests/test_cmd_rooms.h \
tests/unittests/test_cmd_account.c tests/unittests/test_cmd_account.h \
tests/unittests/test_cmd_sub.c tests/unittests/test_cmd_sub.h \
tests/unittests/test_cmd_bookmark.c tests/unittests/test_cmd_bookmark.h \
tests/unittests/test_cmd_otr.c tests/unittests/test_cmd_otr.h \
tests/unittests/test_cmd_pgp.c tests/unittests/test_cmd_pgp.h \
tests/unittests/test_cmd_join.c tests/unittests/test_cmd_join.h \
tests/unittests/test_cmd_roster.c tests/unittests/test_cmd_roster.h \
tests/unittests/test_cmd_disconnect.c tests/unittests/test_cmd_disconnect.h \
tests/unittests/test_callbacks.c tests/unittests/test_callbacks.h \
tests/unittests/test_plugins_disco.c tests/unittests/test_plugins_disco.h \
tests/unittests/tools/test_autocomplete.c tests/unittests/tools/test_autocomplete.h \
tests/unittests/xmpp/test_jid.c tests/unittests/xmpp/test_jid.h \
tests/unittests/tools/test_parser.c tests/unittests/tools/test_parser.h \
tests/unittests/xmpp/test_roster_list.c tests/unittests/xmpp/test_roster_list.h \
tests/unittests/xmpp/test_chat_session.c tests/unittests/xmpp/test_chat_session.h \
tests/unittests/xmpp/test_contact.c tests/unittests/xmpp/test_contact.h \
tests/unittests/config/test_preferences.c tests/unittests/config/test_preferences.h \
tests/unittests/event/test_server_events.c tests/unittests/event/test_server_events.h \
tests/unittests/xmpp/test_muc.c tests/unittests/xmpp/test_muc.h \
tests/unittests/command/test_cmd_presence.c tests/unittests/command/test_cmd_presence.h \
tests/unittests/command/test_cmd_alias.c tests/unittests/command/test_cmd_alias.h \
tests/unittests/command/test_cmd_connect.c tests/unittests/command/test_cmd_connect.h \
tests/unittests/command/test_cmd_rooms.c tests/unittests/command/test_cmd_rooms.h \
tests/unittests/command/test_cmd_account.c tests/unittests/command/test_cmd_account.h \
tests/unittests/command/test_cmd_sub.c tests/unittests/command/test_cmd_sub.h \
tests/unittests/command/test_cmd_bookmark.c tests/unittests/command/test_cmd_bookmark.h \
tests/unittests/command/test_cmd_otr.c tests/unittests/command/test_cmd_otr.h \
tests/unittests/command/test_cmd_pgp.c tests/unittests/command/test_cmd_pgp.h \
tests/unittests/command/test_cmd_join.c tests/unittests/command/test_cmd_join.h \
tests/unittests/command/test_cmd_roster.c tests/unittests/command/test_cmd_roster.h \
tests/unittests/command/test_cmd_disconnect.c tests/unittests/command/test_cmd_disconnect.h \
tests/unittests/command/test_cmd_ac.c tests/unittests/command/test_cmd_ac.h \
tests/unittests/plugins/test_callbacks.c tests/unittests/plugins/test_callbacks.h \
tests/unittests/plugins/test_plugins_disco.c tests/unittests/plugins/test_plugins_disco.h \
tests/unittests/test_forced_encryption.c tests/unittests/test_forced_encryption.h \
tests/unittests/test_ai_client.c tests/unittests/test_ai_client.h \
tests/unittests/test_database_export.c tests/unittests/test_database_export.h \
tests/unittests/test_database_stress.c tests/unittests/test_database_stress.h \
tests/unittests/unittests.c
functionaltest_sources = \
@@ -185,7 +199,12 @@ functionaltest_sources = \
tests/functionaltests/test_software.c tests/functionaltests/test_software.h \
tests/functionaltests/test_muc.c tests/functionaltests/test_muc.h \
tests/functionaltests/test_disconnect.c tests/functionaltests/test_disconnect.h \
tests/functionaltests/test_history.c tests/functionaltests/test_history.h \
tests/functionaltests/test_lastactivity.c tests/functionaltests/test_lastactivity.h \
tests/functionaltests/test_autoping.c tests/functionaltests/test_autoping.h \
tests/functionaltests/test_disco.c tests/functionaltests/test_disco.h \
tests/functionaltests/test_export_import.c tests/functionaltests/test_export_import.h \
tests/functionaltests/test_ai.c tests/functionaltests/test_ai.h \
tests/functionaltests/functionaltests.c
main_source = src/main.c
@@ -255,6 +274,12 @@ core_sources += $(omemo_sources)
unittest_sources += $(omemo_unittest_sources)
endif
sqlite_sources = src/database_sqlite.c
if BUILD_SQLITE
core_sources += $(sqlite_sources)
endif
all_c_sources = $(core_sources) $(unittest_sources) \
$(pgp_sources) $(pgp_unittest_sources) \
$(otr4_sources) $(otr_unittest_sources) \
@@ -287,7 +312,7 @@ endif
TESTS = tests/unittests/unittests
check_PROGRAMS = tests/unittests/unittests
tests_unittests_unittests_CPPFLAGS = -I$(srcdir)/tests
tests_unittests_unittests_CPPFLAGS = -I$(srcdir)/tests -I$(srcdir)/tests/unittests
tests_unittests_unittests_SOURCES = $(unittest_sources)
tests_unittests_unittests_LDADD = -lcmocka
@@ -329,11 +354,256 @@ check-functional-parallel: tests/functionaltests/functionaltests
grep -E 'PASSED|FAILED|Running' $(builddir)/test-logs/group*.log || true; \
if [ $$failed -ne 0 ]; then echo "FUNCTIONAL TESTS FAILED"; exit 1; fi; \
echo "All functional test groups passed!"
# Run functional tests with the flat-file database backend
# Usage: make check-functional-flatfile
check-functional-flatfile: tests/functionaltests/functionaltests
@echo "Running functional tests with flat-file backend ($(words $(FUNC_TEST_GROUPS)) groups)..."
@mkdir -p $(builddir)/test-logs $(builddir)/test-files
@pids=""; \
for g in $(FUNC_TEST_GROUPS); do \
PROF_FLATFILE=1 ./tests/functionaltests/functionaltests $$g > $(builddir)/test-logs/group$$g-flatfile.log 2>&1 & \
pids="$$pids $$!"; \
done; \
failed=0; i=1; \
for pid in $$pids; do \
wait $$pid || { echo "Group $$i FAILED (flatfile)"; cat $(builddir)/test-logs/group$$i-flatfile.log; failed=1; }; \
i=$$((i + 1)); \
done; \
echo "=== Flat-file Test Results Summary ==="; \
grep -E 'PASSED|FAILED|Running' $(builddir)/test-logs/group*-flatfile.log || true; \
if [ $$failed -ne 0 ]; then echo "FLAT-FILE FUNCTIONAL TESTS FAILED"; exit 1; fi; \
echo "All flat-file functional test groups passed!"
endif
endif
man1_MANS = $(man1_sources)
# ---------------------------------------------------------------------------
# Bench harness — synthetic database load tests. Not part of `make check`.
# Build only when invoked via `make bench` / `make bench-quick`.
# Sources live in tests/bench/. See tests/bench/README.md.
bench_common_sources = \
tests/bench/bench_stubs.c \
tests/bench/bench_common.c tests/bench/bench_common.h \
tests/bench/bench_csv.c tests/bench/bench_csv.h \
src/database_flatfile.c src/database_flatfile.h \
src/database_flatfile_parser.c \
src/database_flatfile_verify.c \
src/common.c src/common.h
# Sources needed only by the export/import bench (S7/S8). Pulls in the SQLite
# backend, the cross-backend export/import code, and the dispatcher.
bench_export_sources = \
src/database.c src/database.h \
src/database_sqlite.c \
src/database_export.c
EXTRA_PROGRAMS = \
tests/bench/gen_history \
tests/bench/bench_runner \
tests/bench/bench_long_messages \
tests/bench/bench_failure_modes \
tests/bench/bench_export_import
tests_bench_gen_history_SOURCES = tests/bench/gen_history.c $(bench_common_sources)
tests_bench_gen_history_CPPFLAGS = -I$(srcdir)/src -I$(srcdir)/tests/bench
tests_bench_gen_history_CFLAGS = $(AM_CFLAGS)
tests_bench_gen_history_LDADD =
tests_bench_bench_runner_SOURCES = tests/bench/bench_runner.c $(bench_common_sources)
tests_bench_bench_runner_CPPFLAGS = -I$(srcdir)/src -I$(srcdir)/tests/bench
tests_bench_bench_runner_CFLAGS = $(AM_CFLAGS)
tests_bench_bench_runner_LDADD =
tests_bench_bench_long_messages_SOURCES = tests/bench/bench_long_messages.c $(bench_common_sources)
tests_bench_bench_long_messages_CPPFLAGS = -I$(srcdir)/src -I$(srcdir)/tests/bench
tests_bench_bench_long_messages_CFLAGS = $(AM_CFLAGS)
tests_bench_bench_long_messages_LDADD =
tests_bench_bench_failure_modes_SOURCES = tests/bench/bench_failure_modes.c $(bench_common_sources)
tests_bench_bench_failure_modes_CPPFLAGS = -I$(srcdir)/src -I$(srcdir)/tests/bench
tests_bench_bench_failure_modes_CFLAGS = $(AM_CFLAGS)
tests_bench_bench_failure_modes_LDADD =
tests_bench_bench_export_import_SOURCES = tests/bench/bench_export_import.c \
$(bench_common_sources) $(bench_export_sources)
tests_bench_bench_export_import_CPPFLAGS = -I$(srcdir)/src -I$(srcdir)/tests/bench
tests_bench_bench_export_import_CFLAGS = $(AM_CFLAGS)
tests_bench_bench_export_import_LDADD =
# Volume control: BENCH_VOLUME={small|medium|max}. Default `small` is safe
# even on tight disks (~5 MB). `max` is heavyweight — see README.
BENCH_VOLUME ?= small
BENCH_DATA_DIR ?= /tmp/cproof-bench-corpus
BENCH_CSV ?= tests/bench/current.csv
bench-build: tests/bench/gen_history tests/bench/bench_runner tests/bench/bench_long_messages tests/bench/bench_failure_modes tests/bench/bench_export_import
# Resolve --lines / --years for the configured volume.
bench-gen: bench-build
@mkdir -p $(BENCH_DATA_DIR)
@case "$(BENCH_VOLUME)" in \
small) L=10000; Y=1; P=mixed ;; \
medium) L=500000; Y=5; P=mixed ;; \
max) L=5000000; Y=10; P=long ;; \
*) echo "BENCH_VOLUME must be small|medium|max"; exit 2 ;; \
esac; \
echo "==> generating volume=$(BENCH_VOLUME) lines=$$L years=$$Y profile=$$P"; \
./tests/bench/gen_history --lines=$$L --years=$$Y --msg-len-profile=$$P \
--lmc-rate=3 --mam-ooo-rate=0 --resources-per-contact=3 \
--output=$(BENCH_DATA_DIR) --seed=42
bench-run: bench-build
@echo "==> running scenarios (csv=$(BENCH_CSV))"
@BENCH_VOLUME=$(BENCH_VOLUME) BENCH_DATA_DIR=$(BENCH_DATA_DIR) \
./tests/bench/bench_runner --data=$(BENCH_DATA_DIR) --csv=$(BENCH_CSV)
@echo "==> CSV: $(BENCH_CSV)"
@cat $(BENCH_CSV)
bench: bench-gen bench-run
bench-quick:
@$(MAKE) bench BENCH_VOLUME=small
# Long-message tests (L1L14). Independent of corpus volume, uses its own tmp dir.
bench-longmsg: bench-build
@echo "==> long-message tests (csv=$(BENCH_CSV))"
@./tests/bench/bench_long_messages \
--tmp=$(BENCH_DATA_DIR)-longmsg --csv=$(BENCH_CSV)
@rm -rf $(BENCH_DATA_DIR)-longmsg
# Failure-injection (F1F15). Independent corpus, asserts behaviour on bad data.
bench-failure: bench-build
@echo "==> failure-injection tests (csv=$(BENCH_CSV))"
@./tests/bench/bench_failure_modes \
--tmp=$(BENCH_DATA_DIR)-fail --csv=$(BENCH_CSV)
@rm -rf $(BENCH_DATA_DIR)-fail
# Export/import (S7/S8) pipeline. Independent corpus under BENCH_DATA_DIR-export.
# Default scale: 100k rows (~530 s). Override with BENCH_PIPE_ROWS=N.
BENCH_PIPE_ROWS ?= 100000
BENCH_PIPE_VOLUME ?= pipe$(BENCH_PIPE_ROWS)
bench-export: bench-build
@echo "==> S7 export pipeline ($(BENCH_PIPE_ROWS) rows, csv=$(BENCH_CSV))"
@rm -rf $(BENCH_DATA_DIR)-export
@BENCH_DATA_DIR=$(BENCH_DATA_DIR)-export \
./tests/bench/bench_export_import seed --rows=$(BENCH_PIPE_ROWS) --account=bench@bench.example
@BENCH_DATA_DIR=$(BENCH_DATA_DIR)-export \
./tests/bench/bench_export_import export --account=bench@bench.example \
--csv=$(BENCH_CSV) --label=S7a_export_cold --volume=$(BENCH_PIPE_VOLUME)
@echo " -- second pass (dedup hot path)"
@BENCH_DATA_DIR=$(BENCH_DATA_DIR)-export \
./tests/bench/bench_export_import export --account=bench@bench.example \
--csv=$(BENCH_CSV) --label=S7b_export_dedup --volume=$(BENCH_PIPE_VOLUME)
@rm -rf $(BENCH_DATA_DIR)-export
bench-import: bench-build
@echo "==> S8 import pipeline ($(BENCH_PIPE_ROWS) rows, csv=$(BENCH_CSV))"
@rm -rf $(BENCH_DATA_DIR)-export
@# Seed account A, export it, then wipe its DB but keep flatlog. Import
@# back into the same account — that way the to: header in flatfile
@# matches the importing account so dedup works correctly.
@BENCH_DATA_DIR=$(BENCH_DATA_DIR)-export \
./tests/bench/bench_export_import seed --rows=$(BENCH_PIPE_ROWS) --account=bench@bench.example
@BENCH_DATA_DIR=$(BENCH_DATA_DIR)-export \
./tests/bench/bench_export_import export --account=bench@bench.example \
--csv=$(BENCH_CSV) --label=S7_seed_export --volume=$(BENCH_PIPE_VOLUME)
@rm -f $(BENCH_DATA_DIR)-export/database/bench_at_bench.example/chatlog.db*
@BENCH_DATA_DIR=$(BENCH_DATA_DIR)-export \
./tests/bench/bench_export_import import --account=bench@bench.example \
--csv=$(BENCH_CSV) --label=S8a_import_cold --volume=$(BENCH_PIPE_VOLUME)
@echo " -- second import (idempotency)"
@BENCH_DATA_DIR=$(BENCH_DATA_DIR)-export \
./tests/bench/bench_export_import import --account=bench@bench.example \
--csv=$(BENCH_CSV) --label=S8b_import_idempotent --volume=$(BENCH_PIPE_VOLUME)
@rm -rf $(BENCH_DATA_DIR)-export
# Full roundtrip with content diff at default volume. PASSes only if
# every row in the rebuilt DB matches the source byte-for-byte.
bench-roundtrip: bench-build
@echo "==> S8e roundtrip+full diff ($(BENCH_PIPE_ROWS) rows, csv=$(BENCH_CSV))"
@rm -rf $(BENCH_DATA_DIR)-export
@BENCH_DATA_DIR=$(BENCH_DATA_DIR)-export \
./tests/bench/bench_export_import roundtrip --rows=$(BENCH_PIPE_ROWS) \
--csv=$(BENCH_CSV) --label=S8e_roundtrip --volume=$(BENCH_PIPE_VOLUME) --full-diff
@rm -rf $(BENCH_DATA_DIR)-export
# Pipeline at medium volume (default BENCH_PIPE_ROWS=100k).
bench-pipeline: bench-export bench-import bench-roundtrip
# Pipeline at MAX volume — 1M rows. Heavy: ~510 min, ~500MB SQLite + ~500MB
# flatfile + ~500MB second SQLite. Run only when you can afford the time
# and the disk. Set BENCH_PIPE_ROWS_MAX to override (default 1_000_000).
BENCH_PIPE_ROWS_MAX ?= 1000000
bench-pipeline-max: bench-build
@echo "==> bench-pipeline-max: $(BENCH_PIPE_ROWS_MAX) rows (heavy!)"
@$(MAKE) bench-export BENCH_PIPE_ROWS=$(BENCH_PIPE_ROWS_MAX)
@$(MAKE) bench-import BENCH_PIPE_ROWS=$(BENCH_PIPE_ROWS_MAX)
@$(MAKE) bench-roundtrip BENCH_PIPE_ROWS=$(BENCH_PIPE_ROWS_MAX)
# S9: 200 contacts × 50k lines per contact = 10M lines distributed.
bench-multicontact: bench-build
@mkdir -p $(BENCH_DATA_DIR)-multi
@echo "==> S9: 200 contacts x 5000 lines"
@./tests/bench/gen_history --lines=1000000 --contacts=200 --years=3 \
--msg-len-profile=mixed --output=$(BENCH_DATA_DIR)-multi --seed=42
@BENCH_VOLUME=multicontact BENCH_DATA_DIR=$(BENCH_DATA_DIR)-multi \
./tests/bench/bench_runner --data=$(BENCH_DATA_DIR)-multi --csv=$(BENCH_CSV) \
--scenarios=S4,S6
@rm -rf $(BENCH_DATA_DIR)-multi
# S10: 30% LMC corrections.
bench-lmc: bench-build
@mkdir -p $(BENCH_DATA_DIR)-lmc
@echo "==> S10: LMC-heavy corpus (30%% corrections)"
@./tests/bench/gen_history --lines=100000 --years=2 --lmc-rate=30 \
--msg-len-profile=mixed --output=$(BENCH_DATA_DIR)-lmc --seed=42
@BENCH_VOLUME=lmc BENCH_DATA_DIR=$(BENCH_DATA_DIR)-lmc \
./tests/bench/bench_runner --data=$(BENCH_DATA_DIR)-lmc --csv=$(BENCH_CSV)
@rm -rf $(BENCH_DATA_DIR)-lmc
# S11: 20% MAM out-of-order timestamps.
bench-ooo: bench-build
@mkdir -p $(BENCH_DATA_DIR)-ooo
@echo "==> S11: MAM-OOO corpus (20%% out-of-order)"
@./tests/bench/gen_history --lines=100000 --years=2 --mam-ooo-rate=20 \
--msg-len-profile=mixed --output=$(BENCH_DATA_DIR)-ooo --seed=42
@BENCH_VOLUME=ooo BENCH_DATA_DIR=$(BENCH_DATA_DIR)-ooo \
./tests/bench/bench_runner --data=$(BENCH_DATA_DIR)-ooo --csv=$(BENCH_CSV) \
--scenarios=S6
@rm -rf $(BENCH_DATA_DIR)-ooo
# Run everything: P1 scenarios + long messages + failure injection + multicontact + LMC + OOO.
bench-full: bench bench-longmsg bench-failure bench-multicontact bench-lmc bench-ooo
# Compare current.csv against baseline.csv and exit non-zero on regressions.
BENCH_BASELINE ?= tests/bench/baseline.csv
BENCH_THRESHOLD ?= 25
bench-compare:
@python3 tests/bench/compare_baseline.py \
--baseline=$(BENCH_BASELINE) --current=$(BENCH_CSV) \
--threshold=$(BENCH_THRESHOLD)
# Snapshot current.csv as the new baseline. Run this only after manually
# reviewing that the numbers look healthy.
bench-update-baseline:
@cp $(BENCH_CSV) $(BENCH_BASELINE)
@echo "baseline updated: $(BENCH_BASELINE)"
bench-clean:
rm -rf $(BENCH_DATA_DIR) $(BENCH_DATA_DIR)-longmsg $(BENCH_DATA_DIR)-fail \
$(BENCH_DATA_DIR)-multi $(BENCH_DATA_DIR)-lmc $(BENCH_DATA_DIR)-ooo \
$(BENCH_DATA_DIR)-export $(BENCH_CSV)
.PHONY: bench bench-quick bench-build bench-gen bench-run bench-clean \
bench-longmsg bench-failure bench-multicontact bench-lmc bench-ooo \
bench-full bench-compare bench-update-baseline \
bench-export bench-import bench-roundtrip bench-pipeline bench-pipeline-max
EXTRA_DIST = $(man1_sources) $(icons_sources) $(themes_sources) $(script_sources) profrc.example theme_template LICENSE.txt README.md CHANGELOG
# Ship API documentation with `make dist`
@@ -349,6 +619,20 @@ EXTRA_DIST += \
apidocs/python/src/plugin.py \
apidocs/python/src/prof.py
# Bench harness sources (not built by default; see EXTRA_PROGRAMS above)
EXTRA_DIST += \
tests/bench/README.md \
tests/bench/compare_baseline.py \
tests/bench/gen_history.c \
tests/bench/bench_runner.c \
tests/bench/bench_long_messages.c \
tests/bench/bench_failure_modes.c \
tests/bench/bench_export_import.c \
tests/bench/bench_stubs.c \
tests/bench/bench_common.c tests/bench/bench_common.h \
tests/bench/bench_csv.c tests/bench/bench_csv.h \
tests/bench/baseline.csv
if INCLUDE_GIT_VERSION
EXTRA_DIST += .git/HEAD .git/index

View File

@@ -2,67 +2,40 @@
* Release libstrophe if required
* Run Unit tests: `make check-unit`
* Run Functional tests - Currently disabled
* Run manual valgrind tests for new features
* Build and simple tests in Virtual machines ideally all dists including OSX and Windows (Cygwin)
* Set the correct release version in meson.build
* Run Unit tests: `meson setup build_test -Dtests=true && meson test -C build_test`
* Run Functional tests: same as above but needs stabber installed
* Update Inline command help (./src/command/cmd_defs.c)
* Check copyright dates in all files
* Create clean build folder:
`meson setup build_deb --buildtype=debug -Dnotifications=enabled -Dicons-and-clipboard=enabled -Dotr=enabled -Dpgp=enabled -Domemo=enabled -Dc-plugins=enabled -Dpython-plugins=enabled -Dxscreensaver=enabled -Domemo-qrcode=enabled -Dgdk-pixbuf=enabled`
* Build profanity: `meson compile -C build_deb`
* Generate HTML docs (the docgen argument only works when package status is development)
`./profanity docgen`
`./build_deb/profanity docgen`
* Generate manpages (the mangen argument only works when package status is development)
`./build_deb/profanity mangen`
* Determine if `libprofanity`'s version needs to be [increased](https://github.com/profanity-im/profanity/issues/973)
* Update plugin API docs (./apidocs/c and ./apidocs/python) need to run the `gen.sh` and commit the results to the website git repo
* Update CHANGELOG
* Update CHANGELOG (Use scripts/changelog-helper.py)
* Update profrc.example
* Update profanity.doap (new XEPs and latest version). Look for `DEV` which marks what is done on the development branch.
* Add new release to profanity.doap
## Creating artefacts
* Set the correct release version in configure.ac:
```
AC_INIT([profanity], [0.6.0], [boothj5web@gmail.com])
```
* Set the package status in configure.ac:
```
PACKAGE_STATUS="release"
```
* Set the package status to release: `meson setup build_rel --buildtype=release`
* Update date and version in man pages (profanity.1, profanity-ox-setup.1)
* Generate manpages for profanity commands (the mangen argument only works when package status is development)
`./profanity mangen`
These files should be added to the docs subfolder and added to git whenever a command changes.
* Add generated command manpages: `git add docs/profanity-*.1`
* Commit
* Commit (Release 0.1.2)
* Tag (0.1.2)
* Push
* Configure to generate fresh Makefile:
```
./bootstrap.sh && ./configure
```
* Generate tarballs:
```
make dist
make dist-bzip2
make dist-xz
make dist-zip
```
* Set the package status back to dev:
```
PACKAGE_STATUS="development"
```
* Generate tarballs: `meson dist -C build_rel --formats xztar,zip`
* Remove generated command manpages:
`git rm docs/profanity-*.1`
@@ -71,13 +44,3 @@ PACKAGE_STATUS="development"
* Commit `Start new cycle`
* Push
## Updating website
* Make changes to the git repo including uploading the new artefacts at:
https://github.com/profanity-im/profanity-im.github.io
* Add .xz and .zip tarballs to `tarballs` directory
* Copy `guide/latest` to `guide/newversion`
* Update tarball location and name in index.html
* Update checksums in index.html
* Update profanity_version.txt
* Take results from profanity.doap and put them into xeps.html

View File

@@ -1,8 +1,14 @@
#!/bin/bash
# check-cwe134.sh - Static analysis for CWE-134 format string vulnerabilities
# check-cwe134.sh - Verify __attribute__((format)) on printf-like wrappers
#
# This script detects potentially unsafe usage of format string functions
# where user-controlled data may be passed without "%s" wrapper.
# CWE-134 format string vulnerabilities are caught at compile time by
# -Wformat=2 (includes -Wformat-security + -Wformat-nonliteral), BUT only
# for functions annotated with __attribute__((format(printf, N, M))) or
# G_GNUC_PRINTF(N, M).
#
# This script ensures every variadic function whose last fixed parameter
# looks like a format string has the annotation. Without it, the compiler
# silently ignores format misuse.
#
# Usage: ./check-cwe134.sh [directory]
@@ -10,59 +16,103 @@ set -e
DIR="${1:-src}"
echo "=== CWE-134 Format String Vulnerability Check ==="
echo "=== CWE-134: format attribute audit ==="
echo "Scanning: $DIR"
echo ""
# Functions that accept format strings
FORMAT_FUNCS="cons_show|cons_debug|cons_show_error|log_info|log_error|log_warning|log_debug|win_println|win_print"
ERRORS=0
echo "Checking for unsafe format string usage..."
# --------------------------------------------------------------------- #
# Known printf-like wrappers that MUST have the attribute #
# --------------------------------------------------------------------- #
REQUIRED_ATTRIBUTED=(
"cons_show"
"cons_debug"
"cons_show_error"
"cons_show_padded"
"log_debug"
"log_info"
"log_warning"
"log_error"
"win_print"
"win_println"
"win_println_indent"
"win_println_va"
"win_append"
"win_appendln"
"win_append_highlight"
"win_appendln_highlight"
"win_command_exec_error"
)
echo "Check 1: Known wrappers must have G_GNUC_PRINTF / __attribute__((format))"
echo ""
# Pattern 1: function call with single variable argument (no format string)
# Example: cons_show(variable); - BAD
# Example: cons_show("%s", variable); - OK
# Matches: func(identifier) or func(identifier->member) or func(identifier[index])
RESULTS=$(grep -rn --include="*.c" -P "($FORMAT_FUNCS)\s*\(\s*[a-zA-Z_][a-zA-Z0-9_]*(\s*->\s*\w+|\s*\[\s*\w+\s*\])?\s*\)\s*;" "$DIR" 2>/dev/null || true)
for func in "${REQUIRED_ATTRIBUTED[@]}"; do
HAS_ATTR=$(grep -B1 --include="*.h" -rn "void ${func}\s*(" "$DIR" 2>/dev/null \
| grep -Ec "format\(printf|G_GNUC_PRINTF" || true)
if [ "$HAS_ATTR" -eq 0 ]; then
echo "$func — missing format attribute"
ERRORS=$((ERRORS + 1))
fi
done
# Filter out function definitions, declarations, and safe api_* wrappers
RESULTS=$(echo "$RESULTS" | grep -v "const char\|void \|^[^:]*:[0-9]*:[a-z_]*(\|api_cons_show\|api_log_" || true)
if [ "$ERRORS" -eq 0 ]; then
echo " ✅ All known wrappers annotated."
fi
if [ -n "$RESULTS" ]; then
echo "❌ POTENTIAL CWE-134 VULNERABILITIES FOUND:"
# --------------------------------------------------------------------- #
# Auto-detect new variadic functions that look like printf wrappers #
# but are NOT in the known list and NOT annotated. #
# Heuristic: declaration has (... const char* ..., ...) and no attribute #
# --------------------------------------------------------------------- #
echo ""
echo "Check 2: Detect unannotated printf-like variadic declarations in headers"
echo ""
KNOWN_RE=$(IFS="|"; echo "${REQUIRED_ATTRIBUTED[*]}")
# Find variadic declarations with a const char* / gchar* parameter followed by ...)
# that do NOT have a format attribute on the preceding line
NEW_ISSUES=$(grep -B1 -rn --include="*.h" \
'const g\?char\s*\*.*,\s*\.\.\.)' "$DIR" 2>/dev/null \
| awk '
/format\(printf|G_GNUC_PRINTF/ { skip=1; next }
/const g?char.*,.*\.\.\.\)/ {
if (skip) { skip=0; next }
print
}
{ skip=0 }
' \
| grep -E "void\s+\w+\s*\(" \
| grep -Ev "($KNOWN_RE)" \
|| true)
if [ -n "$NEW_ISSUES" ]; then
echo " ⚠️ Possibly unannotated new printf-like functions:"
echo ""
echo "$RESULTS"
echo "$NEW_ISSUES"
echo ""
ERRORS=$(echo "$RESULTS" | wc -l)
NEW_COUNT=$(echo "$NEW_ISSUES" | wc -l)
ERRORS=$((ERRORS + NEW_COUNT))
else
echo "✅ No obvious CWE-134 issues found."
fi
# Additional check: GString->str passed directly (not as %s argument)
echo ""
echo "Checking for GString->str passed to format functions..."
GSTRING_RESULTS=$(grep -rn --include="*.c" -P "($FORMAT_FUNCS)\s*\([^)]*->str\s*\)" "$DIR" 2>/dev/null | grep -v '"%s"' || true)
if [ -n "$GSTRING_RESULTS" ]; then
echo "⚠️ GString->str passed without \"%s\" (review manually):"
echo ""
echo "$GSTRING_RESULTS"
echo ""
echo " ✅ No unannotated variadic printf-like functions found."
fi
# --------------------------------------------------------------------- #
# Summary #
# --------------------------------------------------------------------- #
echo ""
echo "=== Summary ==="
echo "Critical issues: $ERRORS"
echo "Issues: $ERRORS"
echo ""
if [ "$ERRORS" -gt 0 ]; then
echo "Fix: add the attribute before the declaration in the .h file:"
echo " G_GNUC_PRINTF(N, M) // N = format arg, M = first vararg"
echo " void my_func(ProfWin* w, const char* fmt, ...);"
echo ""
echo "Fix by adding \"%s\" format specifier:"
echo " BAD: cons_show(variable);"
echo " GOOD: cons_show(\"%s\", variable);"
echo "The compiler flag -Wformat=2 will then catch all misuse automatically."
exit 1
fi

View File

@@ -204,6 +204,13 @@ case "$ARCH" in
""
)
source /etc/profile.d/debuginfod.sh 2>/dev/null || true
if grep -q 'ID=arch' /etc/os-release 2>/dev/null && [ -f /etc/makepkg.conf ]; then
echo "--> [Parity Mode] Simulating Pikaur collision..."
set -a
source /etc/makepkg.conf
set +a
fi
;;
darwin*)
# 4 configurations for parallel CI

View File

@@ -2,6 +2,9 @@ FROM archlinux:latest
ENV TERM=xterm
ENV CC="ccache gcc"
# Arch ships a stripped ld-linux; Valgrind needs glibc debug symbols to
# redirect memcmp/memcpy/strlen. debuginfod fetches them on demand.
ENV DEBUGINFOD_URLS=https://debuginfod.archlinux.org
RUN pacman -Syyu --noconfirm

View File

@@ -94,8 +94,17 @@ PKG_CHECK_MODULES([curl], [libcurl >= 7.62.0], [],
[AC_CHECK_LIB([curl], [main], [],
[AC_MSG_ERROR([libcurl 7.62.0 or higher is required])])])
PKG_CHECK_MODULES([SQLITE], [sqlite3 >= 3.22.0], [],
[AC_MSG_ERROR([sqlite3 3.22.0 or higher is required])])
### sqlite (optional — can be disabled with --without-sqlite)
AC_ARG_WITH([sqlite],
[AS_HELP_STRING([--without-sqlite], [build without SQLite support (flat-file backend only)])],
[], [with_sqlite=yes])
AS_IF([test "x$with_sqlite" != "xno"],
[PKG_CHECK_MODULES([SQLITE], [sqlite3 >= 3.22.0],
[AC_DEFINE([HAVE_SQLITE], [1], [SQLite support])],
[AC_MSG_ERROR([sqlite3 3.22.0 or higher is required (use --without-sqlite to disable)])])],
[AC_MSG_NOTICE([Building without SQLite — flat-file backend only])])
AM_CONDITIONAL([BUILD_SQLITE], [test "x$with_sqlite" != "xno"])
ACX_PTHREAD([], [AC_MSG_ERROR([pthread is required])])
AS_IF([test "x$PTHREAD_CC" != x], [ CC="$PTHREAD_CC" ])
@@ -385,9 +394,40 @@ AC_CHECK_LIB([util], [forkpty], [AM_CONDITIONAL([HAVE_FORKPTY], [true]) FORKPTY_
AC_SUBST([FORKPTY_LIB])
## Default parameters
AM_CFLAGS="$AM_CFLAGS -Wall -Wno-deprecated-declarations -std=gnu99 -ggdb3"
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-sign-compare -Wno-cast-function-type"
AM_CFLAGS="$AM_CFLAGS -Wpointer-arith"
AM_CFLAGS="$AM_CFLAGS -Wimplicit-function-declaration"
AM_CFLAGS="$AM_CFLAGS -Wundef"
AM_CFLAGS="$AM_CFLAGS -Wfloat-equal -Wredundant-decls"
AM_CFLAGS="$AM_CFLAGS -fstack-protector-strong -fno-common"
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; 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"
AM_LDFLAGS="$AM_LDFLAGS -export-dynamic"
# Linker hardening (RELRO + immediate binding)
saved_LDFLAGS="$LDFLAGS"
for _flag in -Wl,-z,relro -Wl,-z,now; do
AC_MSG_CHECKING([whether linker supports $_flag])
LDFLAGS="$saved_LDFLAGS $_flag"
AC_LINK_IFELSE([AC_LANG_PROGRAM()],
[AC_MSG_RESULT([yes]); AM_LDFLAGS="$AM_LDFLAGS $_flag"],
[AC_MSG_RESULT([no])])
done
LDFLAGS="$saved_LDFLAGS"
AS_IF([test "x$enable_coverage" = xyes],
[AM_CFLAGS="$AM_CFLAGS --coverage -O0"
AM_LDFLAGS="$AM_LDFLAGS --coverage"
@@ -401,7 +441,7 @@ AS_IF([test "x$PLATFORM" = xosx],
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\\\"\""
AM_CFLAGS="$AM_CFLAGS $CFLAGS"
LIBS="$glib_LIBS $gio_LIBS $PTHREAD_LIBS $curl_LIBS $libnotify_LIBS $python_LIBS ${GTK_LIBS} ${SQLITE_LIBS} $LIBS"
AC_SUBST(AM_LDFLAGS)
@@ -416,6 +456,7 @@ AC_OUTPUT
AC_MSG_NOTICE([Summary of build options:
PLATFORM : $target_os
PACKAGE_STATUS : $PACKAGE_STATUS
SQLite support : $with_sqlite
GTK_VERSION : $GTK_VERSION
LIBS : $LIBS
Install themes : $THEMES_INSTALL

View File

@@ -1,4 +1,4 @@
.TH man 1 "2023-08-03" "0.14.0" "Profanity XMPP client"
.TH man 1 "2026-03-26" "0.17.0" "Profanity XMPP client"
.SH NAME
Profanity \- a simple console based XMPP chat client.
.SH DESCRIPTION

View File

@@ -1,4 +1,4 @@
.TH man 1 "2025-03-27" "0.15.0" "Profanity XMPP client"
.TH man 1 "2026-03-23" "0.17.0" "Profanity XMPP client"
.SH NAME
Profanity \- a simple console based XMPP chat client.
.SH SYNOPSIS
@@ -181,7 +181,15 @@ Stands for Mutli-User Chats (XEP-0045) and are also called, groups, group chats,
The roster is your contact list. By default displayed at the right side on the console window. See RFC6121.
.TP
.BR XEP
XMPP is aa extendable protocol. There are core features and optional features described in XMPP Extension Protocols, short XEPs.
XMPP is an extendable protocol. There are core features and optional features described in XMPP Extension Protocols, short XEPs.
.PP
There are various kind of messages.
.TP
1:1 messages are regular messages from one party to another party, from one JID to the other JID. Often you have the other party added to your roster.
.TP
MUC messages are messages from one party to a group chat.
.TP
MUC PM (MUC private message) are messages from one party to another party which are in the same group chat. You might not have the JID of this person in your roster or don't know their JID at all. Communication is done over the MUC via the nick. Only the two parties can see the message. In the Android client Conversations this is displayed as "whispering". If a user leaves a MUC another user can join the MUC with the same nick unless the nick is registered. Which often isn't the case.
.SH SEE ALSO
.B Profanity
itself has a lot of built\-in help. Check the
@@ -197,7 +205,48 @@ Configuration for
.B Profanity
is stored in
.I $XDG_CONFIG_HOME/profanity/profrc
, details on commands for configuring Profanity can be found at <https://profanity-im.github.io/reference.html> or the respective built\-in help or man pages.
, details on commands for configuring Profanity can be found at <https://jabber.space/command-reference/> or the respective built\-in help or man pages.
.SS Message History Storage
By default, message history is stored in an SQLite database. An alternative flat-file backend
stores messages as human-readable plain text files that can be edited with any text editor.
.PP
To enable flat-file logging, set in
.IR profrc :
.PP
.EX
[logging]
dblog=flatfile
.EE
.PP
Or use the command:
.B /privacy logging flatfile
.PP
Flat-file logs are stored under
.IR $XDG_DATA_HOME/profanity/flatlog/ ,
organized as
.IR {account_jid}/{contact_jid}/history.log .
.PP
Each line has the format:
.br
.EX
{ISO8601} [{type}|{enc}|id:{id}|aid:{archive_id}|corrects:{id}] {sender}: {message}
.EE
.PP
Use
.B /history verify
to check integrity of stored history (both SQLite and flat-file).
.PP
Use
.B /history export [<jid>]
to copy messages from SQLite to flat-file format (merge with existing data), or
.B /history import [<jid>]
to copy from flat-file to SQLite.
Both operations skip duplicates.
Omit the JID to process all contacts.
.PP
Use
.B /history switch sqlite|flatfile
to change the active database backend at runtime without reconnecting.
.SH BUGS
Bugs can either be reported by raising an issue at the Github issue tracker:
.br
@@ -212,7 +261,7 @@ or to the mailing list at:
.br
.SH LICENSE
Copyright (C) 2012 \- 2019 James Booth <boothj5web@gmail.com>.
Copyright (C) 2019 \- 2025 Michael Vetter <jubalh@iodoru.org>.
Copyright (C) 2019 \- 2026 Michael Vetter <jubalh@iodoru.org>.
License GPLv3+: GNU GPL version 3 or later <https://www.gnu.org/licenses/gpl.html>
This is free software; you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.

793
meson.build Normal file
View File

@@ -0,0 +1,793 @@
project('profanity', 'c',
version: '0.17.0',
license: 'GPL-3.0-or-later',
meson_version: '>= 0.56.0',
default_options: [
'c_std=gnu99',
'warning_level=2',
'wrap_mode=nofallback',
]
)
# Determine platform
host_system = host_machine.system()
platform_map = {
'freebsd': 'freebsd',
'netbsd': 'netbsd',
'openbsd': 'openbsd',
'darwin': 'osx',
'cygwin': 'cygwin',
}
platform = platform_map.get(host_system, 'nix')
# Configuration data
conf_data = configuration_data()
conf_data.set_quoted('PACKAGE_NAME', meson.project_name())
conf_data.set_quoted('PACKAGE_VERSION', meson.project_version())
conf_data.set_quoted('PACKAGE_BUGREPORT', 'jubalh@iodoru.org')
conf_data.set_quoted('PACKAGE_URL', 'https://profanity-im.github.io/')
# Possible options from meson:
# debug, debugoptimized, release, plain
# Build type configuration
is_release = get_option('buildtype') == 'release'
is_debug = get_option('buildtype') in ['debug', 'debugoptimized']
conf_data.set_quoted('PACKAGE_STATUS', is_release ? 'release' : 'development')
if platform == 'cygwin'
conf_data.set('PLATFORM_CYGWIN', 1)
endif
if platform == 'osx'
conf_data.set('PLATFORM_OSX', 1)
endif
# Get git version if in development
if is_debug
git = find_program('git', required: false)
if git.found()
conf_data.set('HAVE_GIT_VERSION', 1)
endif
endif
# Compiler setup
cc = meson.get_compiler('c')
# Common compiler flags
add_project_arguments([
'-Wno-deprecated-declarations',
'-Wno-unused-parameter',
], language: 'c')
analyzer_args = []
if is_debug
add_project_arguments([
'-ggdb3',
'-Wunused',
'-Wall',
'-Wextra',
'-fstack-protector-strong',
'-D_FORTIFY_SOURCE=2',
'-Og',
'-Wpointer-arith'
], language: 'c')
if cc.get_id() == 'gcc'
analyzer_args += '-fanalyzer'
endif
endif
if cc.get_id() == 'clang'
add_project_arguments('-Qunused-arguments', language: 'c')
endif
if platform == 'osx'
# Check for homebrew paths on Apple Silicon
if run_command('test', '-d', '/opt/homebrew/include', check: false).returncode() == 0
add_project_arguments('-I/opt/homebrew/include', language: 'c')
add_project_link_arguments('-L/opt/homebrew/lib', language: 'c')
endif
endif
# Required dependencies
glib_dep = dependency('glib-2.0', version: '>= 2.62.0')
gio_dep = dependency('gio-2.0')
curl_dep = dependency('libcurl', version: '>= 7.62.0')
sqlite_dep = dependency('sqlite3', version: '>= 3.35.0')
thread_dep = dependency('threads')
math_dep = cc.find_library('m')
libstrophe_dep = dependency('libstrophe', version: '>= 0.12.3')
# Check for XMPP_CERT_PUBKEY_FINGERPRINT_SHA256 support
if libstrophe_dep.type_name() != 'internal'
if cc.links('''
#include <strophe.h>
int main() {
xmpp_tlscert_get_string(NULL, XMPP_CERT_PUBKEY_FINGERPRINT_SHA256);
return 1;
}
''', dependencies: libstrophe_dep)
conf_data.set('HAVE_XMPP_CERT_PUBKEY_FINGERPRINT_SHA256', 1)
endif
else
conf_data.set('HAVE_XMPP_CERT_PUBKEY_FINGERPRINT_SHA256', 1)
endif
# ncurses
curses_dep = dependency('ncursesw', required: false)
if not curses_dep.found()
curses_dep = dependency('ncurses', required: false)
endif
if not curses_dep.found()
curses_dep = cc.find_library('ncursesw', required: false)
endif
if not curses_dep.found()
curses_dep = cc.find_library('ncurses', required: true)
endif
# Check for ncursesw/ncurses.h, Arch Linux uses ncurses.h for ncursesw
if cc.has_header('ncursesw/ncurses.h')
conf_data.set('HAVE_NCURSESW_NCURSES_H', 1)
elif cc.has_header('ncurses.h')
conf_data.set('HAVE_NCURSES_H', 1)
endif
# Readline
readline_dep = dependency('readline', required: false)
if not readline_dep.found() and platform == 'osx'
brew = find_program('brew', required: false)
if brew.found()
prefix = run_command(brew, '--prefix', 'readline', check: false).stdout().strip()
readline_dep = cc.find_library('readline', dirs: [prefix / 'lib'], required: true)
add_project_arguments('-I' + prefix / 'include', language: 'c')
endif
endif
if not readline_dep.found() and platform == 'openbsd'
readline_dep = cc.find_library('ereadline', dirs: ['/usr/local/lib'], required: false)
endif
if not readline_dep.found()
readline_dep = cc.find_library('readline', required: true)
endif
# Optional dependencies
have_osxnotify = false
libnotify_dep = disabler()
gtk_dep = disabler()
xscrnsaver_dep = []
python_dep = disabler()
dl_dep = disabler()
gpgme_dep = disabler()
libotr_dep = disabler()
gdk_pixbuf_dep = disabler()
omemo_dep = disabler()
gcrypt_dep = disabler()
qrencode_dep = disabler()
enchant_dep = disabler()
# Dependencies for functional tests
stabber_dep = cc.find_library('stabber', required: false)
util_dep = cc.find_library('util', required: false)
# Build flags
build_python_api = false
build_c_api = false
build_pgp = false
build_otr = false
build_omemo = false
# Notifications
if get_option('notifications').enabled()
if platform == 'osx'
terminal_notifier = find_program('terminal-notifier', required: true)
have_osxnotify = true
conf_data.set('HAVE_OSXNOTIFY', 1)
elif platform in ['nix', 'freebsd']
libnotify_dep = dependency('libnotify', required: true)
conf_data.set('HAVE_LIBNOTIFY', 1)
else
error('Notifications not supported on this platform')
endif
endif
# GTK (for icons and clipboard)
x11_extra_dep = []
if get_option('icons-and-clipboard').enabled()
gtk_dep = dependency('gtk+-3.0', version: '>= 3.24.0', required: true)
conf_data.set('HAVE_GTK', 1)
x11_dep = dependency('x11', required: false)
if x11_dep.found()
if cc.has_function('XSetIOErrorExitHandler', dependencies: x11_dep)
conf_data.set('HAVE_XEXITHANDLER', 1)
x11_extra_dep = [x11_dep]
endif
endif
endif
# XScreenSaver
if get_option('xscreensaver').enabled()
xscrnsaver_dep = [
dependency('xscrnsaver', required: true),
dependency('x11', required: true)
]
conf_data.set('HAVE_LIBXSS', 1)
endif
# Python plugins
if get_option('python-plugins').enabled()
python_dep = dependency('python3-embed', required: false)
if not python_dep.found()
python_dep = dependency('python-embed', required: true)
else
conf_data.set('PY_IS_PYTHON3', 1)
endif
build_python_api = true
conf_data.set('HAVE_PYTHON', 1)
endif
# C plugins
if get_option('c-plugins').enabled()
if platform == 'cygwin'
error('C plugins are not supported on Cygwin')
endif
if platform not in ['openbsd', 'freebsd', 'netbsd']
dl_dep = cc.find_library('dl', required: true)
endif
build_c_api = true
conf_data.set('HAVE_C', 1)
endif
# PGP support
if get_option('pgp').enabled()
gpgme_dep = dependency('gpgme', required: false)
build_pgp = true
conf_data.set('HAVE_LIBGPGME', 1)
endif
# OTR support
if get_option('otr').enabled()
libotr_dep = dependency('libotr', version: '>= 4.0', required: true)
build_otr = true
conf_data.set('HAVE_LIBOTR', 1)
endif
# GDK Pixbuf (for avatar scaling)
if get_option('gdk-pixbuf').enabled()
gdk_pixbuf_dep = dependency('gdk-pixbuf-2.0', version: '>= 2.4', required: true)
conf_data.set('HAVE_PIXBUF', 1)
endif
# OMEMO support
if get_option('omemo').enabled()
omemo_backend = get_option('omemo-backend')
if omemo_backend == 'libsignal'
omemo_dep = dependency('libsignal-protocol-c', version: '>= 2.3.2', required: true)
elif omemo_backend == 'libomemo-c'
omemo_dep = dependency('libomemo-c', version: '>= 0.5.1', required: true)
conf_data.set('HAVE_LIBOMEMO_C', 1)
endif
gcrypt_dep = dependency('libgcrypt', version: '>= 1.7.0', required: true)
build_omemo = true
conf_data.set('HAVE_OMEMO', 1)
endif
# QR code support (for OMEMO)
if get_option('omemo-qrcode').enabled()
qrencode_dep = dependency('libqrencode', required: true)
conf_data.set('HAVE_QRENCODE', 1)
endif
# Spellcheck support
if get_option('spellcheck').enabled()
enchant_dep = dependency('enchant-2', required: true)
conf_data.set('HAVE_SPELLCHECK', 1)
endif
# Set installation paths
themes_path = get_option('themes_path')
if themes_path == ''
themes_path = get_option('datadir') / meson.project_name() / 'themes'
endif
icons_path = get_option('datadir') / meson.project_name() / 'icons'
global_python_plugins_path = get_option('datadir') / meson.project_name() / 'plugins'
global_c_plugins_path = get_option('libdir') / meson.project_name() / 'plugins'
conf_data.set_quoted('THEMES_PATH', get_option('prefix') / themes_path)
conf_data.set_quoted('ICONS_PATH', get_option('prefix') / icons_path)
conf_data.set_quoted('GLOBAL_PYTHON_PLUGINS_PATH', get_option('prefix') / global_python_plugins_path)
conf_data.set_quoted('GLOBAL_C_PLUGINS_PATH', get_option('prefix') / global_c_plugins_path)
# Check for required functions
foreach func : ['atexit', 'memset', 'strdup', 'strstr']
if cc.has_function(func)
conf_data.set('HAVE_' + func.to_upper(), 1)
endif
endforeach
# Generate config.h
config_h = configure_file(
output: 'config.h',
configuration: conf_data,
)
# Build dependencies list
profanity_deps = [
glib_dep,
gio_dep,
curl_dep,
sqlite_dep,
thread_dep,
math_dep,
libstrophe_dep,
curses_dep,
readline_dep,
]
# Add optional dependencies only if found
if libnotify_dep.found()
profanity_deps += libnotify_dep
endif
if gtk_dep.found()
profanity_deps += gtk_dep
endif
if xscrnsaver_dep.length() > 0
profanity_deps += xscrnsaver_dep
endif
if x11_extra_dep.length() > 0
profanity_deps += x11_extra_dep
endif
if python_dep.found()
profanity_deps += python_dep
endif
if dl_dep.found()
profanity_deps += dl_dep
endif
if gpgme_dep.found()
profanity_deps += gpgme_dep
endif
if libotr_dep.found()
profanity_deps += libotr_dep
endif
if gdk_pixbuf_dep.found()
profanity_deps += gdk_pixbuf_dep
endif
if omemo_dep.found()
profanity_deps += omemo_dep
endif
if gcrypt_dep.found()
profanity_deps += gcrypt_dep
endif
if qrencode_dep.found()
profanity_deps += qrencode_dep
endif
if enchant_dep.found()
profanity_deps += enchant_dep
endif
# Include directories
inc = include_directories('.', 'src')
# Core source files
core_sources = files(
'src/xmpp/contact.c',
'src/log.c',
'src/common.c',
'src/chatlog.c',
'src/database.c',
'src/profanity.c',
'src/xmpp/chat_session.c',
'src/xmpp/muc.c',
'src/xmpp/jid.c',
'src/xmpp/chat_state.c',
'src/xmpp/resource.c',
'src/xmpp/roster_list.c',
'src/xmpp/capabilities.c',
'src/xmpp/session.c',
'src/xmpp/connection.c',
'src/xmpp/iq.c',
'src/xmpp/message.c',
'src/xmpp/presence.c',
'src/xmpp/stanza.c',
'src/xmpp/roster.c',
'src/xmpp/bookmark.c',
'src/xmpp/blocking.c',
'src/xmpp/form.c',
'src/xmpp/avatar.c',
'src/xmpp/ox.c',
'src/xmpp/vcard.c',
'src/event/common.c',
'src/event/server_events.c',
'src/event/client_events.c',
'src/ui/window.c',
'src/ui/core.c',
'src/ui/titlebar.c',
'src/ui/statusbar.c',
'src/ui/inputwin.c',
'src/ui/screen.c',
'src/ui/console.c',
'src/ui/notifier.c',
'src/ui/window_list.c',
'src/ui/rosterwin.c',
'src/ui/occupantswin.c',
'src/ui/buffer.c',
'src/ui/chatwin.c',
'src/ui/mucwin.c',
'src/ui/privwin.c',
'src/ui/confwin.c',
'src/ui/xmlwin.c',
'src/ui/vcardwin.c',
'src/command/cmd_defs.c',
'src/command/cmd_funcs.c',
'src/command/cmd_ac.c',
'src/tools/parser.c',
'src/tools/http_common.c',
'src/tools/http_upload.c',
'src/tools/http_download.c',
'src/tools/plugin_download.c',
'src/tools/bookmark_ignore.c',
'src/tools/autocomplete.c',
'src/tools/clipboard.c',
'src/tools/editor.c',
'src/tools/spellcheck.c',
'src/config/files.c',
'src/config/conflists.c',
'src/config/accounts.c',
'src/config/tlscerts.c',
'src/config/account.c',
'src/config/preferences.c',
'src/config/theme.c',
'src/config/color.c',
'src/config/scripts.c',
'src/config/cafile.c',
'src/plugins/plugins.c',
'src/plugins/api.c',
'src/plugins/callbacks.c',
'src/plugins/autocompleters.c',
'src/plugins/themes.c',
'src/plugins/settings.c',
'src/plugins/disco.c',
)
# Build the final source list
profanity_sources = core_sources
# Add conditional sources
if gtk_dep.found()
profanity_sources += files('src/ui/tray.c')
endif
if build_python_api
profanity_sources += files(
'src/plugins/python_plugins.c',
'src/plugins/python_api.c',
)
endif
if build_c_api
profanity_sources += files(
'src/plugins/c_plugins.c',
'src/plugins/c_api.c',
)
endif
if build_pgp
profanity_sources += files(
'src/pgp/gpg.c',
'src/pgp/ox.c',
)
endif
if build_otr
profanity_sources += files(
'src/otr/otrlibv4.c',
'src/otr/otr.c',
)
endif
if build_omemo
profanity_sources += files(
'src/omemo/omemo.c',
'src/omemo/crypto.c',
'src/omemo/store.c',
'src/xmpp/omemo.c',
'src/tools/aesgcm_download.c',
)
endif
# Generate git version header if in development
if is_debug
git_branch = run_command('git', 'rev-parse', '--symbolic-full-name', '--abbrev-ref', 'HEAD',
check: false).stdout().strip()
git_revision = run_command('git', 'log', '--pretty=format:%h', '-n', '1',
check: false).stdout().strip()
git_version_conf = configuration_data()
git_version_conf.set('PROF_GIT_BRANCH', '"' + git_branch + '"')
git_version_conf.set('PROF_GIT_REVISION', '"' + git_revision + '"')
configure_file(
input: 'src/gitversion.h.in',
output: 'gitversion.h',
configuration: git_version_conf,
)
endif
# Build the executable
profanity_exe = executable(
'profanity',
profanity_sources,
files('src/main.c'),
config_h,
c_args: analyzer_args,
dependencies: profanity_deps,
include_directories: inc,
install: true,
export_dynamic: true,
)
# Build libprofanity shared library for C plugins
if build_c_api
libprofanity = shared_library(
'profanity',
'src/plugins/profapi.c',
dependencies: profanity_deps,
include_directories: inc,
install: true,
version: '0.0.0',
)
install_headers('src/plugins/profapi.h')
endif
# Install themes if enabled
if get_option('install_themes')
install_subdir('themes',
install_dir: get_option('datadir') / meson.project_name(),
strip_directory: false,
)
endif
# Install icons
install_subdir('icons',
install_dir: get_option('datadir') / meson.project_name(),
strip_directory: false,
)
# Install man pages
man_pages = run_command('find', 'docs', '-name', 'profanity*.1', '-type', 'f',
check: true).stdout().strip().split('\n')
if man_pages.length() > 0 and man_pages[0] != ''
install_man(man_pages)
endif
# Install example config and theme template
install_data(
'profrc.example',
'theme_template',
install_dir: get_option('datadir') / 'doc' / meson.project_name(),
)
# Tests
build_unittests = false
build_functionaltests = false
if get_option('tests')
cmocka_dep = dependency('cmocka', required: false)
if cmocka_dep.found()
build_unittests = true
unittest_sources = files(
'src/xmpp/contact.c',
'src/common.c',
'src/profanity.c',
'src/xmpp/chat_session.c',
'src/xmpp/muc.c',
'src/xmpp/jid.c',
'src/xmpp/resource.c',
'src/xmpp/chat_state.c',
'src/xmpp/roster_list.c',
'src/xmpp/form.c',
'src/command/cmd_defs.c',
'src/command/cmd_funcs.c',
'src/command/cmd_ac.c',
'src/tools/parser.c',
'src/tools/autocomplete.c',
'src/tools/clipboard.c',
'src/tools/editor.c',
'src/tools/spellcheck.c',
'src/tools/bookmark_ignore.c',
'src/config/account.c',
'src/config/files.c',
'src/config/tlscerts.c',
'src/config/preferences.c',
'src/config/theme.c',
'src/config/color.c',
'src/config/scripts.c',
'src/config/conflists.c',
'src/plugins/plugins.c',
'src/plugins/api.c',
'src/plugins/callbacks.c',
'src/plugins/autocompleters.c',
'src/plugins/themes.c',
'src/plugins/settings.c',
'src/plugins/disco.c',
'src/ui/window_list.c',
'src/event/common.c',
'src/event/server_events.c',
'src/event/client_events.c',
'src/ui/tray.c',
'tests/unittests/xmpp/stub_vcard.c',
'tests/unittests/xmpp/stub_avatar.c',
'tests/unittests/xmpp/stub_ox.c',
'tests/unittests/xmpp/stub_xmpp.c',
'tests/unittests/xmpp/stub_message.c',
'tests/unittests/ui/stub_ui.c',
'tests/unittests/ui/stub_vcardwin.c',
'tests/unittests/log/stub_log.c',
'tests/unittests/chatlog/stub_chatlog.c',
'tests/unittests/database/stub_database.c',
'tests/unittests/config/stub_accounts.c',
'tests/unittests/config/stub_cafile.c',
'tests/unittests/tools/stub_http_upload.c',
'tests/unittests/tools/stub_http_download.c',
'tests/unittests/tools/stub_aesgcm_download.c',
'tests/unittests/tools/stub_plugin_download.c',
'tests/unittests/helpers.c',
'tests/unittests/xmpp/test_form.c',
'tests/unittests/test_common.c',
'tests/unittests/tools/test_autocomplete.c',
'tests/unittests/xmpp/test_jid.c',
'tests/unittests/tools/test_parser.c',
'tests/unittests/xmpp/test_roster_list.c',
'tests/unittests/xmpp/test_chat_session.c',
'tests/unittests/xmpp/test_contact.c',
'tests/unittests/config/test_preferences.c',
'tests/unittests/event/test_server_events.c',
'tests/unittests/xmpp/test_muc.c',
'tests/unittests/command/test_cmd_presence.c',
'tests/unittests/command/test_cmd_alias.c',
'tests/unittests/command/test_cmd_connect.c',
'tests/unittests/command/test_cmd_rooms.c',
'tests/unittests/command/test_cmd_account.c',
'tests/unittests/command/test_cmd_sub.c',
'tests/unittests/command/test_cmd_bookmark.c',
'tests/unittests/command/test_cmd_otr.c',
'tests/unittests/command/test_cmd_pgp.c',
'tests/unittests/command/test_cmd_join.c',
'tests/unittests/command/test_cmd_roster.c',
'tests/unittests/command/test_cmd_ac.c',
'tests/unittests/command/test_cmd_disconnect.c',
'tests/unittests/plugins/test_callbacks.c',
'tests/unittests/plugins/test_plugins_disco.c',
'tests/unittests/unittests.c',
)
if build_python_api
unittest_sources += files(
'src/plugins/python_plugins.c',
'src/plugins/python_api.c',
)
endif
if build_c_api
unittest_sources += files(
'src/plugins/c_plugins.c',
'src/plugins/c_api.c',
)
endif
if build_pgp
unittest_sources += files(
'tests/unittests/pgp/stub_gpg.c',
'tests/unittests/pgp/stub_ox.c',
)
endif
if build_otr
unittest_sources += files('tests/unittests/otr/stub_otr.c')
endif
if build_omemo
unittest_sources += files('tests/unittests/omemo/stub_omemo.c')
endif
unittests = executable(
'unittests',
unittest_sources,
dependencies: profanity_deps + [cmocka_dep],
include_directories: [inc, include_directories('tests'), include_directories('tests/unittests')],
build_by_default: false,
)
test('unit tests', unittests)
# Functional tests
if stabber_dep.found() and util_dep.found()
build_functionaltests = true
functionaltest_sources = files(
'tests/functionaltests/proftest.c',
'tests/functionaltests/test_connect.c',
'tests/functionaltests/test_ping.c',
'tests/functionaltests/test_rooms.c',
'tests/functionaltests/test_presence.c',
'tests/functionaltests/test_message.c',
'tests/functionaltests/test_chat_session.c',
'tests/functionaltests/test_carbons.c',
'tests/functionaltests/test_receipts.c',
'tests/functionaltests/test_roster.c',
'tests/functionaltests/test_i18n.c',
'tests/functionaltests/test_software.c',
'tests/functionaltests/test_muc.c',
'tests/functionaltests/test_disconnect.c',
'tests/functionaltests/functionaltests.c',
)
functionaltests = executable(
'functionaltests',
functionaltest_sources,
dependencies: [cmocka_dep, stabber_dep, util_dep] + profanity_deps,
include_directories: [inc, include_directories('tests')],
build_by_default: false,
)
test('functional tests', functionaltests, timeout: 1800)
endif
else
warning('cmocka not found, tests will not be built')
endif
endif
lint_and_test = find_program('scripts/lint-and-test.sh', required: false)
if lint_and_test.found()
run_target('doublecheck',
command: [lint_and_test, '--fix-formatting', '--tests']
)
endif
summary({
'Platform': platform,
'Package status': get_option('buildtype'),
'Install themes': get_option('install_themes'),
'Themes path': themes_path,
'Icons path': icons_path,
'Global Python plugins path': global_python_plugins_path,
'Global C plugins path': global_c_plugins_path,
}, section: 'Directories')
summary({
'Notifications': libnotify_dep.found() or have_osxnotify,
'Python plugins': build_python_api,
'C plugins': build_c_api,
'OTR': build_otr,
'PGP': build_pgp,
'OMEMO': build_omemo,
'XScreenSaver': xscrnsaver_dep.length() > 0,
'GTK': gtk_dep.found(),
'GDK Pixbuf': gdk_pixbuf_dep.found(),
'QR Code': qrencode_dep.found(),
}, section: 'Features')
summary({
'Unit tests': build_unittests,
'Functional tests': build_functionaltests,
}, section: 'Testing')

98
meson_options.txt Normal file
View File

@@ -0,0 +1,98 @@
# Features:
option('notifications',
type: 'feature',
value: 'disabled',
description: 'Enable desktop notifications'
)
option('python-plugins',
type: 'feature',
value: 'disabled',
description: 'Enable Python plugins'
)
option('c-plugins',
type: 'feature',
value: 'disabled',
description: 'Enable C plugins'
)
option('otr',
type: 'feature',
value: 'disabled',
description: 'Enable OTR encryption'
)
option('pgp',
type: 'feature',
value: 'disabled',
description: 'Enable PGP'
)
option('omemo',
type: 'feature',
value: 'disabled',
description: 'Enable OMEMO encryption'
)
option('omemo-backend',
type: 'combo',
choices: ['libsignal', 'libomemo-c'],
value: 'libsignal',
description: 'Select OMEMO backend library'
)
option('xscreensaver',
type: 'feature',
value: 'disabled',
description: 'Use libXScrnSaver to determine idle time'
)
option('icons-and-clipboard',
type: 'feature',
value: 'disabled',
description: 'Enable GTK tray icons and clipboard paste support'
)
option('gdk-pixbuf',
type: 'feature',
value: 'disabled',
description: 'Enable GDK Pixbuf support to scale avatars before uploading'
)
option('omemo-qrcode',
type: 'feature',
value: 'disabled',
description: 'Enable ability to display OMEMO QR code'
)
option('spellcheck',
type: 'feature',
value: 'disabled',
description: 'Enable spellchecking support'
)
# Other:
option('python_framework',
type: 'string',
value: '',
description: 'Set base directory for Python Framework (macOS only)'
)
option('tests',
type: 'boolean',
value: false,
description: 'Build tests'
)
option('install_themes',
type: 'boolean',
value: true,
description: 'Install themes'
)
option('themes_path',
type: 'string',
value: '',
description: 'Custom path for themes installation (empty for default)'
)

View File

@@ -86,7 +86,7 @@
<xmpp:SupportedXep>
<xmpp:xep rdf:resource='https://xmpp.org/extensions/xep-0030.html'/>
<xmpp:status>complete</xmpp:status>
<xmpp:version>2.5rc3</xmpp:version>
<xmpp:version>2.5.0</xmpp:version>
<xmpp:since>0.4.5</xmpp:since>
<xmpp:note xml:lang='en'>Use /disco items and /disco info</xmpp:note>
</xmpp:SupportedXep>
@@ -97,7 +97,7 @@
<xmpp:SupportedXep>
<xmpp:xep rdf:resource='https://xmpp.org/extensions/xep-0045.html'/>
<xmpp:status>partial</xmpp:status>
<xmpp:version>?</xmpp:version>
<xmpp:version>1.35.2</xmpp:version>
<xmpp:since>0.4.5</xmpp:since>
<xmpp:note xml:lang='en'></xmpp:note>
</xmpp:SupportedXep>
@@ -141,7 +141,7 @@
<xmpp:SupportedXep>
<xmpp:xep rdf:resource='https://xmpp.org/extensions/xep-0054.html'/>
<xmpp:status>complete</xmpp:status>
<xmpp:version>1.2</xmpp:version>
<xmpp:version>1.3.0</xmpp:version>
<xmpp:since>0.14.0</xmpp:since>
</xmpp:SupportedXep>
</implements>
@@ -161,7 +161,7 @@
<xmpp:SupportedXep>
<xmpp:xep rdf:resource='https://xmpp.org/extensions/xep-0060.html'/>
<xmpp:status>complete</xmpp:status>
<xmpp:version>1.15.8</xmpp:version>
<xmpp:version>1.30.0</xmpp:version>
<xmpp:since>0.7.0</xmpp:since>
<xmpp:note xml:lang='en'>Used for OMEMO.</xmpp:note>
</xmpp:SupportedXep>
@@ -174,7 +174,6 @@
<xmpp:status>complete</xmpp:status>
<xmpp:version>2.4</xmpp:version>
<xmpp:since>0.11.0</xmpp:since>
<xmpp:note xml:lang='en'>Only password change. On master also to create an account.</xmpp:note>
</xmpp:SupportedXep>
</implements>
@@ -227,7 +226,7 @@
<xmpp:SupportedXep>
<xmpp:xep rdf:resource='https://xmpp.org/extensions/xep-0115.html'/>
<xmpp:status>complete</xmpp:status>
<xmpp:version>unknown</xmpp:version>
<xmpp:version>1.6.0</xmpp:version>
<xmpp:since>0.4.5</xmpp:since>
<xmpp:note xml:lang='en'></xmpp:note>
</xmpp:SupportedXep>
@@ -260,7 +259,7 @@
<xmpp:SupportedXep>
<xmpp:xep rdf:resource='https://xmpp.org/extensions/xep-0184.html'/>
<xmpp:status>complete</xmpp:status>
<xmpp:version>1.2</xmpp:version>
<xmpp:version>1.4.0</xmpp:version>
<xmpp:since>0.4.7</xmpp:since>
<xmpp:note xml:lang='en'></xmpp:note>
</xmpp:SupportedXep>
@@ -282,7 +281,7 @@
<xmpp:SupportedXep>
<xmpp:xep rdf:resource='https://xmpp.org/extensions/xep-0198.html'/>
<xmpp:status>complete</xmpp:status>
<xmpp:version>1.6</xmpp:version>
<xmpp:version>1.6.3</xmpp:version>
<xmpp:since>0.13.0</xmpp:since>
<xmpp:note xml:lang='en'></xmpp:note>
</xmpp:SupportedXep>
@@ -326,7 +325,7 @@
<xmpp:SupportedXep>
<xmpp:xep rdf:resource='https://xmpp.org/extensions/xep-0249.html'/>
<xmpp:status>complete</xmpp:status>
<xmpp:version>1.0</xmpp:version>
<xmpp:version>1.2</xmpp:version>
<xmpp:since>0.4.5</xmpp:since>
<xmpp:note xml:lang='en'></xmpp:note>
</xmpp:SupportedXep>
@@ -348,7 +347,7 @@
<xmpp:SupportedXep>
<xmpp:xep rdf:resource='https://xmpp.org/extensions/xep-0280.html'/>
<xmpp:status>complete</xmpp:status>
<xmpp:version>0.12.1</xmpp:version>
<xmpp:version>1.0.1</xmpp:version>
<xmpp:since>0.4.7</xmpp:since>
<xmpp:note xml:lang='en'></xmpp:note>
</xmpp:SupportedXep>
@@ -370,7 +369,7 @@
<xmpp:SupportedXep>
<xmpp:xep rdf:resource='https://xmpp.org/extensions/xep-0308.html'/>
<xmpp:status>complete</xmpp:status>
<xmpp:version>1.2.0</xmpp:version>
<xmpp:version>1.2.1</xmpp:version>
<xmpp:since>0.9.0</xmpp:since>
<xmpp:note xml:lang='en'></xmpp:note>
</xmpp:SupportedXep>
@@ -381,7 +380,7 @@
<xmpp:SupportedXep>
<xmpp:xep rdf:resource='https://xmpp.org/extensions/xep-0359.html'/>
<xmpp:status>complete</xmpp:status>
<xmpp:version>0.6.1</xmpp:version>
<xmpp:version>0.7.0</xmpp:version>
<xmpp:since>0.8.0</xmpp:since>
<xmpp:note xml:lang='en'></xmpp:note>
</xmpp:SupportedXep>
@@ -392,7 +391,7 @@
<xmpp:SupportedXep>
<xmpp:xep rdf:resource='https://xmpp.org/extensions/xep-0363.html'/>
<xmpp:status>complete</xmpp:status>
<xmpp:version>1.0.0</xmpp:version>
<xmpp:version>1.2.0</xmpp:version>
<xmpp:since>0.5.0</xmpp:since>
<xmpp:note xml:lang='en'></xmpp:note>
</xmpp:SupportedXep>
@@ -436,7 +435,7 @@
<xmpp:SupportedXep>
<xmpp:xep rdf:resource='https://xmpp.org/extensions/xep-0377.html'/>
<xmpp:status>complete</xmpp:status>
<xmpp:version>0.3</xmpp:version>
<xmpp:version>0.4.0</xmpp:version>
<xmpp:since>0.11.0</xmpp:since>
<xmpp:note xml:lang='en'></xmpp:note>
</xmpp:SupportedXep>
@@ -458,7 +457,7 @@
<xmpp:SupportedXep>
<xmpp:xep rdf:resource='https://xmpp.org/extensions/xep-0392.html'/>
<xmpp:status>complete</xmpp:status>
<xmpp:version>0.7.0</xmpp:version>
<xmpp:version>1.0.1</xmpp:version>
<xmpp:since>0.8.0</xmpp:since>
<xmpp:note xml:lang='en'></xmpp:note>
</xmpp:SupportedXep>
@@ -476,9 +475,24 @@
</implements>
<release>
<Version>
<revision>0.17.0</revision>
<created>2026-03-26</created>
<file-release rdf:resource='https://profanity-im.github.io/tarballs/profanity-0.17.0.tar.xz'/>
</Version>
<Version>
<revision>0.16.0</revision>
<created>2026-02-23</created>
<file-release rdf:resource='https://profanity-im.github.io/tarballs/profanity-0.16.0.tar.gz'/>
</Version>
<Version>
<revision>0.15.1</revision>
<created>2025-08-22</created>
<file-release rdf:resource='https://profanity-im.github.io/tarballs/profanity-0.15.1.tar.gz'/>
</Version>
<Version>
<revision>0.15.0</revision>
<created>2024-03-27</created>
<created>2025-03-27</created>
<file-release rdf:resource='https://profanity-im.github.io/tarballs/profanity-0.15.0.tar.gz'/>
</Version>
<Version>

View File

@@ -49,6 +49,8 @@ grlog=true
maxsize=1048580
rotate=true
shared=true
# Database backend: on (SQLite), off, redact, flatfile
dblog=on
[otr]
warn=true

35
scripts/README.md Normal file
View File

@@ -0,0 +1,35 @@
# Scripts
This directory contains various scripts used for development, CI, and maintenance.
## `lint-and-test.sh`
Central validation script mainly for local development.
- **Purpose:** Runs spelling checks, code formatting, and unit testing.
- **Usage:** `./scripts/lint-and-test.sh [options]`
- **Options:**
- `--fix-formatting`: Automatically apply `clang-format` fixes.
- `--tests`: Run unit tests using Meson.
- `--hook`: Run in "hook mode" (checks only staged files).
- `--install`: Install the script as a git `pre-commit` hook.
## `build-configuration-matrix.sh`
Exhaustive build configuration matrix testing. It ensures that Profanity compiles and passes unit tests across a variety of feature configurations for Meson.
- **Purpose:** Verifies architectural compatibility by testing many combinations of build flags.
- **Usage:** `./scripts/build-configuration-matrix.sh [extra-args]`
- **Extra Arguments:** Any arguments are forwarded directly to the configuration command (`meson setup`).
- **Environment:** Primarily used in CI (GitHub Actions), but can be run locally to verify all configurations.
## `changelog-helper.py`
Generates a sorted changelog from git commits since the last tag.
- **Purpose:** Automates the creation of release notes by parsing Conventional Commit messages.
- **Usage:** `python3 scripts/changelog-helper.py [--pr]`
- **Options:** Use `--pr` to append Pull Request numbers to each entry.
## `check-new-xeps.py`
Checks for updates to XMPP Extension Protocols (XEPs).
- **Purpose:** Compares the versions of XEPs implemented in Profanity (tracked in `profanity.doap`) against the latest versions available at `xmpp.org`.
- **Usage:** `python3 scripts/check-new-xeps.py`

View File

@@ -0,0 +1,113 @@
#!/usr/bin/env bash
# Exhaustive build configuration matrix testing for Profanity
set -e
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
log_content() {
echo
if [ -f "$1" ]; then
echo "Content of $1:"
cat "$1"
else
echo "Log file $1 not found."
fi
}
error_handler() {
ERR_CODE=$?
echo -e "${RED}Error ${ERR_CODE} with command '${BASH_COMMAND}' on line ${BASH_LINENO[0]}. Exiting.${NC}"
for log in build_run/meson-logs/testlog.txt build_valgrind/meson-logs/testlog.txt; do
if [ -f "$log" ]; then
echo "--- Meson Test Log ($log) ---"
cat "$log"
fi
done
exit ${ERR_CODE}
}
trap error_handler ERR
num_cores() {
nproc \
|| sysctl -n hw.ncpu \
|| getconf _NPROCESSORS_ONLN 2>/dev/null \
|| echo 2
}
usage() {
echo "Usage: $0 [extra-args]"
echo ""
echo "Run exhaustive build matrix tests."
echo ""
echo "Extra arguments are passed directly to: meson setup [extra-args]"
exit 0
}
if [[ "$1" == "--help" || "$1" == "-h" ]]; then
usage
fi
# Compatibility with old script that took autotools|meson as first arg
if [[ "$1" == "meson" ]]; then
shift
elif [[ "$1" == "autotools" ]]; then
echo -e "${RED}Error: Autotools is no longer supported.${NC}"
exit 1
fi
ARCH="$(uname | tr '[:upper:]' '[:lower:]')"
EXTRA_ARGS="$*"
# Build Matrix
echo -e "${YELLOW}---> Starting build matrix...${NC}"
tests=(
"-Dnotifications=enabled -Dicons-and-clipboard=enabled -Dotr=enabled -Dpgp=enabled -Domemo=enabled -Dc-plugins=enabled -Dpython-plugins=enabled -Dxscreensaver=enabled -Domemo-qrcode=enabled -Dgdk-pixbuf=enabled"
""
"-Dnotifications=disabled"
"-Dicons-and-clipboard=disabled"
"-Dotr=disabled"
"-Dpgp=disabled"
"-Domemo=disabled -Domemo-qrcode=disabled"
"-Dpgp=disabled -Dotr=disabled"
"-Dpgp=disabled -Dotr=disabled -Domemo=disabled"
"-Dpython-plugins=disabled"
"-Dc-plugins=disabled"
"-Dc-plugins=disabled -Dpython-plugins=disabled"
"-Dxscreensaver=disabled"
"-Dgdk-pixbuf=disabled"
)
BACKEND_OPT=""
if [ -n "${OMEMO_BACKEND}" ]; then
BACKEND_OPT="-Domemo-backend=${OMEMO_BACKEND}"
fi
# Valgrind check (Linux only)
if [[ "$ARCH" == linux* ]]; then
echo -e "${YELLOW}--> Running Valgrind check with full features ${BACKEND_OPT} ${EXTRA_ARGS}${NC}"
rm -rf build_valgrind
meson setup build_valgrind ${tests[0]} ${BACKEND_OPT} -Dtests=true -Db_sanitize=undefined ${EXTRA_ARGS}
meson compile -C build_valgrind
meson test -C build_valgrind "unit tests" --print-errorlogs --wrap=valgrind || echo "Valgrind issues detected"
rm -rf build_valgrind
fi
for features in "${tests[@]}"
do
echo -e "${YELLOW}--> Building with: ${features} ${BACKEND_OPT} ${EXTRA_ARGS}${NC}"
rm -rf build_run
meson setup build_run ${features} ${BACKEND_OPT} -Dtests=true ${EXTRA_ARGS}
meson compile -C build_run
meson test -C build_run "unit tests" --print-errorlogs
./build_run/profanity -v
done
echo -e "${GREEN}Build configuration matrix testing successful!${NC}"

165
scripts/changelog-helper.py Executable file
View File

@@ -0,0 +1,165 @@
#!/usr/bin/env python3
# Since we use Conventional Commits
# we can now create our Changelog semi-automatically
import subprocess
import sys
import re
import argparse
from collections import defaultdict
# Configuration for sections and their order
SECTION_CONFIG = [
("fix", "Bug Fixes"),
("feat", "Features"),
("build", "Build System"),
("ci", "CI"),
("docs", "Documentation"),
("perf", "Performance Improvements"),
("refactor", "Refactorings"),
("cleanup", "Cleanup"),
("style", "Style"),
("tests", "Tests"),
("chore", "Chores"),
]
SECTIONS = dict(SECTION_CONFIG)
TYPE_ORDER = [t for t, _ in SECTION_CONFIG]
CORRECTIONS = {
"ests": "tests",
"wleanup": "cleanup",
}
def git_run(args):
"""Run a git command and return stripped output lines."""
try:
result = subprocess.run(["git"] + args, capture_output=True, text=True, check=True)
return [line for line in result.stdout.strip().split('\n') if line]
except subprocess.CalledProcessError:
return []
def get_last_tag():
output = git_run(["describe", "--tags", "--abbrev=0"])
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"])
commits = []
for line in lines:
parts = line.split(' ', 1)
if len(parts) == 2:
commits.append(parts)
return commits
def get_pr_mappings(revision_range):
"""Map commit hashes to PR numbers found in merge commits."""
merge_commits = git_run(["log", revision_range, "--merges", "--format=%H %s"])
pr_map = {}
pr_re = re.compile(r'Merge pull request #(\d+)')
for line in merge_commits:
parts = line.split(' ', 1)
if len(parts) < 2:
continue
m_hash, m_subject = parts
match = pr_re.search(m_subject)
if match:
pr_num = match.group(1)
# Find all commits that are part of this merge branch
branch_commits = git_run(["rev-list", f"{m_hash}^1..{m_hash}^2"])
for b_hash in branch_commits:
pr_map[b_hash] = pr_num
return pr_map
def get_contributors(revision_range):
"""Get sorted list of all unique contributors."""
cmd = ["log", revision_range, "--format=%an%n%(trailers:key=Co-authored-by,valueonly=true)"]
output = git_run(cmd)
contributors = set()
for line in output:
# Remove email part if present: "Name <email@example.com>" -> "Name"
name = line.split('<')[0].strip()
if name:
contributors.add(name)
return sorted(list(contributors))
def format_description(description):
"""Capitalize first letter of description."""
description = description.strip()
if description and description[0].islower():
return description[0].upper() + description[1:]
return 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.")
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'}.")
return
pr_map = get_pr_mappings(revision_range) if args.pr else {}
# Conventional Commit regex: type(scope): description
commit_re = re.compile(r'^(\w+)(?:\(([^)]+)\))?:\s*(.*)$')
grouped = defaultdict(list)
others = []
for c_hash, c_subject 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 ""
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}")
else:
others.append(f"{c_subject}{pr_suffix}")
# Output sections in ordered priority
all_types = TYPE_ORDER + sorted([t for t in grouped if t not in TYPE_ORDER])
first = True
for ctype in all_types:
if ctype in grouped:
if not first:
print()
section_name = SECTIONS.get(ctype, ctype.capitalize())
print(f"{section_name}:")
for msg in sorted(grouped[ctype]):
print(f"- {msg}")
first = False
if others:
if not first:
print()
print("Others:")
for msg in sorted(others):
print(f"- {msg}")
# Contributors section
contributors = get_contributors(revision_range)
if contributors:
print("\nContributors:")
for i, name in enumerate(contributors, 1):
print(f"{i}. {name}")
if __name__ == "__main__":
main()

136
scripts/check-new-xeps.py Executable file
View File

@@ -0,0 +1,136 @@
#!/usr/bin/env python3
import os
import re
import sys
import urllib.request
import xml.etree.ElementTree as ET
from itertools import zip_longest
from typing import Dict, List, Optional
# Namespaces in DOAP
NS = {
"rdf": "http://www.w3.org/1999/02/22-rdf-syntax-ns#",
"foaf": "http://xmlns.com/foaf/0.1/",
"doap": "http://usefulinc.com/ns/doap#",
"xmpp": "https://linkmauve.fr/ns/xmpp-doap#",
"schema": "https://schema.org/",
}
def compare_versions(v1: str, v2: str) -> int:
"""
Compare two version strings.
Returns 1 if v2 > v1, -1 if v1 > v2, 0 if equal.
"""
def parse_v(v: str) -> List[int]:
return [int(x) for x in re.split(r"[^0-9]+", v) if x]
parts1 = parse_v(v1)
parts2 = parse_v(v2)
for p1, p2 in zip_longest(parts1, parts2, fillvalue=0):
if p2 > p1:
return 1
if p1 > p2:
return -1
return 0
def find_doap() -> Optional[str]:
"""Try to find profanity.doap in current or parent directory."""
candidates = ["profanity.doap", "../profanity.doap"]
for c in candidates:
if os.path.exists(c):
return c
return None
def get_implemented_xeps(doap_path: str) -> Dict[str, str]:
"""Parse DOAP file and return a map of XEP number to version."""
implemented_xeps: Dict[str, str] = {}
try:
tree = ET.parse(doap_path)
root = tree.getroot()
for implements in root.findall(".//doap:implements", NS):
supported_xep = implements.find(".//xmpp:SupportedXep", NS)
if supported_xep is not None:
xep_res = supported_xep.find(".//xmpp:xep", NS)
version_elem = supported_xep.find(".//xmpp:version", NS)
if xep_res is not None and version_elem is not None:
resource = xep_res.attrib.get(f"{{{NS['rdf']}}}resource", "")
match = re.search(r"xep-(\d+)\.html", resource)
if match:
xep_num = match.group(1)
current_version = version_elem.text.strip() if version_elem.text else ""
implemented_xeps[xep_num] = current_version
except (ET.ParseError, PermissionError) as e:
print(f"Error reading {doap_path}: {e}")
sys.exit(1)
return implemented_xeps
def check_xeps() -> None:
"""Main logic for checking XEP updates."""
doap_path = find_doap()
if not doap_path:
print("Error: Could not find DOAP file.")
sys.exit(1)
implemented_xeps = get_implemented_xeps(doap_path)
if not implemented_xeps:
print("No XEPs found in DOAP file.")
return
print(f"XEPs tracked: {len(implemented_xeps)}")
try:
url = "https://xmpp.org/extensions/xeplist.xml"
with urllib.request.urlopen(url, timeout=15) as response:
xeplist_xml = response.read()
except Exception as e:
print(f"Error fetching xeplist.xml: {e}")
return
try:
xeplist_tree = ET.fromstring(xeplist_xml)
except ET.ParseError as e:
print(f"Error parsing xeplist.xml: {e}")
return
print(f"\n{'XEP':<10} | {'Name':<35} | {'Ours':<8} | {'Latest':<8}")
print("-" * 80)
updates_found = 0
for xep_node in xeplist_tree.findall("xep"):
number_node = xep_node.find("number")
title_node = xep_node.find("title")
last_revision_node = xep_node.find("last-revision")
if number_node is None or title_node is None or last_revision_node is None:
continue
number = (number_node.text or "0").zfill(4)
name = title_node.text or "Unknown"
version_node = last_revision_node.find("version")
if version_node is None or version_node.text is None:
continue
latest_version = version_node.text
if number in implemented_xeps:
old_version = implemented_xeps[number]
if compare_versions(old_version, latest_version) > 0:
print(f"XEP-{number:<4} | {name[:35]:<35} | {old_version:<8} | {latest_version:<8}")
updates_found += 1
if updates_found == 0:
print("All tracked XEPs are up to date.")
if __name__ == "__main__":
check_xeps()

138
scripts/lint-and-test.sh Executable file
View File

@@ -0,0 +1,138 @@
#!/usr/bin/env bash
# Central linting and testing script for Profanity
set -e
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
# Go to project root
cd "$(dirname "$0")/.."
usage() {
echo "Usage: $0 [options]"
echo ""
echo "Description:"
echo " Code quality checks (spelling, formatting, and unit tests)."
echo " Works with meson and can be used as a git"
echo " pre-commit hook."
echo ""
echo "Options:"
echo " --fix-formatting Apply formatting fixes (default is check only)"
echo " --no-format Skip the formatting check/fix entirely. Use this if your local"
echo " clang-format version produces different results than the CI."
echo " Can also be set via SKIP_FORMAT=1 environment variable."
echo " --tests Run unit tests using Meson (meson test)"
echo " --hook Git hook mode: Checks only staged files for spelling and"
echo " formatting. Does not run tests to ensure fast commits."
echo " --install Install this script as a git pre-commit hook"
echo " --help Show this help message"
}
run_tests() {
local system=$1
echo -e "${YELLOW}---> Running unit tests...${NC}"
if [ "$system" == "meson" ]; then
echo "Using Meson..."
# Uses build_run
meson test -C build_run "unit tests"
fi
}
run_format() {
local mode=$1
local hook_mode=$2
local skip=$3
local files
if [ "$skip" = true ]; then
echo -e "${YELLOW}---> Skipping formatting check.${NC}"
return
fi
if [ "$hook_mode" = true ]; then
echo -e "${YELLOW}---> Checking staged files for formatting...${NC}"
# Only added/modified C files
files=$(git diff --cached --name-only --diff-filter=ACMR | grep -E "\.(c|h)$" || true)
else
echo -e "${YELLOW}---> Checking all files for formatting...${NC}"
files=$(find src tests -name "*.[ch]" || true)
fi
if [ -z "$files" ]; then
echo "No C files to check."
return
fi
if [ "$mode" = "fix" ]; then
clang-format -i $files
echo -e "${GREEN}Formatting applied.${NC}"
else
# --Werror makes it return non-zero on diff
if ! clang-format --dry-run --Werror $files; then
echo -e "${RED}Error: Style violations found. Run '$0 --fix-formatting' to resolve.${NC}"
echo -e "${RED}If this is due to a clang-format version mismatch, use --no-format or SKIP_FORMAT=1.${NC}"
exit 1
fi
echo -e "${GREEN}Style check passed.${NC}"
fi
}
run_spell() {
echo -e "${YELLOW}---> Running spell check...${NC}"
if command -v codespell >/dev/null 2>&1; then
codespell
echo -e "${GREEN}Spell check passed.${NC}"
else
echo -e "${YELLOW}Warning: codespell not found, skipping.${NC}"
fi
}
MODE="check"
BUILD_SYSTEM="none"
HOOK=false
# Support environment variable override
if [[ "$SKIP_FORMAT" == "1" || "$SKIP_FORMAT" == "true" ]]; then
INTERNAL_SKIP_FORMAT=true
else
INTERNAL_SKIP_FORMAT=false
fi
while [[ "$#" -gt 0 ]]; do
case $1 in
--fix-formatting) MODE="fix" ;;
--no-format) INTERNAL_SKIP_FORMAT=true ;;
--tests) BUILD_SYSTEM="meson" ;;
--hook) HOOK=true ;;
--install)
echo "Installing pre-commit hook..."
mkdir -p .git/hooks
echo "#!/usr/bin/env bash" > .git/hooks/pre-commit
echo "./scripts/lint-and-test.sh --hook" >> .git/hooks/pre-commit
chmod +x .git/hooks/pre-commit
echo "Hook installed."
exit 0
;;
--help) usage; exit 0 ;;
*) echo "Unknown parameter: $1"; usage; exit 1 ;;
esac
shift
done
# Always run spell check
run_spell
# Run format check unless skipped
run_format "$MODE" "$HOOK" "$INTERNAL_SKIP_FORMAT"
# Run tests if a build system was specified
if [ "$BUILD_SYSTEM" != "none" ]; then
run_tests "$BUILD_SYSTEM"
fi
echo -e "${GREEN}Validation successful!${NC}"

1685
src/ai/ai_client.c Normal file

File diff suppressed because it is too large Load Diff

291
src/ai/ai_client.h Normal file
View File

@@ -0,0 +1,291 @@
#ifndef AI_CLIENT_H
#define AI_CLIENT_H
#include <glib.h>
/**
* @brief AI message structure for conversation history.
*/
typedef struct ai_message_t
{
gchar* role; /* "user" or "assistant" */
gchar* content; /* Message content */
} AIMessage;
/**
* @brief AI provider configuration.
*/
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 */
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;
/**
* @brief AI chat session structure.
*/
typedef struct ai_session_t
{
pthread_mutex_t lock; /* Protects all session fields below */
gchar* provider_name; /* Provider name */
AIProvider* provider; /* Provider configuration */
gchar* model; /* Model identifier (e.g., "gpt-4", "sonar") */
gchar* api_key; /* API key for this session */
GList* history; /* Conversation history (GList of AIMessage*) */
gint32 ref_count; /* Reference count (atomic) */
} AISession;
/* ========================================================================
* Provider Management
* ======================================================================== */
/**
* Initialize the AI client and load default providers.
*/
void ai_client_init(void);
/**
* Shutdown the AI client and free resources.
*/
void ai_client_shutdown(void);
/**
* Get a provider by name.
* @param name The provider name (e.g., "openai", "perplexity")
* @return AIProvider*, or NULL if not found
*/
AIProvider* ai_get_provider(const gchar* name);
/**
* Add or update a provider configuration.
* @param name The provider name
* @param api_url The API endpoint URL
* @return New AIProvider* (caller must unref when done)
*/
AIProvider* ai_add_provider(const gchar* name, const gchar* api_url);
/**
* Remove a provider by name.
* @param name The provider name
* @return TRUE if provider was removed, FALSE if not found
*/
gboolean ai_remove_provider(const gchar* name);
/**
* Increment the reference count of a provider.
* @param provider The provider to reference
* @return The same provider pointer
*/
AIProvider* ai_provider_ref(AIProvider* provider);
/**
* Decrement the reference count of a provider.
* @param provider The provider to unreference
*/
void ai_provider_unref(AIProvider* provider);
/**
* List all configured providers.
* @return GList of AIProvider* (caller must not free the list or providers,
* and must not unref the returned providers)
*/
GList* ai_list_providers(void);
/**
* Find a provider name for autocomplete.
* @param search_str The search string
* @param previous Whether to go to previous match
* @param context Unused
* @return Provider name, or NULL if not found
*/
gchar* ai_providers_find(const char* const search_str, gboolean previous, void* context);
/**
* Reset the provider autocomplete state.
* Called from cmd_ac_reset() to clear autocomplete state.
*/
void ai_providers_reset_ac(void);
/**
* Get the API key for a provider.
* @param provider_name The provider name
* @return The API key, or NULL if not set (caller must free)
*/
gchar* ai_get_provider_key(const gchar* provider_name);
/**
* Set the API key for a provider.
* @param provider_name The provider name
* @param api_key The API key to set
*/
void ai_set_provider_key(const gchar* provider_name, const gchar* api_key);
/**
* Set the default model for a provider.
* @param provider_name The provider name
* @param model The default model name
*/
void ai_set_provider_default_model(const gchar* provider_name, const gchar* model);
/**
* Get the default model for a provider.
* @param provider_name The provider name
* @return The default model name (caller must not free), or NULL if not set
*/
const gchar* ai_get_provider_default_model(const gchar* provider_name);
/**
* Set a custom setting for a provider.
* @param provider_name The provider name
* @param setting The setting key (e.g., "tools", "search")
* @param value The setting value
*/
void ai_set_provider_setting(const gchar* provider_name, const gchar* setting, const gchar* value);
/**
* Get a custom setting for a provider.
* @param provider_name The provider name
* @param setting The setting key
* @return The setting value (caller must free), or NULL if not set
*/
gchar* ai_get_provider_setting(const gchar* provider_name, const gchar* setting);
/**
* Fetch available models from a provider's API.
* @param provider_name The provider name
* @param user_data User data (ProfAiWin* for UI display)
* @return TRUE if the request was successfully queued, FALSE otherwise
*/
gboolean ai_fetch_models(const gchar* provider_name, gpointer user_data);
/**
* Check if models cache is fresh for a provider.
* @param provider_name The provider name
* @return TRUE if models are fresh, FALSE otherwise
*/
gboolean ai_models_are_fresh(const gchar* provider_name);
/* ========================================================================
* Parsing helpers (exposed for testing)
* ======================================================================== */
/**
* Parse model IDs from an OpenAI-compatible API response.
* Expected format: {"object":"list","data":[{"id":"model1",...},...]}
* @param provider The provider to add models to
* @param json The JSON response from the API
*/
void ai_parse_models_from_json(AIProvider* provider, const gchar* json);
/**
* 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)
*/
gchar* ai_parse_response(const gchar* response_json);
/**
* Extract human-readable error message from an API error envelope.
* Expected format: {"error":{"message":"...","type":"...","code":...}}
* Decodes \" \n \\ \t escapes.
* @param error_json The JSON body of the error response
* @return Newly allocated error message, or NULL if not parseable (caller frees)
*/
gchar* ai_parse_error_message(const gchar* error_json);
/* ========================================================================
* Session Management
* ======================================================================== */
/**
* Create a new AI session with the specified provider and model.
* @param provider_name The provider name (e.g., "openai")
* @param model The model identifier (e.g., "gpt-4")
* @return New AISession*, or NULL on failure
*/
AISession* ai_session_create(const gchar* provider_name, const gchar* model);
/**
* Increment the reference count of an AI session.
* @param session The session to reference
* @return The same session pointer
*/
AISession* ai_session_ref(AISession* session);
/**
* Decrement the reference count and free the session when it reaches zero.
* @param session The session to unreference
*/
void ai_session_unref(AISession* session);
/**
* Add a message to the session history.
* @param session The session
* @param role The message role ("user" or "assistant")
* @param content The message content
*/
void ai_session_add_message(AISession* session, const gchar* role, const gchar* content);
/**
* Clear the conversation history.
* @param session The session
*/
void ai_session_clear_history(AISession* session);
/**
* Get the current model for a session.
* @param session The session
* @return The model name (caller must not free)
*/
const gchar* ai_session_get_model(AISession* session);
/**
* Set the model for a session.
* @param session The session
* @param model The model name
*/
void ai_session_set_model(AISession* session, const gchar* model);
/**
* Atomically switch session provider, model, and API key.
* All mutations happen under the session lock to prevent races with
* _ai_request_thread() which snapshots session state before making requests.
*
* @param session The session
* @param provider_name New provider name
* @param model New model identifier
* @param api_key New API key (caller must free after calling this)
*/
void ai_session_switch(AISession* session, const gchar* provider_name,
const gchar* model, gchar* api_key);
/* ========================================================================
* Request Handling
* ======================================================================== */
/**
* Send a prompt to the AI provider asynchronously.
* @param session The AI session containing provider and model
* @param prompt The prompt to send
* @param user_data User data (ProfAiWin* for UI display)
* @return TRUE if the request was successfully queued, FALSE otherwise
*/
gboolean ai_send_prompt(AISession* session, const gchar* prompt,
gpointer user_data);
/**
* Escape a string for JSON embedding.
* @param str The string to escape
* @return Newly allocated escaped string (caller must free)
*/
gchar* ai_json_escape(const gchar* str);
#endif /* AI_CLIENT_H */

View File

@@ -1,5 +1,5 @@
/*
* log.c
* chatlog.c
* vim: expandtab:ts=4:sts=4:sw=4
*
* Copyright (C) 2012 - 2019 James Booth <boothj5@gmail.com>
@@ -36,442 +36,81 @@
#include "config.h"
#include <assert.h>
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include "glib.h"
#include "glib/gstdio.h"
#include "log.h"
#include "chatlog.h"
#include "common.h"
#include "config/files.h"
#include "config/preferences.h"
#include "xmpp/xmpp.h"
#include "xmpp/muc.h"
static GHashTable* logs;
static GHashTable* groupchat_logs;
struct dated_chat_log
{
gchar* filename;
GDateTime* date;
};
static gboolean _log_roll_needed(struct dated_chat_log* dated_log);
static struct dated_chat_log* _create_chatlog(const char* const other, const char* const login);
static struct dated_chat_log* _create_groupchat_log(const char* const room, const char* const login);
static void _free_chat_log(struct dated_chat_log* dated_log);
static gboolean _key_equals(void* key1, void* key2);
static void _chat_log_chat(const char* const login, const char* const other, const gchar* const msg,
chat_log_direction_t direction, GDateTime* timestamp, const char* const resourcepart);
static void _groupchat_log_chat(const gchar* const login, const gchar* const room, const gchar* const nick,
const gchar* const msg);
void
_chatlog_close(void)
{
g_hash_table_destroy(logs);
g_hash_table_destroy(groupchat_logs);
}
/*
* Background message-logging to per-day plain-text files under
* $XDG_DATA_HOME/profanity/chatlogs/ is disabled. These files were never
* read back by the application — history replay reads from the database
* (log_database_get_previous_chat) — so this subsystem only ever wrote to
* disk for users to read with external tools.
*
* All public entry points are kept as no-ops to avoid touching the ~20
* call sites in event/, otr/ and profanity.c until the API itself is
* removed.
*/
void
chatlog_init(void)
{
log_info("Initialising chat logs");
prof_add_shutdown_routine(_chatlog_close);
logs = g_hash_table_new_full(g_str_hash, (GEqualFunc)_key_equals, free,
(GDestroyNotify)_free_chat_log);
groupchat_logs = g_hash_table_new_full(g_str_hash, (GEqualFunc)_key_equals, free,
(GDestroyNotify)_free_chat_log);
}
void
chat_log_msg_out(const char* const barejid, const char* const msg, const char* const resource)
{
if (prefs_get_boolean(PREF_CHLOG)) {
_chat_log_chat(connection_get_barejid(), barejid, msg, PROF_OUT_LOG, NULL, resource);
}
}
void
chat_log_otr_msg_out(const char* const barejid, const char* const msg, const char* const resource)
{
if (prefs_get_boolean(PREF_CHLOG)) {
const char* mybarejid = connection_get_barejid();
auto_gchar gchar* pref_otr_log = prefs_get_string(PREF_OTR_LOG);
if (strcmp(pref_otr_log, "on") == 0) {
_chat_log_chat(mybarejid, barejid, msg, PROF_OUT_LOG, NULL, resource);
} else if (strcmp(pref_otr_log, "redact") == 0) {
_chat_log_chat(mybarejid, barejid, "[redacted]", PROF_OUT_LOG, NULL, resource);
}
}
}
void
chat_log_pgp_msg_out(const char* const barejid, const char* const msg, const char* const resource)
{
if (prefs_get_boolean(PREF_CHLOG)) {
const char* mybarejid = connection_get_barejid();
auto_gchar gchar* pref_pgp_log = prefs_get_string(PREF_PGP_LOG);
if (strcmp(pref_pgp_log, "on") == 0) {
_chat_log_chat(mybarejid, barejid, msg, PROF_OUT_LOG, NULL, resource);
} else if (strcmp(pref_pgp_log, "redact") == 0) {
_chat_log_chat(mybarejid, barejid, "[redacted]", PROF_OUT_LOG, NULL, resource);
}
}
}
void
chat_log_omemo_msg_out(const char* const barejid, const char* const msg, const char* const resource)
{
if (prefs_get_boolean(PREF_CHLOG)) {
const char* mybarejid = connection_get_barejid();
auto_gchar gchar* pref_omemo_log = prefs_get_string(PREF_OMEMO_LOG);
if (strcmp(pref_omemo_log, "on") == 0) {
_chat_log_chat(mybarejid, barejid, msg, PROF_OUT_LOG, NULL, resource);
} else if (strcmp(pref_omemo_log, "redact") == 0) {
_chat_log_chat(mybarejid, barejid, "[redacted]", PROF_OUT_LOG, NULL, resource);
}
}
}
void
chat_log_otr_msg_in(ProfMessage* message)
{
if (prefs_get_boolean(PREF_CHLOG)) {
const char* mybarejid = connection_get_barejid();
auto_gchar gchar* pref_otr_log = prefs_get_string(PREF_OTR_LOG);
if (message->enc == PROF_MSG_ENC_NONE || (strcmp(pref_otr_log, "on") == 0)) {
if (message->type == PROF_MSG_TYPE_MUCPM) {
_chat_log_chat(mybarejid, message->from_jid->barejid, message->plain, PROF_IN_LOG, message->timestamp, message->from_jid->resourcepart);
} else {
_chat_log_chat(mybarejid, message->from_jid->barejid, message->plain, PROF_IN_LOG, message->timestamp, NULL);
}
} else if (strcmp(pref_otr_log, "redact") == 0) {
if (message->type == PROF_MSG_TYPE_MUCPM) {
_chat_log_chat(mybarejid, message->from_jid->barejid, "[redacted]", PROF_IN_LOG, message->timestamp, message->from_jid->resourcepart);
} else {
_chat_log_chat(mybarejid, message->from_jid->barejid, "[redacted]", PROF_IN_LOG, message->timestamp, NULL);
}
}
}
}
void
chat_log_pgp_msg_in(ProfMessage* message)
{
if (prefs_get_boolean(PREF_CHLOG)) {
const char* mybarejid = connection_get_barejid();
auto_gchar gchar* pref_pgp_log = prefs_get_string(PREF_PGP_LOG);
if (strcmp(pref_pgp_log, "on") == 0) {
if (message->type == PROF_MSG_TYPE_MUCPM) {
_chat_log_chat(mybarejid, message->from_jid->barejid, message->plain, PROF_IN_LOG, message->timestamp, message->from_jid->resourcepart);
} else {
_chat_log_chat(mybarejid, message->from_jid->barejid, message->plain, PROF_IN_LOG, message->timestamp, NULL);
}
} else if (strcmp(pref_pgp_log, "redact") == 0) {
if (message->type == PROF_MSG_TYPE_MUCPM) {
_chat_log_chat(mybarejid, message->from_jid->barejid, "[redacted]", PROF_IN_LOG, message->timestamp, message->from_jid->resourcepart);
} else {
_chat_log_chat(mybarejid, message->from_jid->barejid, "[redacted]", PROF_IN_LOG, message->timestamp, NULL);
}
}
}
}
void
chat_log_omemo_msg_in(ProfMessage* message)
{
if (prefs_get_boolean(PREF_CHLOG)) {
const char* mybarejid = connection_get_barejid();
auto_gchar gchar* pref_omemo_log = prefs_get_string(PREF_OMEMO_LOG);
if (strcmp(pref_omemo_log, "on") == 0) {
if (message->type == PROF_MSG_TYPE_MUCPM) {
_chat_log_chat(mybarejid, message->from_jid->barejid, message->plain, PROF_IN_LOG, message->timestamp, message->from_jid->resourcepart);
} else {
_chat_log_chat(mybarejid, message->from_jid->barejid, message->plain, PROF_IN_LOG, message->timestamp, NULL);
}
} else if (strcmp(pref_omemo_log, "redact") == 0) {
if (message->type == PROF_MSG_TYPE_MUCPM) {
_chat_log_chat(mybarejid, message->from_jid->barejid, "[redacted]", PROF_IN_LOG, message->timestamp, message->from_jid->resourcepart);
} else {
_chat_log_chat(mybarejid, message->from_jid->barejid, "[redacted]", PROF_IN_LOG, message->timestamp, message->from_jid->resourcepart);
}
}
}
}
void
chat_log_msg_in(ProfMessage* message)
{
if (prefs_get_boolean(PREF_CHLOG)) {
const char* mybarejid = connection_get_barejid();
if (message->type == PROF_MSG_TYPE_MUCPM) {
_chat_log_chat(mybarejid, message->from_jid->barejid, message->plain, PROF_IN_LOG, message->timestamp, message->from_jid->resourcepart);
} else {
_chat_log_chat(mybarejid, message->from_jid->barejid, message->plain, PROF_IN_LOG, message->timestamp, NULL);
}
}
}
static void
_chat_log_chat(const char* const login, const char* const other, const char* msg,
chat_log_direction_t direction, GDateTime* timestamp, const char* const resourcepart)
void
chat_log_otr_msg_in(ProfMessage* message)
{
auto_gchar gchar* pref_dblog = prefs_get_string(PREF_DBLOG);
if (g_strcmp0(pref_dblog, "redact") == 0) {
msg = "[REDACTED]";
}
char* other_name;
GString* other_str = NULL;
}
if (resourcepart) {
other_str = g_string_new(other);
g_string_append(other_str, "_");
g_string_append(other_str, resourcepart);
void
chat_log_pgp_msg_in(ProfMessage* message)
{
}
other_name = other_str->str;
} else {
other_name = (char*)other;
}
struct dated_chat_log* dated_log = g_hash_table_lookup(logs, other_name);
// no log for user
if (dated_log == NULL) {
dated_log = _create_chatlog(other_name, login);
g_hash_table_insert(logs, strdup(other_name), dated_log);
// log entry exists but file removed
} else if (!g_file_test(dated_log->filename, G_FILE_TEST_EXISTS)) {
dated_log = _create_chatlog(other_name, login);
g_hash_table_replace(logs, strdup(other_name), dated_log);
// log file needs rolling
} else if (_log_roll_needed(dated_log)) {
dated_log = _create_chatlog(other_name, login);
g_hash_table_replace(logs, strdup(other_name), dated_log);
}
if (resourcepart) {
g_string_free(other_str, TRUE);
}
if (timestamp == NULL) {
timestamp = g_date_time_new_now_local();
} else {
g_date_time_ref(timestamp);
}
auto_gchar gchar* date_fmt = g_date_time_format_iso8601(timestamp);
FILE* chatlogp = fopen(dated_log->filename, "a");
g_chmod(dated_log->filename, S_IRUSR | S_IWUSR);
if (chatlogp) {
if (direction == PROF_IN_LOG) {
if (strncmp(msg, "/me ", 4) == 0) {
if (resourcepart) {
fprintf(chatlogp, "%s - *%s %s\n", date_fmt, resourcepart, msg + 4);
} else {
fprintf(chatlogp, "%s - *%s %s\n", date_fmt, other, msg + 4);
}
} else {
if (resourcepart) {
fprintf(chatlogp, "%s - %s: %s\n", date_fmt, resourcepart, msg);
} else {
fprintf(chatlogp, "%s - %s: %s\n", date_fmt, other, msg);
}
}
} else {
if (strncmp(msg, "/me ", 4) == 0) {
fprintf(chatlogp, "%s - *me %s\n", date_fmt, msg + 4);
} else {
fprintf(chatlogp, "%s - me: %s\n", date_fmt, msg);
}
}
fflush(chatlogp);
int result = fclose(chatlogp);
if (result == EOF) {
log_error("Error closing file %s, errno = %d", dated_log->filename, errno);
}
}
g_date_time_unref(timestamp);
void
chat_log_omemo_msg_in(ProfMessage* message)
{
}
void
groupchat_log_msg_out(const gchar* const room, const gchar* const msg)
{
if (prefs_get_boolean(PREF_GRLOG)) {
const char* const mynick = muc_nick(room);
_groupchat_log_chat(connection_get_barejid(), room, mynick, msg);
}
}
void
groupchat_log_msg_in(const gchar* const room, const gchar* const nick, const gchar* const msg)
{
if (prefs_get_boolean(PREF_GRLOG)) {
_groupchat_log_chat(connection_get_barejid(), room, nick, msg);
}
}
void
groupchat_log_omemo_msg_out(const gchar* const room, const gchar* const msg)
{
if (prefs_get_boolean(PREF_CHLOG)) {
const char* mybarejid = connection_get_barejid();
auto_gchar gchar* pref_omemo_log = prefs_get_string(PREF_OMEMO_LOG);
const char* const mynick = muc_nick(room);
if (strcmp(pref_omemo_log, "on") == 0) {
_groupchat_log_chat(mybarejid, room, mynick, msg);
} else if (strcmp(pref_omemo_log, "redact") == 0) {
_groupchat_log_chat(mybarejid, room, mynick, "[redacted]");
}
}
}
void
groupchat_log_omemo_msg_in(const gchar* const room, const gchar* const nick, const gchar* const msg)
{
if (prefs_get_boolean(PREF_CHLOG)) {
const char* mybarejid = connection_get_barejid();
auto_gchar gchar* pref_omemo_log = prefs_get_string(PREF_OMEMO_LOG);
if (strcmp(pref_omemo_log, "on") == 0) {
_groupchat_log_chat(mybarejid, room, nick, msg);
} else if (strcmp(pref_omemo_log, "redact") == 0) {
_groupchat_log_chat(mybarejid, room, nick, "[redacted]");
}
}
}
void
_groupchat_log_chat(const gchar* const login, const gchar* const room, const gchar* const nick,
const gchar* const msg)
{
struct dated_chat_log* dated_log = g_hash_table_lookup(groupchat_logs, room);
// no log for room
if (dated_log == NULL) {
dated_log = _create_groupchat_log(room, login);
g_hash_table_insert(groupchat_logs, strdup(room), dated_log);
// log exists but needs rolling
} else if (_log_roll_needed(dated_log)) {
dated_log = _create_groupchat_log(room, login);
g_hash_table_replace(logs, strdup(room), dated_log);
}
GDateTime* dt_tmp = g_date_time_new_now_local();
auto_gchar gchar* date_fmt = g_date_time_format_iso8601(dt_tmp);
FILE* grpchatlogp = fopen(dated_log->filename, "a");
g_chmod(dated_log->filename, S_IRUSR | S_IWUSR);
if (grpchatlogp) {
if (strncmp(msg, "/me ", 4) == 0) {
fprintf(grpchatlogp, "%s - *%s %s\n", date_fmt, nick, msg + 4);
} else {
fprintf(grpchatlogp, "%s - %s: %s\n", date_fmt, nick, msg);
}
fflush(grpchatlogp);
int result = fclose(grpchatlogp);
if (result == EOF) {
log_error("Error closing file %s, errno = %d", dated_log->filename, errno);
}
}
g_date_time_unref(dt_tmp);
}
static char*
_get_log_filename(const char* const other, const char* const login, GDateTime* dt, gboolean is_room)
{
auto_gchar gchar* chatlogs_dir = files_file_in_account_data_path(DIR_CHATLOGS, login, is_room ? "rooms" : NULL);
auto_gchar gchar* logfile_name = g_date_time_format(dt, "%Y_%m_%d.log");
auto_char char* other_ = str_replace(other, "@", "_at_");
auto_gchar gchar* logs_path = g_strdup_printf("%s/%s", chatlogs_dir, other_);
gchar* logfile_path = NULL;
if (create_dir(logs_path)) {
logfile_path = g_strdup_printf("%s/%s", logs_path, logfile_name);
}
return logfile_path;
}
static struct dated_chat_log*
_create_chatlog(const char* const other, const char* const login)
{
GDateTime* now = g_date_time_new_now_local();
auto_char char* filename = _get_log_filename(other, login, now, FALSE);
struct dated_chat_log* new_log = malloc(sizeof(struct dated_chat_log));
new_log->filename = strdup(filename);
new_log->date = now;
return new_log;
}
static struct dated_chat_log*
_create_groupchat_log(const char* const room, const char* const login)
{
GDateTime* now = g_date_time_new_now_local();
auto_char char* filename = _get_log_filename(room, login, now, TRUE);
struct dated_chat_log* new_log = malloc(sizeof(struct dated_chat_log));
new_log->filename = strdup(filename);
new_log->date = now;
return new_log;
}
static gboolean
_log_roll_needed(struct dated_chat_log* dated_log)
{
gboolean result = FALSE;
GDateTime* now = g_date_time_new_now_local();
if (g_date_time_get_day_of_year(dated_log->date) != g_date_time_get_day_of_year(now)) {
result = TRUE;
}
g_date_time_unref(now);
return result;
}
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;
}
if (dated_log->date) {
g_date_time_unref(dated_log->date);
dated_log->date = NULL;
}
free(dated_log);
}
}
static gboolean
_key_equals(void* key1, void* key2)
{
gchar* str1 = (gchar*)key1;
gchar* str2 = (gchar*)key2;
return (g_strcmp0(str1, str2) == 0);
}

View File

@@ -4,33 +4,7 @@
*
* Copyright (C) 2012 - 2019 James Booth <boothj5@gmail.com>
*
* This file is part of Profanity.
*
* Profanity is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Profanity is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Profanity. If not, see <https://www.gnu.org/licenses/>.
*
* In addition, as a special exception, the copyright holders give permission to
* link the code of portions of this program with the OpenSSL library under
* certain conditions as described in each individual source file, and
* distribute linked combinations including the two.
*
* You must obey the GNU General Public License in all respects for all of the
* code used other than OpenSSL. If you modify file(s) with this exception, you
* may extend this exception to your version of the file(s), but you are not
* obligated to do so. If you do not wish to do so, delete this exception
* statement from your version. If you delete this exception statement from all
* source files in the program, then also delete it here.
*
* SPDX-License-Identifier: GPL-3.0-or-later WITH OpenSSL-exception
*/
#ifndef CHATLOG_H

View File

@@ -3,35 +3,9 @@
* vim: expandtab:ts=4:sts=4:sw=4
*
* Copyright (C) 2012 - 2019 James Booth <boothj5@gmail.com>
* Copyright (C) 2019 - 2025 Michael Vetter <jubalh@iodoru.org>
*
* This file is part of Profanity.
*
* Profanity is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Profanity is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Profanity. If not, see <https://www.gnu.org/licenses/>.
*
* In addition, as a special exception, the copyright holders give permission to
* link the code of portions of this program with the OpenSSL library under
* certain conditions as described in each individual source file, and
* distribute linked combinations including the two.
*
* You must obey the GNU General Public License in all respects for all of the
* code used other than OpenSSL. If you modify file(s) with this exception, you
* may extend this exception to your version of the file(s), but you are not
* obligated to do so. If you do not wish to do so, delete this exception
* statement from your version. If you delete this exception statement from all
* source files in the program, then also delete it here.
* Copyright (C) 2019 - 2026 Michael Vetter <jubalh@iodoru.org>
*
* SPDX-License-Identifier: GPL-3.0-or-later WITH OpenSSL-exception
*/
#include "config.h"
@@ -66,9 +40,12 @@
#include "omemo/omemo.h"
#endif
#include "ai/ai_client.h"
static char* _sub_autocomplete(ProfWin* window, const char* const input, gboolean previous);
static char* _notify_autocomplete(ProfWin* window, const char* const input, gboolean previous);
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* _autoconnect_autocomplete(ProfWin* window, const char* const input, gboolean previous);
static char* _account_autocomplete(ProfWin* window, const char* const input, gboolean previous);
@@ -134,9 +111,11 @@ static char* _lastactivity_autocomplete(ProfWin* window, const char* const input
static char* _intype_autocomplete(ProfWin* window, const char* const input, gboolean previous);
static char* _mood_autocomplete(ProfWin* window, const char* const input, gboolean previous);
static char* _strophe_autocomplete(ProfWin* window, const char* const input, gboolean previous);
static char* _stamp_autocomplete(ProfWin* window, const char* const input, gboolean previous);
static char* _adhoc_cmd_autocomplete(ProfWin* window, const char* const input, gboolean previous);
static char* _vcard_autocomplete(ProfWin* window, const char* const input, gboolean previous);
static char* _force_encryption_autocomplete(ProfWin* window, const char* const input, gboolean previous);
static char* _ai_autocomplete(ProfWin* window, const char* const input, gboolean previous);
static char* _script_autocomplete_func(const char* const prefix, gboolean previous, void* context);
@@ -164,6 +143,7 @@ static Autocomplete autoconnect_ac;
static Autocomplete wintitle_ac;
static Autocomplete theme_ac;
static Autocomplete theme_load_ac;
static Autocomplete spellcheck_ac;
static Autocomplete account_ac;
static Autocomplete account_set_ac;
static Autocomplete account_clear_ac;
@@ -275,18 +255,27 @@ static Autocomplete logging_ac;
static Autocomplete logging_group_ac;
static Autocomplete privacy_ac;
static Autocomplete privacy_log_ac;
static Autocomplete history_ac;
static Autocomplete history_switch_ac;
static Autocomplete color_ac;
static Autocomplete correction_ac;
static Autocomplete avatar_ac;
static Autocomplete ai_subcommands_ac;
static Autocomplete ai_set_subcommands_ac;
static Autocomplete ai_set_custom_subcommands_ac;
static Autocomplete ai_remove_subcommands_ac;
static Autocomplete url_ac;
static Autocomplete executable_ac;
static Autocomplete executable_param_ac;
static Autocomplete intype_ac;
static Autocomplete mood_ac;
static Autocomplete ai_models_ac;
static Autocomplete mood_type_ac;
static Autocomplete strophe_ac;
static Autocomplete strophe_sm_ac;
static Autocomplete strophe_verbosity_ac;
static Autocomplete stamp_ac;
static Autocomplete stamp_unset_ac;
static Autocomplete adhoc_cmd_ac;
static Autocomplete lastactivity_ac;
static Autocomplete vcard_ac;
@@ -300,6 +289,8 @@ static Autocomplete vcard_address_type_ac;
static Autocomplete force_encryption_ac;
static Autocomplete force_encryption_policy_ac;
static char* last_filepath_input = NULL;
static Autocomplete* all_acs[] = {
&commands_ac,
&who_room_ac,
@@ -322,6 +313,7 @@ static Autocomplete* all_acs[] = {
&autoconnect_ac,
&wintitle_ac,
&theme_ac,
&spellcheck_ac,
&account_ac,
&account_set_ac,
&account_clear_ac,
@@ -429,6 +421,8 @@ static Autocomplete* all_acs[] = {
&logging_group_ac,
&privacy_ac,
&privacy_log_ac,
&history_ac,
&history_switch_ac,
&color_ac,
&correction_ac,
&avatar_ac,
@@ -441,6 +435,8 @@ static Autocomplete* all_acs[] = {
&strophe_ac,
&strophe_sm_ac,
&strophe_verbosity_ac,
&stamp_ac,
&stamp_unset_ac,
&adhoc_cmd_ac,
&lastactivity_ac,
&vcard_ac,
@@ -452,7 +448,12 @@ static Autocomplete* all_acs[] = {
&vcard_togglable_param_ac,
&vcard_address_type_ac,
&force_encryption_ac,
&force_encryption_policy_ac
&force_encryption_policy_ac,
&ai_subcommands_ac,
&ai_set_subcommands_ac,
&ai_set_custom_subcommands_ac,
&ai_remove_subcommands_ac,
&ai_models_ac
};
static GHashTable* ac_funcs = NULL;
@@ -587,6 +588,11 @@ cmd_ac_init(void)
autocomplete_add(theme_ac, "colours");
autocomplete_add(theme_ac, "properties");
autocomplete_add(spellcheck_ac, "on");
autocomplete_add(spellcheck_ac, "off");
autocomplete_add(spellcheck_ac, "list");
autocomplete_add(spellcheck_ac, "lang");
autocomplete_add(disco_ac, "info");
autocomplete_add(disco_ac, "items");
@@ -858,8 +864,9 @@ cmd_ac_init(void)
autocomplete_add(tls_property_ac, "force");
autocomplete_add(tls_property_ac, "allow");
autocomplete_add(tls_property_ac, "trust");
autocomplete_add(tls_property_ac, "legacy");
autocomplete_add(tls_property_ac, "direct");
autocomplete_add(tls_property_ac, "disable");
autocomplete_add(tls_property_ac, "legacy");
autocomplete_add(auth_property_ac, "default");
autocomplete_add(auth_property_ac, "legacy");
@@ -1115,6 +1122,7 @@ cmd_ac_init(void)
autocomplete_add(statusbar_show_ac, "name");
autocomplete_add(statusbar_show_ac, "number");
autocomplete_add(statusbar_show_ac, "read");
autocomplete_add(statusbar_show_ac, "dbbackend");
autocomplete_add(statusbar_tabmode_ac, "actlist");
autocomplete_add(statusbar_tabmode_ac, "dynamic");
@@ -1129,15 +1137,24 @@ cmd_ac_init(void)
autocomplete_add(status_state_ac, "xa");
autocomplete_add(status_state_ac, "dnd");
autocomplete_add(logging_ac, "chat");
autocomplete_add(logging_ac, "group");
autocomplete_add(privacy_ac, "logging");
autocomplete_add(privacy_ac, "os");
autocomplete_add(privacy_log_ac, "on");
autocomplete_add(privacy_log_ac, "off");
autocomplete_add(privacy_log_ac, "redact");
autocomplete_add(privacy_log_ac, "flatfile");
autocomplete_add(history_ac, "on");
autocomplete_add(history_ac, "off");
autocomplete_add(history_ac, "backend");
autocomplete_add(history_ac, "switch");
autocomplete_add(history_ac, "verify");
autocomplete_add(history_ac, "export");
autocomplete_add(history_ac, "import");
autocomplete_add(history_switch_ac, "sqlite");
autocomplete_add(history_switch_ac, "flatfile");
autocomplete_add(logging_group_ac, "on");
autocomplete_add(logging_group_ac, "off");
@@ -1153,6 +1170,26 @@ cmd_ac_init(void)
autocomplete_add(correction_ac, "off");
autocomplete_add(correction_ac, "char");
autocomplete_add_unsorted(ai_subcommands_ac, "set", FALSE);
autocomplete_add_unsorted(ai_subcommands_ac, "remove", FALSE);
autocomplete_add_unsorted(ai_subcommands_ac, "start", FALSE);
autocomplete_add_unsorted(ai_subcommands_ac, "clear", FALSE);
autocomplete_add_unsorted(ai_subcommands_ac, "providers", FALSE);
autocomplete_add_unsorted(ai_subcommands_ac, "switch", FALSE);
autocomplete_add_unsorted(ai_subcommands_ac, "models", FALSE);
autocomplete_add_unsorted(ai_set_subcommands_ac, "provider", FALSE);
autocomplete_add_unsorted(ai_set_subcommands_ac, "token", FALSE);
autocomplete_add_unsorted(ai_set_subcommands_ac, "default-model", FALSE);
autocomplete_add_unsorted(ai_set_subcommands_ac, "custom", FALSE);
autocomplete_add_unsorted(ai_set_custom_subcommands_ac, "tools", FALSE);
autocomplete_add_unsorted(ai_set_custom_subcommands_ac, "search", FALSE);
autocomplete_add_unsorted(ai_set_custom_subcommands_ac, "memory", FALSE);
autocomplete_add_unsorted(ai_set_custom_subcommands_ac, "plugins", FALSE);
autocomplete_add_unsorted(ai_remove_subcommands_ac, "provider", FALSE);
autocomplete_add(avatar_ac, "set");
autocomplete_add(avatar_ac, "disable");
autocomplete_add(avatar_ac, "get");
@@ -1185,6 +1222,13 @@ cmd_ac_init(void)
autocomplete_add(strophe_verbosity_ac, "2");
autocomplete_add(strophe_verbosity_ac, "3");
autocomplete_add(stamp_ac, "outgoing");
autocomplete_add(stamp_ac, "incoming");
autocomplete_add(stamp_ac, "unset");
autocomplete_add(stamp_unset_ac, "outgoing");
autocomplete_add(stamp_unset_ac, "incoming");
autocomplete_add(mood_ac, "set");
autocomplete_add(mood_ac, "clear");
autocomplete_add(mood_ac, "on");
@@ -1414,6 +1458,7 @@ cmd_ac_init(void)
g_hash_table_insert(ac_funcs, "/status", _status_autocomplete);
g_hash_table_insert(ac_funcs, "/statusbar", _statusbar_autocomplete);
g_hash_table_insert(ac_funcs, "/strophe", _strophe_autocomplete);
g_hash_table_insert(ac_funcs, "/stamp", _stamp_autocomplete);
g_hash_table_insert(ac_funcs, "/sub", _sub_autocomplete);
g_hash_table_insert(ac_funcs, "/subject", _subject_autocomplete);
g_hash_table_insert(ac_funcs, "/theme", _theme_autocomplete);
@@ -1428,6 +1473,7 @@ cmd_ac_init(void)
g_hash_table_insert(ac_funcs, "/wins", _wins_autocomplete);
g_hash_table_insert(ac_funcs, "/wintitle", _wintitle_autocomplete);
g_hash_table_insert(ac_funcs, "/force-encryption", _force_encryption_autocomplete);
g_hash_table_insert(ac_funcs, "/ai", _ai_autocomplete);
}
void
@@ -1557,6 +1603,10 @@ cmd_ac_remove_form_fields(DataForm* form)
char*
cmd_ac_complete(ProfWin* window, const char* const input, gboolean previous)
{
if (!input) {
return NULL;
}
char* found = NULL;
// autocomplete command
if ((strncmp(input, "/", 1) == 0) && (!strchr(input, ' '))) {
@@ -1646,6 +1696,7 @@ cmd_ac_reset(ProfWin* window)
win_reset_search_attempts();
win_close_reset_search_attempts();
plugins_reset_autocomplete();
ai_providers_reset_ac();
}
void
@@ -1660,65 +1711,74 @@ cmd_ac_uninit(void)
autocomplete_free(plugins_unload_ac);
autocomplete_free(plugins_reload_ac);
autocomplete_free(script_show_ac);
g_free(last_filepath_input);
last_filepath_input = NULL;
g_hash_table_destroy(ac_funcs);
ac_funcs = NULL;
}
static void
_filepath_item_free(char** ptr)
_filepath_item_free(gchar** ptr)
{
char* item = *ptr;
free(item);
gchar* item = *ptr;
g_free(item);
}
char*
cmd_ac_complete_filepath(const char* const input, char* const startstr, gboolean previous)
{
unsigned int output_off = 0;
char* tmp = NULL;
// strip command
char* inpcp = (char*)input + strlen(startstr);
while (*inpcp == ' ') {
inpcp++;
char* inpcp_ptr = (char*)input + strlen(startstr);
while (*inpcp_ptr == ' ') {
inpcp_ptr++;
}
inpcp = strdup(inpcp);
// strip quotes
if (*inpcp == '"') {
tmp = strrchr(inpcp + 1, '"');
if (tmp) {
*tmp = '\0';
}
tmp = strdup(inpcp + 1);
free(inpcp);
inpcp = tmp;
tmp = NULL;
}
// expand ~ to $HOME
if (inpcp[0] == '~' && inpcp[1] == '/') {
char* home = getenv("HOME");
if (!home) {
free(inpcp);
return NULL;
}
tmp = g_strdup_printf("%s/%sfoo", home, inpcp + 2);
output_off = strlen(home) + 1;
} else {
tmp = g_strdup_printf("%sfoo", inpcp);
}
free(inpcp);
if (!tmp) {
auto_gchar gchar* inpcp = g_strdup(inpcp_ptr);
if (!inpcp) {
return NULL;
}
char* foofile = strdup(basename(tmp));
char* directory = strdup(dirname(tmp));
g_free(tmp);
// strip quotes
if (inpcp[0] == '"') {
char* last_quote = strrchr(inpcp + 1, '"');
if (last_quote) {
*last_quote = '\0';
}
gchar* unquoted = g_strdup(inpcp + 1);
g_free(inpcp);
inpcp = unquoted;
}
GArray* files = g_array_new(TRUE, FALSE, sizeof(char*));
auto_gchar gchar* expanded = get_expanded_path(inpcp);
if (!expanded) {
return NULL;
}
auto_gchar gchar* foofile = g_path_get_basename(expanded);
auto_gchar gchar* directory = g_path_get_dirname(expanded);
// If the input is already a known completion, don't update to allow cycling
if (last_filepath_input && (g_strcmp0(inpcp, last_filepath_input) == 0 || autocomplete_contains(filepath_ac, inpcp))) {
return autocomplete_param_with_ac(input, startstr, filepath_ac, TRUE, previous);
}
// If the input ends with a slash, the basename will be "." or "/"
// In that case, we are looking for all files in that directory
gboolean find_all = FALSE;
size_t inpcp_len = strlen(inpcp);
if (inpcp_len == 0 || inpcp[inpcp_len - 1] == '/') {
find_all = TRUE;
}
char* last_slash = strrchr(inpcp, '/');
auto_gchar gchar* user_dir_part = NULL;
if (last_slash) {
user_dir_part = g_strndup(inpcp, last_slash - inpcp + 1);
} else {
user_dir_part = g_strdup("");
}
GArray* files = g_array_new(TRUE, FALSE, sizeof(gchar*));
g_array_set_clear_func(files, (GDestroyNotify)_filepath_item_free);
DIR* d = opendir(directory);
@@ -1726,47 +1786,48 @@ cmd_ac_complete_filepath(const char* const input, char* const startstr, gboolean
struct dirent* dir;
while ((dir = readdir(d)) != NULL) {
if (strcmp(dir->d_name, ".") == 0) {
continue;
} else if (strcmp(dir->d_name, "..") == 0) {
continue;
} else if (*(dir->d_name) == '.' && *foofile != '.') {
// only show hidden files on explicit request
if (strcmp(dir->d_name, ".") == 0 || strcmp(dir->d_name, "..") == 0) {
continue;
}
char* acstring = NULL;
if (output_off) {
tmp = g_strdup_printf("%s/%s", directory, dir->d_name);
if (tmp) {
acstring = g_strdup_printf("~/%s", tmp + output_off);
g_free(tmp);
}
} else if (strcmp(directory, "/") == 0) {
acstring = g_strdup_printf("/%s", dir->d_name);
} else {
acstring = g_strdup_printf("%s/%s", directory, dir->d_name);
}
if (!acstring) {
g_array_free(files, TRUE);
free(foofile);
free(directory);
return NULL;
// check if it matches prefix
if (!find_all && !g_str_has_prefix(dir->d_name, foofile)) {
continue;
}
g_array_append_val(files, acstring);
// only show hidden files on explicit request
if (dir->d_name[0] == '.' && (find_all || foofile[0] != '.')) {
continue;
}
gchar* acstring = g_strdup_printf("%s%s", user_dir_part, dir->d_name);
if (acstring) {
g_array_append_val(files, acstring);
}
}
closedir(d);
}
free(directory);
free(foofile);
autocomplete_update(filepath_ac, (char**)files->data);
g_array_free(files, TRUE);
g_free(last_filepath_input);
last_filepath_input = g_strdup(inpcp);
return autocomplete_param_with_ac(input, startstr, filepath_ac, TRUE, previous);
}
static char*
_spellcheck_autocomplete(ProfWin* window, const char* const input, gboolean previous)
{
char* result = NULL;
result = autocomplete_param_with_ac(input, "/spellcheck", spellcheck_ac, TRUE, previous);
return result;
}
static char*
_cmd_ac_complete_params(ProfWin* window, const char* const input, gboolean previous)
{
@@ -1776,16 +1837,39 @@ _cmd_ac_complete_params(ProfWin* window, const char* const input, gboolean previ
// autocomplete boolean settings
gchar* boolean_choices[] = { "/beep", "/states", "/outtype", "/flash", "/splash",
"/history", "/vercheck", "/privileges", "/wrap",
"/vercheck", "/privileges", "/wrap",
"/carbons", "/slashguard", "/mam", "/silence" };
for (int i = 0; i < ARRAY_SIZE(boolean_choices); i++) {
for (size_t i = 0; i < ARRAY_SIZE(boolean_choices); i++) {
result = autocomplete_param_with_func(input, boolean_choices[i], prefs_autocomplete_boolean_choice, previous, NULL);
if (result) {
return result;
}
}
result = autocomplete_param_with_ac(input, "/history switch", history_switch_ac, TRUE, previous);
if (result) {
return result;
}
gchar* history_jid_subcmds[] = { "/history verify", "/history export", "/history import" };
for (size_t i = 0; i < ARRAY_SIZE(history_jid_subcmds); i++) {
result = autocomplete_param_with_func(input, history_jid_subcmds[i], roster_barejid_autocomplete, previous, NULL);
if (result) {
return result;
}
}
result = autocomplete_param_with_ac(input, "/history", history_ac, TRUE, previous);
if (result) {
return result;
}
result = _spellcheck_autocomplete(window, input, previous);
if (result) {
return result;
}
// autocomplete nickname in chat rooms
if (window->type == WIN_MUC) {
ProfMucWin* mucwin = (ProfMucWin*)window;
@@ -1796,7 +1880,7 @@ _cmd_ac_complete_params(ProfWin* window, const char* const input, gboolean previ
// Remove quote character before and after names when doing autocomplete
char* unquoted = strip_arg_quotes(input);
for (int i = 0; i < ARRAY_SIZE(nick_choices); i++) {
for (size_t i = 0; i < ARRAY_SIZE(nick_choices); i++) {
result = autocomplete_param_with_ac(unquoted, nick_choices[i], nick_ac, TRUE, previous);
if (result) {
free(unquoted);
@@ -1811,7 +1895,7 @@ _cmd_ac_complete_params(ProfWin* window, const char* const input, gboolean previ
gchar* contact_choices[] = { "/msg", "/info" };
// Remove quote character before and after names when doing autocomplete
char* unquoted = strip_arg_quotes(input);
for (int i = 0; i < ARRAY_SIZE(contact_choices); i++) {
for (size_t i = 0; i < ARRAY_SIZE(contact_choices); i++) {
result = autocomplete_param_with_func(unquoted, contact_choices[i], roster_contact_autocomplete, previous, NULL);
if (result) {
free(unquoted);
@@ -1826,7 +1910,7 @@ _cmd_ac_complete_params(ProfWin* window, const char* const input, gboolean previ
free(unquoted);
gchar* resource_choices[] = { "/caps", "/ping" };
for (int i = 0; i < ARRAY_SIZE(resource_choices); i++) {
for (size_t i = 0; i < ARRAY_SIZE(resource_choices); i++) {
result = autocomplete_param_with_func(input, resource_choices[i], roster_fulljid_autocomplete, previous, NULL);
if (result) {
return result;
@@ -1835,7 +1919,7 @@ _cmd_ac_complete_params(ProfWin* window, const char* const input, gboolean previ
}
gchar* invite_choices[] = { "/join" };
for (int i = 0; i < ARRAY_SIZE(invite_choices); i++) {
for (size_t i = 0; i < ARRAY_SIZE(invite_choices); i++) {
result = autocomplete_param_with_func(input, invite_choices[i], muc_invites_find, previous, NULL);
if (result) {
return result;
@@ -1855,16 +1939,16 @@ _cmd_ac_complete_params(ProfWin* window, const char* const input, gboolean previ
{ "/inputwin", winpos_ac },
};
for (int i = 0; i < ARRAY_SIZE(ac_cmds); i++) {
for (size_t i = 0; i < ARRAY_SIZE(ac_cmds); i++) {
result = autocomplete_param_with_ac(input, ac_cmds[i].cmd, ac_cmds[i].completer, TRUE, previous);
if (result) {
return result;
}
}
int len = strlen(input);
size_t len = strlen(input);
char parsed[len + 1];
int i = 0;
size_t i = 0;
while (i < len) {
if (input[i] == ' ') {
break;
@@ -1875,7 +1959,8 @@ _cmd_ac_complete_params(ProfWin* window, const char* const input, gboolean previ
}
parsed[i] = '\0';
char* (*ac_func)(ProfWin*, const char* const, gboolean) = g_hash_table_lookup(ac_funcs, parsed);
char* (*ac_func)(ProfWin*, const char* const, gboolean) = (char* (*)(ProfWin*, const char* const, gboolean))g_hash_table_lookup(ac_funcs, parsed);
if (ac_func) {
result = ac_func(window, input, previous);
if (result) {
@@ -1944,7 +2029,7 @@ _who_autocomplete(ProfWin* window, const char* const input, gboolean previous)
"/who chat", "/who away", "/who xa", "/who dnd", "/who available",
"/who unavailable" };
for (int i = 0; i < ARRAY_SIZE(group_commands); i++) {
for (size_t i = 0; i < ARRAY_SIZE(group_commands); i++) {
result = autocomplete_param_with_func(input, group_commands[i], roster_group_autocomplete, previous, NULL);
if (result) {
return result;
@@ -2266,7 +2351,7 @@ _notify_autocomplete(ProfWin* window, const char* const input, gboolean previous
gchar* boolean_choices1[] = { "/notify room current", "/notify chat current", "/notify typing current",
"/notify room text", "/notify chat text", "/notify room offline" };
for (int i = 0; i < ARRAY_SIZE(boolean_choices1); i++) {
for (size_t i = 0; i < ARRAY_SIZE(boolean_choices1); i++) {
result = autocomplete_param_with_func(input, boolean_choices1[i], prefs_autocomplete_boolean_choice, previous, NULL);
if (result) {
return result;
@@ -2299,7 +2384,7 @@ _notify_autocomplete(ProfWin* window, const char* const input, gboolean previous
}
gchar* boolean_choices2[] = { "/notify invite", "/notify sub", "/notify mention", "/notify trigger" };
for (int i = 0; i < ARRAY_SIZE(boolean_choices2); i++) {
for (size_t i = 0; i < ARRAY_SIZE(boolean_choices2); i++) {
result = autocomplete_param_with_func(input, boolean_choices2[i], prefs_autocomplete_boolean_choice, previous, NULL);
if (result) {
return result;
@@ -3745,7 +3830,7 @@ _account_autocomplete(ProfWin* window, const char* const input, gboolean previou
"/account disable", "/account rename", "/account clear", "/account remove",
"/account default set" };
for (int i = 0; i < ARRAY_SIZE(account_choice); i++) {
for (size_t i = 0; i < ARRAY_SIZE(account_choice); i++) {
found = autocomplete_param_with_func(input, account_choice[i], accounts_find_all, previous, NULL);
if (found) {
return found;
@@ -4010,11 +4095,6 @@ _logging_autocomplete(ProfWin* window, const char* const input, gboolean previou
return result;
}
result = autocomplete_param_with_func(input, "/logging chat", prefs_autocomplete_boolean_choice, previous, NULL);
if (result) {
return result;
}
result = autocomplete_param_with_ac(input, "/logging group", logging_group_ac, TRUE, previous);
return result;
}
@@ -4262,6 +4342,20 @@ _strophe_autocomplete(ProfWin* window, const char* const input, gboolean previou
return autocomplete_param_with_ac(input, "/strophe", strophe_ac, FALSE, previous);
}
static char*
_stamp_autocomplete(ProfWin* window, const char* const input, gboolean previous)
{
char* result = NULL;
result = autocomplete_param_with_ac(input, "/stamp unset", stamp_unset_ac, TRUE, previous);
if (result) {
return result;
}
result = autocomplete_param_with_ac(input, "/stamp", stamp_ac, TRUE, previous);
return result;
}
static char*
_adhoc_cmd_autocomplete(ProfWin* window, const char* const input, gboolean previous)
{
@@ -4286,7 +4380,7 @@ _vcard_autocomplete(ProfWin* window, const char* const input, gboolean previous)
gboolean is_num = TRUE;
if (num_args >= 2) {
for (int i = 0; i < strlen(args[1]); i++) {
for (size_t i = 0; i < strlen(args[1]); i++) {
if (!isdigit((int)args[1][i])) {
is_num = FALSE;
break;
@@ -4416,4 +4510,163 @@ _force_encryption_autocomplete(ProfWin* window, const char* const input, gboolea
result = autocomplete_param_with_ac(input, "/force-encryption", force_encryption_ac, TRUE, previous);
return result;
}
}
/* Forward declaration */
static char* _ai_provider_and_model_autocomplete(ProfWin* window, const char* const input,
gboolean previous, const char* cmd);
static char*
_ai_autocomplete(ProfWin* window, const char* const input, gboolean previous)
{
char* result = NULL;
/* Parse once for reuse - /ai <subcommand> [<arg1>] [<arg2>] */
gboolean parse_result = FALSE;
auto_gcharv gchar** args = parse_args(input, 1, 4, &parse_result);
int num_args = g_strv_length(args);
/* Top-level /ai <subcommand> autocomplete (e.g., /ai s<tab> -> /ai set) */
result = autocomplete_param_with_func(input, "/ai set provider", ai_providers_find, previous, NULL);
if (result) {
return result;
}
// /ai set token <provider> <token> - autocomplete provider names
result = autocomplete_param_with_func(input, "/ai set token", ai_providers_find, previous, NULL);
if (result) {
return result;
}
// /ai set default-model <provider> <model> - autocomplete provider names
result = autocomplete_param_with_func(input, "/ai set default-model", ai_providers_find, previous, NULL);
if (result) {
return result;
}
// /ai set custom <provider> <setting> <value> - autocomplete provider names
result = autocomplete_param_with_func(input, "/ai set custom", ai_providers_find, previous, NULL);
if (result) {
return result;
}
// /ai set custom <provider> <setting> - autocomplete settings
/* args[0]="set", args[1]="custom", args[2]=provider (if typed), args[3]=setting (if typed) */
if (num_args == 3 && g_strcmp0(args[1], "custom") == 0) {
/* /ai set custom <provider> - check if provider is valid */
if (ai_get_provider(args[2])) {
/* Valid provider, try settings autocomplete */
result = autocomplete_param_with_ac(input, "/ai set custom ", ai_set_custom_subcommands_ac, TRUE, previous);
if (result) {
return result;
}
}
}
// /ai set <subcommand> - autocomplete subcommands
result = autocomplete_param_with_ac(input, "/ai set", ai_set_subcommands_ac, TRUE, previous);
if (result) {
return result;
}
// /ai remove provider <name> - autocomplete provider names
result = autocomplete_param_with_func(input, "/ai remove provider", ai_providers_find, previous, NULL);
if (result) {
return result;
}
result = autocomplete_param_with_ac(input, "/ai remove", ai_remove_subcommands_ac, TRUE, previous);
if (result) {
return result;
}
// /ai start <provider> <model> - autocomplete provider names and model names
result = _ai_provider_and_model_autocomplete(window, input, previous, "/ai start");
if (result) {
return result;
}
// /ai switch <provider> <model> - autocomplete provider names and model names
result = _ai_provider_and_model_autocomplete(window, input, previous, "/ai switch");
if (result) {
return result;
}
// /ai set default-model <provider> <model> - autocomplete provider names and model names
result = _ai_provider_and_model_autocomplete(window, input, previous, "/ai set default-model");
if (result) {
return result;
}
// /ai models <provider> - autocomplete provider names
result = autocomplete_param_with_func(input, "/ai models", ai_providers_find, previous, NULL);
if (result) {
return result;
}
// /ai clear - no autocomplete
// /ai providers - no autocomplete
result = autocomplete_param_with_ac(input, "/ai", ai_subcommands_ac, FALSE, previous);
return result;
}
/**
* Autocomplete provider names and model names for /ai <cmd> <provider> <model> patterns.
* First tries to autocomplete provider names, then model names if provider is valid.
* Uses static ai_models_ac to preserve cycling state (last_found) across calls.
* The cmd_prefix should NOT include trailing space (e.g., "/ai start" not "/ai start ").
*/
static char*
_ai_provider_and_model_autocomplete(ProfWin* window, const char* const input, gboolean previous, const char* cmd)
{
char* result;
/* First, try provider name autocomplete */
result = autocomplete_param_with_func(input, cmd, ai_providers_find, previous, NULL);
if (result) {
return result;
}
/* Provider was specified, try model name autocomplete */
/* Build the full command prefix with space */
auto_gchar gchar* cmd_prefix = g_strdup_printf("%s ", cmd);
if (!g_str_has_prefix(input, cmd_prefix)) {
return NULL;
}
/* Extract provider name from input (after cmd_prefix) */
auto_gchar gchar* rest = g_strdup(input + strlen(cmd_prefix));
char* space = strchr(rest, ' ');
if (space) {
*space = '\0';
}
/* Look up the provider by name and get its models */
AIProvider* prov = ai_get_provider(rest);
if (!prov || !prov->models) {
return NULL;
}
if (!ai_models_ac) {
ai_models_ac = autocomplete_new();
}
/* Convert GList* to char** for autocomplete_update */
int model_count = g_list_length(prov->models);
char** model_array = g_new0(char*, model_count + 1);
GList* curr = prov->models;
int i = 0;
while (curr) {
model_array[i++] = curr->data;
curr = g_list_next(curr);
}
autocomplete_update(ai_models_ac, model_array);
g_free(model_array);
auto_gchar gchar* full_prefix = g_strdup_printf("%s%s", cmd_prefix, rest);
result = autocomplete_param_with_ac(input, full_prefix, ai_models_ac, TRUE, previous);
return result;
}

View File

@@ -4,33 +4,7 @@
*
* Copyright (C) 2012 - 2019 James Booth <boothj5@gmail.com>
*
* This file is part of Profanity.
*
* Profanity is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Profanity is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Profanity. If not, see <https://www.gnu.org/licenses/>.
*
* In addition, as a special exception, the copyright holders give permission to
* link the code of portions of this program with the OpenSSL library under
* certain conditions as described in each individual source file, and
* distribute linked combinations including the two.
*
* You must obey the GNU General Public License in all respects for all of the
* code used other than OpenSSL. If you modify file(s) with this exception, you
* may extend this exception to your version of the file(s), but you are not
* obligated to do so. If you do not wish to do so, delete this exception
* statement from your version. If you delete this exception statement from all
* source files in the program, then also delete it here.
*
* SPDX-License-Identifier: GPL-3.0-or-later WITH OpenSSL-exception
*/
#ifndef COMMAND_CMD_AC_H

View File

@@ -3,35 +3,9 @@
* vim: expandtab:ts=4:sts=4:sw=4
*
* Copyright (C) 2012 - 2019 James Booth <boothj5@gmail.com>
* Copyright (C) 2019 - 2025 Michael Vetter <jubalh@iodoru.org>
*
* This file is part of Profanity.
*
* Profanity is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Profanity is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Profanity. If not, see <https://www.gnu.org/licenses/>.
*
* In addition, as a special exception, the copyright holders give permission to
* link the code of portions of this program with the OpenSSL library under
* certain conditions as described in each individual source file, and
* distribute linked combinations including the two.
*
* You must obey the GNU General Public License in all respects for all of the
* code used other than OpenSSL. If you modify file(s) with this exception, you
* may extend this exception to your version of the file(s), but you are not
* obligated to do so. If you do not wish to do so, delete this exception
* statement from your version. If you delete this exception statement from all
* source files in the program, then also delete it here.
* Copyright (C) 2019 - 2026 Michael Vetter <jubalh@iodoru.org>
*
* SPDX-License-Identifier: GPL-3.0-or-later WITH OpenSSL-exception
*/
#include "config.h"
@@ -105,6 +79,9 @@ static gboolean _cmd_has_tag(Command* pcmd, const char* const tag);
* Command list
*/
#define CMD_TLS_DIRECT "Use direct TLS for the connection. It means TLS handshake is started right after TCP connection is established."
#define CMD_TLS_LEGACY "Alternative keyword for 'direct', which was created when one still thought that 'STARTTLS' is the future."
// clang-format off
static const struct cmd_t command_defs[] = {
{ CMD_PREAMBLE("/help",
@@ -147,7 +124,7 @@ static const struct cmd_t command_defs[] = {
CMD_TAG_CONNECTION)
CMD_SYN(
"/connect [<account>]",
"/connect <account> [server <server>] [port <port>] [tls force|allow|trust|legacy|disable] [auth default|legacy]",
"/connect <account> [server <server>] [port <port>] [tls force|allow|trust|direct|disable|legacy] [auth default|legacy]",
"/connect <server>")
CMD_DESC(
"Login to a chat service. "
@@ -162,8 +139,9 @@ static const struct cmd_t command_defs[] = {
{ "tls force", "Force TLS connection, and fail if one cannot be established, this is default behaviour." },
{ "tls allow", "Use TLS for the connection if it is available." },
{ "tls trust", "Force TLS connection and trust server's certificate." },
{ "tls legacy", "Use legacy TLS for the connection. It means server doesn't support STARTTLS and TLS is forced just after TCP connection is established." },
{ "tls direct", CMD_TLS_DIRECT },
{ "tls disable", "Disable TLS for the connection." },
{ "tls legacy", CMD_TLS_LEGACY },
{ "auth default", "Default authentication process." },
{ "auth legacy", "Allow legacy authentication." })
CMD_EXAMPLES(
@@ -404,15 +382,17 @@ static const struct cmd_t command_defs[] = {
CMD_DESC(
"Manage blocked users (XEP-0191), calling with no arguments shows the current list of blocked users. "
"To blog a certain user in a MUC use the following as jid: room@conference.example.org/spammy-user"
"It is also possible to block and report (XEP-0377) a user with the report-abuse and report-spam commands.")
"It is also possible to block and report (XEP-0377) a user with the report-abuse and report-spam commands. "
"For spam reporting, please include the content of the spam message as evidence for the service operator.")
CMD_ARGS(
{ "add [<jid>]", "Block the specified Jabber ID. If in a chat window and no jid is specified, the current recipient will be blocked." },
{ "remove <jid>", "Remove the specified Jabber ID from the blocked list." },
{ "report-abuse <jid> [<message>]", "Report the jid as abuse with an optional message to the service operator." },
{ "report-spam <jid> [<message>]", "Report the jid as spam with an optional message to the service operator." })
{ "report-abuse <jid> [<message>]", "Report a user for abuse with an optional description of the behavior." },
{ "report-spam <jid> [<message>]", "Report a user for spam, including the actual spam message as evidence." })
CMD_EXAMPLES(
"/blocked add hel@helheim.edda",
"/blocked report-spam hel@helheim.edda Very annoying guy",
"/blocked report-abuse hel@helheim.edda Harassing me in MUC",
"/blocked report-spam spambot@example.com \"You won a free prize!\"",
"/blocked add profanity@rooms.dismail.de/spammy-user")
},
@@ -1289,8 +1269,8 @@ static const struct cmd_t command_defs[] = {
CMD_TAGS(
CMD_TAG_UI)
CMD_SYN(
"/statusbar show name|number|read",
"/statusbar hide name|number|read",
"/statusbar show name|number|read|dbbackend",
"/statusbar hide name|number|read|dbbackend",
"/statusbar maxtabs <value>",
"/statusbar tablen <value>",
"/statusbar tabmode default|dynamic|actlist",
@@ -1308,6 +1288,7 @@ static const struct cmd_t command_defs[] = {
{ "show|hide name", "Show or hide names in tabs." },
{ "show|hide number", "Show or hide numbers in tabs." },
{ "show|hide read", "Show or hide inactive tabs." },
{ "show|hide dbbackend", "Show or hide the database backend indicator [sqlite] or [flatfile]." },
{ "self user|barejid|fulljid", "Show account user name, barejid, fulljid as status bar title." },
{ "self off", "Disable showing self as status bar title." },
{ "chat user|jid", "Show users name, or fulljid. Change needs a redraw/restart to take effect." },
@@ -1472,6 +1453,23 @@ static const struct cmd_t command_defs[] = {
{ "on|off", "Enable or disable splash logo." })
},
{ CMD_PREAMBLE("/spellcheck",
parse_args, 1, 2, &cons_spellcheck_setting)
CMD_MAINFUNC(cmd_spellcheck)
CMD_TAGS(
CMD_TAG_UI)
CMD_SYN(
"/spellcheck on|off",
"/spellcheck list",
"/spellcheck lang <locale>")
CMD_DESC(
"Enable or disable spellchecking, or set the language.")
CMD_ARGS(
{ "on|off", "Enable or disable spellchecking." },
{ "list", "List available dictionaries recognized by Enchant." },
{ "lang <locale>", "Set the spellcheck language (en_US)." })
},
{ CMD_PREAMBLE("/autoconnect",
parse_args, 1, 2, &cons_autoconnect_setting)
CMD_MAINFUNC(cmd_autoconnect)
@@ -1659,23 +1657,14 @@ static const struct cmd_t command_defs[] = {
},
{ CMD_PREAMBLE("/logging",
parse_args, 2, 3, &cons_logging_setting)
parse_args, 0, 0, NULL)
CMD_MAINFUNC(cmd_logging)
CMD_TAGS(
CMD_TAG_CHAT)
CMD_SYN(
"/logging chat|group on|off")
"/logging")
CMD_DESC(
"Configure chat logging. "
"Switch logging on or off. "
"Chat logging will be enabled if /history is set to on. "
"When disabling this option, /history will also be disabled. ")
CMD_ARGS(
{ "chat on|off", "Enable/Disable regular chat logging." },
{ "group on|off", "Enable/Disable groupchat (room) logging." })
CMD_EXAMPLES(
"/logging chat on",
"/logging group off")
"Deprecated. Use '/history' instead.")
},
{ CMD_PREAMBLE("/states",
@@ -1875,18 +1864,34 @@ static const struct cmd_t command_defs[] = {
},
{ CMD_PREAMBLE("/history",
parse_args, 1, 1, &cons_history_setting)
parse_args, 1, 2, &cons_history_setting)
CMD_MAINFUNC(cmd_history)
CMD_TAGS(
CMD_TAG_UI,
CMD_TAG_CHAT)
CMD_SYN(
"/history on|off")
"/history on|off",
"/history backend",
"/history switch sqlite|flatfile",
"/history verify [<jid>]",
"/history export [<jid>]",
"/history import [<jid>]")
CMD_DESC(
"Switch chat history on or off, /logging chat will automatically be enabled when this setting is on. "
"When history is enabled, previous messages are shown in chat windows.")
"Enable or disable chat history. "
"When on, messages are persisted via the active database backend and shown in chat windows. "
"When off, persistence stops and old history is hidden on chat-window open. "
"Use 'backend' to show the active database backend. "
"Use 'switch' to change the active database backend at runtime without reconnecting. "
"Use 'verify' to check integrity of stored message history. "
"Use 'export' to copy messages from SQLite to flat-file format, or 'import' to copy from flat-file to SQLite. "
"Both export and import merge with existing data (duplicates are skipped).")
CMD_ARGS(
{ "on|off", "Enable or disable showing chat history." })
{ "on|off", "Enable or disable persistence and showing chat history." },
{ "backend", "Show the name of the active database backend." },
{ "switch sqlite|flatfile", "Switch the active database backend at runtime." },
{ "verify [<jid>]", "Verify integrity of message history. Optionally specify a JID to check only one contact." },
{ "export [<jid>]", "Export SQLite history to flat-file format. Optionally specify a JID to export only one contact." },
{ "import [<jid>]", "Import flat-file history into SQLite. Optionally specify a JID to import only one contact." })
},
{ CMD_PREAMBLE("/log",
@@ -2071,7 +2076,7 @@ static const struct cmd_t command_defs[] = {
"/account set <account> pgpkeyid <pgpkeyid>",
"/account set <account> startscript <script>",
"/account set <account> clientid \"<name> <version>\"",
"/account set <account> tls force|allow|trust|legacy|disable",
"/account set <account> tls force|allow|trust|direct|disable|legacy",
"/account set <account> auth default|legacy",
"/account set <account> theme <theme>",
"/account set <account> session_alarm <max_sessions>",
@@ -2117,8 +2122,9 @@ static const struct cmd_t command_defs[] = {
{ "set <account> tls force", "Force TLS connection, and fail if one cannot be established, this is default behaviour." },
{ "set <account> tls allow", "Use TLS for the connection if it is available." },
{ "set <account> tls trust", "Force TLS connection and trust server's certificate." },
{ "set <account> tls legacy", "Use legacy TLS for the connection. It means server doesn't support STARTTLS and TLS is forced just after TCP connection is established." },
{ "set <account> tls direct", CMD_TLS_DIRECT },
{ "set <account> tls disable", "Disable TLS for the connection." },
{ "set <account> tls legacy", CMD_TLS_LEGACY },
{ "set <account> auth default", "Use default authentication process." },
{ "set <account> auth legacy", "Allow legacy authentication." },
{ "set <account> theme <theme>", "Set the UI theme for the account." },
@@ -2145,6 +2151,7 @@ static const struct cmd_t command_defs[] = {
"/account set me status dnd",
"/account set me dnd -1",
"/account set me clientid \"Profanity 0.42 (Dev)\"",
"/account set me eval_password \"pass \\\"Test Accounts/my user\\\"\"",
"/account rename me chattyme",
"/account clear me pgpkeyid")
},
@@ -2328,13 +2335,18 @@ static const struct cmd_t command_defs[] = {
"/omemo clear_device_list",
"/omemo qrcode")
CMD_DESC(
"OMEMO commands to manage keys, and perform encryption during chat sessions.")
"OMEMO commands to manage keys, and perform encryption during chat sessions.\n"
"The title bar will show the OMEMO session status:\n"
"[OMEMO][trusted] - All active devices for the contact are trusted.\n"
"[OMEMO][untrusted] - One or more active devices for the contact are untrusted.\n")
CMD_ARGS(
{ "gen", "Generate OMEMO cryptographic materials for current account." },
{ "start [<contact>]", "Start an OMEMO session with contact, or current recipient if omitted." },
{ "end", "End the current OMEMO session." },
{ "log on|off", "Enable or disable plaintext logging of OMEMO encrypted messages." },
{ "log redact", "Log OMEMO encrypted messages, but replace the contents with [redacted]." },
{ "trust [<contact>] <fp>", "Trust a fingerprint for a contact, or current recipient if omitted. If all active devices are trusted, the title bar will show [trusted]. Otherwise, it will show [untrusted]." },
{ "untrust [<contact>] <fp>","Untrust a fingerprint for a contact, or current recipient if omitted." },
{ "fingerprint [<contact>]", "Show contact's fingerprints, or current recipient's if omitted." },
{ "char <char>", "Set the character to be displayed next to OMEMO encrypted messages." },
{ "trustmode manual", "Set the global OMEMO trust mode to manual, OMEMO keys has to be trusted manually." },
@@ -2353,6 +2365,15 @@ static const struct cmd_t command_defs[] = {
"/omemo char *")
},
{ CMD_PREAMBLE("/changes",
parse_args, 0, 0, NULL)
CMD_MAINFUNC(cmd_changes)
CMD_SYN(
"/changes")
CMD_DESC(
"Show changes from saved configuration file.")
},
{ CMD_PREAMBLE("/save",
parse_args, 0, 0, NULL)
CMD_MAINFUNC(cmd_save)
@@ -2411,7 +2432,7 @@ static const struct cmd_t command_defs[] = {
"/stamp unset outgoing|incoming")
CMD_DESC("Set chat window stamp. "
"The format of line in the chat window is: \"<timestamp> <encryption sign> <stamp> <message>\" "
"where <stamp> is \"me:\" for incoming messages or \"username@server/resource\" for outgoing messages. "
"where <stamp> is \"me:\" for outgoing messages or \"username@server/resource\" for incoming messages. "
"This command allows to change <stamp> value.")
CMD_ARGS({ "outgoing", "Set outgoing stamp" },
{ "incoming", "Set incoming stamp"},
@@ -2651,7 +2672,7 @@ static const struct cmd_t command_defs[] = {
CMD_TAGS(
CMD_TAG_CONNECTION)
CMD_SYN(
"/register <username> <server> [port <port>] [tls force|allow|trust|legacy|disable]")
"/register <username> <server> [port <port>] [tls force|allow|trust|direct|disable|legacy]")
CMD_DESC(
"Register an account on a server.")
CMD_ARGS(
@@ -2661,8 +2682,9 @@ static const struct cmd_t command_defs[] = {
{ "tls force", "Force TLS connection, and fail if one cannot be established. This is the default behavior." },
{ "tls allow", "Use TLS for the connection if it is available." },
{ "tls trust", "Force TLS connection and trust the server's certificate." },
{ "tls legacy", "Use legacy TLS for the connection. This forces TLS just after the TCP connection is established. Use when a server doesn't support STARTTLS." },
{ "tls disable", "Disable TLS for the connection." })
{ "tls direct", CMD_TLS_DIRECT },
{ "tls disable", "Disable TLS for the connection." },
{ "tls legacy", CMD_TLS_LEGACY })
CMD_EXAMPLES(
"/register odin valhalla.edda ",
"/register freyr vanaheimr.edda port 5678",
@@ -2718,7 +2740,7 @@ static const struct cmd_t command_defs[] = {
CMD_TAG_CHAT,
CMD_TAG_DISCOVERY)
CMD_SYN(
"/privacy logging on|redact|off",
"/privacy logging on|redact|off|flatfile",
"/privacy os on|off")
CMD_DESC(
"Configure privacy settings. "
@@ -2726,12 +2748,13 @@ static const struct cmd_t command_defs[] = {
"clientid to set the client identification name "
"session_alarm to configure an alarm when more clients log in.")
CMD_ARGS(
{ "logging on|redact|off", "Switch chat logging. This will also disable logging in the internally used SQL database. Your messages will not be saved anywhere locally. This might have unintended consequences, such as not being able to decrypt OMEMO encrypted messages received later via MAM, and should be used with caution." },
{ "logging on|redact|off|flatfile", "Switch chat logging backend. 'on' uses the SQLite database (default). 'flatfile' stores messages as plain-text files under ~/.local/share/profanity/flatlog/ that can be edited with any text editor. 'off' disables logging entirely. 'redact' keeps logging but replaces every message body with '[REDACTED]'. Note: 'off' may have side effects such as not being able to decrypt OMEMO messages received later via MAM. Backend changes ('on' / 'flatfile') take effect immediately when connected, otherwise on next connect." },
{ "os on|off", "Choose whether to include the OS name if a user asks for software information (XEP-0092)." }
)
CMD_EXAMPLES(
"/privacy",
"/privacy logging off",
"/privacy logging flatfile",
"/privacy os off")
},
@@ -2771,6 +2794,67 @@ static const struct cmd_t command_defs[] = {
"/force-encryption policy block")
},
{ CMD_PREAMBLE("/ai",
parse_args, 0, 5, NULL)
CMD_SUBFUNCS(
{ "set", cmd_ai_set },
{ "remove", cmd_ai_remove },
{ "start", cmd_ai_start },
{ "clear", cmd_ai_clear },
{ "providers", cmd_ai_providers },
{ "switch", cmd_ai_switch },
{ "models", cmd_ai_models })
CMD_MAINFUNC(cmd_ai)
CMD_TAGS(
CMD_TAG_CHAT)
CMD_SYN(
"/ai",
"/ai set provider <name> <url>",
"/ai set token <provider> <token>",
"/ai set default-model <provider> <model>",
"/ai set custom <provider> <setting> <value>",
"/ai remove provider <name>",
"/ai providers",
"/ai start [<provider>] [<model>]",
"/ai switch <provider> [<model>]",
"/ai switch <model>",
"/ai models <provider>",
"/ai clear")
CMD_DESC(
"Interact with AI models via OpenAI-compatible APIs. "
"Supports multiple providers (openai, perplexity, custom). "
"Each provider has its own API key, endpoint, default model, and settings. "
"Chat history is maintained per session and not persisted locally.")
CMD_ARGS(
{ "", "Display current AI settings and configured providers" },
{ "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., 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)" },
{ "switch <provider> [<model>]", "Switch to different provider (and optionally model)" },
{ "models <provider>", "Fetch and display available models for provider" },
{ "clear", "Clear current chat history" })
CMD_EXAMPLES(
"/ai",
"/ai set token openai sk-xxx",
"/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 tools enabled",
"/ai remove provider custom",
"/ai start",
"/ai start perplexity",
"/ai start perplexity sonar",
"/ai start openai gpt-4o",
"/ai switch gpt-4o",
"/ai switch openai gpt-5.4-nano",
"/ai models perplexity",
"/ai clear")
},
// NEXT-COMMAND (search helper)
};
@@ -2808,7 +2892,7 @@ _cmd_index(const Command* cmd)
g_string_free(index_source, TRUE);
GString* index = g_string_new("");
for (int i = 0; i < g_strv_length(tokens); i++) {
for (guint i = 0; i < g_strv_length(tokens); i++) {
index = g_string_append(index, tokens[i]);
index = g_string_append(index, " ");
}
@@ -2985,16 +3069,11 @@ command_docgen(void)
cmds = g_list_insert_sorted(cmds, (gpointer)pcmd, (GCompareFunc)_cmp_command);
}
FILE* toc_fragment = fopen("toc_fragment.html", "w");
if (!toc_fragment) {
log_error("command_docgen(): unable to open toc_fragment.html for writing: %s", g_strerror(errno));
g_list_free(cmds);
return;
}
FILE* main_fragment = fopen("main_fragment.html", "w");
if (!main_fragment) {
log_error("command_docgen(): unable to open main_fragment.html for writing: %s", g_strerror(errno));
fclose(toc_fragment);
auto_FILE FILE* toc_fragment = fopen("toc_fragment.html", "w");
auto_FILE FILE* main_fragment = fopen("main_fragment.html", "w");
if (!toc_fragment || !main_fragment) {
log_error("command_docgen(): unable to open html files: %s", g_strerror(errno));
g_list_free(cmds);
return;
}
@@ -3065,9 +3144,7 @@ command_docgen(void)
fputs("</ul></ul>\n", toc_fragment);
fclose(toc_fragment);
fclose(main_fragment);
printf("\nProcessed %d commands.\n\n", g_list_length(cmds));
printf("\nProcessed %u commands.\n\n", g_list_length(cmds));
g_list_free(cmds);
}
@@ -3145,7 +3222,7 @@ command_mangen(void)
curr = g_list_next(curr);
}
printf("\nProcessed %d commands.\n\n", g_list_length(cmds));
printf("\nProcessed %u commands.\n\n", g_list_length(cmds));
g_list_free(cmds);
}

View File

@@ -3,35 +3,9 @@
* vim: expandtab:ts=4:sts=4:sw=4
*
* Copyright (C) 2012 - 2019 James Booth <boothj5@gmail.com>
* Copyright (C) 2019 - 2025 Michael Vetter <jubalh@iodoru.org>
*
* This file is part of Profanity.
*
* Profanity is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Profanity is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Profanity. If not, see <https://www.gnu.org/licenses/>.
*
* In addition, as a special exception, the copyright holders give permission to
* link the code of portions of this program with the OpenSSL library under
* certain conditions as described in each individual source file, and
* distribute linked combinations including the two.
*
* You must obey the GNU General Public License in all respects for all of the
* code used other than OpenSSL. If you modify file(s) with this exception, you
* may extend this exception to your version of the file(s), but you are not
* obligated to do so. If you do not wish to do so, delete this exception
* statement from your version. If you delete this exception statement from all
* source files in the program, then also delete it here.
* Copyright (C) 2019 - 2026 Michael Vetter <jubalh@iodoru.org>
*
* SPDX-License-Identifier: GPL-3.0-or-later WITH OpenSSL-exception
*/
#ifndef COMMAND_CMD_DEFS_H

File diff suppressed because it is too large Load Diff

View File

@@ -3,35 +3,9 @@
* vim: expandtab:ts=4:sts=4:sw=4
*
* Copyright (C) 2012 - 2019 James Booth <boothj5@gmail.com>
* Copyright (C) 2019 - 2025 Michael Vetter <jubalh@iodoru.org>
*
* This file is part of Profanity.
*
* Profanity is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Profanity is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Profanity. If not, see <https://www.gnu.org/licenses/>.
*
* In addition, as a special exception, the copyright holders give permission to
* link the code of portions of this program with the OpenSSL library under
* certain conditions as described in each individual source file, and
* distribute linked combinations including the two.
*
* You must obey the GNU General Public License in all respects for all of the
* code used other than OpenSSL. If you modify file(s) with this exception, you
* may extend this exception to your version of the file(s), but you are not
* obligated to do so. If you do not wish to do so, delete this exception
* statement from your version. If you delete this exception statement from all
* source files in the program, then also delete it here.
* Copyright (C) 2019 - 2026 Michael Vetter <jubalh@iodoru.org>
*
* SPDX-License-Identifier: GPL-3.0-or-later WITH OpenSSL-exception
*/
#ifndef COMMAND_CMD_FUNCS_H
@@ -129,6 +103,7 @@ gboolean cmd_bookmark_ignore(ProfWin* window, const char* const command, gchar**
gboolean cmd_roster(ProfWin* window, const char* const command, gchar** args);
gboolean cmd_software(ProfWin* window, const char* const command, gchar** args);
gboolean cmd_splash(ProfWin* window, const char* const command, gchar** args);
gboolean cmd_spellcheck(ProfWin* window, const char* const command, gchar** args);
gboolean cmd_states(ProfWin* window, const char* const command, gchar** args);
gboolean cmd_status_get(ProfWin* window, const char* const command, gchar** args);
gboolean cmd_status_set(ProfWin* window, const char* const command, gchar** args);
@@ -166,6 +141,14 @@ gboolean cmd_console(ProfWin* window, const char* const command, gchar** args);
gboolean cmd_command_list(ProfWin* window, const char* const command, gchar** args);
gboolean cmd_command_exec(ProfWin* window, const char* const command, gchar** args);
gboolean cmd_change_password(ProfWin* window, const char* const command, gchar** args);
gboolean cmd_ai(ProfWin* window, const char* const command, gchar** args);
gboolean cmd_ai_set(ProfWin* window, const char* const command, gchar** args);
gboolean cmd_ai_remove(ProfWin* window, const char* const command, gchar** args);
gboolean cmd_ai_start(ProfWin* window, const char* const command, gchar** args);
gboolean cmd_ai_clear(ProfWin* window, const char* const command, gchar** args);
gboolean cmd_ai_providers(ProfWin* window, const char* const command, gchar** args);
gboolean cmd_ai_switch(ProfWin* window, const char* const command, gchar** args);
gboolean cmd_ai_models(ProfWin* window, const char* const command, gchar** args);
gboolean cmd_plugins(ProfWin* window, const char* const command, gchar** args);
gboolean cmd_plugins_sourcepath(ProfWin* window, const char* const command, gchar** args);
@@ -234,6 +217,7 @@ gboolean cmd_omemo_policy(ProfWin* window, const char* const command, gchar** ar
gboolean cmd_omemo_clear_device_list(ProfWin* window, const char* const command, gchar** args);
gboolean cmd_omemo_qrcode(ProfWin* window, const char* const command, gchar** args);
gboolean cmd_changes(ProfWin* window, const char* const command, gchar** args);
gboolean cmd_save(ProfWin* window, const char* const command, gchar** args);
gboolean cmd_reload(ProfWin* window, const char* const command, gchar** args);

View File

@@ -3,35 +3,9 @@
* vim: expandtab:ts=4:sts=4:sw=4
*
* Copyright (C) 2012 - 2019 James Booth <boothj5@gmail.com>
* Copyright (C) 2019 - 2025 Michael Vetter <jubalh@iodoru.org>
*
* This file is part of Profanity.
*
* Profanity is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Profanity is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Profanity. If not, see <https://www.gnu.org/licenses/>.
*
* In addition, as a special exception, the copyright holders give permission to
* link the code of portions of this program with the OpenSSL library under
* certain conditions as described in each individual source file, and
* distribute linked combinations including the two.
*
* You must obey the GNU General Public License in all respects for all of the
* code used other than OpenSSL. If you modify file(s) with this exception, you
* may extend this exception to your version of the file(s), but you are not
* obligated to do so. If you do not wish to do so, delete this exception
* statement from your version. If you delete this exception statement from all
* source files in the program, then also delete it here.
* Copyright (C) 2019 - 2026 Michael Vetter <jubalh@iodoru.org>
*
* SPDX-License-Identifier: GPL-3.0-or-later WITH OpenSSL-exception
*/
#include "config.h"
@@ -64,6 +38,7 @@
#include "log.h"
#include "common.h"
#include "config/files.h"
#include "ui/ui.h"
#ifdef HAVE_GIT_VERSION
#include "gitversion.h"
@@ -161,20 +136,27 @@ auto_close_FILE(FILE** fd)
log_error("%s", g_strerror(errno));
}
void
auto_free_gerror(GError** err)
{
if (err == NULL)
return;
PROF_GERROR_FREE(*err);
}
static gboolean
_load_keyfile(prof_keyfile_t* keyfile)
{
GError* error = NULL;
auto_gerror GError* error = NULL;
keyfile->keyfile = g_key_file_new();
if (g_key_file_load_from_file(keyfile->keyfile, keyfile->filename, G_KEY_FILE_KEEP_COMMENTS | G_KEY_FILE_KEEP_TRANSLATIONS, &error)) {
return TRUE;
} else if (error->code != G_FILE_ERROR_NOENT) {
} else if (error && error->code != G_FILE_ERROR_NOENT) {
log_warning("[Keyfile] error loading %s: %s", keyfile->filename, error->message);
g_error_free(error);
} else {
log_warning("[Keyfile] no such file: %s", keyfile->filename);
g_error_free(error);
}
return FALSE;
}
@@ -212,10 +194,9 @@ load_custom_keyfile(prof_keyfile_t* keyfile, gchar* filename)
gboolean
save_keyfile(prof_keyfile_t* keyfile)
{
GError* error = NULL;
auto_gerror GError* error = NULL;
if (!g_key_file_save_to_file(keyfile->keyfile, keyfile->filename, &error)) {
log_error("[Keyfile]: saving file %s failed! %s", keyfile->filename, error->message);
g_error_free(error);
log_error("[Keyfile]: saving file %s failed! %s", STR_MAYBE_NULL(keyfile->filename), PROF_GERROR_MESSAGE(error));
return FALSE;
}
g_chmod(keyfile->filename, S_IRUSR | S_IWUSR);
@@ -254,11 +235,8 @@ copy_file(const char* const sourcepath, const char* const targetpath, const gboo
{
GFile* source = g_file_new_for_path(sourcepath);
GFile* dest = g_file_new_for_path(targetpath);
GError* error = NULL;
GFileCopyFlags flags = overwrite_existing ? G_FILE_COPY_OVERWRITE : G_FILE_COPY_NONE;
gboolean success = g_file_copy(source, dest, flags, NULL, NULL, NULL, &error);
if (error != NULL)
g_error_free(error);
gboolean success = g_file_copy(source, dest, flags, NULL, NULL, NULL, NULL);
g_object_unref(source);
g_object_unref(dest);
return success;
@@ -335,29 +313,89 @@ gboolean
strtoi_range(const char* str, int* saveptr, int min, int max, gchar** err_msg)
{
char* ptr;
int val;
long lval;
if (str == NULL) {
if (err_msg)
*err_msg = g_strdup_printf("'str' input pointer can not be NULL");
return FALSE;
}
errno = 0;
val = (int)strtol(str, &ptr, 0);
lval = strtol(str, &ptr, 0);
if (errno != 0 || *str == '\0' || *ptr != '\0') {
if (err_msg)
*err_msg = g_strdup_printf("Could not convert \"%s\" to a number.", str);
return FALSE;
} else if (val < min || val > max) {
} else if (lval < (long)min || lval > (long)max) {
if (err_msg)
*err_msg = g_strdup_printf("Value %s out of range. Must be in %d..%d.", str, min, max);
return FALSE;
}
*saveptr = val;
*saveptr = (int)lval;
return TRUE;
}
gboolean
string_matches_one_of(const char* what, const char* is, gboolean is_can_be_null, const char* first, ...)
{
gboolean ret = FALSE;
va_list ap;
const char* cur = first;
if (!is)
return is_can_be_null;
va_start(ap, first);
while (cur != NULL) {
if (g_strcmp0(is, cur) == 0) {
ret = TRUE;
break;
}
cur = va_arg(ap, const char*);
}
va_end(ap);
if (!ret && what) {
cons_show("Invalid %s: '%s'", what, is);
char errmsg[256] = { 0 };
size_t sz = 0;
int s = snprintf(errmsg, sizeof(errmsg) - sz, "%s must be one of:", what);
if (s < 0 || s + sz >= sizeof(errmsg))
return ret;
sz += s;
cur = first;
va_start(ap, first);
while (cur != NULL) {
const char* next = va_arg(ap, const char*);
if (next) {
s = snprintf(errmsg + sz, sizeof(errmsg) - sz, " '%s',", cur);
} else {
/* remove last ',' */
sz--;
errmsg[sz] = '\0';
s = snprintf(errmsg + sz, sizeof(errmsg) - sz, " or '%s'.", cur);
}
if (s < 0 || s + sz >= sizeof(errmsg)) {
log_debug("Error message too long or some other error occurred (%d).", s);
s = -1;
break;
}
sz += s;
cur = next;
}
va_end(ap);
if (s > 0)
cons_show("%s", errmsg);
}
return ret;
}
gboolean
valid_tls_policy_option(const char* is)
{
return string_matches_one_of("TLS policy", is, TRUE, "force", "allow", "trust", "disable", "legacy", "direct", NULL);
}
int
utf8_display_len(const char* const str)
{
@@ -380,10 +418,37 @@ utf8_display_len(const char* const str)
return len;
}
/**
* Removes illegal XML 1.0 characters from a string.
*
* This function creates a new string that excludes characters in the range
* U+0000 to U+001F, except for U+0009 (TAB), U+000A (LF), and U+000D (CR).
*/
gchar*
str_xml_sanitize(const char* const str)
{
if (str == NULL) {
return NULL;
}
GString* sanitized = g_string_new_len(NULL, strlen(str));
const char* curr = str;
while (*curr != '\0') {
gunichar c = g_utf8_get_char(curr);
if ((c >= 0x20) || (c == 0x09) || (c == 0x0A) || (c == 0x0D)) {
g_string_append_unichar(sanitized, c);
}
curr = g_utf8_next_char(curr);
}
return g_string_free(sanitized, FALSE);
}
char*
release_get_latest(void)
{
char* url = "https://profanity-im.github.io/profanity_version.txt";
const char* url = "https://profanity-im.github.io/profanity_version.txt";
CURL* handle = curl_easy_init();
struct curl_data_t output;
@@ -407,11 +472,15 @@ release_get_latest(void)
}
gboolean
release_is_new(char* found_version)
release_is_new(const char* const curr_version, const char* const found_version)
{
if (!curr_version || !found_version) {
return FALSE;
}
int curr_maj, curr_min, curr_patch, found_maj, found_min, found_patch;
int parse_curr = sscanf(PACKAGE_VERSION, "%d.%d.%d", &curr_maj, &curr_min,
int parse_curr = sscanf(curr_version, "%d.%d.%d", &curr_maj, &curr_min,
&curr_patch);
int parse_found = sscanf(found_version, "%d.%d.%d", &found_maj, &found_min,
&found_patch);
@@ -474,23 +543,32 @@ _get_file_or_linked(gchar* loc)
char*
strip_arg_quotes(const char* const input)
{
char* unquoted = strdup(input);
if (input == NULL) {
return NULL;
}
// Remove starting quote if it exists
if (strchr(unquoted, '"')) {
if (strchr(unquoted, ' ') + 1 == strchr(unquoted, '"')) {
memmove(strchr(unquoted, '"'), strchr(unquoted, '"') + 1, strchr(unquoted, '\0') - strchr(unquoted, '"'));
// Unescape and strip quotes
GString* unescaped = g_string_new("");
for (const char* p = input; *p; p++) {
if (*p == '\\' && (*(p + 1) != '\0')) {
p++;
g_string_append_c(unescaped, *p);
} else if (*p == '"') {
// Only strip if it's the first char or preceded by a space
if (p == input || *(p - 1) == ' ') {
continue;
}
// Or if it's the last char
if (*(p + 1) == '\0') {
continue;
}
g_string_append_c(unescaped, *p);
} else {
g_string_append_c(unescaped, *p);
}
}
// Remove ending quote if it exists
if (strchr(unquoted, '"')) {
if (strchr(unquoted, '\0') - 1 == strchr(unquoted, '"')) {
memmove(strchr(unquoted, '"'), strchr(unquoted, '"') + 1, strchr(unquoted, '\0') - strchr(unquoted, '"'));
}
}
return unquoted;
return g_string_free(unescaped, FALSE);
}
gboolean
@@ -514,36 +592,48 @@ is_notify_enabled(void)
GSList*
prof_occurrences(const char* const needle, const char* const haystack, int offset, gboolean whole_word, GSList** result)
{
if (needle == NULL || haystack == NULL) {
if (needle == NULL || haystack == NULL || *needle == '\0') {
return *result;
}
do {
gchar* haystack_curr = g_utf8_offset_to_pointer(haystack, offset);
if (g_str_has_prefix(haystack_curr, needle)) {
size_t needle_len = strlen(needle);
gchar* p = g_utf8_offset_to_pointer(haystack, offset);
GSList* matches = NULL;
while (p) {
if (g_str_has_prefix(p, needle)) {
if (whole_word) {
gunichar before = 0;
gchar* haystack_before_ch = g_utf8_find_prev_char(haystack, haystack_curr);
gchar* haystack_before_ch = g_utf8_find_prev_char(haystack, p);
if (haystack_before_ch) {
before = g_utf8_get_char(haystack_before_ch);
}
gunichar after = 0;
gchar* haystack_after_ch = haystack_curr + strlen(needle);
if (haystack_after_ch[0] != '\0') {
gchar* haystack_after_ch = p + needle_len;
if (*haystack_after_ch != '\0') {
after = g_utf8_get_char(haystack_after_ch);
}
if (!g_unichar_isalnum(before) && !g_unichar_isalnum(after)) {
*result = g_slist_append(*result, GINT_TO_POINTER(offset));
matches = g_slist_prepend(matches, GINT_TO_POINTER(offset));
}
} else {
*result = g_slist_append(*result, GINT_TO_POINTER(offset));
matches = g_slist_prepend(matches, GINT_TO_POINTER(offset));
}
}
if (*p == '\0') {
break;
}
p = g_utf8_next_char(p);
offset++;
} while (g_strcmp0(g_utf8_offset_to_pointer(haystack, offset), "\0") != 0);
}
if (matches) {
*result = g_slist_concat(*result, g_slist_reverse(matches));
}
return *result;
}
@@ -599,15 +689,15 @@ get_file_paths_recursive(const char* path, GSList** contents)
}
}
char*
gchar*
get_random_string(int length)
{
GRand* prng;
char* rand;
char alphabet[] = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
gchar* rand;
gchar alphabet[] = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
int endrange = sizeof(alphabet) - 1;
rand = calloc(length + 1, sizeof(char));
rand = g_malloc0(length + 1);
prng = g_rand_new();
@@ -634,7 +724,7 @@ get_mentions(gboolean whole_word, gboolean case_sensitive, const char* const mes
gboolean
call_external(gchar** argv)
{
GError* spawn_error;
auto_gerror GError* spawn_error = NULL;
gboolean is_successful;
GSpawnFlags flags = G_SPAWN_SEARCH_PATH | G_SPAWN_STDOUT_TO_DEV_NULL | G_SPAWN_STDERR_TO_DEV_NULL;
@@ -647,9 +737,7 @@ call_external(gchar** argv)
&spawn_error);
if (!is_successful) {
auto_gchar gchar* cmd = g_strjoinv(" ", argv);
log_error("Spawning '%s' failed with error '%s'", cmd, spawn_error ? spawn_error->message : "Unknown, spawn_error is NULL");
g_error_free(spawn_error);
log_error("Spawning '%s' failed with error '%s'", cmd, PROF_GERROR_MESSAGE(spawn_error));
}
return is_successful;
@@ -711,7 +799,7 @@ _unique_filename(const char* filename)
return unique;
}
static bool
static gboolean
_has_directory_suffix(const char* path)
{
return (g_str_has_suffix(path, ".")
@@ -809,6 +897,20 @@ unique_filename_from_url(const char* url, const char* path)
return unique_filename;
}
/* This returns a timestamp formatted in ISO8601 format.
* Either it returns the current time if `dt` is NULL, or
* it returns the formatted time value passed in `dt`.
*/
gchar*
prof_date_time_format_iso8601(GDateTime* dt)
{
GDateTime* dt_ = (dt == NULL) ? g_date_time_new_now_local() : dt;
gchar* ret = g_date_time_format_iso8601(dt_);
if (dt == NULL)
g_date_time_unref(dt_);
return ret;
}
/* build profanity version string.
* example: 0.13.1dev.master.69d8c1f9
*/

View File

@@ -4,36 +4,7 @@
*
* Copyright (C) 2012 - 2019 James Booth <boothj5@gmail.com>
*
* This file is part of Profanity.
*
* Profanity is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Profanity is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Profanity. If not, see <https://www.gnu.org/licenses/>.
*
* In addition, as a special exception, the copyright holders give permission to
* link the code of portions of this program with the OpenSSL library under
* certain conditions as described in each individual source file, and
* distribute linked combinations including the two.
*
* You must obey the GNU General Public License in all respects for all of the
* code used other than OpenSSL. If you modify file(s) with this exception, you
* may extend this exception to your version of the file(s), but you are not
* obligated to do so. If you do not wish to do so, delete this exception
* statement from your version. If you delete this exception statement from all
* source files in the program, then also delete it here.
*
* @file common.h
*
* @brief Common utilities for the project.
* SPDX-License-Identifier: GPL-3.0-or-later WITH OpenSSL-exception
*/
#ifndef COMMON_H
@@ -112,12 +83,22 @@ void auto_close_gfd(gint* fd);
void auto_close_FILE(FILE** fd);
void auto_free_gerror(GError** err);
#define auto_gerror __attribute__((__cleanup__(auto_free_gerror)))
#if defined(__OpenBSD__)
#define STR_MAYBE_NULL(p) (p) ?: "(null)"
#else
#define STR_MAYBE_NULL(p) (p)
#endif
#define PROF_GERROR_MESSAGE(err) (err) ? (err)->message : "error message missing"
#define PROF_GERROR_FREE(err) \
do { \
if (err) \
g_error_free(err); \
} while (0)
typedef struct prof_keyfile_t
{
gchar* filename;
@@ -171,8 +152,6 @@ typedef enum {
RESOURCE_XA
} resource_presence_t;
extern gboolean background_mode;
gboolean string_to_verbosity(const char* cmd, int* verbosity, gchar** err_msg);
gboolean create_dir(const char* name);
@@ -180,9 +159,13 @@ gboolean copy_file(const char* const src, const char* const target, const gboole
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);
int utf8_display_len(const char* const str);
gchar* str_xml_sanitize(const char* const str);
gboolean string_matches_one_of(const char* what, const char* is, gboolean is_can_be_null, const char* first, ...) __attribute__((sentinel));
gboolean valid_tls_policy_option(const char* is);
char* release_get_latest(void);
gboolean release_is_new(char* found_version);
gboolean release_is_new(const char* const curr_version, const char* const found_version);
char* strip_arg_quotes(const char* const input);
gboolean is_notify_enabled(void);
@@ -195,7 +178,7 @@ int is_regular_file(const char* path);
int is_dir(const char* path);
void get_file_paths_recursive(const char* directory, GSList** contents);
char* get_random_string(int length);
gchar* get_random_string(int length);
gboolean call_external(gchar** argv);
gchar** format_call_external_argv(const char* template, const char* url, const char* filename);
@@ -205,6 +188,8 @@ gchar* get_expanded_path(const char* path);
char* basename_from_url(const char* url);
gchar* prof_date_time_format_iso8601(GDateTime* dt);
gchar* prof_get_version(void);
void prof_add_shutdown_routine(void (*routine)(void));

View File

@@ -4,33 +4,7 @@
*
* Copyright (C) 2012 - 2019 James Booth <boothj5@gmail.com>
*
* This file is part of Profanity.
*
* Profanity is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Profanity is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Profanity. If not, see <https://www.gnu.org/licenses/>.
*
* In addition, as a special exception, the copyright holders give permission to
* link the code of portions of this program with the OpenSSL library under
* certain conditions as described in each individual source file, and
* distribute linked combinations including the two.
*
* You must obey the GNU General Public License in all respects for all of the
* code used other than OpenSSL. If you modify file(s) with this exception, you
* may extend this exception to your version of the file(s), but you are not
* obligated to do so. If you do not wish to do so, delete this exception
* statement from your version. If you delete this exception statement from all
* source files in the program, then also delete it here.
*
* SPDX-License-Identifier: GPL-3.0-or-later WITH OpenSSL-exception
*/
#include "config.h"
@@ -39,6 +13,7 @@
#include <string.h>
#include <assert.h>
#include <errno.h>
#include <sys/wait.h>
#include <glib.h>
@@ -59,7 +34,7 @@ account_new(gchar* name, gchar* jid, gchar* password, gchar* eval_password, gboo
gchar* startscript, gchar* theme, gchar* tls_policy, gchar* auth_policy,
gchar* client, int max_sessions)
{
ProfAccount* new_account = calloc(1, sizeof(ProfAccount));
ProfAccount* new_account = g_new0(ProfAccount, 1);
new_account->name = name;
@@ -144,13 +119,13 @@ account_new(gchar* name, gchar* jid, gchar* password, gchar* eval_password, gboo
return new_account;
}
char*
gchar*
account_create_connect_jid(ProfAccount* account)
{
if (account->resource) {
return create_fulljid(account->jid, account->resource);
} else {
return strdup(account->jid);
return g_strdup(account->jid);
}
}
@@ -160,53 +135,41 @@ account_eval_password(ProfAccount* account)
assert(account != NULL);
assert(account->eval_password != NULL);
errno = 0;
gchar* stdout_buf = NULL;
GError* error = NULL;
gint exit_status = 0;
gchar** argv = NULL;
FILE* stream = popen(account->eval_password, "r");
if (stream == NULL) {
const char* errmsg = strerror(errno);
if (errmsg) {
log_error("Could not execute `eval_password` command (%s).",
errmsg);
} else {
log_error("Failed to allocate memory for `eval_password` command.");
if (!g_shell_parse_argv(account->eval_password, NULL, &argv, &error)) {
log_error("Failed to parse `eval_password` command: %s", error->message);
g_error_free(error);
return FALSE;
}
if (!g_spawn_sync(NULL, argv, NULL, G_SPAWN_SEARCH_PATH | G_SPAWN_CHILD_INHERITS_STDIN, NULL, NULL, &stdout_buf, NULL, &exit_status, &error)) {
log_error("Failed to execute `eval_password` command: %s", error->message);
g_strfreev(argv);
g_error_free(error);
return FALSE;
}
g_strfreev(argv);
if (WIFEXITED(exit_status) && WEXITSTATUS(exit_status) == 0) {
account->password = stdout_buf;
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;
return FALSE;
}
return TRUE;
} else {
log_error("`eval_password` command failed with status %d", exit_status);
g_free(stdout_buf);
return FALSE;
}
account->password = g_malloc(READ_BUF_SIZE);
if (!account->password) {
log_error("Failed to allocate enough memory to read `eval_password` "
"output.");
return FALSE;
}
account->password = fgets(account->password, READ_BUF_SIZE, stream);
if (!account->password) {
log_error("Failed to read password from stream.");
return FALSE;
}
int exit_status = pclose(stream);
if (exit_status > 0) {
log_error("Command for `eval_password` returned error status (%s).",
exit_status);
return FALSE;
} else if (exit_status < 0) {
log_error("Failed to close stream for `eval_password` command output "
"(%s).",
strerror(errno));
return FALSE;
};
// Remove leading and trailing whitespace from output.
g_strstrip(account->password);
if (!account->password) {
log_error("Empty password returned by `eval_password` command.");
return FALSE;
}
return TRUE;
}
void

View File

@@ -4,33 +4,7 @@
*
* Copyright (C) 2012 - 2019 James Booth <boothj5@gmail.com>
*
* This file is part of Profanity.
*
* Profanity is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Profanity is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Profanity. If not, see <https://www.gnu.org/licenses/>.
*
* In addition, as a special exception, the copyright holders give permission to
* link the code of portions of this program with the OpenSSL library under
* certain conditions as described in each individual source file, and
* distribute linked combinations including the two.
*
* You must obey the GNU General Public License in all respects for all of the
* code used other than OpenSSL. If you modify file(s) with this exception, you
* may extend this exception to your version of the file(s), but you are not
* obligated to do so. If you do not wish to do so, delete this exception
* statement from your version. If you delete this exception statement from all
* source files in the program, then also delete it here.
*
* SPDX-License-Identifier: GPL-3.0-or-later WITH OpenSSL-exception
*/
#ifndef CONFIG_ACCOUNT_H
@@ -84,7 +58,7 @@ ProfAccount* account_new(gchar* name, gchar* jid, gchar* password, gchar* eval_p
GList* ox_enabled, GList* pgp_enabled, gchar* pgp_keyid,
gchar* startscript, gchar* theme, gchar* tls_policy, gchar* auth_policy,
gchar* client, int max_sessions);
char* account_create_connect_jid(ProfAccount* account);
gchar* account_create_connect_jid(ProfAccount* account);
gboolean account_eval_password(ProfAccount* account);
void account_free(ProfAccount* account);
void account_set_server(ProfAccount* account, const char* server);

View File

@@ -3,35 +3,9 @@
* vim: expandtab:ts=4:sts=4:sw=4
*
* Copyright (C) 2012 - 2019 James Booth <boothj5@gmail.com>
* Copyright (C) 2019 - 2025 Michael Vetter <jubalh@iodoru.org>
*
* This file is part of Profanity.
*
* Profanity is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Profanity is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Profanity. If not, see <https://www.gnu.org/licenses/>.
*
* In addition, as a special exception, the copyright holders give permission to
* link the code of portions of this program with the OpenSSL library under
* certain conditions as described in each individual source file, and
* distribute linked combinations including the two.
*
* You must obey the GNU General Public License in all respects for all of the
* code used other than OpenSSL. If you modify file(s) with this exception, you
* may extend this exception to your version of the file(s), but you are not
* obligated to do so. If you do not wish to do so, delete this exception
* statement from your version. If you delete this exception statement from all
* source files in the program, then also delete it here.
* Copyright (C) 2019 - 2026 Michael Vetter <jubalh@iodoru.org>
*
* SPDX-License-Identifier: GPL-3.0-or-later WITH OpenSSL-exception
*/
#include "config.h"
@@ -59,7 +33,74 @@ static GKeyFile* accounts;
static Autocomplete all_ac;
static Autocomplete enabled_ac;
static void _save_accounts(void);
static gchar*
_sanitize_account_name(const char* const name)
{
if (!name) {
return NULL;
}
gchar* sanitized = g_strdup(name);
gchar* p = sanitized;
while (*p) {
// GKeyFile special characters: [ ] = # \n \r
if (*p == '[' || *p == ']' || *p == '=' || *p == '#' || *p == '\n' || *p == '\r') {
*p = '_';
}
p++;
}
return sanitized;
}
static gboolean
_accounts_has_group(const char* account_name)
{
if (!account_name || !accounts)
return FALSE;
auto_gchar gchar* sanitized = _sanitize_account_name(account_name);
return g_key_file_has_group(accounts, sanitized);
}
static void
_accounts_save(const char* account_name)
{
prof_keyfile_t current;
if (!load_data_keyfile(&current, FILE_ACCOUNTS)) {
log_error("Could not load accounts");
return;
}
auto_gchar gchar* sanitized = _sanitize_account_name(account_name);
if (_accounts_has_group(sanitized)) {
// Remove keys from file that are no longer in memory
gsize nkeys_disk;
auto_gcharv gchar** keys_disk = g_key_file_get_keys(current.keyfile, sanitized, &nkeys_disk, NULL);
if (keys_disk) {
for (gsize j = 0; j < nkeys_disk; ++j) {
if (!g_key_file_has_key(accounts_prof_keyfile.keyfile, sanitized, keys_disk[j], NULL)) {
g_key_file_remove_key(current.keyfile, sanitized, keys_disk[j], NULL);
}
}
}
gsize nkeys;
auto_gcharv gchar** keys = g_key_file_get_keys(accounts_prof_keyfile.keyfile, sanitized, &nkeys, NULL);
if (keys) {
for (gsize j = 0; j < nkeys; ++j) {
auto_gchar gchar* new_value = g_key_file_get_value(accounts_prof_keyfile.keyfile, sanitized, keys[j], NULL);
g_key_file_set_value(current.keyfile, sanitized, keys[j], new_value);
}
}
} else {
g_key_file_remove_group(current.keyfile, sanitized, NULL);
}
save_keyfile(&current);
free_keyfile(&current);
}
static void
_accounts_close(void)
@@ -79,7 +120,9 @@ accounts_load(void)
all_ac = autocomplete_new();
enabled_ac = autocomplete_new();
load_data_keyfile(&accounts_prof_keyfile, FILE_ACCOUNTS);
if (!load_data_keyfile(&accounts_prof_keyfile, FILE_ACCOUNTS)) {
log_error("Could not load accounts");
}
accounts = accounts_prof_keyfile.keyfile;
// create the logins searchable list for autocompletion
@@ -132,54 +175,58 @@ accounts_add(const char* account_name, const char* altdomain, const int port, co
}
}
if (g_key_file_has_group(accounts, account_name)) {
auto_gchar gchar* sanitized_account_name = _sanitize_account_name(account_name);
if (_accounts_has_group(sanitized_account_name)) {
log_error("Can't add account \"%s\", it already exists.", account_name);
return;
}
g_key_file_set_boolean(accounts, account_name, "enabled", TRUE);
g_key_file_set_string(accounts, account_name, "jid", barejid);
g_key_file_set_string(accounts, account_name, "resource", resource);
g_key_file_set_boolean(accounts, sanitized_account_name, "enabled", TRUE);
g_key_file_set_string(accounts, sanitized_account_name, "jid", barejid);
g_key_file_set_string(accounts, sanitized_account_name, "resource", resource);
if (altdomain) {
g_key_file_set_string(accounts, account_name, "server", altdomain);
g_key_file_set_string(accounts, sanitized_account_name, "server", altdomain);
}
if (port != 0) {
g_key_file_set_integer(accounts, account_name, "port", port);
g_key_file_set_integer(accounts, sanitized_account_name, "port", port);
}
if (tls_policy) {
g_key_file_set_string(accounts, account_name, "tls.policy", tls_policy);
g_key_file_set_string(accounts, sanitized_account_name, "tls.policy", tls_policy);
}
if (auth_policy) {
g_key_file_set_string(accounts, account_name, "auth.policy", auth_policy);
g_key_file_set_string(accounts, sanitized_account_name, "auth.policy", auth_policy);
}
auto_jid Jid* jidp = jid_create(barejid);
if (jidp->localpart == NULL) {
g_key_file_set_string(accounts, account_name, "muc.nick", jidp->domainpart);
g_key_file_set_string(accounts, sanitized_account_name, "muc.nick", jidp->domainpart);
} else {
g_key_file_set_string(accounts, account_name, "muc.nick", jidp->localpart);
g_key_file_set_string(accounts, sanitized_account_name, "muc.nick", jidp->localpart);
}
g_key_file_set_string(accounts, account_name, "presence.last", "online");
g_key_file_set_string(accounts, account_name, "presence.login", "online");
g_key_file_set_integer(accounts, account_name, "priority.online", 0);
g_key_file_set_integer(accounts, account_name, "priority.chat", 0);
g_key_file_set_integer(accounts, account_name, "priority.away", 0);
g_key_file_set_integer(accounts, account_name, "priority.xa", 0);
g_key_file_set_integer(accounts, account_name, "priority.dnd", 0);
g_key_file_set_string(accounts, sanitized_account_name, "presence.last", "online");
g_key_file_set_string(accounts, sanitized_account_name, "presence.login", "online");
g_key_file_set_integer(accounts, sanitized_account_name, "priority.online", 0);
g_key_file_set_integer(accounts, sanitized_account_name, "priority.chat", 0);
g_key_file_set_integer(accounts, sanitized_account_name, "priority.away", 0);
g_key_file_set_integer(accounts, sanitized_account_name, "priority.xa", 0);
g_key_file_set_integer(accounts, sanitized_account_name, "priority.dnd", 0);
_save_accounts();
autocomplete_add(all_ac, account_name);
autocomplete_add(enabled_ac, account_name);
_accounts_save(account_name);
autocomplete_add(all_ac, sanitized_account_name);
autocomplete_add(enabled_ac, sanitized_account_name);
}
int
gboolean
accounts_remove(const char* account_name)
{
int r = g_key_file_remove_group(accounts, account_name, NULL);
_save_accounts();
autocomplete_remove(all_ac, account_name);
autocomplete_remove(enabled_ac, account_name);
auto_gchar gchar* sanitized_account_name = _sanitize_account_name(account_name);
gboolean r = g_key_file_remove_group(accounts, sanitized_account_name, NULL);
_accounts_save(account_name);
autocomplete_remove(all_ac, sanitized_account_name);
autocomplete_remove(enabled_ac, sanitized_account_name);
return r;
}
@@ -189,167 +236,123 @@ accounts_get_list(void)
return g_key_file_get_groups(accounts, NULL);
}
ProfAccount*
accounts_get_account(const char* const name)
static GList*
_g_strv_to_glist(gchar** in, gsize length)
{
if (!g_key_file_has_group(accounts, name)) {
if (in == NULL)
return NULL;
GList* out = NULL;
for (gsize i = 0; i < length; i++) {
out = g_list_append(out, in[i]);
}
g_free(in);
return out;
}
static GList*
_accounts_get_glist(const gchar* sanitized_account_name,
const gchar* key)
{
gsize length = 0;
gchar** list = g_key_file_get_string_list(accounts, sanitized_account_name, key, &length, NULL);
return _g_strv_to_glist(list, length);
}
ProfAccount*
accounts_get_account(const char* const account_name)
{
auto_gchar gchar* sanitized_account_name = _sanitize_account_name(account_name);
if (!_accounts_has_group(sanitized_account_name)) {
return NULL;
} else {
gchar* jid = g_key_file_get_string(accounts, name, "jid", NULL);
gchar* jid = g_key_file_get_string(accounts, sanitized_account_name, "jid", NULL);
// fix accounts that have no jid property by setting to name
// fix accounts that have no jid property by setting to account_name
if (jid == NULL) {
g_key_file_set_string(accounts, name, "jid", name);
_save_accounts();
g_key_file_set_string(accounts, sanitized_account_name, "jid", account_name);
_accounts_save(account_name);
}
gchar* password = g_key_file_get_string(accounts, name, "password", NULL);
gchar* eval_password = g_key_file_get_string(accounts, name, "eval_password", NULL);
gboolean enabled = g_key_file_get_boolean(accounts, name, "enabled", NULL);
gchar* password = g_key_file_get_string(accounts, sanitized_account_name, "password", NULL);
gchar* eval_password = g_key_file_get_string(accounts, sanitized_account_name, "eval_password", NULL);
gboolean enabled = g_key_file_get_boolean(accounts, sanitized_account_name, "enabled", NULL);
gchar* server = g_key_file_get_string(accounts, name, "server", NULL);
gchar* resource = g_key_file_get_string(accounts, name, "resource", NULL);
int port = g_key_file_get_integer(accounts, name, "port", NULL);
gchar* server = g_key_file_get_string(accounts, sanitized_account_name, "server", NULL);
gchar* resource = g_key_file_get_string(accounts, sanitized_account_name, "resource", NULL);
int port = g_key_file_get_integer(accounts, sanitized_account_name, "port", NULL);
gchar* last_presence = g_key_file_get_string(accounts, name, "presence.last", NULL);
gchar* login_presence = g_key_file_get_string(accounts, name, "presence.login", NULL);
gchar* last_presence = g_key_file_get_string(accounts, sanitized_account_name, "presence.last", NULL);
gchar* login_presence = g_key_file_get_string(accounts, sanitized_account_name, "presence.login", NULL);
int priority_online = g_key_file_get_integer(accounts, name, "priority.online", NULL);
int priority_chat = g_key_file_get_integer(accounts, name, "priority.chat", NULL);
int priority_away = g_key_file_get_integer(accounts, name, "priority.away", NULL);
int priority_xa = g_key_file_get_integer(accounts, name, "priority.xa", NULL);
int priority_dnd = g_key_file_get_integer(accounts, name, "priority.dnd", NULL);
int priority_online = g_key_file_get_integer(accounts, sanitized_account_name, "priority.online", NULL);
int priority_chat = g_key_file_get_integer(accounts, sanitized_account_name, "priority.chat", NULL);
int priority_away = g_key_file_get_integer(accounts, sanitized_account_name, "priority.away", NULL);
int priority_xa = g_key_file_get_integer(accounts, sanitized_account_name, "priority.xa", NULL);
int priority_dnd = g_key_file_get_integer(accounts, sanitized_account_name, "priority.dnd", NULL);
gchar* muc_service = NULL;
if (g_key_file_has_key(accounts, name, "muc.service", NULL)) {
muc_service = g_key_file_get_string(accounts, name, "muc.service", NULL);
if (g_key_file_has_key(accounts, sanitized_account_name, "muc.service", NULL)) {
muc_service = g_key_file_get_string(accounts, sanitized_account_name, "muc.service", NULL);
} else {
jabber_conn_status_t conn_status = connection_get_status();
if (conn_status == JABBER_CONNECTED) {
const char* conf_jid = connection_jid_for_feature(XMPP_FEATURE_MUC);
if (conf_jid) {
muc_service = strdup(conf_jid);
muc_service = g_strdup(conf_jid);
}
}
}
gchar* muc_nick = g_key_file_get_string(accounts, name, "muc.nick", NULL);
gchar* muc_nick = g_key_file_get_string(accounts, sanitized_account_name, "muc.nick", NULL);
gchar* otr_policy = NULL;
if (g_key_file_has_key(accounts, name, "otr.policy", NULL)) {
otr_policy = g_key_file_get_string(accounts, name, "otr.policy", NULL);
}
gchar* otr_policy = g_key_file_get_string(accounts, sanitized_account_name, "otr.policy", NULL);
GList* otr_manual = _accounts_get_glist(sanitized_account_name, "otr.manual");
GList* otr_opportunistic = _accounts_get_glist(sanitized_account_name, "otr.opportunistic");
GList* otr_always = _accounts_get_glist(sanitized_account_name, "otr.always");
gsize length;
GList* otr_manual = NULL;
auto_gcharv gchar** manual = g_key_file_get_string_list(accounts, name, "otr.manual", &length, NULL);
if (manual) {
for (int i = 0; i < length; i++) {
otr_manual = g_list_append(otr_manual, strdup(manual[i]));
}
}
gchar* omemo_policy = g_key_file_get_string(accounts, sanitized_account_name, "omemo.policy", NULL);
GList* omemo_enabled = _accounts_get_glist(sanitized_account_name, "omemo.enabled");
GList* omemo_disabled = _accounts_get_glist(sanitized_account_name, "omemo.disabled");
GList* otr_opportunistic = NULL;
auto_gcharv gchar** opportunistic = g_key_file_get_string_list(accounts, name, "otr.opportunistic", &length, NULL);
if (opportunistic) {
for (int i = 0; i < length; i++) {
otr_opportunistic = g_list_append(otr_opportunistic, strdup(opportunistic[i]));
}
}
GList* ox_enabled = _accounts_get_glist(sanitized_account_name, "ox.enabled");
GList* otr_always = NULL;
auto_gcharv gchar** always = g_key_file_get_string_list(accounts, name, "otr.always", &length, NULL);
if (always) {
for (int i = 0; i < length; i++) {
otr_always = g_list_append(otr_always, strdup(always[i]));
}
}
GList* pgp_enabled = _accounts_get_glist(sanitized_account_name, "pgp.enabled");
gchar* omemo_policy = NULL;
if (g_key_file_has_key(accounts, name, "omemo.policy", NULL)) {
omemo_policy = g_key_file_get_string(accounts, name, "omemo.policy", NULL);
}
gchar* pgp_keyid = g_key_file_get_string(accounts, sanitized_account_name, "pgp.keyid", NULL);
GList* omemo_enabled = NULL;
auto_gcharv gchar** omemo_enabled_list = g_key_file_get_string_list(accounts, name, "omemo.enabled", &length, NULL);
if (omemo_enabled_list) {
for (int i = 0; i < length; i++) {
omemo_enabled = g_list_append(omemo_enabled, strdup(omemo_enabled_list[i]));
}
}
gchar* startscript = g_key_file_get_string(accounts, sanitized_account_name, "script.start", NULL);
GList* omemo_disabled = NULL;
auto_gcharv gchar** omemo_disabled_list = g_key_file_get_string_list(accounts, name, "omemo.disabled", &length, NULL);
if (omemo_disabled_list) {
for (int i = 0; i < length; i++) {
omemo_disabled = g_list_append(omemo_disabled, strdup(omemo_disabled_list[i]));
}
}
gchar* client = g_key_file_get_string(accounts, sanitized_account_name, "client.account_name", NULL);
GList* ox_enabled = NULL;
auto_gcharv gchar** ox_enabled_list = g_key_file_get_string_list(accounts, name, "ox.enabled", &length, NULL);
if (ox_enabled_list) {
for (int i = 0; i < length; i++) {
ox_enabled = g_list_append(ox_enabled, strdup(ox_enabled_list[i]));
}
}
gchar* theme = g_key_file_get_string(accounts, sanitized_account_name, "theme", NULL);
GList* pgp_enabled = NULL;
auto_gcharv gchar** pgp_enabled_list = g_key_file_get_string_list(accounts, name, "pgp.enabled", &length, NULL);
if (pgp_enabled_list) {
for (int i = 0; i < length; i++) {
pgp_enabled = g_list_append(pgp_enabled, strdup(pgp_enabled_list[i]));
}
}
gchar* pgp_keyid = NULL;
if (g_key_file_has_key(accounts, name, "pgp.keyid", NULL)) {
pgp_keyid = g_key_file_get_string(accounts, name, "pgp.keyid", NULL);
}
gchar* startscript = NULL;
if (g_key_file_has_key(accounts, name, "script.start", NULL)) {
startscript = g_key_file_get_string(accounts, name, "script.start", NULL);
}
gchar* client = NULL;
if (g_key_file_has_key(accounts, name, "client.name", NULL)) {
client = g_key_file_get_string(accounts, name, "client.name", NULL);
}
gchar* theme = NULL;
if (g_key_file_has_key(accounts, name, "theme", NULL)) {
theme = g_key_file_get_string(accounts, name, "theme", NULL);
}
gchar* tls_policy = g_key_file_get_string(accounts, name, "tls.policy", NULL);
if (tls_policy && ((g_strcmp0(tls_policy, "force") != 0) && (g_strcmp0(tls_policy, "allow") != 0) && (g_strcmp0(tls_policy, "trust") != 0) && (g_strcmp0(tls_policy, "disable") != 0) && (g_strcmp0(tls_policy, "legacy") != 0))) {
gchar* tls_policy = g_key_file_get_string(accounts, sanitized_account_name, "tls.policy", NULL);
if (tls_policy && !valid_tls_policy_option(tls_policy)) {
g_free(tls_policy);
tls_policy = NULL;
}
gchar* auth_policy = g_key_file_get_string(accounts, name, "auth.policy", NULL);
gchar* auth_policy = g_key_file_get_string(accounts, sanitized_account_name, "auth.policy", NULL);
int max_sessions = g_key_file_get_integer(accounts, name, "max.sessions", 0);
int max_sessions = g_key_file_get_integer(accounts, sanitized_account_name, "max.sessions", 0);
ProfAccount* new_account = account_new(g_strdup(name), jid, password, eval_password, enabled,
server, port, resource, last_presence, login_presence,
priority_online, priority_chat, priority_away, priority_xa,
priority_dnd, muc_service, muc_nick, otr_policy, otr_manual,
otr_opportunistic, otr_always, omemo_policy, omemo_enabled,
omemo_disabled, ox_enabled, pgp_enabled, pgp_keyid,
startscript, theme, tls_policy, auth_policy, client, max_sessions);
return new_account;
return account_new(g_strdup(account_name), jid, password, eval_password, enabled,
server, port, resource, last_presence, login_presence,
priority_online, priority_chat, priority_away, priority_xa,
priority_dnd, muc_service, muc_nick, otr_policy, otr_manual,
otr_opportunistic, otr_always, omemo_policy, omemo_enabled,
omemo_disabled, ox_enabled, pgp_enabled, pgp_keyid,
startscript, theme, tls_policy, auth_policy, client, max_sessions);
}
}
gboolean
accounts_enable(const char* const name)
accounts_enable(const char* const account_name)
{
if (g_key_file_has_group(accounts, name)) {
g_key_file_set_boolean(accounts, name, "enabled", TRUE);
_save_accounts();
autocomplete_add(enabled_ac, name);
auto_gchar gchar* sanitized_account_name = _sanitize_account_name(account_name);
if (_accounts_has_group(sanitized_account_name)) {
g_key_file_set_boolean(accounts, sanitized_account_name, "enabled", TRUE);
_accounts_save(account_name);
autocomplete_add(enabled_ac, sanitized_account_name);
return TRUE;
} else {
return FALSE;
@@ -357,12 +360,13 @@ accounts_enable(const char* const name)
}
gboolean
accounts_disable(const char* const name)
accounts_disable(const char* const account_name)
{
if (g_key_file_has_group(accounts, name)) {
g_key_file_set_boolean(accounts, name, "enabled", FALSE);
_save_accounts();
autocomplete_remove(enabled_ac, name);
auto_gchar gchar* sanitized_account_name = _sanitize_account_name(account_name);
if (_accounts_has_group(sanitized_account_name)) {
g_key_file_set_boolean(accounts, sanitized_account_name, "enabled", FALSE);
_accounts_save(account_name);
autocomplete_remove(enabled_ac, sanitized_account_name);
return TRUE;
} else {
return FALSE;
@@ -372,63 +376,33 @@ accounts_disable(const char* const name)
gboolean
accounts_rename(const char* const account_name, const char* const new_name)
{
if (g_key_file_has_group(accounts, new_name)) {
auto_gchar gchar* sanitized_account_name = _sanitize_account_name(account_name);
auto_gchar gchar* sanitized_new_account_name = _sanitize_account_name(new_name);
if (_accounts_has_group(sanitized_new_account_name)) {
return FALSE;
}
if (!g_key_file_has_group(accounts, account_name)) {
if (!_accounts_has_group(sanitized_account_name)) {
return FALSE;
}
// treat all properties as strings for copy
gchar* string_keys[] = {
"enabled",
"jid",
"server",
"port",
"resource",
"password",
"eval_password",
"presence.last",
"presence.laststatus",
"presence.login",
"priority.online",
"priority.chat",
"priority.away",
"priority.xa",
"priority.dnd",
"muc.service",
"muc.nick",
"otr.policy",
"otr.manual",
"otr.opportunistic",
"otr.always",
"omemo.policy",
"omemo.enabled",
"omemo.disabled",
"ox.enabled",
"pgp.enabled",
"pgp.keyid",
"last.activity",
"script.start",
"tls.policy"
};
for (int i = 0; i < ARRAY_SIZE(string_keys); i++) {
auto_gchar gchar* value = g_key_file_get_string(accounts, account_name, string_keys[i], NULL);
if (value) {
g_key_file_set_string(accounts, new_name, string_keys[i], value);
}
gsize nkeys;
auto_gcharv gchar** keys = g_key_file_get_keys(accounts, sanitized_account_name, &nkeys, NULL);
for (gsize i = 0; i < nkeys; i++) {
auto_gchar gchar* new_value = g_key_file_get_value(accounts, sanitized_account_name, keys[i], NULL);
g_key_file_set_value(accounts, sanitized_new_account_name, keys[i], new_value);
}
g_key_file_remove_group(accounts, account_name, NULL);
_save_accounts();
g_key_file_remove_group(accounts, sanitized_account_name, NULL);
_accounts_save(account_name);
_accounts_save(new_name);
autocomplete_remove(all_ac, account_name);
autocomplete_add(all_ac, new_name);
if (g_key_file_get_boolean(accounts, new_name, "enabled", NULL)) {
autocomplete_remove(enabled_ac, account_name);
autocomplete_add(enabled_ac, new_name);
autocomplete_remove(all_ac, sanitized_account_name);
autocomplete_remove(enabled_ac, sanitized_account_name);
autocomplete_add(all_ac, sanitized_new_account_name);
if (g_key_file_get_boolean(accounts, sanitized_new_account_name, "enabled", NULL)) {
autocomplete_add(enabled_ac, sanitized_new_account_name);
}
return TRUE;
@@ -437,7 +411,7 @@ accounts_rename(const char* const account_name, const char* const new_name)
gboolean
accounts_account_exists(const char* const account_name)
{
return g_key_file_has_group(accounts, account_name);
return _accounts_has_group(account_name);
}
void
@@ -445,19 +419,20 @@ accounts_set_jid(const char* const account_name, const char* const value)
{
auto_jid Jid* jid = jid_create(value);
if (jid) {
if (accounts_account_exists(account_name)) {
g_key_file_set_string(accounts, account_name, "jid", jid->barejid);
auto_gchar gchar* sanitized_account_name = _sanitize_account_name(account_name);
if (_accounts_has_group(sanitized_account_name)) {
g_key_file_set_string(accounts, sanitized_account_name, "jid", jid->barejid);
if (jid->resourcepart) {
g_key_file_set_string(accounts, account_name, "resource", jid->resourcepart);
g_key_file_set_string(accounts, sanitized_account_name, "resource", jid->resourcepart);
}
if (jid->localpart == NULL) {
g_key_file_set_string(accounts, account_name, "muc.nick", jid->domainpart);
g_key_file_set_string(accounts, sanitized_account_name, "muc.nick", jid->domainpart);
} else {
g_key_file_set_string(accounts, account_name, "muc.nick", jid->localpart);
g_key_file_set_string(accounts, sanitized_account_name, "muc.nick", jid->localpart);
}
_save_accounts();
_accounts_save(account_name);
}
}
}
@@ -465,9 +440,10 @@ accounts_set_jid(const char* const account_name, const char* const value)
void
accounts_set_server(const char* const account_name, const char* const value)
{
if (accounts_account_exists(account_name)) {
g_key_file_set_string(accounts, account_name, "server", value);
_save_accounts();
auto_gchar gchar* sanitized_account_name = _sanitize_account_name(account_name);
if (_accounts_has_group(sanitized_account_name)) {
g_key_file_set_string(accounts, sanitized_account_name, "server", value);
_accounts_save(account_name);
}
}
@@ -475,35 +451,41 @@ void
accounts_set_port(const char* const account_name, const int value)
{
if (value != 0) {
g_key_file_set_integer(accounts, account_name, "port", value);
_save_accounts();
auto_gchar gchar* sanitized_account_name = _sanitize_account_name(account_name);
if (_accounts_has_group(sanitized_account_name)) {
g_key_file_set_integer(accounts, sanitized_account_name, "port", value);
_accounts_save(account_name);
}
}
}
static void
_accounts_set_string_option(const char* account_name, const char* const option, const char* const value)
{
if (accounts_account_exists(account_name)) {
g_key_file_set_string(accounts, account_name, option, value ?: "");
_save_accounts();
auto_gchar gchar* sanitized_account_name = _sanitize_account_name(account_name);
if (_accounts_has_group(sanitized_account_name)) {
g_key_file_set_string(accounts, sanitized_account_name, option, value ?: "");
_accounts_save(account_name);
}
}
static void
_accounts_set_int_option(const char* account_name, const char* const option, int value)
{
if (accounts_account_exists(account_name)) {
g_key_file_set_integer(accounts, account_name, option, value);
_save_accounts();
auto_gchar gchar* sanitized_account_name = _sanitize_account_name(account_name);
if (_accounts_has_group(sanitized_account_name)) {
g_key_file_set_integer(accounts, sanitized_account_name, option, value);
_accounts_save(account_name);
}
}
static void
_accounts_clear_string_option(const char* account_name, const char* const option)
{
if (accounts_account_exists(account_name)) {
g_key_file_remove_key(accounts, account_name, option, NULL);
_save_accounts();
auto_gchar gchar* sanitized_account_name = _sanitize_account_name(account_name);
if (_accounts_has_group(sanitized_account_name)) {
g_key_file_remove_key(accounts, sanitized_account_name, option, NULL);
_accounts_save(account_name);
}
}
@@ -630,83 +612,88 @@ accounts_clear_max_sessions(const char* const account_name)
void
accounts_add_otr_policy(const char* const account_name, const char* const contact_jid, const char* const policy)
{
if (accounts_account_exists(account_name)) {
auto_gchar gchar* sanitized_account_name = _sanitize_account_name(account_name);
if (_accounts_has_group(sanitized_account_name)) {
GString* key = g_string_new("otr.");
g_string_append(key, policy);
conf_string_list_add(accounts, account_name, key->str, contact_jid);
conf_string_list_add(accounts, sanitized_account_name, key->str, contact_jid);
g_string_free(key, TRUE);
// check for and remove from other lists
if (strcmp(policy, "manual") == 0) {
conf_string_list_remove(accounts, account_name, "otr.opportunistic", contact_jid);
conf_string_list_remove(accounts, account_name, "otr.always", contact_jid);
conf_string_list_remove(accounts, sanitized_account_name, "otr.opportunistic", contact_jid);
conf_string_list_remove(accounts, sanitized_account_name, "otr.always", contact_jid);
}
if (strcmp(policy, "opportunistic") == 0) {
conf_string_list_remove(accounts, account_name, "otr.manual", contact_jid);
conf_string_list_remove(accounts, account_name, "otr.always", contact_jid);
conf_string_list_remove(accounts, sanitized_account_name, "otr.manual", contact_jid);
conf_string_list_remove(accounts, sanitized_account_name, "otr.always", contact_jid);
}
if (strcmp(policy, "always") == 0) {
conf_string_list_remove(accounts, account_name, "otr.opportunistic", contact_jid);
conf_string_list_remove(accounts, account_name, "otr.manual", contact_jid);
conf_string_list_remove(accounts, sanitized_account_name, "otr.opportunistic", contact_jid);
conf_string_list_remove(accounts, sanitized_account_name, "otr.manual", contact_jid);
}
_save_accounts();
_accounts_save(account_name);
}
}
void
accounts_add_omemo_state(const char* const account_name, const char* const contact_jid, gboolean enabled)
{
if (accounts_account_exists(account_name)) {
auto_gchar gchar* sanitized_account_name = _sanitize_account_name(account_name);
if (_accounts_has_group(sanitized_account_name)) {
if (enabled) {
conf_string_list_add(accounts, account_name, "omemo.enabled", contact_jid);
conf_string_list_remove(accounts, account_name, "omemo.disabled", contact_jid);
conf_string_list_add(accounts, sanitized_account_name, "omemo.enabled", contact_jid);
conf_string_list_remove(accounts, sanitized_account_name, "omemo.disabled", contact_jid);
} else {
conf_string_list_add(accounts, account_name, "omemo.disabled", contact_jid);
conf_string_list_remove(accounts, account_name, "omemo.enabled", contact_jid);
conf_string_list_add(accounts, sanitized_account_name, "omemo.disabled", contact_jid);
conf_string_list_remove(accounts, sanitized_account_name, "omemo.enabled", contact_jid);
}
_save_accounts();
_accounts_save(account_name);
}
}
void
accounts_add_ox_state(const char* const account_name, const char* const contact_jid, gboolean enabled)
{
if (accounts_account_exists(account_name)) {
auto_gchar gchar* sanitized_account_name = _sanitize_account_name(account_name);
if (_accounts_has_group(sanitized_account_name)) {
if (enabled) {
conf_string_list_add(accounts, account_name, "ox.enabled", contact_jid);
conf_string_list_add(accounts, sanitized_account_name, "ox.enabled", contact_jid);
} else {
conf_string_list_remove(accounts, account_name, "ox.enabled", contact_jid);
conf_string_list_remove(accounts, sanitized_account_name, "ox.enabled", contact_jid);
}
_save_accounts();
_accounts_save(account_name);
}
}
void
accounts_add_pgp_state(const char* const account_name, const char* const contact_jid, gboolean enabled)
{
if (accounts_account_exists(account_name)) {
auto_gchar gchar* sanitized_account_name = _sanitize_account_name(account_name);
if (_accounts_has_group(sanitized_account_name)) {
if (enabled) {
conf_string_list_add(accounts, account_name, "pgp.enabled", contact_jid);
conf_string_list_remove(accounts, account_name, "pgp.disabled", contact_jid);
conf_string_list_add(accounts, sanitized_account_name, "pgp.enabled", contact_jid);
conf_string_list_remove(accounts, sanitized_account_name, "pgp.disabled", contact_jid);
} else {
conf_string_list_add(accounts, account_name, "pgp.disabled", contact_jid);
conf_string_list_remove(accounts, account_name, "pgp.enabled", contact_jid);
conf_string_list_add(accounts, sanitized_account_name, "pgp.disabled", contact_jid);
conf_string_list_remove(accounts, sanitized_account_name, "pgp.enabled", contact_jid);
}
_save_accounts();
_accounts_save(account_name);
}
}
void
accounts_clear_omemo_state(const char* const account_name, const char* const contact_jid)
{
if (accounts_account_exists(account_name)) {
conf_string_list_remove(accounts, account_name, "omemo.enabled", contact_jid);
conf_string_list_remove(accounts, account_name, "omemo.disabled", contact_jid);
_save_accounts();
auto_gchar gchar* sanitized_account_name = _sanitize_account_name(account_name);
if (_accounts_has_group(sanitized_account_name)) {
conf_string_list_remove(accounts, sanitized_account_name, "omemo.enabled", contact_jid);
conf_string_list_remove(accounts, sanitized_account_name, "omemo.disabled", contact_jid);
_accounts_save(account_name);
}
}
@@ -779,7 +766,8 @@ accounts_set_priority_dnd(const char* const account_name, const gint value)
void
accounts_set_priority_all(const char* const account_name, const gint value)
{
if (accounts_account_exists(account_name)) {
auto_gchar gchar* sanitized_account_name = _sanitize_account_name(account_name);
if (_accounts_has_group(sanitized_account_name)) {
accounts_set_priority_online(account_name, value);
accounts_set_priority_chat(account_name, value);
accounts_set_priority_away(account_name, value);
@@ -793,22 +781,23 @@ accounts_get_priority_for_presence_type(const char* const account_name,
resource_presence_t presence_type)
{
gint result;
auto_gchar gchar* sanitized_account_name = _sanitize_account_name(account_name);
switch (presence_type) {
case (RESOURCE_ONLINE):
result = g_key_file_get_integer(accounts, account_name, "priority.online", NULL);
result = g_key_file_get_integer(accounts, sanitized_account_name, "priority.online", NULL);
break;
case (RESOURCE_CHAT):
result = g_key_file_get_integer(accounts, account_name, "priority.chat", NULL);
result = g_key_file_get_integer(accounts, sanitized_account_name, "priority.chat", NULL);
break;
case (RESOURCE_AWAY):
result = g_key_file_get_integer(accounts, account_name, "priority.away", NULL);
result = g_key_file_get_integer(accounts, sanitized_account_name, "priority.away", NULL);
break;
case (RESOURCE_XA):
result = g_key_file_get_integer(accounts, account_name, "priority.xa", NULL);
result = g_key_file_get_integer(accounts, sanitized_account_name, "priority.xa", NULL);
break;
default:
result = g_key_file_get_integer(accounts, account_name, "priority.dnd", NULL);
result = g_key_file_get_integer(accounts, sanitized_account_name, "priority.dnd", NULL);
break;
}
@@ -833,7 +822,8 @@ accounts_set_last_status(const char* const account_name, const char* const value
void
accounts_set_last_activity(const char* const account_name)
{
if (accounts_account_exists(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);
@@ -841,8 +831,8 @@ accounts_set_last_activity(const char* const account_name)
if (res) {
auto_char char* timestr = g_time_val_to_iso8601(&nowtv);
g_key_file_set_string(accounts, account_name, "last.activity", timestr);
_save_accounts();
g_key_file_set_string(accounts, sanitized_account_name, "last.activity", timestr);
_accounts_save(account_name);
}
}
}
@@ -850,8 +840,9 @@ accounts_set_last_activity(const char* const account_name)
gchar*
accounts_get_last_activity(const char* const account_name)
{
if (accounts_account_exists(account_name)) {
return g_key_file_get_string(accounts, account_name, "last.activity", NULL);
auto_gchar gchar* sanitized_account_name = _sanitize_account_name(account_name);
if (_accounts_has_group(sanitized_account_name)) {
return g_key_file_get_string(accounts, sanitized_account_name, "last.activity", NULL);
} else {
return NULL;
}
@@ -860,27 +851,30 @@ accounts_get_last_activity(const char* const account_name)
gchar*
accounts_get_resource(const char* const account_name)
{
if (!accounts_account_exists(account_name)) {
auto_gchar gchar* sanitized_account_name = _sanitize_account_name(account_name);
if (!_accounts_has_group(sanitized_account_name)) {
return NULL;
}
return g_key_file_get_string(accounts, account_name, "resource", NULL);
return g_key_file_get_string(accounts, sanitized_account_name, "resource", NULL);
}
int
accounts_get_max_sessions(const char* const account_name)
{
if (!accounts_account_exists(account_name)) {
auto_gchar gchar* sanitized_account_name = _sanitize_account_name(account_name);
if (!_accounts_has_group(sanitized_account_name)) {
return 0;
}
return g_key_file_get_integer(accounts, account_name, "max.sessions", 0);
return g_key_file_get_integer(accounts, sanitized_account_name, "max.sessions", 0);
}
void
accounts_set_login_presence(const char* const account_name, const char* const value)
{
if (accounts_account_exists(account_name)) {
g_key_file_set_string(accounts, account_name, "presence.login", value);
_save_accounts();
auto_gchar gchar* sanitized_account_name = _sanitize_account_name(account_name);
if (_accounts_has_group(sanitized_account_name)) {
g_key_file_set_string(accounts, sanitized_account_name, "presence.login", value);
_accounts_save(account_name);
}
}
@@ -888,7 +882,8 @@ resource_presence_t
accounts_get_last_presence(const char* const account_name)
{
resource_presence_t result;
auto_gchar gchar* setting = g_key_file_get_string(accounts, account_name, "presence.last", NULL);
auto_gchar gchar* sanitized_account_name = _sanitize_account_name(account_name);
auto_gchar gchar* setting = g_key_file_get_string(accounts, sanitized_account_name, "presence.last", NULL);
if (setting == NULL || (strcmp(setting, "online") == 0)) {
result = RESOURCE_ONLINE;
@@ -912,14 +907,16 @@ accounts_get_last_presence(const char* const account_name)
char*
accounts_get_last_status(const char* const account_name)
{
return g_key_file_get_string(accounts, account_name, "presence.laststatus", NULL);
auto_gchar gchar* sanitized_account_name = _sanitize_account_name(account_name);
return g_key_file_get_string(accounts, sanitized_account_name, "presence.laststatus", NULL);
}
resource_presence_t
accounts_get_login_presence(const char* const account_name)
{
resource_presence_t result;
auto_gchar gchar* setting = g_key_file_get_string(accounts, account_name, "presence.login", NULL);
auto_gchar gchar* sanitized_account_name = _sanitize_account_name(account_name);
auto_gchar gchar* setting = g_key_file_get_string(accounts, sanitized_account_name, "presence.login", NULL);
if (setting == NULL || (strcmp(setting, "online") == 0)) {
result = RESOURCE_ONLINE;
@@ -944,7 +941,8 @@ accounts_get_login_presence(const char* const account_name)
char*
accounts_get_login_status(const char* const account_name)
{
auto_gchar gchar* setting = g_key_file_get_string(accounts, account_name, "presence.login", NULL);
auto_gchar gchar* sanitized_account_name = _sanitize_account_name(account_name);
auto_gchar gchar* setting = g_key_file_get_string(accounts, sanitized_account_name, "presence.login", NULL);
gchar* status = NULL;
if (g_strcmp0(setting, "last") == 0) {
@@ -953,9 +951,3 @@ accounts_get_login_status(const char* const account_name)
return status;
}
static void
_save_accounts(void)
{
save_keyfile(&accounts_prof_keyfile);
}

View File

@@ -4,33 +4,7 @@
*
* Copyright (C) 2012 - 2019 James Booth <boothj5@gmail.com>
*
* This file is part of Profanity.
*
* Profanity is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Profanity is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Profanity. If not, see <https://www.gnu.org/licenses/>.
*
* In addition, as a special exception, the copyright holders give permission to
* link the code of portions of this program with the OpenSSL library under
* certain conditions as described in each individual source file, and
* distribute linked combinations including the two.
*
* You must obey the GNU General Public License in all respects for all of the
* code used other than OpenSSL. If you modify file(s) with this exception, you
* may extend this exception to your version of the file(s), but you are not
* obligated to do so. If you do not wish to do so, delete this exception
* statement from your version. If you delete this exception statement from all
* source files in the program, then also delete it here.
*
* SPDX-License-Identifier: GPL-3.0-or-later WITH OpenSSL-exception
*/
#ifndef CONFIG_ACCOUNTS_H
@@ -48,7 +22,7 @@ char* accounts_find_enabled(const char* const prefix, gboolean previous, void* c
void accounts_reset_all_search(void);
void accounts_reset_enabled_search(void);
void accounts_add(const char* jid, const char* altdomain, const int port, const char* const tls_policy, const char* const auth_policy);
int accounts_remove(const char* jid);
gboolean accounts_remove(const char* jid);
gchar** accounts_get_list(void);
ProfAccount* accounts_get_account(const char* const name);
gboolean accounts_enable(const char* const name);

View File

@@ -4,33 +4,7 @@
*
* Copyright (C) 2022 Steffen Jaeckel <jaeckel-floss@eyet-services.de>
*
* This file is part of Profanity.
*
* Profanity is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Profanity is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Profanity. If not, see <https://www.gnu.org/licenses/>.
*
* In addition, as a special exception, the copyright holders give permission to
* link the code of portions of this program with the OpenSSL library under
* certain conditions as described in each individual source file, and
* distribute linked combinations including the two.
*
* You must obey the GNU General Public License in all respects for all of the
* code used other than OpenSSL. If you modify file(s) with this exception, you
* may extend this exception to your version of the file(s), but you are not
* obligated to do so. If you do not wish to do so, delete this exception
* statement from your version. If you delete this exception statement from all
* source files in the program, then also delete it here.
*
* SPDX-License-Identifier: GPL-3.0-or-later WITH OpenSSL-exception
*/
#include "config.h"
@@ -53,8 +27,7 @@ _cafile_name(void)
if (!create_dir(certs_dir)) {
return NULL;
}
gchar* filename = g_strdup_printf("%s/CAfile.pem", certs_dir);
return filename;
return g_strdup_printf("%s/CAfile.pem", certs_dir);
}
void
@@ -70,21 +43,25 @@ cafile_add(const TLSCertificate* cert)
auto_gchar gchar* contents = NULL;
auto_gchar gchar* new_contents = NULL;
gsize length;
GError* glib_error = NULL;
auto_gerror GError* glib_error = NULL;
if (g_file_test(cafile, G_FILE_TEST_EXISTS)) {
if (!g_file_get_contents(cafile, &contents, &length, &glib_error)) {
log_error("[CAfile] could not read from %s: %s", cafile, glib_error ? glib_error->message : "No GLib error given");
log_error("[CAfile] could not read from %s: %s", cafile, PROF_GERROR_MESSAGE(glib_error));
return;
}
if (strstr(contents, cert->fingerprint)) {
log_debug("[CAfile] fingerprint %s already stored", cert->fingerprint);
return;
}
if (strstr(contents, cert->fingerprint_sha1)) {
log_debug("[CAfile] fingerprint %s already stored", cert->fingerprint_sha1);
return;
}
}
const char* header = "# Profanity CAfile\n# DO NOT EDIT - this file is automatically generated";
new_contents = g_strdup_printf("%s\n\n# %s\n%s", contents ? contents : header, cert->fingerprint, cert->pem);
new_contents = g_strdup_printf("%s\n# %s\n%s", contents ? contents : header, cert->fingerprint, cert->pem);
if (!g_file_set_contents(cafile, new_contents, -1, &glib_error))
log_error("[CAfile] could not write to %s: %s", cafile, glib_error ? glib_error->message : "No GLib error given");
log_error("[CAfile] could not write to %s: %s", cafile, PROF_GERROR_MESSAGE(glib_error));
}
gchar*

View File

@@ -4,33 +4,7 @@
*
* Copyright (C) 2022 Steffen Jaeckel <jaeckel-floss@eyet-services.de>
*
* This file is part of Profanity.
*
* Profanity is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Profanity is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Profanity. If not, see <https://www.gnu.org/licenses/>.
*
* In addition, as a special exception, the copyright holders give permission to
* link the code of portions of this program with the OpenSSL library under
* certain conditions as described in each individual source file, and
* distribute linked combinations including the two.
*
* You must obey the GNU General Public License in all respects for all of the
* code used other than OpenSSL. If you modify file(s) with this exception, you
* may extend this exception to your version of the file(s), but you are not
* obligated to do so. If you do not wish to do so, delete this exception
* statement from your version. If you delete this exception statement from all
* source files in the program, then also delete it here.
*
* SPDX-License-Identifier: GPL-3.0-or-later WITH OpenSSL-exception
*/
#ifndef CONFIG_CAFILE_H

View File

@@ -3,35 +3,9 @@
* vim: expandtab:ts=4:sts=4:sw=4
*
* Copyright (C) 2019 Aurelien Aptel <aurelien.aptel@gmail.com>
* Copyright (C) 2019 - 2025 Michael Vetter <jubalh@iodoru.org>
*
* This file is part of Profanity.
*
* Profanity is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Profanity is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Profanity. If not, see <https://www.gnu.org/licenses/>.
*
* In addition, as a special exception, the copyright holders give permission to
* link the code of portions of this program with the OpenSSL library under
* certain conditions as described in each individual source file, and
* distribute linked combinations including the two.
*
* You must obey the GNU General Public License in all respects for all of the
* code used other than OpenSSL. If you modify file(s) with this exception, you
* may extend this exception to your version of the file(s), but you are not
* obligated to do so. If you do not wish to do so, delete this exception
* statement from your version. If you delete this exception statement from all
* source files in the program, then also delete it here.
* Copyright (C) 2019 - 2026 Michael Vetter <jubalh@iodoru.org>
*
* SPDX-License-Identifier: GPL-3.0-or-later WITH OpenSSL-exception
*/
#include "config.h"
@@ -59,7 +33,7 @@ static struct color_pair_cache
{
struct
{
int16_t fg, bg;
short fg, bg;
}* pairs;
int size;
int capacity;
@@ -341,14 +315,18 @@ color_distance(const struct color_def* a, const struct color_def* b)
return h * h + s * s + l * l;
}
static int
/* Indices into the 256-colour palette and ncurses palette IDs are
* 16-bit (the size_t-or-short distinction in ncurses init_pair).
* Use `short` end-to-end so the caller boundaries don't need casts.
*/
static short
find_closest_col(int h, int s, int l)
{
struct color_def a = { h, s, l };
int min = 0;
struct color_def a = { (uint16_t)h, (uint8_t)s, (uint8_t)l, NULL };
short min = 0;
int dmin = color_distance(&a, &color_names[0]);
for (int i = 1; i < COLOR_NAME_SIZE; i++) {
for (short i = 1; i < COLOR_NAME_SIZE; i++) {
int d = color_distance(&a, &color_names[i]);
if (d < dmin) {
dmin = d;
@@ -358,8 +336,8 @@ find_closest_col(int h, int s, int l)
return min;
}
static int
find_col(const char* col_name, int n)
static short
find_col(const char* col_name, size_t n)
{
char name[32] = { 0 };
@@ -371,7 +349,7 @@ find_col(const char* col_name, int n)
if (n >= sizeof(name)) {
/* truncate */
log_error("Color: <%s,%d> bigger than %zu", col_name, n, sizeof(name));
log_error("Color: <%s,%zu> bigger than %zu", col_name, n, sizeof(name));
n = sizeof(name) - 1;
}
memcpy(name, col_name, n);
@@ -380,7 +358,7 @@ find_col(const char* col_name, int n)
return -1;
}
for (int i = 0; i < COLOR_NAME_SIZE; i++) {
for (short i = 0; i < COLOR_NAME_SIZE; i++) {
if (g_ascii_strcasecmp(name, color_names[i].name) == 0) {
return i;
}
@@ -389,13 +367,13 @@ find_col(const char* col_name, int n)
return COL_ERR;
}
static int
static short
color_hash(const char* str, color_profile profile)
{
GChecksum* cs = NULL;
guint8 buf[256] = { 0 };
gsize len = 256;
int rc = -1; /* default ncurse color */
short rc = -1; /* default ncurse color */
cs = g_checksum_new(G_CHECKSUM_SHA1);
if (!cs)
@@ -464,7 +442,7 @@ color_pair_cache_reset(void)
}
static int
_color_pair_cache_get(int fg, int bg)
_color_pair_cache_get(short fg, short bg)
{
if (COLORS < 256) {
if (fg > 7 || bg > 7) {
@@ -489,10 +467,13 @@ _color_pair_cache_get(int fg, int bg)
}
int i = cache.size;
// TODO: init_pair uses 15-bit palette indices. Terminals exposing
// extended colour palettes (>32767) need init_extended_pair here.
// Upstream-inherited limitation.
cache.pairs[i].fg = fg;
cache.pairs[i].bg = bg;
/* (re-)define the new pair in curses */
init_pair(i, fg, bg);
init_pair((short)i, fg, bg);
cache.size++;
@@ -511,8 +492,8 @@ _color_pair_cache_get(int fg, int bg)
int
color_pair_cache_hash_str(const char* str, color_profile profile)
{
int fg = color_hash(str, profile);
int bg = -1;
short fg = color_hash(str, profile);
short bg = -1;
auto_char char* bkgnd = theme_get_bkgnd();
if (bkgnd) {
@@ -532,7 +513,7 @@ int
color_pair_cache_get(const char* pair_name)
{
const char* sep;
int fg, bg;
short fg, bg;
sep = strchr(pair_name, '_');
if (!sep) {
@@ -540,7 +521,7 @@ color_pair_cache_get(const char* pair_name)
return -1;
}
fg = find_col(pair_name, sep - pair_name);
fg = find_col(pair_name, (size_t)(sep - pair_name));
bg = find_col(sep + 1, strlen(sep));
if (fg == COL_ERR || bg == COL_ERR) {
log_error("Color: bad color name %s", pair_name);

View File

@@ -4,33 +4,7 @@
*
* Copyright (C) 2019 Aurelien Aptel <aurelien.aptel@gmail.com>
*
* This file is part of Profanity.
*
* Profanity is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Profanity is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Profanity. If not, see <https://www.gnu.org/licenses/>.
*
* In addition, as a special exception, the copyright holders give permission to
* link the code of portions of this program with the OpenSSL library under
* certain conditions as described in each individual source file, and
* distribute linked combinations including the two.
*
* You must obey the GNU General Public License in all respects for all of the
* code used other than OpenSSL. If you modify file(s) with this exception, you
* may extend this exception to your version of the file(s), but you are not
* obligated to do so. If you do not wish to do so, delete this exception
* statement from your version. If you delete this exception statement from all
* source files in the program, then also delete it here.
*
* SPDX-License-Identifier: GPL-3.0-or-later WITH OpenSSL-exception
*/
#ifndef _COLOR_H_

View File

@@ -4,33 +4,7 @@
*
* Copyright (C) 2012 - 2019 James Booth <boothj5@gmail.com>
*
* This file is part of Profanity.
*
* Profanity is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Profanity is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Profanity. If not, see <https://www.gnu.org/licenses/>.
*
* In addition, as a special exception, the copyright holders give permission to
* link the code of portions of this program with the OpenSSL library under
* certain conditions as described in each individual source file, and
* distribute linked combinations including the two.
*
* You must obey the GNU General Public License in all respects for all of the
* code used other than OpenSSL. If you modify file(s) with this exception, you
* may extend this exception to your version of the file(s), but you are not
* obligated to do so. If you do not wish to do so, delete this exception
* statement from your version. If you delete this exception statement from all
* source files in the program, then also delete it here.
*
* SPDX-License-Identifier: GPL-3.0-or-later WITH OpenSSL-exception
*/
#include "config.h"
@@ -66,9 +40,7 @@ conf_string_list_add(GKeyFile* keyfile, const char* const group, const char* con
// Add item to the existing list
const gchar** new_list = g_new(const gchar*, length + 2);
for (gsize i = 0; i < length; ++i) {
new_list[i] = list[i];
}
memcpy(new_list, list, sizeof(list[0]) * length);
new_list[length] = item;
new_list[length + 1] = NULL;
@@ -89,33 +61,28 @@ conf_string_list_remove(GKeyFile* keyfile, const char* const group, const char*
return FALSE;
}
GList* glist = NULL;
gsize new_length = 0;
const gchar** new_list = g_new(const gchar*, length + 1);
gboolean deleted = FALSE;
for (int i = 0; i < length; i++) {
for (gsize i = 0; i < length; i++) {
if (strcmp(list[i], item) == 0) {
deleted = TRUE;
continue;
}
glist = g_list_append(glist, strdup(list[i]));
new_list[new_length++] = list[i];
}
new_list[new_length] = NULL;
if (deleted) {
if (g_list_length(glist) == 0) {
if (new_length == 0) {
g_key_file_remove_key(keyfile, group, key, NULL);
} else {
const gchar* new_list[g_list_length(glist) + 1];
int i = 0;
for (GList* curr = glist; curr; curr = g_list_next(curr)) {
new_list[i++] = curr->data;
}
new_list[i] = NULL;
g_key_file_set_string_list(keyfile, group, key, new_list, g_list_length(glist));
g_key_file_set_string_list(keyfile, group, key, new_list, new_length);
}
}
g_list_free_full(glist, g_free);
g_free(new_list);
return deleted;
}

View File

@@ -4,33 +4,7 @@
*
* Copyright (C) 2012 - 2019 James Booth <boothj5@gmail.com>
*
* This file is part of Profanity.
*
* Profanity is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Profanity is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Profanity. If not, see <https://www.gnu.org/licenses/>.
*
* In addition, as a special exception, the copyright holders give permission to
* link the code of portions of this program with the OpenSSL library under
* certain conditions as described in each individual source file, and
* distribute linked combinations including the two.
*
* You must obey the GNU General Public License in all respects for all of the
* code used other than OpenSSL. If you modify file(s) with this exception, you
* may extend this exception to your version of the file(s), but you are not
* obligated to do so. If you do not wish to do so, delete this exception
* statement from your version. If you delete this exception statement from all
* source files in the program, then also delete it here.
*
* SPDX-License-Identifier: GPL-3.0-or-later WITH OpenSSL-exception
*/
#ifndef CONFIG_CONFLISTS_H

View File

@@ -3,35 +3,9 @@
* vim: expandtab:ts=4:sts=4:sw=4
*
* Copyright (C) 2012 - 2019 James Booth <boothj5@gmail.com>
* Copyright (C) 2020 - 2025 Michael Vetter <jubalh@iodoru.org>
*
* This file is part of Profanity.
*
* Profanity is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Profanity is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Profanity. If not, see <https://www.gnu.org/licenses/>.
*
* In addition, as a special exception, the copyright holders give permission to
* link the code of portions of this program with the OpenSSL library under
* certain conditions as described in each individual source file, and
* distribute linked combinations including the two.
*
* You must obey the GNU General Public License in all respects for all of the
* code used other than OpenSSL. If you modify file(s) with this exception, you
* may extend this exception to your version of the file(s), but you are not
* obligated to do so. If you do not wish to do so, delete this exception
* statement from your version. If you delete this exception statement from all
* source files in the program, then also delete it here.
* Copyright (C) 2020 - 2026 Michael Vetter <jubalh@iodoru.org>
*
* SPDX-License-Identifier: GPL-3.0-or-later WITH OpenSSL-exception
*/
#include "config.h"
@@ -114,30 +88,26 @@ gchar*
files_get_log_file(const char* const log_file)
{
auto_gchar gchar* xdg_data = _files_get_xdg_data_home();
GString* logfile;
if (log_file) {
auto_gchar gchar* log_path = g_path_get_dirname(log_file);
if (!create_dir(log_path)) {
log_error("Error while creating directory %s", log_path);
}
logfile = g_string_new(log_file);
} else {
logfile = g_string_new(xdg_data);
g_string_append(logfile, "/profanity/logs/profanity");
if (!prefs_get_boolean(PREF_LOG_SHARED)) {
g_string_append_printf(logfile, "%d", getpid());
}
g_string_append(logfile, ".log");
return g_strdup(log_file);
}
GString* logfile = g_string_new(xdg_data);
g_string_append(logfile, "/profanity/logs/profanity");
if (!prefs_get_boolean(PREF_LOG_SHARED)) {
g_string_append_printf(logfile, "%d", getpid());
}
g_string_append(logfile, ".log");
gchar* result = g_strdup(logfile->str);
g_string_free(logfile, TRUE);
return result;
}

View File

@@ -3,35 +3,9 @@
* vim: expandtab:ts=4:sts=4:sw=4
*
* Copyright (C) 2012 - 2019 James Booth <boothj5@gmail.com>
* Copyright (C) 2018 - 2025 Michael Vetter <jubalh@iodoru.org>
*
* This file is part of Profanity.
*
* Profanity is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Profanity is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Profanity. If not, see <https://www.gnu.org/licenses/>.
*
* In addition, as a special exception, the copyright holders give permission to
* link the code of portions of this program with the OpenSSL library under
* certain conditions as described in each individual source file, and
* distribute linked combinations including the two.
*
* You must obey the GNU General Public License in all respects for all of the
* code used other than OpenSSL. If you modify file(s) with this exception, you
* may extend this exception to your version of the file(s), but you are not
* obligated to do so. If you do not wish to do so, delete this exception
* statement from your version. If you delete this exception statement from all
* source files in the program, then also delete it here.
* Copyright (C) 2018 - 2026 Michael Vetter <jubalh@iodoru.org>
*
* SPDX-License-Identifier: GPL-3.0-or-later WITH OpenSSL-exception
*/
#ifndef CONFIG_FILES_H
@@ -57,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"

File diff suppressed because it is too large Load Diff

View File

@@ -3,35 +3,9 @@
* vim: expandtab:ts=4:sts=4:sw=4
*
* Copyright (C) 2012 - 2019 James Booth <boothj5@gmail.com>
* Copyright (C) 2019 - 2025 Michael Vetter <jubalh@iodoru.org>
*
* This file is part of Profanity.
*
* Profanity is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Profanity is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Profanity. If not, see <https://www.gnu.org/licenses/>.
*
* In addition, as a special exception, the copyright holders give permission to
* link the code of portions of this program with the OpenSSL library under
* certain conditions as described in each individual source file, and
* distribute linked combinations including the two.
*
* You must obey the GNU General Public License in all respects for all of the
* code used other than OpenSSL. If you modify file(s) with this exception, you
* may extend this exception to your version of the file(s), but you are not
* obligated to do so. If you do not wish to do so, delete this exception
* statement from your version. If you delete this exception statement from all
* source files in the program, then also delete it here.
* Copyright (C) 2019 - 2026 Michael Vetter <jubalh@iodoru.org>
*
* SPDX-License-Identifier: GPL-3.0-or-later WITH OpenSSL-exception
*/
#ifndef CONFIG_PREFERENCES_H
@@ -162,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,
@@ -183,12 +158,17 @@ 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,
PREF_STATUSBAR_TABMODE,
PREF_FORCE_ENCRYPTION,
PREF_FORCE_ENCRYPTION_MODE
PREF_FORCE_ENCRYPTION_MODE,
PREF_SPELLCHECK_ENABLE,
PREF_SPELLCHECK_LANG,
} preference_t;
typedef struct prof_alias_t
@@ -205,15 +185,16 @@ typedef struct prof_winplacement_t
int inputwin_pos;
} ProfWinPlacement;
void prefs_load(const char* config_file);
void prefs_load(const gchar* config_file);
void prefs_changes(void);
void prefs_save(void);
void prefs_close(void);
void prefs_reload(void);
gchar* prefs_autocomplete_boolean_choice(const char* const prefix, gboolean previous, void* context);
gchar* prefs_autocomplete_boolean_choice(const gchar* const prefix, gboolean previous, void* context);
void prefs_reset_boolean_choice(void);
gchar* prefs_autocomplete_room_trigger(const char* const prefix, gboolean previous, void* context);
gchar* prefs_autocomplete_room_trigger(const gchar* const prefix, gboolean previous, void* context);
void prefs_reset_room_trigger_ac(void);
gint prefs_get_gone(void);
@@ -250,42 +231,42 @@ gint prefs_get_autoxa_time(void);
void prefs_set_autoxa_time(gint value);
gchar** prefs_get_plugins(void);
void prefs_add_plugin(const char* const name);
void prefs_remove_plugin(const char* const name);
void prefs_add_plugin(const gchar* const name);
void prefs_remove_plugin(const gchar* const name);
gchar* prefs_get_otr_char(void);
gboolean prefs_set_otr_char(char* ch);
gboolean prefs_set_otr_char(gchar* ch);
gchar* prefs_get_pgp_char(void);
gboolean prefs_set_pgp_char(char* ch);
gboolean prefs_set_pgp_char(gchar* ch);
gchar* prefs_get_omemo_char(void);
gboolean prefs_set_omemo_char(char* ch);
gboolean prefs_set_omemo_char(gchar* ch);
// XEP-0373: OpenPGP for XMPP
char* prefs_get_ox_char(void);
gboolean prefs_set_ox_char(char* ch);
gboolean prefs_set_ox_char(gchar* ch);
gchar* prefs_get_roster_header_char(void);
void prefs_set_roster_header_char(char* ch);
void prefs_set_roster_header_char(gchar* ch);
void prefs_clear_roster_header_char(void);
gchar* prefs_get_roster_contact_char(void);
void prefs_set_roster_contact_char(char* ch);
void prefs_set_roster_contact_char(gchar* ch);
void prefs_clear_roster_contact_char(void);
gchar* prefs_get_roster_resource_char(void);
void prefs_set_roster_resource_char(char* ch);
void prefs_set_roster_resource_char(gchar* ch);
void prefs_clear_roster_resource_char(void);
gchar* prefs_get_roster_private_char(void);
void prefs_set_roster_private_char(char* ch);
void prefs_set_roster_private_char(gchar* ch);
void prefs_clear_roster_private_char(void);
gchar* prefs_get_roster_room_char(void);
void prefs_set_roster_room_char(char* ch);
void prefs_set_roster_room_char(gchar* ch);
void prefs_clear_roster_room_char(void);
gchar* prefs_get_roster_room_private_char(void);
void prefs_set_roster_room_private_char(char* ch);
void prefs_set_roster_room_private_char(gchar* ch);
void prefs_clear_roster_room_private_char(void);
gchar* prefs_get_occupants_char(void);
void prefs_set_occupants_char(char* ch);
void prefs_set_occupants_char(gchar* ch);
void prefs_clear_occupants_char(void);
gchar* prefs_get_occupants_header_char(void);
void prefs_set_occupants_header_char(char* ch);
void prefs_set_occupants_header_char(gchar* ch);
void prefs_clear_occupants_header_char(void);
gint prefs_get_roster_contact_indent(void);
@@ -298,21 +279,21 @@ gint prefs_get_occupants_indent(void);
void prefs_set_occupants_indent(gint value);
gchar* prefs_get_correction_char(void);
void prefs_set_correction_char(char* ch);
void prefs_set_correction_char(gchar* ch);
void prefs_add_login(const char* jid);
void prefs_add_login(const gchar* jid);
void prefs_set_tray_timer(gint value);
gint prefs_get_tray_timer(void);
gboolean prefs_add_alias(const char* const name, const char* const value);
gboolean prefs_remove_alias(const char* const name);
gchar* prefs_get_alias(const char* const name);
gboolean prefs_add_alias(const gchar* const name, const gchar* const value);
gboolean prefs_remove_alias(const gchar* const name);
gchar* prefs_get_alias(const gchar* const name);
GList* prefs_get_aliases(void);
void prefs_free_aliases(GList* aliases);
gboolean prefs_add_room_notify_trigger(const char* const text);
gboolean prefs_remove_room_notify_trigger(const char* const text);
gboolean prefs_add_room_notify_trigger(const gchar* const text);
gboolean prefs_remove_room_notify_trigger(const gchar* const text);
GList* prefs_get_room_notify_triggers(void);
ProfWinPlacement* prefs_get_win_placement(void);
@@ -356,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

View File

@@ -4,33 +4,7 @@
*
* Copyright (C) 2012 - 2019 James Booth <boothj5@gmail.com>
*
* This file is part of Profanity.
*
* Profanity is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Profanity is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Profanity. If not, see <https://www.gnu.org/licenses/>.
*
* In addition, as a special exception, the copyright holders give permission to
* link the code of portions of this program with the OpenSSL library under
* certain conditions as described in each individual source file, and
* distribute linked combinations including the two.
*
* You must obey the GNU General Public License in all respects for all of the
* code used other than OpenSSL. If you modify file(s) with this exception, you
* may extend this exception to your version of the file(s), but you are not
* obligated to do so. If you do not wish to do so, delete this exception
* statement from your version. If you delete this exception statement from all
* source files in the program, then also delete it here.
*
* SPDX-License-Identifier: GPL-3.0-or-later WITH OpenSSL-exception
*/
#include "config.h"

View File

@@ -4,33 +4,7 @@
*
* Copyright (C) 2012 - 2019 James Booth <boothj5@gmail.com>
*
* This file is part of Profanity.
*
* Profanity is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Profanity is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Profanity. If not, see <https://www.gnu.org/licenses/>.
*
* In addition, as a special exception, the copyright holders give permission to
* link the code of portions of this program with the OpenSSL library under
* certain conditions as described in each individual source file, and
* distribute linked combinations including the two.
*
* You must obey the GNU General Public License in all respects for all of the
* code used other than OpenSSL. If you modify file(s) with this exception, you
* may extend this exception to your version of the file(s), but you are not
* obligated to do so. If you do not wish to do so, delete this exception
* statement from your version. If you delete this exception statement from all
* source files in the program, then also delete it here.
*
* SPDX-License-Identifier: GPL-3.0-or-later WITH OpenSSL-exception
*/
#ifndef CONFIG_SCRIPTS_H

View File

@@ -3,35 +3,9 @@
* vim: expandtab:ts=4:sts=4:sw=4
*
* Copyright (C) 2012 - 2019 James Booth <boothj5@gmail.com>
* Copyright (C) 2019 - 2025 Michael Vetter <jubalh@iodoru.org>
*
* This file is part of Profanity.
*
* Profanity is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Profanity is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Profanity. If not, see <https://www.gnu.org/licenses/>.
*
* In addition, as a special exception, the copyright holders give permission to
* link the code of portions of this program with the OpenSSL library under
* certain conditions as described in each individual source file, and
* distribute linked combinations including the two.
*
* You must obey the GNU General Public License in all respects for all of the
* code used other than OpenSSL. If you modify file(s) with this exception, you
* may extend this exception to your version of the file(s), but you are not
* obligated to do so. If you do not wish to do so, delete this exception
* statement from your version. If you delete this exception statement from all
* source files in the program, then also delete it here.
* Copyright (C) 2019 - 2026 Michael Vetter <jubalh@iodoru.org>
*
* SPDX-License-Identifier: GPL-3.0-or-later WITH OpenSSL-exception
*/
#include "config.h"
@@ -117,6 +91,7 @@ theme_init(const char* const theme_name)
g_hash_table_insert(defaults, strdup("mention"), strdup("yellow"));
g_hash_table_insert(defaults, strdup("trigger"), strdup("yellow"));
g_hash_table_insert(defaults, strdup("input.text"), strdup("default"));
g_hash_table_insert(defaults, strdup("input.misspelled"), strdup("red"));
g_hash_table_insert(defaults, strdup("main.time"), strdup("default"));
g_hash_table_insert(defaults, strdup("titlebar.text"), strdup("white"));
g_hash_table_insert(defaults, strdup("titlebar.brackets"), strdup("cyan"));
@@ -533,7 +508,7 @@ _theme_find(const char* const theme_name)
g_string_append(path, "/");
g_string_append(path, theme_name);
if (!g_file_test(path->str, G_FILE_TEST_EXISTS)) {
g_string_free(path, true);
g_string_free(path, TRUE);
path = NULL;
}
}
@@ -667,25 +642,17 @@ _theme_prep_fgnd(char* setting, GString* lookup_str, gboolean* bold)
}
}
char*
gchar*
theme_get_string(char* str)
{
gchar* res = g_key_file_get_string(theme, "colours", str, NULL);
if (!res) {
return strdup(g_hash_table_lookup(defaults, str));
return g_strdup(g_hash_table_lookup(defaults, str));
} else {
return res;
}
}
void
theme_free_string(char* str)
{
if (str) {
g_free(str);
}
}
int
theme_hash_attrs(const char* str)
{
@@ -748,6 +715,9 @@ theme_attrs(theme_item_t attrs)
case THEME_INPUT_TEXT:
_theme_prep_fgnd("input.text", lookup_str, &bold);
break;
case THEME_INPUT_MISSPELLED:
_theme_prep_fgnd("input.misspelled", lookup_str, &bold);
break;
case THEME_TIME:
_theme_prep_fgnd("main.time", lookup_str, &bold);
break;

View File

@@ -4,33 +4,7 @@
*
* Copyright (C) 2012 - 2019 James Booth <boothj5@gmail.com>
*
* This file is part of Profanity.
*
* Profanity is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Profanity is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Profanity. If not, see <https://www.gnu.org/licenses/>.
*
* In addition, as a special exception, the copyright holders give permission to
* link the code of portions of this program with the OpenSSL library under
* certain conditions as described in each individual source file, and
* distribute linked combinations including the two.
*
* You must obey the GNU General Public License in all respects for all of the
* code used other than OpenSSL. If you modify file(s) with this exception, you
* may extend this exception to your version of the file(s), but you are not
* obligated to do so. If you do not wish to do so, delete this exception
* statement from your version. If you delete this exception statement from all
* source files in the program, then also delete it here.
*
* SPDX-License-Identifier: GPL-3.0-or-later WITH OpenSSL-exception
*/
#ifndef CONFIG_THEME_H
@@ -53,6 +27,7 @@ typedef enum {
THEME_MENTION,
THEME_TRIGGER,
THEME_INPUT_TEXT,
THEME_INPUT_MISSPELLED,
THEME_TIME,
THEME_TITLE_TEXT,
THEME_TITLE_BRACKET,
@@ -152,7 +127,6 @@ void theme_close(void);
int theme_hash_attrs(const char* str);
int theme_attrs(theme_item_t attrs);
char* theme_get_string(char* str);
void theme_free_string(char* str);
theme_item_t theme_main_presence_attrs(const char* const presence);
theme_item_t theme_roster_unread_presence_attrs(const char* const presence);
theme_item_t theme_roster_active_presence_attrs(const char* const presence);

View File

@@ -4,33 +4,7 @@
*
* Copyright (C) 2012 - 2019 James Booth <boothj5@gmail.com>
*
* This file is part of Profanity.
*
* Profanity is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Profanity is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Profanity. If not, see <https://www.gnu.org/licenses/>.
*
* In addition, as a special exception, the copyright holders give permission to
* link the code of portions of this program with the OpenSSL library under
* certain conditions as described in each individual source file, and
* distribute linked combinations including the two.
*
* You must obey the GNU General Public License in all respects for all of the
* code used other than OpenSSL. If you modify file(s) with this exception, you
* may extend this exception to your version of the file(s), but you are not
* obligated to do so. If you do not wish to do so, delete this exception
* statement from your version. If you delete this exception statement from all
* source files in the program, then also delete it here.
*
* SPDX-License-Identifier: GPL-3.0-or-later WITH OpenSSL-exception
*/
#include "config.h"
@@ -54,7 +28,7 @@ static void _save_tlscerts(void);
static Autocomplete certs_ac;
static char* current_fp;
static gchar* current_fp;
static void
_tlscerts_close(void)
@@ -62,7 +36,7 @@ _tlscerts_close(void)
free_keyfile(&tlscerts_prof_keyfile);
tlscerts = NULL;
free(current_fp);
g_free(current_fp);
current_fp = NULL;
autocomplete_free(certs_ac);
@@ -82,7 +56,7 @@ tlscerts_init(void)
gsize len = 0;
auto_gcharv gchar** groups = g_key_file_get_groups(tlscerts, &len);
for (int i = 0; i < g_strv_length(groups); i++) {
for (guint i = 0; i < g_strv_length(groups); i++) {
autocomplete_add(certs_ac, groups[i]);
}
@@ -90,33 +64,116 @@ tlscerts_init(void)
}
void
tlscerts_set_current(const char* const fp)
tlscerts_set_current(const TLSCertificate* cert)
{
if (current_fp) {
free(current_fp);
g_free(current_fp);
}
current_fp = strdup(fp);
current_fp = g_strdup(cert->fingerprint);
}
char*
tlscerts_get_current(void)
gboolean
tlscerts_current_fingerprint_equals(const TLSCertificate* cert)
{
return current_fp;
return g_strcmp0(current_fp, cert->fingerprint) == 0;
}
void
tlscerts_clear_current(void)
{
if (current_fp) {
free(current_fp);
g_free(current_fp);
current_fp = NULL;
}
}
gboolean
tlscerts_exists(const char* const fingerprint)
tlscerts_exists(const TLSCertificate* cert)
{
return g_key_file_has_group(tlscerts, fingerprint);
return g_key_file_has_group(tlscerts, cert->fingerprint);
}
#define _name_to_tlscert_name(in, cert, which) \
do { \
_name_to_tlscert_name_(in, &cert->which, #which); \
} while (0)
static void
_name_to_tlscert_name_(const char* in, tls_cert_name_t* out, const char* name);
static TLSCertificate*
_tlscerts_new(gchar* fingerprint_sha1, int version, gchar* serialnumber, gchar* subjectname,
gchar* issuername, gchar* notbefore, gchar* notafter,
gchar* key_alg, gchar* signature_alg, gchar* pem,
gchar* fingerprint_sha256, gchar* pubkey_fingerprint)
{
TLSCertificate* cert = g_new0(TLSCertificate, 1);
if (fingerprint_sha256 && fingerprint_sha1) {
cert->fingerprint_sha256 = fingerprint_sha256;
cert->fingerprint_sha1 = fingerprint_sha1;
cert->fingerprint = cert->fingerprint_sha256;
} else {
if (fingerprint_sha256) {
size_t s = strlen(fingerprint_sha256);
switch (s) {
case 40:
cert->fingerprint_sha1 = fingerprint_sha256;
cert->fingerprint = cert->fingerprint_sha1;
break;
case 64:
cert->fingerprint_sha256 = fingerprint_sha256;
cert->fingerprint = cert->fingerprint_sha256;
break;
default:
log_error("fingerprint_sha256 of length %zu unexpected: %s", s, fingerprint_sha256);
}
} else {
cert->fingerprint_sha1 = fingerprint_sha1;
cert->fingerprint = cert->fingerprint_sha1;
}
}
cert->version = version;
cert->serialnumber = serialnumber;
cert->subjectname = subjectname;
cert->issuername = issuername;
cert->notbefore = notbefore;
cert->notafter = notafter;
cert->key_alg = key_alg;
cert->signature_alg = signature_alg;
cert->pem = pem;
cert->pubkey_fingerprint = pubkey_fingerprint;
/* Do this check after assigning all values, in order to not leak them,
* even though the situation is most likely already FUBAR */
if (!fingerprint_sha256 && !fingerprint_sha1) {
log_error("No TLSCertificate w/o fingerprint");
tlscerts_free(cert);
return NULL;
}
_name_to_tlscert_name(subjectname, cert, subject);
_name_to_tlscert_name(issuername, cert, issuer);
return cert;
}
static TLSCertificate*
_get_TLSCertificate(const gchar* fingerprint)
{
int version = g_key_file_get_integer(tlscerts, fingerprint, "version", NULL);
gchar* serialnumber = g_key_file_get_string(tlscerts, fingerprint, "serialnumber", NULL);
gchar* subjectname = g_key_file_get_string(tlscerts, fingerprint, "subjectname", NULL);
gchar* issuername = g_key_file_get_string(tlscerts, fingerprint, "issuername", NULL);
gchar* notbefore = g_key_file_get_string(tlscerts, fingerprint, "start", NULL);
gchar* notafter = g_key_file_get_string(tlscerts, fingerprint, "end", NULL);
gchar* keyalg = g_key_file_get_string(tlscerts, fingerprint, "keyalg", NULL);
gchar* signaturealg = g_key_file_get_string(tlscerts, fingerprint, "signaturealg", NULL);
gchar* fingerprint_sha1 = g_key_file_get_string(tlscerts, fingerprint, "fingerprint_sha1", NULL);
gchar* pubkey_fingerprint = g_key_file_get_string(tlscerts, fingerprint, "pubkey_fingerprint", NULL);
return _tlscerts_new(fingerprint_sha1, version, serialnumber, subjectname, issuername, notbefore,
notafter, keyalg, signaturealg, NULL, g_strdup(fingerprint), pubkey_fingerprint);
}
GList*
@@ -126,125 +183,81 @@ tlscerts_list(void)
gsize len = 0;
auto_gcharv gchar** groups = g_key_file_get_groups(tlscerts, &len);
for (int i = 0; i < g_strv_length(groups); i++) {
char* fingerprint = strdup(groups[i]);
int version = g_key_file_get_integer(tlscerts, fingerprint, "version", NULL);
auto_gchar gchar* serialnumber = g_key_file_get_string(tlscerts, fingerprint, "serialnumber", NULL);
auto_gchar gchar* subjectname = g_key_file_get_string(tlscerts, fingerprint, "subjectname", NULL);
auto_gchar gchar* issuername = g_key_file_get_string(tlscerts, fingerprint, "issuername", NULL);
auto_gchar gchar* notbefore = g_key_file_get_string(tlscerts, fingerprint, "start", NULL);
auto_gchar gchar* notafter = g_key_file_get_string(tlscerts, fingerprint, "end", NULL);
auto_gchar gchar* keyalg = g_key_file_get_string(tlscerts, fingerprint, "keyalg", NULL);
auto_gchar gchar* signaturealg = g_key_file_get_string(tlscerts, fingerprint, "signaturealg", NULL);
TLSCertificate* cert = tlscerts_new(fingerprint, version, serialnumber, subjectname, issuername, notbefore,
notafter, keyalg, signaturealg, NULL);
res = g_list_append(res, cert);
for (gsize i = 0; i < len; i++) {
res = g_list_append(res, _get_TLSCertificate(groups[i]));
}
return res;
}
TLSCertificate*
tlscerts_new(const char* const fingerprint, int version, const char* const serialnumber, const char* const subjectname,
const char* const issuername, const char* const notbefore, const char* const notafter,
const char* const key_alg, const char* const signature_alg, const char* const pem)
static void
_name_to_tlscert_name_(const char* in, tls_cert_name_t* out, const char* name)
{
TLSCertificate* cert = calloc(1, sizeof(TLSCertificate));
if (fingerprint) {
cert->fingerprint = strdup(fingerprint);
}
cert->version = version;
if (serialnumber) {
cert->serialnumber = strdup(serialnumber);
}
if (subjectname) {
cert->subjectname = strdup(subjectname);
}
if (issuername) {
cert->issuername = strdup(issuername);
}
if (notbefore) {
cert->notbefore = strdup(notbefore);
}
if (notafter) {
cert->notafter = strdup(notafter);
}
if (key_alg) {
cert->key_alg = strdup(key_alg);
}
if (signature_alg) {
cert->signature_alg = strdup(signature_alg);
}
if (pem) {
cert->pem = strdup(pem);
}
auto_gcharv gchar** fields = g_strsplit(subjectname, "/", 0);
for (int i = 0; i < g_strv_length(fields); i++) {
auto_gcharv gchar** fields = g_strsplit(in, "/", 0);
const guint fields_num = g_strv_length(fields);
for (guint i = 0; i < fields_num; i++) {
auto_gcharv gchar** keyval = g_strsplit(fields[i], "=", 2);
if (g_strv_length(keyval) == 2) {
#define tls_cert_name_set(which) \
do { \
if (out->which) { \
log_warning("%s." #which " already set, skip %s", name, keyval[1]); \
continue; \
} \
out->which = g_strdup(keyval[1]); \
} while (0)
if ((g_strcmp0(keyval[0], "C") == 0) || (g_strcmp0(keyval[0], "countryName") == 0)) {
cert->subject_country = strdup(keyval[1]);
tls_cert_name_set(country);
}
if ((g_strcmp0(keyval[0], "ST") == 0) || (g_strcmp0(keyval[0], "stateOrProvinceName") == 0)) {
cert->subject_state = strdup(keyval[1]);
tls_cert_name_set(state);
}
if (g_strcmp0(keyval[0], "dnQualifier") == 0) {
cert->subject_distinguishedname = strdup(keyval[1]);
tls_cert_name_set(distinguishedname);
}
if (g_strcmp0(keyval[0], "serialnumber") == 0) {
cert->subject_serialnumber = strdup(keyval[1]);
tls_cert_name_set(serialnumber);
}
if ((g_strcmp0(keyval[0], "CN") == 0) || (g_strcmp0(keyval[0], "commonName") == 0)) {
cert->subject_commonname = strdup(keyval[1]);
tls_cert_name_set(commonname);
}
if ((g_strcmp0(keyval[0], "O") == 0) || (g_strcmp0(keyval[0], "organizationName") == 0)) {
cert->subject_organisation = strdup(keyval[1]);
tls_cert_name_set(organisation);
}
if ((g_strcmp0(keyval[0], "OU") == 0) || (g_strcmp0(keyval[0], "organizationalUnitName") == 0)) {
cert->subject_organisation_unit = strdup(keyval[1]);
tls_cert_name_set(organisation_unit);
}
if (g_strcmp0(keyval[0], "emailAddress") == 0) {
cert->subject_email = strdup(keyval[1]);
tls_cert_name_set(email);
}
#undef tls_cert_name_set
}
}
}
auto_gcharv gchar** fields2 = g_strsplit(issuername, "/", 0);
for (int i = 0; i < g_strv_length(fields2); i++) {
auto_gcharv gchar** keyval = g_strsplit(fields2[i], "=", 2);
if (g_strv_length(keyval) == 2) {
if ((g_strcmp0(keyval[0], "C") == 0) || (g_strcmp0(keyval[0], "countryName") == 0)) {
cert->issuer_country = strdup(keyval[1]);
}
if ((g_strcmp0(keyval[0], "ST") == 0) || (g_strcmp0(keyval[0], "stateOrProvinceName") == 0)) {
cert->issuer_state = strdup(keyval[1]);
}
if (g_strcmp0(keyval[0], "dnQualifier") == 0) {
cert->issuer_distinguishedname = strdup(keyval[1]);
}
if (g_strcmp0(keyval[0], "serialnumber") == 0) {
cert->issuer_serialnumber = strdup(keyval[1]);
}
if ((g_strcmp0(keyval[0], "CN") == 0) || (g_strcmp0(keyval[0], "commonName") == 0)) {
cert->issuer_commonname = strdup(keyval[1]);
}
if ((g_strcmp0(keyval[0], "O") == 0) || (g_strcmp0(keyval[0], "organizationName") == 0)) {
cert->issuer_organisation = strdup(keyval[1]);
}
if ((g_strcmp0(keyval[0], "OU") == 0) || (g_strcmp0(keyval[0], "organizationalUnitName") == 0)) {
cert->issuer_organisation_unit = strdup(keyval[1]);
}
if (g_strcmp0(keyval[0], "emailAddress") == 0) {
cert->issuer_email = strdup(keyval[1]);
}
}
}
TLSCertificate*
tlscerts_new(const char* fingerprint_sha1, int version, const char* serialnumber, const char* subjectname,
const char* issuername, const char* notbefore, const char* notafter,
const char* key_alg, const char* signature_alg, const char* pem,
const char* fingerprint_sha256, const char* pubkey_fingerprint)
{
return _tlscerts_new(g_strdup(fingerprint_sha1), version, g_strdup(serialnumber), g_strdup(subjectname),
g_strdup(issuername), g_strdup(notbefore), g_strdup(notafter),
g_strdup(key_alg), g_strdup(signature_alg), g_strdup(pem),
g_strdup(fingerprint_sha256), g_strdup(pubkey_fingerprint));
}
return cert;
static void
_checked_g_key_file_set_string(GKeyFile* key_file,
const gchar* group_name,
const gchar* key,
const gchar* string)
{
if (string == NULL)
return;
g_key_file_set_string(key_file, group_name, key, string);
}
void
@@ -261,33 +274,21 @@ tlscerts_add(const TLSCertificate* cert)
autocomplete_add(certs_ac, cert->fingerprint);
g_key_file_set_integer(tlscerts, cert->fingerprint, "version", cert->version);
if (cert->serialnumber) {
g_key_file_set_string(tlscerts, cert->fingerprint, "serialnumber", cert->serialnumber);
}
if (cert->subjectname) {
g_key_file_set_string(tlscerts, cert->fingerprint, "subjectname", cert->subjectname);
}
if (cert->issuername) {
g_key_file_set_string(tlscerts, cert->fingerprint, "issuername", cert->issuername);
}
if (cert->notbefore) {
g_key_file_set_string(tlscerts, cert->fingerprint, "start", cert->notbefore);
}
if (cert->notafter) {
g_key_file_set_string(tlscerts, cert->fingerprint, "end", cert->notafter);
}
if (cert->key_alg) {
g_key_file_set_string(tlscerts, cert->fingerprint, "keyalg", cert->key_alg);
}
if (cert->signature_alg) {
g_key_file_set_string(tlscerts, cert->fingerprint, "signaturealg", cert->signature_alg);
}
_checked_g_key_file_set_string(tlscerts, cert->fingerprint, "serialnumber", cert->serialnumber);
_checked_g_key_file_set_string(tlscerts, cert->fingerprint, "subjectname", cert->subjectname);
_checked_g_key_file_set_string(tlscerts, cert->fingerprint, "issuername", cert->issuername);
_checked_g_key_file_set_string(tlscerts, cert->fingerprint, "start", cert->notbefore);
_checked_g_key_file_set_string(tlscerts, cert->fingerprint, "end", cert->notafter);
_checked_g_key_file_set_string(tlscerts, cert->fingerprint, "keyalg", cert->key_alg);
_checked_g_key_file_set_string(tlscerts, cert->fingerprint, "signaturealg", cert->signature_alg);
_checked_g_key_file_set_string(tlscerts, cert->fingerprint, "fingerprint_sha1", cert->fingerprint_sha1);
_checked_g_key_file_set_string(tlscerts, cert->fingerprint, "pubkey_fingerprint", cert->pubkey_fingerprint);
_save_tlscerts();
}
gboolean
tlscerts_revoke(const char* const fingerprint)
tlscerts_revoke(const char* fingerprint)
{
gboolean result = g_key_file_remove_group(tlscerts, fingerprint, NULL);
if (result) {
@@ -300,28 +301,24 @@ tlscerts_revoke(const char* const fingerprint)
}
TLSCertificate*
tlscerts_get_trusted(const char* const fingerprint)
tlscerts_get_trusted(const gchar* fingerprint)
{
if (!g_key_file_has_group(tlscerts, fingerprint)) {
gsize len = 0;
auto_gcharv gchar** groups = g_key_file_get_groups(tlscerts, &len);
for (gsize i = 0; i < len; i++) {
auto_gchar gchar* fingerprint_sha1 = g_key_file_get_string(tlscerts, groups[i], "fingerprint_sha1", NULL);
if (g_strcmp0(fingerprint, fingerprint_sha1) == 0) {
return _get_TLSCertificate(fingerprint_sha1);
}
}
return NULL;
}
int version = g_key_file_get_integer(tlscerts, fingerprint, "version", NULL);
auto_gchar gchar* serialnumber = g_key_file_get_string(tlscerts, fingerprint, "serialnumber", NULL);
auto_gchar gchar* subjectname = g_key_file_get_string(tlscerts, fingerprint, "subjectname", NULL);
auto_gchar gchar* issuername = g_key_file_get_string(tlscerts, fingerprint, "issuername", NULL);
auto_gchar gchar* notbefore = g_key_file_get_string(tlscerts, fingerprint, "start", NULL);
auto_gchar gchar* notafter = g_key_file_get_string(tlscerts, fingerprint, "end", NULL);
auto_gchar gchar* keyalg = g_key_file_get_string(tlscerts, fingerprint, "keyalg", NULL);
auto_gchar gchar* signaturealg = g_key_file_get_string(tlscerts, fingerprint, "signaturealg", NULL);
TLSCertificate* cert = tlscerts_new(fingerprint, version, serialnumber, subjectname, issuername, notbefore,
notafter, keyalg, signaturealg, NULL);
return cert;
return _get_TLSCertificate(fingerprint);
}
char*
tlscerts_complete(const char* const prefix, gboolean previous, void* context)
tlscerts_complete(const char* prefix, gboolean previous, void* context)
{
return autocomplete_complete(certs_ac, prefix, TRUE, previous);
}
@@ -332,40 +329,37 @@ tlscerts_reset_ac(void)
autocomplete_reset(certs_ac);
}
static void
_tls_cert_name_free(tls_cert_name_t* name)
{
g_free(name->country);
g_free(name->state);
g_free(name->distinguishedname);
g_free(name->serialnumber);
g_free(name->commonname);
g_free(name->organisation);
g_free(name->organisation_unit);
g_free(name->email);
}
void
tlscerts_free(TLSCertificate* cert)
{
if (cert) {
free(cert->serialnumber);
_tls_cert_name_free(&cert->subject);
_tls_cert_name_free(&cert->issuer);
free(cert->subjectname);
free(cert->subject_country);
free(cert->subject_state);
free(cert->subject_distinguishedname);
free(cert->subject_serialnumber);
free(cert->subject_commonname);
free(cert->subject_organisation);
free(cert->subject_organisation_unit);
free(cert->subject_email);
free(cert->issuername);
free(cert->issuer_country);
free(cert->issuer_state);
free(cert->issuer_distinguishedname);
free(cert->issuer_serialnumber);
free(cert->issuer_commonname);
free(cert->issuer_organisation);
free(cert->issuer_organisation_unit);
free(cert->issuer_email);
free(cert->notbefore);
free(cert->notafter);
free(cert->fingerprint);
free(cert->key_alg);
free(cert->signature_alg);
free(cert->pem);
g_free(cert->pubkey_fingerprint);
g_free(cert->pem);
g_free(cert->signature_alg);
g_free(cert->key_alg);
g_free(cert->notafter);
g_free(cert->notbefore);
g_free(cert->issuername);
g_free(cert->subjectname);
g_free(cert->serialnumber);
g_free(cert->fingerprint_sha1);
g_free(cert->fingerprint_sha256);
free(cert);
}

View File

@@ -4,33 +4,7 @@
*
* Copyright (C) 2012 - 2019 James Booth <boothj5@gmail.com>
*
* This file is part of Profanity.
*
* Profanity is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Profanity is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Profanity. If not, see <https://www.gnu.org/licenses/>.
*
* In addition, as a special exception, the copyright holders give permission to
* link the code of portions of this program with the OpenSSL library under
* certain conditions as described in each individual source file, and
* distribute linked combinations including the two.
*
* You must obey the GNU General Public License in all respects for all of the
* code used other than OpenSSL. If you modify file(s) with this exception, you
* may extend this exception to your version of the file(s), but you are not
* obligated to do so. If you do not wish to do so, delete this exception
* statement from your version. If you delete this exception statement from all
* source files in the program, then also delete it here.
*
* SPDX-License-Identifier: GPL-3.0-or-later WITH OpenSSL-exception
*/
#ifndef CONFIG_TLSCERTS_H
@@ -38,61 +12,63 @@
#include <glib.h>
typedef struct tls_cert_name_t
{
gchar* country;
gchar* state;
gchar* distinguishedname;
gchar* serialnumber;
gchar* commonname;
gchar* organisation;
gchar* organisation_unit;
gchar* email;
} tls_cert_name_t;
typedef struct tls_cert_t
{
int version;
char* serialnumber;
char* subjectname;
char* subject_country;
char* subject_state;
char* subject_distinguishedname;
char* subject_serialnumber;
char* subject_commonname;
char* subject_organisation;
char* subject_organisation_unit;
char* subject_email;
char* issuername;
char* issuer_country;
char* issuer_state;
char* issuer_distinguishedname;
char* issuer_serialnumber;
char* issuer_commonname;
char* issuer_organisation;
char* issuer_organisation_unit;
char* issuer_email;
char* notbefore;
char* notafter;
char* fingerprint;
char* key_alg;
char* signature_alg;
char* pem;
const gchar* fingerprint;
gchar* serialnumber;
gchar* subjectname;
tls_cert_name_t subject;
gchar* issuername;
tls_cert_name_t issuer;
gchar* notbefore;
gchar* notafter;
gchar* fingerprint_sha1;
gchar* fingerprint_sha256;
gchar* key_alg;
gchar* signature_alg;
gchar* pem;
gchar* pubkey_fingerprint;
} TLSCertificate;
void tlscerts_init(void);
TLSCertificate* tlscerts_new(const char* const fingerprint, int version, const char* const serialnumber, const char* const subjectname,
const char* const issuername, const char* const notbefore, const char* const notafter,
const char* const key_alg, const char* const signature_alg, const char* const pem);
TLSCertificate* tlscerts_new(const char* fingerprint_sha1, int version, const char* serialnumber, const char* subjectname,
const char* issuername, const char* notbefore, const char* notafter,
const char* key_alg, const char* signature_alg, const char* pem,
const char* fingerprint_sha256, const char* pubkey_fingerprint);
void tlscerts_set_current(const char* const fp);
void tlscerts_set_current(const TLSCertificate* cert);
char* tlscerts_get_current(void);
gboolean tlscerts_current_fingerprint_equals(const TLSCertificate* cert);
void tlscerts_clear_current(void);
gboolean tlscerts_exists(const char* const fingerprint);
gboolean tlscerts_exists(const TLSCertificate* cert);
void tlscerts_add(const TLSCertificate* cert);
gboolean tlscerts_revoke(const char* const fingerprint);
gboolean tlscerts_revoke(const char* fingerprint);
TLSCertificate* tlscerts_get_trusted(const char* const fingerprint);
TLSCertificate* tlscerts_get_trusted(const char* fingerprint);
void tlscerts_free(TLSCertificate* cert);
GList* tlscerts_list(void);
char* tlscerts_complete(const char* const prefix, gboolean previous, void* context);
char* tlscerts_complete(const char* prefix, gboolean previous, void* context);
void tlscerts_reset_ac(void);

View File

@@ -35,703 +35,187 @@
#include "config.h"
#include <sys/stat.h>
#include <sys/statvfs.h>
#include <sqlite3.h>
#include <glib.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include "log.h"
#include "common.h"
#include "config/files.h"
#include "config.h"
#include "database.h"
#include "config/preferences.h"
#include "config/accounts.h"
#include "ui/ui.h"
#include "xmpp/xmpp.h"
#include "xmpp/message.h"
static sqlite3* g_chatlog_database;
db_backend_t* active_db_backend = NULL;
static void _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 prof_enc_t _get_message_enc_type(const char* const encstr);
static int _get_db_version(void);
static gboolean _migrate_to_v2(void);
static gboolean _check_available_space_for_db_migration(char* path_to_db);
static const int latest_version = 2;
// Helper: close DB handle (if any), warn on busy, and shutdown SQLite
static void
_db_teardown(const char* ctx)
void
integrity_issue_free(integrity_issue_t* issue)
{
if (g_chatlog_database) {
int rc = sqlite3_close_v2(g_chatlog_database);
if (rc != SQLITE_OK) {
log_warning("sqlite3_close_v2 in %s returned %d; database may still have active statements.",
ctx ? ctx : "db_teardown", rc);
}
g_chatlog_database = NULL;
if (issue) {
g_free(issue->file);
g_free(issue->message);
g_free(issue);
}
// Safe to call unconditionally; no-op if not initialized.
// See: https://www.sqlite.org/c3ref/initialize.html
sqlite3_shutdown();
}
// Helper: prepare a statement and log a contextual error on failure
static gboolean
_db_prepare_ctx(const char* query, sqlite3_stmt** stmt, const char* ctx)
gboolean
log_database_can_recover_messages(void)
{
int rc = sqlite3_prepare_v2(g_chatlog_database, query, -1, stmt, NULL);
if (rc != SQLITE_OK) {
log_error("SQLite error in %s: (error code: %d) %s",
ctx ? ctx : "sqlite3_prepare_v2",
rc,
sqlite3_errmsg(g_chatlog_database));
if (!active_db_backend) {
return FALSE;
}
auto_gchar gchar* pref_dblog = prefs_get_string(PREF_DBLOG);
if (g_strcmp0(pref_dblog, "off") == 0) {
return FALSE;
}
if (g_strcmp0(pref_dblog, "redact") == 0) {
return FALSE;
}
return TRUE;
}
static char*
_db_strdup(const char* str)
{
return str ? strdup(str) : NULL;
}
#define auto_sqlite __attribute__((__cleanup__(auto_free_sqlite)))
static void
auto_free_sqlite(gchar** str)
{
if (str == NULL)
return;
sqlite3_free(*str);
}
static char*
_get_db_filename(ProfAccount* account)
{
return files_file_in_account_data_path(DIR_DATABASE, account->jid, "chatlog.db");
}
gboolean
log_database_init(ProfAccount* account)
{
int ret = sqlite3_initialize();
if (ret != SQLITE_OK) {
log_error("Error initializing SQLite database: %d", ret);
return FALSE;
}
auto_gchar gchar* pref_dblog = prefs_get_string(PREF_DBLOG);
auto_char char* filename = _get_db_filename(account);
if (!filename) {
sqlite3_shutdown();
return FALSE;
}
ret = sqlite3_open(filename, &g_chatlog_database);
if (ret != SQLITE_OK) {
const char* err_msg = g_chatlog_database ? sqlite3_errmsg(g_chatlog_database) : "(no handle)";
log_error("Error opening SQLite database: %s", err_msg);
_db_teardown("log_database_init(open)");
return FALSE;
}
char* err_msg = NULL;
int db_version = _get_db_version();
if (db_version == latest_version) {
return TRUE;
}
// ChatLogs Table
// Contains all chat messages
//
// id is primary key
// from_jid is the sender's jid
// to_jid is the receiver's jid
// from_resource is the sender's resource
// to_resource is the receiver's resource
// message is the message's text
// timestamp is the timestamp like "2020/03/24 11:12:14"
// type is there to distinguish: message (chat), MUC message (muc), muc pm (mucpm)
// stanza_id is the ID in <message>
// archive_id is the stanza-id from from XEP-0359: Unique and Stable Stanza IDs used for XEP-0313: Message Archive Management
// encryption is to distinguish: none, omemo, otr, pgp
// marked_read is 0/1 whether a message has been marked as read via XEP-0333: Chat Markers
// replace_id is the ID from XEP-0308: Last Message Correction
// replaces_db_id is ID (primary key) of the original message that LMC message corrects/replaces
// replaced_by_db_id is ID (primary key) of the last correcting (LMC) message for the original message
char* query = "CREATE TABLE IF NOT EXISTS `ChatLogs` ("
"`id` INTEGER PRIMARY KEY AUTOINCREMENT, "
"`from_jid` TEXT NOT NULL, "
"`to_jid` TEXT NOT NULL, "
"`from_resource` TEXT, "
"`to_resource` TEXT, "
"`message` TEXT, "
"`timestamp` TEXT, "
"`type` TEXT, "
"`stanza_id` TEXT, "
"`archive_id` TEXT, "
"`encryption` TEXT, "
"`marked_read` INTEGER, "
"`replace_id` TEXT, "
"`replaces_db_id` INTEGER, "
"`replaced_by_db_id` INTEGER)";
if (SQLITE_OK != sqlite3_exec(g_chatlog_database, query, NULL, 0, &err_msg)) {
goto out;
}
query = "CREATE TRIGGER IF NOT EXISTS update_corrected_message "
"AFTER INSERT ON ChatLogs "
"FOR EACH ROW "
"WHEN NEW.replaces_db_id IS NOT NULL "
"BEGIN "
"UPDATE ChatLogs "
"SET replaced_by_db_id = NEW.id "
"WHERE id = NEW.replaces_db_id; "
"END;";
if (SQLITE_OK != sqlite3_exec(g_chatlog_database, query, NULL, 0, &err_msg)) {
log_error("Unable to add `update_corrected_message` trigger.");
goto out;
}
query = "CREATE INDEX IF NOT EXISTS ChatLogs_timestamp_IDX ON `ChatLogs` (`timestamp`)";
if (SQLITE_OK != sqlite3_exec(g_chatlog_database, query, NULL, 0, &err_msg)) {
log_error("Unable to create index for timestamp.");
goto out;
}
query = "CREATE INDEX IF NOT EXISTS ChatLogs_to_from_jid_IDX ON `ChatLogs` (`to_jid`, `from_jid`)";
if (SQLITE_OK != sqlite3_exec(g_chatlog_database, query, NULL, 0, &err_msg)) {
log_error("Unable to create index for to_jid.");
goto out;
}
query = "CREATE TABLE IF NOT EXISTS `DbVersion` (`dv_id` INTEGER PRIMARY KEY, `version` INTEGER UNIQUE)";
if (SQLITE_OK != sqlite3_exec(g_chatlog_database, query, NULL, 0, &err_msg)) {
goto out;
}
if (db_version == -1) {
query = "INSERT OR IGNORE INTO `DbVersion` (`version`) VALUES ('2')";
if (SQLITE_OK != sqlite3_exec(g_chatlog_database, query, NULL, 0, &err_msg)) {
goto out;
}
db_version = _get_db_version();
}
// Unlikely event, but we don't want to migrate if we are just unable to determine the DB version
if (db_version == -1) {
cons_show_error("DB Initialization Error: Unable to check DB version.");
goto out;
}
if (db_version < latest_version) {
cons_show("Migrating database schema. This operation may take a while...");
if (db_version < 2 && (!_check_available_space_for_db_migration(filename) || !_migrate_to_v2())) {
cons_show_error("Database Initialization Error: Unable to migrate database to version 2. Please, check error logs for details.");
goto out;
}
cons_show("Database schema migration was successful.");
}
log_debug("Initialized SQLite database: %s", filename);
return TRUE;
out:
if (err_msg) {
log_error("SQLite error in log_database_init(): %s", err_msg);
sqlite3_free(err_msg);
// Select backend based on preference
if (g_strcmp0(pref_dblog, "flatfile") == 0) {
active_db_backend = db_backend_flatfile();
log_info("Using flat-file database backend");
} else {
log_error("Unknown SQLite error in log_database_init().");
#ifdef HAVE_SQLITE
active_db_backend = db_backend_sqlite();
log_info("Using SQLite database backend");
#else
log_warning("SQLite not compiled in, falling back to flat-file backend");
active_db_backend = db_backend_flatfile();
#endif
}
_db_teardown("log_database_init(out)");
return FALSE;
if (!active_db_backend) {
log_error("log_database_init: no backend available");
return FALSE;
}
return active_db_backend->init(account);
}
void
log_database_close(void)
{
log_debug("log_database_close() called");
_db_teardown("log_database_close");
if (active_db_backend) {
active_db_backend->close();
}
active_db_backend = NULL;
}
gboolean
log_database_switch_backend(const char* new_backend)
{
// Close current backend
log_database_close();
// Update preference (user must /save to persist across restarts)
prefs_set_string(PREF_DBLOG, new_backend);
// Get current account to reinitialize
const char* account_name = session_get_account_name();
if (!account_name) {
log_error("log_database_switch_backend: no active session");
return FALSE;
}
ProfAccount* account = accounts_get_account(account_name);
if (!account) {
log_error("log_database_switch_backend: account '%s' not found", account_name);
return FALSE;
}
gboolean result = log_database_init(account);
account_free(account);
return result;
}
void
log_database_add_incoming(ProfMessage* message)
{
if (message->to_jid) {
_add_to_db(message, NULL, message->from_jid, message->to_jid);
if (active_db_backend && active_db_backend->add_incoming) {
active_db_backend->add_incoming(message);
} else {
_add_to_db(message, NULL, message->from_jid, connection_get_jid());
log_warning("log_database_add_incoming: no backend or method available");
}
}
static void
_log_database_add_outgoing(char* type, const char* const id, const char* const barejid, const char* const message, const char* const replace_id, prof_enc_t enc)
{
ProfMessage* msg = message_init();
msg->id = _db_strdup(id);
msg->from_jid = jid_create(barejid);
msg->plain = _db_strdup(message);
msg->replace_id = _db_strdup(replace_id);
msg->timestamp = g_date_time_new_now_local(); // TODO: get from outside. best to have whole ProfMessage from outside
msg->enc = enc;
_add_to_db(msg, type, connection_get_jid(), msg->from_jid); // TODO: myjid now in profmessage
message_free(msg);
}
void
log_database_add_outgoing_chat(const char* const id, const char* const barejid, const char* const message, const char* const replace_id, prof_enc_t enc)
{
_log_database_add_outgoing("chat", id, barejid, message, replace_id, enc);
if (active_db_backend && active_db_backend->add_outgoing_chat) {
active_db_backend->add_outgoing_chat(id, barejid, message, replace_id, enc);
} else {
log_warning("log_database_add_outgoing_chat: no backend or method available");
}
}
void
log_database_add_outgoing_muc(const char* const id, const char* const barejid, const char* const message, const char* const replace_id, prof_enc_t enc)
{
_log_database_add_outgoing("muc", id, barejid, message, replace_id, enc);
if (active_db_backend && active_db_backend->add_outgoing_muc) {
active_db_backend->add_outgoing_muc(id, barejid, message, replace_id, enc);
} else {
log_warning("log_database_add_outgoing_muc: no backend or method available");
}
}
void
log_database_add_outgoing_muc_pm(const char* const id, const char* const barejid, const char* const message, const char* const replace_id, prof_enc_t enc)
{
_log_database_add_outgoing("mucpm", id, barejid, message, replace_id, enc);
if (active_db_backend && active_db_backend->add_outgoing_muc_pm) {
active_db_backend->add_outgoing_muc_pm(id, barejid, message, replace_id, enc);
} else {
log_warning("log_database_add_outgoing_muc_pm: no backend or method available");
}
}
// Get info (timestamp and stanza_id) of the first or last message in db
ProfMessage*
log_database_get_limits_info(const gchar* const contact_barejid, gboolean is_last)
{
sqlite3_stmt* stmt = NULL;
const Jid* myjid = connection_get_jid();
// Always return a valid ProfMessage to avoid NULL dereferences in callers
ProfMessage* msg = message_init();
if (!myjid || !myjid->str) {
// If caller requested the last message and we have no context, fall back to now
if (is_last) {
msg->timestamp = g_date_time_new_now_utc();
}
return msg;
}
const char* order = is_last ? "DESC" : "ASC";
auto_sqlite char* query = sqlite3_mprintf("SELECT `archive_id`, `timestamp` FROM `ChatLogs` WHERE "
"(`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);
if (!query) {
log_error("Could not allocate memory for SQL query in log_database_get_limits_info()");
if (is_last) {
msg->timestamp = g_date_time_new_now_utc();
}
return msg;
}
if (!_db_prepare_ctx(query, &stmt, "log_database_get_limits_info()")) {
if (is_last) {
msg->timestamp = g_date_time_new_now_utc();
}
return msg;
}
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);
}
sqlite3_finalize(stmt);
// If nothing was found and caller expects the last message, provide a sane default
if (!msg->timestamp && is_last) {
msg->timestamp = g_date_time_new_now_utc();
}
return msg;
}
// 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
db_history_result_t
log_database_get_previous_chat(const gchar* const contact_barejid, const gchar* start_time, const gchar* end_time, gboolean from_start, gboolean flip, GSList** result)
{
if (!g_chatlog_database) {
log_warning("log_database_get_previous_chat() called but db is not initialized");
return DB_RESPONSE_ERROR;
if (active_db_backend && active_db_backend->get_previous_chat) {
return active_db_backend->get_previous_chat(contact_barejid, start_time, end_time, from_start, flip, result);
}
sqlite3_stmt* stmt = NULL;
const Jid* myjid = connection_get_jid();
if (!myjid->str) {
log_warning("log_database_get_previous_chat() called but no connection detected.");
return DB_RESPONSE_ERROR;
}
// Flip order when querying older pages
gchar* sort1 = from_start ? "ASC" : "DESC";
gchar* sort2 = !flip ? "ASC" : "DESC";
GDateTime* now = g_date_time_new_now_local();
auto_gchar gchar* end_date_fmt = end_time ? g_strdup(end_time) : g_date_time_format_iso8601(now);
auto_sqlite gchar* query = sqlite3_mprintf("SELECT * FROM ("
"SELECT COALESCE(B.`message`, A.`message`) AS message, "
"A.`timestamp`, A.`from_jid`, A.`from_resource`, A.`to_jid`, A.`to_resource`, A.`type`, A.`encryption`, A.`stanza_id` FROM `ChatLogs` AS A "
"LEFT JOIN `ChatLogs` AS B ON (A.`replaced_by_db_id` = B.`id` AND A.`from_jid` = B.`from_jid`) "
"WHERE (A.`replaces_db_id` IS NULL) "
"AND ((A.`from_jid` = %Q AND A.`to_jid` = %Q) OR (A.`from_jid` = %Q AND A.`to_jid` = %Q)) "
"AND A.`timestamp` < %Q "
"AND (%Q IS NULL OR A.`timestamp` > %Q) "
"ORDER BY A.`timestamp` %s LIMIT %d) "
"ORDER BY `timestamp` %s;",
contact_barejid, myjid->barejid, myjid->barejid, contact_barejid, end_date_fmt, start_time, start_time, sort1, MESSAGES_TO_RETRIEVE, sort2);
g_date_time_unref(now);
if (!query) {
log_error("Could not allocate memory.");
return DB_RESPONSE_ERROR;
}
if (!_db_prepare_ctx(query, &stmt, "log_database_get_previous_chat()")) {
return DB_RESPONSE_ERROR;
}
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);
*result = g_slist_append(*result, msg);
}
sqlite3_finalize(stmt);
return g_slist_length(*result) != 0 ? DB_RESPONSE_SUCCESS : DB_RESPONSE_EMPTY;
return DB_RESPONSE_ERROR;
}
static const char*
_get_message_type_str(prof_msg_type_t type)
ProfMessage*
log_database_get_limits_info(const gchar* const contact_barejid, gboolean is_last)
{
switch (type) {
case PROF_MSG_TYPE_CHAT:
return "chat";
case PROF_MSG_TYPE_MUC:
return "muc";
case PROF_MSG_TYPE_MUCPM:
return "mucpm";
case PROF_MSG_TYPE_UNINITIALIZED:
return NULL;
if (active_db_backend && active_db_backend->get_limits_info) {
return active_db_backend->get_limits_info(contact_barejid, is_last);
}
return NULL;
// Fallback: return an empty message with sane defaults
ProfMessage* msg = message_init();
if (is_last) {
msg->timestamp = g_date_time_new_now_utc();
}
return msg;
}
static prof_msg_type_t
_get_message_type_type(const char* const type)
GSList*
log_database_verify_integrity(const gchar* const contact_barejid)
{
if (g_strcmp0(type, "chat") == 0) {
return PROF_MSG_TYPE_CHAT;
} else if (g_strcmp0(type, "muc") == 0) {
return PROF_MSG_TYPE_MUC;
} else if (g_strcmp0(type, "mucpm") == 0) {
return PROF_MSG_TYPE_MUCPM;
} else {
return PROF_MSG_TYPE_UNINITIALIZED;
}
}
static const char*
_get_message_enc_str(prof_enc_t enc)
{
switch (enc) {
case PROF_MSG_ENC_OX:
return "ox";
case PROF_MSG_ENC_PGP:
return "pgp";
case PROF_MSG_ENC_OTR:
return "otr";
case PROF_MSG_ENC_OMEMO:
return "omemo";
case PROF_MSG_ENC_NONE:
return "none";
}
return "none";
}
static prof_enc_t
_get_message_enc_type(const char* const encstr)
{
if (g_strcmp0(encstr, "ox") == 0) {
return PROF_MSG_ENC_OX;
} else if (g_strcmp0(encstr, "pgp") == 0) {
return PROF_MSG_ENC_PGP;
} else if (g_strcmp0(encstr, "otr") == 0) {
return PROF_MSG_ENC_OTR;
} else if (g_strcmp0(encstr, "omemo") == 0) {
return PROF_MSG_ENC_OMEMO;
}
return PROF_MSG_ENC_NONE;
}
static void
_add_to_db(ProfMessage* message, char* type, const Jid* const from_jid, const Jid* const to_jid)
{
auto_gchar gchar* pref_dblog = prefs_get_string(PREF_DBLOG);
sqlite_int64 original_message_id = -1;
if (g_strcmp0(pref_dblog, "off") == 0) {
return;
} else if (g_strcmp0(pref_dblog, "redact") == 0) {
if (message->plain) {
free(message->plain);
}
message->plain = strdup("[REDACTED]");
}
if (!g_chatlog_database) {
log_debug("log_database_add() called but db is not initialized");
return;
}
char* err_msg;
auto_gchar gchar* date_fmt = NULL;
if (message->timestamp) {
date_fmt = g_date_time_format_iso8601(message->timestamp);
} else {
GDateTime* dt = g_date_time_new_now_local();
date_fmt = g_date_time_format_iso8601(dt);
g_date_time_unref(dt);
}
const char* enc = _get_message_enc_str(message->enc);
if (!type) {
type = (char*)_get_message_type_str(message->type);
}
// Apply LMC and check its validity (XEP-0308)
if (message->replace_id) {
auto_sqlite char* replace_check_query = sqlite3_mprintf("SELECT `id`, `from_jid`, `replaces_db_id` FROM `ChatLogs` WHERE `stanza_id` = %Q ORDER BY `timestamp` DESC LIMIT 1",
message->replace_id);
if (!replace_check_query) {
log_error("Could not allocate memory for SQL replace query in log_database_add()");
return;
}
sqlite3_stmt* lmc_stmt = NULL;
if (!_db_prepare_ctx(replace_check_query, &lmc_stmt, "_add_to_db(replace_check)")) {
return;
}
if (sqlite3_step(lmc_stmt) == SQLITE_ROW) {
original_message_id = sqlite3_column_int64(lmc_stmt, 0);
const char* from_jid_orig = (const char*)sqlite3_column_text(lmc_stmt, 1);
// Handle non-XEP-compliant replacement messages (edit->edit->original)
sqlite_int64 tmp = sqlite3_column_int64(lmc_stmt, 2);
original_message_id = tmp ? tmp : original_message_id;
if (g_strcmp0(from_jid_orig, from_jid->barejid) != 0) {
log_error("Mismatch in sender JIDs when trying to do LMC. Corrected message sender: %s. Original message sender: %s. Replace-ID: %s. Message: %s", from_jid->barejid, from_jid_orig, message->replace_id, message->plain);
cons_show_error("%s sent a message correction with mismatched sender. See log for details.", from_jid->barejid);
sqlite3_finalize(lmc_stmt);
return;
}
} else {
log_warning("Got LMC message that does not have original message counterpart in the database from %s", message->from_jid->fulljid);
}
sqlite3_finalize(lmc_stmt);
}
// stanza-id (XEP-0359) doesn't have to be present in the message.
// But if it's duplicated, it's a serious server-side problem, so we better track it.
// Unless it's MAM, in that case it's expected behaviour.
if (message->stanzaid && !message->is_mam) {
auto_sqlite char* duplicate_check_query = sqlite3_mprintf("SELECT 1 FROM `ChatLogs` WHERE (`archive_id` = %Q)",
message->stanzaid);
if (!duplicate_check_query) {
log_error("Could not allocate memory for SQL duplicate query in log_database_add()");
return;
}
sqlite3_stmt* stmt;
if (_db_prepare_ctx(duplicate_check_query, &stmt, "_add_to_db(duplicate_check)")) {
if (sqlite3_step(stmt) == SQLITE_ROW) {
log_error("Duplicate stanza-id found for the message. stanza_id: %s; archive_id: %s; sender: %s; content: %s", message->id, message->stanzaid, from_jid->barejid, message->plain);
cons_show_error("Got a message with duplicate (server-generated) stanza-id from %s.", from_jid->fulljid);
}
sqlite3_finalize(stmt);
}
}
auto_sqlite char* orig_message_id = original_message_id == -1 ? NULL : sqlite3_mprintf("%d", original_message_id);
auto_sqlite char* query = sqlite3_mprintf("INSERT INTO `ChatLogs` "
"(`from_jid`, `from_resource`, `to_jid`, `to_resource`, "
"`message`, `timestamp`, `stanza_id`, `archive_id`, "
"`replaces_db_id`, `replace_id`, `type`, `encryption`) "
"VALUES (%Q, %Q, %Q, %Q, %Q, %Q, %Q, %Q, %Q, %Q, %Q, %Q)",
from_jid->barejid,
from_jid->resourcepart,
to_jid->barejid,
to_jid->resourcepart,
message->plain,
date_fmt,
message->id,
message->stanzaid,
orig_message_id,
message->replace_id,
type,
enc);
if (!query) {
log_error("Could not allocate memory for SQL insert query in log_database_add()");
return;
}
log_debug("Writing to DB. Query: %s", query);
if (SQLITE_OK != sqlite3_exec(g_chatlog_database, query, NULL, 0, &err_msg)) {
if (err_msg) {
log_error("SQLite error in _add_to_db(): %s", err_msg);
sqlite3_free(err_msg);
} else {
log_error("Unknown SQLite error in _add_to_db().");
}
} else {
int inserted_rows_count = sqlite3_changes(g_chatlog_database);
if (inserted_rows_count < 1) {
log_error("SQLite did not insert message (rows: %d, id: %s, content: %s)", inserted_rows_count, message->id, message->plain);
}
}
}
static int
_get_db_version(void)
{
int current_version = -1;
const char* query = "SELECT `version` FROM `DbVersion` LIMIT 1";
sqlite3_stmt* statement;
if (_db_prepare_ctx(query, &statement, "_get_db_version()")) {
if (sqlite3_step(statement) == SQLITE_ROW) {
current_version = sqlite3_column_int(statement, 0);
}
sqlite3_finalize(statement);
}
return current_version;
}
/**
* Migration to version 2 introduces new columns. Returns TRUE on success.
*
* New columns:
* `replaces_db_id` database ID for correcting message of the original message
* `replaced_by_db_id` database ID for original message of the last correcting message
*/
static gboolean
_migrate_to_v2(void)
{
char* err_msg = NULL;
// from_resource, to_resource, message, timestamp, stanza_id, archive_id, replace_id, type, encryption
const char* sql_statements[] = {
"BEGIN TRANSACTION",
"ALTER TABLE `ChatLogs` ADD COLUMN `replaces_db_id` INTEGER;",
"ALTER TABLE `ChatLogs` ADD COLUMN `replaced_by_db_id` INTEGER;",
"UPDATE `ChatLogs` AS A "
"SET `replaces_db_id` = B.`id` "
"FROM `ChatLogs` AS B "
"WHERE A.`replace_id` IS NOT NULL AND A.`replace_id` != '' "
"AND A.`replace_id` = B.`stanza_id` "
"AND A.`from_jid` = B.`from_jid` AND A.`to_jid` = B.`to_jid`;",
"UPDATE `ChatLogs` AS A "
"SET `replaced_by_db_id` = B.`id` "
"FROM `ChatLogs` AS B "
"WHERE (A.`replace_id` IS NULL OR A.`replace_id` = '') "
"AND A.`id` = B.`replaces_db_id` "
"AND A.`from_jid` = B.`from_jid`;",
"UPDATE ChatLogs SET "
"from_resource = COALESCE(NULLIF(from_resource, ''), NULL), "
"to_resource = COALESCE(NULLIF(to_resource, ''), NULL), "
"message = COALESCE(NULLIF(message, ''), NULL), "
"timestamp = COALESCE(NULLIF(timestamp, ''), NULL), "
"stanza_id = COALESCE(NULLIF(stanza_id, ''), NULL), "
"archive_id = COALESCE(NULLIF(archive_id, ''), NULL), "
"replace_id = COALESCE(NULLIF(replace_id, ''), NULL), "
"type = COALESCE(NULLIF(type, ''), NULL), "
"encryption = COALESCE(NULLIF(encryption, ''), NULL);",
"UPDATE `DbVersion` SET `version` = 2;",
"END TRANSACTION"
};
int statements_count = sizeof(sql_statements) / sizeof(sql_statements[0]);
for (int i = 0; i < statements_count; i++) {
if (SQLITE_OK != sqlite3_exec(g_chatlog_database, sql_statements[i], NULL, 0, &err_msg)) {
log_error("SQLite error in _migrate_to_v2() on statement %d: %s", i, err_msg);
if (err_msg) {
sqlite3_free(err_msg);
err_msg = NULL;
}
goto cleanup;
}
}
return TRUE;
cleanup:
if (SQLITE_OK != sqlite3_exec(g_chatlog_database, "ROLLBACK;", NULL, 0, &err_msg)) {
log_error("[DB Migration] Unable to ROLLBACK: %s", err_msg);
if (err_msg) {
sqlite3_free(err_msg);
}
}
return FALSE;
}
// Checks if there is more system storage space available than current database takes + 40% (for indexing and other potential size increases)
static gboolean
_check_available_space_for_db_migration(char* path_to_db)
{
struct stat file_stat;
struct statvfs fs_stat;
if (statvfs(path_to_db, &fs_stat) == 0 && stat(path_to_db, &file_stat) == 0) {
unsigned long long file_size = file_stat.st_size / 1024;
unsigned long long available_space_kb = fs_stat.f_frsize * fs_stat.f_bavail / 1024;
log_debug("_check_available_space_for_db_migration(): Available space on disk: %llu KB; DB size: %llu KB", available_space_kb, file_size);
return (available_space_kb >= (file_size + (file_size * 10 / 4)));
} else {
log_error("Error checking available space.");
return FALSE;
if (active_db_backend && active_db_backend->verify_integrity) {
return active_db_backend->verify_integrity(contact_barejid);
}
GSList* issues = NULL;
integrity_issue_t* issue = g_malloc0(sizeof(integrity_issue_t));
issue->level = INTEGRITY_WARNING;
issue->file = g_strdup("N/A");
issue->line = 0;
issue->message = g_strdup("Active backend does not support integrity verification");
issues = g_slist_append(issues, issue);
return issues;
}

View File

@@ -2,35 +2,9 @@
* database.h
* vim: expandtab:ts=4:sts=4:sw=4
*
* Copyright (C) 2020 - 2025 Michael Vetter <jubalh@iodoru.org>
*
* This file is part of Profanity.
*
* Profanity is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Profanity is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Profanity. If not, see <https://www.gnu.org/licenses/>.
*
* In addition, as a special exception, the copyright holders give permission to
* link the code of portions of this program with the OpenSSL library under
* certain conditions as described in each individual source file, and
* distribute linked combinations including the two.
*
* You must obey the GNU General Public License in all respects for all of the
* code used other than OpenSSL. If you modify file(s) with this exception, you
* may extend this exception to your version of the file(s), but you are not
* obligated to do so. If you do not wish to do so, delete this exception
* statement from your version. If you delete this exception statement from all
* source files in the program, then also delete it here.
* Copyright (C) 2020 - 2026 Michael Vetter <jubalh@iodoru.org>
*
* SPDX-License-Identifier: GPL-3.0-or-later WITH OpenSSL-exception
*/
#ifndef DATABASE_H
@@ -48,7 +22,58 @@ typedef enum {
DB_RESPONSE_SUCCESS
} db_history_result_t;
// Integrity verification issue levels
typedef enum {
INTEGRITY_ERROR,
INTEGRITY_WARNING,
INTEGRITY_INFO
} integrity_level_t;
// A single integrity issue found during verification
typedef struct
{
integrity_level_t level;
char* file;
int line;
char* message;
} integrity_issue_t;
void integrity_issue_free(integrity_issue_t* issue);
// Backend vtable: pluggable storage backends implement this interface
typedef struct db_backend_t
{
const char* name;
gboolean (*init)(ProfAccount* account);
void (*close)(void);
void (*add_incoming)(ProfMessage* message);
void (*add_outgoing_chat)(const char* const id, const char* const barejid, const char* const message, const char* const replace_id, prof_enc_t enc);
void (*add_outgoing_muc)(const char* const id, const char* const barejid, const char* const message, const char* const replace_id, prof_enc_t enc);
void (*add_outgoing_muc_pm)(const char* const id, const char* const barejid, const char* const message, const char* const replace_id, prof_enc_t enc);
db_history_result_t (*get_previous_chat)(const gchar* const contact_barejid, const gchar* start_time, const gchar* end_time, gboolean from_start, gboolean flip, GSList** result);
ProfMessage* (*get_limits_info)(const gchar* const contact_barejid, gboolean is_last);
GSList* (*verify_integrity)(const gchar* const contact_barejid);
} db_backend_t;
// Active backend (set during init based on PREF_DBLOG)
extern db_backend_t* active_db_backend;
// Backend registry
#ifdef HAVE_SQLITE
db_backend_t* db_backend_sqlite(void);
GSList* db_sqlite_list_contacts(void);
GSList* db_sqlite_get_all_chat(const gchar* const contact_barejid);
void db_sqlite_begin_transaction(void);
void db_sqlite_end_transaction(void);
void db_sqlite_rollback_transaction(void);
int db_sqlite_last_changes(void);
gboolean db_sqlite_is_open(void);
#endif
db_backend_t* db_backend_flatfile(void);
// Public API (dispatches to active_db_backend)
gboolean log_database_init(ProfAccount* account);
gboolean log_database_switch_backend(const char* new_backend);
void log_database_add_incoming(ProfMessage* message);
void log_database_add_outgoing_chat(const char* const id, const char* const barejid, const char* const message, const char* const replace_id, prof_enc_t enc);
void log_database_add_outgoing_muc(const char* const id, const char* const barejid, const char* const message, const char* const replace_id, prof_enc_t enc);
@@ -56,5 +81,15 @@ void log_database_add_outgoing_muc_pm(const char* const id, const char* const ba
db_history_result_t log_database_get_previous_chat(const gchar* const contact_barejid, const gchar* start_time, const gchar* end_time, gboolean from_start, gboolean flip, GSList** result);
ProfMessage* log_database_get_limits_info(const gchar* const contact_barejid, gboolean is_last);
void log_database_close(void);
GSList* log_database_verify_integrity(const gchar* const contact_barejid);
// FALSE if no backend, or PREF_DBLOG is "off" / "redact".
gboolean log_database_can_recover_messages(void);
// Cross-backend export/import (requires HAVE_SQLITE)
#ifdef HAVE_SQLITE
int log_database_export_to_flatfile(const gchar* const contact_jid);
int log_database_import_from_flatfile(const gchar* const contact_jid);
#endif
#endif // DATABASE_H

680
src/database_export.c Normal file
View File

@@ -0,0 +1,680 @@
// SPDX-License-Identifier: GPL-3.0-or-later
//
// This file is part of CProof.
// See LICENSE for the full GPLv3 text and the special OpenSSL linking exception.
/*
* database_export.c
* vim: expandtab:ts=4:sts=4:sw=4
*
* Cross-backend export/import for message history.
* Reads from one backend, writes to the other, with merge + dedup.
*/
#include "config.h"
#include <glib.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/file.h>
#ifdef HAVE_SQLITE
#include <sqlite3.h>
#endif
#include "log.h"
#include "common.h"
#include "config/files.h"
#include "config/accounts.h"
#include "database.h"
#include "database_flatfile.h"
#include "config/preferences.h"
#include "ui/ui.h"
#include "xmpp/session.h"
#include "xmpp/xmpp.h"
#include "xmpp/message.h"
// =========================================================================
// Everything below is only used when HAVE_SQLITE is defined
// =========================================================================
#ifdef HAVE_SQLITE
// Print progress to console every N messages during export/import
#define EXPORT_PROGRESS_INTERVAL 500
// Open SQLite if it isn't already (e.g. when flatfile is the active backend
// and sqlite was never initialised this session, or was closed by /history
// switch). Returns 1 if we opened it (caller must close), 0 if it was already
// open, -1 on failure.
static int
_ensure_sqlite_open(void)
{
if (db_sqlite_is_open())
return 0;
db_backend_t* sqlite_be = db_backend_sqlite();
if (!sqlite_be || !sqlite_be->init)
return -1;
const char* account_name = session_get_account_name();
if (!account_name)
return -1;
ProfAccount* account = accounts_get_account(account_name);
if (!account)
return -1;
gboolean ok = sqlite_be->init(account);
account_free(account);
return ok ? 1 : -1;
}
static void
_close_temp_sqlite(int opened)
{
if (opened != 1)
return;
db_backend_t* sqlite_be = db_backend_sqlite();
if (sqlite_be && sqlite_be->close)
sqlite_be->close();
}
// =========================================================================
// Dedup key: stanza_id if present, else hash of timestamp+from+body
// =========================================================================
// Returns a g_strdup'd key — callers free with g_free (gchar* convention).
//
// stanza_id is mixed into the hash but is NOT used as the sole key, since
// some clients (older Pidgin, Adium) reuse incremental ids per session and
// servers echo them back unchanged. Trusting stanza_id alone silently drops
// legitimate distinct messages on collision. By hashing id + timestamp +
// from_jid + body together we preserve id as a disambiguator without making
// it a single point of failure: true exact duplicates still dedup, but two
// messages that share an id with different content (or different timestamps)
// stay distinct.
static gchar*
_make_dedup_key(const char* stanza_id, const char* timestamp, const char* from_jid, const char* body)
{
GChecksum* sum = g_checksum_new(G_CHECKSUM_SHA256);
g_checksum_update(sum, (const guchar*)(stanza_id ? stanza_id : ""), -1);
g_checksum_update(sum, (const guchar*)"|", 1);
g_checksum_update(sum, (const guchar*)(timestamp ? timestamp : ""), -1);
g_checksum_update(sum, (const guchar*)"|", 1);
g_checksum_update(sum, (const guchar*)(from_jid ? from_jid : ""), -1);
g_checksum_update(sum, (const guchar*)"|", 1);
g_checksum_update(sum, (const guchar*)(body ? body : ""), -1);
gchar* key = g_strdup(g_checksum_get_string(sum));
g_checksum_free(sum);
return key;
}
// =========================================================================
// Helper: reverse ff_jid_to_dir (best-effort)
// '_at_' -> '@'
// Returns a g_strdup'd jid — callers free with g_free (gchar* convention).
// str_replace allocates via libc malloc, so we hand over to g_-allocated.
// =========================================================================
static gchar*
_dir_to_jid(const char* dirname)
{
if (!dirname)
return NULL;
char* raw = str_replace(dirname, "_at_", "@");
if (!raw)
return NULL;
gchar* gres = g_strdup(raw);
free(raw);
return gres;
}
// =========================================================================
// Helper: list all contact JIDs from flatfile directory
// Returns a GSList of g_strdup'd JID strings. Caller frees with g_slist_free_full(list, g_free).
// =========================================================================
static GSList*
_ff_list_contacts(void)
{
GSList* contacts = NULL;
if (!g_flatfile_account_jid)
return NULL;
auto_gchar gchar* data_path = files_get_data_path(DIR_FLATLOG);
auto_gchar gchar* my_dir = ff_jid_to_dir(g_flatfile_account_jid);
auto_gchar gchar* base_dir = g_strdup_printf("%s/%s", data_path, my_dir);
GError* err = NULL;
GDir* dir = g_dir_open(base_dir, 0, &err);
if (!dir) {
if (err) {
log_warning("export: cannot open flatlog dir %s: %s", base_dir, err->message);
g_error_free(err);
} else {
log_warning("export: cannot open flatlog dir %s (no error details)", base_dir);
}
return NULL;
}
const gchar* entry;
while ((entry = g_dir_read_name(dir)) != NULL) {
auto_gchar gchar* full_path = g_strdup_printf("%s/%s/history.log", base_dir, entry);
if (g_file_test(full_path, G_FILE_TEST_IS_REGULAR)) {
gchar* jid = _dir_to_jid(entry);
if (jid) {
contacts = g_slist_prepend(contacts, jid);
}
}
}
g_dir_close(dir);
return g_slist_reverse(contacts);
}
// =========================================================================
// Helper: read ALL parsed lines from a flatfile for a contact
// Returns GSList<ff_parsed_line_t*>. Caller frees with g_slist_free_full(list, (GDestroyNotify)ff_parsed_line_free).
// =========================================================================
// Hard ceiling on the total size of a single contact's history.log that
// _ff_read_all_lines is willing to slurp into memory. Each line expands
// ~10x on parse (12 g_strdup'd fields + GDateTime), so 2 GB on disk
// becomes ~20 GB peak heap — well past any realistic working set.
#define FF_EXPORT_MAX_FILE_BYTES (2LL * 1024 * 1024 * 1024)
static GSList*
_ff_read_all_lines(const char* contact_barejid)
{
GSList* lines = NULL;
auto_gchar gchar* log_path = ff_get_log_path(contact_barejid);
if (!log_path)
return NULL;
// Refuse upfront if the file is so large that parsing will OOM the
// process. We have no streaming-parse path; the whole file becomes
// a GSList of ff_parsed_line_t in memory. Surface a clear error so
// the user knows what hit them, rather than the kernel killing prof.
struct stat st;
if (stat(log_path, &st) == 0 && st.st_size > FF_EXPORT_MAX_FILE_BYTES) {
log_error("export: %s is %lld bytes (> %lld limit); refusing to load.",
log_path, (long long)st.st_size,
(long long)FF_EXPORT_MAX_FILE_BYTES);
cons_show_error("Flat-file for %s exceeds %lld MB load limit; cannot export.",
contact_barejid, (long long)(FF_EXPORT_MAX_FILE_BYTES / (1024 * 1024)));
return NULL;
}
FILE* fp = fopen(log_path, "r");
if (!fp)
return NULL;
while (1) {
gboolean truncated = FALSE;
auto_char char* buf = ff_readline(fp, &truncated);
if (!buf)
break;
if (truncated)
break;
if (buf[0] == '\0' || buf[0] == '#')
continue;
ff_parsed_line_t* pl = ff_parse_line(buf);
if (pl) {
lines = g_slist_prepend(lines, pl);
}
}
fclose(fp);
return g_slist_reverse(lines);
}
// =========================================================================
// Comparator for sorting ff_parsed_line_t by timestamp (ISO8601 is lexicographic)
// =========================================================================
static gint
_compare_parsed_lines_by_timestamp(gconstpointer a, gconstpointer b)
{
const ff_parsed_line_t* la = a;
const ff_parsed_line_t* lb = b;
int cmp = g_strcmp0(la->timestamp_str, lb->timestamp_str);
if (cmp != 0)
return cmp;
// Secondary key: stanza_id (stabilises sort for same-second messages)
cmp = g_strcmp0(la->stanza_id, lb->stanza_id);
if (cmp != 0)
return cmp;
return g_strcmp0(la->from_jid, lb->from_jid);
}
// =========================================================================
// Export: SQLite -> flatfile (merge)
// =========================================================================
// Callback data for the SQLite query
typedef struct
{
GHashTable* seen_keys; // dedup set
FILE* fp; // output file
int exported; // counter
int skipped; // counter
} export_ctx_t;
// Export a single contact's messages from SQLite to flatfile.
// Returns the number of newly exported messages, or -1 on error/skip.
static int
_export_one_contact(const char* contact)
{
// 1. Read existing flatfile lines for dedup
GHashTable* seen_keys = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL);
GSList* existing = _ff_read_all_lines(contact);
for (GSList* l = existing; l; l = l->next) {
ff_parsed_line_t* pl = l->data;
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 = 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);
if (!sqlite_lines) {
log_debug("export: no SQLite messages for %s", contact);
g_hash_table_destroy(seen_keys);
g_slist_free_full(existing, (GDestroyNotify)ff_parsed_line_free);
return -1;
}
// 3. Merge: write existing flatfile lines + new SQLite lines to temp file
auto_gchar gchar* log_path = ff_get_log_path(contact);
if (!log_path) {
g_hash_table_destroy(seen_keys);
g_slist_free_full(existing, (GDestroyNotify)ff_parsed_line_free);
g_slist_free_full(sqlite_lines, (GDestroyNotify)message_free);
return -1;
}
// Ensure directory
auto_gchar gchar* dir = g_path_get_dirname(log_path);
ff_ensure_dir(dir);
// Use mkstemp() to create a unique-named temp file. The previous fixed
// "{log_path}.export.tmp" name + unlink-retry was vulnerable to two
// concurrent exports clobbering each other's in-flight tmp files
// (process B unlinks A's mid-write tmp, both race to write, second
// rename wins). Random suffix eliminates that.
//
// mkstemp on glibc creates the file with mode 0600. We fchmod to be
// sure across libc implementations. mkstemp uses O_RDWR|O_CREAT|O_EXCL
// internally; symlink attacks aren't possible because the name was
// just generated and no other process knows it.
auto_gchar gchar* tmp_path = g_strdup_printf("%s.export.XXXXXX", log_path);
int tmp_fd = mkstemp(tmp_path);
if (tmp_fd < 0) {
log_error("export: mkstemp(%s) failed: %s", tmp_path, strerror(errno));
g_hash_table_destroy(seen_keys);
g_slist_free_full(existing, (GDestroyNotify)ff_parsed_line_free);
g_slist_free_full(sqlite_lines, (GDestroyNotify)message_free);
return -1;
}
if (fchmod(tmp_fd, S_IRUSR | S_IWUSR) != 0) {
log_warning("export: fchmod(%s, 0600) failed: %s", tmp_path, strerror(errno));
}
FILE* fp = fdopen(tmp_fd, "w");
if (!fp) {
log_error("export: fdopen failed for %s: %s", tmp_path, strerror(errno));
close(tmp_fd);
unlink(tmp_path);
g_hash_table_destroy(seen_keys);
g_slist_free_full(existing, (GDestroyNotify)ff_parsed_line_free);
g_slist_free_full(sqlite_lines, (GDestroyNotify)message_free);
return -1;
}
// Lock the temp file
int fd = fileno(fp);
if (flock(fd, LOCK_EX) != 0) {
log_warning("export: flock(%s) failed: %s", tmp_path, strerror(errno));
}
fprintf(fp, "%s", FLATFILE_HEADER);
int contact_exported = 0;
int contact_skipped = 0;
// Collect ALL lines into a merged list for sorted output.
// Transfer ownership of every parsed line from `existing` directly into
// `merged` instead of deep-copying — saves ~12 g_strdup calls per row,
// which dominate wall time at 100k+ rows. After splicing we free the
// list nodes (not the data) and NULL out existing so the final cleanup
// doesn't double-free.
GSList* merged = NULL;
for (GSList* l = existing; l; l = l->next) {
merged = g_slist_prepend(merged, l->data);
}
g_slist_free(existing);
existing = NULL;
// Add SQLite messages that aren't already in the flatfile
for (GSList* l = sqlite_lines; l; l = l->next) {
ProfMessage* msg = l->data;
if (!msg || !msg->timestamp)
continue;
auto_gchar gchar* ts = g_date_time_format_iso8601(msg->timestamp);
const char* from_jid = msg->from_jid ? msg->from_jid->barejid : "unknown";
const char* body = msg->plain ? msg->plain : "";
const char* sid = msg->id ? msg->id : "";
gchar* key = _make_dedup_key(sid, ts, from_jid, body);
if (g_hash_table_contains(seen_keys, key)) {
g_free(key);
contact_skipped++;
continue;
}
g_hash_table_add(seen_keys, key);
ff_parsed_line_t* pl = g_malloc0(sizeof(ff_parsed_line_t));
pl->timestamp_str = g_strdup(ts);
pl->timestamp = g_date_time_ref(msg->timestamp);
pl->type = g_strdup(ff_get_message_type_str(msg->type));
pl->enc = g_strdup(ff_get_message_enc_str(msg->enc));
pl->stanza_id = g_strdup(sid);
pl->archive_id = msg->stanzaid ? g_strdup(msg->stanzaid) : NULL;
pl->replace_id = NULL;
pl->from_jid = g_strdup(from_jid);
pl->from_resource = msg->from_jid ? g_strdup(msg->from_jid->resourcepart) : NULL;
pl->to_jid = msg->to_jid ? g_strdup(msg->to_jid->barejid) : NULL;
pl->to_resource = msg->to_jid ? g_strdup(msg->to_jid->resourcepart) : NULL;
pl->marked_read = msg->marked_read;
pl->message = g_strdup(body);
merged = g_slist_prepend(merged, pl);
contact_exported++;
}
// Sort merged list by timestamp (ISO8601 is lexicographically sortable)
merged = g_slist_sort(merged, _compare_parsed_lines_by_timestamp);
// Cache the total length once — calling g_slist_length() inside the
// write loop is O(n) per call, turning the loop into O(n²) wall time
// (catastrophic at >100k rows).
int merged_total = (int)g_slist_length(merged);
// Write all lines sorted
int written = 0;
for (GSList* l = merged; l; l = l->next) {
ff_parsed_line_t* pl = l->data;
ff_write_line(fp, pl->timestamp_str,
pl->type, pl->enc,
pl->stanza_id, pl->archive_id, pl->replace_id,
pl->from_jid, pl->from_resource,
pl->to_jid, pl->to_resource, pl->marked_read,
pl->message);
written++;
if (written % EXPORT_PROGRESS_INTERVAL == 0) {
cons_show(" ... %s: %d/%d written so far", contact, written, merged_total);
}
}
g_slist_free_full(merged, (GDestroyNotify)ff_parsed_line_free);
fflush(fp);
fsync(fd); // ensure data is on disk before atomic rename
fclose(fp); // also releases flock
// Atomic rename
int result = -1;
if (rename(tmp_path, log_path) != 0) {
log_error("export: rename %s -> %s failed: %s", tmp_path, log_path, strerror(errno));
cons_show_error("Export failed for %s: could not rename temp file (%s)", contact, strerror(errno));
unlink(tmp_path);
} else {
cons_show("Exported %s: %d new, %d skipped (already existed: %d)",
contact, contact_exported, contact_skipped, existing_count);
result = contact_exported;
}
g_hash_table_destroy(seen_keys);
g_slist_free_full(existing, (GDestroyNotify)ff_parsed_line_free);
g_slist_free_full(sqlite_lines, (GDestroyNotify)message_free);
return result;
}
int
log_database_export_to_flatfile(const gchar* const contact_jid)
{
const Jid* myjid = connection_get_jid();
if (!myjid || !myjid->barejid) {
cons_show_error("Export failed: not connected.");
return -1;
}
if (!g_flatfile_account_jid) {
// Flatfile backend not initialized — init it temporarily
// We need g_flatfile_account_jid for path construction
g_flatfile_account_jid = g_strdup(myjid->barejid);
}
// Get the SQLite backend
db_backend_t* sqlite_be = db_backend_sqlite();
if (!sqlite_be) {
cons_show_error("Export failed: SQLite backend not available.");
return -1;
}
// SQLite must be open for db_sqlite_list_contacts/db_sqlite_get_all_chat
// to return rows. When the flatfile backend is active, sqlite is closed —
// queries silently return NULL and the export reports zero changes.
int sqlite_opened = _ensure_sqlite_open();
if (sqlite_opened < 0) {
cons_show_error("Export failed: could not open SQLite database.");
return -1;
}
// Determine contact list
GSList* contacts = NULL;
if (contact_jid) {
contacts = g_slist_append(contacts, g_strdup(contact_jid));
} else {
contacts = db_sqlite_list_contacts();
if (!contacts) {
cons_show("No contacts found in SQLite database.");
_close_temp_sqlite(sqlite_opened);
return 0;
}
cons_show("Exporting %d contact(s) to flat-file format...", g_slist_length(contacts));
}
int total_exported = 0;
for (GSList* c = contacts; c; c = c->next) {
const char* contact = c->data;
int exported = _export_one_contact(contact);
if (exported > 0)
total_exported += exported;
}
g_slist_free_full(contacts, g_free);
_close_temp_sqlite(sqlite_opened);
return total_exported;
}
// =========================================================================
// Import: flatfile -> SQLite (merge)
// =========================================================================
int
log_database_import_from_flatfile(const gchar* const contact_jid)
{
const Jid* myjid = connection_get_jid();
if (!myjid || !myjid->barejid) {
cons_show_error("Import failed: not connected.");
return -1;
}
if (!g_flatfile_account_jid) {
g_flatfile_account_jid = g_strdup(myjid->barejid);
}
db_backend_t* sqlite_be = db_backend_sqlite();
if (!sqlite_be) {
cons_show_error("Import failed: SQLite backend not available.");
return -1;
}
int sqlite_opened = _ensure_sqlite_open();
if (sqlite_opened < 0) {
cons_show_error("Import failed: could not open SQLite database.");
return -1;
}
// Determine contacts
GSList* contacts = NULL;
if (contact_jid) {
contacts = g_slist_append(contacts, g_strdup(contact_jid));
} else {
contacts = _ff_list_contacts();
if (!contacts) {
cons_show("No flat-file history found to import.");
_close_temp_sqlite(sqlite_opened);
return 0;
}
}
int total_imported = 0;
for (GSList* c = contacts; c; c = c->next) {
const char* contact = c->data;
// 1. Read all flatfile lines
GSList* ff_lines = _ff_read_all_lines(contact);
if (!ff_lines) {
log_debug("import: no flatfile messages for %s", contact);
continue;
}
// 2. Build dedup set from SQLite (existing stanza_ids + fallback keys)
GHashTable* seen_keys = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL);
// Read all SQLite messages for this contact via direct SQL query
GSList* existing_msgs = db_sqlite_get_all_chat(contact);
for (GSList* l = existing_msgs; l; l = l->next) {
ProfMessage* msg = l->data;
if (!msg)
continue;
auto_gchar gchar* ts = msg->timestamp ? g_date_time_format_iso8601(msg->timestamp) : g_strdup("");
gchar* key = _make_dedup_key(msg->id, ts, msg->from_jid ? msg->from_jid->barejid : "", msg->plain);
g_hash_table_add(seen_keys, key);
}
g_slist_free_full(existing_msgs, (GDestroyNotify)message_free);
// 3. For each flatfile line not in seen_keys, insert via add_incoming
// Wrap in a transaction for atomicity and performance
int contact_imported = 0;
int contact_skipped = 0;
int total_lines = g_slist_length(ff_lines);
db_sqlite_begin_transaction();
gboolean import_ok = TRUE;
for (GSList* l = ff_lines; l; l = l->next) {
ff_parsed_line_t* pl = l->data;
if (!pl || !pl->timestamp)
continue;
auto_gchar gchar* ts = g_date_time_format_iso8601(pl->timestamp);
gchar* key = _make_dedup_key(pl->stanza_id, ts, pl->from_jid, pl->message);
if (g_hash_table_contains(seen_keys, key)) {
g_free(key);
contact_skipped++;
continue;
}
g_hash_table_add(seen_keys, key);
// Build a ProfMessage and insert via add_incoming
// (add_incoming uses _add_to_db which preserves the original timestamp)
gboolean is_outgoing = (g_strcmp0(pl->from_jid, myjid->barejid) == 0);
ProfMessage* msg = message_init();
msg->id = pl->stanza_id ? g_strdup(pl->stanza_id) : NULL;
msg->stanzaid = pl->archive_id ? g_strdup(pl->archive_id) : NULL;
msg->plain = g_strdup(pl->message ? pl->message : "");
msg->timestamp = g_date_time_ref(pl->timestamp);
msg->type = ff_get_message_type_type(pl->type);
msg->enc = ff_get_message_enc_type(pl->enc);
msg->replace_id = pl->replace_id ? g_strdup(pl->replace_id) : NULL;
msg->is_mam = TRUE; // Suppress dedup check in the backend
if (is_outgoing) {
msg->from_jid = jid_create(myjid->barejid);
if (pl->to_jid) {
msg->to_jid = jid_create_from_bare_and_resource(pl->to_jid, pl->to_resource);
} else {
msg->to_jid = jid_create(contact);
}
} else {
msg->from_jid = jid_create_from_bare_and_resource(pl->from_jid, pl->from_resource);
if (pl->to_jid) {
msg->to_jid = jid_create_from_bare_and_resource(pl->to_jid, pl->to_resource);
} else {
msg->to_jid = jid_create(myjid->barejid);
}
}
sqlite_be->add_incoming(msg);
message_free(msg);
// Verify the row was actually inserted
if (db_sqlite_last_changes() < 1) {
log_error("import: insert failed for %s at %s", contact, ts);
import_ok = FALSE;
break;
}
contact_imported++;
if (contact_imported % EXPORT_PROGRESS_INTERVAL == 0) {
cons_show(" ... %s: %d/%d imported so far", contact, contact_imported, total_lines);
}
}
if (import_ok) {
db_sqlite_end_transaction();
} else {
db_sqlite_rollback_transaction();
cons_show_error("Import of %s failed — transaction rolled back.", contact);
}
cons_show("Imported %s: %d new, %d skipped (already in SQLite)",
contact, contact_imported, contact_skipped);
total_imported += contact_imported;
g_hash_table_destroy(seen_keys);
g_slist_free_full(ff_lines, (GDestroyNotify)ff_parsed_line_free);
// If the per-contact import bailed mid-stream (likely a system-level
// problem: disk full, sqlite locked, etc.) — don't blindly try the
// remaining contacts; they'll almost certainly hit the same error
// and produce a confusing wall of "Import of X failed" messages.
// Stop here so the user can investigate the root cause.
if (!import_ok) {
cons_show_error("Aborting import — system-level failure suspected. Fix the underlying issue and retry.");
break;
}
}
g_slist_free_full(contacts, g_free);
_close_temp_sqlite(sqlite_opened);
return total_imported;
}
#endif /* HAVE_SQLITE */

1019
src/database_flatfile.c Normal file

File diff suppressed because it is too large Load Diff

172
src/database_flatfile.h Normal file
View File

@@ -0,0 +1,172 @@
// SPDX-License-Identifier: GPL-3.0-or-later
//
// This file is part of CProof.
// See LICENSE for the full GPLv3 text and the special OpenSSL linking exception.
/*
* database_flatfile.h
* vim: expandtab:ts=4:sts=4:sw=4
*
* Internal header shared between database_flatfile*.c modules.
* Not part of the public API — do not include from outside the flatfile backend.
*/
#ifndef DATABASE_FLATFILE_H
#define DATABASE_FLATFILE_H
#include <glib.h>
#include <stdio.h>
#include <sys/types.h>
#include "database.h"
#include "xmpp/xmpp.h"
#include "xmpp/message.h"
// --- Constants ---
#define DIR_FLATLOG "flatlog"
#define FLATFILE_FORMAT_VERSION 1
#define FF_VERSION_MARKER "format-version: "
#define FF_STRINGIFY_(x) #x
#define FF_STRINGIFY(x) FF_STRINGIFY_(x)
#define FLATFILE_HEADER "# cproof chat log — UTF-8, LF line endings, " FF_VERSION_MARKER FF_STRINGIFY(FLATFILE_FORMAT_VERSION) "\n"
#define FF_MAX_LINE_LEN (10 * 1024 * 1024) /* 10 MB — reject lines longer than this */
#define FF_MAX_LMC_DEPTH 100 /* max correction chain depth */
// --- Shared global ---
// Account JID stored during init for path construction.
// Defined in database_flatfile.c, used by all flatfile modules.
extern char* g_flatfile_account_jid;
// --- Parsed line structure ---
typedef struct
{
char* timestamp_str;
GDateTime* timestamp;
char* type;
char* enc;
char* stanza_id;
char* archive_id;
char* replace_id;
char* from_jid;
char* from_resource;
char* to_jid;
char* to_resource;
int marked_read; // -1 = unset (NULL in DB), 0 = unread, 1 = read
char* message;
off_t file_offset;
} ff_parsed_line_t;
// --- Sparse index for single-file lookup ---
// Sample every N-th log line for the sparse index.
// 500 lines ≈ one index entry per ~50 KB of log data, so a 100K-message
// contact requires only ~200 entries (~3 KB) for O(log n) time-range lookup.
#define FF_INDEX_STEP 500
// --- Metadata field prefixes (used in _ff_cache_line_ids and ff_parse_line) ---
#define FF_META_PREFIX_ID "id:"
#define FF_META_PREFIX_AID "aid:"
typedef struct
{
off_t byte_offset;
gint64 timestamp_epoch;
} ff_index_entry_t;
typedef struct
{
time_t mtime;
off_t size;
ino_t inode;
} ff_file_stamp_t;
typedef struct
{
char* filepath;
ff_index_entry_t* entries;
size_t n_entries;
size_t cap_entries;
size_t total_lines;
ff_file_stamp_t stamp;
int bom_len;
GHashTable* archive_ids; // set: archive_id -> NULL (MAM dedup, O(1))
GHashTable* stanza_senders; // map: stanza_id -> from_jid (LMC validation, O(1))
} ff_contact_state_t;
// State management
ff_contact_state_t* ff_state_new(const char* filepath);
void ff_state_free(ff_contact_state_t* state);
gboolean ff_state_ensure_fresh(ff_contact_state_t* state);
off_t ff_state_offset_for_time(ff_contact_state_t* state, const char* iso_time);
// --- Type conversion helpers ---
const char* ff_get_message_type_str(prof_msg_type_t type);
prof_msg_type_t ff_get_message_type_type(const char* const type);
const char* ff_get_message_enc_str(prof_enc_t enc);
prof_enc_t ff_get_message_enc_type(const char* const encstr);
// --- Path helpers ---
char* ff_jid_to_dir(const char* jid);
char* ff_get_contact_dir(const char* contact_barejid);
char* ff_get_log_path(const char* contact_barejid);
gboolean ff_ensure_dir(const char* path);
// --- Escape / unescape ---
char* ff_escape_message(const char* text);
char* ff_unescape_message(const char* text);
char* ff_escape_meta_value(const char* val);
char* ff_unescape_meta_value(const char* val);
// --- I/O ---
// Skip UTF-8 BOM if present; returns 3 if skipped, 0 otherwise.
int ff_skip_bom(FILE* fp);
// Scan leading '#' comment lines for the format-version marker. Returns
// the version found, 0 if no marker is present, or -1 on read error.
// File position is rewound to the start of the comment block on entry.
int ff_read_format_version(FILE* fp);
char* ff_readline(FILE* fp, gboolean* truncated);
void ff_write_line(FILE* fp, const char* timestamp, const char* type, const char* enc,
const char* stanza_id, const char* archive_id, const char* replace_id,
const char* from_jid, const char* from_resource,
const char* to_jid, const char* to_resource, int marked_read,
const char* message_text);
// --- Parser helpers ---
const char* ff_find_unescaped_char(const char* str, char ch);
char** ff_split_meta(const char* meta);
const char* ff_find_unescaped_colonspace(const char* str);
char* ff_unescape_sender_resource(const char* res);
// --- Parser ---
void ff_parsed_line_free(ff_parsed_line_t* pl);
ff_parsed_line_t* ff_parse_line(const char* line);
ProfMessage* ff_parsed_to_profmessage(ff_parsed_line_t* pl);
// --- Integrity verification (database_flatfile_verify.c) ---
// Run integrity checks against one contact's history.log (or every contact
// when contact_barejid == NULL).
//
// Returns a freshly allocated GSList<integrity_issue_t*> reporting:
// - File-level: missing log, BOM, CRLF, empty, wrong permissions
// - Line-level: invalid UTF-8, control characters, unparsable lines,
// timestamps out of order, duplicate stanza-id /
// archive-id (tracked separately)
// - Cross-line: broken `corrects:` LMC references
//
// Callers must free with g_slist_free_full(issues, integrity_issue_free).
GSList* ff_verify_integrity(const gchar* const contact_barejid);
#endif

View File

@@ -0,0 +1,825 @@
// SPDX-License-Identifier: GPL-3.0-or-later
//
// This file is part of CProof.
// See LICENSE for the full GPLv3 text and the special OpenSSL linking exception.
/*
* database_flatfile_parser.c
* vim: expandtab:ts=4:sts=4:sw=4
*
* Flat-file backend: type helpers, path helpers, escape/unescape,
* line I/O, and the tolerant log-line parser.
*/
#include "config.h"
#include <sys/stat.h>
#include <glib.h>
#include <glib/gstdio.h>
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include "log.h"
#include "common.h"
#include "config/files.h"
#include "database_flatfile.h"
// =========================================================================
// Type conversion helpers
// =========================================================================
const char*
ff_get_message_type_str(prof_msg_type_t type)
{
switch (type) {
case PROF_MSG_TYPE_CHAT:
return "chat";
case PROF_MSG_TYPE_MUC:
return "muc";
case PROF_MSG_TYPE_MUCPM:
return "mucpm";
case PROF_MSG_TYPE_UNINITIALIZED:
return "chat";
}
return "chat";
}
prof_msg_type_t
ff_get_message_type_type(const char* const type)
{
if (g_strcmp0(type, "chat") == 0) {
return PROF_MSG_TYPE_CHAT;
} else if (g_strcmp0(type, "muc") == 0) {
return PROF_MSG_TYPE_MUC;
} else if (g_strcmp0(type, "mucpm") == 0) {
return PROF_MSG_TYPE_MUCPM;
}
return PROF_MSG_TYPE_CHAT;
}
const char*
ff_get_message_enc_str(prof_enc_t enc)
{
switch (enc) {
case PROF_MSG_ENC_OX:
return "ox";
case PROF_MSG_ENC_PGP:
return "pgp";
case PROF_MSG_ENC_OTR:
return "otr";
case PROF_MSG_ENC_OMEMO:
return "omemo";
case PROF_MSG_ENC_NONE:
return "none";
}
return "none";
}
prof_enc_t
ff_get_message_enc_type(const char* const encstr)
{
if (g_strcmp0(encstr, "ox") == 0) {
return PROF_MSG_ENC_OX;
} else if (g_strcmp0(encstr, "pgp") == 0) {
return PROF_MSG_ENC_PGP;
} else if (g_strcmp0(encstr, "otr") == 0) {
return PROF_MSG_ENC_OTR;
} else if (g_strcmp0(encstr, "omemo") == 0) {
return PROF_MSG_ENC_OMEMO;
}
return PROF_MSG_ENC_NONE;
}
// =========================================================================
// Path helpers
// =========================================================================
// Sanitise a JID for use as a directory name.
// 1. Replace '@' with '_at_'
// 2. Reject / strip path-separator and traversal characters: '/', '\\', '..'
// This prevents a malicious federated JID like "../../../tmp/pwned" from
// escaping the log directory tree.
char*
ff_jid_to_dir(const char* jid)
{
if (!jid || jid[0] == '\0')
return NULL;
// Replace '@' with '_at_' (str_replace returns a freshly allocated string)
char* result = str_replace(jid, "@", "_at_");
if (!result)
return NULL;
// Replace path-separator characters in place
for (char* p = result; *p; p++) {
if (*p == '/' || *p == '\\')
*p = '_';
}
// Collapse any remaining ".." sequences to "__" (belt-and-suspenders)
char* dotdot;
while ((dotdot = strstr(result, "..")) != NULL) {
dotdot[0] = '_';
dotdot[1] = '_';
}
if (result[0] == '\0') {
free(result);
return NULL;
}
// Callers free via g_free, str_replace allocates via malloc — handover.
char* gresult = g_strdup(result);
free(result);
return gresult;
}
// Get the base directory for a contact's logs:
// ~/.local/share/profanity/flatlog/{my_jid_dir}/{contact_jid_dir}/
char*
ff_get_contact_dir(const char* contact_barejid)
{
if (!g_flatfile_account_jid || !contact_barejid)
return NULL;
auto_gchar gchar* data_path = files_get_data_path(DIR_FLATLOG);
auto_gchar gchar* my_dir = ff_jid_to_dir(g_flatfile_account_jid);
auto_gchar gchar* contact_dir = ff_jid_to_dir(contact_barejid);
char* result = g_strdup_printf("%s/%s/%s", data_path, my_dir, contact_dir);
return result;
}
// Get the single log file path for a contact: {contact_dir}/history.log
char*
ff_get_log_path(const char* contact_barejid)
{
auto_gchar gchar* contact_dir = ff_get_contact_dir(contact_barejid);
if (!contact_dir)
return NULL;
char* result = g_strdup_printf("%s/history.log", contact_dir);
return result;
}
// Ensure the directory exists, create if needed.
// Refuses to follow symlinks at the final component.
gboolean
ff_ensure_dir(const char* path)
{
if (g_file_test(path, G_FILE_TEST_IS_SYMLINK)) {
log_error("flatfile: directory path is a symlink, refusing: %s", path);
return FALSE;
}
if (g_file_test(path, G_FILE_TEST_IS_DIR))
return TRUE;
if (g_mkdir_with_parents(path, S_IRWXU) != 0) {
log_error("flatfile: Could not create directory: %s (errno=%d)", path, errno);
return FALSE;
}
return TRUE;
}
// =========================================================================
// Escape / unescape helpers
// =========================================================================
//
// Message text and metadata values from remote peers can contain arbitrary
// characters including newlines, pipes and brackets. Without escaping, a
// crafted message could inject fake log lines (log injection / format
// injection). We escape on write and unescape on read.
// Escape message body: \ -> \\, \n -> \n literal, \r -> \r literal
char*
ff_escape_message(const char* text)
{
if (!text)
return g_strdup("");
GString* out = g_string_sized_new(strlen(text));
for (const char* p = text; *p; p++) {
switch (*p) {
case '\\':
g_string_append(out, "\\\\");
break;
case '\n':
g_string_append(out, "\\n");
break;
case '\r':
g_string_append(out, "\\r");
break;
default:
g_string_append_c(out, *p);
break;
}
}
return g_string_free(out, FALSE);
}
// Unescape message body: \\ -> \, \n -> newline, \r -> CR
char*
ff_unescape_message(const char* text)
{
if (!text)
return g_strdup("");
GString* out = g_string_sized_new(strlen(text));
for (const char* p = text; *p; p++) {
if (*p != '\\' || !*(p + 1)) {
g_string_append_c(out, *p);
continue;
}
p++;
switch (*p) {
case '\\':
g_string_append_c(out, '\\');
break;
case 'n':
g_string_append_c(out, '\n');
break;
case 'r':
g_string_append_c(out, '\r');
break;
default:
g_string_append_c(out, '\\');
g_string_append_c(out, *p);
break;
}
}
return g_string_free(out, FALSE);
}
// Escape metadata value (stanza_id, archive_id, replace_id):
// these come from remote servers and may contain |, ], \, newlines.
char*
ff_escape_meta_value(const char* val)
{
if (!val || strlen(val) == 0)
return NULL;
GString* out = g_string_sized_new(strlen(val));
for (const char* p = val; *p; p++) {
switch (*p) {
case '|':
g_string_append(out, "\\|");
break;
case ']':
g_string_append(out, "\\]");
break;
case '\\':
g_string_append(out, "\\\\");
break;
case '\n':
g_string_append(out, "\\n");
break;
case '\r':
g_string_append(out, "\\r");
break;
default:
g_string_append_c(out, *p);
break;
}
}
return g_string_free(out, FALSE);
}
// Unescape metadata value
char*
ff_unescape_meta_value(const char* val)
{
if (!val)
return NULL;
GString* out = g_string_sized_new(strlen(val));
for (const char* p = val; *p; p++) {
if (*p != '\\' || !*(p + 1)) {
g_string_append_c(out, *p);
continue;
}
p++;
switch (*p) {
case '|':
g_string_append_c(out, '|');
break;
case ']':
g_string_append_c(out, ']');
break;
case '\\':
g_string_append_c(out, '\\');
break;
case 'n':
g_string_append_c(out, '\n');
break;
case 'r':
g_string_append_c(out, '\r');
break;
default:
g_string_append_c(out, '\\');
g_string_append_c(out, *p);
break;
}
}
return g_string_free(out, FALSE);
}
// =========================================================================
// BOM helper
// =========================================================================
//
// Skip UTF-8 BOM (EF BB BF) at the beginning of a file.
// Returns 3 if BOM was present, 0 otherwise. File position is set past
// the BOM (or rewound to the start).
int
ff_skip_bom(FILE* fp)
{
int c1 = fgetc(fp);
int c2 = fgetc(fp);
int c3 = fgetc(fp);
if (c1 == 0xEF && c2 == 0xBB && c3 == 0xBF)
return 3;
fseek(fp, 0, SEEK_SET);
return 0;
}
// Format-version helper
// =========================================================================
//
// Scan leading '#' comment lines for the `format-version: <N>` marker
// (anywhere within the comment line). Stops at the first non-comment
// line or after FF_VERSION_SCAN_MAX comments. Restores fp to its
// position at entry, so callers can re-parse the header normally
// afterwards.
#define FF_VERSION_SCAN_MAX 16
int
ff_read_format_version(FILE* fp)
{
long start = ftell(fp);
if (start < 0)
return -1;
int version = 0;
size_t marker_len = strlen(FF_VERSION_MARKER);
for (int i = 0; i < FF_VERSION_SCAN_MAX; i++) {
gboolean truncated = FALSE;
char* line = ff_readline(fp, &truncated);
if (!line)
break;
if (line[0] != '#') {
free(line);
break;
}
char* found = strstr(line, FF_VERSION_MARKER);
if (found) {
char* num_start = found + marker_len;
char* endptr = NULL;
long v = strtol(num_start, &endptr, 10);
if (endptr != num_start && v > 0 && v <= INT_MAX)
version = (int)v;
free(line);
break;
}
free(line);
}
fseek(fp, start, SEEK_SET);
return version;
}
// Readline helper
// =========================================================================
//
// POSIX getline() for dynamic-length lines. Returns line without trailing
// newline, or NULL on EOF. Sets *truncated=TRUE if the line had no trailing
// newline at EOF (partial write detection).
char*
ff_readline(FILE* fp, gboolean* truncated)
{
char* line = NULL;
size_t cap = 0;
ssize_t nread = getline(&line, &cap, fp);
if (nread == -1) {
free(line);
return NULL;
}
// Guard against pathological lines that could exhaust memory.
// getline() already allocated, so we free and skip if too long.
if (nread > FF_MAX_LINE_LEN) {
log_error("flatfile: line too long (%zd bytes), skipping", nread);
// Check newline termination before freeing
gboolean had_newline = (nread > 0 && line[nread - 1] == '\n');
free(line);
// Skip to next newline if the overlength line wasn't newline-terminated
if (!had_newline) {
int ch;
while ((ch = fgetc(fp)) != EOF && ch != '\n') {}
}
if (truncated)
*truncated = FALSE;
// Return empty string so caller's loop continues (parse will reject it).
// strdup() so callers can always free() consistently.
return strdup("");
}
if (truncated)
*truncated = FALSE;
if (nread > 0 && line[nread - 1] == '\n') {
line[--nread] = '\0';
} else if (feof(fp)) {
// Line without trailing newline at EOF — likely a partial write
if (truncated)
*truncated = TRUE;
}
return line;
}
// =========================================================================
// Line writer
// =========================================================================
//
// Format: {ISO8601} [{type}|{enc}|id:{stanza_id}|aid:{archive_id}|corrects:{replace_id}] {from_jid}/{resource}: {message}
//
// Message text is escaped: \\ -> \\\\, newline -> \\n, CR -> \\r
// Metadata values are escaped: |, ], \\, newline, CR
// Lines starting with '#' are comments.
// Empty lines are skipped.
void
ff_write_line(FILE* fp, const char* timestamp, const char* type, const char* enc,
const char* stanza_id, const char* archive_id, const char* replace_id,
const char* from_jid, const char* from_resource,
const char* to_jid, const char* to_resource, int marked_read,
const char* message_text)
{
// Escape metadata values from remote peers
auto_gchar gchar* safe_sid = ff_escape_meta_value(stanza_id);
auto_gchar gchar* safe_aid = ff_escape_meta_value(archive_id);
auto_gchar gchar* safe_rid = ff_escape_meta_value(replace_id);
// Build metadata section: [type|enc|id:...|aid:...|corrects:...|to:...|to_res:...|read:...]
GString* meta = g_string_new("[");
g_string_append(meta, type ? type : "chat");
g_string_append_c(meta, '|');
g_string_append(meta, enc ? enc : "none");
if (safe_sid) {
g_string_append_printf(meta, "|id:%s", safe_sid);
}
if (safe_aid) {
g_string_append_printf(meta, "|aid:%s", safe_aid);
}
if (safe_rid) {
g_string_append_printf(meta, "|corrects:%s", safe_rid);
}
if (to_jid && strlen(to_jid) > 0
&& g_strcmp0(to_jid, "(null)") != 0) {
auto_gchar gchar* safe_to = ff_escape_meta_value(to_jid);
g_string_append_printf(meta, "|to:%s", safe_to);
}
if (to_resource && strlen(to_resource) > 0
&& g_strcmp0(to_resource, "(null)") != 0) {
auto_gchar gchar* safe_tores = ff_escape_meta_value(to_resource);
g_string_append_printf(meta, "|to_res:%s", safe_tores);
}
if (marked_read >= 0) {
g_string_append_printf(meta, "|read:%d", marked_read ? 1 : 0);
}
g_string_append_c(meta, ']');
// Build sender — escape ": " in the resource part to prevent
// the parser from splitting at the wrong point.
const gboolean from_jid_usable = from_jid && *from_jid
&& g_strcmp0(from_jid, "(null)") != 0;
GString* sender = g_string_new(from_jid_usable ? from_jid : "unknown");
if (from_resource && strlen(from_resource) > 0
&& g_strcmp0(from_resource, "(null)") != 0) {
// Escape backslash and colon-space in resource
GString* safe_res = g_string_sized_new(strlen(from_resource));
for (const char* p = from_resource; *p; p++) {
if (*p == '\\') {
g_string_append(safe_res, "\\\\");
} else if (*p == ':' && *(p + 1) == ' ') {
g_string_append(safe_res, "\\: ");
p++; // skip the space too
} else {
g_string_append_c(safe_res, *p);
}
}
g_string_append_printf(sender, "/%s", safe_res->str);
g_string_free(safe_res, TRUE);
}
// Escape message body to prevent log injection
auto_char char* safe_msg = ff_escape_message(message_text);
int ret = fprintf(fp, "%s %s %s: %s\n",
timestamp, meta->str, sender->str, safe_msg);
if (ret < 0) {
log_error("flatfile: fprintf failed (errno=%d)", errno);
}
g_string_free(meta, TRUE);
g_string_free(sender, TRUE);
}
// =========================================================================
// Parser helpers
// =========================================================================
// Find the first occurrence of 'ch' that is not preceded by an unescaped backslash.
const char*
ff_find_unescaped_char(const char* str, char ch)
{
if (!str)
return NULL;
for (const char* p = str; *p; p++) {
if (*p == '\\' && *(p + 1)) {
p++; // skip escaped character
continue;
}
if (*p == ch)
return p;
}
return NULL;
}
// Split metadata content on unescaped '|'. Returns a NULL-terminated array.
// Caller must g_strfreev() the result.
char**
ff_split_meta(const char* meta)
{
GPtrArray* arr = g_ptr_array_new();
const char* start = meta;
for (const char* p = meta;; p++) {
if (*p == '\\' && *(p + 1)) {
p++; // skip escaped char
continue;
}
if (*p == '|' || *p == '\0') {
g_ptr_array_add(arr, g_strndup(start, p - start));
if (*p == '\0')
break;
start = p + 1;
}
}
g_ptr_array_add(arr, NULL);
return (char**)g_ptr_array_free(arr, FALSE);
}
// Find first unescaped ": " (colon-space) in a string.
const char*
ff_find_unescaped_colonspace(const char* str)
{
if (!str)
return NULL;
for (const char* p = str; *p; p++) {
if (*p == '\\' && *(p + 1)) {
p++; // skip escaped character
continue;
}
if (*p == ':' && *(p + 1) == ' ') {
return p;
}
}
return NULL;
}
// Unescape a sender resource: "\\" -> '\', "\: " -> ": "
char*
ff_unescape_sender_resource(const char* res)
{
if (!res)
return NULL;
GString* out = g_string_sized_new(strlen(res));
for (const char* p = res; *p; p++) {
if (*p != '\\' || !*(p + 1)) {
g_string_append_c(out, *p);
continue;
}
p++;
if (*p == '\\') {
g_string_append_c(out, '\\');
} else if (*p == ':' && *(p + 1) == ' ') {
g_string_append(out, ": ");
p++; // skip the space
} else {
// Unknown escape — preserve literally
g_string_append_c(out, '\\');
g_string_append_c(out, *p);
}
}
return g_string_free(out, FALSE);
}
// =========================================================================
// Line parser
// =========================================================================
void
ff_parsed_line_free(ff_parsed_line_t* pl)
{
if (!pl)
return;
g_free(pl->timestamp_str);
if (pl->timestamp)
g_date_time_unref(pl->timestamp);
g_free(pl->type);
g_free(pl->enc);
g_free(pl->stanza_id);
g_free(pl->archive_id);
g_free(pl->replace_id);
g_free(pl->from_jid);
g_free(pl->from_resource);
g_free(pl->to_jid);
g_free(pl->to_resource);
g_free(pl->message);
g_free(pl);
}
// Populate the parsed-line metadata fields from the pipe-split parts[]
// (the contents of a "[...]" section). Index 0 is type, index 1 is enc;
// remaining parts are key:value pairs (id:, aid:, corrects:, to:, to_res:, read:).
static void
_ff_parse_meta_parts(char** parts, ff_parsed_line_t* out)
{
for (int i = 0; parts[i]; i++) {
if (i == 0) {
out->type = g_strdup(parts[i]);
} else if (i == 1) {
out->enc = g_strdup(parts[i]);
} else if (g_str_has_prefix(parts[i], FF_META_PREFIX_ID)) {
out->stanza_id = ff_unescape_meta_value(parts[i] + strlen(FF_META_PREFIX_ID));
} else if (g_str_has_prefix(parts[i], FF_META_PREFIX_AID)) {
out->archive_id = ff_unescape_meta_value(parts[i] + strlen(FF_META_PREFIX_AID));
} else if (g_str_has_prefix(parts[i], "corrects:")) {
out->replace_id = ff_unescape_meta_value(parts[i] + strlen("corrects:"));
} else if (g_str_has_prefix(parts[i], "to:")) {
out->to_jid = ff_unescape_meta_value(parts[i] + strlen("to:"));
} else if (g_str_has_prefix(parts[i], "to_res:")) {
out->to_resource = ff_unescape_meta_value(parts[i] + strlen("to_res:"));
} else if (g_str_has_prefix(parts[i], "read:")) {
out->marked_read = atoi(parts[i] + strlen("read:")) ? 1 : 0;
}
}
}
// Parse a single line. Returns NULL on parse failure.
// Line format: {timestamp} [{metadata}] {sender}: {message}
ff_parsed_line_t*
ff_parse_line(const char* line)
{
if (!line || line[0] == '\0' || line[0] == '#') {
return NULL;
}
// Strip trailing \r if present (CRLF handling)
char* work = g_strdup(line);
gsize len = strlen(work);
if (len > 0 && work[len - 1] == '\r') {
work[len - 1] = '\0';
len--;
}
if (len == 0) {
g_free(work);
return NULL;
}
// UTF-8 validation
const gchar* end;
if (!g_utf8_validate(work, -1, &end)) {
log_warning("flatfile: invalid UTF-8 at byte offset %ld", (long)(end - work));
// Attempt Latin-1 fallback
gsize br, bw;
GError* err = NULL;
char* converted = g_convert(work, -1, "UTF-8", "ISO-8859-1", &br, &bw, &err);
if (converted) {
g_free(work);
work = converted;
} else {
if (err)
g_error_free(err);
g_free(work);
return NULL;
}
}
ff_parsed_line_t* result = g_malloc0(sizeof(ff_parsed_line_t));
result->file_offset = -1;
result->marked_read = -1; // unset by default
// Parse timestamp — everything up to first space followed by '['
char* bracket_start = strchr(work, '[');
char* first_space = strchr(work, ' ');
if (bracket_start && first_space && first_space < bracket_start) {
// Standard format with metadata: {timestamp} [{meta}] {sender}: {msg}
result->timestamp_str = g_strndup(work, first_space - work);
// Parse metadata section [...]
const char* bracket_end = ff_find_unescaped_char(bracket_start + 1, ']');
if (!bracket_end) {
// No closing bracket — malformed metadata
ff_parsed_line_free(result);
g_free(work);
return NULL;
}
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);
g_strfreev(parts);
}
// Parse sender: message after '] '
const char* after_meta = bracket_end + 1;
if (*after_meta == ' ')
after_meta++;
// 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, 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, slash - raw_sender);
result->from_resource = ff_unescape_sender_resource(slash + 1);
} else {
result->from_jid = g_strdup(raw_sender);
}
result->message = ff_unescape_message(colon + 2);
} else {
// No ': ' found, treat entire rest as message with unknown sender
result->from_jid = g_strdup("unknown");
result->message = ff_unescape_message(after_meta);
}
} else if (first_space) {
// Legacy/simple format without metadata: {timestamp} - {sender}: {msg}
result->timestamp_str = g_strndup(work, first_space - work);
result->type = g_strdup("chat");
result->enc = g_strdup("none");
char* rest = first_space + 1;
// Skip " - " if present (chatlog.c format)
if (g_str_has_prefix(rest, "- ")) {
rest += 2;
}
char* colon = strstr(rest, ": ");
if (colon) {
char* sender = g_strndup(rest, colon - rest);
char* slash = strchr(sender, '/');
if (slash) {
result->from_jid = g_strndup(sender, slash - sender);
result->from_resource = g_strdup(slash + 1);
} else {
result->from_jid = g_strdup(sender);
}
g_free(sender);
result->message = ff_unescape_message(colon + 2);
} else {
result->from_jid = g_strdup("unknown");
result->message = ff_unescape_message(rest);
}
} else {
// No space at all — can't parse
ff_parsed_line_free(result);
g_free(work);
return NULL;
}
// Parse timestamp
result->timestamp = g_date_time_new_from_iso8601(result->timestamp_str, NULL);
if (!result->timestamp) {
log_warning("flatfile: unparsable timestamp: %s", result->timestamp_str);
ff_parsed_line_free(result);
g_free(work);
return NULL;
}
// Default type/enc if missing
if (!result->type)
result->type = g_strdup("chat");
if (!result->enc)
result->enc = g_strdup("none");
g_free(work);
return result;
}
// Convert parsed line to ProfMessage
ProfMessage*
ff_parsed_to_profmessage(ff_parsed_line_t* pl)
{
ProfMessage* msg = message_init();
msg->id = pl->stanza_id ? g_strdup(pl->stanza_id) : NULL;
msg->from_jid = jid_create_from_bare_and_resource(pl->from_jid, pl->from_resource);
if (pl->to_jid) {
msg->to_jid = jid_create_from_bare_and_resource(pl->to_jid, pl->to_resource);
}
msg->plain = g_strdup(pl->message ? pl->message : "");
msg->timestamp = g_date_time_ref(pl->timestamp);
msg->type = ff_get_message_type_type(pl->type);
msg->enc = ff_get_message_enc_type(pl->enc);
return msg;
}

View File

@@ -0,0 +1,370 @@
// SPDX-License-Identifier: GPL-3.0-or-later
//
// This file is part of CProof.
// See LICENSE for the full GPLv3 text and the special OpenSSL linking exception.
/*
* database_flatfile_verify.c
* vim: expandtab:ts=4:sts=4:sw=4
*
* Flat-file backend: integrity verification (/history verify).
*
* High-level flow (see ff_verify_integrity):
* 1. Resolve target contact directories — either a single contact (when
* contact_barejid != NULL) or every contact directory under
* flatlog/<account>/.
* 2. For each contact's history.log:
* - Check file permissions (warn if not 0600).
* - First pass: line-by-line UTF-8 / control-char / parser /
* duplicate-id / timestamp-ordering checks. Records every
* stanza-id and archive-id seen so the second pass can resolve
* LMC references. stanza-id and archive-id are tracked in
* *separate* hash tables — duplicate detection is reported
* per id-kind to avoid spurious cross-kind warnings.
* - Second pass: re-read the file and verify each `corrects:` link
* points at a stanza-id that was seen in pass 1.
* 3. Aggregate every issue into a GSList<integrity_issue_t> and return
* it to the caller, ready for /history verify rendering.
*
* The file is split into small helpers so each pass can be understood in
* isolation; ff_verify_integrity itself is just orchestration.
*/
#include "config.h"
#include <sys/stat.h>
#include <glib.h>
#include <glib/gstdio.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "log.h"
#include "config/files.h"
#include "database_flatfile.h"
// =========================================================================
// Issue construction
// =========================================================================
static integrity_issue_t*
_issue_new(integrity_level_t level, const char* file, int line, char* message)
{
integrity_issue_t* issue = g_new0(integrity_issue_t, 1);
issue->level = level;
issue->file = g_strdup(file ? file : "");
issue->line = line;
issue->message = message; // takes ownership of the (g_strdup'd) message
return issue;
}
// =========================================================================
// Contact discovery
// =========================================================================
// Build the list of contact directories to verify. If contact_barejid is
// supplied, the list is either a singleton or empty (and an INFO issue is
// appended explaining no logs exist for that contact). Otherwise, every
// subdirectory of flatlog/<account>/ is enumerated.
static GSList*
_collect_contact_dirs(const gchar* const contact_barejid, GSList** issues)
{
GSList* contact_dirs = NULL;
if (contact_barejid) {
auto_gchar gchar* cdir = ff_get_contact_dir(contact_barejid);
if (cdir && g_file_test(cdir, G_FILE_TEST_IS_DIR)) {
contact_dirs = g_slist_prepend(contact_dirs, g_strdup(cdir));
} else {
*issues = g_slist_prepend(*issues,
_issue_new(INTEGRITY_INFO, contact_barejid, 0,
g_strdup("No log files found for this contact")));
}
return contact_dirs;
}
auto_gchar gchar* data_path = files_get_data_path(DIR_FLATLOG);
auto_gchar gchar* my_dir = ff_jid_to_dir(g_flatfile_account_jid);
auto_gchar gchar* base_dir = g_strdup_printf("%s/%s", data_path, my_dir);
GDir* dir = g_dir_open(base_dir, 0, NULL);
if (!dir)
return NULL;
const gchar* dname;
while ((dname = g_dir_read_name(dir)) != NULL) {
char* full = g_strdup_printf("%s/%s", base_dir, dname);
if (g_file_test(full, G_FILE_TEST_IS_DIR)) {
contact_dirs = g_slist_prepend(contact_dirs, full);
} else {
g_free(full);
}
}
g_dir_close(dir);
return contact_dirs;
}
// =========================================================================
// Permission check
// =========================================================================
static void
_check_permissions(const char* filepath, const char* basename, GSList** issues)
{
struct stat st;
if (g_stat(filepath, &st) != 0)
return;
if ((st.st_mode & 0777) == (S_IRUSR | S_IWUSR))
return;
*issues = g_slist_prepend(*issues,
_issue_new(INTEGRITY_WARNING, basename, 0,
g_strdup_printf("File permissions are %o, expected 600 (sensitive data)",
st.st_mode & 0777)));
}
// =========================================================================
// First pass: per-line content checks + id collection
// =========================================================================
//
// For every non-empty, non-comment line:
// - validate UTF-8
// - flag control characters
// - try to parse via ff_parse_line(); if that fails, record the lineno
// - check timestamp monotonicity
// - flag duplicate stanza-id and duplicate archive-id (separate tables)
// - record every stanza-id encountered for LMC pass 2
//
// Also detects: BOM (informational), CRLF line endings (warning), empty
// file (informational).
static void
_first_pass(FILE* fp, const char* basename,
GHashTable* seen_stanza_ids, GHashTable* seen_archive_ids,
GHashTable* all_stanza_ids, GSList** issues)
{
if (ff_skip_bom(fp)) {
*issues = g_slist_prepend(*issues,
_issue_new(INTEGRITY_INFO, basename, 0,
g_strdup("File has UTF-8 BOM — harmless but unnecessary")));
}
char* buf = NULL;
int lineno = 0;
GDateTime* prev_ts = NULL;
gboolean has_crlf = FALSE;
gboolean is_empty = TRUE;
while ((buf = ff_readline(fp, NULL)) != NULL) {
lineno++;
gsize len = strlen(buf);
if (len > 0 && buf[len - 1] == '\r') {
has_crlf = TRUE;
buf[--len] = '\0';
}
if (len == 0 || buf[0] == '#') {
free(buf);
continue;
}
is_empty = FALSE;
const gchar* end;
if (!g_utf8_validate(buf, -1, &end)) {
*issues = g_slist_prepend(*issues,
_issue_new(INTEGRITY_ERROR, basename, lineno,
g_strdup_printf("Invalid UTF-8 at byte offset %ld", (long)(end - buf))));
free(buf);
continue;
}
for (gsize i = 0; i < len; i++) {
unsigned char ch = (unsigned char)buf[i];
if (ch < 0x20 && ch != '\t') {
*issues = g_slist_prepend(*issues,
_issue_new(INTEGRITY_WARNING, basename, lineno,
g_strdup_printf("Contains control character 0x%02x", ch)));
break;
}
}
ff_parsed_line_t* pl = ff_parse_line(buf);
if (!pl) {
*issues = g_slist_prepend(*issues,
_issue_new(INTEGRITY_ERROR, basename, lineno,
g_strdup("Unparsable line")));
free(buf);
continue;
}
free(buf);
if (prev_ts && g_date_time_compare(pl->timestamp, prev_ts) < 0) {
auto_gchar gchar* ts_cur = g_date_time_format_iso8601(pl->timestamp);
auto_gchar gchar* ts_prev = g_date_time_format_iso8601(prev_ts);
*issues = g_slist_prepend(*issues,
_issue_new(INTEGRITY_WARNING, basename, lineno,
g_strdup_printf("Timestamp out of order (%s after %s)", ts_cur, ts_prev)));
}
if (prev_ts)
g_date_time_unref(prev_ts);
prev_ts = g_date_time_ref(pl->timestamp);
if (pl->stanza_id && pl->stanza_id[0] != '\0') {
if (g_hash_table_contains(seen_stanza_ids, pl->stanza_id)) {
// Older clients (Pidgin, Adium, even early Profanity)
// sometimes reuse client-generated stanza-ids across
// distinct messages, so a collision is not a real
// integrity defect — log it for diagnostics but don't
// surface it through /history verify.
log_debug("flatfile verify: duplicate stanza-id \"%s\" at %s:%d",
pl->stanza_id, basename, lineno);
} else {
g_hash_table_insert(seen_stanza_ids, g_strdup(pl->stanza_id), GINT_TO_POINTER(lineno));
}
g_hash_table_insert(all_stanza_ids, g_strdup(pl->stanza_id), GINT_TO_POINTER(lineno));
}
if (pl->archive_id && pl->archive_id[0] != '\0') {
if (g_hash_table_contains(seen_archive_ids, pl->archive_id)) {
*issues = g_slist_prepend(*issues,
_issue_new(INTEGRITY_WARNING, basename, lineno,
g_strdup_printf("Duplicate archive-id \"%s\"", pl->archive_id)));
} else {
g_hash_table_insert(seen_archive_ids, g_strdup(pl->archive_id), GINT_TO_POINTER(lineno));
}
}
ff_parsed_line_free(pl);
}
if (prev_ts)
g_date_time_unref(prev_ts);
if (has_crlf) {
*issues = g_slist_prepend(*issues,
_issue_new(INTEGRITY_WARNING, basename, 0,
g_strdup("File uses Windows line endings (CRLF) — consider converting to LF")));
}
if (is_empty) {
*issues = g_slist_prepend(*issues,
_issue_new(INTEGRITY_INFO, basename, 0,
g_strdup("File is empty (no message lines)")));
}
}
// =========================================================================
// Second pass: LMC reference resolution
// =========================================================================
//
// Walks the file again, and for every line that carries a `corrects:` link,
// checks that the referenced stanza-id was seen in pass 1. Broken references
// surface as INTEGRITY_ERROR.
static void
_lmc_pass(FILE* fp, const char* basename, GHashTable* all_stanza_ids, GSList** issues)
{
ff_skip_bom(fp);
char* buf = NULL;
int lineno = 0;
while ((buf = ff_readline(fp, NULL)) != NULL) {
lineno++;
gsize len = strlen(buf);
if (len > 0 && buf[len - 1] == '\r')
buf[--len] = '\0';
if (len == 0 || buf[0] == '#') {
free(buf);
continue;
}
ff_parsed_line_t* pl = ff_parse_line(buf);
free(buf);
if (!pl)
continue;
if (pl->replace_id && pl->replace_id[0] != '\0'
&& !g_hash_table_contains(all_stanza_ids, pl->replace_id)) {
*issues = g_slist_prepend(*issues,
_issue_new(INTEGRITY_ERROR, basename, lineno,
g_strdup_printf("Broken correction reference: corrects:%s not found",
pl->replace_id)));
}
ff_parsed_line_free(pl);
}
}
// =========================================================================
// Per-contact verification
// =========================================================================
static void
_verify_contact_dir(const char* cdir_path, GSList** issues)
{
auto_gchar gchar* filepath = g_strdup_printf("%s/history.log", cdir_path);
if (!g_file_test(filepath, G_FILE_TEST_EXISTS)) {
log_debug("flatfile verify: skipping %s (no history.log)", cdir_path);
return;
}
// Reconstruct the contact JID from the directory name so issue reports
// identify which contact the problem belongs to (every contact has its
// own history.log, so the bare filename is ambiguous).
auto_gchar gchar* dir_name = g_path_get_basename(cdir_path);
auto_gcharv gchar** parts = g_strsplit(dir_name, "_at_", -1);
auto_gchar gchar* contact_jid = g_strjoinv("@", parts);
auto_gchar gchar* basename = g_strdup_printf("%s/history.log", contact_jid);
_check_permissions(filepath, basename, issues);
FILE* fp = fopen(filepath, "r");
if (!fp) {
log_warning("flatfile verify: cannot open %s for reading", filepath);
return;
}
// Separate tables by id-kind: a stanza-id and an archive-id can legitimately
// share the same string without that being a duplicate.
GHashTable* seen_stanza_ids = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL);
GHashTable* seen_archive_ids = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL);
GHashTable* all_stanza_ids = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL);
_first_pass(fp, basename, seen_stanza_ids, seen_archive_ids, all_stanza_ids, issues);
fclose(fp);
// Second pass: LMC references
fp = fopen(filepath, "r");
if (fp) {
_lmc_pass(fp, basename, all_stanza_ids, issues);
fclose(fp);
}
g_hash_table_destroy(seen_stanza_ids);
g_hash_table_destroy(seen_archive_ids);
g_hash_table_destroy(all_stanza_ids);
}
// =========================================================================
// Public entry point
// =========================================================================
GSList*
ff_verify_integrity(const gchar* const contact_barejid)
{
GSList* issues = NULL;
if (!g_flatfile_account_jid) {
issues = g_slist_prepend(issues,
_issue_new(INTEGRITY_ERROR, "N/A", 0,
g_strdup("Flat-file backend not initialized")));
return issues;
}
GSList* contact_dirs = _collect_contact_dirs(contact_barejid, &issues);
for (GSList* cd = contact_dirs; cd; cd = cd->next) {
_verify_contact_dir((const char*)cd->data, &issues);
}
g_slist_free_full(contact_dirs, g_free);
return g_slist_reverse(issues);
}

1104
src/database_sqlite.c Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -3,42 +3,18 @@
* vim: expandtab:ts=4:sts=4:sw=4
*
* Copyright (C) 2012 - 2019 James Booth <boothj5@gmail.com>
* Copyright (C) 2019 - 2025 Michael Vetter <jubalh@iodoru.org>
*
* This file is part of Profanity.
*
* Profanity is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Profanity is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Profanity. If not, see <https://www.gnu.org/licenses/>.
*
* In addition, as a special exception, the copyright holders give permission to
* link the code of portions of this program with the OpenSSL library under
* certain conditions as described in each individual source file, and
* distribute linked combinations including the two.
*
* You must obey the GNU General Public License in all respects for all of the
* code used other than OpenSSL. If you modify file(s) with this exception, you
* may extend this exception to your version of the file(s), but you are not
* obligated to do so. If you do not wish to do so, delete this exception
* statement from your version. If you delete this exception statement from all
* source files in the program, then also delete it here.
* Copyright (C) 2019 - 2026 Michael Vetter <jubalh@iodoru.org>
*
* SPDX-License-Identifier: GPL-3.0-or-later WITH OpenSSL-exception
*/
#include "config.h"
#include <stdlib.h>
#include <assert.h>
#include <glib.h>
#include "ai/ai_client.h"
#include "log.h"
#include "chatlog.h"
#include "database.h"
@@ -47,7 +23,7 @@
#include "event/common.h"
#include "plugins/plugins.h"
#include "ui/inputwin.h"
#include "ui/window_list.h"
#include "ui/window.h"
#include "xmpp/chat_session.h"
#include "xmpp/session.h"
#include "xmpp/xmpp.h"
@@ -118,7 +94,7 @@ cl_ev_reconnect(void)
void
cl_ev_presence_send(const resource_presence_t presence_type, const int idle_secs)
{
auto_char char* signed_status = NULL;
auto_gchar gchar* signed_status = NULL;
#ifdef HAVE_LIBGPGME
ProfAccount* account = accounts_get_account(session_get_account_name());
@@ -137,9 +113,19 @@ cl_ev_send_msg_correct(ProfChatWin* chatwin, const char* const msg, const char*
chat_state_active(chatwin->state);
gboolean request_receipt = prefs_get_boolean(PREF_RECEIPTS_REQUEST);
if (request_receipt) {
auto_char char* jid = chat_session_get_jid(chatwin->barejid);
EntityCapabilities* caps = caps_lookup(jid);
if (caps != NULL) {
GSList* found = g_slist_find_custom(caps->features, XMPP_FEATURE_RECEIPTS, (GCompareFunc)g_strcmp0);
request_receipt = (found != NULL);
caps_destroy(caps);
}
}
auto_char char* plugin_msg = plugins_pre_chat_message_send(chatwin->barejid, msg);
const char* const message = plugin_msg ?: msg;
auto_gchar gchar* sanitized_msg = str_xml_sanitize(plugin_msg ?: msg);
const char* message = sanitized_msg;
char* replace_id = NULL;
if (correct_last_msg) {
@@ -201,7 +187,8 @@ void
cl_ev_send_muc_msg_corrected(ProfMucWin* mucwin, const char* const msg, const char* const oob_url, gboolean correct_last_msg)
{
auto_char char* plugin_msg = plugins_pre_room_message_send(mucwin->roomjid, msg);
const char* const message = plugin_msg ?: msg;
auto_gchar gchar* sanitized_msg = str_xml_sanitize(plugin_msg ?: msg);
const char* message = sanitized_msg;
char* replace_id = NULL;
if (correct_last_msg) {
@@ -242,7 +229,8 @@ cl_ev_send_priv_msg(ProfPrivateWin* privwin, const char* const msg, const char*
privwin_message_left_room(privwin);
} else {
auto_char char* plugin_msg = plugins_pre_priv_message_send(privwin->fulljid, msg);
const char* const message = plugin_msg ?: msg;
auto_gchar gchar* sanitized_msg = str_xml_sanitize(plugin_msg ?: msg);
const char* message = sanitized_msg;
auto_jid Jid* jidp = jid_create(privwin->fulljid);
auto_char char* id = message_send_private(privwin->fulljid, message, oob_url);
@@ -284,3 +272,24 @@ allow_unencrypted_message(ProfChatWin* chatwin, const char* const msg)
win_println((ProfWin*)chatwin, THEME_ERROR, "-", "Message not sent: invalid encryption mode (%s). Use '/force-encryption policy resend-to-confirm'.", force_enc_mode);
return FALSE;
}
void
cl_ev_send_ai_msg(ProfAiWin* aiwin, const char* const message, const char* const id)
{
if (!aiwin || !message) {
return;
}
assert(aiwin->memcheck == PROFAIWIN_MEMCHECK);
if (!aiwin->session) {
log_error("[AI] AI session not initialized");
return;
}
// Display user message in AI window.
win_print_outgoing(&aiwin->window, ">>", id, NULL, message);
// Send to AI provider.
ai_send_prompt(aiwin->session, message, aiwin);
}

View File

@@ -4,33 +4,7 @@
*
* Copyright (C) 2012 - 2019 James Booth <boothj5@gmail.com>
*
* This file is part of Profanity.
*
* Profanity is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Profanity is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Profanity. If not, see <https://www.gnu.org/licenses/>.
*
* In addition, as a special exception, the copyright holders give permission to
* link the code of portions of this program with the OpenSSL library under
* certain conditions as described in each individual source file, and
* distribute linked combinations including the two.
*
* You must obey the GNU General Public License in all respects for all of the
* code used other than OpenSSL. If you modify file(s) with this exception, you
* may extend this exception to your version of the file(s), but you are not
* obligated to do so. If you do not wish to do so, delete this exception
* statement from your version. If you delete this exception statement from all
* source files in the program, then also delete it here.
*
* SPDX-License-Identifier: GPL-3.0-or-later WITH OpenSSL-exception
*/
#ifndef EVENT_CLIENT_EVENTS_H
@@ -51,6 +25,7 @@ void cl_ev_send_msg(ProfChatWin* chatwin, const char* const msg, const char* con
void cl_ev_send_muc_msg_corrected(ProfMucWin* mucwin, const char* const msg, const char* const oob_url, gboolean correct_last_msg);
void cl_ev_send_muc_msg(ProfMucWin* mucwin, const char* const msg, const char* const oob_url);
void cl_ev_send_priv_msg(ProfPrivateWin* privwin, const char* const msg, const char* const oob_url);
void cl_ev_send_ai_msg(ProfAiWin* aiwin, const char* const message, const char* const id);
// Checks if an unencrypted message can be sent based on encryption preferences.
// Returns TRUE if allowed, FALSE if blocked.

View File

@@ -2,35 +2,9 @@
* common.c
* vim: expandtab:ts=4:sts=4:sw=4
*
* Copyright (C) 2019 - 2025 Michael Vetter <jubalh@iodoru.org>
*
* This file is part of Profanity.
*
* Profanity is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Profanity is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Profanity. If not, see <https://www.gnu.org/licenses/>.
*
* In addition, as a special exception, the copyright holders give permission to
* link the code of portions of this program with the OpenSSL library under
* certain conditions as described in each individual source file, and
* distribute linked combinations including the two.
*
* You must obey the GNU General Public License in all respects for all of the
* code used other than OpenSSL. If you modify file(s) with this exception, you
* may extend this exception to your version of the file(s), but you are not
* obligated to do so. If you do not wish to do so, delete this exception
* statement from your version. If you delete this exception statement from all
* source files in the program, then also delete it here.
* Copyright (C) 2019 - 2026 Michael Vetter <jubalh@iodoru.org>
*
* SPDX-License-Identifier: GPL-3.0-or-later WITH OpenSSL-exception
*/
#include "config.h"

View File

@@ -2,35 +2,9 @@
* common.h
* vim: expandtab:ts=4:sts=4:sw=4
*
* Copyright (C) 2019 - 2025 Michael Vetter <jubalh@iodoru.org>
*
* This file is part of Profanity.
*
* Profanity is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Profanity is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Profanity. If not, see <https://www.gnu.org/licenses/>.
*
* In addition, as a special exception, the copyright holders give permission to
* link the code of portions of this program with the OpenSSL library under
* certain conditions as described in each individual source file, and
* distribute linked combinations including the two.
*
* You must obey the GNU General Public License in all respects for all of the
* code used other than OpenSSL. If you modify file(s) with this exception, you
* may extend this exception to your version of the file(s), but you are not
* obligated to do so. If you do not wish to do so, delete this exception
* statement from your version. If you delete this exception statement from all
* source files in the program, then also delete it here.
* Copyright (C) 2019 - 2026 Michael Vetter <jubalh@iodoru.org>
*
* SPDX-License-Identifier: GPL-3.0-or-later WITH OpenSSL-exception
*/
#ifndef EVENT_COMMON_H

View File

@@ -3,35 +3,9 @@
* vim: expandtab:ts=4:sts=4:sw=4
*
* Copyright (C) 2012 - 2019 James Booth <boothj5@gmail.com>
* Copyright (C) 2019 - 2025 Michael Vetter <jubalh@iodoru.org>
*
* This file is part of Profanity.
*
* Profanity is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Profanity is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Profanity. If not, see <https://www.gnu.org/licenses/>.
*
* In addition, as a special exception, the copyright holders give permission to
* link the code of portions of this program with the OpenSSL library under
* certain conditions as described in each individual source file, and
* distribute linked combinations including the two.
*
* You must obey the GNU General Public License in all respects for all of the
* code used other than OpenSSL. If you modify file(s) with this exception, you
* may extend this exception to your version of the file(s), but you are not
* obligated to do so. If you do not wish to do so, delete this exception
* statement from your version. If you delete this exception statement from all
* source files in the program, then also delete it here.
* Copyright (C) 2019 - 2026 Michael Vetter <jubalh@iodoru.org>
*
* SPDX-License-Identifier: GPL-3.0-or-later WITH OpenSSL-exception
*/
#include "config.h"
@@ -86,6 +60,10 @@ void
sv_ev_login_account_success(char* account_name, gboolean secured)
{
ProfAccount* account = accounts_get_account(account_name);
if (!account) {
log_error("Could not find account: %s", account_name);
return;
}
bookmark_ignore_on_connect(account->jid);
@@ -103,7 +81,9 @@ sv_ev_login_account_success(char* account_name, gboolean secured)
omemo_on_connect(account);
#endif
log_database_init(account);
if (!log_database_init(account)) {
log_error("Failed to initialize database for account: %s", account->jid);
}
vcard_user_refresh();
avatar_pep_subscribe();
@@ -176,7 +156,7 @@ sv_ev_roster_received(void)
GDateTime* lastdt = g_date_time_new_from_timeval_utc(&lasttv);
GTimeSpan diff_micros = g_date_time_difference(nowdt, lastdt);
diff_secs = (diff_micros / 1000) / 1000;
diff_secs = (int)((diff_micros / 1000) / 1000);
g_date_time_unref(lastdt);
}
g_date_time_unref(nowdt);
@@ -587,6 +567,8 @@ _sv_ev_incoming_omemo(ProfChatWin* chatwin, gboolean new_win, ProfMessage* messa
chat_log_omemo_msg_in(message);
}
chatwin->pgp_recv = FALSE;
wins_omemo_trust_changed(chatwin->barejid);
#endif
}
@@ -596,6 +578,9 @@ _sv_ev_incoming_plain(ProfChatWin* chatwin, gboolean new_win, ProfMessage* messa
if (message->body) {
message->enc = PROF_MSG_ENC_NONE;
message->plain = strdup(message->body);
if (message->plain == NULL) {
return;
}
_clean_incoming_message(message);
chatwin_incoming_msg(chatwin, message, new_win);
log_database_add_incoming(message);
@@ -1150,14 +1135,13 @@ int
sv_ev_certfail(const char* const errormsg, const TLSCertificate* cert)
{
// check profanity trusted certs
if (tlscerts_exists(cert->fingerprint)) {
if (tlscerts_exists(cert)) {
cafile_add(cert);
return 1;
}
// check current cert
char* current_fp = tlscerts_get_current();
if (current_fp && g_strcmp0(current_fp, cert->fingerprint) == 0) {
if (tlscerts_current_fingerprint_equals(cert)) {
return 1;
}
@@ -1188,11 +1172,11 @@ sv_ev_certfail(const char* const errormsg, const TLSCertificate* cert)
if (g_strcmp0(cmd, "/tls allow") == 0) {
cons_show("Continuing with connection.");
tlscerts_set_current(cert->fingerprint);
tlscerts_set_current(cert);
return 1;
} else if (g_strcmp0(cmd, "/tls always") == 0) {
cons_show("Adding %s to trusted certificates.", cert->fingerprint);
if (!tlscerts_exists(cert->fingerprint)) {
if (!tlscerts_exists(cert)) {
tlscerts_add(cert);
cafile_add(cert);
}
@@ -1303,6 +1287,10 @@ sv_ev_bookmark_autojoin(Bookmark* bookmark)
static void
_cut(ProfMessage* message, const char* cut)
{
if (message->plain == NULL) {
return;
}
if (strstr(message->plain, cut)) {
auto_gcharv gchar** split = g_strsplit(message->plain, cut, -1);
free(message->plain);

View File

@@ -4,33 +4,7 @@
*
* Copyright (C) 2012 - 2019 James Booth <boothj5@gmail.com>
*
* This file is part of Profanity.
*
* Profanity is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Profanity is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Profanity. If not, see <https://www.gnu.org/licenses/>.
*
* In addition, as a special exception, the copyright holders give permission to
* link the code of portions of this program with the OpenSSL library under
* certain conditions as described in each individual source file, and
* distribute linked combinations including the two.
*
* You must obey the GNU General Public License in all respects for all of the
* code used other than OpenSSL. If you modify file(s) with this exception, you
* may extend this exception to your version of the file(s), but you are not
* obligated to do so. If you do not wish to do so, delete this exception
* statement from your version. If you delete this exception statement from all
* source files in the program, then also delete it here.
*
* SPDX-License-Identifier: GPL-3.0-or-later WITH OpenSSL-exception
*/
#ifndef EVENT_SERVER_EVENTS_H

6
src/gitversion.h.in Normal file
View 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

View File

@@ -3,35 +3,9 @@
* vim: expandtab:ts=4:sts=4:sw=4
*
* Copyright (C) 2012 - 2019 James Booth <boothj5@gmail.com>
* Copyright (C) 2018 - 2025 Michael Vetter <jubalh@iodoru.org>
*
* This file is part of Profanity.
*
* Profanity is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Profanity is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Profanity. If not, see <https://www.gnu.org/licenses/>.
*
* In addition, as a special exception, the copyright holders give permission to
* link the code of portions of this program with the OpenSSL library under
* certain conditions as described in each individual source file, and
* distribute linked combinations including the two.
*
* You must obey the GNU General Public License in all respects for all of the
* code used other than OpenSSL. If you modify file(s) with this exception, you
* may extend this exception to your version of the file(s), but you are not
* obligated to do so. If you do not wish to do so, delete this exception
* statement from your version. If you delete this exception statement from all
* source files in the program, then also delete it here.
* Copyright (C) 2018 - 2026 Michael Vetter <jubalh@iodoru.org>
*
* SPDX-License-Identifier: GPL-3.0-or-later WITH OpenSSL-exception
*/
#include "config.h"
@@ -60,6 +34,7 @@ static FILE* logp;
static gchar* mainlogfile = NULL;
static gboolean user_provided_log = FALSE;
static log_level_t level_filter;
static pid_t prof_pid;
static int stderr_inited;
static log_level_t stderr_level;
@@ -75,6 +50,8 @@ enum {
static void
_rotate_log_file(void)
{
if (!mainlogfile)
return;
auto_gchar gchar* log_file = g_strdup(mainlogfile);
size_t len = strlen(log_file);
auto_gchar gchar* log_file_new = malloc(len + 5);
@@ -198,6 +175,8 @@ log_init(log_level_t filter, const char* const log_file)
logp = fopen(mainlogfile, "a");
g_chmod(mainlogfile, S_IRUSR | S_IWUSR);
prof_pid = getpid();
}
const gchar*
@@ -225,14 +204,11 @@ log_close(void)
static void
_log_msg(log_level_t level, const char* const area, const char* const msg)
{
GDateTime* dt = g_date_time_new_now_local();
char* level_str = _log_abbreviation_string_from_level(level);
auto_gchar gchar* date_fmt = g_date_time_format_iso8601(dt);
auto_gchar gchar* date_fmt = prof_date_time_format_iso8601(NULL);
fprintf(logp, "%s: %s: %s: %s\n", date_fmt, area, level_str, msg);
g_date_time_unref(dt);
fprintf(logp, "%s: %08d: %s: %s: %s\n", date_fmt, prof_pid, area, level_str, msg);
fflush(logp);
@@ -359,11 +335,6 @@ log_stderr_init(log_level_t level)
if (rc != 0)
goto err;
close(STDERR_FILENO);
rc = dup2(stderr_pipe[1], STDERR_FILENO);
if (rc < 0)
goto err_close;
rc = log_stderr_nonblock_set(stderr_pipe[0])
?: log_stderr_nonblock_set(stderr_pipe[1]);
if (rc != 0)
@@ -371,15 +342,22 @@ log_stderr_init(log_level_t level)
stderr_buf = malloc(STDERR_BUFSIZE);
stderr_msg = g_string_sized_new(STDERR_BUFSIZE);
stderr_level = level;
stderr_inited = 1;
prof_add_shutdown_routine(_log_stderr_close);
if (stderr_buf == NULL || stderr_msg == NULL) {
errno = ENOMEM;
goto err_free;
}
if (dup2(stderr_pipe[1], STDERR_FILENO) < 0)
goto err_free;
// fd 2 now owns the pipe write end; release the original.
close(stderr_pipe[1]);
stderr_pipe[1] = STDERR_FILENO;
stderr_level = level;
stderr_inited = 1;
prof_add_shutdown_routine(_log_stderr_close);
return;
err_free:

View File

@@ -4,33 +4,7 @@
*
* Copyright (C) 2012 - 2019 James Booth <boothj5@gmail.com>
*
* This file is part of Profanity.
*
* Profanity is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Profanity is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Profanity. If not, see <https://www.gnu.org/licenses/>.
*
* In addition, as a special exception, the copyright holders give permission to
* link the code of portions of this program with the OpenSSL library under
* certain conditions as described in each individual source file, and
* distribute linked combinations including the two.
*
* You must obey the GNU General Public License in all respects for all of the
* code used other than OpenSSL. If you modify file(s) with this exception, you
* may extend this exception to your version of the file(s), but you are not
* obligated to do so. If you do not wish to do so, delete this exception
* statement from your version. If you delete this exception statement from all
* source files in the program, then also delete it here.
*
* SPDX-License-Identifier: GPL-3.0-or-later WITH OpenSSL-exception
*/
#ifndef LOG_H
@@ -54,9 +28,13 @@ void log_init(log_level_t filter, const char* const log_file);
log_level_t log_get_filter(void);
void log_close(void);
const gchar* get_log_file_location(void);
G_GNUC_PRINTF(1, 2)
void log_debug(const char* const msg, ...);
G_GNUC_PRINTF(1, 2)
void log_info(const char* const msg, ...);
G_GNUC_PRINTF(1, 2)
void log_warning(const char* const msg, ...);
G_GNUC_PRINTF(1, 2)
void log_error(const char* const msg, ...);
void log_msg(log_level_t level, const char* const area, const char* const msg);
int log_level_from_string(char* log_level, log_level_t* level);

View File

@@ -3,35 +3,9 @@
* vim: expandtab:ts=4:sts=4:sw=4
*
* Copyright (C) 2012 - 2019 James Booth <boothj5@gmail.com>
* Copyright (C) 2019 - 2025 Michael Vetter <jubalh@iodoru.org>
*
* This file is part of Profanity.
*
* Profanity is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Profanity is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Profanity. If not, see <https://www.gnu.org/licenses/>.
*
* In addition, as a special exception, the copyright holders give permission to
* link the code of portions of this program with the OpenSSL library under
* certain conditions as described in each individual source file, and
* distribute linked combinations including the two.
*
* You must obey the GNU General Public License in all respects for all of the
* code used other than OpenSSL. If you modify file(s) with this exception, you
* may extend this exception to your version of the file(s), but you are not
* obligated to do so. If you do not wish to do so, delete this exception
* statement from your version. If you delete this exception statement from all
* source files in the program, then also delete it here.
* Copyright (C) 2019 - 2026 Michael Vetter <jubalh@iodoru.org>
*
* SPDX-License-Identifier: GPL-3.0-or-later WITH OpenSSL-exception
*/
#include "config.h"
@@ -78,7 +52,7 @@ main(int argc, char** argv)
GOptionEntry entries[] = {
{ "version", 'v', 0, G_OPTION_ARG_NONE, &version, "Show version information", NULL },
{ "account", 'a', 0, G_OPTION_ARG_STRING, &account_name, "Auto connect to an account on startup" },
{ "account", 'a', 0, G_OPTION_ARG_STRING, &account_name, "Auto connect to an account on startup", NULL },
{ "log", 'l', 0, G_OPTION_ARG_STRING, &log, "Set logging levels, DEBUG, INFO, WARN (default), ERROR", "LEVEL" },
{ "config", 'c', 0, G_OPTION_ARG_STRING, &config_file, "Use an alternative configuration file", NULL },
{ "logfile", 'f', 0, G_OPTION_ARG_STRING, &log_file, "Specify log file", NULL },
@@ -93,9 +67,9 @@ main(int argc, char** argv)
context = g_option_context_new(NULL);
g_option_context_add_main_entries(context, entries, NULL);
if (!g_option_context_parse(context, &argc, &argv, &error)) {
g_print("%s\n", error->message);
g_print("%s\n", PROF_GERROR_MESSAGE(error));
g_option_context_free(context);
g_error_free(error);
PROF_GERROR_FREE(error);
return 1;
}
@@ -107,7 +81,7 @@ main(int argc, char** argv)
// lets use fixed email instead of PACKAGE_BUGREPORT
g_print("Copyright (C) 2012 - 2019 James Booth <boothj5web@gmail.com>.\n");
g_print("Copyright (C) 2019 - 2025 Michael Vetter <jubalh@iodoru.org>.\n");
g_print("Copyright (C) 2019 - 2026 Michael Vetter <jubalh@iodoru.org>.\n");
g_print("License GPLv3+: GNU GPL version 3 or later <https://www.gnu.org/licenses/gpl.html>\n");
g_print("\n");
g_print("This is free software; you are free to change and redistribute it.\n");

View File

@@ -4,39 +4,18 @@
*
* Copyright (C) 2019 Paul Fariello <paul@fariello.eu>
*
* This file is part of Profanity.
*
* Profanity is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Profanity is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Profanity. If not, see <https://www.gnu.org/licenses/>.
*
* In addition, as a special exception, the copyright holders give permission to
* link the code of portions of this program with the OpenSSL library under
* certain conditions as described in each individual source file, and
* distribute linked combinations including the two.
*
* You must obey the GNU General Public License in all respects for all of the
* code used other than OpenSSL. If you modify file(s) with this exception, you
* may extend this exception to your version of the file(s), but you are not
* obligated to do so. If you do not wish to do so, delete this exception
* statement from your version. If you delete this exception statement from all
* source files in the program, then also delete it here.
*
* SPDX-License-Identifier: GPL-3.0-or-later WITH OpenSSL-exception
*/
#include "config.h"
#include <assert.h>
#ifdef HAVE_LIBOMEMO_C
#include <omemo/signal_protocol.h>
#include <omemo/signal_protocol_types.h>
#else
#include <signal/signal_protocol.h>
#include <signal/signal_protocol_types.h>
#endif
#include "log.h"
#include "omemo/omemo.h"
@@ -254,7 +233,7 @@ omemo_decrypt_func(signal_buffer** output, int cipher, const uint8_t* key, size_
{
int ret = SG_SUCCESS;
gcry_cipher_hd_t hd;
unsigned char* plaintext;
unsigned char* plaintext = NULL;
size_t plaintext_len;
int mode;
int algo;
@@ -289,6 +268,13 @@ omemo_decrypt_func(signal_buffer** output, int cipher, const uint8_t* key, size_
}
plaintext_len = ciphertext_len;
// PKCS#5/PKCS#7 padding requires at least one byte of plaintext;
// reject malformed empty ciphertext before any plaintext[len - 1]
// access could underflow / read out-of-bounds.
if (plaintext_len == 0) {
ret = SG_ERR_INVAL;
goto out;
}
plaintext = malloc(plaintext_len);
gcry_cipher_decrypt(hd, plaintext, plaintext_len, ciphertext, ciphertext_len);
@@ -300,6 +286,12 @@ omemo_decrypt_func(signal_buffer** output, int cipher, const uint8_t* key, size_
assert(FALSE);
}
// padding byte must address bytes that exist in plaintext.
if (padding == 0 || (size_t)padding > plaintext_len) {
ret = SG_ERR_UNKNOWN;
goto out;
}
for (int i = 0; i < padding; i++) {
if (plaintext[plaintext_len - 1 - i] != padding) {
ret = SG_ERR_UNKNOWN;
@@ -389,7 +381,7 @@ out:
gcry_error_t
aes256gcm_crypt_file(FILE* in, FILE* out, off_t file_size,
unsigned char key[], unsigned char nonce[], bool encrypt)
unsigned char key[], unsigned char nonce[], gboolean encrypt)
{
if (!gcry_control(GCRYCTL_INITIALIZATION_FINISHED_P)) {
@@ -422,7 +414,7 @@ aes256gcm_crypt_file(FILE* in, FILE* out, off_t file_size,
unsigned char buffer[AES256_GCM_BUFFER_SIZE];
int bytes = 0;
size_t bytes = 0;
off_t bytes_read = 0, bytes_available = 0, read_size = 0;
while (bytes_read < file_size) {
bytes_available = file_size - bytes_read;
@@ -437,8 +429,8 @@ aes256gcm_crypt_file(FILE* in, FILE* out, off_t file_size,
read_size = AES256_GCM_BUFFER_SIZE;
}
bytes = fread(buffer, 1, read_size, in);
bytes_read += bytes;
bytes = fread(buffer, 1, (size_t)read_size, in);
bytes_read += (off_t)bytes;
if (encrypt) {
res = gcry_cipher_encrypt(hd, buffer, bytes, NULL, 0);

View File

@@ -4,37 +4,18 @@
*
* Copyright (C) 2019 Paul Fariello <paul@fariello.eu>
*
* This file is part of Profanity.
*
* Profanity is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Profanity is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Profanity. If not, see <https://www.gnu.org/licenses/>.
*
* In addition, as a special exception, the copyright holders give permission to
* link the code of portions of this program with the OpenSSL library under
* certain conditions as described in each individual source file, and
* distribute linked combinations including the two.
*
* You must obey the GNU General Public License in all respects for all of the
* code used other than OpenSSL. If you modify file(s) with this exception, you
* may extend this exception to your version of the file(s), but you are not
* obligated to do so. If you do not wish to do so, delete this exception
* statement from your version. If you delete this exception statement from all
* source files in the program, then also delete it here.
*
* SPDX-License-Identifier: GPL-3.0-or-later WITH OpenSSL-exception
*/
#include <stdio.h>
#include <stdbool.h>
#include "config.h"
#ifdef HAVE_LIBOMEMO_C
#include <omemo/signal_protocol_types.h>
#else
#include <signal/signal_protocol_types.h>
#endif
#include <gcrypt.h>
#define AES128_GCM_KEY_LENGTH 16
@@ -185,7 +166,7 @@ int aes128gcm_decrypt(unsigned char* plaintext,
const unsigned char* const key, const unsigned char* const tag);
gcry_error_t aes256gcm_crypt_file(FILE* in, FILE* out, off_t file_size,
unsigned char key[], unsigned char nonce[], bool encrypt);
unsigned char key[], unsigned char nonce[], gboolean encrypt);
char* aes256gcm_create_secure_fragment(unsigned char* key,
unsigned char* nonce);

File diff suppressed because it is too large Load Diff

View File

@@ -4,33 +4,7 @@
*
* Copyright (C) 2019 Paul Fariello <paul@fariello.eu>
*
* This file is part of Profanity.
*
* Profanity is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Profanity is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Profanity. If not, see <https://www.gnu.org/licenses/>.
*
* In addition, as a special exception, the copyright holders give permission to
* link the code of portions of this program with the OpenSSL library under
* certain conditions as described in each individual source file, and
* distribute linked combinations including the two.
*
* You must obey the GNU General Public License in all respects for all of the
* code used other than OpenSSL. If you modify file(s) with this exception, you
* may extend this exception to your version of the file(s), but you are not
* obligated to do so. If you do not wish to do so, delete this exception
* statement from your version. If you delete this exception statement from all
* source files in the program, then also delete it here.
*
* SPDX-License-Identifier: GPL-3.0-or-later WITH OpenSSL-exception
*/
#include <glib.h>
#include <gcrypt.h>
@@ -85,6 +59,9 @@ void omemo_trust(const char* const jid, const char* const fingerprint);
void omemo_untrust(const char* const jid, const char* const fingerprint);
GList* omemo_known_device_identities(const char* const jid);
gboolean omemo_is_trusted_identity(const char* const jid, const char* const fingerprint);
gboolean omemo_is_jid_trusted(const char* const jid);
GList* omemo_get_jid_untrusted_fingerprints(const char* const jid);
gboolean omemo_is_device_active(const char* const jid, const char* const fingerprint);
char* omemo_fingerprint_autocomplete(const char* const search_str, gboolean previous, void* context);
void omemo_fingerprint_autocomplete_reset(void);
gboolean omemo_automatic_start(const char* const recipient);
@@ -95,8 +72,9 @@ void omemo_start_muc_sessions(const char* const roomjid);
void omemo_start_device_session(const char* const jid, uint32_t device_id, GList* prekeys, uint32_t signed_prekey_id, const unsigned char* const signed_prekey, size_t signed_prekey_len, const unsigned char* const signature, size_t signature_len, const unsigned char* const identity_key, size_t identity_key_len);
gboolean omemo_loaded(void);
char* omemo_on_message_send(ProfWin* win, const char* const message, gboolean request_receipt, gboolean muc, const char* const replace_id);
char* omemo_on_message_recv(const char* const from, uint32_t sid, const unsigned char* const iv, size_t iv_len, GList* keys, const unsigned char* const payload, size_t payload_len, gboolean muc, gboolean* trusted);
char* omemo_on_message_recv(const char* const from, uint32_t sid, const unsigned char* const iv, size_t iv_len, GList* keys, const unsigned char* const payload, size_t payload_len, gboolean muc, gboolean* trusted, omemo_error_t* error) __attribute__((nonnull(9, 10)));
char* omemo_encrypt_file(FILE* in, FILE* out, off_t file_size, int* gcry_res);
gcry_error_t omemo_decrypt_file(FILE* in, FILE* out, off_t file_size, const char* fragment);

View File

@@ -4,38 +4,17 @@
*
* Copyright (C) 2019 Paul Fariello <paul@fariello.eu>
*
* This file is part of Profanity.
*
* Profanity is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Profanity is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Profanity. If not, see <https://www.gnu.org/licenses/>.
*
* In addition, as a special exception, the copyright holders give permission to
* link the code of portions of this program with the OpenSSL library under
* certain conditions as described in each individual source file, and
* distribute linked combinations including the two.
*
* You must obey the GNU General Public License in all respects for all of the
* code used other than OpenSSL. If you modify file(s) with this exception, you
* may extend this exception to your version of the file(s), but you are not
* obligated to do so. If you do not wish to do so, delete this exception
* statement from your version. If you delete this exception statement from all
* source files in the program, then also delete it here.
*
* SPDX-License-Identifier: GPL-3.0-or-later WITH OpenSSL-exception
*/
#include <glib.h>
#include <signal/signal_protocol.h>
#include "config.h"
#ifdef HAVE_LIBOMEMO_C
#include <omemo/signal_protocol.h>
#else
#include <signal/signal_protocol.h>
#endif
#include "log.h"
#include "omemo/omemo.h"
#include "omemo/store.h"

View File

@@ -4,38 +4,16 @@
*
* Copyright (C) 2019 Paul Fariello <paul@fariello.eu>
*
* This file is part of Profanity.
*
* Profanity is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Profanity is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Profanity. If not, see <https://www.gnu.org/licenses/>.
*
* In addition, as a special exception, the copyright holders give permission to
* link the code of portions of this program with the OpenSSL library under
* certain conditions as described in each individual source file, and
* distribute linked combinations including the two.
*
* You must obey the GNU General Public License in all respects for all of the
* code used other than OpenSSL. If you modify file(s) with this exception, you
* may extend this exception to your version of the file(s), but you are not
* obligated to do so. If you do not wish to do so, delete this exception
* statement from your version. If you delete this exception statement from all
* source files in the program, then also delete it here.
*
* SPDX-License-Identifier: GPL-3.0-or-later WITH OpenSSL-exception
*/
#include <signal/signal_protocol.h>
#include "config.h"
#ifdef HAVE_LIBOMEMO_C
#include <omemo/signal_protocol.h>
#else
#include <signal/signal_protocol.h>
#endif
#define OMEMO_STORE_GROUP_IDENTITY "identity"
#define OMEMO_STORE_GROUP_PREKEYS "prekeys"
#define OMEMO_STORE_GROUP_SIGNED_PREKEYS "signed_prekeys"
@@ -50,7 +28,7 @@ typedef struct
signal_buffer* private;
uint32_t registration_id;
GHashTable* trusted;
bool recv;
gboolean recv;
} identity_key_store_t;
GHashTable* session_store_new(void);

View File

@@ -4,33 +4,7 @@
*
* Copyright (C) 2012 - 2019 James Booth <boothj5@gmail.com>
*
* This file is part of Profanity.
*
* Profanity is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Profanity is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Profanity. If not, see <https://www.gnu.org/licenses/>.
*
* In addition, as a special exception, the copyright holders give permission to
* link the code of portions of this program with the OpenSSL library under
* certain conditions as described in each individual source file, and
* distribute linked combinations including the two.
*
* You must obey the GNU General Public License in all respects for all of the
* code used other than OpenSSL. If you modify file(s) with this exception, you
* may extend this exception to your version of the file(s), but you are not
* obligated to do so. If you do not wish to do so, delete this exception
* statement from your version. If you delete this exception statement from all
* source files in the program, then also delete it here.
*
* SPDX-License-Identifier: GPL-3.0-or-later WITH OpenSSL-exception
*/
#include "config.h"
@@ -296,7 +270,8 @@ otr_on_message_recv(const char* const barejid, const char* const resource, const
// check for OTR whitespace (opportunistic or always)
if (policy == PROF_OTRPOLICY_OPPORTUNISTIC || policy == PROF_OTRPOLICY_ALWAYS) {
if (whitespace_base) {
if (strstr(message, OTRL_MESSAGE_TAG_V2) || strstr(message, OTRL_MESSAGE_TAG_V1)) {
char* tag_position = whitespace_base + strlen(OTRL_MESSAGE_TAG_BASE);
if (strncmp(tag_position, OTRL_MESSAGE_TAG_V2, strlen(OTRL_MESSAGE_TAG_V2)) == 0 || strncmp(tag_position, OTRL_MESSAGE_TAG_V1, strlen(OTRL_MESSAGE_TAG_V1)) == 0) {
// Remove whitespace pattern for proper display in UI
// Handle both BASE+TAGV1/2(16+8) and BASE+TAGV1+TAGV2(16+8+8)
int tag_length = 24;
@@ -661,11 +636,9 @@ otr_get_policy(const char* const recipient)
prof_otrpolicy_t result = PROF_OTRPOLICY_MANUAL;
if (g_strcmp0(account->otr_policy, "manual") == 0) {
result = PROF_OTRPOLICY_MANUAL;
}
if (g_strcmp0(account->otr_policy, "opportunistic") == 0) {
} else if (g_strcmp0(account->otr_policy, "opportunistic") == 0) {
result = PROF_OTRPOLICY_OPPORTUNISTIC;
}
if (g_strcmp0(account->otr_policy, "always") == 0) {
} else if (g_strcmp0(account->otr_policy, "always") == 0) {
result = PROF_OTRPOLICY_ALWAYS;
}
account_free(account);

View File

@@ -4,33 +4,7 @@
*
* Copyright (C) 2012 - 2019 James Booth <boothj5@gmail.com>
*
* This file is part of Profanity.
*
* Profanity is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Profanity is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Profanity. If not, see <https://www.gnu.org/licenses/>.
*
* In addition, as a special exception, the copyright holders give permission to
* link the code of portions of this program with the OpenSSL library under
* certain conditions as described in each individual source file, and
* distribute linked combinations including the two.
*
* You must obey the GNU General Public License in all respects for all of the
* code used other than OpenSSL. If you modify file(s) with this exception, you
* may extend this exception to your version of the file(s), but you are not
* obligated to do so. If you do not wish to do so, delete this exception
* statement from your version. If you delete this exception statement from all
* source files in the program, then also delete it here.
*
* SPDX-License-Identifier: GPL-3.0-or-later WITH OpenSSL-exception
*/
#ifndef OTR_OTR_H

View File

@@ -4,33 +4,7 @@
*
* Copyright (C) 2012 - 2019 James Booth <boothj5@gmail.com>
*
* This file is part of Profanity.
*
* Profanity is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Profanity is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Profanity. If not, see <https://www.gnu.org/licenses/>.
*
* In addition, as a special exception, the copyright holders give permission to
* link the code of portions of this program with the OpenSSL library under
* certain conditions as described in each individual source file, and
* distribute linked combinations including the two.
*
* You must obey the GNU General Public License in all respects for all of the
* code used other than OpenSSL. If you modify file(s) with this exception, you
* may extend this exception to your version of the file(s), but you are not
* obligated to do so. If you do not wish to do so, delete this exception
* statement from your version. If you delete this exception statement from all
* source files in the program, then also delete it here.
*
* SPDX-License-Identifier: GPL-3.0-or-later WITH OpenSSL-exception
*/
#ifndef OTR_OTRLIB_H

View File

@@ -4,33 +4,7 @@
*
* Copyright (C) 2012 - 2019 James Booth <boothj5@gmail.com>
*
* This file is part of Profanity.
*
* Profanity is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Profanity is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Profanity. If not, see <https://www.gnu.org/licenses/>.
*
* In addition, as a special exception, the copyright holders give permission to
* link the code of portions of this program with the OpenSSL library under
* certain conditions as described in each individual source file, and
* distribute linked combinations including the two.
*
* You must obey the GNU General Public License in all respects for all of the
* code used other than OpenSSL. If you modify file(s) with this exception, you
* may extend this exception to your version of the file(s), but you are not
* obligated to do so. If you do not wish to do so, delete this exception
* statement from your version. If you delete this exception statement from all
* source files in the program, then also delete it here.
*
* SPDX-License-Identifier: GPL-3.0-or-later WITH OpenSSL-exception
*/
#include "config.h"

View File

@@ -3,35 +3,9 @@
* vim: expandtab:ts=4:sts=4:sw=4
*
* Copyright (C) 2012 - 2019 James Booth <boothj5@gmail.com>
* Copyright (C) 2018 - 2025 Michael Vetter <jubalh@iodoru.org>
*
* This file is part of Profanity.
*
* Profanity is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Profanity is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Profanity. If not, see <https://www.gnu.org/licenses/>.
*
* In addition, as a special exception, the copyright holders give permission to
* link the code of portions of this program with the OpenSSL library under
* certain conditions as described in each individual source file, and
* distribute linked combinations including the two.
*
* You must obey the GNU General Public License in all respects for all of the
* code used other than OpenSSL. If you modify file(s) with this exception, you
* may extend this exception to your version of the file(s), but you are not
* obligated to do so. If you do not wish to do so, delete this exception
* statement from your version. If you delete this exception statement from all
* source files in the program, then also delete it here.
* Copyright (C) 2018 - 2026 Michael Vetter <jubalh@iodoru.org>
*
* SPDX-License-Identifier: GPL-3.0-or-later WITH OpenSSL-exception
*/
#include "config.h"
@@ -66,27 +40,28 @@ static GHashTable* pubkeys;
static prof_keyfile_t pubkeys_prof_keyfile;
static GKeyFile* pubkeyfile;
static char* passphrase;
static char* passphrase_attempt;
static gchar* passphrase;
static gchar* passphrase_attempt;
static Autocomplete key_ac;
static char* _remove_header_footer(char* str, const char* const footer);
static char* _add_header_footer(const char* const str, const char* const header, const char* const footer);
static char* _gpgme_data_to_char(gpgme_data_t data);
static gchar* _remove_header_footer(gchar* str, const char* const footer);
static gchar* _add_header_footer(const gchar* const str, const char* const header, const char* const footer);
static gchar* _gpgme_data_to_char(gpgme_data_t data);
static void _save_pubkeys(void);
static ProfPGPKey* _gpgme_key_to_ProfPGPKey(gpgme_key_t key);
static const gchar* _gpgme_key_get_email(gpgme_key_t key);
void
_p_gpg_free_pubkeyid(ProfPGPPubKeyId* pubkeyid)
{
if (pubkeyid) {
free(pubkeyid->id);
g_free(pubkeyid->id);
}
free(pubkeyid);
g_free(pubkeyid);
}
static gpgme_error_t*
static gpgme_error_t
_p_gpg_passphrase_cb(void* hook, const char* uid_hint, const char* passphrase_info, int prev_was_bad, int fd)
{
if (passphrase) {
@@ -94,21 +69,21 @@ _p_gpg_passphrase_cb(void* hook, const char* uid_hint, const char* passphrase_in
} else {
GString* pass_term = g_string_new("");
auto_char char* password = ui_ask_pgp_passphrase(uid_hint, prev_was_bad);
auto_gchar gchar* password = ui_ask_pgp_passphrase(uid_hint, prev_was_bad);
if (password) {
g_string_append(pass_term, password);
}
g_string_append(pass_term, "\n");
if (passphrase_attempt) {
free(passphrase_attempt);
g_free(passphrase_attempt);
}
passphrase_attempt = g_string_free(pass_term, FALSE);
gpgme_io_write(fd, passphrase_attempt, strlen(passphrase_attempt));
}
return 0;
return GPG_ERR_NO_ERROR;
}
static void
@@ -126,13 +101,11 @@ _p_gpg_close(void)
key_ac = NULL;
if (passphrase) {
free(passphrase);
passphrase = NULL;
GFREE_SET_NULL(passphrase);
}
if (passphrase_attempt) {
free(passphrase_attempt);
passphrase_attempt = NULL;
GFREE_SET_NULL(passphrase_attempt);
}
}
@@ -156,7 +129,7 @@ p_gpg_init(void)
}
void
p_gpg_on_connect(const char* const barejid)
p_gpg_on_connect(const gchar* const barejid)
{
gchar* pubsloc = files_file_in_account_data_path(DIR_PGP, barejid, "pubkeys");
if (!pubsloc) {
@@ -179,13 +152,13 @@ p_gpg_on_connect(const char* const barejid)
return;
}
for (int i = 0; i < len; i++) {
for (gsize i = 0; i < len; i++) {
GError* gerr = NULL;
gchar* jid = jids[i];
auto_gchar gchar* keyid = g_key_file_get_string(pubkeyfile, jid, "keyid", &gerr);
if (gerr) {
log_error("Error loading PGP key id for %s", jid);
g_error_free(gerr);
PROF_GERROR_FREE(gerr);
} else {
gpgme_key_t key = NULL;
error = gpgme_get_key(ctx, keyid, &key, 0);
@@ -194,10 +167,10 @@ p_gpg_on_connect(const char* const barejid)
continue;
}
ProfPGPPubKeyId* pubkeyid = malloc(sizeof(ProfPGPPubKeyId));
pubkeyid->id = strdup(keyid);
ProfPGPPubKeyId* pubkeyid = g_new0(ProfPGPPubKeyId, 1);
pubkeyid->id = g_strdup(keyid);
pubkeyid->received = FALSE;
g_hash_table_replace(pubkeys, strdup(jid), pubkeyid);
g_hash_table_replace(pubkeys, g_strdup(jid), pubkeyid);
gpgme_key_unref(key);
}
}
@@ -216,7 +189,7 @@ p_gpg_on_disconnect(void)
}
gboolean
p_gpg_addkey(const char* const jid, const char* const keyid)
p_gpg_addkey(const gchar* const jid, const gchar* const keyid)
{
gpgme_ctx_t ctx;
gpgme_error_t error = gpgme_new(&ctx);
@@ -239,10 +212,10 @@ p_gpg_addkey(const char* const jid, const char* const keyid)
_save_pubkeys();
// update in memory pubkeys list
ProfPGPPubKeyId* pubkeyid = malloc(sizeof(ProfPGPPubKeyId));
pubkeyid->id = strdup(keyid);
ProfPGPPubKeyId* pubkeyid = g_new0(ProfPGPPubKeyId, 1);
pubkeyid->id = g_strdup(keyid);
pubkeyid->received = FALSE;
g_hash_table_replace(pubkeys, strdup(jid), pubkeyid);
g_hash_table_replace(pubkeys, g_strdup(jid), pubkeyid);
gpgme_key_unref(key);
return TRUE;
@@ -251,7 +224,7 @@ p_gpg_addkey(const char* const jid, const char* const keyid)
ProfPGPKey*
p_gpg_key_new(void)
{
ProfPGPKey* p_pgpkey = malloc(sizeof(ProfPGPKey));
ProfPGPKey* p_pgpkey = g_new0(ProfPGPKey, 1);
p_pgpkey->id = NULL;
p_pgpkey->name = NULL;
p_pgpkey->fp = NULL;
@@ -268,10 +241,10 @@ void
p_gpg_free_key(ProfPGPKey* key)
{
if (key) {
free(key->id);
free(key->name);
free(key->fp);
free(key);
g_free(key->id);
g_free(key->name);
g_free(key->fp);
g_free(key);
}
}
@@ -294,7 +267,7 @@ GHashTable*
p_gpg_list_keys(void)
{
gpgme_error_t error;
GHashTable* result = g_hash_table_new_full(g_str_hash, g_str_equal, free, (GDestroyNotify)p_gpg_free_key);
GHashTable* result = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, (GDestroyNotify)p_gpg_free_key);
gpgme_ctx_t ctx;
error = gpgme_new(&ctx);
@@ -312,7 +285,7 @@ p_gpg_list_keys(void)
while (!error) {
ProfPGPKey* p_pgpkey = _gpgme_key_to_ProfPGPKey(key);
if (p_pgpkey != NULL) {
g_hash_table_insert(result, strdup(p_pgpkey->name), p_pgpkey);
g_hash_table_insert(result, g_strdup(p_pgpkey->name), p_pgpkey);
}
gpgme_key_unref(key);
@@ -368,7 +341,7 @@ p_gpg_pubkeys(void)
return pubkeys;
}
const char*
const gchar*
p_gpg_libver(void)
{
if (libversion == NULL) {
@@ -378,14 +351,14 @@ p_gpg_libver(void)
}
gboolean
p_gpg_valid_key(const char* const keyid, char** err_str)
p_gpg_valid_key(const gchar* const keyid, gchar** err_str)
{
gpgme_ctx_t ctx;
gpgme_error_t error = gpgme_new(&ctx);
if (error) {
log_error("GPG: Failed to create gpgme context. %s %s", gpgme_strsource(error), gpgme_strerror(error));
if (err_str) {
*err_str = strdup(gpgme_strerror(error));
*err_str = g_strdup(gpgme_strerror(error));
}
return FALSE;
}
@@ -396,7 +369,7 @@ p_gpg_valid_key(const char* const keyid, char** err_str)
if (error || key == NULL) {
log_error("GPG: Failed to get key. %s %s", gpgme_strsource(error), gpgme_strerror(error));
if (err_str) {
*err_str = strdup(error ? gpgme_strerror(error) : "gpgme didn't return any error, but it didn't return a key");
*err_str = g_strdup(error ? gpgme_strerror(error) : "gpgme didn't return any error, but it didn't return a key");
}
gpgme_release(ctx);
return FALSE;
@@ -408,14 +381,14 @@ p_gpg_valid_key(const char* const keyid, char** err_str)
}
gboolean
p_gpg_available(const char* const barejid)
p_gpg_available(const gchar* const barejid)
{
char* pubkey = g_hash_table_lookup(pubkeys, barejid);
gchar* pubkey = g_hash_table_lookup(pubkeys, barejid);
return (pubkey != NULL);
}
void
p_gpg_verify(const char* const barejid, const char* const sign)
p_gpg_verify(const gchar* const barejid, const gchar* const sign)
{
if (!sign) {
return;
@@ -429,7 +402,7 @@ p_gpg_verify(const char* const barejid, const char* const sign)
return;
}
auto_char char* sign_with_header_footer = _add_header_footer(sign, PGP_SIGNATURE_HEADER, PGP_SIGNATURE_FOOTER);
auto_gchar gchar* sign_with_header_footer = _add_header_footer(sign, PGP_SIGNATURE_HEADER, PGP_SIGNATURE_FOOTER);
gpgme_data_t sign_data;
gpgme_data_new_from_mem(&sign_data, sign_with_header_footer, strlen(sign_with_header_footer), 1);
@@ -455,10 +428,10 @@ p_gpg_verify(const char* const barejid, const char* const sign)
log_debug("Could not find PGP key with ID %s for %s", result->signatures->fpr, barejid);
} else {
log_debug("Fingerprint found for %s: %s ", barejid, key->subkeys->fpr);
ProfPGPPubKeyId* pubkeyid = malloc(sizeof(ProfPGPPubKeyId));
pubkeyid->id = strdup(key->subkeys->keyid);
ProfPGPPubKeyId* pubkeyid = g_new0(ProfPGPPubKeyId, 1);
pubkeyid->id = g_strdup(key->subkeys->keyid);
pubkeyid->received = TRUE;
g_hash_table_replace(pubkeys, strdup(barejid), pubkeyid);
g_hash_table_replace(pubkeys, g_strdup(barejid), pubkeyid);
}
gpgme_key_unref(key);
@@ -468,8 +441,8 @@ p_gpg_verify(const char* const barejid, const char* const sign)
gpgme_release(ctx);
}
char*
p_gpg_sign(const char* const str, const char* const fp)
gchar*
p_gpg_sign(const gchar* const str, const gchar* const fp)
{
gpgme_ctx_t ctx;
gpgme_error_t error = gpgme_new(&ctx);
@@ -505,6 +478,10 @@ p_gpg_sign(const char* const str, const char* const fp)
} else {
str_or_empty = strdup("");
}
if (!str_or_empty) {
log_error("GPG: strdup failed");
return NULL;
}
gpgme_data_t str_data;
gpgme_data_new_from_mem(&str_data, str_or_empty, strlen(str_or_empty), 1);
@@ -522,10 +499,10 @@ p_gpg_sign(const char* const str, const char* const fp)
return NULL;
}
char* result = NULL;
gchar* result = NULL;
size_t len = 0;
char* signed_str = gpgme_data_release_and_get_mem(signed_data, &len);
gchar* signed_str = gpgme_data_release_and_get_mem(signed_data, &len);
if (signed_str) {
GString* signed_gstr = g_string_new("");
g_string_append_len(signed_gstr, signed_str, len);
@@ -535,14 +512,14 @@ p_gpg_sign(const char* const str, const char* const fp)
}
if (passphrase_attempt) {
passphrase = strdup(passphrase_attempt);
passphrase = g_strdup(passphrase_attempt);
}
return result;
}
char*
p_gpg_encrypt(const char* const barejid, const char* const message, const char* const fp)
gchar*
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) {
@@ -616,8 +593,8 @@ p_gpg_encrypt(const char* const barejid, const char* const message, const char*
return result;
}
char*
p_gpg_decrypt(const char* const cipher)
gchar*
p_gpg_decrypt(const gchar* const cipher)
{
gpgme_ctx_t ctx;
gpgme_error_t error = gpgme_new(&ctx);
@@ -629,7 +606,7 @@ p_gpg_decrypt(const char* const cipher)
gpgme_set_passphrase_cb(ctx, (gpgme_passphrase_cb_t)_p_gpg_passphrase_cb, NULL);
auto_char char* cipher_with_headers = _add_header_footer(cipher, PGP_MESSAGE_HEADER, PGP_MESSAGE_FOOTER);
auto_gchar gchar* cipher_with_headers = _add_header_footer(cipher, PGP_MESSAGE_HEADER, PGP_MESSAGE_FOOTER);
gpgme_data_t cipher_data;
gpgme_data_new_from_mem(&cipher_data, cipher_with_headers, strlen(cipher_with_headers), 1);
@@ -655,11 +632,9 @@ p_gpg_decrypt(const char* const cipher)
error = gpgme_get_key(ctx, recipient->keyid, &key, 1);
if (!error && key) {
for (gpgme_user_id_t uid = key->uids; uid != NULL; uid = uid->next) {
if (uid->email) {
g_string_append(recipients_str, uid->email);
break;
}
const gchar* addr = _gpgme_key_get_email(key);
if (addr) {
g_string_append(recipients_str, addr);
}
gpgme_key_unref(key);
} else if (error) {
@@ -680,7 +655,7 @@ p_gpg_decrypt(const char* const cipher)
gpgme_release(ctx);
if (passphrase_attempt) {
passphrase = strdup(passphrase_attempt);
passphrase = g_strdup(passphrase_attempt);
}
return _gpgme_data_to_char(plain_data);
@@ -692,8 +667,8 @@ p_gpg_free_decrypted(char* decrypted)
g_free(decrypted);
}
char*
p_gpg_autocomplete_key(const char* const search_str, gboolean previous, void* context)
gchar*
p_gpg_autocomplete_key(const gchar* const search_str, gboolean previous, void* context)
{
return autocomplete_complete(key_ac, search_str, TRUE, previous);
}
@@ -704,16 +679,16 @@ p_gpg_autocomplete_key_reset(void)
autocomplete_reset(key_ac);
}
char*
p_gpg_format_fp_str(char* fp)
gchar*
p_gpg_format_fp_str(gchar* fp)
{
if (!fp) {
return NULL;
}
GString* format = g_string_new("");
int len = strlen(fp);
for (int i = 0; i < len; i++) {
size_t len = strlen(fp);
for (size_t i = 0; i < len; i++) {
g_string_append_c(format, fp[i]);
if (((i + 1) % 4 == 0) && (i + 1 < len)) {
g_string_append_c(format, ' ');
@@ -728,12 +703,12 @@ p_gpg_format_fp_str(char* fp)
*
* @param keyid The key ID for which to retrieve the public key data.
* If the key ID is empty or NULL, returns NULL.
* @return The public key data as a null-terminated char* string allocated using malloc.
* @return The public key data as a null-terminated gchar* string allocated using malloc.
* The returned string should be freed by the caller.
* Returns NULL on error, and errors are written to the error log.
*/
char*
p_gpg_get_pubkey(const char* keyid)
gchar*
p_gpg_get_pubkey(const gchar* keyid)
{
if (!keyid || *keyid == '\0') {
return NULL;
@@ -777,18 +752,18 @@ cleanup:
* @return TRUE if the buffer has the expected header and footer of an armored public key, FALSE otherwise.
*/
gboolean
p_gpg_is_public_key_format(const char* buffer)
p_gpg_is_public_key_format(const gchar* buffer)
{
if (buffer == NULL || buffer[0] == '\0') {
return false;
}
const char* headerPos = strstr(buffer, PGP_PUBLIC_KEY_HEADER);
const gchar* headerPos = strstr(buffer, PGP_PUBLIC_KEY_HEADER);
if (headerPos == NULL) {
return false;
}
const char* footerPos = strstr(buffer, PGP_PUBLIC_KEY_FOOTER);
const gchar* footerPos = strstr(buffer, PGP_PUBLIC_KEY_FOOTER);
return (footerPos != NULL && footerPos > headerPos);
}
@@ -803,7 +778,7 @@ p_gpg_is_public_key_format(const char* buffer)
* by calling p_gpg_free_key() to avoid resource leaks.
*/
ProfPGPKey*
p_gpg_import_pubkey(const char* buffer)
p_gpg_import_pubkey(const gchar* buffer)
{
gpgme_ctx_t ctx;
ProfPGPKey* result = NULL;
@@ -873,9 +848,9 @@ _gpgme_key_to_ProfPGPKey(gpgme_key_t key)
ProfPGPKey* p_pgpkey = p_gpg_key_new();
gpgme_subkey_t sub = key->subkeys;
p_pgpkey->id = strdup(sub->keyid);
p_pgpkey->name = strdup(key->uids->uid);
p_pgpkey->fp = strdup(sub->fpr);
p_pgpkey->id = g_strdup(sub->keyid);
p_pgpkey->name = g_strdup(key->uids->uid);
p_pgpkey->fp = g_strdup(sub->fpr);
while (sub) {
if (sub->can_encrypt)
@@ -892,6 +867,40 @@ _gpgme_key_to_ProfPGPKey(gpgme_key_t key)
return p_pgpkey;
}
/**
* Extract the first email address from a gpgme_key_t object.
* This function provides backwards compatibility for both old and new GPGME versions.
* - GPGME < 2.0.0: Uses gpgme_key_get_string_attr (if available)
* - GPGME >= 2.0.0: Uses modern key->uids->email API
*
* @param key The gpgme_key_t object to extract email from.
* @return The first email address found in the key's user IDs, or NULL if none found.
* The returned string should not be freed as it points to internal gpgme memory.
*/
static const gchar*
_gpgme_key_get_email(gpgme_key_t key)
{
if (!key) {
return NULL;
}
#ifdef GPGME_ATTR_EMAIL
/* Use deprecated function if available (GPGME < 2.0.0) */
return gpgme_key_get_string_attr(key, GPGME_ATTR_EMAIL, NULL, 0);
#else
/* Use modern API for GPGME >= 2.0.0 */
gpgme_user_id_t uid = key->uids;
while (uid) {
if (uid->email && strlen(uid->email) > 0) {
return uid->email;
}
uid = uid->next;
}
return NULL;
#endif
}
/**
* Convert a gpgme_data_t object to a null-terminated char* string.
*
@@ -899,7 +908,7 @@ _gpgme_key_to_ProfPGPKey(gpgme_key_t key)
* @return The converted string allocated using malloc, which should be freed by the caller.
* If an error occurs or the data is empty, NULL is returned and errors are written to the error log.
*/
static char*
static gchar*
_gpgme_data_to_char(gpgme_data_t data)
{
size_t buffer_size = 0;
@@ -910,16 +919,13 @@ _gpgme_data_to_char(gpgme_data_t data)
return NULL;
}
char* buffer = malloc(buffer_size + 1);
memcpy(buffer, gpgme_buffer, buffer_size);
buffer[buffer_size] = '\0';
gchar* buffer = g_strndup(gpgme_buffer, buffer_size);
gpgme_free(gpgme_buffer);
return buffer;
}
static char*
_remove_header_footer(char* str, const char* const footer)
static gchar*
_remove_header_footer(gchar* str, const char* const footer)
{
int pos = 0;
int newlines = 0;
@@ -935,15 +941,15 @@ _remove_header_footer(char* str, const char* const footer)
}
}
char* stripped = strdup(&str[pos]);
char* footer_start = g_strrstr(stripped, footer);
gchar* stripped = g_strdup(&str[pos]);
gchar* footer_start = g_strrstr(stripped, footer);
footer_start[0] = '\0';
return stripped;
}
static char*
_add_header_footer(const char* const str, const char* const header, const char* const footer)
static gchar*
_add_header_footer(const gchar* const str, const char* const header, const char* const footer)
{
return g_strdup_printf("%s\n\n%s\n%s", header, str, footer);
}

View File

@@ -4,33 +4,7 @@
*
* Copyright (C) 2012 - 2019 James Booth <boothj5@gmail.com>
*
* This file is part of Profanity.
*
* Profanity is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Profanity is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Profanity. If not, see <https://www.gnu.org/licenses/>.
*
* In addition, as a special exception, the copyright holders give permission to
* link the code of portions of this program with the OpenSSL library under
* certain conditions as described in each individual source file, and
* distribute linked combinations including the two.
*
* You must obey the GNU General Public License in all respects for all of the
* code used other than OpenSSL. If you modify file(s) with this exception, you
* may extend this exception to your version of the file(s), but you are not
* obligated to do so. If you do not wish to do so, delete this exception
* statement from your version. If you delete this exception statement from all
* source files in the program, then also delete it here.
*
* SPDX-License-Identifier: GPL-3.0-or-later WITH OpenSSL-exception
*/
#ifndef PGP_GPG_H
@@ -38,9 +12,9 @@
typedef struct pgp_key_t
{
char* id;
char* name;
char* fp;
gchar* id;
gchar* name;
gchar* fp;
gboolean encrypt;
gboolean sign;
gboolean certify;
@@ -50,31 +24,31 @@ typedef struct pgp_key_t
typedef struct pgp_pubkeyid_t
{
char* id;
gchar* id;
gboolean received;
} ProfPGPPubKeyId;
void p_gpg_init(void);
void p_gpg_on_connect(const char* const barejid);
void p_gpg_on_connect(const gchar* const barejid);
void p_gpg_on_disconnect(void);
GHashTable* p_gpg_list_keys(void);
void p_gpg_free_keys(GHashTable* keys);
gboolean p_gpg_addkey(const char* const jid, const char* const keyid);
gboolean p_gpg_addkey(const gchar* const jid, const gchar* const keyid);
GHashTable* p_gpg_pubkeys(void);
gboolean p_gpg_valid_key(const char* const keyid, char** err_str);
gboolean p_gpg_available(const char* const barejid);
const char* p_gpg_libver(void);
char* p_gpg_sign(const char* const str, const char* const fp);
void p_gpg_verify(const char* const barejid, const char* const sign);
char* p_gpg_encrypt(const char* const barejid, const char* const message, const char* const fp);
char* p_gpg_decrypt(const char* const cipher);
void p_gpg_free_decrypted(char* decrypted);
char* p_gpg_autocomplete_key(const char* const search_str, gboolean previous, void* context);
gboolean p_gpg_valid_key(const gchar* const keyid, gchar** err_str);
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* 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);
void p_gpg_autocomplete_key_reset(void);
char* p_gpg_format_fp_str(char* fp);
char* p_gpg_get_pubkey(const char* const keyid);
gboolean p_gpg_is_public_key_format(const char* buffer);
ProfPGPKey* p_gpg_import_pubkey(const char* buffer);
gchar* p_gpg_format_fp_str(gchar* fp);
gchar* p_gpg_get_pubkey(const gchar* const keyid);
gboolean p_gpg_is_public_key_format(const gchar* buffer);
ProfPGPKey* p_gpg_import_pubkey(const gchar* buffer);
ProfPGPKey* p_gpg_key_new(void);
void p_gpg_free_key(ProfPGPKey* key);

View File

@@ -3,35 +3,9 @@
* vim: expandtab:ts=4:sts=4:sw=4
*
* Copyright (C) 2020 Stefan Kropp <stefan@debxwoody.de>
* Copyright (C) 2020 - 2025 Michael Vetter <jubalh@iodoru.org>
*
* This file is part of Profanity.
*
* Profanity is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Profanity is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Profanity. If not, see <https://www.gnu.org/licenses/>.
*
* In addition, as a special exception, the copyright holders give permission to
* link the code of portions of this program with the OpenSSL library under
* certain conditions as described in each individual source file, and
* distribute linked combinations including the two.
*
* You must obey the GNU General Public License in all respects for all of the
* code used other than OpenSSL. If you modify file(s) with this exception, you
* may extend this exception to your version of the file(s), but you are not
* obligated to do so. If you do not wish to do so, delete this exception
* statement from your version. If you delete this exception statement from all
* source files in the program, then also delete it here.
* Copyright (C) 2020 - 2026 Michael Vetter <jubalh@iodoru.org>
*
* SPDX-License-Identifier: GPL-3.0-or-later WITH OpenSSL-exception
*/
#include "config.h"
@@ -85,6 +59,7 @@ ox_gpg_public_keys(void)
if (error != GPG_ERR_EOF && error != GPG_ERR_NO_ERROR) {
log_error("OX: gpgme_op_keylist_next %s %s", gpgme_strsource(error), gpgme_strerror(error));
g_hash_table_destroy(result);
gpgme_release(ctx);
return NULL;
}
while (!error) {
@@ -145,10 +120,16 @@ ox_gpg_public_keys(void)
char*
p_ox_gpg_signcrypt(const char* const sender_barejid, const char* const recipient_barejid, const char* const message)
{
char* result = NULL;
gpgme_ctx_t ctx = NULL;
gpgme_key_t recp[3] = { NULL, NULL, NULL };
gpgme_data_t plain = NULL;
gpgme_data_t cipher = NULL;
char* cipher_str = NULL;
setlocale(LC_ALL, "");
gpgme_check_version(NULL);
gpgme_set_locale(NULL, LC_CTYPE, setlocale(LC_CTYPE, NULL));
gpgme_ctx_t ctx;
gpgme_error_t error = gpgme_new(&ctx);
if (GPG_ERR_NO_ERROR != error) {
@@ -165,13 +146,6 @@ p_ox_gpg_signcrypt(const char* const sender_barejid, const char* const recipient
gpgme_set_textmode(ctx, 0);
gpgme_set_offline(ctx, 1);
gpgme_set_keylist_mode(ctx, GPGME_KEYLIST_MODE_LOCAL);
if (error != 0) {
log_error("OX: Signcrypt error: %s", gpgme_strerror(error));
}
gpgme_key_t recp[3];
recp[0] = NULL,
recp[1] = NULL;
char* xmpp_jid_me = alloca((strlen(sender_barejid) + 6) * sizeof(char));
char* xmpp_jid_recipient = alloca((strlen(recipient_barejid) + 6) * sizeof(char));
@@ -185,64 +159,73 @@ p_ox_gpg_signcrypt(const char* const sender_barejid, const char* const recipient
// lookup own key
recp[0] = _ox_key_lookup(sender_barejid, TRUE);
if (error != 0) {
if (recp[0] == NULL) {
cons_show_error("Can't find OX key for %s", xmpp_jid_me);
log_error("OX: Key not found for %s. Error: %s", xmpp_jid_me, gpgme_strerror(error));
return NULL;
log_error("OX: Key not found for %s.", xmpp_jid_me);
goto cleanup;
}
error = gpgme_signers_add(ctx, recp[0]);
if (error != 0) {
log_error("OX: gpgme_signers_add %s. Error: %s", xmpp_jid_me, gpgme_strerror(error));
return NULL;
goto cleanup;
}
// lookup key of recipient
recp[1] = _ox_key_lookup(recipient_barejid, FALSE);
if (error != 0) {
if (recp[1] == NULL) {
cons_show_error("Can't find OX key for %s", xmpp_jid_recipient);
log_error("OX: Key not found for %s. Error: %s", xmpp_jid_recipient, gpgme_strerror(error));
return NULL;
log_error("OX: Key not found for %s.", xmpp_jid_recipient);
goto cleanup;
}
recp[2] = NULL;
log_debug("OX: %s <%s>", recp[0]->uids->name, recp[0]->uids->email);
log_debug("OX: %s <%s>", recp[1]->uids->name, recp[1]->uids->email);
if (recp[0]->uids) {
log_debug("OX: %s <%s>", recp[0]->uids->name, recp[0]->uids->email);
}
if (recp[1]->uids) {
log_debug("OX: %s <%s>", recp[1]->uids->name, recp[1]->uids->email);
}
gpgme_encrypt_flags_t flags = 0;
gpgme_data_t plain;
gpgme_data_t cipher;
error = gpgme_data_new(&plain);
if (error != 0) {
log_error("OX: %s", gpgme_strerror(error));
return NULL;
}
error = gpgme_data_new_from_mem(&plain, message, strlen(message), 0);
if (error != 0) {
log_error("OX: %s", gpgme_strerror(error));
return NULL;
goto cleanup;
}
error = gpgme_data_new(&cipher);
if (error != 0) {
log_error("OX: %s", gpgme_strerror(error));
return NULL;
goto cleanup;
}
error = gpgme_op_encrypt_sign(ctx, recp, flags, plain, cipher);
if (error != 0) {
log_error("OX: %s", gpgme_strerror(error));
return NULL;
goto cleanup;
}
size_t len;
char* cipher_str = gpgme_data_release_and_get_mem(cipher, &len);
char* result = g_base64_encode((unsigned char*)cipher_str, len);
gpgme_key_release(recp[0]);
gpgme_key_release(recp[1]);
gpgme_release(ctx);
cipher_str = gpgme_data_release_and_get_mem(cipher, &len);
cipher = NULL; // Already released by gpgme_data_release_and_get_mem
result = g_base64_encode((unsigned char*)cipher_str, len);
cleanup:
if (cipher_str)
gpgme_free(cipher_str);
if (plain)
gpgme_data_release(plain);
if (cipher)
gpgme_data_release(cipher);
if (recp[0])
gpgme_key_release(recp[0]);
if (recp[1])
gpgme_key_release(recp[1]);
if (ctx)
gpgme_release(ctx);
return result;
}
@@ -313,6 +296,7 @@ _ox_key_lookup(const char* const barejid, gboolean secret_only)
if (uid->name && strlen(uid->name) >= 10) {
if (g_strcmp0(uid->name, xmppuri->str) == 0) {
gpgme_release(ctx);
g_string_free(xmppuri, TRUE);
return key;
}
}
@@ -321,6 +305,7 @@ _ox_key_lookup(const char* const barejid, gboolean secret_only)
gpgme_key_unref(key);
error = gpgme_op_keylist_next(ctx, &key);
}
g_string_free(xmppuri, TRUE);
}
gpgme_release(ctx);
@@ -362,6 +347,13 @@ _ox_key_is_usable(gpgme_key_t key, const char* const barejid, gboolean secret)
char*
p_ox_gpg_decrypt(char* base64)
{
char* result = NULL;
gpgme_ctx_t ctx = NULL;
gpgme_data_t plain = NULL;
gpgme_data_t cipher = NULL;
guchar* encrypted = NULL;
char* plain_str = NULL;
// if there is no private key avaibale,
// we don't try do decrypt
if (!ox_is_private_key_available(connection_get_barejid())) {
@@ -370,7 +362,6 @@ p_ox_gpg_decrypt(char* base64)
setlocale(LC_ALL, "");
gpgme_check_version(NULL);
gpgme_set_locale(NULL, LC_CTYPE, setlocale(LC_CTYPE, NULL));
gpgme_ctx_t ctx;
gpgme_error_t error = gpgme_new(&ctx);
if (GPG_ERR_NO_ERROR != error) {
@@ -387,25 +378,19 @@ p_ox_gpg_decrypt(char* base64)
gpgme_set_textmode(ctx, 0);
gpgme_set_offline(ctx, 1);
gpgme_set_keylist_mode(ctx, GPGME_KEYLIST_MODE_LOCAL);
if (error != 0) {
log_error("OX: %s", gpgme_strerror(error));
}
gpgme_data_t plain = NULL;
gpgme_data_t cipher = NULL;
gsize s;
guchar* encrypted = g_base64_decode(base64, &s);
encrypted = g_base64_decode(base64, &s);
error = gpgme_data_new_from_mem(&cipher, (char*)encrypted, s, 0);
if (error != 0) {
log_error("OX: gpgme_data_new_from_mem: %s", gpgme_strerror(error));
return NULL;
goto cleanup;
}
error = gpgme_data_new(&plain);
if (error != 0) {
log_error("OX: %s", gpgme_strerror(error));
return NULL;
goto cleanup;
}
error = gpgme_op_decrypt_verify(ctx, cipher, plain);
@@ -413,18 +398,29 @@ p_ox_gpg_decrypt(char* base64)
log_error("OX: gpgme_op_decrypt: %s", gpgme_strerror(error));
error = gpgme_op_decrypt(ctx, cipher, plain);
if (error != 0) {
return NULL;
goto cleanup;
}
}
size_t len;
char* plain_str = gpgme_data_release_and_get_mem(plain, &len);
char* result = NULL;
plain_str = gpgme_data_release_and_get_mem(plain, &len);
plain = NULL; // Already released by gpgme_data_release_and_get_mem
if (plain_str) {
result = strndup(plain_str, len);
gpgme_free(plain_str);
}
cleanup:
if (encrypted)
g_free(encrypted);
if (plain_str)
gpgme_free(plain_str);
if (plain)
gpgme_data_release(plain);
if (cipher)
gpgme_data_release(cipher);
if (ctx)
gpgme_release(ctx);
return result;
}
@@ -455,30 +451,33 @@ p_ox_gpg_readkey(const char* const filename, char** key, char** fp)
{
log_info("PX: Read OpenPGP Key from file %s", filename);
GError* error = NULL;
GError* gerr = NULL;
gchar* data = NULL;
gsize size = -1;
gpgme_ctx_t ctx = NULL;
gpgme_data_t gpgme_data = NULL;
gpgme_key_t gkey = NULL;
gpgme_key_t end = NULL;
gboolean success = g_file_get_contents(filename,
&data,
&size,
&error);
&gerr);
if (success) {
setlocale(LC_ALL, "");
gpgme_check_version(NULL);
gpgme_set_locale(NULL, LC_CTYPE, setlocale(LC_CTYPE, NULL));
gpgme_ctx_t ctx;
gpgme_error_t error = gpgme_new(&ctx);
if (GPG_ERR_NO_ERROR != error) {
log_error("OX: Read OpenPGP key from file: gpgme_new failed: %s", gpgme_strerror(error));
return;
goto cleanup;
}
error = gpgme_set_protocol(ctx, GPGME_PROTOCOL_OPENPGP);
if (error != GPG_ERR_NO_ERROR) {
log_error("OX: Read OpenPGP key from file: set GPGME_PROTOCOL_OPENPGP: %s", gpgme_strerror(error));
return;
goto cleanup;
}
gpgme_set_armor(ctx, 0);
@@ -486,49 +485,54 @@ p_ox_gpg_readkey(const char* const filename, char** key, char** fp)
gpgme_set_offline(ctx, 1);
gpgme_set_keylist_mode(ctx, GPGME_KEYLIST_MODE_LOCAL);
gpgme_data_t gpgme_data = NULL;
error = gpgme_data_new(&gpgme_data);
if (error != GPG_ERR_NO_ERROR) {
log_error("OX: Read OpenPGP key from file: gpgme_data_new %s", gpgme_strerror(error));
return;
}
error = gpgme_data_new_from_mem(&gpgme_data, (char*)data, size, 0);
if (error != GPG_ERR_NO_ERROR) {
log_error("OX: Read OpenPGP key from file: gpgme_data_new_from_mem %s", gpgme_strerror(error));
return;
goto cleanup;
}
error = gpgme_op_keylist_from_data_start(ctx, gpgme_data, 0);
if (error != GPG_ERR_NO_ERROR) {
log_error("OX: Read OpenPGP key from file: gpgme_op_keylist_from_data_start %s", gpgme_strerror(error));
return;
goto cleanup;
}
gpgme_key_t gkey;
error = gpgme_op_keylist_next(ctx, &gkey);
if (error != GPG_ERR_NO_ERROR) {
log_error("OX: Read OpenPGP key from file: gpgme_op_keylist_next %s", gpgme_strerror(error));
return;
goto cleanup;
}
gpgme_key_t end;
error = gpgme_op_keylist_next(ctx, &end);
if (error == GPG_ERR_NO_ERROR) {
log_error("OX: Read OpenPGP key from file: ambiguous key");
return;
goto cleanup;
}
if (gkey->revoked || gkey->expired || gkey->disabled || gkey->invalid || gkey->secret) {
log_error("OX: Read OpenPGP key from file: Key is not valid");
return;
goto cleanup;
}
gchar* keybase64 = g_base64_encode((const guchar*)data, size);
*key = strdup(keybase64);
*fp = strdup(gkey->fpr);
g_free(keybase64);
} else {
log_error("OX: Read OpenPGP key from file: Unable to read file: %s", error->message);
log_error("OX: Read OpenPGP key from file: Unable to read file: %s", gerr->message);
g_error_free(gerr);
}
cleanup:
if (data)
g_free(data);
if (gpgme_data)
gpgme_data_release(gpgme_data);
if (gkey)
gpgme_key_unref(gkey);
if (end)
gpgme_key_unref(end);
if (ctx)
gpgme_release(ctx);
}
gboolean
@@ -536,22 +540,26 @@ p_ox_gpg_import(char* base64_public_key)
{
gsize size = -1;
guchar* key = g_base64_decode(base64_public_key, &size);
gboolean result = TRUE;
setlocale(LC_ALL, "");
gpgme_check_version(NULL);
gpgme_set_locale(NULL, LC_CTYPE, setlocale(LC_CTYPE, NULL));
gpgme_ctx_t ctx;
gpgme_ctx_t ctx = NULL;
gpgme_data_t gpgme_data = NULL;
gpgme_error_t error = gpgme_new(&ctx);
if (GPG_ERR_NO_ERROR != error) {
log_error("OX: Read OpenPGP key from file: gpgme_new failed: %s", gpgme_strerror(error));
return FALSE;
result = FALSE;
goto cleanup;
}
error = gpgme_set_protocol(ctx, GPGME_PROTOCOL_OPENPGP);
if (error != GPG_ERR_NO_ERROR) {
log_error("OX: Read OpenPGP key from file: set GPGME_PROTOCOL_OPENPGP: %s", gpgme_strerror(error));
return FALSE;
result = FALSE;
goto cleanup;
}
gpgme_set_armor(ctx, 0);
@@ -559,18 +567,26 @@ p_ox_gpg_import(char* base64_public_key)
gpgme_set_offline(ctx, 1);
gpgme_set_keylist_mode(ctx, GPGME_KEYLIST_MODE_LOCAL);
gpgme_data_t gpgme_data = NULL;
error = gpgme_data_new(&gpgme_data);
error = gpgme_data_new_from_mem(&gpgme_data, (gchar*)key, size, 0);
if (error != GPG_ERR_NO_ERROR) {
log_error("OX: Read OpenPGP key from file: gpgme_data_new %s", gpgme_strerror(error));
return FALSE;
log_error("OX: Read OpenPGP key from file: gpgme_data_new_from_mem %s", gpgme_strerror(error));
result = FALSE;
goto cleanup;
}
gpgme_data_new_from_mem(&gpgme_data, (gchar*)key, size, 0);
error = gpgme_op_import(ctx, gpgme_data);
if (error != GPG_ERR_NO_ERROR) {
log_error("OX: Failed to import key");
result = FALSE;
}
return TRUE;
cleanup:
if (key)
g_free(key);
if (gpgme_data)
gpgme_data_release(gpgme_data);
if (ctx)
gpgme_release(ctx);
return result;
}

View File

@@ -3,35 +3,9 @@
* vim: expandtab:ts=4:sts=4:sw=4
*
* Copyright (C) 2020 Stefan Kropp <stefan@debxwoody.de>
* Copyright (C) 2020 - 2025 Michael Vetter <jubalh@iodoru.org>
*
* This file is part of Profanity.
*
* Profanity is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Profanity is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Profanity. If not, see <https://www.gnu.org/licenses/>.
*
* In addition, as a special exception, the copyright holders give permission to
* link the code of portions of this program with the OpenSSL library under
* certain conditions as described in each individual source file, and
* distribute linked combinations including the two.
*
* You must obey the GNU General Public License in all respects for all of the
* code used other than OpenSSL. If you modify file(s) with this exception, you
* may extend this exception to your version of the file(s), but you are not
* obligated to do so. If you do not wish to do so, delete this exception
* statement from your version. If you delete this exception statement from all
* source files in the program, then also delete it here.
* Copyright (C) 2020 - 2026 Michael Vetter <jubalh@iodoru.org>
*
* SPDX-License-Identifier: GPL-3.0-or-later WITH OpenSSL-exception
*/
#ifndef PGP_OX_H

View File

@@ -4,33 +4,7 @@
*
* Copyright (C) 2012 - 2019 James Booth <boothj5@gmail.com>
*
* This file is part of Profanity.
*
* Profanity is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Profanity is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Profanity. If not, see <https://www.gnu.org/licenses/>.
*
* In addition, as a special exception, the copyright holders give permission to
* link the code of portions of this program with the OpenSSL library under
* certain conditions as described in each individual source file, and
* distribute linked combinations including the two.
*
* You must obey the GNU General Public License in all respects for all of the
* code used other than OpenSSL. If you modify file(s) with this exception, you
* may extend this exception to your version of the file(s), but you are not
* obligated to do so. If you do not wish to do so, delete this exception
* statement from your version. If you delete this exception statement from all
* source files in the program, then also delete it here.
*
* SPDX-License-Identifier: GPL-3.0-or-later WITH OpenSSL-exception
*/
#include "config.h"
@@ -112,29 +86,29 @@ api_register_command(const char* const plugin_name, const char* command_name, in
char** synopsis, const char* description, char* arguments[][2], char** examples,
void* callback, void (*callback_exec)(PluginCommand* command, gchar** args), void (*callback_destroy)(void* callback))
{
PluginCommand* command = calloc(1, sizeof(PluginCommand));
command->command_name = strdup(command_name);
PluginCommand* command = g_new0(PluginCommand, 1);
command->command_name = g_strdup(command_name);
command->min_args = min_args;
command->max_args = max_args;
command->callback = callback;
command->callback_exec = callback_exec;
command->callback_destroy = callback_destroy;
CommandHelp* help = calloc(1, sizeof(CommandHelp));
CommandHelp* help = g_new0(CommandHelp, 1);
int i;
for (i = 0; synopsis[i] != NULL; i++) {
help->synopsis[i] = strdup(synopsis[i]);
help->synopsis[i] = g_strdup(synopsis[i]);
}
help->desc = strdup(description);
help->desc = g_strdup(description);
for (i = 0; arguments[i][0] != NULL; i++) {
help->args[i][0] = strdup(arguments[i][0]);
help->args[i][1] = strdup(arguments[i][1]);
help->args[i][0] = g_strdup(arguments[i][0]);
help->args[i][1] = g_strdup(arguments[i][1]);
}
for (i = 0; examples[i] != NULL; i++) {
help->examples[i] = strdup(examples[i]);
help->examples[i] = g_strdup(examples[i]);
}
command->help = help;
@@ -146,7 +120,7 @@ void
api_register_timed(const char* const plugin_name, void* callback, int interval_seconds,
void (*callback_exec)(PluginTimedFunction* timed_function), void (*callback_destroy)(void* callback))
{
PluginTimedFunction* timed_function = malloc(sizeof(PluginTimedFunction));
PluginTimedFunction* timed_function = g_new0(PluginTimedFunction, 1);
timed_function->callback = callback;
timed_function->callback_exec = callback_exec;
timed_function->callback_destroy = callback_destroy;
@@ -234,7 +208,7 @@ api_get_current_nick(void)
ProfMucWin* mucwin = (ProfMucWin*)current;
assert(mucwin->memcheck == PROFMUCWIN_MEMCHECK);
const char* const nick = muc_nick(mucwin->roomjid);
return nick ? strdup(nick) : NULL;
return nick ? g_strdup(nick) : NULL;
} else {
return NULL;
}
@@ -243,7 +217,7 @@ api_get_current_nick(void)
char*
api_get_name_from_roster(const char* barejid)
{
return strdup(roster_get_display_name(barejid));
return g_strdup(roster_get_display_name(barejid));
}
char*
@@ -261,14 +235,22 @@ api_get_current_occupants(void)
assert(mucwin->memcheck == PROFMUCWIN_MEMCHECK);
GList* occupants_list = muc_roster(mucwin->roomjid);
char** result = malloc((g_list_length(occupants_list) + 1) * sizeof(char*));
if (result == NULL) {
g_list_free(occupants_list);
return NULL;
}
GList* curr = occupants_list;
int i = 0;
while (curr) {
Occupant* occupant = curr->data;
result[i++] = strdup(occupant->nick);
result[i++] = g_strdup(occupant->nick);
curr = g_list_next(curr);
}
result[i] = NULL;
g_list_free(occupants_list);
return result;
} else {
return NULL;
@@ -291,7 +273,7 @@ api_get_room_nick(const char* barejid)
{
const char* const nick = muc_nick(barejid);
return nick ? strdup(nick) : NULL;
return nick ? g_strdup(nick) : NULL;
}
void
@@ -339,7 +321,7 @@ api_win_create(
return;
}
PluginWindowCallback* window = malloc(sizeof(PluginWindowCallback));
PluginWindowCallback* window = g_new0(PluginWindowCallback, 1);
window->callback = callback;
window->callback_exec = callback_exec;
window->callback_destroy = callback_destroy;
@@ -493,7 +475,7 @@ api_incoming_message(const char* const barejid, const char* const resource, cons
{
ProfMessage* message = message_init();
message->from_jid = jid_create_from_bare_and_resource(barejid, resource);
message->plain = strdup(plain);
message->plain = g_strdup(plain);
sv_ev_incoming_message(message);
@@ -532,7 +514,7 @@ api_encryption_reset(const char* const barejid)
ProfChatWin* chatwin = wins_get_chat(barejid);
if (chatwin == NULL) {
log_warning("%s", "api_encryption_reset failed, could not find chat window for %s", barejid);
log_warning("api_encryption_reset failed, could not find chat window for %s", barejid);
return;
}
@@ -566,7 +548,7 @@ api_chat_set_titlebar_enctext(const char* const barejid, const char* const encte
ProfChatWin* chatwin = wins_get_chat(barejid);
if (chatwin == NULL) {
log_warning("%s", "api_chat_set_titlebar_enctext failed, could not find chat window for %s", barejid);
log_warning("api_chat_set_titlebar_enctext failed, could not find chat window for %s", barejid);
return 0;
}
@@ -585,7 +567,7 @@ api_chat_unset_titlebar_enctext(const char* const barejid)
ProfChatWin* chatwin = wins_get_chat(barejid);
if (chatwin == NULL) {
log_warning("%s", "api_chat_unset_titlebar_enctext failed, could not find chat window for %s", barejid);
log_warning("api_chat_unset_titlebar_enctext failed, could not find chat window for %s", barejid);
return 0;
}
@@ -614,7 +596,7 @@ api_chat_set_incoming_char(const char* const barejid, const char* const ch)
ProfChatWin* chatwin = wins_get_chat(barejid);
if (chatwin == NULL) {
log_warning("%s", "api_chat_set_incoming_char failed, could not find chat window for %s", barejid);
log_warning("api_chat_set_incoming_char failed, could not find chat window for %s", barejid);
return 0;
}
@@ -633,7 +615,7 @@ api_chat_unset_incoming_char(const char* const barejid)
ProfChatWin* chatwin = wins_get_chat(barejid);
if (chatwin == NULL) {
log_warning("%s", "api_chat_unset_incoming_char failed, could not find chat window for %s", barejid);
log_warning("api_chat_unset_incoming_char failed, could not find chat window for %s", barejid);
return 0;
}
@@ -662,7 +644,7 @@ api_chat_set_outgoing_char(const char* const barejid, const char* const ch)
ProfChatWin* chatwin = wins_get_chat(barejid);
if (chatwin == NULL) {
log_warning("%s", "api_chat_set_outgoing_char failed, could not find chat window for %s", barejid);
log_warning("api_chat_set_outgoing_char failed, could not find chat window for %s", barejid);
return 0;
}
@@ -681,7 +663,7 @@ api_chat_unset_outgoing_char(const char* const barejid)
ProfChatWin* chatwin = wins_get_chat(barejid);
if (chatwin == NULL) {
log_warning("%s", "api_chat_unset_outgoing_char failed, could not find chat window for %s", barejid);
log_warning("api_chat_unset_outgoing_char failed, could not find chat window for %s", barejid);
return 0;
}
@@ -705,7 +687,7 @@ api_room_set_titlebar_enctext(const char* const roomjid, const char* const encte
ProfMucWin* mucwin = wins_get_muc(roomjid);
if (mucwin == NULL) {
log_warning("%s", "api_room_set_titlebar_enctext failed, could not find room window for %s", roomjid);
log_warning("api_room_set_titlebar_enctext failed, could not find room window for %s", roomjid);
return 0;
}
@@ -724,7 +706,7 @@ api_room_unset_titlebar_enctext(const char* const roomjid)
ProfMucWin* mucwin = wins_get_muc(roomjid);
if (mucwin == NULL) {
log_warning("%s", "api_room_unset_titlebar_enctext failed, could not find room window for %s", roomjid);
log_warning("api_room_unset_titlebar_enctext failed, could not find room window for %s", roomjid);
return 0;
}
@@ -753,7 +735,7 @@ api_room_set_message_char(const char* const roomjid, const char* const ch)
ProfMucWin* mucwin = wins_get_muc(roomjid);
if (mucwin == NULL) {
log_warning("%s", "api_room_set_message_char failed, could not find room window for %s", roomjid);
log_warning("api_room_set_message_char failed, could not find room window for %s", roomjid);
return 0;
}
@@ -772,7 +754,7 @@ api_room_unset_message_char(const char* const roomjid)
ProfMucWin* mucwin = wins_get_muc(roomjid);
if (mucwin == NULL) {
log_warning("%s", "api_room_unset_message_char failed, could not find room window for %s", roomjid);
log_warning("api_room_unset_message_char failed, could not find room window for %s", roomjid);
return 0;
}
@@ -796,7 +778,7 @@ api_chat_show(const char* const barejid, const char* message)
ProfChatWin* chatwin = wins_get_chat(barejid);
if (chatwin == NULL) {
log_warning("%s", "api_chat_show failed, could not find chat window for %s", barejid);
log_warning("api_chat_show failed, could not find chat window for %s", barejid);
return 0;
}
@@ -832,7 +814,7 @@ api_chat_show_themed(const char* const barejid, const char* const group, const c
ProfChatWin* chatwin = wins_get_chat(barejid);
if (chatwin == NULL) {
log_warning("%s", "api_chat_show_themed failed, could not find chat window for %s", barejid);
log_warning("api_chat_show_themed failed, could not find chat window for %s", barejid);
return 0;
}
@@ -859,7 +841,7 @@ api_room_show(const char* const roomjid, const char* message)
ProfMucWin* mucwin = wins_get_muc(roomjid);
if (mucwin == NULL) {
log_warning("%s", "api_room_show failed, could not find room window for %s", roomjid);
log_warning("api_room_show failed, could not find room window for %s", roomjid);
return 0;
}
@@ -895,7 +877,7 @@ api_room_show_themed(const char* const roomjid, const char* const group, const c
ProfMucWin* mucwin = wins_get_muc(roomjid);
if (mucwin == NULL) {
log_warning("%s", "api_room_show_themed failed, could not find room window for %s", roomjid);
log_warning("api_room_show_themed failed, could not find room window for %s", roomjid);
return 0;
}

View File

@@ -4,33 +4,7 @@
*
* Copyright (C) 2012 - 2019 James Booth <boothj5@gmail.com>
*
* This file is part of Profanity.
*
* Profanity is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Profanity is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Profanity. If not, see <https://www.gnu.org/licenses/>.
*
* In addition, as a special exception, the copyright holders give permission to
* link the code of portions of this program with the OpenSSL library under
* certain conditions as described in each individual source file, and
* distribute linked combinations including the two.
*
* You must obey the GNU General Public License in all respects for all of the
* code used other than OpenSSL. If you modify file(s) with this exception, you
* may extend this exception to your version of the file(s), but you are not
* obligated to do so. If you do not wish to do so, delete this exception
* statement from your version. If you delete this exception statement from all
* source files in the program, then also delete it here.
*
* SPDX-License-Identifier: GPL-3.0-or-later WITH OpenSSL-exception
*/
#ifndef PLUGINS_API_H

View File

@@ -4,33 +4,7 @@
*
* Copyright (C) 2012 - 2019 James Booth <boothj5@gmail.com>
*
* This file is part of Profanity.
*
* Profanity is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Profanity is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Profanity. If not, see <https://www.gnu.org/licenses/>.
*
* In addition, as a special exception, the copyright holders give permission to
* link the code of portions of this program with the OpenSSL library under
* certain conditions as described in each individual source file, and
* distribute linked combinations including the two.
*
* You must obey the GNU General Public License in all respects for all of the
* code used other than OpenSSL. If you modify file(s) with this exception, you
* may extend this exception to your version of the file(s), but you are not
* obligated to do so. If you do not wish to do so, delete this exception
* statement from your version. If you delete this exception statement from all
* source files in the program, then also delete it here.
*
* SPDX-License-Identifier: GPL-3.0-or-later WITH OpenSSL-exception
*/
#include "config.h"
@@ -75,14 +49,14 @@ autocompleters_add(const char* const plugin_name, const char* key, char** items)
} else {
Autocomplete new_ac = autocomplete_new();
autocomplete_add_all(new_ac, items);
g_hash_table_insert(key_to_ac, strdup(key), new_ac);
g_hash_table_insert(key_to_ac, g_strdup(key), new_ac);
}
} else {
key_to_ac = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, (GDestroyNotify)autocomplete_free);
Autocomplete new_ac = autocomplete_new();
autocomplete_add_all(new_ac, items);
g_hash_table_insert(key_to_ac, strdup(key), new_ac);
g_hash_table_insert(plugin_to_acs, strdup(plugin_name), key_to_ac);
g_hash_table_insert(key_to_ac, g_strdup(key), new_ac);
g_hash_table_insert(plugin_to_acs, g_strdup(plugin_name), key_to_ac);
}
}
@@ -123,11 +97,11 @@ autocompleters_filepath_add(const char* const plugin_name, const char* prefix)
{
GHashTable* prefixes = g_hash_table_lookup(plugin_to_filepath_acs, plugin_name);
if (prefixes) {
g_hash_table_add(prefixes, strdup(prefix));
g_hash_table_add(prefixes, g_strdup(prefix));
} else {
prefixes = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL);
g_hash_table_add(prefixes, strdup(prefix));
g_hash_table_insert(plugin_to_filepath_acs, strdup(plugin_name), prefixes);
g_hash_table_add(prefixes, g_strdup(prefix));
g_hash_table_insert(plugin_to_filepath_acs, g_strdup(plugin_name), prefixes);
}
}

View File

@@ -4,33 +4,7 @@
*
* Copyright (C) 2012 - 2019 James Booth <boothj5@gmail.com>
*
* This file is part of Profanity.
*
* Profanity is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Profanity is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Profanity. If not, see <https://www.gnu.org/licenses/>.
*
* In addition, as a special exception, the copyright holders give permission to
* link the code of portions of this program with the OpenSSL library under
* certain conditions as described in each individual source file, and
* distribute linked combinations including the two.
*
* You must obey the GNU General Public License in all respects for all of the
* code used other than OpenSSL. If you modify file(s) with this exception, you
* may extend this exception to your version of the file(s), but you are not
* obligated to do so. If you do not wish to do so, delete this exception
* statement from your version. If you delete this exception statement from all
* source files in the program, then also delete it here.
*
* SPDX-License-Identifier: GPL-3.0-or-later WITH OpenSSL-exception
*/
#ifndef PLUGINS_AUTOCOMPLETERS_H

Some files were not shown because too many files have changed in this diff Show More