diff --git a/.builds/openbsd.yml b/.builds/openbsd.yml deleted file mode 100644 index 326bdb5f..00000000 --- a/.builds/openbsd.yml +++ /dev/null @@ -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 diff --git a/.commitlintrc.json b/.commitlintrc.json new file mode 100644 index 00000000..8c6c4297 --- /dev/null +++ b/.commitlintrc.json @@ -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] + } +} diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..96bcc9c2 --- /dev/null +++ b/.gitattributes @@ -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 diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md deleted file mode 100644 index b554ac1d..00000000 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ /dev/null @@ -1,44 +0,0 @@ ---- -name: Bug report -about: Create a report -title: '' -labels: bug -assignees: '' - ---- - - - - - - -## Expected Behavior - - - -## Current Behavior - - - -## Possible Solution - - - -## Steps to Reproduce (for bugs) - - -1. -2. -3. -4. - -## Context - - -## 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 diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 00000000..4fe86d5e --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -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. diff --git a/.github/issue_template.md b/.github/issue_template.md index 08f60b0e..7a2fc7ac 100644 --- a/.github/issue_template.md +++ b/.github/issue_template.md @@ -1,3 +1,12 @@ +--- +name: Bug report +about: Create a report +title: '' +labels: bug +assignees: '' + +--- + @@ -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 - diff --git a/.github/workflows/ci-code.yml b/.github/workflows/ci-code.yml index f46947d7..313ce2e3 100644 --- a/.github/workflows/ci-code.yml +++ b/.github/workflows/ci-code.yml @@ -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 \ No newline at end of file diff --git a/Brewfile b/Brewfile deleted file mode 100644 index d73a973c..00000000 --- a/Brewfile +++ /dev/null @@ -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' diff --git a/CHANGELOG b/CHANGELOG index a87f345a..60411a02 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,5 @@ -0.16.0 (unreleased) -=================== +cproof fork (unreleased) +======================== Changes: - Add flat-file database backend as alternative to SQLite for message history. @@ -10,6 +10,271 @@ Changes: - 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 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) =================== diff --git a/Makefile.am b/Makefile.am index 3de888ee..c3e3503f 100644 --- a/Makefile.am +++ b/Makefile.am @@ -152,31 +152,32 @@ 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 \ @@ -311,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 diff --git a/RELEASE_GUIDE.md b/RELEASE_GUIDE.md index 8e7cf2b8..964dd5fd 100644 --- a/RELEASE_GUIDE.md +++ b/RELEASE_GUIDE.md @@ -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 diff --git a/Dockerfile.arch b/ci/Dockerfile.arch similarity index 89% rename from Dockerfile.arch rename to ci/Dockerfile.arch index 7f1088b1..55516871 100644 --- a/Dockerfile.arch +++ b/ci/Dockerfile.arch @@ -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 diff --git a/Dockerfile.debian b/ci/Dockerfile.debian similarity index 100% rename from Dockerfile.debian rename to ci/Dockerfile.debian diff --git a/Dockerfile.fedora b/ci/Dockerfile.fedora similarity index 100% rename from Dockerfile.fedora rename to ci/Dockerfile.fedora diff --git a/Dockerfile.tumbleweed b/ci/Dockerfile.tumbleweed similarity index 100% rename from Dockerfile.tumbleweed rename to ci/Dockerfile.tumbleweed diff --git a/Dockerfile.ubuntu b/ci/Dockerfile.ubuntu similarity index 100% rename from Dockerfile.ubuntu rename to ci/Dockerfile.ubuntu diff --git a/docs/profanity-ox-setup.1 b/docs/profanity-ox-setup.1 index 70b3afa8..f6c3e635 100644 --- a/docs/profanity-ox-setup.1 +++ b/docs/profanity-ox-setup.1 @@ -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 diff --git a/docs/profanity.1 b/docs/profanity.1 index 9a6fcaf8..4401df93 100755 --- a/docs/profanity.1 +++ b/docs/profanity.1 @@ -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 @@ -253,7 +261,7 @@ or to the mailing list at: .br .SH LICENSE Copyright (C) 2012 \- 2019 James Booth . -Copyright (C) 2019 \- 2025 Michael Vetter . +Copyright (C) 2019 \- 2026 Michael Vetter . License GPLv3+: GNU GPL version 3 or later This is free software; you are free to change and redistribute it. There is NO WARRANTY, to the extent permitted by law. diff --git a/meson.build b/meson.build new file mode 100644 index 00000000..9c29537c --- /dev/null +++ b/meson.build @@ -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 + 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') diff --git a/meson_options.txt b/meson_options.txt new file mode 100644 index 00000000..03018334 --- /dev/null +++ b/meson_options.txt @@ -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)' +) diff --git a/profanity.doap b/profanity.doap index c7e74387..b7303c18 100644 --- a/profanity.doap +++ b/profanity.doap @@ -86,7 +86,7 @@ complete - 2.5rc3 + 2.5.0 0.4.5 Use /disco items and /disco info @@ -97,7 +97,7 @@ partial - ? + 1.35.2 0.4.5 @@ -141,7 +141,7 @@ complete - 1.2 + 1.3.0 0.14.0 @@ -161,7 +161,7 @@ complete - 1.15.8 + 1.30.0 0.7.0 Used for OMEMO. @@ -174,7 +174,6 @@ complete 2.4 0.11.0 - Only password change. On master also to create an account. @@ -227,7 +226,7 @@ complete - unknown + 1.6.0 0.4.5 @@ -260,7 +259,7 @@ complete - 1.2 + 1.4.0 0.4.7 @@ -282,7 +281,7 @@ complete - 1.6 + 1.6.3 0.13.0 @@ -326,7 +325,7 @@ complete - 1.0 + 1.2 0.4.5 @@ -348,7 +347,7 @@ complete - 0.12.1 + 1.0.1 0.4.7 @@ -370,7 +369,7 @@ complete - 1.2.0 + 1.2.1 0.9.0 @@ -381,7 +380,7 @@ complete - 0.6.1 + 0.7.0 0.8.0 @@ -392,7 +391,7 @@ complete - 1.0.0 + 1.2.0 0.5.0 @@ -436,7 +435,7 @@ complete - 0.3 + 0.4.0 0.11.0 @@ -458,7 +457,7 @@ complete - 0.7.0 + 1.0.1 0.8.0 @@ -476,9 +475,24 @@ + + 0.17.0 + 2026-03-26 + + + + 0.16.0 + 2026-02-23 + + + + 0.15.1 + 2025-08-22 + + 0.15.0 - 2024-03-27 + 2025-03-27 diff --git a/scripts/README.md b/scripts/README.md new file mode 100644 index 00000000..ede16111 --- /dev/null +++ b/scripts/README.md @@ -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` diff --git a/scripts/build-configuration-matrix.sh b/scripts/build-configuration-matrix.sh new file mode 100755 index 00000000..6a3c3dc9 --- /dev/null +++ b/scripts/build-configuration-matrix.sh @@ -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}" diff --git a/scripts/changelog-helper.py b/scripts/changelog-helper.py new file mode 100755 index 00000000..9a97c3ef --- /dev/null +++ b/scripts/changelog-helper.py @@ -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 " -> "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() diff --git a/scripts/check-new-xeps.py b/scripts/check-new-xeps.py new file mode 100755 index 00000000..a62d6a09 --- /dev/null +++ b/scripts/check-new-xeps.py @@ -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() diff --git a/scripts/lint-and-test.sh b/scripts/lint-and-test.sh new file mode 100755 index 00000000..723ab768 --- /dev/null +++ b/scripts/lint-and-test.sh @@ -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}" diff --git a/src/chatlog.h b/src/chatlog.h index 94717fbf..27d3a2fd 100644 --- a/src/chatlog.h +++ b/src/chatlog.h @@ -4,33 +4,7 @@ * * Copyright (C) 2012 - 2019 James Booth * - * 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 . - * - * 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 diff --git a/src/command/cmd_ac.c b/src/command/cmd_ac.c index acab53b5..d7949bad 100644 --- a/src/command/cmd_ac.c +++ b/src/command/cmd_ac.c @@ -3,35 +3,9 @@ * vim: expandtab:ts=4:sts=4:sw=4 * * Copyright (C) 2012 - 2019 James Booth - * Copyright (C) 2019 - 2025 Michael Vetter - * - * 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 . - * - * 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 * + * SPDX-License-Identifier: GPL-3.0-or-later WITH OpenSSL-exception */ #include "config.h" @@ -71,6 +45,7 @@ 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); @@ -136,6 +111,7 @@ 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); @@ -167,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; @@ -297,6 +274,8 @@ 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; @@ -310,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, @@ -332,6 +313,7 @@ static Autocomplete* all_acs[] = { &autoconnect_ac, &wintitle_ac, &theme_ac, + &spellcheck_ac, &account_ac, &account_set_ac, &account_clear_ac, @@ -453,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, @@ -604,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"); @@ -875,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"); @@ -1232,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"); @@ -1461,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); @@ -1605,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, ' '))) { @@ -1709,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); @@ -1775,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) { @@ -1828,7 +1840,7 @@ _cmd_ac_complete_params(ProfWin* window, const char* const input, gboolean previ "/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; @@ -1841,7 +1853,7 @@ _cmd_ac_complete_params(ProfWin* window, const char* const input, gboolean previ } gchar* history_jid_subcmds[] = { "/history verify", "/history export", "/history import" }; - for (int i = 0; i < ARRAY_SIZE(history_jid_subcmds); i++) { + 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; @@ -1853,6 +1865,11 @@ _cmd_ac_complete_params(ProfWin* window, const char* const input, gboolean previ 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; @@ -1863,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); @@ -1878,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); @@ -1893,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; @@ -1902,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; @@ -1922,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; @@ -1942,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) { @@ -2011,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; @@ -2333,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; @@ -2366,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; @@ -3812,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; @@ -4324,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) { @@ -4348,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; diff --git a/src/command/cmd_ac.h b/src/command/cmd_ac.h index ceab49b2..a36bd109 100644 --- a/src/command/cmd_ac.h +++ b/src/command/cmd_ac.h @@ -4,33 +4,7 @@ * * Copyright (C) 2012 - 2019 James Booth * - * 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 . - * - * 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 diff --git a/src/command/cmd_defs.c b/src/command/cmd_defs.c index e89fa62d..60a85eec 100644 --- a/src/command/cmd_defs.c +++ b/src/command/cmd_defs.c @@ -3,35 +3,9 @@ * vim: expandtab:ts=4:sts=4:sw=4 * * Copyright (C) 2012 - 2019 James Booth - * Copyright (C) 2019 - 2025 Michael Vetter - * - * 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 . - * - * 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 * + * 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 []", - "/connect [server ] [port ] [tls force|allow|trust|legacy|disable] [auth default|legacy]", + "/connect [server ] [port ] [tls force|allow|trust|direct|disable|legacy] [auth default|legacy]", "/connect ") 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 []", "Block the specified Jabber ID. If in a chat window and no jid is specified, the current recipient will be blocked." }, { "remove ", "Remove the specified Jabber ID from the blocked list." }, - { "report-abuse []", "Report the jid as abuse with an optional message to the service operator." }, - { "report-spam []", "Report the jid as spam with an optional message to the service operator." }) + { "report-abuse []", "Report a user for abuse with an optional description of the behavior." }, + { "report-spam []", "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") }, @@ -1473,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 ") + 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 ", "Set the spellcheck language (en_US)." }) + }, + { CMD_PREAMBLE("/autoconnect", parse_args, 1, 2, &cons_autoconnect_setting) CMD_MAINFUNC(cmd_autoconnect) @@ -2079,7 +2076,7 @@ static const struct cmd_t command_defs[] = { "/account set pgpkeyid ", "/account set startscript