Compare commits
95 Commits
build/reen
...
fix/pad-de
| Author | SHA1 | Date | |
|---|---|---|---|
|
36512d9dde
|
|||
| e33874e6b5 | |||
|
830479cf20
|
|||
|
2d3d1ced71
|
|||
|
adb078a3e2
|
|||
|
5b45b35b3e
|
|||
|
57d79ecf9c
|
|||
|
bfd7064a40
|
|||
|
3f6b8f69fd
|
|||
|
a04a8948e1
|
|||
|
c1093db090
|
|||
|
72f4f186da
|
|||
|
3b673150b4
|
|||
|
53cdf488b6
|
|||
|
4776c1f1ec
|
|||
|
a3a45ad477
|
|||
|
2952466abd
|
|||
|
06b80bc89a
|
|||
|
f9e0ba9630
|
|||
|
7469f31c78
|
|||
|
5e329c77e1
|
|||
|
9e5dfb14f8
|
|||
|
1aaa382d5e
|
|||
|
3f36c303c2
|
|||
|
0feacbc9da
|
|||
|
0722dc9e36
|
|||
|
f84ed1bf6a
|
|||
|
9ec01fa8cc
|
|||
|
1508f27e73
|
|||
|
d4254db814
|
|||
|
9e1b95a814
|
|||
|
24e1dac354
|
|||
|
f20a4da160
|
|||
|
663a959f9c
|
|||
|
31538580fb
|
|||
|
37ca2de308
|
|||
|
4fce333c9a
|
|||
|
467222d0ca
|
|||
|
f8826b7c79
|
|||
|
8353a29b4f
|
|||
|
85c817ee8c
|
|||
|
a90eef1cb2
|
|||
|
48ab4b9360
|
|||
|
8f580f91a8
|
|||
|
2d7de2caf6
|
|||
|
889a6e2b63
|
|||
|
44de29a199
|
|||
|
e31240a4be
|
|||
|
88b48000f8
|
|||
|
f446f48d07
|
|||
|
20af44196b
|
|||
|
84d6253561
|
|||
|
74cfd32c1e
|
|||
|
fe6912a30c
|
|||
|
266f5aa046
|
|||
|
3738be4af1
|
|||
|
ad95edb2b9
|
|||
|
fac1e224bc
|
|||
|
6ad8a19053
|
|||
|
40dd773c6a
|
|||
|
8849d9b79e
|
|||
|
92cb930810
|
|||
|
9fd62e2304
|
|||
|
c94263486f
|
|||
|
353b6bdb11
|
|||
|
105c496ab1
|
|||
|
cb6f302208
|
|||
|
4cdcf9a8eb
|
|||
|
7e4c60e2c3
|
|||
|
e998d5e7ef
|
|||
|
3ffeda1531
|
|||
|
a4cead7fe8
|
|||
|
052e168e9b
|
|||
|
c94cd0b549
|
|||
|
4780fdd4a1
|
|||
|
|
9b1cb6ab85
|
||
|
|
f419e65fad
|
||
|
|
110b3d27b4
|
||
|
|
75b378cf54
|
||
|
|
988d3663d1
|
||
|
|
d3fa1c2f78
|
||
|
|
1a385b8cd2
|
||
|
|
39aab45162
|
||
|
|
0d5123e69a
|
||
|
|
36f183d6a4
|
||
|
|
6b6576b31c
|
||
|
|
1ae78400a4
|
||
|
ea8a758ecf
|
|||
|
2808def933
|
|||
|
741dc4c275
|
|||
|
65cf9ddb9c
|
|||
|
51d2355d97
|
|||
|
e33c7a477d
|
|||
|
f4405d114a
|
|||
|
442bfb6ce1
|
@@ -1,49 +0,0 @@
|
||||
image: openbsd/7.0
|
||||
|
||||
packages:
|
||||
- gcc-11.2.0p0
|
||||
- cmake
|
||||
- gmake
|
||||
- cmocka
|
||||
- libtool
|
||||
- automake-1.16.3
|
||||
- pkgconf
|
||||
- readline
|
||||
- python-3.8.12
|
||||
- autoconf-2.69p3
|
||||
- autoconf-archive
|
||||
- curl
|
||||
- gpgme
|
||||
- glib2
|
||||
- gtk+2
|
||||
- libotr
|
||||
- libassuan
|
||||
- libgpg-error
|
||||
- libgcrypt
|
||||
- libsignal-protocol-c
|
||||
- sqlite3
|
||||
|
||||
sources:
|
||||
- https://github.com/strophe/libstrophe.git#0.11.0
|
||||
- https://github.com/profanity-im/profanity
|
||||
|
||||
environment:
|
||||
LANG: en_US.UTF-8
|
||||
|
||||
tasks:
|
||||
- symlink: |
|
||||
doas ln -sf /usr/local/bin/python3 /usr/local/bin/python
|
||||
doas ln -sf /usr/local/bin/python3-config /usr/local/bin/python-config
|
||||
doas ln -sf /usr/local/bin/pydoc3 /usr/local/bin/pydoc
|
||||
- build: |
|
||||
export AUTOCONF_VERSION=2.69
|
||||
export AUTOMAKE_VERSION=1.16
|
||||
|
||||
cd ~/libstrophe
|
||||
./bootstrap.sh
|
||||
./configure
|
||||
make
|
||||
doas make install
|
||||
|
||||
cd ~/profanity
|
||||
./ci-build.sh
|
||||
23
.commitlintrc.json
Normal file
23
.commitlintrc.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"extends": ["@commitlint/config-conventional"],
|
||||
"rules": {
|
||||
"type-enum": [
|
||||
2,
|
||||
"always",
|
||||
[
|
||||
"feat",
|
||||
"fix",
|
||||
"docs",
|
||||
"style",
|
||||
"refactor",
|
||||
"perf",
|
||||
"tests",
|
||||
"build",
|
||||
"ci",
|
||||
"chore",
|
||||
"cleanup"
|
||||
]
|
||||
],
|
||||
"subject-case": [0]
|
||||
}
|
||||
}
|
||||
11
.gitattributes
vendored
Normal file
11
.gitattributes
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
# Pattern Attribute
|
||||
.clang-format export-ignore
|
||||
.codespellrc export-ignore
|
||||
.commitlintrc.json export-ignore
|
||||
.git-blame-ignore-revs export-ignore
|
||||
.github/ export-ignore
|
||||
CONTRIBUTING.md export-ignore
|
||||
RELEASE_GUIDE.md export-ignore
|
||||
ci/ export-ignore
|
||||
prof.supp export-ignore
|
||||
scripts/ export-ignore
|
||||
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: feature
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
@@ -1,3 +1,12 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report
|
||||
title: ''
|
||||
labels: bug
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
<!--- Provide a general summary of the issue in the Title above -->
|
||||
|
||||
<!--- More than 50 issues open? Please don't file any new feature requests -->
|
||||
@@ -31,4 +40,3 @@
|
||||
* glib version
|
||||
* libstrophe version
|
||||
* Some bugs might be due to specific implementation in the server. `/serversoftware example.domain` can be helpful
|
||||
|
||||
44
.github/workflows/ci-api-docs.yml
vendored
Normal file
44
.github/workflows/ci-api-docs.yml
vendored
Normal file
@@ -0,0 +1,44 @@
|
||||
name: CI API Docs
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
paths:
|
||||
- 'apidocs/**'
|
||||
pull_request:
|
||||
branches: [master]
|
||||
paths:
|
||||
- 'apidocs/**'
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
test-c-api-docs:
|
||||
runs-on: ubuntu-latest
|
||||
name: Test C API Documentation Generation
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y --no-install-recommends make doxygen
|
||||
- name: Test C API docs generation
|
||||
run: |
|
||||
cd apidocs/c/
|
||||
doxygen c-prof.conf
|
||||
|
||||
test-python-api-docs:
|
||||
runs-on: ubuntu-latest
|
||||
name: Test Python API Documentation Generation
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y --no-install-recommends make python3-sphinx
|
||||
- name: Test Python API docs generation
|
||||
run: |
|
||||
cd apidocs/python/
|
||||
sphinx-apidoc -f -o . src && make -j$(nproc) xml SPHINXOPTS="-W --keep-going -n"
|
||||
@@ -1,10 +1,18 @@
|
||||
name: CI
|
||||
name: CI Code
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
paths-ignore:
|
||||
- 'docs/**'
|
||||
- 'apidocs/**'
|
||||
- 'README.md'
|
||||
pull_request:
|
||||
branches: [master]
|
||||
paths-ignore:
|
||||
- 'docs/**'
|
||||
- 'apidocs/**'
|
||||
- 'README.md'
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
@@ -21,10 +29,10 @@ jobs:
|
||||
name: Linux
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Build
|
||||
run: docker build -f ci/Dockerfile.${{ matrix.flavor }} -t profanity .
|
||||
- name: Run tests
|
||||
run: |
|
||||
docker build -f Dockerfile.${{ matrix.flavor }} -t profanity .
|
||||
docker run profanity ./ci-build.sh
|
||||
run: docker run profanity ./ci-build.sh
|
||||
|
||||
code-style:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -37,10 +45,13 @@ 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
|
||||
|
||||
- name: Install clang-format
|
||||
run: |
|
||||
@@ -83,10 +94,20 @@ jobs:
|
||||
continue-on-error: true
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: install dependencies
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo apt update
|
||||
sudo apt install -y --no-install-recommends codespell
|
||||
- name: Check spelling
|
||||
run: |
|
||||
codespell
|
||||
|
||||
coverage:
|
||||
runs-on: ubuntu-latest
|
||||
name: Code Coverage
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Build
|
||||
run: docker build -f ci/Dockerfile.arch -t profanity-cov .
|
||||
- name: Run coverage
|
||||
run: docker run profanity-cov ./ci-build.sh --coverage-only
|
||||
30
.gitignore
vendored
30
.gitignore
vendored
@@ -17,6 +17,8 @@ compile_commands.json
|
||||
.project
|
||||
.settings/
|
||||
.vscode/
|
||||
.kilo/
|
||||
.roo/
|
||||
*.plist/
|
||||
|
||||
# autotools
|
||||
@@ -62,6 +64,8 @@ tests/unittests/unittests
|
||||
tests/unittests/unittests.log
|
||||
tests/unittests/unittests.trs
|
||||
test-suite.log
|
||||
test-files/
|
||||
test-logs/
|
||||
|
||||
# valgrind output
|
||||
profval*
|
||||
@@ -107,3 +111,29 @@ breaks
|
||||
*.tar.*
|
||||
*.zip
|
||||
*.log*
|
||||
coverage/
|
||||
*.gcno
|
||||
*.gcda
|
||||
*.gcov
|
||||
coverage.info
|
||||
|
||||
# Bench harness build artefacts (sources are committed; binaries/CSVs aren't)
|
||||
tests/bench/gen_history
|
||||
tests/bench/bench_runner
|
||||
tests/bench/bench_long_messages
|
||||
tests/bench/bench_failure_modes
|
||||
tests/bench/bench_export_import
|
||||
tests/bench/*.o
|
||||
tests/bench/.deps/
|
||||
tests/bench/current.csv
|
||||
tests/bench/full.csv
|
||||
tests/bench/quick.csv
|
||||
tests/bench/longmsg.csv
|
||||
tests/bench/fail.csv
|
||||
tests/bench/multi.csv
|
||||
tests/bench/lmc.csv
|
||||
tests/bench/ooo.csv
|
||||
tests/bench/pipe.csv
|
||||
tests/bench/pipeline.csv
|
||||
tests/bench/maxpipeline.csv
|
||||
tests/bench/imp.csv
|
||||
|
||||
22
Brewfile
22
Brewfile
@@ -1,22 +0,0 @@
|
||||
brew 'autoconf'
|
||||
brew 'autoconf-archive'
|
||||
brew 'automake'
|
||||
brew 'check'
|
||||
brew 'cmocka'
|
||||
brew 'curl'
|
||||
brew 'expat'
|
||||
brew 'glib'
|
||||
brew 'gnutls'
|
||||
brew 'gpgme'
|
||||
brew 'gtk+'
|
||||
brew 'libffi'
|
||||
brew 'libotr'
|
||||
brew 'libsignal-protocol-c'
|
||||
brew 'libstrophe'
|
||||
brew 'libtool'
|
||||
brew 'ncurses'
|
||||
brew 'openssl'
|
||||
brew 'ossp-uuid'
|
||||
brew 'pkg-config'
|
||||
brew 'readline'
|
||||
brew 'sqlite'
|
||||
278
CHANGELOG
278
CHANGELOG
@@ -1,3 +1,281 @@
|
||||
cproof fork (unreleased)
|
||||
========================
|
||||
|
||||
Changes:
|
||||
- Add flat-file database backend as alternative to SQLite for message history.
|
||||
Stores messages as human-readable plain text files. Configure with `/privacy logging flatfile`.
|
||||
Files are stored in ~/.local/share/profanity/flatlog/.
|
||||
- Add `/history verify [<jid>]` command to check integrity of stored message
|
||||
history (works with both SQLite and flat-file backends).
|
||||
- Add vtable-based database backend abstraction allowing pluggable storage.
|
||||
- Add `make check-functional-flatfile` target to run functional tests with
|
||||
flat-file backend.
|
||||
- Add AI client with multi-provider chat support (OpenAI, local, etc.).
|
||||
- Consolidate logging controls; show database backend in the statusbar.
|
||||
|
||||
0.17.0 (2026-03-26)
|
||||
===================
|
||||
|
||||
Special thanks to our sponsor Matthew Fennell!
|
||||
|
||||
3 people contributed to this release: @botantony, @sjaeckel and @jubalh.
|
||||
|
||||
The last release (0.16.0) was the first that could be compiled with the Meson build system.
|
||||
It provided a tarball for autotools and a separate one for meson. The autotools tarball was done
|
||||
so that distributions don't need dependencies on automake, autoconf and libtool.
|
||||
|
||||
Several distributions are dropping vendor tarballs and use git checkouts or autogenerated tarballs nowadays.
|
||||
Meaning they need to run the above mentioned tools in the case of the autotools build anyways.
|
||||
|
||||
So this release will only be shipped with tarballs generated with meson. Profanity can still be compiled with both autotools or meson for easier adjustments. Autotols users just need to generate configure first. The next release will only use Meson. So we encourage everybody to build with Meson arleady and report any bugs they encounter.
|
||||
|
||||
We finally re-enabled functional tests (along with our existing unit tests) again!
|
||||
It would be appreciated if we could find people who want to help us expand the unit and functional tests so we are less likely to introduce regressions when we rewrite existing code.
|
||||
|
||||
Features:
|
||||
- Add OMEMO trust status indicators to the titlebar (#2104)
|
||||
- Add command autocompletion for /stamp (#2126)
|
||||
- Add jid_is_valid_user_jid() to check for valid user JIDs (#2102)
|
||||
- Add unified quality-check script and git hook support (#2099)
|
||||
- Display incoming reports (XEP-0377) (#2117)
|
||||
- Handle and allow JIDs with trailing slashes (#2102)
|
||||
- Implement color coded status messages for file transfers (#2131)
|
||||
- Implement dynamic pad resizing (#2122)
|
||||
- Improve XEP-0377 support with report-origin and third-party elements (#2117)
|
||||
- Improve feedback during OMEMO session Initiation (#2104)
|
||||
- Improve validation of JIDs (#2102)
|
||||
- Notify users when new OMEMO devices or fingerprints are discovered (#2104)
|
||||
- Only request receipts when supported (#2101)
|
||||
- Provide descriptive fallback messages for OMEMO decryption failures (#2104)
|
||||
- Provide detailed encryption failure messages for OMEMO (#2104)
|
||||
- Sanitize illegal XML characters from outgoing messages (#2116)
|
||||
- Show active and trust status in /omemo fingerprint (#2104)
|
||||
- Support UTF-8 characters in autocompletion (#2115)
|
||||
- Suppress "new device" alerts for already known devices (#2113)
|
||||
- Suppress redundant `session already exists` messages (#2113)
|
||||
- Suppress repetitive missing device ID warnings (#2113)
|
||||
- Introduce `prof_date_time_format_iso8601()`. (#2112)
|
||||
- Optimize random data generation. (#2109)
|
||||
|
||||
Bug Fixes:
|
||||
- Allow adding own JID to roster (#2127)
|
||||
- Check for necessary pointers in omemo_receive_message
|
||||
- Correct handling of create_fulljid without resource (#2102)
|
||||
- Database return NULL if no history limits are found (#2102)
|
||||
- Define `prefs_changes_print` outside of `prefs_changes` (#2090)
|
||||
- Dont OMEMO trust check so often (#2108)
|
||||
- Fix -Wanalyzer-deref-before-check warning in cmd_ac_complete (#2095)
|
||||
- Fix Floating Point Exception in OMEMO session building (#2096)
|
||||
- Fix NULL dereference and memory leak in _mam_rsm_id_handler (#2094)
|
||||
- Fix NULL dereference and memory leaks in OX logic (#2094)
|
||||
- Fix api_get_current_occupants() memory problems (#2094)
|
||||
- Fix custom outgoing stamp and fix stamp command help (#2126)
|
||||
- Fix file autocompletion bugs and restore cycling (#2100)
|
||||
- Fix memory leak and potential crash in iq_id_handler_add (#2094)
|
||||
- Fix not saving first created account (#2107)
|
||||
- Fix rare memleak (#2123)
|
||||
- Fix redundant error reporting in http download (#2094)
|
||||
- Fix usage error in /time command (#2101)
|
||||
- Handle potential NULL from malloc in OMEMO fingerprint decoding (#2094)
|
||||
- Ignore self-presence for untracked MUC rooms (#2097)
|
||||
- Improve status reporting and filename handling for /url save (#2131)
|
||||
- Incorrect omemo decryption error for Key Transport Messages (#2130)
|
||||
- Increase max arguments for /connect to 9 (#2101)
|
||||
- Initialize OMEMO pointers and add NULL checks (#2094)
|
||||
- Missing plugins_post_chat_message_display calls (#2118)
|
||||
- Only allow /msg with valid JIDs (#2102)
|
||||
- Sanitize account names (#2105)
|
||||
- Segfault when using /command help (#2094)
|
||||
- Use <body> tag for spam reporting (XEP-0377) (#2117)
|
||||
- Fix -Wanalyzer-deref-before-check warning in get_message_from_editor (#2095)
|
||||
- Fix potential double free. (#2112)
|
||||
- Fix potential segfault. (#2109)
|
||||
- Flush OMEMO store only after encrypting for all recipients. (#2109)
|
||||
- Check connection state before accepting command. (#2124)
|
||||
|
||||
Documentation:
|
||||
- Add Conventional Commit Structure
|
||||
- Add a section explaining how to write functional tests (#2101)
|
||||
- Add information about footers in git commit messages
|
||||
- Add note about sanitizers overhead (#2101)
|
||||
- Add section explaining how to run functional tests (#2101)
|
||||
- Expand the build section in CONTRIBUTING.md (#2099)
|
||||
- Explain how to turn clang-format of for some blocks of code (#2099)
|
||||
- Explain how to write unit tests (#2099)
|
||||
- Improve help for /blocked (#2117)
|
||||
- Mention generated html pages in release guide
|
||||
- Sort entries in theme_template
|
||||
- Update implemented XEPs
|
||||
- Update theme_template with missing options
|
||||
|
||||
Cleanup:
|
||||
- Adapt loop counter to proper type (#2089)
|
||||
- Adapt type and cast to get ride of warnings (#2089)
|
||||
- Add a defensive check in cmd_process_input() (#2094)
|
||||
- Be a bit more defensive in (#2094)
|
||||
- Be a bit more defensive in server_events.c (#2094)
|
||||
- Cast device ids in omemo.c (#2089)
|
||||
- Cast g_hash_table_lookup return to function pointer (#2089)
|
||||
- Cast to get rid of warnings (#2089)
|
||||
- Check for strdup() success in stanza_create_http_upload_request() (#2094)
|
||||
- Cleanup log module (#2094)
|
||||
- Cleanup types (#2104)
|
||||
- Correct comparison in cons_show_wins() (#2089)
|
||||
- Fix cast function type warnings in pgp.c (#2089)
|
||||
- Fix potential NULL dereference and leaks in cmd_sendfile (#2094)
|
||||
- Fix potential NULL dereference in cmd_omemo_(un)trust (#2094)
|
||||
- Fix potential null pointer dereference (#2094)
|
||||
- Fix uninitialized field in color_distance() (#2089)
|
||||
- Initialize GOptionEntry entries correctly (#2089)
|
||||
- Initialize optional fields in profModule (#2089)
|
||||
- Initialize waittime to 0 (#2094)
|
||||
- Make _connection_handler() safer (#2094)
|
||||
- Make muc_nick() return const char* (#2089)
|
||||
- Map test files according to structure in src (#2099)
|
||||
- Move loop var from int to size_t in cmd_funcs.c (#2089)
|
||||
- Move loop var from int to size_t in omemo.c (#2089)
|
||||
- Move more functions from int to guint in statusbar.c (#2089)
|
||||
- Move some variables from int to guint in statusbar.c (#2089)
|
||||
- Refactor account_eval_password to use glib (#2094)
|
||||
- Use auto_FILE and handle fopen failure in command_docgen (#2094)
|
||||
- Use g_malloc and auto_gchar in _win_print_wrapped (#2094)
|
||||
- Use g_new() instead of malloc in prof_add_shutdown_routine() (#2094)
|
||||
- Use g_new0 and g_strdup for alias allocation (#2094)
|
||||
- Use g_new0 instead of malloc in buffer_get_entry_by_id() (#2094)
|
||||
- Wrap release handler in iq.c (#2089)
|
||||
- The buffer API should use `unsigned` types. (#2112)
|
||||
|
||||
Tests:
|
||||
- Add prof_send_raw (#2115)
|
||||
- Add test for get_expanded_path() (#2093)
|
||||
- Add test for get_mentions() (#2093)
|
||||
- Add test for release_is_new() (#2093)
|
||||
- Add test for string_to_verbosity() (#2093)
|
||||
- Add test for tab autocompletion of /msg (#2115)
|
||||
- Add test for valid_tls_policy_option() (#2093)
|
||||
- Add tests for string_matches_one_of() (#2093)
|
||||
- Add tests for strtoi_range() (#2093)
|
||||
- Apply coding style to unit tests (#2099)
|
||||
- Check for binary before starting functional tests
|
||||
- Don't build functional test with analyzer and increase timeouts (#2101)
|
||||
- Fix functional presence tests (#2101)
|
||||
- Fix functional test connect_shows_presence_updates (#2101)
|
||||
- Fix functional test message_receive_chatwin (#2101)
|
||||
- Fix functional test message_send (#2101)
|
||||
- Fix functional test ping_jid (#2101)
|
||||
- Fix functional test ping_responds_to_server_request (#2101)
|
||||
- Fix functional test receive_carbon (#2101)
|
||||
- Fix functional test resets_to_barejid_after_presence_received (#2101)
|
||||
- Fix functional test rooms_query (#2101)
|
||||
- Fix functional test send_receipt_on_request (#2101)
|
||||
- Fix functional test sends_room_join (#2101)
|
||||
- Fix functional test test_ping (#2101)
|
||||
- Fix passing of special characters to prof_expect() (#2101)
|
||||
- Improve autocomplete unit tests (#2093)
|
||||
- Re-enable functional tests (#2101)
|
||||
- Remove apparently unused code (#2101)
|
||||
- Standardize naming convention (#2099)
|
||||
- Try two places for profanity binary (#2101)
|
||||
- Update unit tests (#2094)
|
||||
|
||||
Build System:
|
||||
- Add --enable-functional-tests switch (#2123)
|
||||
- Add -Wpointer-arith flag (#2095)
|
||||
- Add -fstack-protector-strong compiler flag (#2094)
|
||||
- Add .gitattributes to exclude development files from exports (#2123)
|
||||
- Add support for libomemo-c as OMEMO backend (#2120)
|
||||
- Enable -fanalyzer for static analysis (#2094)
|
||||
- Enable _FORTIFY_SOURCE and -Og optimization (#2094)
|
||||
- Enable more warnings in debug mode (#2089)
|
||||
- Set -Qunused-arguments depending on compiler not os
|
||||
|
||||
Refactorings:
|
||||
- Centralize validation within jid_is_valid (#2102)
|
||||
- Change vcard_print() checks (#2094)
|
||||
- Fix clang-format expansion of table (#2099)
|
||||
- Improve key transport message handling and deduplicate errors (#2130)
|
||||
- Make Jid use glib functions (#2094)
|
||||
- Make Resource use glib functions (#2094)
|
||||
- Make _writecsv safer and with better performance (#2094)
|
||||
- Modernize cmd_ac_complete_filepath and simplify path handling (#2094)
|
||||
- Move pgp module to gchar (#2094)
|
||||
- Optimize prof_occurrences() (#2093)
|
||||
- Partly move plugins to glib (#2094)
|
||||
- Refactor OMEMO error handling and add non-null attributes (#2104)
|
||||
- Refactor form_set_value to use glib list management (#2095)
|
||||
- Replace calloc with g_new0 for struct allocations (#2094)
|
||||
- Replace malloc with g_new0 in many occasions (#2094)
|
||||
- Roster export uses GString and g_file_set_contents now (#2094)
|
||||
- Start to standardize on gchar (#2094)
|
||||
- Use g_malloc0 instead of calloc in get_random_string() (#2094)
|
||||
- Use g_new in message_pubsub_event_handler_add() (#2094)
|
||||
- Use p_contact_new instead of malloc in p_contact_new() (#2094)
|
||||
|
||||
Chores:
|
||||
- Add 0.16.0 to release in doap file
|
||||
- Add README explaining the purpose and usage of helper scripts (#2128)
|
||||
- Add XEP comparison script
|
||||
- Add changelog-helper script (#2128)
|
||||
- Add copyright header to C files who had none
|
||||
- Add linter file to be ignored upon export
|
||||
- Make meson doublecheck only available if the script exists
|
||||
- Move to SPDX license header
|
||||
- Require DCO from now on (#2123)
|
||||
- Unify build configuration script into one (#2128)
|
||||
- Update clang-format action to 4.16.0
|
||||
- Update valgrind suppression file (#2123)
|
||||
- Use more descriptive names for helper scripts (#2128)
|
||||
|
||||
0.16.0 (2026-02-23)
|
||||
===================
|
||||
|
||||
5 people contributed to this release: @balejk, @mcalierno, @ritesh006, @sjaeckel and @jubalh.
|
||||
|
||||
Thanks a lot to our sponsors: Matthew Fennell, Martin Dosch and one anonymous sponsor.
|
||||
If you want to support us too: https://profanity-im.github.io/donate.html
|
||||
|
||||
This release depends on libstrophe >= 0.12.3.
|
||||
|
||||
Changes:
|
||||
- Add new command /changes (#2080)
|
||||
With that command one can see the modifications of the runtime
|
||||
configuration vs. the saved configuration.
|
||||
- Explain the different kinds of messages in the manpage (#2063)
|
||||
- Fix OTR detection (#1957, #2072)
|
||||
- Fix OMEMO startup (79ff9ba)
|
||||
- Fix overwriting new accounts when running multiple instances (#2080)
|
||||
- Fix reconnect when no account has been set up yet (#2080)
|
||||
- Don't publish keys if the server doesn't support pubsub (#2078, #2080)
|
||||
- Fix compilation on Apple silicon macs (#2075)
|
||||
- Handle SIGTERM and SIGHUP (#2082)
|
||||
- Cleanup (#2067, #2080)
|
||||
- Add cygwin CI (#2066)
|
||||
- Replace ACX_PTHREAD with AX_PTHREAD (#2062)
|
||||
- Add meson build system (#2086)
|
||||
Mainly for testing purposes. Details will be announced in a later release.
|
||||
|
||||
0.15.1 (2025-08-22)
|
||||
===================
|
||||
|
||||
5 people contributed to this release: @andreasstieger, @killerdevildog, @mdosch,
|
||||
@sjaeckel and @jubalh.
|
||||
|
||||
Thanks a lot to our sponsors: Matthew Fennell, Martin Dosch and one anonymous sponsor.
|
||||
If you want to support us too: https://profanity-im.github.io/donate.html
|
||||
|
||||
This release depends on libstrophe >= 0.12.3.
|
||||
|
||||
Changes:
|
||||
- Add `iso8601` as valid time format
|
||||
`/time all set iso8601` instead of manual specification
|
||||
- Fix ignoring of roster pushes (#2035)
|
||||
- Print location of decrypted files (#2041)
|
||||
- Fix GPGME >= 2.0.0 compatibility issue (#2048)
|
||||
- Fix tests with gcc15 and uintptr_t (#2055)
|
||||
- Reduce noise in log files (#1911, #2060)
|
||||
- Cleanup, code improvement and memory fixes (#2033, #2041, #2053)
|
||||
- Improve documentation (#2040, 18f157b, #2058)
|
||||
|
||||
0.15.0 (2025-03-27)
|
||||
===================
|
||||
|
||||
|
||||
@@ -90,6 +90,24 @@ set -e
|
||||
```
|
||||
|
||||
This will run the same tests that the CI runs and refuse the push if it fails.
|
||||
The CI script runs 4 parallel builds with different configurations:
|
||||
- **Full** — all features enabled (+ coverage in `--coverage-only` mode)
|
||||
- **Minimal** — all optional features disabled
|
||||
- **NoEncrypt** — no encryption (OTR, PGP, OMEMO disabled)
|
||||
- **Default** — default ./configure options
|
||||
|
||||
Each build runs Valgrind and functional tests on Linux.
|
||||
Use `./ci-build.sh --coverage-only` to run only the Full build with coverage collection.
|
||||
|
||||
Output shows test results per build:
|
||||
```
|
||||
✓ Full PASSED
|
||||
Unit tests: 437 passed, 0 failed
|
||||
Functional tests: 69 passed, 0 failed
|
||||
Coverage: Lines: 27.5% | Functions: 36.2% | Branches: 18.1%
|
||||
Duration: 5m39s
|
||||
```
|
||||
|
||||
Note that it will run on the actual content of the repository directory and not
|
||||
what may have been staged/committed.
|
||||
|
||||
@@ -105,6 +123,20 @@ Test your changes with the following tools to find mistakes.
|
||||
|
||||
Run `make check` to run the unit tests with your current configuration or `./ci-build.sh` to check with different switches passed to configure.
|
||||
|
||||
### flat-file backend tests
|
||||
|
||||
To run functional tests with the flat-file database backend (instead of SQLite):
|
||||
|
||||
```bash
|
||||
make check-functional-flatfile
|
||||
```
|
||||
|
||||
Or manually for a single group:
|
||||
|
||||
```bash
|
||||
PROF_FLATFILE=1 PROF_TEST_GROUP=1 ./tests/functionaltests/functionaltests 1
|
||||
```
|
||||
|
||||
### valgrind
|
||||
We provide a suppressions file `prof.supp`. It is a combination of the suppressions for shipped with glib2, python and custom rules.
|
||||
|
||||
@@ -126,6 +158,16 @@ scan-build make
|
||||
scan-view ...
|
||||
```
|
||||
|
||||
### Security checks
|
||||
|
||||
We have a static analyzer `check-cwe134.sh` that detects CWE-134 format string vulnerabilities. It runs automatically in CI but you can also run it locally:
|
||||
|
||||
```bash
|
||||
./check-cwe134.sh
|
||||
```
|
||||
|
||||
This checks for unsafe patterns where data could be passed directly as a format string to functions like `printf`, `cons_show`, etc. Never pass a raw string for formatting; use `"%s"` format specifier instead.
|
||||
|
||||
### Finding typos
|
||||
|
||||
We include a `.codespellrc` configuration file for `codespell` in the root directory.
|
||||
@@ -137,3 +179,55 @@ You can run the `make spell` command for this.
|
||||
|
||||
`make doublecheck` will run the code formatter, spell checker and unit tests.
|
||||
|
||||
|
||||
### Functional tests
|
||||
|
||||
The functional test suite uses [stabber](https://git.jabber.space/devs/stabber) as a mock XMPP server. Tests are located in `tests/functionaltests/`.
|
||||
|
||||
#### Running functional tests
|
||||
|
||||
Functional tests require stabber to be installed. Once installed, tests run as part of `make check`:
|
||||
|
||||
```bash
|
||||
make check # Run all tests (unit + functional)
|
||||
make check-functional-parallel # Run functional tests in parallel (~3x faster)
|
||||
./tests/functionaltests/functionaltests # Run all functional tests sequentially
|
||||
./tests/functionaltests/functionaltests 1 # Run specific group (1-4)
|
||||
```
|
||||
|
||||
#### Test groups
|
||||
|
||||
Tests are organized into 4 groups for parallel execution:
|
||||
|
||||
| Group | Description |
|
||||
|-------|-------------|
|
||||
| 1 | Connect, Ping, Rooms, Software |
|
||||
| 2 | Message, Receipts, Roster, Chat Session |
|
||||
| 3 | Presence, Disconnect |
|
||||
| 4 | MUC, Carbons |
|
||||
|
||||
To add a new group:
|
||||
1. Define the test array in `functionaltests.c`
|
||||
2. Add entry to `groups[]` array
|
||||
3. Update `FUNC_TEST_GROUPS` in `Makefile.am`
|
||||
|
||||
#### Writing functional tests
|
||||
|
||||
Use content-based stubbing with stabber:
|
||||
|
||||
```c
|
||||
// Use stbbr_for_query for IQ queries (roster, disco, etc.)
|
||||
stbbr_for_query("jabber:iq:roster", "<iq type='result'>...</iq>");
|
||||
|
||||
// Use stbbr_send for presence, message, and push-style stanzas
|
||||
stbbr_send("<presence from='buddy@localhost'>...</presence>");
|
||||
```
|
||||
|
||||
Guidelines:
|
||||
|
||||
1. Use `stbbr_for_query(namespace, xml)` for IQ queries where the namespace is stable.
|
||||
2. Use `stbbr_send(xml)` for presence, message, and other push-style stanzas.
|
||||
3. Keep assertions tolerant of ordering when possible; use `prof_output_regex()` for flexible matching.
|
||||
4. If timing issues appear, use `prof_timeout()` around critical expectations and reset afterwards.
|
||||
5. When adding new tests, place them in the appropriate group based on functionality.
|
||||
|
||||
|
||||
417
Makefile.am
417
Makefile.am
@@ -3,6 +3,10 @@ core_sources = \
|
||||
src/log.c src/common.c \
|
||||
src/chatlog.c src/chatlog.h \
|
||||
src/database.h src/database.c \
|
||||
src/database_flatfile.c src/database_flatfile.h \
|
||||
src/database_flatfile_parser.c \
|
||||
src/database_flatfile_verify.c \
|
||||
src/database_export.c \
|
||||
src/log.h src/profanity.c src/common.h \
|
||||
src/profanity.h src/xmpp/chat_session.c \
|
||||
src/xmpp/chat_session.h src/xmpp/muc.c src/xmpp/muc.h src/xmpp/jid.h src/xmpp/jid.c \
|
||||
@@ -33,6 +37,7 @@ core_sources = \
|
||||
src/ui/window_list.c src/ui/window_list.h \
|
||||
src/ui/rosterwin.c src/ui/occupantswin.c \
|
||||
src/ui/buffer.c src/ui/buffer.h \
|
||||
src/ui/aiwin.c \
|
||||
src/ui/chatwin.c \
|
||||
src/ui/mucwin.c \
|
||||
src/ui/privwin.c \
|
||||
@@ -74,7 +79,8 @@ core_sources = \
|
||||
src/plugins/themes.c src/plugins/themes.h \
|
||||
src/plugins/settings.c src/plugins/settings.h \
|
||||
src/plugins/disco.c src/plugins/disco.h \
|
||||
src/ui/tray.h src/ui/tray.c
|
||||
src/ui/tray.h src/ui/tray.c \
|
||||
src/ai/ai_client.h src/ai/ai_client.c
|
||||
|
||||
unittest_sources = \
|
||||
src/xmpp/contact.c src/xmpp/contact.h src/common.c \
|
||||
@@ -85,6 +91,7 @@ unittest_sources = \
|
||||
src/xmpp/chat_state.h src/xmpp/chat_state.c \
|
||||
src/xmpp/roster_list.c src/xmpp/roster_list.h \
|
||||
src/xmpp/xmpp.h src/xmpp/form.c \
|
||||
src/ai/ai_client.h src/ai/ai_client.c \
|
||||
src/ui/ui.h \
|
||||
src/otr/otr.h \
|
||||
src/pgp/gpg.h \
|
||||
@@ -93,6 +100,8 @@ unittest_sources = \
|
||||
src/omemo/crypto.h \
|
||||
src/omemo/store.h \
|
||||
src/xmpp/vcard.h src/xmpp/vcard_funcs.h \
|
||||
src/database_flatfile.h \
|
||||
src/database_flatfile_parser.c \
|
||||
src/command/cmd_defs.h src/command/cmd_defs.c \
|
||||
src/command/cmd_funcs.h src/command/cmd_funcs.c \
|
||||
src/command/cmd_ac.h src/command/cmd_ac.c \
|
||||
@@ -124,6 +133,7 @@ unittest_sources = \
|
||||
src/event/server_events.c src/event/server_events.h \
|
||||
src/event/client_events.c src/event/client_events.h \
|
||||
src/ui/tray.h src/ui/tray.c \
|
||||
tests/prof_cmocka.h \
|
||||
tests/unittests/xmpp/stub_vcard.c \
|
||||
tests/unittests/xmpp/stub_avatar.c \
|
||||
tests/unittests/xmpp/stub_ox.c \
|
||||
@@ -131,6 +141,7 @@ unittest_sources = \
|
||||
tests/unittests/xmpp/stub_message.c \
|
||||
tests/unittests/ui/stub_ui.c tests/unittests/ui/stub_ui.h \
|
||||
tests/unittests/ui/stub_vcardwin.c \
|
||||
tests/unittests/ui/stub_ai.c \
|
||||
tests/unittests/log/stub_log.c \
|
||||
tests/unittests/chatlog/stub_chatlog.c \
|
||||
tests/unittests/database/stub_database.c \
|
||||
@@ -141,34 +152,40 @@ unittest_sources = \
|
||||
tests/unittests/tools/stub_aesgcm_download.c \
|
||||
tests/unittests/tools/stub_plugin_download.c \
|
||||
tests/unittests/helpers.c tests/unittests/helpers.h \
|
||||
tests/unittests/test_form.c tests/unittests/test_form.h \
|
||||
tests/unittests/xmpp/test_form.c tests/unittests/xmpp/test_form.h \
|
||||
tests/unittests/test_common.c tests/unittests/test_common.h \
|
||||
tests/unittests/test_autocomplete.c tests/unittests/test_autocomplete.h \
|
||||
tests/unittests/test_jid.c tests/unittests/test_jid.h \
|
||||
tests/unittests/test_parser.c tests/unittests/test_parser.h \
|
||||
tests/unittests/test_roster_list.c tests/unittests/test_roster_list.h \
|
||||
tests/unittests/test_chat_session.c tests/unittests/test_chat_session.h \
|
||||
tests/unittests/test_contact.c tests/unittests/test_contact.h \
|
||||
tests/unittests/test_preferences.c tests/unittests/test_preferences.h \
|
||||
tests/unittests/test_server_events.c tests/unittests/test_server_events.h \
|
||||
tests/unittests/test_muc.c tests/unittests/test_muc.h \
|
||||
tests/unittests/test_cmd_presence.c tests/unittests/test_cmd_presence.h \
|
||||
tests/unittests/test_cmd_alias.c tests/unittests/test_cmd_alias.h \
|
||||
tests/unittests/test_cmd_connect.c tests/unittests/test_cmd_connect.h \
|
||||
tests/unittests/test_cmd_rooms.c tests/unittests/test_cmd_rooms.h \
|
||||
tests/unittests/test_cmd_account.c tests/unittests/test_cmd_account.h \
|
||||
tests/unittests/test_cmd_sub.c tests/unittests/test_cmd_sub.h \
|
||||
tests/unittests/test_cmd_bookmark.c tests/unittests/test_cmd_bookmark.h \
|
||||
tests/unittests/test_cmd_otr.c tests/unittests/test_cmd_otr.h \
|
||||
tests/unittests/test_cmd_pgp.c tests/unittests/test_cmd_pgp.h \
|
||||
tests/unittests/test_cmd_join.c tests/unittests/test_cmd_join.h \
|
||||
tests/unittests/test_cmd_roster.c tests/unittests/test_cmd_roster.h \
|
||||
tests/unittests/test_cmd_disconnect.c tests/unittests/test_cmd_disconnect.h \
|
||||
tests/unittests/test_callbacks.c tests/unittests/test_callbacks.h \
|
||||
tests/unittests/test_plugins_disco.c tests/unittests/test_plugins_disco.h \
|
||||
tests/unittests/tools/test_autocomplete.c tests/unittests/tools/test_autocomplete.h \
|
||||
tests/unittests/xmpp/test_jid.c tests/unittests/xmpp/test_jid.h \
|
||||
tests/unittests/tools/test_parser.c tests/unittests/tools/test_parser.h \
|
||||
tests/unittests/xmpp/test_roster_list.c tests/unittests/xmpp/test_roster_list.h \
|
||||
tests/unittests/xmpp/test_chat_session.c tests/unittests/xmpp/test_chat_session.h \
|
||||
tests/unittests/xmpp/test_contact.c tests/unittests/xmpp/test_contact.h \
|
||||
tests/unittests/config/test_preferences.c tests/unittests/config/test_preferences.h \
|
||||
tests/unittests/event/test_server_events.c tests/unittests/event/test_server_events.h \
|
||||
tests/unittests/xmpp/test_muc.c tests/unittests/xmpp/test_muc.h \
|
||||
tests/unittests/command/test_cmd_presence.c tests/unittests/command/test_cmd_presence.h \
|
||||
tests/unittests/command/test_cmd_alias.c tests/unittests/command/test_cmd_alias.h \
|
||||
tests/unittests/command/test_cmd_connect.c tests/unittests/command/test_cmd_connect.h \
|
||||
tests/unittests/command/test_cmd_rooms.c tests/unittests/command/test_cmd_rooms.h \
|
||||
tests/unittests/command/test_cmd_account.c tests/unittests/command/test_cmd_account.h \
|
||||
tests/unittests/command/test_cmd_sub.c tests/unittests/command/test_cmd_sub.h \
|
||||
tests/unittests/command/test_cmd_bookmark.c tests/unittests/command/test_cmd_bookmark.h \
|
||||
tests/unittests/command/test_cmd_otr.c tests/unittests/command/test_cmd_otr.h \
|
||||
tests/unittests/command/test_cmd_pgp.c tests/unittests/command/test_cmd_pgp.h \
|
||||
tests/unittests/command/test_cmd_join.c tests/unittests/command/test_cmd_join.h \
|
||||
tests/unittests/command/test_cmd_roster.c tests/unittests/command/test_cmd_roster.h \
|
||||
tests/unittests/command/test_cmd_disconnect.c tests/unittests/command/test_cmd_disconnect.h \
|
||||
tests/unittests/command/test_cmd_ac.c tests/unittests/command/test_cmd_ac.h \
|
||||
tests/unittests/plugins/test_callbacks.c tests/unittests/plugins/test_callbacks.h \
|
||||
tests/unittests/plugins/test_plugins_disco.c tests/unittests/plugins/test_plugins_disco.h \
|
||||
tests/unittests/test_forced_encryption.c tests/unittests/test_forced_encryption.h \
|
||||
tests/unittests/test_ai_client.c tests/unittests/test_ai_client.h \
|
||||
tests/unittests/test_database_export.c tests/unittests/test_database_export.h \
|
||||
tests/unittests/test_database_stress.c tests/unittests/test_database_stress.h \
|
||||
tests/unittests/unittests.c
|
||||
|
||||
functionaltest_sources = \
|
||||
tests/prof_cmocka.h \
|
||||
tests/functionaltests/proftest.c tests/functionaltests/proftest.h \
|
||||
tests/functionaltests/test_connect.c tests/functionaltests/test_connect.h \
|
||||
tests/functionaltests/test_ping.c tests/functionaltests/test_ping.h \
|
||||
@@ -182,6 +199,12 @@ functionaltest_sources = \
|
||||
tests/functionaltests/test_software.c tests/functionaltests/test_software.h \
|
||||
tests/functionaltests/test_muc.c tests/functionaltests/test_muc.h \
|
||||
tests/functionaltests/test_disconnect.c tests/functionaltests/test_disconnect.h \
|
||||
tests/functionaltests/test_history.c tests/functionaltests/test_history.h \
|
||||
tests/functionaltests/test_lastactivity.c tests/functionaltests/test_lastactivity.h \
|
||||
tests/functionaltests/test_autoping.c tests/functionaltests/test_autoping.h \
|
||||
tests/functionaltests/test_disco.c tests/functionaltests/test_disco.h \
|
||||
tests/functionaltests/test_export_import.c tests/functionaltests/test_export_import.h \
|
||||
tests/functionaltests/test_ai.c tests/functionaltests/test_ai.h \
|
||||
tests/functionaltests/functionaltests.c
|
||||
|
||||
main_source = src/main.c
|
||||
@@ -251,6 +274,12 @@ core_sources += $(omemo_sources)
|
||||
unittest_sources += $(omemo_unittest_sources)
|
||||
endif
|
||||
|
||||
sqlite_sources = src/database_sqlite.c
|
||||
|
||||
if BUILD_SQLITE
|
||||
core_sources += $(sqlite_sources)
|
||||
endif
|
||||
|
||||
all_c_sources = $(core_sources) $(unittest_sources) \
|
||||
$(pgp_sources) $(pgp_unittest_sources) \
|
||||
$(otr4_sources) $(otr_unittest_sources) \
|
||||
@@ -283,27 +312,298 @@ endif
|
||||
|
||||
TESTS = tests/unittests/unittests
|
||||
check_PROGRAMS = tests/unittests/unittests
|
||||
tests_unittests_unittests_CPPFLAGS = -I$(srcdir)/tests -I$(srcdir)/tests/unittests
|
||||
tests_unittests_unittests_SOURCES = $(unittest_sources)
|
||||
tests_unittests_unittests_LDADD = -lcmocka
|
||||
|
||||
# Functional test were commented out because of:
|
||||
# https://github.com/profanity-im/profanity/pull/1010
|
||||
# An issue was raised for stabber:
|
||||
# https://github.com/profanity-im/stabber/issues/5
|
||||
# Once this issue is resolved functional tests should be enabled again
|
||||
# Functional tests require libstabber.
|
||||
# They are only built when libstabber is available.
|
||||
# See: https://github.com/profanity-im/profanity/pull/1010
|
||||
# https://github.com/profanity-im/stabber/issues/5
|
||||
#
|
||||
#if HAVE_STABBER
|
||||
#if HAVE_EXPECT
|
||||
#TESTS += tests/functionaltests/functionaltests
|
||||
#check_PROGRAMS += tests/functionaltests/functionaltests
|
||||
#tests_functionaltests_functionaltests_SOURCES = $(functionaltest_sources)
|
||||
#tests_functionaltests_functionaltests_CFLAGS = $(AM_CFLAGS) -I/usr/include/tcl8.6 -I/usr/include/tcl8.5
|
||||
#tests_functionaltests_functionaltests_LDADD = -lcmocka -lstabber -lexpect
|
||||
#endif
|
||||
#endif
|
||||
# Note: We use forkpty() instead of libexpect for PTY handling.
|
||||
|
||||
if HAVE_STABBER
|
||||
if HAVE_FORKPTY
|
||||
TESTS += tests/functionaltests/functionaltests
|
||||
check_PROGRAMS += tests/functionaltests/functionaltests
|
||||
tests_functionaltests_functionaltests_SOURCES = $(functionaltest_sources)
|
||||
tests_functionaltests_functionaltests_CPPFLAGS = -I$(srcdir)/tests
|
||||
tests_functionaltests_functionaltests_CFLAGS = $(AM_CFLAGS)
|
||||
tests_functionaltests_functionaltests_LDADD = -lcmocka -lstabber @FORKPTY_LIB@
|
||||
|
||||
# Parallel functional tests target (~3x faster than sequential)
|
||||
# Usage: make check-functional-parallel
|
||||
# To add more groups: increase FUNC_TEST_GROUPS and add group in functionaltests.c
|
||||
FUNC_TEST_GROUPS = 1 2 3 4
|
||||
|
||||
check-functional-parallel: tests/functionaltests/functionaltests
|
||||
@echo "Running functional tests in parallel ($(words $(FUNC_TEST_GROUPS)) groups)..."
|
||||
@mkdir -p $(builddir)/test-logs $(builddir)/test-files
|
||||
@pids=""; \
|
||||
for g in $(FUNC_TEST_GROUPS); do \
|
||||
./tests/functionaltests/functionaltests $$g > $(builddir)/test-logs/group$$g.log 2>&1 & \
|
||||
pids="$$pids $$!"; \
|
||||
done; \
|
||||
failed=0; i=1; \
|
||||
for pid in $$pids; do \
|
||||
wait $$pid || { echo "Group $$i FAILED"; cat $(builddir)/test-logs/group$$i.log; failed=1; }; \
|
||||
i=$$((i + 1)); \
|
||||
done; \
|
||||
echo "=== Test Results Summary ==="; \
|
||||
grep -E 'PASSED|FAILED|Running' $(builddir)/test-logs/group*.log || true; \
|
||||
if [ $$failed -ne 0 ]; then echo "FUNCTIONAL TESTS FAILED"; exit 1; fi; \
|
||||
echo "All functional test groups passed!"
|
||||
|
||||
# Run functional tests with the flat-file database backend
|
||||
# Usage: make check-functional-flatfile
|
||||
check-functional-flatfile: tests/functionaltests/functionaltests
|
||||
@echo "Running functional tests with flat-file backend ($(words $(FUNC_TEST_GROUPS)) groups)..."
|
||||
@mkdir -p $(builddir)/test-logs $(builddir)/test-files
|
||||
@pids=""; \
|
||||
for g in $(FUNC_TEST_GROUPS); do \
|
||||
PROF_FLATFILE=1 ./tests/functionaltests/functionaltests $$g > $(builddir)/test-logs/group$$g-flatfile.log 2>&1 & \
|
||||
pids="$$pids $$!"; \
|
||||
done; \
|
||||
failed=0; i=1; \
|
||||
for pid in $$pids; do \
|
||||
wait $$pid || { echo "Group $$i FAILED (flatfile)"; cat $(builddir)/test-logs/group$$i-flatfile.log; failed=1; }; \
|
||||
i=$$((i + 1)); \
|
||||
done; \
|
||||
echo "=== Flat-file Test Results Summary ==="; \
|
||||
grep -E 'PASSED|FAILED|Running' $(builddir)/test-logs/group*-flatfile.log || true; \
|
||||
if [ $$failed -ne 0 ]; then echo "FLAT-FILE FUNCTIONAL TESTS FAILED"; exit 1; fi; \
|
||||
echo "All flat-file functional test groups passed!"
|
||||
endif
|
||||
endif
|
||||
|
||||
man1_MANS = $(man1_sources)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Bench harness — synthetic database load tests. Not part of `make check`.
|
||||
# Build only when invoked via `make bench` / `make bench-quick`.
|
||||
# Sources live in tests/bench/. See tests/bench/README.md.
|
||||
|
||||
bench_common_sources = \
|
||||
tests/bench/bench_stubs.c \
|
||||
tests/bench/bench_common.c tests/bench/bench_common.h \
|
||||
tests/bench/bench_csv.c tests/bench/bench_csv.h \
|
||||
src/database_flatfile.c src/database_flatfile.h \
|
||||
src/database_flatfile_parser.c \
|
||||
src/database_flatfile_verify.c \
|
||||
src/common.c src/common.h
|
||||
|
||||
# Sources needed only by the export/import bench (S7/S8). Pulls in the SQLite
|
||||
# backend, the cross-backend export/import code, and the dispatcher.
|
||||
bench_export_sources = \
|
||||
src/database.c src/database.h \
|
||||
src/database_sqlite.c \
|
||||
src/database_export.c
|
||||
|
||||
EXTRA_PROGRAMS = \
|
||||
tests/bench/gen_history \
|
||||
tests/bench/bench_runner \
|
||||
tests/bench/bench_long_messages \
|
||||
tests/bench/bench_failure_modes \
|
||||
tests/bench/bench_export_import
|
||||
|
||||
tests_bench_gen_history_SOURCES = tests/bench/gen_history.c $(bench_common_sources)
|
||||
tests_bench_gen_history_CPPFLAGS = -I$(srcdir)/src -I$(srcdir)/tests/bench
|
||||
tests_bench_gen_history_CFLAGS = $(AM_CFLAGS)
|
||||
tests_bench_gen_history_LDADD =
|
||||
|
||||
tests_bench_bench_runner_SOURCES = tests/bench/bench_runner.c $(bench_common_sources)
|
||||
tests_bench_bench_runner_CPPFLAGS = -I$(srcdir)/src -I$(srcdir)/tests/bench
|
||||
tests_bench_bench_runner_CFLAGS = $(AM_CFLAGS)
|
||||
tests_bench_bench_runner_LDADD =
|
||||
|
||||
tests_bench_bench_long_messages_SOURCES = tests/bench/bench_long_messages.c $(bench_common_sources)
|
||||
tests_bench_bench_long_messages_CPPFLAGS = -I$(srcdir)/src -I$(srcdir)/tests/bench
|
||||
tests_bench_bench_long_messages_CFLAGS = $(AM_CFLAGS)
|
||||
tests_bench_bench_long_messages_LDADD =
|
||||
|
||||
tests_bench_bench_failure_modes_SOURCES = tests/bench/bench_failure_modes.c $(bench_common_sources)
|
||||
tests_bench_bench_failure_modes_CPPFLAGS = -I$(srcdir)/src -I$(srcdir)/tests/bench
|
||||
tests_bench_bench_failure_modes_CFLAGS = $(AM_CFLAGS)
|
||||
tests_bench_bench_failure_modes_LDADD =
|
||||
|
||||
tests_bench_bench_export_import_SOURCES = tests/bench/bench_export_import.c \
|
||||
$(bench_common_sources) $(bench_export_sources)
|
||||
tests_bench_bench_export_import_CPPFLAGS = -I$(srcdir)/src -I$(srcdir)/tests/bench
|
||||
tests_bench_bench_export_import_CFLAGS = $(AM_CFLAGS)
|
||||
tests_bench_bench_export_import_LDADD =
|
||||
|
||||
# Volume control: BENCH_VOLUME={small|medium|max}. Default `small` is safe
|
||||
# even on tight disks (~5 MB). `max` is heavyweight — see README.
|
||||
BENCH_VOLUME ?= small
|
||||
BENCH_DATA_DIR ?= /tmp/cproof-bench-corpus
|
||||
BENCH_CSV ?= tests/bench/current.csv
|
||||
|
||||
bench-build: tests/bench/gen_history tests/bench/bench_runner tests/bench/bench_long_messages tests/bench/bench_failure_modes tests/bench/bench_export_import
|
||||
|
||||
# Resolve --lines / --years for the configured volume.
|
||||
bench-gen: bench-build
|
||||
@mkdir -p $(BENCH_DATA_DIR)
|
||||
@case "$(BENCH_VOLUME)" in \
|
||||
small) L=10000; Y=1; P=mixed ;; \
|
||||
medium) L=500000; Y=5; P=mixed ;; \
|
||||
max) L=5000000; Y=10; P=long ;; \
|
||||
*) echo "BENCH_VOLUME must be small|medium|max"; exit 2 ;; \
|
||||
esac; \
|
||||
echo "==> generating volume=$(BENCH_VOLUME) lines=$$L years=$$Y profile=$$P"; \
|
||||
./tests/bench/gen_history --lines=$$L --years=$$Y --msg-len-profile=$$P \
|
||||
--lmc-rate=3 --mam-ooo-rate=0 --resources-per-contact=3 \
|
||||
--output=$(BENCH_DATA_DIR) --seed=42
|
||||
|
||||
bench-run: bench-build
|
||||
@echo "==> running scenarios (csv=$(BENCH_CSV))"
|
||||
@BENCH_VOLUME=$(BENCH_VOLUME) BENCH_DATA_DIR=$(BENCH_DATA_DIR) \
|
||||
./tests/bench/bench_runner --data=$(BENCH_DATA_DIR) --csv=$(BENCH_CSV)
|
||||
@echo "==> CSV: $(BENCH_CSV)"
|
||||
@cat $(BENCH_CSV)
|
||||
|
||||
bench: bench-gen bench-run
|
||||
|
||||
bench-quick:
|
||||
@$(MAKE) bench BENCH_VOLUME=small
|
||||
|
||||
# Long-message tests (L1–L14). Independent of corpus volume, uses its own tmp dir.
|
||||
bench-longmsg: bench-build
|
||||
@echo "==> long-message tests (csv=$(BENCH_CSV))"
|
||||
@./tests/bench/bench_long_messages \
|
||||
--tmp=$(BENCH_DATA_DIR)-longmsg --csv=$(BENCH_CSV)
|
||||
@rm -rf $(BENCH_DATA_DIR)-longmsg
|
||||
|
||||
# Failure-injection (F1–F15). Independent corpus, asserts behaviour on bad data.
|
||||
bench-failure: bench-build
|
||||
@echo "==> failure-injection tests (csv=$(BENCH_CSV))"
|
||||
@./tests/bench/bench_failure_modes \
|
||||
--tmp=$(BENCH_DATA_DIR)-fail --csv=$(BENCH_CSV)
|
||||
@rm -rf $(BENCH_DATA_DIR)-fail
|
||||
|
||||
# Export/import (S7/S8) pipeline. Independent corpus under BENCH_DATA_DIR-export.
|
||||
# Default scale: 100k rows (~5–30 s). Override with BENCH_PIPE_ROWS=N.
|
||||
BENCH_PIPE_ROWS ?= 100000
|
||||
|
||||
BENCH_PIPE_VOLUME ?= pipe$(BENCH_PIPE_ROWS)
|
||||
|
||||
bench-export: bench-build
|
||||
@echo "==> S7 export pipeline ($(BENCH_PIPE_ROWS) rows, csv=$(BENCH_CSV))"
|
||||
@rm -rf $(BENCH_DATA_DIR)-export
|
||||
@BENCH_DATA_DIR=$(BENCH_DATA_DIR)-export \
|
||||
./tests/bench/bench_export_import seed --rows=$(BENCH_PIPE_ROWS) --account=bench@bench.example
|
||||
@BENCH_DATA_DIR=$(BENCH_DATA_DIR)-export \
|
||||
./tests/bench/bench_export_import export --account=bench@bench.example \
|
||||
--csv=$(BENCH_CSV) --label=S7a_export_cold --volume=$(BENCH_PIPE_VOLUME)
|
||||
@echo " -- second pass (dedup hot path)"
|
||||
@BENCH_DATA_DIR=$(BENCH_DATA_DIR)-export \
|
||||
./tests/bench/bench_export_import export --account=bench@bench.example \
|
||||
--csv=$(BENCH_CSV) --label=S7b_export_dedup --volume=$(BENCH_PIPE_VOLUME)
|
||||
@rm -rf $(BENCH_DATA_DIR)-export
|
||||
|
||||
bench-import: bench-build
|
||||
@echo "==> S8 import pipeline ($(BENCH_PIPE_ROWS) rows, csv=$(BENCH_CSV))"
|
||||
@rm -rf $(BENCH_DATA_DIR)-export
|
||||
@# Seed account A, export it, then wipe its DB but keep flatlog. Import
|
||||
@# back into the same account — that way the to: header in flatfile
|
||||
@# matches the importing account so dedup works correctly.
|
||||
@BENCH_DATA_DIR=$(BENCH_DATA_DIR)-export \
|
||||
./tests/bench/bench_export_import seed --rows=$(BENCH_PIPE_ROWS) --account=bench@bench.example
|
||||
@BENCH_DATA_DIR=$(BENCH_DATA_DIR)-export \
|
||||
./tests/bench/bench_export_import export --account=bench@bench.example \
|
||||
--csv=$(BENCH_CSV) --label=S7_seed_export --volume=$(BENCH_PIPE_VOLUME)
|
||||
@rm -f $(BENCH_DATA_DIR)-export/database/bench_at_bench.example/chatlog.db*
|
||||
@BENCH_DATA_DIR=$(BENCH_DATA_DIR)-export \
|
||||
./tests/bench/bench_export_import import --account=bench@bench.example \
|
||||
--csv=$(BENCH_CSV) --label=S8a_import_cold --volume=$(BENCH_PIPE_VOLUME)
|
||||
@echo " -- second import (idempotency)"
|
||||
@BENCH_DATA_DIR=$(BENCH_DATA_DIR)-export \
|
||||
./tests/bench/bench_export_import import --account=bench@bench.example \
|
||||
--csv=$(BENCH_CSV) --label=S8b_import_idempotent --volume=$(BENCH_PIPE_VOLUME)
|
||||
@rm -rf $(BENCH_DATA_DIR)-export
|
||||
|
||||
# Full roundtrip with content diff at default volume. PASSes only if
|
||||
# every row in the rebuilt DB matches the source byte-for-byte.
|
||||
bench-roundtrip: bench-build
|
||||
@echo "==> S8e roundtrip+full diff ($(BENCH_PIPE_ROWS) rows, csv=$(BENCH_CSV))"
|
||||
@rm -rf $(BENCH_DATA_DIR)-export
|
||||
@BENCH_DATA_DIR=$(BENCH_DATA_DIR)-export \
|
||||
./tests/bench/bench_export_import roundtrip --rows=$(BENCH_PIPE_ROWS) \
|
||||
--csv=$(BENCH_CSV) --label=S8e_roundtrip --volume=$(BENCH_PIPE_VOLUME) --full-diff
|
||||
@rm -rf $(BENCH_DATA_DIR)-export
|
||||
|
||||
# Pipeline at medium volume (default BENCH_PIPE_ROWS=100k).
|
||||
bench-pipeline: bench-export bench-import bench-roundtrip
|
||||
|
||||
# Pipeline at MAX volume — 1M rows. Heavy: ~5–10 min, ~500MB SQLite + ~500MB
|
||||
# flatfile + ~500MB second SQLite. Run only when you can afford the time
|
||||
# and the disk. Set BENCH_PIPE_ROWS_MAX to override (default 1_000_000).
|
||||
BENCH_PIPE_ROWS_MAX ?= 1000000
|
||||
bench-pipeline-max: bench-build
|
||||
@echo "==> bench-pipeline-max: $(BENCH_PIPE_ROWS_MAX) rows (heavy!)"
|
||||
@$(MAKE) bench-export BENCH_PIPE_ROWS=$(BENCH_PIPE_ROWS_MAX)
|
||||
@$(MAKE) bench-import BENCH_PIPE_ROWS=$(BENCH_PIPE_ROWS_MAX)
|
||||
@$(MAKE) bench-roundtrip BENCH_PIPE_ROWS=$(BENCH_PIPE_ROWS_MAX)
|
||||
|
||||
# S9: 200 contacts × 50k lines per contact = 10M lines distributed.
|
||||
bench-multicontact: bench-build
|
||||
@mkdir -p $(BENCH_DATA_DIR)-multi
|
||||
@echo "==> S9: 200 contacts x 5000 lines"
|
||||
@./tests/bench/gen_history --lines=1000000 --contacts=200 --years=3 \
|
||||
--msg-len-profile=mixed --output=$(BENCH_DATA_DIR)-multi --seed=42
|
||||
@BENCH_VOLUME=multicontact BENCH_DATA_DIR=$(BENCH_DATA_DIR)-multi \
|
||||
./tests/bench/bench_runner --data=$(BENCH_DATA_DIR)-multi --csv=$(BENCH_CSV) \
|
||||
--scenarios=S4,S6
|
||||
@rm -rf $(BENCH_DATA_DIR)-multi
|
||||
|
||||
# S10: 30% LMC corrections.
|
||||
bench-lmc: bench-build
|
||||
@mkdir -p $(BENCH_DATA_DIR)-lmc
|
||||
@echo "==> S10: LMC-heavy corpus (30%% corrections)"
|
||||
@./tests/bench/gen_history --lines=100000 --years=2 --lmc-rate=30 \
|
||||
--msg-len-profile=mixed --output=$(BENCH_DATA_DIR)-lmc --seed=42
|
||||
@BENCH_VOLUME=lmc BENCH_DATA_DIR=$(BENCH_DATA_DIR)-lmc \
|
||||
./tests/bench/bench_runner --data=$(BENCH_DATA_DIR)-lmc --csv=$(BENCH_CSV)
|
||||
@rm -rf $(BENCH_DATA_DIR)-lmc
|
||||
|
||||
# S11: 20% MAM out-of-order timestamps.
|
||||
bench-ooo: bench-build
|
||||
@mkdir -p $(BENCH_DATA_DIR)-ooo
|
||||
@echo "==> S11: MAM-OOO corpus (20%% out-of-order)"
|
||||
@./tests/bench/gen_history --lines=100000 --years=2 --mam-ooo-rate=20 \
|
||||
--msg-len-profile=mixed --output=$(BENCH_DATA_DIR)-ooo --seed=42
|
||||
@BENCH_VOLUME=ooo BENCH_DATA_DIR=$(BENCH_DATA_DIR)-ooo \
|
||||
./tests/bench/bench_runner --data=$(BENCH_DATA_DIR)-ooo --csv=$(BENCH_CSV) \
|
||||
--scenarios=S6
|
||||
@rm -rf $(BENCH_DATA_DIR)-ooo
|
||||
|
||||
# Run everything: P1 scenarios + long messages + failure injection + multicontact + LMC + OOO.
|
||||
bench-full: bench bench-longmsg bench-failure bench-multicontact bench-lmc bench-ooo
|
||||
|
||||
# Compare current.csv against baseline.csv and exit non-zero on regressions.
|
||||
BENCH_BASELINE ?= tests/bench/baseline.csv
|
||||
BENCH_THRESHOLD ?= 25
|
||||
bench-compare:
|
||||
@python3 tests/bench/compare_baseline.py \
|
||||
--baseline=$(BENCH_BASELINE) --current=$(BENCH_CSV) \
|
||||
--threshold=$(BENCH_THRESHOLD)
|
||||
|
||||
# Snapshot current.csv as the new baseline. Run this only after manually
|
||||
# reviewing that the numbers look healthy.
|
||||
bench-update-baseline:
|
||||
@cp $(BENCH_CSV) $(BENCH_BASELINE)
|
||||
@echo "baseline updated: $(BENCH_BASELINE)"
|
||||
|
||||
bench-clean:
|
||||
rm -rf $(BENCH_DATA_DIR) $(BENCH_DATA_DIR)-longmsg $(BENCH_DATA_DIR)-fail \
|
||||
$(BENCH_DATA_DIR)-multi $(BENCH_DATA_DIR)-lmc $(BENCH_DATA_DIR)-ooo \
|
||||
$(BENCH_DATA_DIR)-export $(BENCH_CSV)
|
||||
|
||||
.PHONY: bench bench-quick bench-build bench-gen bench-run bench-clean \
|
||||
bench-longmsg bench-failure bench-multicontact bench-lmc bench-ooo \
|
||||
bench-full bench-compare bench-update-baseline \
|
||||
bench-export bench-import bench-roundtrip bench-pipeline bench-pipeline-max
|
||||
|
||||
EXTRA_DIST = $(man1_sources) $(icons_sources) $(themes_sources) $(script_sources) profrc.example theme_template LICENSE.txt README.md CHANGELOG
|
||||
|
||||
# Ship API documentation with `make dist`
|
||||
@@ -319,6 +619,20 @@ EXTRA_DIST += \
|
||||
apidocs/python/src/plugin.py \
|
||||
apidocs/python/src/prof.py
|
||||
|
||||
# Bench harness sources (not built by default; see EXTRA_PROGRAMS above)
|
||||
EXTRA_DIST += \
|
||||
tests/bench/README.md \
|
||||
tests/bench/compare_baseline.py \
|
||||
tests/bench/gen_history.c \
|
||||
tests/bench/bench_runner.c \
|
||||
tests/bench/bench_long_messages.c \
|
||||
tests/bench/bench_failure_modes.c \
|
||||
tests/bench/bench_export_import.c \
|
||||
tests/bench/bench_stubs.c \
|
||||
tests/bench/bench_common.c tests/bench/bench_common.h \
|
||||
tests/bench/bench_csv.c tests/bench/bench_csv.h \
|
||||
tests/bench/baseline.csv
|
||||
|
||||
if INCLUDE_GIT_VERSION
|
||||
EXTRA_DIST += .git/HEAD .git/index
|
||||
|
||||
@@ -343,19 +657,40 @@ clean-local:
|
||||
rm -f $(git_include) $(git_include).in
|
||||
endif
|
||||
|
||||
.PHONY: my-prof.supp
|
||||
clean-functional-tests:
|
||||
rm -rf $(builddir)/test-files $(builddir)/test-logs
|
||||
|
||||
.PHONY: my-prof.supp clean-functional-tests
|
||||
my-prof.supp:
|
||||
@sed '/^# AUTO-GENERATED START/q' prof.supp > $@
|
||||
@printf "\n\n# glib\n" >> $@
|
||||
@cat /usr/share/glib-2.0/valgrind/glib.supp >> $@
|
||||
@printf "\n\n# Python\n" >> $@
|
||||
@wget -O- https://raw.githubusercontent.com/python/cpython/refs/tags/v`python3 --version | cut -d' ' -f2`/Misc/valgrind-python.supp >> $@
|
||||
@printf "\n\n# gtk\n" >> $@
|
||||
@test -z "@GTK_VERSION@" || wget -O- https://raw.githubusercontent.com/GNOME/gtk/refs/tags/@GTK_VERSION@/gtk.supp >> $@
|
||||
@printf "\n\n# more gtk\n" >> $@
|
||||
@test -z "@GTK_VERSION@" || cat /usr/share/gtk-3.0/valgrind/gtk.supp >> $@
|
||||
|
||||
check-unit: tests/unittests/unittests
|
||||
tests/unittests/unittests
|
||||
|
||||
@VALGRIND_CHECK_RULES@
|
||||
VALGRIND_SUPPRESSIONS_FILES=prof.supp
|
||||
VALGRIND_SUPPRESSIONS_FILES=$(srcdir)/prof.supp
|
||||
|
||||
# Code coverage targets (requires --enable-coverage)
|
||||
coverage-clean:
|
||||
find . -name '*.gcda' -delete
|
||||
find . -name '*.gcno' -delete
|
||||
rm -rf coverage-html coverage.info
|
||||
|
||||
coverage-report: check
|
||||
lcov --capture --directory . --output-file coverage.info --ignore-errors inconsistent
|
||||
lcov --remove coverage.info '/usr/*' '*/tests/*' --output-file coverage.info --ignore-errors inconsistent
|
||||
genhtml coverage.info --output-directory coverage-html
|
||||
@echo "Coverage report generated in coverage-html/index.html"
|
||||
|
||||
.PHONY: coverage-clean coverage-report
|
||||
|
||||
format: $(all_c_sources)
|
||||
clang-format -i $(all_c_sources)
|
||||
|
||||
40
README.md
40
README.md
@@ -1,40 +1,36 @@
|
||||
# Profanity
|
||||
 [](https://github.com/profanity-im/profanity/graphs/contributors/) [](https://repology.org/project/profanity/versions)
|
||||
# CProof
|
||||

|
||||
|
||||
Profanity is a console based XMPP client inspired by [Irssi](http://www.irssi.org/).
|
||||
CProof is a console based XMPP chat client based on [Profanity](https://profanity-im.github.io/).
|
||||
|
||||

|
||||

|
||||
|
||||
See the [User Guide](https://profanity-im.github.io/userguide.html) for information on installing and using Profanity.
|
||||
See the [Quick Start Guide](https://jabber.space/getting-started/quick-start/) for information on installing and using CProof.
|
||||
|
||||
## Project
|
||||
This project is about freedom, privacy and choice. We want to enable people to chat with one another in a safe way. Thus supporting encryption (OTR, PGP, OMEMO, OX) and being decentralized, meaning everyone can run their own server. We believe [XMPP](https://xmpp.org/) is a great proven protocol with an excellent community serving this purpose well.
|
||||
|
||||
CProof enables you to communicate with privacy, freedom and comfort. Our open-source chat application delivers secure, end-to-end encrypted messaging (OTR, PGP, OMEMO, OX) built on the trusted [XMPP](https://xmpp.org/) protocol. With a decentralized design, you can connect directly or even host your own server, keeping your data in your hands. Whether you're chatting with friends or collaborating securely, CProof makes private communication simple, reliable, and truly yours.
|
||||
|
||||
## Installation
|
||||
Our [user guide](https://profanity-im.github.io/userguide.html) contains an [install](https://profanity-im.github.io/guide/latest/install.html) section and a section for [building](https://profanity-im.github.io/guide/latest/build.html) from source yourself.
|
||||
Check our [installation guide](https://jabber.space/getting-started/installation/) for detailed instructions.
|
||||
|
||||
## How to contribute
|
||||
We tried to sum things up on our [helpout](https://profanity-im.github.io/helpout.html) page.
|
||||
Additionally you can check out our [blog](https://profanity-im.github.io/blog/) where we have articles like:
|
||||
[How to get a backtrace](https://profanity-im.github.io/blog/post/how-to-get-a-backtrace/) and [Contributing a Patch via GitHub](https://profanity-im.github.io/blog/post/contributing-a-patch-via-github/).
|
||||
For more technical details check out our [CONTRIBUTING.md](CONTRIBUTING.md) file.
|
||||
See our [Helping Out](https://jabber.space/contributing/helpout/) page for a concise summary of ways to help us.
|
||||
|
||||
Review the [Contributing Guide](CONTRIBUTING.md) and [Code Overview](https://jabber.space/contributing/code-overview/) pages for advanced technical details.
|
||||
|
||||
## Getting help
|
||||
To get help, first read our [User Guide](https://profanity-im.github.io/userguide.html) then check out the [FAQ](https://profanity-im.github.io/faq.html).
|
||||
If you are having a problem then first search the [issue tracker](https://github.com/profanity-im/profanity/issues).
|
||||
If you don't find anything there either come to our [MUC](xmpp:profanity@rooms.dismail.de?join) or create a new issue depending on what your problem is.
|
||||
Prior to asking questions, check our [User Guide](https://profanity-im.github.io/userguide.html), then check out the [FAQ](https://jabber.space/about/faq/).
|
||||
|
||||
If you are still having a problem then search the [issue tracker](https://git.jabber.space/devs/cproof/issues).
|
||||
|
||||
As a last resort, feel free to write us on [support@jabber.tech](mailto:support@jabber.tech) or create a new issue depending on what your problem is.
|
||||
|
||||
## Links
|
||||
|
||||
### Website
|
||||
<!-- TODO:
|
||||
URL: https://jabber.space/
|
||||
-->
|
||||
Feel free to visit our website: [jabber.space](https://jabber.space/).
|
||||
|
||||
Repo: https://git.jabber.space/devs/profanity
|
||||
|
||||
<!-- TODO: blog/docs link -->
|
||||
You may also check the repository if you like, available on [git.jabber.space/devs/profanity](https://git.jabber.space/devs/profanity).
|
||||
|
||||
### Plugins
|
||||
Plugins repository: https://github.com/profanity-im/profanity-plugins
|
||||
Plugins repository: https://git.jabber.space/devs/cproof-plugins
|
||||
|
||||
@@ -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
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,220 +1,247 @@
|
||||
/** @file
|
||||
C Hooks.
|
||||
*/
|
||||
/** @file profhooks.h
|
||||
* @brief C Plugin Hooks for CProof.
|
||||
*
|
||||
* This header defines optional callback hooks that CProof plugins can implement to
|
||||
* handle events such as startup, shutdown, message processing, and presence updates in
|
||||
* CProof, a console-based XMPP client. These hooks are called by CProof when the
|
||||
* corresponding events occur, allowing plugins to customize behavior. All hooks are
|
||||
* optional; plugins can define only the hooks they need, and CProof will automatically
|
||||
* invoke them when appropriate.
|
||||
*
|
||||
* To use these hooks, define the functions in your plugin with the signatures provided
|
||||
* in this header. For example, to handle the startup event:
|
||||
*
|
||||
* @code
|
||||
* #include "profhooks.h"
|
||||
* void prof_on_start(void) {
|
||||
* // Handle CProof startup
|
||||
* }
|
||||
* @endcode
|
||||
*
|
||||
* For additional plugin functionality, such as displaying messages or registering
|
||||
* commands, see the CProof API functions in profapi.h.
|
||||
*
|
||||
* @see profapi.h
|
||||
*/
|
||||
|
||||
/** @mainpage CProof Plugins API and Hooks
|
||||
* List of all API functions available to plugins: @ref profapi.h
|
||||
* List of all hooks which plugins may implement: @ref profhooks.h
|
||||
*/
|
||||
|
||||
/**
|
||||
Called when a plugin is loaded, either when profanity is started, or when the /plugins load or /plugins install commands are called
|
||||
@param version the version of Profanity
|
||||
@param status the package status of Profanity, "development" or "release"
|
||||
@param account_name account name of the currently logged in account, or NULL if not logged in
|
||||
@param fulljid the users full Jabber ID (barejid and resource) if logged in, NULL otherwise
|
||||
*/
|
||||
* Called when a plugin is loaded, either when CProof is started, or when the /plugins load or /plugins install commands are called
|
||||
* @param version The version of CProof
|
||||
* @param status The package status of CProof, "development" or "release"
|
||||
* @param account_name Account name of the currently logged in account, or NULL if not logged in
|
||||
* @param fulljid The user's full Jabber ID (barejid and resource) if logged in, NULL otherwise
|
||||
*/
|
||||
void prof_init(const char * const version, const char * const status, const char *const account_name, const char *const fulljid);
|
||||
|
||||
/**
|
||||
Called when Profanity is started
|
||||
*/
|
||||
* Called when CProof is started
|
||||
*/
|
||||
void prof_on_start(void);
|
||||
|
||||
/**
|
||||
Called when the user quits Profanity
|
||||
*/
|
||||
* Called when the user quits CProof
|
||||
*/
|
||||
void prof_on_shutdown(void);
|
||||
|
||||
/**
|
||||
Called when a plugin is unloaded with the /plugins unload command
|
||||
*/
|
||||
* Called when a plugin is unloaded with the /plugins unload command
|
||||
*/
|
||||
void prof_on_unload(void);
|
||||
|
||||
/**
|
||||
Called when the user connects with an account
|
||||
@param account_name account name of the account used for logging in
|
||||
@param fulljid the full Jabber ID (barejid and resource) of the account
|
||||
*/
|
||||
* Called when the user connects with an account
|
||||
* @param account_name Account name of the account used for logging in
|
||||
* @param fulljid The full Jabber ID (barejid and resource) of the account
|
||||
*/
|
||||
void prof_on_connect(const char * const account_name, const char * const fulljid);
|
||||
|
||||
/**
|
||||
Called when the user disconnects an account
|
||||
@param account_name account name of the account being disconnected
|
||||
@param fulljid the full Jabber ID (barejid and resource) of the account
|
||||
*/
|
||||
* Called when the user disconnects an account
|
||||
* @param account_name Account name of the account being disconnected
|
||||
* @param fulljid The full Jabber ID (barejid and resource) of the account
|
||||
*/
|
||||
void prof_on_disconnect(const char * const account_name, const char * const fulljid);
|
||||
|
||||
/**
|
||||
Called before a chat message is displayed
|
||||
@param barejid Jabber ID of the message sender
|
||||
@param resource resource of the message sender
|
||||
@param message the received message
|
||||
@return the new message to display, or NULL to preserve the original message
|
||||
*/
|
||||
* Called before a chat message is displayed
|
||||
* @param barejid Jabber ID of the message sender
|
||||
* @param resource Resource of the message sender
|
||||
* @param message The received message
|
||||
* @return The new message to display, or NULL to preserve the original message
|
||||
*/
|
||||
char* prof_pre_chat_message_display(const char * const barejid, const char *const resource, const char *message);
|
||||
|
||||
/**
|
||||
Called after a chat message is displayed
|
||||
@param barejid Jabber ID of the message sender
|
||||
@param resource resource of the message sender
|
||||
@param message the received message
|
||||
*/
|
||||
* Called after a chat message is displayed
|
||||
* @param barejid Jabber ID of the message sender
|
||||
* @param resource Resource of the message sender
|
||||
* @param message The received message
|
||||
*/
|
||||
void prof_post_chat_message_display(const char * const barejid, const char *const resource, const char *message);
|
||||
|
||||
/**
|
||||
Called before a chat message is sent
|
||||
@param barejid Jabber ID of the message recipient
|
||||
@param message the message to be sent
|
||||
@return the modified or original message to send, or NULL to cancel sending of the message
|
||||
*/
|
||||
* Called before a chat message is sent
|
||||
* @param barejid Jabber ID of the message recipient
|
||||
* @param message The message to be sent
|
||||
* @return The modified or original message to send, or NULL to cancel sending of the message
|
||||
*/
|
||||
char* prof_pre_chat_message_send(const char * const barejid, const char *message);
|
||||
|
||||
/**
|
||||
Called after a chat message has been sent
|
||||
@param barejid Jabber ID of the message recipient
|
||||
@param message the sent message
|
||||
*/
|
||||
* Called after a chat message has been sent
|
||||
* @param barejid Jabber ID of the message recipient
|
||||
* @param message The sent message
|
||||
*/
|
||||
void prof_post_chat_message_send(const char * const barejid, const char *message);
|
||||
|
||||
/**
|
||||
Called before a chat room message is displayed
|
||||
@param barejid Jabber ID of the room
|
||||
@param nick nickname of message sender
|
||||
@param message the received message
|
||||
@return the new message to display, or NULL to preserve the original message
|
||||
*/
|
||||
* Called before a chat room message is displayed
|
||||
* @param barejid Jabber ID of the room
|
||||
* @param nick Nickname of message sender
|
||||
* @param message The received message
|
||||
* @return The new message to display, or NULL to preserve the original message
|
||||
*/
|
||||
char* prof_pre_room_message_display(const char * const barejid, const char * const nick, const char *message);
|
||||
|
||||
/**
|
||||
Called after a chat room message is displayed
|
||||
@param barejid Jabber ID of the room
|
||||
@param nick nickname of the message sender
|
||||
@param message the received message
|
||||
*/
|
||||
* Called after a chat room message is displayed
|
||||
* @param barejid Jabber ID of the room
|
||||
* @param nick Nickname of the message sender
|
||||
* @param message The received message
|
||||
*/
|
||||
void prof_post_room_message_display(const char * const barejid, const char * const nick, const char *message);
|
||||
|
||||
/**
|
||||
Called before a chat room message is sent
|
||||
@param barejid Jabber ID of the room
|
||||
@param message the message to be sent
|
||||
@return the modified or original message to send, or NULL to cancel sending of the message
|
||||
*/
|
||||
* Called before a chat room message is sent
|
||||
* @param barejid Jabber ID of the room
|
||||
* @param message The message to be sent
|
||||
* @return The modified or original message to send, or NULL to cancel sending of the message
|
||||
*/
|
||||
char* prof_pre_room_message_send(const char * const barejid, const char *message);
|
||||
|
||||
/**
|
||||
Called after a chat room message has been sent
|
||||
@param barejid Jabber ID of the room
|
||||
@param message the sent message
|
||||
*/
|
||||
* Called after a chat room message has been sent
|
||||
* @param barejid Jabber ID of the room
|
||||
* @param message The sent message
|
||||
*/
|
||||
void prof_post_room_message_send(const char * const barejid, const char *message);
|
||||
|
||||
/**
|
||||
Called when the server sends a chat room history message
|
||||
@param barejid Jabber ID of the room
|
||||
@param nick nickname of the message sender
|
||||
@param message the message to be sent
|
||||
@param timestamp time the message was originally sent to the room, in ISO8601 format
|
||||
*/
|
||||
* Called when the server sends a chat room history message
|
||||
* @param barejid Jabber ID of the room
|
||||
* @param nick Nickname of the message sender
|
||||
* @param message The message to be sent
|
||||
* @param timestamp Time the message was originally sent to the room, in ISO8601 format
|
||||
*/
|
||||
void prof_on_room_history_message(const char * const barejid, const char *const nick, const char *const message, const char *const timestamp);
|
||||
|
||||
/**
|
||||
Called before a private chat room message is displayed
|
||||
@param barejid Jabber ID of the room
|
||||
@param nick nickname of message sender
|
||||
@param message the received message
|
||||
@return the new message to display, or NULL to preserve the original message
|
||||
*/
|
||||
* Called before a private chat room message is displayed
|
||||
* @param barejid Jabber ID of the room
|
||||
* @param nick Nickname of message sender
|
||||
* @param message The received message
|
||||
* @return The new message to display, or NULL to preserve the original message
|
||||
*/
|
||||
char* prof_pre_priv_message_display(const char * const barejid, const char * const nick, const char *message);
|
||||
|
||||
/**
|
||||
Called after a private chat room message is displayed
|
||||
@param barejid Jabber ID of the room
|
||||
@param nick nickname of the message sender
|
||||
@param message the received message
|
||||
*/
|
||||
* Called after a private chat room message is displayed
|
||||
* @param barejid Jabber ID of the room
|
||||
* @param nick Nickname of the message sender
|
||||
* @param message The received message
|
||||
*/
|
||||
void prof_post_priv_message_display(const char * const barejid, const char * const nick, const char *message);
|
||||
|
||||
/**
|
||||
Called before a private chat room message is sent
|
||||
@param barejid Jabber ID of the room
|
||||
@param nick nickname of message recipient
|
||||
@param message the message to be sent
|
||||
@return the modified or original message to send, or NULL to cancel sending of the message
|
||||
*/
|
||||
* Called before a private chat room message is sent
|
||||
* @param barejid Jabber ID of the room
|
||||
* @param nick Nickname of message recipient
|
||||
* @param message The message to be sent
|
||||
* @return The modified or original message to send, or NULL to cancel sending of the message
|
||||
*/
|
||||
char* prof_pre_priv_message_send(const char * const barejid, const char * const nick, const char *message);
|
||||
|
||||
/**
|
||||
Called after a private chat room message has been sent
|
||||
@param barejid Jabber ID of the room
|
||||
@param nick nickname of the message recipient
|
||||
@param message the sent message
|
||||
*/
|
||||
* Called after a private chat room message has been sent
|
||||
* @param barejid Jabber ID of the room
|
||||
* @param nick Nickname of the message recipient
|
||||
* @param message The sent message
|
||||
*/
|
||||
void prof_post_priv_message_send(const char * const barejid, const char * const nick, const char *message);
|
||||
|
||||
/**
|
||||
Called before an XMPP message stanza is sent
|
||||
@param stanza The stanza to send
|
||||
@return The new stanza to send, or NULL to preserve the original stanza
|
||||
*/
|
||||
* Called before an XMPP message stanza is sent
|
||||
* @param stanza The stanza to send
|
||||
* @return The new stanza to send, or NULL to preserve the original stanza
|
||||
*/
|
||||
char* prof_on_message_stanza_send(const char *const stanza);
|
||||
|
||||
/**
|
||||
Called when an XMPP message stanza is received
|
||||
@param stanza The stanza received
|
||||
@return 1 if Profanity should continue to process the message stanza, 0 otherwise
|
||||
*/
|
||||
* Called when an XMPP message stanza is received
|
||||
* @param stanza The stanza received
|
||||
* @return 1 if CProof should continue to process the message stanza, 0 otherwise
|
||||
*/
|
||||
int prof_on_message_stanza_receive(const char *const stanza);
|
||||
|
||||
/**
|
||||
Called before an XMPP presence stanza is sent
|
||||
@param stanza The stanza to send
|
||||
@return The new stanza to send, or NULL to preserve the original stanza
|
||||
*/
|
||||
* Called before an XMPP presence stanza is sent
|
||||
* @param stanza The stanza to send
|
||||
* @return The new stanza to send, or NULL to preserve the original stanza
|
||||
*/
|
||||
char* prof_on_presence_stanza_send(const char *const stanza);
|
||||
|
||||
/**
|
||||
Called when an XMPP presence stanza is received
|
||||
@param stanza The stanza received
|
||||
@return 1 if Profanity should continue to process the presence stanza, 0 otherwise
|
||||
*/
|
||||
* Called when an XMPP presence stanza is received
|
||||
* @param stanza The stanza received
|
||||
* @return 1 if CProof should continue to process the presence stanza, 0 otherwise
|
||||
*/
|
||||
int prof_on_presence_stanza_receive(const char *const stanza);
|
||||
|
||||
/**
|
||||
Called before an XMPP iq stanza is sent
|
||||
@param stanza The stanza to send
|
||||
@return The new stanza to send, or NULL to preserve the original stanza
|
||||
*/
|
||||
* Called before an XMPP iq stanza is sent
|
||||
* @param stanza The stanza to send
|
||||
* @return The new stanza to send, or NULL to preserve the original stanza
|
||||
*/
|
||||
char* prof_on_iq_stanza_send(const char *const stanza);
|
||||
|
||||
/**
|
||||
Called when an XMPP iq stanza is received
|
||||
@param stanza The stanza received
|
||||
@return 1 if Profanity should continue to process the iq stanza, 0 otherwise
|
||||
*/
|
||||
* Called when an XMPP iq stanza is received
|
||||
* @param stanza The stanza received
|
||||
* @return 1 if CProof should continue to process the iq stanza, 0 otherwise
|
||||
*/
|
||||
int prof_on_iq_stanza_receive(const char *const stanza);
|
||||
|
||||
/**
|
||||
Called when a contact goes offline
|
||||
@param barejid Jabber ID of the contact
|
||||
@param resource the resource being disconnected
|
||||
@param status the status message received with the offline presence, or NULL
|
||||
*/
|
||||
* Called when a contact goes offline
|
||||
* @param barejid Jabber ID of the contact
|
||||
* @param resource The resource being disconnected
|
||||
* @param status The status message received with the offline presence, or NULL
|
||||
*/
|
||||
void prof_on_contact_offline(const char *const barejid, const char *const resource, const char *const status);
|
||||
|
||||
/**
|
||||
Called when a presence notification is received from a contact
|
||||
@param barejid Jabber ID of the contact
|
||||
@param resource the resource being disconnected
|
||||
@param presence presence of the contact, one of "chat", "online", "away", "xa" or "dnd"
|
||||
@param status the status message received with the presence, or NULL
|
||||
@param priority the priority associated with the resource
|
||||
*/
|
||||
* Called when a presence notification is received from a contact
|
||||
* @param barejid Jabber ID of the contact
|
||||
* @param resource The resource being disconnected
|
||||
* @param presence Presence of the contact, one of "chat", "online", "away", "xa" or "dnd"
|
||||
* @param status The status message received with the presence, or NULL
|
||||
* @param priority The priority associated with the resource
|
||||
*/
|
||||
void prof_on_contact_presence(const char *const barejid, const char *const resource, const char *const presence, const char *const status, const int priority);
|
||||
|
||||
/**
|
||||
Called when a chat window is focused
|
||||
@param barejid Jabber ID of the chat window recipient
|
||||
*/
|
||||
* Called when a chat window is focused
|
||||
* @param barejid Jabber ID of the chat window recipient
|
||||
*/
|
||||
void prof_on_chat_win_focus(const char *const barejid);
|
||||
|
||||
/**
|
||||
Called when a chat room window is focused
|
||||
@param barejid Jabber ID of the room
|
||||
*/
|
||||
void prof_on_room_win_focus(const char *const barejid);
|
||||
* Called when a chat room window is focused
|
||||
* @param barejid Jabber ID of the room
|
||||
*/
|
||||
void prof_on_room_win_focus(const char *const barejid);
|
||||
@@ -1,7 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Profanity Python Plugins API documentation build configuration file, created by
|
||||
# sphinx-quickstart on Thu Mar 17 23:01:31 2016.
|
||||
# CProof Python Plugins API documentation build configuration file.
|
||||
#
|
||||
# This file is execfile()d with the current directory set to its
|
||||
# containing dir.
|
||||
@@ -47,25 +46,25 @@ source_suffix = '.rst'
|
||||
master_doc = 'index'
|
||||
|
||||
# General information about the project.
|
||||
project = u'Profanity Python Plugins API'
|
||||
copyright = u'2016 - 2018, boothj5'
|
||||
author = u'boothj5'
|
||||
project = u'CProof Python Plugins API'
|
||||
copyright = u'2025, CProof, based on Profanity by boothj5 et al.'
|
||||
author = u'CProof Developers'
|
||||
|
||||
# The version info for the project you're documenting, acts as replacement for
|
||||
# |version| and |release|, also used in various other places throughout the
|
||||
# built documents.
|
||||
#
|
||||
# The short X.Y version.
|
||||
version = u'0.5.0'
|
||||
version = u'0.15.0'
|
||||
# The full version, including alpha/beta/rc tags.
|
||||
release = u'0.5.0'
|
||||
release = u'0.15.0'
|
||||
|
||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||
# for a list of supported languages.
|
||||
#
|
||||
# This is also used if you do content translation via gettext catalogs.
|
||||
# Usually you set "language" from the command line for these cases.
|
||||
language = None
|
||||
language = 'en'
|
||||
|
||||
# There are two options for replacing |today|: either, you set today to some
|
||||
# non-false value, then it is used:
|
||||
@@ -75,7 +74,7 @@ language = None
|
||||
|
||||
# List of patterns, relative to source directory, that match files and
|
||||
# directories to ignore when looking for source files.
|
||||
exclude_patterns = ['_build']
|
||||
exclude_patterns = ['_build', 'modules.rst']
|
||||
|
||||
# The reST default role (used for this markup: `text`) to use for all
|
||||
# documents.
|
||||
@@ -122,7 +121,7 @@ html_theme = 'default'
|
||||
|
||||
# The name for this set of Sphinx documents. If None, it defaults to
|
||||
# "<project> v<release> documentation".
|
||||
html_title = "Profanity Python Plugins"
|
||||
html_title = "CProof Python Plugins"
|
||||
|
||||
# A shorter title for the navigation bar. Default is the same as html_title.
|
||||
#html_short_title = None
|
||||
@@ -139,7 +138,7 @@ html_title = "Profanity Python Plugins"
|
||||
# Add any paths that contain custom static files (such as style sheets) here,
|
||||
# relative to this directory. They are copied after the builtin static files,
|
||||
# so a file named "default.css" will overwrite the builtin "default.css".
|
||||
html_static_path = ['_static']
|
||||
# html_static_path = ['_static']
|
||||
|
||||
# Add any extra paths that contain custom files (such as robots.txt or
|
||||
# .htaccess) here, relative to this directory. These files are copied
|
||||
@@ -202,7 +201,7 @@ html_show_copyright = False
|
||||
#html_search_scorer = 'scorer.js'
|
||||
|
||||
# Output file base name for HTML help builder.
|
||||
htmlhelp_basename = 'ProfanityPythonPluginsAPIdoc'
|
||||
htmlhelp_basename = 'CProofPythonPluginsAPIdoc'
|
||||
|
||||
# -- Options for LaTeX output ---------------------------------------------
|
||||
|
||||
@@ -224,8 +223,8 @@ latex_elements = {
|
||||
# (source start file, target name, title,
|
||||
# author, documentclass [howto, manual, or own class]).
|
||||
latex_documents = [
|
||||
(master_doc, 'ProfanityPythonPluginsAPI.tex', u'Profanity Python Plugins API Documentation',
|
||||
u'boothj5', 'manual'),
|
||||
(master_doc, 'CProofPythonPluginsAPI.tex', u'CProof Python Plugins API Documentation',
|
||||
u'CProof Developers', 'manual'),
|
||||
]
|
||||
|
||||
# The name of an image file (relative to this directory) to place at the top of
|
||||
@@ -254,7 +253,7 @@ latex_documents = [
|
||||
# One entry per manual page. List of tuples
|
||||
# (source start file, name, description, authors, manual section).
|
||||
man_pages = [
|
||||
(master_doc, 'profanitypythonpluginsapi', u'Profanity Python Plugins API Documentation',
|
||||
(master_doc, 'cproofpythonpluginsapi', u'CProof Python Plugins API Documentation',
|
||||
[author], 1)
|
||||
]
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
Profanity Python Plugins API
|
||||
============================
|
||||
CProof Python Plugins API
|
||||
===========================
|
||||
|
||||
The ``prof`` module describes functions that plugins may call to interact with Profanity.
|
||||
The ``plugin`` module describes functions that plugins may define to be notified of various events in Profanity.
|
||||
The ``prof`` module describes functions that plugins may call to interact with CProof.
|
||||
The ``plugin`` module describes functions that plugins may define to be notified of various events in CProof.
|
||||
|
||||
Contents:
|
||||
|
||||
@@ -11,4 +11,5 @@ Contents:
|
||||
|
||||
prof
|
||||
plugin
|
||||
* :ref:`genindex`
|
||||
|
||||
:ref:`genindex`
|
||||
|
||||
@@ -1,360 +1,495 @@
|
||||
"""
|
||||
This page describes functions that plugins may implement to be called from Profanity on certain events. All functions are optional.
|
||||
The plugin module defines optional callback functions that CProof plugins can
|
||||
implement to handle events such as startup, shutdown, message processing, and
|
||||
presence updates.
|
||||
|
||||
Examples:
|
||||
::
|
||||
def prof_on_start():
|
||||
prof.cons_show("Profanity has started...")
|
||||
All functions are optional and are called by CProof when the corresponding event
|
||||
occurs. You need to define them in your module and they will automatically be used
|
||||
for callback events.
|
||||
|
||||
def prof_pre_room_message_display(room, nick, message):
|
||||
prof.cons_show("Manipulating chat room message before display...")
|
||||
new_message = message + " (added by plugin)"
|
||||
return new_message
|
||||
To utilize full functionality, such as the ``prof.cons_show`` function, see ``prof``
|
||||
documentation and use::
|
||||
|
||||
def prof_on_contact_presence(barejid, resource, presence, status, priority):
|
||||
notify_message = barejid + " is " + presence
|
||||
prof.notify(notify_message, 5, "Presence")
|
||||
import prof
|
||||
"""
|
||||
|
||||
def prof_init(version, status, account_name, fulljid):
|
||||
"""Called when a plugin is loaded, either when profanity is started, or when the ``/plugins load`` or ``/plugins install`` commands are called
|
||||
# Initialization and Lifecycle
|
||||
# ---------------------------
|
||||
|
||||
:param version: the version of Profanity
|
||||
:param status: the package status of Profanity, ``"development"`` or ``"release"``
|
||||
:param account_name: account name of the currently logged in account, or ``None`` if not logged in
|
||||
:param fulljid: the users full Jabber ID (barejid and resource) if logged in, ``None`` otherwise
|
||||
:type version: str or unicode
|
||||
:type status: str or unicode
|
||||
:type account_name: str, unicode or None
|
||||
:type fulljid: str, unicode or None
|
||||
def prof_init(version: str, status: str, account_name: str | None, fulljid: str | None) -> None:
|
||||
"""Initializes the plugin when loaded by CProof.
|
||||
|
||||
Called when CProof starts or when the plugin is loaded via the ``/plugins load``
|
||||
or ``/plugins install`` commands.
|
||||
|
||||
:param version: The version of CProof (e.g., ``"1.0.0"``).
|
||||
:param status: The package status of CProof (``"development"`` or ``"release"``).
|
||||
:param account_name: The account name of the logged-in user, or None if not logged in.
|
||||
:param fulljid: The full Jabber ID (barejid/resource) of the logged-in user, or None if not logged in.
|
||||
:return: None
|
||||
|
||||
Example::
|
||||
|
||||
def prof_init(version: str, status: str, account_name: str | None, fulljid: str | None) -> None:
|
||||
prof.cons_show(f"MyPlugin for CProof {version} ({status}) has been loaded, account: {account_name}")
|
||||
"""
|
||||
pass
|
||||
|
||||
def prof_on_start() -> None:
|
||||
"""Called when CProof starts.
|
||||
|
||||
def prof_on_start():
|
||||
"""Called when Profanity is started
|
||||
Use this to perform setup tasks that should occur at application startup.
|
||||
|
||||
:return: None
|
||||
|
||||
Example::
|
||||
|
||||
def prof_on_start() -> None:
|
||||
prof.cons_show("CProof has started...")
|
||||
"""
|
||||
pass
|
||||
|
||||
def prof_on_shutdown() -> None:
|
||||
"""Called when CProof is shutting down.
|
||||
|
||||
def prof_on_shutdown():
|
||||
"""Called when the user quits Profanity
|
||||
Use this to perform cleanup tasks before the application exits.
|
||||
|
||||
:return: None
|
||||
|
||||
Example::
|
||||
|
||||
def prof_on_shutdown() -> None:
|
||||
prof.cons_show("CProof is shutting down...")
|
||||
"""
|
||||
pass
|
||||
|
||||
def prof_on_unload() -> None:
|
||||
"""Called when the plugin is unloaded via the ``/plugins unload`` command.
|
||||
|
||||
def prof_on_unload():
|
||||
"""Called when a plugin is unloaded with the ``/plugins unload`` command
|
||||
Use this to clean up plugin-specific resources.
|
||||
|
||||
:return: None
|
||||
|
||||
Example::
|
||||
|
||||
def prof_on_unload() -> None:
|
||||
prof.cons_show("Plugin unloaded")
|
||||
"""
|
||||
pass
|
||||
|
||||
def prof_on_connect(account_name: str, fulljid: str) -> None:
|
||||
"""Called when a user connects to CProof with an account.
|
||||
|
||||
def prof_on_connect(account_name, fulljid):
|
||||
"""Called when the user connects with an account
|
||||
:param account_name: The account name used for login.
|
||||
:param fulljid: The full Jabber ID (barejid/resource) of the connected account.
|
||||
:return: None
|
||||
|
||||
:param account_name: account name of the account used for logging in
|
||||
:param fulljid: the full Jabber ID (barejid and resource) of the account
|
||||
:type account_name: str or unicode
|
||||
:type fulljid: str or unicode
|
||||
Example::
|
||||
|
||||
def prof_on_connect(account_name: str, fulljid: str) -> None:
|
||||
prof.cons_show(f"Connected as {account_name} ({fulljid})")
|
||||
"""
|
||||
pass
|
||||
|
||||
def prof_on_disconnect(account_name: str, fulljid: str) -> None:
|
||||
"""Called when a user disconnects an account from CProof.
|
||||
|
||||
def prof_on_disconnect(account_name, fulljid):
|
||||
"""Called when the user disconnects an account
|
||||
:param account_name: The account name being disconnected.
|
||||
:param fulljid: The full Jabber ID (barejid/resource) of the disconnected account.
|
||||
:return: None
|
||||
|
||||
:param account_name: account name of the account being disconnected
|
||||
:param fulljid: the full Jabber ID (barejid and resource) of the account
|
||||
:type account_name: str or unicode
|
||||
:type fulljid: str or unicode
|
||||
Example::
|
||||
|
||||
def prof_on_disconnect(account_name: str, fulljid: str) -> None:
|
||||
prof.cons_show(f"Disconnected {account_name} ({fulljid})")
|
||||
"""
|
||||
pass
|
||||
|
||||
# Chat Message Handlers
|
||||
# ---------------------
|
||||
|
||||
def prof_pre_chat_message_display(barejid, resource, message):
|
||||
"""Called before a chat message is displayed
|
||||
def prof_pre_chat_message_display(barejid: str, resource: str, message: str) -> str | None:
|
||||
"""Called before a chat message is displayed in a chat window.
|
||||
|
||||
:param barejid: Jabber ID of the message sender
|
||||
:param resource: resource of the message sender
|
||||
:param message: the received message
|
||||
:type barejid: str or unicode
|
||||
:type resource: str or unicode
|
||||
:type message: str or unicode
|
||||
:return: the new message to display, or ``None`` to preserve the original message
|
||||
:rtype: str or unicode
|
||||
Allows the plugin to modify or cancel the message display.
|
||||
|
||||
:param barejid: The Jabber ID of the message sender (e.g., ``bob@example.com``).
|
||||
:param resource: The sender's resource (e.g., ``laptop``).
|
||||
:param message: The received message.
|
||||
:return: The modified message to display, or None to preserve the original.
|
||||
|
||||
Example::
|
||||
|
||||
def prof_pre_chat_message_display(barejid: str, resource: str, message: str) -> str | None:
|
||||
new_message = f"{message} (from {barejid})"
|
||||
return new_message
|
||||
"""
|
||||
pass
|
||||
|
||||
def prof_post_chat_message_display(barejid: str, resource: str, message: str) -> None:
|
||||
"""Called after a chat message is displayed in a chat window.
|
||||
|
||||
def prof_post_chat_message_display(barejid, resource, message):
|
||||
"""Called after a chat message is displayed
|
||||
Use this to perform actions after the message is shown.
|
||||
|
||||
:param barejid: Jabber ID of the message sender
|
||||
:param resource: resource of the message sender
|
||||
:param message: the received message
|
||||
:type barejid: str or unicode
|
||||
:type resource: str or unicode
|
||||
:type message: str or unicode
|
||||
:param barejid: The Jabber ID of the message sender (e.g., ``bob@example.com``).
|
||||
:param resource: The sender's resource (e.g., ``laptop``).
|
||||
:param message: The displayed message.
|
||||
:return: None
|
||||
|
||||
Example::
|
||||
|
||||
def prof_post_chat_message_display(barejid: str, resource: str, message: str) -> None:
|
||||
prof.cons_show(f"Displayed message from {barejid}/{resource}: {message}")
|
||||
"""
|
||||
pass
|
||||
|
||||
def prof_pre_chat_message_send(barejid: str, message: str) -> str | None:
|
||||
"""Called before a chat message is sent from CProof.
|
||||
|
||||
def prof_pre_chat_message_send(barejid, message):
|
||||
"""Called before a chat message is sent
|
||||
Allows the plugin to modify or cancel the message.
|
||||
|
||||
:param barejid: Jabber ID of the message recipient
|
||||
:param message: the message to be sent
|
||||
:type barejid: str or unicode
|
||||
:type message: str or unicode
|
||||
:return: the modified or original message to send, or ``None`` to cancel sending of the message
|
||||
:rtype: str or unicode
|
||||
:param barejid: The Jabber ID of the recipient (e.g., ``bob@example.com``).
|
||||
:param message: The message to be sent.
|
||||
:return: The modified message to send, or None to cancel sending.
|
||||
|
||||
Example::
|
||||
|
||||
def prof_pre_chat_message_send(barejid: str, message: str) -> str | None:
|
||||
return f"{message} (sent by plugin)"
|
||||
"""
|
||||
pass
|
||||
|
||||
def prof_post_chat_message_send(barejid: str, message: str) -> None:
|
||||
"""Called after a chat message is sent from CProof.
|
||||
|
||||
def prof_post_chat_message_send(barejid, message):
|
||||
"""Called after a chat message has been sent
|
||||
Use this to log or react to sent messages.
|
||||
|
||||
:param barejid: Jabber ID of the message recipient
|
||||
:param message: the sent message
|
||||
:type barejid: str or unicode
|
||||
:type message: str or unicode
|
||||
:param barejid: The Jabber ID of the recipient (e.g., ``bob@example.com``).
|
||||
:param message: The sent message.
|
||||
:return: None
|
||||
|
||||
Example::
|
||||
|
||||
def prof_post_chat_message_send(barejid: str, message: str) -> None:
|
||||
prof.cons_show(f"Sent to {barejid}: {message}")
|
||||
"""
|
||||
pass
|
||||
|
||||
# Room Message Handlers
|
||||
# ---------------------
|
||||
|
||||
def prof_pre_room_message_display(barejid, nick, message):
|
||||
"""Called before a chat room message is displayed
|
||||
def prof_pre_room_message_display(barejid: str, nick: str, message: str) -> str | None:
|
||||
"""Called before a chat room message is displayed.
|
||||
|
||||
:param barejid: Jabber ID of the room
|
||||
:param nick: nickname of message sender
|
||||
:param message: the received message
|
||||
:type barejid: str or unicode
|
||||
:type nick: str or unicode
|
||||
:type message: str or unicode
|
||||
:return: the new message to display, or ``None`` to preserve the original message
|
||||
:rtype: str or unicode
|
||||
Allows the plugin to modify or cancel the message display.
|
||||
|
||||
:param barejid: The Jabber ID of the room (e.g., ``chat@conference.example.com``).
|
||||
:param nick: The nickname of the message sender.
|
||||
:param message: The received message.
|
||||
:return: The modified message to display, or None to preserve the original.
|
||||
|
||||
Example::
|
||||
|
||||
def prof_pre_room_message_display(barejid: str, nick: str, message: str) -> str | None:
|
||||
return f"{message} (from {nick})"
|
||||
"""
|
||||
pass
|
||||
|
||||
def prof_post_room_message_display(barejid: str, nick: str, message: str) -> None:
|
||||
"""Called after a chat room message is displayed.
|
||||
|
||||
def prof_post_room_message_display(barejid, nick, message):
|
||||
"""Called after a chat room message is displayed
|
||||
Use this to perform actions after the message is shown.
|
||||
|
||||
:param barejid: Jabber ID of the room
|
||||
:param nick: nickname of the message sender
|
||||
:param message: the received message
|
||||
:type barejid: str or unicode
|
||||
:type nick: str or unicode
|
||||
:type message: str or unicode
|
||||
:param barejid: The Jabber ID of the room (e.g., ``chat@conference.example.com``).
|
||||
:param nick: The nickname of the message sender.
|
||||
:param message: The displayed message.
|
||||
:return: None
|
||||
|
||||
Example::
|
||||
|
||||
def prof_post_room_message_display(barejid: str, nick: str, message: str) -> None:
|
||||
prof.cons_show(f"Displayed in {barejid} from {nick}: {message}")
|
||||
"""
|
||||
pass
|
||||
|
||||
def prof_pre_room_message_send(barejid: str, message: str) -> str | None:
|
||||
"""Called before a chat room message is sent.
|
||||
|
||||
def prof_pre_room_message_send(barejid, message):
|
||||
"""Called before a chat room message is sent
|
||||
Allows the plugin to modify or cancel the message.
|
||||
|
||||
:param barejid: Jabber ID of the room
|
||||
:param message: the message to be sent
|
||||
:type barejid: str or unicode
|
||||
:type message: str or unicode
|
||||
:return: the modified or original message to send, or ``None`` to cancel sending of the message
|
||||
:rtype: str or unicode
|
||||
:param barejid: The Jabber ID of the room (e.g., ``chat@conference.example.com``).
|
||||
:param message: The message to be sent.
|
||||
:return: The modified message to send, or None to cancel sending.
|
||||
|
||||
Example::
|
||||
|
||||
def prof_pre_room_message_send(barejid: str, message: str) -> str | None:
|
||||
return f"{message} (sent by plugin)"
|
||||
"""
|
||||
pass
|
||||
|
||||
def prof_post_room_message_send(barejid: str, message: str) -> None:
|
||||
"""Called after a chat room message is sent.
|
||||
|
||||
def prof_post_room_message_send(barejid, message):
|
||||
"""Called after a chat room message has been sent
|
||||
Use this to log or react to sent messages.
|
||||
|
||||
:param barejid: Jabber ID of the room
|
||||
:param message: the sent message
|
||||
:type barejid: str or unicode
|
||||
:type message: str or unicode
|
||||
:param barejid: The Jabber ID of the room (e.g., ``chat@conference.example.com``).
|
||||
:param message: The sent message.
|
||||
:return: None
|
||||
|
||||
Example::
|
||||
|
||||
def prof_post_room_message_send(barejid: str, message: str) -> None:
|
||||
prof.cons_show(f"Sent to {barejid}: {message}")
|
||||
"""
|
||||
pass
|
||||
|
||||
def prof_on_room_history_message(barejid: str, nick: str, message: str, timestamp: str) -> None:
|
||||
"""Called when a chat room history message is received from the server.
|
||||
|
||||
def prof_on_room_history_message(barejid, nick, message, timestamp):
|
||||
"""Called when the server sends a chat room history message
|
||||
:param barejid: The Jabber ID of the room (e.g., ``chat@conference.example.com``).
|
||||
:param nick: The nickname of the message sender.
|
||||
:param message: The received message.
|
||||
:param timestamp: The message's original send time in ISO 8601 format (e.g., ``2025-09-10T19:45:00Z``).
|
||||
:return: None
|
||||
|
||||
:param barejid: Jabber ID of the room
|
||||
:param nick: nickname of the message sender
|
||||
:param message: the message to be sent
|
||||
:param timestamp: time the message was originally sent to the room, in ISO8601 format
|
||||
:type barejid: str or unicode
|
||||
:type nick: str or unicode
|
||||
:type message: str or unicode
|
||||
:type timestamp: str or unicode
|
||||
Example::
|
||||
|
||||
def prof_on_room_history_message(barejid: str, nick: str, message: str, timestamp: str) -> None:
|
||||
prof.cons_show(f"History in {barejid} from {nick} at {timestamp}: {message}")
|
||||
"""
|
||||
pass
|
||||
|
||||
# Private Message Handlers
|
||||
# ------------------------
|
||||
|
||||
def prof_pre_priv_message_display(barejid, nick, message):
|
||||
"""Called before a private chat room message is displayed
|
||||
def prof_pre_priv_message_display(barejid: str, nick: str, message: str) -> str | None:
|
||||
"""Called before a private chat room message is displayed.
|
||||
|
||||
:param barejid: Jabber ID of the room
|
||||
:param nick: nickname of message sender
|
||||
:param message: the received message
|
||||
:type barejid: str or unicode
|
||||
:type nick: str or unicode
|
||||
:type message: str or unicode
|
||||
:return: the new message to display, or ``None`` to preserve the original message
|
||||
:rtype: str or unicode
|
||||
Allows the plugin to modify or cancel the message display.
|
||||
|
||||
:param barejid: The Jabber ID of the room (e.g., ``chat@conference.example.com``).
|
||||
:param nick: The nickname of the message sender.
|
||||
:param message: The received message.
|
||||
:return: The modified message to display, or None to preserve the original.
|
||||
|
||||
Example::
|
||||
|
||||
def prof_pre_priv_message_display(barejid: str, nick: str, message: str) -> str | None:
|
||||
return f"{message} (private from {nick})"
|
||||
"""
|
||||
pass
|
||||
|
||||
def prof_post_priv_message_display(barejid: str, nick: str, message: str) -> None:
|
||||
"""Called after a private chat room message is displayed.
|
||||
|
||||
def prof_post_priv_message_display(barejid, nick, message):
|
||||
"""Called after a private chat room message is displayed
|
||||
Use this to perform actions after the message is shown.
|
||||
|
||||
:param barejid: Jabber ID of the room
|
||||
:param nick: nickname of the message sender
|
||||
:param message: the received message
|
||||
:type barejid: str or unicode
|
||||
:type nick: str or unicode
|
||||
:type message: str or unicode
|
||||
:param barejid: The Jabber ID of the room (e.g., ``chat@conference.example.com``).
|
||||
:param nick: The nickname of the message sender.
|
||||
:param message: The displayed message.
|
||||
:return: None
|
||||
|
||||
Example::
|
||||
|
||||
def prof_post_priv_message_display(barejid: str, nick: str, message: str) -> None:
|
||||
prof.cons_show(f"Displayed private in {barejid} from {nick}: {message}")
|
||||
"""
|
||||
pass
|
||||
|
||||
def prof_pre_priv_message_send(barejid: str, nick: str, message: str) -> str | None:
|
||||
"""Called before a private chat room message is sent.
|
||||
|
||||
def prof_pre_priv_message_send(barejid, nick, message):
|
||||
"""Called before a private chat room message is sent
|
||||
Allows the plugin to modify or cancel the message.
|
||||
|
||||
:param barejid: Jabber ID of the room
|
||||
:param nick: nickname of message recipient
|
||||
:param message: the message to be sent
|
||||
:type barejid: str or unicode
|
||||
:type nick: str or unicode
|
||||
:type message: str or unicode
|
||||
:return: the modified or original message to send, or ``None`` to cancel sending of the message
|
||||
:rtype: str or unicode
|
||||
:param barejid: The Jabber ID of the room (e.g., ``chat@conference.example.com``).
|
||||
:param nick: The nickname of the message recipient.
|
||||
:param message: The message to be sent.
|
||||
:return: The modified message to send, or None to cancel sending.
|
||||
|
||||
Example::
|
||||
|
||||
def prof_pre_priv_message_send(barejid: str, nick: str, message: str) -> str | None:
|
||||
return f"{message} (private to {nick})"
|
||||
"""
|
||||
pass
|
||||
|
||||
def prof_post_priv_message_send(barejid: str, nick: str, message: str) -> None:
|
||||
"""Called after a private chat room message is sent.
|
||||
|
||||
def prof_post_priv_message_send(barejid, nick, message):
|
||||
"""Called after a private chat room message has been sent
|
||||
Use this to log or react to sent messages.
|
||||
|
||||
:param barejid: Jabber ID of the room
|
||||
:param nick: nickname of the message recipient
|
||||
:param message: the sent message
|
||||
:type barejid: str or unicode
|
||||
:type nick: str or unicode
|
||||
:type message: str or unicode
|
||||
:param barejid: The Jabber ID of the room (e.g., ``chat@conference.example.com``).
|
||||
:param nick: The nickname of the message recipient.
|
||||
:param message: The sent message.
|
||||
:return: None
|
||||
|
||||
Example::
|
||||
|
||||
def prof_post_priv_message_send(barejid: str, nick: str, message: str) -> None:
|
||||
prof.cons_show(f"Sent private to {nick} in {barejid}: {message}")
|
||||
"""
|
||||
pass
|
||||
|
||||
# Stanza Handlers
|
||||
# ---------------
|
||||
|
||||
def prof_on_message_stanza_send(stanza):
|
||||
"""Called before an XMPP message stanza is sent
|
||||
def prof_on_message_stanza_send(stanza: str) -> str | None:
|
||||
"""Called before an XMPP message stanza is sent.
|
||||
|
||||
:param stanza: The stanza to send
|
||||
:type stanza: str or unicode
|
||||
:return: The new stanza to send, or ``None`` to preserve the original stanza
|
||||
:rtype: str or unicode
|
||||
Allows the plugin to modify or cancel the stanza.
|
||||
|
||||
:param stanza: The XMPP message stanza to send.
|
||||
:return: The modified stanza to send, or None to preserve the original.
|
||||
|
||||
Example::
|
||||
|
||||
def prof_on_message_stanza_send(stanza: str) -> str | None:
|
||||
prof.cons_show(f"Sending message stanza: {stanza}")
|
||||
return stanza
|
||||
"""
|
||||
pass
|
||||
|
||||
def prof_on_message_stanza_receive(stanza: str) -> bool:
|
||||
"""Called when an XMPP message stanza is received.
|
||||
|
||||
def prof_on_message_stanza_receive(stanza):
|
||||
"""Called when an XMPP message stanza is received
|
||||
Allows the plugin to control whether CProof processes the stanza.
|
||||
|
||||
:param stanza: The stanza received
|
||||
:type stanza: str or unicode
|
||||
:return: ``True`` if Profanity should continue to process the message stanza, ``False`` otherwise
|
||||
:rtype: boolean
|
||||
:param stanza: The received XMPP message stanza.
|
||||
:return: True to allow CProof to process the stanza, False to block processing.
|
||||
|
||||
Example::
|
||||
|
||||
def prof_on_message_stanza_receive(stanza: str) -> bool:
|
||||
prof.cons_show(f"Received message stanza: {stanza}")
|
||||
return True
|
||||
"""
|
||||
pass
|
||||
|
||||
def prof_on_presence_stanza_send(stanza: str) -> str | None:
|
||||
"""Called before an XMPP presence stanza is sent.
|
||||
|
||||
def prof_on_presence_stanza_send(stanza):
|
||||
"""Called before an XMPP presence stanza is sent
|
||||
Allows the plugin to modify or cancel the stanza.
|
||||
|
||||
:param stanza: The stanza to send
|
||||
:type stanza: str or unicode
|
||||
:return: The new stanza to send, or ``None`` to preserve the original stanza
|
||||
:rtype: str or unicode
|
||||
:param stanza: The XMPP presence stanza to send.
|
||||
:return: The modified stanza to send, or None to preserve the original.
|
||||
|
||||
Example::
|
||||
|
||||
def prof_on_presence_stanza_send(stanza: str) -> str | None:
|
||||
prof.cons_show(f"Sending presence stanza: {stanza}")
|
||||
return stanza
|
||||
"""
|
||||
pass
|
||||
|
||||
def prof_on_presence_stanza_receive(stanza: str) -> bool:
|
||||
"""Called when an XMPP presence stanza is received.
|
||||
|
||||
def prof_on_presence_stanza_receive(stanza):
|
||||
"""Called when an XMPP presence stanza is received
|
||||
Allows the plugin to control whether CProof processes the stanza.
|
||||
|
||||
:param stanza: The stanza received
|
||||
:type stanza: str or unicode
|
||||
:return: ``True`` if Profanity should continue to process the presence stanza, ``False`` otherwise
|
||||
:rtype: boolean
|
||||
:param stanza: The received XMPP presence stanza.
|
||||
:return: True to allow CProof to process the stanza, False to block processing.
|
||||
|
||||
Example::
|
||||
|
||||
def prof_on_presence_stanza_receive(stanza: str) -> bool:
|
||||
prof.cons_show(f"Received presence stanza: {stanza}")
|
||||
return True
|
||||
"""
|
||||
pass
|
||||
|
||||
def prof_on_iq_stanza_send(stanza: str) -> str | None:
|
||||
"""Called before an XMPP IQ stanza is sent.
|
||||
|
||||
def prof_on_iq_stanza_send(stanza):
|
||||
"""Called before an XMPP iq stanza is sent
|
||||
Allows the plugin to modify or cancel the stanza.
|
||||
|
||||
:param stanza: The stanza to send
|
||||
:type stanza: str or unicode
|
||||
:return: The new stanza to send, or ``None`` to preserve the original stanza
|
||||
:rtype: str or unicode
|
||||
:param stanza: The XMPP IQ stanza to send.
|
||||
:return: The modified stanza to send, or None to preserve the original.
|
||||
|
||||
Example::
|
||||
|
||||
def prof_on_iq_stanza_send(stanza: str) -> str | None:
|
||||
prof.cons_show(f"Sending IQ stanza: {stanza}")
|
||||
return stanza
|
||||
"""
|
||||
pass
|
||||
|
||||
def prof_on_iq_stanza_receive(stanza: str) -> bool:
|
||||
"""Called when an XMPP IQ stanza is received.
|
||||
|
||||
def prof_on_iq_stanza_receive(stanza):
|
||||
"""Called when an XMPP iq stanza is received
|
||||
Allows the plugin to control whether CProof processes the stanza.
|
||||
|
||||
:param stanza: The stanza received
|
||||
:type stanza: str or unicode
|
||||
:return: ``True`` if Profanity should continue to process the iq stanza, ``False`` otherwise
|
||||
:rtype: boolean
|
||||
:param stanza: The received XMPP IQ stanza.
|
||||
:return: True to allow CProof to process the stanza, False to block processing.
|
||||
|
||||
Example::
|
||||
|
||||
def prof_on_iq_stanza_receive(stanza: str) -> bool:
|
||||
prof.cons_show(f"Received IQ stanza: {stanza}")
|
||||
return True
|
||||
"""
|
||||
pass
|
||||
|
||||
# Contact Presence Handlers
|
||||
# -------------------------
|
||||
|
||||
def prof_on_contact_offline(barejid, resource, status):
|
||||
"""Called when a contact goes offline
|
||||
def prof_on_contact_offline(barejid: str, resource: str, status: str | None) -> None:
|
||||
"""Called when a contact goes offline.
|
||||
|
||||
:param barejid: Jabber ID of the contact
|
||||
:param resource: the resource being disconnected
|
||||
:param status: the status message received with the offline presence, or ``None``
|
||||
:type barejid: str or unicode
|
||||
:type resource: str or unicode
|
||||
:type status: str or unicode
|
||||
:param barejid: The Jabber ID of the contact (e.g., ``bob@example.com``).
|
||||
:param resource: The resource being disconnected (e.g., ``laptop``).
|
||||
:param status: The status message received with the offline presence, or None.
|
||||
:return: None
|
||||
|
||||
Example::
|
||||
|
||||
def prof_on_contact_offline(barejid: str, resource: str, status: str | None) -> None:
|
||||
prof.cons_show(f"{barejid}/{resource} went offline: {status or 'No status'}")
|
||||
"""
|
||||
pass
|
||||
|
||||
def prof_on_contact_presence(barejid: str, resource: str, presence: str, status: str | None, priority: int) -> None:
|
||||
"""Called when a presence notification is received from a contact.
|
||||
|
||||
def prof_on_contact_presence(barejid, resource, presence, status, priority):
|
||||
"""Called when a presence notification is received from a contact
|
||||
:param barejid: The Jabber ID of the contact (e.g., ``bob@example.com``).
|
||||
:param resource: The resource of the contact (e.g., ``laptop``).
|
||||
:param presence: The contact's presence (``"chat"``, ``"online"``, ``"away"``, ``"xa"``, or ``"dnd"``).
|
||||
:param status: The status message received with the presence, or None.
|
||||
:param priority: The priority associated with the resource.
|
||||
:return: None
|
||||
|
||||
:param barejid: Jabber ID of the contact
|
||||
:param resource: the resource being disconnected
|
||||
:param presence: presence of the contact, one of ``"chat"``, ``"online"``, ``"away"``, ``"xa"`` or ``"dnd"``
|
||||
:param status: the status message received with the presence, or ``None``
|
||||
:param priority: the priority associated with the resource
|
||||
:type barejid: str or unicode
|
||||
:type resource: str or unicode
|
||||
:type presence: str or unicode
|
||||
:type status: str or unicode
|
||||
:type priority: int
|
||||
Example::
|
||||
|
||||
def prof_on_contact_presence(barejid: str, resource: str, presence: str, status: str | None, priority: int) -> None:
|
||||
prof.notify(f"{barejid} is {presence}", 5000, "Presence")
|
||||
"""
|
||||
pass
|
||||
|
||||
# Window Focus Handlers
|
||||
# ---------------------
|
||||
|
||||
def prof_on_chat_win_focus(barejid):
|
||||
"""Called when a chat window is focused
|
||||
def prof_on_chat_win_focus(barejid: str) -> None:
|
||||
"""Called when a chat window is focused.
|
||||
|
||||
:param barejid: Jabber ID of the chat window recipient
|
||||
:type barejid: str or unicode
|
||||
:param barejid: The Jabber ID of the chat window recipient (e.g., ``bob@example.com``).
|
||||
:return: None
|
||||
|
||||
Example::
|
||||
|
||||
def prof_on_chat_win_focus(barejid: str) -> None:
|
||||
prof.cons_show(f"Focused chat window for {barejid}")
|
||||
"""
|
||||
pass
|
||||
|
||||
def prof_on_room_win_focus(barejid: str) -> None:
|
||||
"""Called when a chat room window is focused.
|
||||
|
||||
def prof_on_room_win_focus(barejid):
|
||||
"""Called when a chat room window is focused
|
||||
:param barejid: The Jabber ID of the room (e.g., ``chat@conference.example.com``).
|
||||
:return: None
|
||||
|
||||
:param barejid: Jabber ID of the room
|
||||
:type barejid: str or unicode
|
||||
Example::
|
||||
|
||||
def prof_on_room_win_focus(barejid: str) -> None:
|
||||
prof.cons_show(f"Focused room window for {barejid}")
|
||||
"""
|
||||
pass
|
||||
pass
|
||||
File diff suppressed because it is too large
Load Diff
119
check-cwe134.sh
Executable file
119
check-cwe134.sh
Executable file
@@ -0,0 +1,119 @@
|
||||
#!/bin/bash
|
||||
# check-cwe134.sh - Verify __attribute__((format)) on printf-like wrappers
|
||||
#
|
||||
# CWE-134 format string vulnerabilities are caught at compile time by
|
||||
# -Wformat=2 (includes -Wformat-security + -Wformat-nonliteral), BUT only
|
||||
# for functions annotated with __attribute__((format(printf, N, M))) or
|
||||
# G_GNUC_PRINTF(N, M).
|
||||
#
|
||||
# This script ensures every variadic function whose last fixed parameter
|
||||
# looks like a format string has the annotation. Without it, the compiler
|
||||
# silently ignores format misuse.
|
||||
#
|
||||
# Usage: ./check-cwe134.sh [directory]
|
||||
|
||||
set -e
|
||||
|
||||
DIR="${1:-src}"
|
||||
|
||||
echo "=== CWE-134: format attribute audit ==="
|
||||
echo "Scanning: $DIR"
|
||||
echo ""
|
||||
|
||||
ERRORS=0
|
||||
|
||||
# --------------------------------------------------------------------- #
|
||||
# Known printf-like wrappers that MUST have the attribute #
|
||||
# --------------------------------------------------------------------- #
|
||||
REQUIRED_ATTRIBUTED=(
|
||||
"cons_show"
|
||||
"cons_debug"
|
||||
"cons_show_error"
|
||||
"cons_show_padded"
|
||||
"log_debug"
|
||||
"log_info"
|
||||
"log_warning"
|
||||
"log_error"
|
||||
"win_print"
|
||||
"win_println"
|
||||
"win_println_indent"
|
||||
"win_println_va"
|
||||
"win_append"
|
||||
"win_appendln"
|
||||
"win_append_highlight"
|
||||
"win_appendln_highlight"
|
||||
"win_command_exec_error"
|
||||
)
|
||||
|
||||
echo "Check 1: Known wrappers must have G_GNUC_PRINTF / __attribute__((format))"
|
||||
echo ""
|
||||
|
||||
for func in "${REQUIRED_ATTRIBUTED[@]}"; do
|
||||
HAS_ATTR=$(grep -B1 --include="*.h" -rn "void ${func}\s*(" "$DIR" 2>/dev/null \
|
||||
| grep -Ec "format\(printf|G_GNUC_PRINTF" || true)
|
||||
if [ "$HAS_ATTR" -eq 0 ]; then
|
||||
echo " ❌ $func — missing format attribute"
|
||||
ERRORS=$((ERRORS + 1))
|
||||
fi
|
||||
done
|
||||
|
||||
if [ "$ERRORS" -eq 0 ]; then
|
||||
echo " ✅ All known wrappers annotated."
|
||||
fi
|
||||
|
||||
# --------------------------------------------------------------------- #
|
||||
# Auto-detect new variadic functions that look like printf wrappers #
|
||||
# but are NOT in the known list and NOT annotated. #
|
||||
# Heuristic: declaration has (... const char* ..., ...) and no attribute #
|
||||
# --------------------------------------------------------------------- #
|
||||
echo ""
|
||||
echo "Check 2: Detect unannotated printf-like variadic declarations in headers"
|
||||
echo ""
|
||||
|
||||
KNOWN_RE=$(IFS="|"; echo "${REQUIRED_ATTRIBUTED[*]}")
|
||||
|
||||
# Find variadic declarations with a const char* / gchar* parameter followed by ...)
|
||||
# that do NOT have a format attribute on the preceding line
|
||||
NEW_ISSUES=$(grep -B1 -rn --include="*.h" \
|
||||
'const g\?char\s*\*.*,\s*\.\.\.)' "$DIR" 2>/dev/null \
|
||||
| awk '
|
||||
/format\(printf|G_GNUC_PRINTF/ { skip=1; next }
|
||||
/const g?char.*,.*\.\.\.\)/ {
|
||||
if (skip) { skip=0; next }
|
||||
print
|
||||
}
|
||||
{ skip=0 }
|
||||
' \
|
||||
| grep -E "void\s+\w+\s*\(" \
|
||||
| grep -Ev "($KNOWN_RE)" \
|
||||
|| true)
|
||||
|
||||
if [ -n "$NEW_ISSUES" ]; then
|
||||
echo " ⚠️ Possibly unannotated new printf-like functions:"
|
||||
echo ""
|
||||
echo "$NEW_ISSUES"
|
||||
echo ""
|
||||
NEW_COUNT=$(echo "$NEW_ISSUES" | wc -l)
|
||||
ERRORS=$((ERRORS + NEW_COUNT))
|
||||
else
|
||||
echo " ✅ No unannotated variadic printf-like functions found."
|
||||
fi
|
||||
|
||||
# --------------------------------------------------------------------- #
|
||||
# Summary #
|
||||
# --------------------------------------------------------------------- #
|
||||
echo ""
|
||||
echo "=== Summary ==="
|
||||
echo "Issues: $ERRORS"
|
||||
echo ""
|
||||
|
||||
if [ "$ERRORS" -gt 0 ]; then
|
||||
echo "Fix: add the attribute before the declaration in the .h file:"
|
||||
echo " G_GNUC_PRINTF(N, M) // N = format arg, M = first vararg"
|
||||
echo " void my_func(ProfWin* w, const char* fmt, ...);"
|
||||
echo ""
|
||||
echo "The compiler flag -Wformat=2 will then catch all misuse automatically."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
exit 0
|
||||
456
ci-build.sh
456
ci-build.sh
@@ -15,14 +15,144 @@ error_handler()
|
||||
log_content ./test-suite.log
|
||||
log_content ./test-suite-memcheck.log
|
||||
|
||||
echo
|
||||
echo "Error ${ERR_CODE} with command '${BASH_COMMAND}' on line ${BASH_LINENO[0]}. Exiting."
|
||||
echo
|
||||
echo >&2
|
||||
echo "Error ${ERR_CODE} with command '${BASH_COMMAND}' on line ${BASH_LINENO[0]}. Exiting." >&2
|
||||
echo >&2
|
||||
exit ${ERR_CODE}
|
||||
}
|
||||
|
||||
trap error_handler ERR
|
||||
|
||||
# =============================================================================
|
||||
# Constants
|
||||
# =============================================================================
|
||||
|
||||
# Number of parallel build configurations
|
||||
readonly TEST_BUILDS=4
|
||||
|
||||
# Human-readable names for each build configuration
|
||||
readonly BUILD_NAMES=(
|
||||
"Full" # 1. All features enabled
|
||||
"Minimal" # 2. All optional features disabled
|
||||
"NoEncrypt" # 3. No encryption (otr, pgp, omemo disabled)
|
||||
"Default" # 4. Default ./configure options
|
||||
)
|
||||
|
||||
# Regex patterns for parsing test output
|
||||
readonly CMOCKA_PASSED_PATTERN='^\[ PASSED \] [0-9]+ test'
|
||||
readonly CMOCKA_FAILED_PATTERN='^\[ FAILED \] [0-9]+ test'
|
||||
|
||||
# Coverage extraction patterns (matches both Docker and CI paths)
|
||||
readonly COVERAGE_PATTERNS='*/profanity/src/* */src/src/*'
|
||||
|
||||
# =============================================================================
|
||||
# Helper Functions
|
||||
# =============================================================================
|
||||
|
||||
# Parse STATS line from build log and set global variables
|
||||
# Usage: parse_build_stats <log_file>
|
||||
parse_build_stats() {
|
||||
local log_file="$1"
|
||||
local stats_line
|
||||
stats_line=$(grep "^STATS:" "$log_file" 2>/dev/null | tail -1)
|
||||
|
||||
STAT_UNIT_P=$(echo "$stats_line" | grep -oE "unit_passed=[0-9]+" | cut -d= -f2)
|
||||
STAT_UNIT_F=$(echo "$stats_line" | grep -oE "unit_failed=[0-9]+" | cut -d= -f2)
|
||||
STAT_FUNC_P=$(echo "$stats_line" | grep -oE "func_passed=[0-9]+" | cut -d= -f2)
|
||||
STAT_FUNC_F=$(echo "$stats_line" | grep -oE "func_failed=[0-9]+" | cut -d= -f2)
|
||||
STAT_COV_LINES=$(echo "$stats_line" | grep -oE "cov_lines=[0-9.]+%|cov_lines=n/a" | cut -d= -f2)
|
||||
STAT_COV_FUNCS=$(echo "$stats_line" | grep -oE "cov_funcs=[0-9.]+%|cov_funcs=n/a" | cut -d= -f2)
|
||||
STAT_COV_BRANCHES=$(echo "$stats_line" | grep -oE "cov_branches=[0-9.]+%|cov_branches=n/a" | cut -d= -f2)
|
||||
STAT_TIME=$(echo "$stats_line" | grep -oE "time=[0-9]+m[0-9]+s" | cut -d= -f2)
|
||||
|
||||
: "${STAT_UNIT_P:=0}"
|
||||
: "${STAT_UNIT_F:=0}"
|
||||
: "${STAT_FUNC_P:=0}"
|
||||
: "${STAT_FUNC_F:=0}"
|
||||
}
|
||||
|
||||
# Extract test count from log file
|
||||
# Usage: extract_test_count <log_file> <pattern>
|
||||
extract_test_count() {
|
||||
grep -E "$2" "$1" 2>/dev/null | grep -oE "[0-9]+" | head -1
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# Test Verification
|
||||
# =============================================================================
|
||||
|
||||
# Verify that test failures are properly detected
|
||||
# This is a meta-test: it runs a deliberately failing test
|
||||
# and checks that the test framework reports the failure correctly
|
||||
verify_test_failure_detection()
|
||||
{
|
||||
echo "==> Verifying test failure detection..."
|
||||
|
||||
# Create a simple failing test
|
||||
cat > /tmp/test_must_fail.c << 'EOF'
|
||||
#include <stdarg.h>
|
||||
#include <stddef.h>
|
||||
#include <setjmp.h>
|
||||
#include <cmocka.h>
|
||||
|
||||
static void test_that_must_fail(void **state) {
|
||||
(void)state;
|
||||
assert_true(0); // This MUST fail
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
const struct CMUnitTest tests[] = {
|
||||
cmocka_unit_test(test_that_must_fail),
|
||||
};
|
||||
return cmocka_run_group_tests(tests, NULL, NULL);
|
||||
}
|
||||
EOF
|
||||
|
||||
# Compile the failing test
|
||||
if ! gcc -o /tmp/test_must_fail /tmp/test_must_fail.c -lcmocka 2>/dev/null; then
|
||||
echo "Warning: Could not compile test failure verification (cmocka not available?)"
|
||||
echo "Skipping test failure detection verification"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Test 1: Single failing test detection
|
||||
echo " Testing single test failure detection..."
|
||||
if /tmp/test_must_fail > /tmp/test_must_fail.log 2>&1; then
|
||||
echo "ERROR: Test that should fail returned success (exit code 0)" >&2
|
||||
echo "This means the test framework is NOT detecting failures correctly!" >&2
|
||||
echo "--- Test output ---" >&2
|
||||
cat /tmp/test_must_fail.log >&2
|
||||
echo "--- End output ---" >&2
|
||||
rm -f /tmp/test_must_fail /tmp/test_must_fail.c /tmp/test_must_fail.log
|
||||
exit 1
|
||||
fi
|
||||
echo " ✓ Single test failure correctly detected"
|
||||
|
||||
# Test 2: Parallel failure detection (simulates check-functional-parallel)
|
||||
echo " Testing parallel test failure detection..."
|
||||
failed=0
|
||||
/tmp/test_must_fail > /tmp/p1.log 2>&1 & pid1=$!
|
||||
true > /tmp/p2.log 2>&1 & pid2=$! # This passes
|
||||
/tmp/test_must_fail > /tmp/p3.log 2>&1 & pid3=$!
|
||||
true > /tmp/p4.log 2>&1 & pid4=$! # This passes
|
||||
|
||||
wait $pid1 || failed=$((failed + 1))
|
||||
wait $pid2 || failed=$((failed + 1))
|
||||
wait $pid3 || failed=$((failed + 1))
|
||||
wait $pid4 || failed=$((failed + 1))
|
||||
|
||||
if [ $failed -ne 2 ]; then
|
||||
echo "ERROR: Expected 2 failures in parallel tests, got $failed" >&2
|
||||
echo "Parallel failure detection is broken!" >&2
|
||||
rm -f /tmp/test_must_fail /tmp/test_must_fail.c /tmp/test_must_fail.log /tmp/p?.log
|
||||
exit 1
|
||||
fi
|
||||
echo " ✓ Parallel test failures correctly detected (2 of 4 failed as expected)"
|
||||
|
||||
rm -f /tmp/test_must_fail /tmp/test_must_fail.c /tmp/test_must_fail.log /tmp/p?.log
|
||||
echo "✓ Test failure detection verified"
|
||||
}
|
||||
|
||||
num_cores()
|
||||
{
|
||||
# Check for cores, for systems with:
|
||||
@@ -34,6 +164,20 @@ num_cores()
|
||||
|| getconf _NPROCESSORS_ONLN 2>/dev/null
|
||||
}
|
||||
|
||||
# Run test failure detection verification first
|
||||
verify_test_failure_detection
|
||||
|
||||
# Parse arguments
|
||||
COVERAGE_ONLY=no
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--coverage-only)
|
||||
COVERAGE_ONLY=yes
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
./bootstrap.sh
|
||||
|
||||
tests=()
|
||||
@@ -44,49 +188,46 @@ ARCH="$(uname | tr '[:upper:]' '[:lower:]')"
|
||||
|
||||
case "$ARCH" in
|
||||
linux*)
|
||||
# 4 configurations for parallel CI
|
||||
tests=(
|
||||
# 1. Full build (all features enabled)
|
||||
"--enable-notifications --enable-icons-and-clipboard --enable-otr --enable-pgp
|
||||
--enable-omemo --enable-plugins --enable-c-plugins
|
||||
--enable-python-plugins --with-xscreensaver --enable-omemo-qrcode --enable-gdk-pixbuf"
|
||||
# 2. Minimal build (all optional features disabled)
|
||||
"--disable-notifications --disable-icons-and-clipboard --disable-otr --disable-pgp
|
||||
--disable-omemo --disable-plugins --disable-c-plugins
|
||||
--disable-python-plugins --without-xscreensaver"
|
||||
"--disable-notifications"
|
||||
"--disable-icons-and-clipboard"
|
||||
"--disable-otr"
|
||||
"--disable-pgp"
|
||||
"--disable-omemo --disable-omemo-qrcode"
|
||||
"--disable-pgp --disable-otr"
|
||||
"--disable-pgp --disable-otr --disable-omemo"
|
||||
"--disable-plugins"
|
||||
"--disable-python-plugins"
|
||||
"--disable-c-plugins"
|
||||
"--disable-c-plugins --disable-python-plugins"
|
||||
"--without-xscreensaver"
|
||||
"--disable-gdk-pixbuf"
|
||||
"")
|
||||
--disable-python-plugins --without-xscreensaver --disable-omemo-qrcode --disable-gdk-pixbuf"
|
||||
# 3. No encryption (disable otr, pgp, omemo)
|
||||
"--disable-pgp --disable-otr --disable-omemo --disable-omemo-qrcode"
|
||||
# 4. Default configuration
|
||||
""
|
||||
)
|
||||
source /etc/profile.d/debuginfod.sh 2>/dev/null || true
|
||||
|
||||
if grep -q 'ID=arch' /etc/os-release 2>/dev/null && [ -f /etc/makepkg.conf ]; then
|
||||
echo "--> [Parity Mode] Simulating Pikaur collision..."
|
||||
set -a
|
||||
source /etc/makepkg.conf
|
||||
set +a
|
||||
fi
|
||||
;;
|
||||
darwin*)
|
||||
# 4 configurations for parallel CI
|
||||
tests=(
|
||||
# 1. Full build (all features enabled)
|
||||
"--enable-notifications --enable-icons-and-clipboard --enable-otr --enable-pgp
|
||||
--enable-omemo --enable-plugins --enable-c-plugins
|
||||
--enable-python-plugins"
|
||||
# 2. Minimal build (all optional features disabled)
|
||||
"--disable-notifications --disable-icons-and-clipboard --disable-otr --disable-pgp
|
||||
--disable-omemo --disable-plugins --disable-c-plugins
|
||||
--disable-python-plugins"
|
||||
"--disable-notifications"
|
||||
"--disable-icons-and-clipboard"
|
||||
"--disable-otr"
|
||||
"--disable-pgp"
|
||||
"--disable-omemo"
|
||||
"--disable-pgp --disable-otr"
|
||||
# 3. No encryption (disable otr, pgp, omemo)
|
||||
"--disable-pgp --disable-otr --disable-omemo"
|
||||
"--disable-plugins"
|
||||
"--disable-python-plugins"
|
||||
"--disable-c-plugins"
|
||||
"--disable-c-plugins --disable-python-plugins"
|
||||
"")
|
||||
# 4. Default configuration
|
||||
""
|
||||
)
|
||||
;;
|
||||
openbsd*)
|
||||
MAKE="gmake"
|
||||
@@ -96,59 +237,242 @@ case "$ARCH" in
|
||||
# src/event/server_events.c:1454:19: error: universal character names are only valid in C++ and C99
|
||||
CC="egcc -std=gnu99 -fexec-charset=UTF-8"
|
||||
|
||||
# 4 configurations for parallel CI
|
||||
tests=(
|
||||
# 1. Full build (all features enabled)
|
||||
"--enable-notifications --enable-icons-and-clipboard --enable-otr --enable-pgp
|
||||
--enable-omemo --enable-plugins --enable-c-plugins
|
||||
--enable-python-plugins"
|
||||
# 2. Minimal build (all optional features disabled)
|
||||
"--disable-notifications --disable-icons-and-clipboard --disable-otr --disable-pgp
|
||||
--disable-omemo --disable-plugins --disable-c-plugins
|
||||
--disable-python-plugins"
|
||||
"--disable-notifications"
|
||||
"--disable-icons-and-clipboard"
|
||||
"--disable-otr"
|
||||
"--disable-pgp"
|
||||
"--disable-omemo"
|
||||
"--disable-pgp --disable-otr"
|
||||
# 3. No encryption (disable otr, pgp, omemo)
|
||||
"--disable-pgp --disable-otr --disable-omemo"
|
||||
"--disable-plugins"
|
||||
"--disable-python-plugins"
|
||||
"--disable-c-plugins"
|
||||
"--disable-c-plugins --disable-python-plugins"
|
||||
"")
|
||||
# 4. Default configuration
|
||||
""
|
||||
)
|
||||
;;
|
||||
esac
|
||||
|
||||
case "$ARCH" in
|
||||
linux*)
|
||||
echo
|
||||
echo "--> Building with ./configure ${tests[0]} --enable-valgrind $*"
|
||||
echo
|
||||
# Function to build and test a single configuration
|
||||
build_and_test() {
|
||||
local features="$1"
|
||||
local extra_args="$2"
|
||||
local idx="$3"
|
||||
local run_valgrind="$4"
|
||||
local run_coverage="$5"
|
||||
local build_dir="build-$idx"
|
||||
local log_file="build-$idx.log"
|
||||
|
||||
# shellcheck disable=SC2086
|
||||
./configure ${tests[0]} --enable-valgrind $*
|
||||
{
|
||||
echo "=== Build $idx started at $(date) ==="
|
||||
echo "--> Building in $build_dir with ./configure -C $features $extra_args"
|
||||
|
||||
$MAKE CC="${CC}"
|
||||
if grep '^ID=' /etc/os-release | grep -q -e debian; then
|
||||
$MAKE check-valgrind
|
||||
else
|
||||
$MAKE check-valgrind || log_content ./test-suite-memcheck.log
|
||||
local start_time=$SECONDS
|
||||
|
||||
mkdir -p "$build_dir"
|
||||
cd "$build_dir"
|
||||
|
||||
# shellcheck disable=SC2086
|
||||
if ! ../configure -C $features $extra_args; then
|
||||
echo "ERROR: configure failed"
|
||||
exit 1
|
||||
fi
|
||||
$MAKE distclean
|
||||
;;
|
||||
esac
|
||||
|
||||
for features in "${tests[@]}"
|
||||
do
|
||||
if ! $MAKE CC="${CC}"; then
|
||||
echo "ERROR: make failed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Run unit tests
|
||||
local unit_passed=0 unit_failed=0
|
||||
if [ "$run_valgrind" = "yes" ]; then
|
||||
echo "--> Running unit tests under Valgrind..."
|
||||
# Build unit tests first
|
||||
$MAKE tests/unittests/unittests
|
||||
# Run valgrind directly to capture cmocka output
|
||||
valgrind --error-exitcode=1 --leak-check=full \
|
||||
--suppressions=../prof.supp \
|
||||
tests/unittests/unittests 2>&1 | tee unit-tests-output.log
|
||||
valgrind_exit=${PIPESTATUS[0]}
|
||||
if [ $valgrind_exit -ne 0 ]; then
|
||||
echo "ERROR: Valgrind unit tests failed (exit code $valgrind_exit)"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo "--> Running unit tests..."
|
||||
$MAKE tests/unittests/unittests
|
||||
tests/unittests/unittests 2>&1 | tee unit-tests-output.log
|
||||
if [ ${PIPESTATUS[0]} -ne 0 ]; then
|
||||
echo "ERROR: Unit tests failed"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
# Extract unit test counts from cmocka output
|
||||
unit_passed=$(extract_test_count unit-tests-output.log "$CMOCKA_PASSED_PATTERN")
|
||||
unit_failed=$(extract_test_count unit-tests-output.log "$CMOCKA_FAILED_PATTERN")
|
||||
: "${unit_passed:=0}" "${unit_failed:=0}"
|
||||
echo "UNIT_TESTS: passed=$unit_passed failed=$unit_failed"
|
||||
|
||||
# Set build index for port allocation: build 1 uses ports 5230-5233,
|
||||
# build 2 uses 5234-5237, etc. This prevents port conflicts in parallel builds.
|
||||
export PROF_BUILD_INDEX=$idx
|
||||
local func_passed=0 func_failed=0
|
||||
if ! $MAKE check-functional-parallel; then
|
||||
echo "ERROR: functional tests failed"
|
||||
exit 1
|
||||
fi
|
||||
# Extract functional test counts from group logs
|
||||
echo "=== Functional test results ==="
|
||||
for glog in ./test-logs/group*.log; do
|
||||
if [ -f "$glog" ]; then
|
||||
cnt=$(extract_test_count "$glog" "$CMOCKA_PASSED_PATTERN")
|
||||
[ -n "$cnt" ] && func_passed=$((func_passed + cnt))
|
||||
cnt=$(extract_test_count "$glog" "$CMOCKA_FAILED_PATTERN")
|
||||
[ -n "$cnt" ] && func_failed=$((func_failed + cnt))
|
||||
fi
|
||||
done
|
||||
echo "FUNC_TESTS: passed=$func_passed failed=$func_failed"
|
||||
|
||||
# Collect coverage data if enabled (lines, functions, branches)
|
||||
# Must be done BEFORE make clean which removes .gcda files
|
||||
local cov_lines="n/a" cov_funcs="n/a" cov_branches="n/a"
|
||||
if [ "$run_coverage" = "yes" ]; then
|
||||
echo "--> Collecting coverage data..."
|
||||
if command -v lcov >/dev/null 2>&1; then
|
||||
lcov --capture --directory . --output-file coverage-full.info \
|
||||
--rc lcov_branch_coverage=1 --ignore-errors inconsistent 2>&1 || true
|
||||
# Extract only production code from src/ directory, exclude tests
|
||||
# shellcheck disable=SC2086
|
||||
lcov --extract coverage-full.info $COVERAGE_PATTERNS \
|
||||
--output-file coverage.info \
|
||||
--rc lcov_branch_coverage=1 --ignore-errors inconsistent 2>&1 || true
|
||||
if [ -f coverage.info ] && [ -s coverage.info ]; then
|
||||
local summary
|
||||
summary=$(lcov --summary coverage.info \
|
||||
--rc lcov_branch_coverage=1 --ignore-errors inconsistent 2>&1 || true)
|
||||
cov_lines=$(echo "$summary" | grep -E "lines\.*:" | grep -oE "[0-9]+\.[0-9]+%" | head -1)
|
||||
cov_funcs=$(echo "$summary" | grep -E "functions\.*:" | grep -oE "[0-9]+\.[0-9]+%" | head -1)
|
||||
cov_branches=$(echo "$summary" | grep -E "branches\.*:" | grep -oE "[0-9]+\.[0-9]+%" | head -1)
|
||||
[ -z "$cov_lines" ] && cov_lines="n/a"
|
||||
[ -z "$cov_funcs" ] && cov_funcs="n/a"
|
||||
[ -z "$cov_branches" ] && cov_branches="n/a"
|
||||
else
|
||||
echo "WARNING: coverage.info is empty or not created"
|
||||
fi
|
||||
else
|
||||
echo "WARNING: lcov not found"
|
||||
fi
|
||||
echo "COVERAGE: lines=$cov_lines funcs=$cov_funcs branches=$cov_branches"
|
||||
fi
|
||||
|
||||
./profanity -v
|
||||
|
||||
# Save coverage.info before cleanup (for CI artifact)
|
||||
# Only copy in CI environment to avoid leaving artifacts during local runs
|
||||
if [ "$run_coverage" = "yes" ] && [ -f coverage.info ] && [ -n "$CI" ]; then
|
||||
cp coverage.info ../coverage.info
|
||||
echo "Coverage report saved to coverage.info"
|
||||
fi
|
||||
|
||||
$MAKE clean
|
||||
|
||||
cd ..
|
||||
rm -rf "$build_dir"
|
||||
|
||||
local elapsed=$((SECONDS - start_time))
|
||||
local mins=$((elapsed / 60))
|
||||
local secs=$((elapsed % 60))
|
||||
|
||||
echo "=== Build $idx completed at $(date) ==="
|
||||
echo "STATS: unit_passed=$unit_passed unit_failed=$unit_failed func_passed=$func_passed func_failed=$func_failed cov_lines=$cov_lines cov_funcs=$cov_funcs cov_branches=$cov_branches time=${mins}m${secs}s"
|
||||
} > "$log_file" 2>&1
|
||||
}
|
||||
|
||||
# Run configurations
|
||||
# Coverage enabled only for build 1 (Full) - it has most code paths
|
||||
echo
|
||||
echo "=== Start build ==="
|
||||
echo
|
||||
|
||||
if [ "$COVERAGE_ONLY" = "yes" ]; then
|
||||
echo "Running coverage-only mode (${BUILD_NAMES[0]} build)..."
|
||||
echo
|
||||
echo "--> Building with ./configure ${features} $*"
|
||||
run_valgrind="no"
|
||||
run_coverage="yes"
|
||||
extra_flags="--enable-coverage"
|
||||
build_and_test "${tests[0]}" "$* $extra_flags" "1" "$run_valgrind" "$run_coverage" &
|
||||
pids=("$!")
|
||||
echo " → ${BUILD_NAMES[0]}: ${tests[0]} [+Coverage]"
|
||||
else
|
||||
echo "Starting $TEST_BUILDS parallel build configurations..."
|
||||
echo
|
||||
pids=()
|
||||
for idx in $(seq 1 $TEST_BUILDS); do
|
||||
if [ $idx -le ${#tests[@]} ]; then
|
||||
# All builds run Valgrind on Linux
|
||||
if [ "$ARCH" = "linux" ]; then
|
||||
run_valgrind="yes"
|
||||
extra_flags="--enable-valgrind"
|
||||
else
|
||||
run_valgrind="no"
|
||||
extra_flags=""
|
||||
fi
|
||||
run_coverage="no"
|
||||
build_and_test "${tests[$((idx-1))]}" "$* $extra_flags" "$idx" "$run_valgrind" "$run_coverage" &
|
||||
pids+=("$!")
|
||||
flags_desc=""
|
||||
[ "$run_valgrind" = "yes" ] && flags_desc=" [+Valgrind]"
|
||||
echo " → ${BUILD_NAMES[$((idx-1))]}: ${tests[$((idx-1))]}$flags_desc"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
echo
|
||||
|
||||
# shellcheck disable=SC2086
|
||||
./configure $features $*
|
||||
|
||||
$MAKE CC="${CC}"
|
||||
$MAKE check
|
||||
|
||||
./profanity -v
|
||||
$MAKE clean
|
||||
# Wait for all builds and check exit codes
|
||||
echo "Waiting for builds to complete..."
|
||||
echo
|
||||
failed_builds=()
|
||||
for i in "${!pids[@]}"; do
|
||||
idx=$((i + 1))
|
||||
if wait "${pids[$i]}"; then
|
||||
if [ -f "build-$idx.log" ]; then
|
||||
parse_build_stats "build-$idx.log"
|
||||
|
||||
echo "✓ ${BUILD_NAMES[$i]} PASSED"
|
||||
echo " Unit tests: $STAT_UNIT_P passed, $STAT_UNIT_F failed"
|
||||
echo " Functional tests: $STAT_FUNC_P passed, $STAT_FUNC_F failed"
|
||||
if [ "$STAT_COV_LINES" != "n/a" ] && [ -n "$STAT_COV_LINES" ]; then
|
||||
echo " Coverage: Lines: $STAT_COV_LINES | Functions: $STAT_COV_FUNCS | Branches: $STAT_COV_BRANCHES"
|
||||
fi
|
||||
echo " Duration: ${STAT_TIME:-?}"
|
||||
else
|
||||
echo "✓ ${BUILD_NAMES[$i]} passed (no stats available)"
|
||||
fi
|
||||
else
|
||||
echo "✗ ${BUILD_NAMES[$i]} FAILED" >&2
|
||||
failed_builds+=("$idx")
|
||||
fi
|
||||
echo
|
||||
done
|
||||
|
||||
# Show failed builds full logs
|
||||
for idx in "${failed_builds[@]}"; do
|
||||
if [ -f "build-$idx.log" ]; then
|
||||
echo "=== ${BUILD_NAMES[$((idx-1))]} FAILURE LOG ===" >&2
|
||||
cat "build-$idx.log" >&2
|
||||
echo >&2
|
||||
fi
|
||||
done
|
||||
|
||||
if [ ${#failed_builds[@]} -gt 0 ]; then
|
||||
echo "RESULT: FAILED (builds ${failed_builds[*]})" >&2
|
||||
exit 1
|
||||
else
|
||||
if [ "$COVERAGE_ONLY" = "yes" ]; then
|
||||
echo "RESULT: COVERAGE BUILD PASSED ✓"
|
||||
else
|
||||
echo "RESULT: ALL $TEST_BUILDS BUILDS PASSED ✓"
|
||||
fi
|
||||
fi
|
||||
|
||||
@@ -1,13 +1,28 @@
|
||||
FROM archlinux
|
||||
FROM archlinux:latest
|
||||
|
||||
RUN pacman -Syu --noconfirm && pacman -S --needed --noconfirm \
|
||||
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
|
||||
|
||||
# reflector is optional - if it fails due to network issues, continue with default mirrorlist
|
||||
RUN pacman -S --needed --noconfirm reflector && \
|
||||
(reflector --latest 20 --protocol https --sort rate --save /etc/pacman.d/mirrorlist || true)
|
||||
RUN pacman-key --init
|
||||
RUN pacman -S --needed --noconfirm \
|
||||
autoconf \
|
||||
autoconf-archive \
|
||||
automake \
|
||||
base-devel \
|
||||
ca-certificates \
|
||||
ccache \
|
||||
check \
|
||||
cmake \
|
||||
cmocka \
|
||||
lcov \
|
||||
curl \
|
||||
debuginfod \
|
||||
doxygen \
|
||||
@@ -15,7 +30,7 @@ RUN pacman -Syu --noconfirm && pacman -S --needed --noconfirm \
|
||||
gcc \
|
||||
git \
|
||||
gpgme \
|
||||
gtk2 \
|
||||
gtk3 \
|
||||
libgcrypt \
|
||||
libmicrohttpd \
|
||||
libnotify \
|
||||
@@ -54,13 +69,13 @@ USER root
|
||||
RUN pacman -U --noconfirm libstrophe-git/libstrophe-git-*.pkg.tar.zst
|
||||
|
||||
WORKDIR /usr/src
|
||||
#RUN git clone https://github.com/boothj5/stabber
|
||||
RUN git clone --depth 1 https://git.jabber.space/devs/stabber
|
||||
|
||||
#WORKDIR /usr/src/stabber
|
||||
#RUN ./bootstrap.sh
|
||||
#RUN ./configure --prefix=/usr --disable-dependency-tracking
|
||||
#RUN make
|
||||
#RUN make install
|
||||
WORKDIR /usr/src/stabber
|
||||
RUN ./bootstrap.sh
|
||||
RUN ./configure --prefix=/usr --disable-dependency-tracking
|
||||
RUN make -j$(nproc)
|
||||
RUN make install
|
||||
|
||||
WORKDIR /usr/src/profanity
|
||||
COPY . /usr/src/profanity
|
||||
@@ -1,13 +1,19 @@
|
||||
# Build the latest Debian testing image
|
||||
FROM debian:testing
|
||||
|
||||
ENV DEBIAN_FRONTEND="noninteractive"
|
||||
ENV TERM=xterm
|
||||
ENV CC="ccache gcc"
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
autoconf \
|
||||
autoconf-archive \
|
||||
automake \
|
||||
expect \
|
||||
ca-certificates \
|
||||
ccache \
|
||||
gcc \
|
||||
git \
|
||||
lcov \
|
||||
libcmocka-dev \
|
||||
libcurl3-dev \
|
||||
libgcrypt-dev \
|
||||
@@ -35,19 +41,19 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
RUN mkdir -p /usr/src/{stabber,libstrophe,profanity}
|
||||
WORKDIR /usr/src
|
||||
|
||||
#RUN git clone https://github.com/boothj5/stabber
|
||||
RUN git clone -c http.sslverify=false https://github.com/strophe/libstrophe
|
||||
RUN git clone --depth 1 https://git.jabber.space/devs/stabber
|
||||
RUN git clone --depth 1 https://github.com/strophe/libstrophe
|
||||
|
||||
#WORKDIR /usr/src/stabber
|
||||
#RUN ./bootstrap.sh
|
||||
#RUN ./configure --prefix=/usr --disable-dependency-tracking
|
||||
#RUN make
|
||||
#RUN make install
|
||||
WORKDIR /usr/src/stabber
|
||||
RUN ./bootstrap.sh
|
||||
RUN ./configure --prefix=/usr --disable-dependency-tracking
|
||||
RUN make -j$(nproc)
|
||||
RUN make install
|
||||
|
||||
WORKDIR /usr/src/libstrophe
|
||||
RUN ./bootstrap.sh
|
||||
RUN ./configure --prefix=/usr
|
||||
RUN make
|
||||
RUN make -j$(nproc)
|
||||
RUN make install
|
||||
|
||||
WORKDIR /usr/src/profanity
|
||||
@@ -1,7 +1,9 @@
|
||||
# Build the latest Fedora image
|
||||
FROM fedora:latest
|
||||
|
||||
# expect - for functional tests
|
||||
ENV TERM=xterm
|
||||
ENV CC="ccache gcc"
|
||||
|
||||
# libmicrohttpd - for stabber
|
||||
# glibc-locale - to have en_US locale
|
||||
RUN dnf install -y \
|
||||
@@ -9,9 +11,11 @@ RUN dnf install -y \
|
||||
autoconf-archive \
|
||||
automake \
|
||||
awk \
|
||||
expect-devel \
|
||||
ca-certificates \
|
||||
ccache \
|
||||
gcc \
|
||||
git \
|
||||
lcov \
|
||||
glib2-devel \
|
||||
glibc-all-langpacks \
|
||||
gtk2-devel \
|
||||
@@ -42,25 +46,25 @@ RUN dnf install -y \
|
||||
ENV LANG en_US.UTF-8
|
||||
ENV LANGUAGE en_US:en
|
||||
ENV LC_ALL en_US.UTF-8
|
||||
ENV TERM=xterm
|
||||
|
||||
RUN mkdir -p /usr/src
|
||||
WORKDIR /usr/src
|
||||
|
||||
#RUN mkdir -p /usr/src/stabber
|
||||
#RUN git clone https://github.com/boothj5/stabber
|
||||
#WORKDIR /usr/src/stabber
|
||||
#RUN ./bootstrap.sh
|
||||
#RUN ./configure --prefix=/usr --disable-dependency-tracking
|
||||
#RUN make
|
||||
#RUN make install
|
||||
RUN git clone --depth 1 https://git.jabber.space/devs/stabber
|
||||
WORKDIR /usr/src/stabber
|
||||
RUN ./bootstrap.sh
|
||||
RUN ./configure --prefix=/usr --disable-dependency-tracking
|
||||
RUN make -j$(nproc)
|
||||
RUN make install
|
||||
|
||||
WORKDIR /usr/src
|
||||
RUN mkdir -p /usr/src/libstrophe
|
||||
RUN git clone -c http.sslverify=false https://github.com/strophe/libstrophe
|
||||
RUN git clone --depth 1 https://github.com/strophe/libstrophe
|
||||
WORKDIR /usr/src/libstrophe
|
||||
RUN ./bootstrap.sh
|
||||
RUN ./configure --prefix=/usr
|
||||
RUN make
|
||||
RUN make -j$(nproc)
|
||||
RUN make install
|
||||
|
||||
RUN mkdir -p /usr/src/profanity
|
||||
@@ -1,16 +1,20 @@
|
||||
# Build the latest openSUSE Tumbleweed image
|
||||
FROM opensuse/tumbleweed
|
||||
|
||||
# expect - for functional tests
|
||||
ENV TERM=xterm
|
||||
ENV CC="ccache gcc"
|
||||
|
||||
# libmicrohttpd - for stabber
|
||||
# glibc-locale - to have en_US locale
|
||||
RUN zypper --non-interactive in --no-recommends \
|
||||
autoconf \
|
||||
autoconf-archive \
|
||||
automake \
|
||||
expect-devel \
|
||||
ca-certificates \
|
||||
ccache \
|
||||
gcc \
|
||||
git \
|
||||
lcov \
|
||||
glib2-devel \
|
||||
glibc-locale \
|
||||
gtk2-devel \
|
||||
@@ -41,17 +45,17 @@ RUN zypper --non-interactive in --no-recommends \
|
||||
ENV LANG en_US.UTF-8
|
||||
ENV LANGUAGE en_US:en
|
||||
ENV LC_ALL en_US.UTF-8
|
||||
ENV TERM=xterm
|
||||
|
||||
RUN mkdir -p /usr/src
|
||||
WORKDIR /usr/src
|
||||
|
||||
#RUN mkdir -p /usr/src/stabber
|
||||
#RUN git clone git://github.com/boothj5/stabber.git
|
||||
#WORKDIR /usr/src/stabber
|
||||
#RUN ./bootstrap.sh
|
||||
#RUN ./configure --prefix=/usr --disable-dependency-tracking
|
||||
#RUN make
|
||||
#RUN make install
|
||||
RUN git clone --depth 1 https://git.jabber.space/devs/stabber
|
||||
WORKDIR /usr/src/stabber
|
||||
RUN ./bootstrap.sh
|
||||
RUN ./configure --prefix=/usr --disable-dependency-tracking
|
||||
RUN make -j$(nproc)
|
||||
RUN make install
|
||||
|
||||
RUN mkdir -p /usr/src/profanity
|
||||
WORKDIR /usr/src/profanity
|
||||
@@ -1,14 +1,18 @@
|
||||
FROM ubuntu:latest
|
||||
|
||||
ENV DEBIAN_FRONTEND="noninteractive"
|
||||
ENV TERM=xterm
|
||||
ENV CC="ccache gcc"
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
autoconf \
|
||||
autoconf-archive \
|
||||
automake \
|
||||
expect \
|
||||
ca-certificates \
|
||||
ccache \
|
||||
gcc \
|
||||
git \
|
||||
lcov \
|
||||
libcmocka-dev \
|
||||
libcurl3-dev \
|
||||
libgcrypt-dev \
|
||||
@@ -36,20 +40,19 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
RUN mkdir -p /usr/src/{stabber,libstrophe,profanity}
|
||||
WORKDIR /usr/src
|
||||
|
||||
#RUN git clone https://github.com/boothj5/stabber
|
||||
RUN git clone -c http.sslverify=false https://github.com/strophe/libstrophe
|
||||
RUN git clone --depth 1 https://git.jabber.space/devs/stabber
|
||||
RUN git clone --depth 1 https://github.com/strophe/libstrophe
|
||||
|
||||
# TODO: Re-enable once libmicrohttpd-dev has been updated.
|
||||
#WORKDIR /usr/src/stabber
|
||||
#RUN ./bootstrap.sh
|
||||
#RUN ./configure --prefix=/usr --disable-dependency-tracking
|
||||
#RUN make
|
||||
#RUN make install
|
||||
WORKDIR /usr/src/stabber
|
||||
RUN ./bootstrap.sh
|
||||
RUN ./configure --prefix=/usr --disable-dependency-tracking
|
||||
RUN make -j$(nproc)
|
||||
RUN make install
|
||||
|
||||
WORKDIR /usr/src/libstrophe
|
||||
RUN ./bootstrap.sh
|
||||
RUN ./configure --prefix=/usr
|
||||
RUN make
|
||||
RUN make -j$(nproc)
|
||||
RUN make install
|
||||
|
||||
WORKDIR /usr/src/profanity
|
||||
106
configure.ac
106
configure.ac
@@ -69,6 +69,12 @@ AC_ARG_ENABLE([gdk-pixbuf],
|
||||
[AS_HELP_STRING([--enable-gdk-pixbuf], [enable GDK Pixbuf support to scale avatars before uploading])])
|
||||
AC_ARG_ENABLE([omemo-qrcode],
|
||||
[AS_HELP_STRING([--enable-omemo-qrcode], [enable ability to display omemo qr code])])
|
||||
AC_ARG_ENABLE([coverage],
|
||||
[AS_HELP_STRING([--enable-coverage], [enable code coverage analysis])])
|
||||
AC_ARG_ENABLE([sanitizers],
|
||||
[AS_HELP_STRING([--enable-sanitizers], [enable AddressSanitizer + UndefinedBehaviorSanitizer + unsigned-integer-overflow])])
|
||||
AC_ARG_ENABLE([hardening],
|
||||
[AS_HELP_STRING([--enable-hardening], [enable extra hardening warnings (e.g. -Wnull-dereference) that may produce false positives under -O2])])
|
||||
|
||||
m4_include([m4/ax_valgrind_check.m4])
|
||||
AX_VALGRIND_DFLT([drd], [off])
|
||||
@@ -92,8 +98,17 @@ PKG_CHECK_MODULES([curl], [libcurl >= 7.62.0], [],
|
||||
[AC_CHECK_LIB([curl], [main], [],
|
||||
[AC_MSG_ERROR([libcurl 7.62.0 or higher is required])])])
|
||||
|
||||
PKG_CHECK_MODULES([SQLITE], [sqlite3 >= 3.22.0], [],
|
||||
[AC_MSG_ERROR([sqlite3 3.22.0 or higher is required])])
|
||||
### sqlite (optional — can be disabled with --without-sqlite)
|
||||
AC_ARG_WITH([sqlite],
|
||||
[AS_HELP_STRING([--without-sqlite], [build without SQLite support (flat-file backend only)])],
|
||||
[], [with_sqlite=yes])
|
||||
|
||||
AS_IF([test "x$with_sqlite" != "xno"],
|
||||
[PKG_CHECK_MODULES([SQLITE], [sqlite3 >= 3.22.0],
|
||||
[AC_DEFINE([HAVE_SQLITE], [1], [SQLite support])],
|
||||
[AC_MSG_ERROR([sqlite3 3.22.0 or higher is required (use --without-sqlite to disable)])])],
|
||||
[AC_MSG_NOTICE([Building without SQLite — flat-file backend only])])
|
||||
AM_CONDITIONAL([BUILD_SQLITE], [test "x$with_sqlite" != "xno"])
|
||||
|
||||
ACX_PTHREAD([], [AC_MSG_ERROR([pthread is required])])
|
||||
AS_IF([test "x$PTHREAD_CC" != x], [ CC="$PTHREAD_CC" ])
|
||||
@@ -374,23 +389,99 @@ PKG_CHECK_MODULES([cmocka], [cmocka], [],
|
||||
AM_CONDITIONAL([HAVE_STABBER], [false])
|
||||
AC_CHECK_LIB([stabber], [stbbr_start], [AM_CONDITIONAL([HAVE_STABBER], [true])],
|
||||
[AC_MSG_NOTICE([libstabber not found, will not be able to run functional tests])])
|
||||
AM_CONDITIONAL([HAVE_EXPECT], [false])
|
||||
AC_CHECK_LIB([expect], [exp_expectl], [AM_CONDITIONAL([HAVE_EXPECT], [true])],
|
||||
[AC_MSG_NOTICE([libexpect not found, will not be able to run functional tests])])
|
||||
dnl Check for forkpty (needed for functional tests PTY handling)
|
||||
dnl On Linux it's in libutil, on some BSDs it's in libc
|
||||
AM_CONDITIONAL([HAVE_FORKPTY], [false])
|
||||
AC_CHECK_LIB([util], [forkpty], [AM_CONDITIONAL([HAVE_FORKPTY], [true]) FORKPTY_LIB="-lutil"],
|
||||
[AC_CHECK_FUNC([forkpty], [AM_CONDITIONAL([HAVE_FORKPTY], [true]) FORKPTY_LIB=""],
|
||||
[AC_MSG_NOTICE([forkpty not found, will not be able to run functional tests])])])
|
||||
AC_SUBST([FORKPTY_LIB])
|
||||
|
||||
## Default parameters
|
||||
AM_CFLAGS="$AM_CFLAGS -Wall -Wno-deprecated-declarations -std=gnu99 -ggdb3"
|
||||
AM_CFLAGS="$AM_CFLAGS -Wall -Wextra -Wformat=2 -Wno-format-zero-length"
|
||||
AM_CFLAGS="$AM_CFLAGS -Wno-deprecated-declarations -Wno-unused-parameter -Wno-missing-field-initializers -Wno-cast-function-type"
|
||||
AM_CFLAGS="$AM_CFLAGS -Wpointer-arith"
|
||||
AM_CFLAGS="$AM_CFLAGS -Wimplicit-function-declaration"
|
||||
AM_CFLAGS="$AM_CFLAGS -Wundef"
|
||||
AM_CFLAGS="$AM_CFLAGS -Wfloat-equal -Wredundant-decls"
|
||||
AM_CFLAGS="$AM_CFLAGS -fstack-protector-strong -fno-common"
|
||||
AM_CFLAGS="$AM_CFLAGS -std=gnu99 -ggdb3"
|
||||
|
||||
# GCC-specific warnings (not supported by clang) — test each one
|
||||
saved_CFLAGS="$CFLAGS"
|
||||
for _flag in -Wlogical-op -Wduplicated-cond -Wduplicated-branches \
|
||||
-Wstringop-overflow -Warray-bounds=2 -Walloc-zero \
|
||||
-Wsign-compare; do
|
||||
AC_MSG_CHECKING([whether $CC supports $_flag])
|
||||
CFLAGS="$saved_CFLAGS $_flag -Werror"
|
||||
AC_COMPILE_IFELSE([AC_LANG_PROGRAM()],
|
||||
[AC_MSG_RESULT([yes]); AM_CFLAGS="$AM_CFLAGS $_flag"],
|
||||
[AC_MSG_RESULT([no])])
|
||||
done
|
||||
CFLAGS="$saved_CFLAGS"
|
||||
|
||||
AM_LDFLAGS="$AM_LDFLAGS -export-dynamic"
|
||||
|
||||
# Linker hardening (RELRO + immediate binding)
|
||||
saved_LDFLAGS="$LDFLAGS"
|
||||
for _flag in -Wl,-z,relro -Wl,-z,now; do
|
||||
AC_MSG_CHECKING([whether linker supports $_flag])
|
||||
LDFLAGS="$saved_LDFLAGS $_flag"
|
||||
AC_LINK_IFELSE([AC_LANG_PROGRAM()],
|
||||
[AC_MSG_RESULT([yes]); AM_LDFLAGS="$AM_LDFLAGS $_flag"],
|
||||
[AC_MSG_RESULT([no])])
|
||||
done
|
||||
LDFLAGS="$saved_LDFLAGS"
|
||||
|
||||
AS_IF([test "x$enable_coverage" = xyes],
|
||||
[AM_CFLAGS="$AM_CFLAGS --coverage -O0"
|
||||
AM_LDFLAGS="$AM_LDFLAGS --coverage"
|
||||
AC_MSG_NOTICE([Code coverage analysis enabled])])
|
||||
|
||||
AS_IF([test "x$enable_sanitizers" = xyes],
|
||||
[SAN_FLAGS="-fsanitize=address,undefined"
|
||||
saved_CFLAGS="$CFLAGS"
|
||||
CFLAGS="$saved_CFLAGS -fsanitize=unsigned-integer-overflow -Werror"
|
||||
AC_MSG_CHECKING([whether $CC supports -fsanitize=unsigned-integer-overflow])
|
||||
AC_COMPILE_IFELSE([AC_LANG_PROGRAM()],
|
||||
[AC_MSG_RESULT([yes]); SAN_FLAGS="$SAN_FLAGS,unsigned-integer-overflow"],
|
||||
[AC_MSG_RESULT([no])])
|
||||
CFLAGS="$saved_CFLAGS"
|
||||
AM_CFLAGS="$AM_CFLAGS $SAN_FLAGS -fno-sanitize-recover=all -fno-omit-frame-pointer"
|
||||
AM_LDFLAGS="$AM_LDFLAGS -fsanitize=address,undefined"
|
||||
AC_MSG_NOTICE([Sanitizers enabled: $SAN_FLAGS])])
|
||||
|
||||
AS_IF([test "x$enable_hardening" = xyes],
|
||||
[saved_CFLAGS="$CFLAGS"
|
||||
for _flag in -Wnull-dereference; do
|
||||
AC_MSG_CHECKING([whether $CC supports $_flag])
|
||||
CFLAGS="$saved_CFLAGS $_flag -Werror"
|
||||
AC_COMPILE_IFELSE([AC_LANG_PROGRAM()],
|
||||
[AC_MSG_RESULT([yes]); AM_CFLAGS="$AM_CFLAGS $_flag"],
|
||||
[AC_MSG_RESULT([no])])
|
||||
done
|
||||
CFLAGS="$saved_CFLAGS"
|
||||
AC_MSG_NOTICE([Extra hardening warnings enabled])])
|
||||
|
||||
# Skip _FORTIFY_SOURCE=2 if env already set one (Pikaur/makepkg) or under coverage -O0 (FORTIFY needs -O>0).
|
||||
AS_IF([test "x$enable_coverage" != xyes],
|
||||
[AS_CASE([" ${CPPFLAGS} ${CFLAGS} "],
|
||||
[*_FORTIFY_SOURCE=*], [AC_MSG_NOTICE([_FORTIFY_SOURCE preset from environment, leaving as-is])],
|
||||
[AM_CFLAGS="$AM_CFLAGS -D_FORTIFY_SOURCE=2"])])
|
||||
|
||||
AS_IF([test "x$PACKAGE_STATUS" = xdevelopment],
|
||||
[AM_CFLAGS="$AM_CFLAGS -Wunused -Werror"])
|
||||
AS_IF([test "x$PLATFORM" = xosx],
|
||||
[AM_CFLAGS="$AM_CFLAGS -Qunused-arguments"])
|
||||
|
||||
# Treat glib/gio headers as system to keep their macros out of our -W* set.
|
||||
glib_CFLAGS=$(echo "$glib_CFLAGS" | sed 's/-I/-isystem /g')
|
||||
gio_CFLAGS=$(echo "$gio_CFLAGS" | sed 's/-I/-isystem /g')
|
||||
|
||||
AM_CFLAGS="$AM_CFLAGS $PTHREAD_CFLAGS $glib_CFLAGS $gio_CFLAGS $curl_CFLAGS ${SQLITE_CFLAGS}"
|
||||
AM_CFLAGS="$AM_CFLAGS $libnotify_CFLAGS ${GTK_CFLAGS} $python_CFLAGS"
|
||||
AM_CFLAGS="$AM_CFLAGS -DTHEMES_PATH=\"\\\"$THEMES_PATH\\\"\" -DICONS_PATH=\"\\\"$ICONS_PATH\\\"\" -DGLOBAL_PYTHON_PLUGINS_PATH=\"\\\"$GLOBAL_PYTHON_PLUGINS_PATH\\\"\" -DGLOBAL_C_PLUGINS_PATH=\"\\\"$GLOBAL_C_PLUGINS_PATH\\\"\""
|
||||
AM_CFLAGS="$AM_CFLAGS $CFLAGS"
|
||||
|
||||
LIBS="$glib_LIBS $gio_LIBS $PTHREAD_LIBS $curl_LIBS $libnotify_LIBS $python_LIBS ${GTK_LIBS} ${SQLITE_LIBS} $LIBS"
|
||||
|
||||
AC_SUBST(AM_LDFLAGS)
|
||||
@@ -405,6 +496,7 @@ AC_OUTPUT
|
||||
AC_MSG_NOTICE([Summary of build options:
|
||||
PLATFORM : $target_os
|
||||
PACKAGE_STATUS : $PACKAGE_STATUS
|
||||
SQLite support : $with_sqlite
|
||||
GTK_VERSION : $GTK_VERSION
|
||||
LIBS : $LIBS
|
||||
Install themes : $THEMES_INSTALL
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
.TH man 1 "2025-03-27" "0.15.0" "Profanity XMPP client"
|
||||
.TH man 1 "2026-03-23" "0.17.0" "Profanity XMPP client"
|
||||
.SH NAME
|
||||
Profanity \- a simple console based XMPP chat client.
|
||||
.SH SYNOPSIS
|
||||
@@ -181,7 +181,15 @@ Stands for Mutli-User Chats (XEP-0045) and are also called, groups, group chats,
|
||||
The roster is your contact list. By default displayed at the right side on the console window. See RFC6121.
|
||||
.TP
|
||||
.BR XEP
|
||||
XMPP is aa extendable protocol. There are core features and optional features described in XMPP Extension Protocols, short XEPs.
|
||||
XMPP is an extendable protocol. There are core features and optional features described in XMPP Extension Protocols, short XEPs.
|
||||
.PP
|
||||
There are various kind of messages.
|
||||
.TP
|
||||
1:1 messages are regular messages from one party to another party, from one JID to the other JID. Often you have the other party added to your roster.
|
||||
.TP
|
||||
MUC messages are messages from one party to a group chat.
|
||||
.TP
|
||||
MUC PM (MUC private message) are messages from one party to another party which are in the same group chat. You might not have the JID of this person in your roster or don't know their JID at all. Communication is done over the MUC via the nick. Only the two parties can see the message. In the Android client Conversations this is displayed as "whispering". If a user leaves a MUC another user can join the MUC with the same nick unless the nick is registered. Which often isn't the case.
|
||||
.SH SEE ALSO
|
||||
.B Profanity
|
||||
itself has a lot of built\-in help. Check the
|
||||
@@ -197,7 +205,48 @@ Configuration for
|
||||
.B Profanity
|
||||
is stored in
|
||||
.I $XDG_CONFIG_HOME/profanity/profrc
|
||||
, details on commands for configuring Profanity can be found at <https://profanity-im.github.io/reference.html> or the respective built\-in help or man pages.
|
||||
, details on commands for configuring Profanity can be found at <https://jabber.space/command-reference/> or the respective built\-in help or man pages.
|
||||
.SS Message History Storage
|
||||
By default, message history is stored in an SQLite database. An alternative flat-file backend
|
||||
stores messages as human-readable plain text files that can be edited with any text editor.
|
||||
.PP
|
||||
To enable flat-file logging, set in
|
||||
.IR profrc :
|
||||
.PP
|
||||
.EX
|
||||
[logging]
|
||||
dblog=flatfile
|
||||
.EE
|
||||
.PP
|
||||
Or use the command:
|
||||
.B /privacy logging flatfile
|
||||
.PP
|
||||
Flat-file logs are stored under
|
||||
.IR $XDG_DATA_HOME/profanity/flatlog/ ,
|
||||
organized as
|
||||
.IR {account_jid}/{contact_jid}/history.log .
|
||||
.PP
|
||||
Each line has the format:
|
||||
.br
|
||||
.EX
|
||||
{ISO8601} [{type}|{enc}|id:{id}|aid:{archive_id}|corrects:{id}] {sender}: {message}
|
||||
.EE
|
||||
.PP
|
||||
Use
|
||||
.B /history verify
|
||||
to check integrity of stored history (both SQLite and flat-file).
|
||||
.PP
|
||||
Use
|
||||
.B /history export [<jid>]
|
||||
to copy messages from SQLite to flat-file format (merge with existing data), or
|
||||
.B /history import [<jid>]
|
||||
to copy from flat-file to SQLite.
|
||||
Both operations skip duplicates.
|
||||
Omit the JID to process all contacts.
|
||||
.PP
|
||||
Use
|
||||
.B /history switch sqlite|flatfile
|
||||
to change the active database backend at runtime without reconnecting.
|
||||
.SH BUGS
|
||||
Bugs can either be reported by raising an issue at the Github issue tracker:
|
||||
.br
|
||||
@@ -212,7 +261,7 @@ or to the mailing list at:
|
||||
.br
|
||||
.SH LICENSE
|
||||
Copyright (C) 2012 \- 2019 James Booth <boothj5web@gmail.com>.
|
||||
Copyright (C) 2019 \- 2025 Michael Vetter <jubalh@iodoru.org>.
|
||||
Copyright (C) 2019 \- 2026 Michael Vetter <jubalh@iodoru.org>.
|
||||
License GPLv3+: GNU GPL version 3 or later <https://www.gnu.org/licenses/gpl.html>
|
||||
This is free software; you are free to change and redistribute it.
|
||||
There is NO WARRANTY, to the extent permitted by law.
|
||||
|
||||
793
meson.build
Normal file
793
meson.build
Normal file
@@ -0,0 +1,793 @@
|
||||
project('profanity', 'c',
|
||||
version: '0.17.0',
|
||||
license: 'GPL-3.0-or-later',
|
||||
meson_version: '>= 0.56.0',
|
||||
default_options: [
|
||||
'c_std=gnu99',
|
||||
'warning_level=2',
|
||||
'wrap_mode=nofallback',
|
||||
]
|
||||
)
|
||||
|
||||
# Determine platform
|
||||
host_system = host_machine.system()
|
||||
platform_map = {
|
||||
'freebsd': 'freebsd',
|
||||
'netbsd': 'netbsd',
|
||||
'openbsd': 'openbsd',
|
||||
'darwin': 'osx',
|
||||
'cygwin': 'cygwin',
|
||||
}
|
||||
platform = platform_map.get(host_system, 'nix')
|
||||
|
||||
# Configuration data
|
||||
conf_data = configuration_data()
|
||||
conf_data.set_quoted('PACKAGE_NAME', meson.project_name())
|
||||
conf_data.set_quoted('PACKAGE_VERSION', meson.project_version())
|
||||
conf_data.set_quoted('PACKAGE_BUGREPORT', 'jubalh@iodoru.org')
|
||||
conf_data.set_quoted('PACKAGE_URL', 'https://profanity-im.github.io/')
|
||||
|
||||
# Possible options from meson:
|
||||
# debug, debugoptimized, release, plain
|
||||
# Build type configuration
|
||||
is_release = get_option('buildtype') == 'release'
|
||||
is_debug = get_option('buildtype') in ['debug', 'debugoptimized']
|
||||
|
||||
conf_data.set_quoted('PACKAGE_STATUS', is_release ? 'release' : 'development')
|
||||
|
||||
if platform == 'cygwin'
|
||||
conf_data.set('PLATFORM_CYGWIN', 1)
|
||||
endif
|
||||
|
||||
if platform == 'osx'
|
||||
conf_data.set('PLATFORM_OSX', 1)
|
||||
endif
|
||||
|
||||
# Get git version if in development
|
||||
if is_debug
|
||||
git = find_program('git', required: false)
|
||||
if git.found()
|
||||
conf_data.set('HAVE_GIT_VERSION', 1)
|
||||
endif
|
||||
endif
|
||||
|
||||
# Compiler setup
|
||||
cc = meson.get_compiler('c')
|
||||
|
||||
# Common compiler flags
|
||||
add_project_arguments([
|
||||
'-Wno-deprecated-declarations',
|
||||
'-Wno-unused-parameter',
|
||||
], language: 'c')
|
||||
|
||||
analyzer_args = []
|
||||
if is_debug
|
||||
add_project_arguments([
|
||||
'-ggdb3',
|
||||
'-Wunused',
|
||||
'-Wall',
|
||||
'-Wextra',
|
||||
'-fstack-protector-strong',
|
||||
'-D_FORTIFY_SOURCE=2',
|
||||
'-Og',
|
||||
'-Wpointer-arith'
|
||||
], language: 'c')
|
||||
if cc.get_id() == 'gcc'
|
||||
analyzer_args += '-fanalyzer'
|
||||
endif
|
||||
endif
|
||||
|
||||
if cc.get_id() == 'clang'
|
||||
add_project_arguments('-Qunused-arguments', language: 'c')
|
||||
endif
|
||||
|
||||
if platform == 'osx'
|
||||
# Check for homebrew paths on Apple Silicon
|
||||
if run_command('test', '-d', '/opt/homebrew/include', check: false).returncode() == 0
|
||||
add_project_arguments('-I/opt/homebrew/include', language: 'c')
|
||||
add_project_link_arguments('-L/opt/homebrew/lib', language: 'c')
|
||||
endif
|
||||
endif
|
||||
|
||||
# Required dependencies
|
||||
glib_dep = dependency('glib-2.0', version: '>= 2.62.0')
|
||||
gio_dep = dependency('gio-2.0')
|
||||
curl_dep = dependency('libcurl', version: '>= 7.62.0')
|
||||
sqlite_dep = dependency('sqlite3', version: '>= 3.35.0')
|
||||
thread_dep = dependency('threads')
|
||||
math_dep = cc.find_library('m')
|
||||
libstrophe_dep = dependency('libstrophe', version: '>= 0.12.3')
|
||||
|
||||
# Check for XMPP_CERT_PUBKEY_FINGERPRINT_SHA256 support
|
||||
if libstrophe_dep.type_name() != 'internal'
|
||||
if cc.links('''
|
||||
#include <strophe.h>
|
||||
int main() {
|
||||
xmpp_tlscert_get_string(NULL, XMPP_CERT_PUBKEY_FINGERPRINT_SHA256);
|
||||
return 1;
|
||||
}
|
||||
''', dependencies: libstrophe_dep)
|
||||
conf_data.set('HAVE_XMPP_CERT_PUBKEY_FINGERPRINT_SHA256', 1)
|
||||
endif
|
||||
else
|
||||
conf_data.set('HAVE_XMPP_CERT_PUBKEY_FINGERPRINT_SHA256', 1)
|
||||
endif
|
||||
|
||||
# ncurses
|
||||
curses_dep = dependency('ncursesw', required: false)
|
||||
if not curses_dep.found()
|
||||
curses_dep = dependency('ncurses', required: false)
|
||||
endif
|
||||
if not curses_dep.found()
|
||||
curses_dep = cc.find_library('ncursesw', required: false)
|
||||
endif
|
||||
if not curses_dep.found()
|
||||
curses_dep = cc.find_library('ncurses', required: true)
|
||||
endif
|
||||
|
||||
# Check for ncursesw/ncurses.h, Arch Linux uses ncurses.h for ncursesw
|
||||
if cc.has_header('ncursesw/ncurses.h')
|
||||
conf_data.set('HAVE_NCURSESW_NCURSES_H', 1)
|
||||
elif cc.has_header('ncurses.h')
|
||||
conf_data.set('HAVE_NCURSES_H', 1)
|
||||
endif
|
||||
|
||||
# Readline
|
||||
readline_dep = dependency('readline', required: false)
|
||||
|
||||
if not readline_dep.found() and platform == 'osx'
|
||||
brew = find_program('brew', required: false)
|
||||
if brew.found()
|
||||
prefix = run_command(brew, '--prefix', 'readline', check: false).stdout().strip()
|
||||
readline_dep = cc.find_library('readline', dirs: [prefix / 'lib'], required: true)
|
||||
add_project_arguments('-I' + prefix / 'include', language: 'c')
|
||||
endif
|
||||
endif
|
||||
|
||||
if not readline_dep.found() and platform == 'openbsd'
|
||||
readline_dep = cc.find_library('ereadline', dirs: ['/usr/local/lib'], required: false)
|
||||
endif
|
||||
|
||||
if not readline_dep.found()
|
||||
readline_dep = cc.find_library('readline', required: true)
|
||||
endif
|
||||
|
||||
# Optional dependencies
|
||||
have_osxnotify = false
|
||||
libnotify_dep = disabler()
|
||||
gtk_dep = disabler()
|
||||
xscrnsaver_dep = []
|
||||
python_dep = disabler()
|
||||
dl_dep = disabler()
|
||||
gpgme_dep = disabler()
|
||||
libotr_dep = disabler()
|
||||
gdk_pixbuf_dep = disabler()
|
||||
omemo_dep = disabler()
|
||||
gcrypt_dep = disabler()
|
||||
qrencode_dep = disabler()
|
||||
enchant_dep = disabler()
|
||||
|
||||
# Dependencies for functional tests
|
||||
stabber_dep = cc.find_library('stabber', required: false)
|
||||
util_dep = cc.find_library('util', required: false)
|
||||
|
||||
# Build flags
|
||||
build_python_api = false
|
||||
build_c_api = false
|
||||
build_pgp = false
|
||||
build_otr = false
|
||||
build_omemo = false
|
||||
|
||||
# Notifications
|
||||
if get_option('notifications').enabled()
|
||||
if platform == 'osx'
|
||||
terminal_notifier = find_program('terminal-notifier', required: true)
|
||||
have_osxnotify = true
|
||||
conf_data.set('HAVE_OSXNOTIFY', 1)
|
||||
elif platform in ['nix', 'freebsd']
|
||||
libnotify_dep = dependency('libnotify', required: true)
|
||||
conf_data.set('HAVE_LIBNOTIFY', 1)
|
||||
else
|
||||
error('Notifications not supported on this platform')
|
||||
endif
|
||||
endif
|
||||
|
||||
# GTK (for icons and clipboard)
|
||||
x11_extra_dep = []
|
||||
if get_option('icons-and-clipboard').enabled()
|
||||
gtk_dep = dependency('gtk+-3.0', version: '>= 3.24.0', required: true)
|
||||
conf_data.set('HAVE_GTK', 1)
|
||||
|
||||
x11_dep = dependency('x11', required: false)
|
||||
if x11_dep.found()
|
||||
if cc.has_function('XSetIOErrorExitHandler', dependencies: x11_dep)
|
||||
conf_data.set('HAVE_XEXITHANDLER', 1)
|
||||
x11_extra_dep = [x11_dep]
|
||||
endif
|
||||
endif
|
||||
endif
|
||||
|
||||
# XScreenSaver
|
||||
if get_option('xscreensaver').enabled()
|
||||
xscrnsaver_dep = [
|
||||
dependency('xscrnsaver', required: true),
|
||||
dependency('x11', required: true)
|
||||
]
|
||||
conf_data.set('HAVE_LIBXSS', 1)
|
||||
endif
|
||||
|
||||
# Python plugins
|
||||
if get_option('python-plugins').enabled()
|
||||
python_dep = dependency('python3-embed', required: false)
|
||||
if not python_dep.found()
|
||||
python_dep = dependency('python-embed', required: true)
|
||||
else
|
||||
conf_data.set('PY_IS_PYTHON3', 1)
|
||||
endif
|
||||
|
||||
build_python_api = true
|
||||
conf_data.set('HAVE_PYTHON', 1)
|
||||
endif
|
||||
|
||||
# C plugins
|
||||
if get_option('c-plugins').enabled()
|
||||
if platform == 'cygwin'
|
||||
error('C plugins are not supported on Cygwin')
|
||||
endif
|
||||
|
||||
if platform not in ['openbsd', 'freebsd', 'netbsd']
|
||||
dl_dep = cc.find_library('dl', required: true)
|
||||
endif
|
||||
|
||||
build_c_api = true
|
||||
conf_data.set('HAVE_C', 1)
|
||||
endif
|
||||
|
||||
# PGP support
|
||||
if get_option('pgp').enabled()
|
||||
gpgme_dep = dependency('gpgme', required: false)
|
||||
|
||||
build_pgp = true
|
||||
conf_data.set('HAVE_LIBGPGME', 1)
|
||||
endif
|
||||
|
||||
# OTR support
|
||||
if get_option('otr').enabled()
|
||||
libotr_dep = dependency('libotr', version: '>= 4.0', required: true)
|
||||
build_otr = true
|
||||
conf_data.set('HAVE_LIBOTR', 1)
|
||||
endif
|
||||
|
||||
# GDK Pixbuf (for avatar scaling)
|
||||
if get_option('gdk-pixbuf').enabled()
|
||||
gdk_pixbuf_dep = dependency('gdk-pixbuf-2.0', version: '>= 2.4', required: true)
|
||||
conf_data.set('HAVE_PIXBUF', 1)
|
||||
endif
|
||||
|
||||
# OMEMO support
|
||||
if get_option('omemo').enabled()
|
||||
omemo_backend = get_option('omemo-backend')
|
||||
if omemo_backend == 'libsignal'
|
||||
omemo_dep = dependency('libsignal-protocol-c', version: '>= 2.3.2', required: true)
|
||||
elif omemo_backend == 'libomemo-c'
|
||||
omemo_dep = dependency('libomemo-c', version: '>= 0.5.1', required: true)
|
||||
conf_data.set('HAVE_LIBOMEMO_C', 1)
|
||||
endif
|
||||
gcrypt_dep = dependency('libgcrypt', version: '>= 1.7.0', required: true)
|
||||
build_omemo = true
|
||||
conf_data.set('HAVE_OMEMO', 1)
|
||||
endif
|
||||
|
||||
# QR code support (for OMEMO)
|
||||
if get_option('omemo-qrcode').enabled()
|
||||
qrencode_dep = dependency('libqrencode', required: true)
|
||||
conf_data.set('HAVE_QRENCODE', 1)
|
||||
endif
|
||||
|
||||
# Spellcheck support
|
||||
if get_option('spellcheck').enabled()
|
||||
enchant_dep = dependency('enchant-2', required: true)
|
||||
conf_data.set('HAVE_SPELLCHECK', 1)
|
||||
endif
|
||||
|
||||
# Set installation paths
|
||||
themes_path = get_option('themes_path')
|
||||
if themes_path == ''
|
||||
themes_path = get_option('datadir') / meson.project_name() / 'themes'
|
||||
endif
|
||||
|
||||
icons_path = get_option('datadir') / meson.project_name() / 'icons'
|
||||
global_python_plugins_path = get_option('datadir') / meson.project_name() / 'plugins'
|
||||
global_c_plugins_path = get_option('libdir') / meson.project_name() / 'plugins'
|
||||
|
||||
conf_data.set_quoted('THEMES_PATH', get_option('prefix') / themes_path)
|
||||
conf_data.set_quoted('ICONS_PATH', get_option('prefix') / icons_path)
|
||||
conf_data.set_quoted('GLOBAL_PYTHON_PLUGINS_PATH', get_option('prefix') / global_python_plugins_path)
|
||||
conf_data.set_quoted('GLOBAL_C_PLUGINS_PATH', get_option('prefix') / global_c_plugins_path)
|
||||
|
||||
# Check for required functions
|
||||
foreach func : ['atexit', 'memset', 'strdup', 'strstr']
|
||||
if cc.has_function(func)
|
||||
conf_data.set('HAVE_' + func.to_upper(), 1)
|
||||
endif
|
||||
endforeach
|
||||
|
||||
# Generate config.h
|
||||
config_h = configure_file(
|
||||
output: 'config.h',
|
||||
configuration: conf_data,
|
||||
)
|
||||
|
||||
# Build dependencies list
|
||||
profanity_deps = [
|
||||
glib_dep,
|
||||
gio_dep,
|
||||
curl_dep,
|
||||
sqlite_dep,
|
||||
thread_dep,
|
||||
math_dep,
|
||||
libstrophe_dep,
|
||||
curses_dep,
|
||||
readline_dep,
|
||||
]
|
||||
|
||||
# Add optional dependencies only if found
|
||||
if libnotify_dep.found()
|
||||
profanity_deps += libnotify_dep
|
||||
endif
|
||||
|
||||
if gtk_dep.found()
|
||||
profanity_deps += gtk_dep
|
||||
endif
|
||||
|
||||
if xscrnsaver_dep.length() > 0
|
||||
profanity_deps += xscrnsaver_dep
|
||||
endif
|
||||
|
||||
if x11_extra_dep.length() > 0
|
||||
profanity_deps += x11_extra_dep
|
||||
endif
|
||||
|
||||
if python_dep.found()
|
||||
profanity_deps += python_dep
|
||||
endif
|
||||
|
||||
if dl_dep.found()
|
||||
profanity_deps += dl_dep
|
||||
endif
|
||||
|
||||
if gpgme_dep.found()
|
||||
profanity_deps += gpgme_dep
|
||||
endif
|
||||
|
||||
if libotr_dep.found()
|
||||
profanity_deps += libotr_dep
|
||||
endif
|
||||
|
||||
if gdk_pixbuf_dep.found()
|
||||
profanity_deps += gdk_pixbuf_dep
|
||||
endif
|
||||
|
||||
if omemo_dep.found()
|
||||
profanity_deps += omemo_dep
|
||||
endif
|
||||
|
||||
if gcrypt_dep.found()
|
||||
profanity_deps += gcrypt_dep
|
||||
endif
|
||||
|
||||
if qrencode_dep.found()
|
||||
profanity_deps += qrencode_dep
|
||||
endif
|
||||
|
||||
if enchant_dep.found()
|
||||
profanity_deps += enchant_dep
|
||||
endif
|
||||
|
||||
# Include directories
|
||||
inc = include_directories('.', 'src')
|
||||
|
||||
# Core source files
|
||||
core_sources = files(
|
||||
'src/xmpp/contact.c',
|
||||
'src/log.c',
|
||||
'src/common.c',
|
||||
'src/chatlog.c',
|
||||
'src/database.c',
|
||||
'src/profanity.c',
|
||||
'src/xmpp/chat_session.c',
|
||||
'src/xmpp/muc.c',
|
||||
'src/xmpp/jid.c',
|
||||
'src/xmpp/chat_state.c',
|
||||
'src/xmpp/resource.c',
|
||||
'src/xmpp/roster_list.c',
|
||||
'src/xmpp/capabilities.c',
|
||||
'src/xmpp/session.c',
|
||||
'src/xmpp/connection.c',
|
||||
'src/xmpp/iq.c',
|
||||
'src/xmpp/message.c',
|
||||
'src/xmpp/presence.c',
|
||||
'src/xmpp/stanza.c',
|
||||
'src/xmpp/roster.c',
|
||||
'src/xmpp/bookmark.c',
|
||||
'src/xmpp/blocking.c',
|
||||
'src/xmpp/form.c',
|
||||
'src/xmpp/avatar.c',
|
||||
'src/xmpp/ox.c',
|
||||
'src/xmpp/vcard.c',
|
||||
'src/event/common.c',
|
||||
'src/event/server_events.c',
|
||||
'src/event/client_events.c',
|
||||
'src/ui/window.c',
|
||||
'src/ui/core.c',
|
||||
'src/ui/titlebar.c',
|
||||
'src/ui/statusbar.c',
|
||||
'src/ui/inputwin.c',
|
||||
'src/ui/screen.c',
|
||||
'src/ui/console.c',
|
||||
'src/ui/notifier.c',
|
||||
'src/ui/window_list.c',
|
||||
'src/ui/rosterwin.c',
|
||||
'src/ui/occupantswin.c',
|
||||
'src/ui/buffer.c',
|
||||
'src/ui/chatwin.c',
|
||||
'src/ui/mucwin.c',
|
||||
'src/ui/privwin.c',
|
||||
'src/ui/confwin.c',
|
||||
'src/ui/xmlwin.c',
|
||||
'src/ui/vcardwin.c',
|
||||
'src/command/cmd_defs.c',
|
||||
'src/command/cmd_funcs.c',
|
||||
'src/command/cmd_ac.c',
|
||||
'src/tools/parser.c',
|
||||
'src/tools/http_common.c',
|
||||
'src/tools/http_upload.c',
|
||||
'src/tools/http_download.c',
|
||||
'src/tools/plugin_download.c',
|
||||
'src/tools/bookmark_ignore.c',
|
||||
'src/tools/autocomplete.c',
|
||||
'src/tools/clipboard.c',
|
||||
'src/tools/editor.c',
|
||||
'src/tools/spellcheck.c',
|
||||
'src/config/files.c',
|
||||
'src/config/conflists.c',
|
||||
'src/config/accounts.c',
|
||||
'src/config/tlscerts.c',
|
||||
'src/config/account.c',
|
||||
'src/config/preferences.c',
|
||||
'src/config/theme.c',
|
||||
'src/config/color.c',
|
||||
'src/config/scripts.c',
|
||||
'src/config/cafile.c',
|
||||
'src/plugins/plugins.c',
|
||||
'src/plugins/api.c',
|
||||
'src/plugins/callbacks.c',
|
||||
'src/plugins/autocompleters.c',
|
||||
'src/plugins/themes.c',
|
||||
'src/plugins/settings.c',
|
||||
'src/plugins/disco.c',
|
||||
)
|
||||
|
||||
# Build the final source list
|
||||
profanity_sources = core_sources
|
||||
|
||||
# Add conditional sources
|
||||
if gtk_dep.found()
|
||||
profanity_sources += files('src/ui/tray.c')
|
||||
endif
|
||||
|
||||
if build_python_api
|
||||
profanity_sources += files(
|
||||
'src/plugins/python_plugins.c',
|
||||
'src/plugins/python_api.c',
|
||||
)
|
||||
endif
|
||||
|
||||
if build_c_api
|
||||
profanity_sources += files(
|
||||
'src/plugins/c_plugins.c',
|
||||
'src/plugins/c_api.c',
|
||||
)
|
||||
endif
|
||||
|
||||
if build_pgp
|
||||
profanity_sources += files(
|
||||
'src/pgp/gpg.c',
|
||||
'src/pgp/ox.c',
|
||||
)
|
||||
endif
|
||||
|
||||
if build_otr
|
||||
profanity_sources += files(
|
||||
'src/otr/otrlibv4.c',
|
||||
'src/otr/otr.c',
|
||||
)
|
||||
endif
|
||||
|
||||
if build_omemo
|
||||
profanity_sources += files(
|
||||
'src/omemo/omemo.c',
|
||||
'src/omemo/crypto.c',
|
||||
'src/omemo/store.c',
|
||||
'src/xmpp/omemo.c',
|
||||
'src/tools/aesgcm_download.c',
|
||||
)
|
||||
endif
|
||||
|
||||
# Generate git version header if in development
|
||||
if is_debug
|
||||
git_branch = run_command('git', 'rev-parse', '--symbolic-full-name', '--abbrev-ref', 'HEAD',
|
||||
check: false).stdout().strip()
|
||||
git_revision = run_command('git', 'log', '--pretty=format:%h', '-n', '1',
|
||||
check: false).stdout().strip()
|
||||
|
||||
git_version_conf = configuration_data()
|
||||
git_version_conf.set('PROF_GIT_BRANCH', '"' + git_branch + '"')
|
||||
git_version_conf.set('PROF_GIT_REVISION', '"' + git_revision + '"')
|
||||
|
||||
configure_file(
|
||||
input: 'src/gitversion.h.in',
|
||||
output: 'gitversion.h',
|
||||
configuration: git_version_conf,
|
||||
)
|
||||
endif
|
||||
|
||||
# Build the executable
|
||||
profanity_exe = executable(
|
||||
'profanity',
|
||||
profanity_sources,
|
||||
files('src/main.c'),
|
||||
config_h,
|
||||
c_args: analyzer_args,
|
||||
dependencies: profanity_deps,
|
||||
include_directories: inc,
|
||||
install: true,
|
||||
export_dynamic: true,
|
||||
)
|
||||
|
||||
# Build libprofanity shared library for C plugins
|
||||
if build_c_api
|
||||
libprofanity = shared_library(
|
||||
'profanity',
|
||||
'src/plugins/profapi.c',
|
||||
dependencies: profanity_deps,
|
||||
include_directories: inc,
|
||||
install: true,
|
||||
version: '0.0.0',
|
||||
)
|
||||
|
||||
install_headers('src/plugins/profapi.h')
|
||||
endif
|
||||
|
||||
# Install themes if enabled
|
||||
if get_option('install_themes')
|
||||
install_subdir('themes',
|
||||
install_dir: get_option('datadir') / meson.project_name(),
|
||||
strip_directory: false,
|
||||
)
|
||||
endif
|
||||
|
||||
# Install icons
|
||||
install_subdir('icons',
|
||||
install_dir: get_option('datadir') / meson.project_name(),
|
||||
strip_directory: false,
|
||||
)
|
||||
|
||||
# Install man pages
|
||||
man_pages = run_command('find', 'docs', '-name', 'profanity*.1', '-type', 'f',
|
||||
check: true).stdout().strip().split('\n')
|
||||
|
||||
if man_pages.length() > 0 and man_pages[0] != ''
|
||||
install_man(man_pages)
|
||||
endif
|
||||
|
||||
# Install example config and theme template
|
||||
install_data(
|
||||
'profrc.example',
|
||||
'theme_template',
|
||||
install_dir: get_option('datadir') / 'doc' / meson.project_name(),
|
||||
)
|
||||
|
||||
# Tests
|
||||
build_unittests = false
|
||||
build_functionaltests = false
|
||||
|
||||
if get_option('tests')
|
||||
cmocka_dep = dependency('cmocka', required: false)
|
||||
if cmocka_dep.found()
|
||||
build_unittests = true
|
||||
unittest_sources = files(
|
||||
'src/xmpp/contact.c',
|
||||
'src/common.c',
|
||||
'src/profanity.c',
|
||||
'src/xmpp/chat_session.c',
|
||||
'src/xmpp/muc.c',
|
||||
'src/xmpp/jid.c',
|
||||
'src/xmpp/resource.c',
|
||||
'src/xmpp/chat_state.c',
|
||||
'src/xmpp/roster_list.c',
|
||||
'src/xmpp/form.c',
|
||||
'src/command/cmd_defs.c',
|
||||
'src/command/cmd_funcs.c',
|
||||
'src/command/cmd_ac.c',
|
||||
'src/tools/parser.c',
|
||||
'src/tools/autocomplete.c',
|
||||
'src/tools/clipboard.c',
|
||||
'src/tools/editor.c',
|
||||
'src/tools/spellcheck.c',
|
||||
'src/tools/bookmark_ignore.c',
|
||||
'src/config/account.c',
|
||||
'src/config/files.c',
|
||||
'src/config/tlscerts.c',
|
||||
'src/config/preferences.c',
|
||||
'src/config/theme.c',
|
||||
'src/config/color.c',
|
||||
'src/config/scripts.c',
|
||||
'src/config/conflists.c',
|
||||
'src/plugins/plugins.c',
|
||||
'src/plugins/api.c',
|
||||
'src/plugins/callbacks.c',
|
||||
'src/plugins/autocompleters.c',
|
||||
'src/plugins/themes.c',
|
||||
'src/plugins/settings.c',
|
||||
'src/plugins/disco.c',
|
||||
'src/ui/window_list.c',
|
||||
'src/event/common.c',
|
||||
'src/event/server_events.c',
|
||||
'src/event/client_events.c',
|
||||
'src/ui/tray.c',
|
||||
'tests/unittests/xmpp/stub_vcard.c',
|
||||
'tests/unittests/xmpp/stub_avatar.c',
|
||||
'tests/unittests/xmpp/stub_ox.c',
|
||||
'tests/unittests/xmpp/stub_xmpp.c',
|
||||
'tests/unittests/xmpp/stub_message.c',
|
||||
'tests/unittests/ui/stub_ui.c',
|
||||
'tests/unittests/ui/stub_vcardwin.c',
|
||||
'tests/unittests/log/stub_log.c',
|
||||
'tests/unittests/chatlog/stub_chatlog.c',
|
||||
'tests/unittests/database/stub_database.c',
|
||||
'tests/unittests/config/stub_accounts.c',
|
||||
'tests/unittests/config/stub_cafile.c',
|
||||
'tests/unittests/tools/stub_http_upload.c',
|
||||
'tests/unittests/tools/stub_http_download.c',
|
||||
'tests/unittests/tools/stub_aesgcm_download.c',
|
||||
'tests/unittests/tools/stub_plugin_download.c',
|
||||
'tests/unittests/helpers.c',
|
||||
'tests/unittests/xmpp/test_form.c',
|
||||
'tests/unittests/test_common.c',
|
||||
'tests/unittests/tools/test_autocomplete.c',
|
||||
'tests/unittests/xmpp/test_jid.c',
|
||||
'tests/unittests/tools/test_parser.c',
|
||||
'tests/unittests/xmpp/test_roster_list.c',
|
||||
'tests/unittests/xmpp/test_chat_session.c',
|
||||
'tests/unittests/xmpp/test_contact.c',
|
||||
'tests/unittests/config/test_preferences.c',
|
||||
'tests/unittests/event/test_server_events.c',
|
||||
'tests/unittests/xmpp/test_muc.c',
|
||||
'tests/unittests/command/test_cmd_presence.c',
|
||||
'tests/unittests/command/test_cmd_alias.c',
|
||||
'tests/unittests/command/test_cmd_connect.c',
|
||||
'tests/unittests/command/test_cmd_rooms.c',
|
||||
'tests/unittests/command/test_cmd_account.c',
|
||||
'tests/unittests/command/test_cmd_sub.c',
|
||||
'tests/unittests/command/test_cmd_bookmark.c',
|
||||
'tests/unittests/command/test_cmd_otr.c',
|
||||
'tests/unittests/command/test_cmd_pgp.c',
|
||||
'tests/unittests/command/test_cmd_join.c',
|
||||
'tests/unittests/command/test_cmd_roster.c',
|
||||
'tests/unittests/command/test_cmd_ac.c',
|
||||
'tests/unittests/command/test_cmd_disconnect.c',
|
||||
'tests/unittests/plugins/test_callbacks.c',
|
||||
'tests/unittests/plugins/test_plugins_disco.c',
|
||||
'tests/unittests/unittests.c',
|
||||
)
|
||||
|
||||
if build_python_api
|
||||
unittest_sources += files(
|
||||
'src/plugins/python_plugins.c',
|
||||
'src/plugins/python_api.c',
|
||||
)
|
||||
endif
|
||||
|
||||
if build_c_api
|
||||
unittest_sources += files(
|
||||
'src/plugins/c_plugins.c',
|
||||
'src/plugins/c_api.c',
|
||||
)
|
||||
endif
|
||||
|
||||
if build_pgp
|
||||
unittest_sources += files(
|
||||
'tests/unittests/pgp/stub_gpg.c',
|
||||
'tests/unittests/pgp/stub_ox.c',
|
||||
)
|
||||
endif
|
||||
|
||||
if build_otr
|
||||
unittest_sources += files('tests/unittests/otr/stub_otr.c')
|
||||
endif
|
||||
|
||||
if build_omemo
|
||||
unittest_sources += files('tests/unittests/omemo/stub_omemo.c')
|
||||
endif
|
||||
|
||||
unittests = executable(
|
||||
'unittests',
|
||||
unittest_sources,
|
||||
dependencies: profanity_deps + [cmocka_dep],
|
||||
include_directories: [inc, include_directories('tests'), include_directories('tests/unittests')],
|
||||
build_by_default: false,
|
||||
)
|
||||
|
||||
test('unit tests', unittests)
|
||||
|
||||
# Functional tests
|
||||
if stabber_dep.found() and util_dep.found()
|
||||
build_functionaltests = true
|
||||
functionaltest_sources = files(
|
||||
'tests/functionaltests/proftest.c',
|
||||
'tests/functionaltests/test_connect.c',
|
||||
'tests/functionaltests/test_ping.c',
|
||||
'tests/functionaltests/test_rooms.c',
|
||||
'tests/functionaltests/test_presence.c',
|
||||
'tests/functionaltests/test_message.c',
|
||||
'tests/functionaltests/test_chat_session.c',
|
||||
'tests/functionaltests/test_carbons.c',
|
||||
'tests/functionaltests/test_receipts.c',
|
||||
'tests/functionaltests/test_roster.c',
|
||||
'tests/functionaltests/test_i18n.c',
|
||||
'tests/functionaltests/test_software.c',
|
||||
'tests/functionaltests/test_muc.c',
|
||||
'tests/functionaltests/test_disconnect.c',
|
||||
'tests/functionaltests/functionaltests.c',
|
||||
)
|
||||
|
||||
functionaltests = executable(
|
||||
'functionaltests',
|
||||
functionaltest_sources,
|
||||
dependencies: [cmocka_dep, stabber_dep, util_dep] + profanity_deps,
|
||||
include_directories: [inc, include_directories('tests')],
|
||||
build_by_default: false,
|
||||
)
|
||||
|
||||
test('functional tests', functionaltests, timeout: 1800)
|
||||
endif
|
||||
else
|
||||
warning('cmocka not found, tests will not be built')
|
||||
endif
|
||||
endif
|
||||
|
||||
lint_and_test = find_program('scripts/lint-and-test.sh', required: false)
|
||||
if lint_and_test.found()
|
||||
run_target('doublecheck',
|
||||
command: [lint_and_test, '--fix-formatting', '--tests']
|
||||
)
|
||||
endif
|
||||
|
||||
summary({
|
||||
'Platform': platform,
|
||||
'Package status': get_option('buildtype'),
|
||||
'Install themes': get_option('install_themes'),
|
||||
'Themes path': themes_path,
|
||||
'Icons path': icons_path,
|
||||
'Global Python plugins path': global_python_plugins_path,
|
||||
'Global C plugins path': global_c_plugins_path,
|
||||
}, section: 'Directories')
|
||||
|
||||
summary({
|
||||
'Notifications': libnotify_dep.found() or have_osxnotify,
|
||||
'Python plugins': build_python_api,
|
||||
'C plugins': build_c_api,
|
||||
'OTR': build_otr,
|
||||
'PGP': build_pgp,
|
||||
'OMEMO': build_omemo,
|
||||
'XScreenSaver': xscrnsaver_dep.length() > 0,
|
||||
'GTK': gtk_dep.found(),
|
||||
'GDK Pixbuf': gdk_pixbuf_dep.found(),
|
||||
'QR Code': qrencode_dep.found(),
|
||||
}, section: 'Features')
|
||||
|
||||
summary({
|
||||
'Unit tests': build_unittests,
|
||||
'Functional tests': build_functionaltests,
|
||||
}, section: 'Testing')
|
||||
98
meson_options.txt
Normal file
98
meson_options.txt
Normal file
@@ -0,0 +1,98 @@
|
||||
# Features:
|
||||
option('notifications',
|
||||
type: 'feature',
|
||||
value: 'disabled',
|
||||
description: 'Enable desktop notifications'
|
||||
)
|
||||
|
||||
option('python-plugins',
|
||||
type: 'feature',
|
||||
value: 'disabled',
|
||||
description: 'Enable Python plugins'
|
||||
)
|
||||
|
||||
option('c-plugins',
|
||||
type: 'feature',
|
||||
value: 'disabled',
|
||||
description: 'Enable C plugins'
|
||||
)
|
||||
|
||||
option('otr',
|
||||
type: 'feature',
|
||||
value: 'disabled',
|
||||
description: 'Enable OTR encryption'
|
||||
)
|
||||
|
||||
option('pgp',
|
||||
type: 'feature',
|
||||
value: 'disabled',
|
||||
description: 'Enable PGP'
|
||||
)
|
||||
|
||||
option('omemo',
|
||||
type: 'feature',
|
||||
value: 'disabled',
|
||||
description: 'Enable OMEMO encryption'
|
||||
)
|
||||
|
||||
option('omemo-backend',
|
||||
type: 'combo',
|
||||
choices: ['libsignal', 'libomemo-c'],
|
||||
value: 'libsignal',
|
||||
description: 'Select OMEMO backend library'
|
||||
)
|
||||
|
||||
option('xscreensaver',
|
||||
type: 'feature',
|
||||
value: 'disabled',
|
||||
description: 'Use libXScrnSaver to determine idle time'
|
||||
)
|
||||
|
||||
option('icons-and-clipboard',
|
||||
type: 'feature',
|
||||
value: 'disabled',
|
||||
description: 'Enable GTK tray icons and clipboard paste support'
|
||||
)
|
||||
|
||||
option('gdk-pixbuf',
|
||||
type: 'feature',
|
||||
value: 'disabled',
|
||||
description: 'Enable GDK Pixbuf support to scale avatars before uploading'
|
||||
)
|
||||
|
||||
option('omemo-qrcode',
|
||||
type: 'feature',
|
||||
value: 'disabled',
|
||||
description: 'Enable ability to display OMEMO QR code'
|
||||
)
|
||||
|
||||
option('spellcheck',
|
||||
type: 'feature',
|
||||
value: 'disabled',
|
||||
description: 'Enable spellchecking support'
|
||||
)
|
||||
|
||||
# Other:
|
||||
option('python_framework',
|
||||
type: 'string',
|
||||
value: '',
|
||||
description: 'Set base directory for Python Framework (macOS only)'
|
||||
)
|
||||
|
||||
option('tests',
|
||||
type: 'boolean',
|
||||
value: false,
|
||||
description: 'Build tests'
|
||||
)
|
||||
|
||||
option('install_themes',
|
||||
type: 'boolean',
|
||||
value: true,
|
||||
description: 'Install themes'
|
||||
)
|
||||
|
||||
option('themes_path',
|
||||
type: 'string',
|
||||
value: '',
|
||||
description: 'Custom path for themes installation (empty for default)'
|
||||
)
|
||||
381
prof.supp
381
prof.supp
@@ -8,6 +8,106 @@
|
||||
# * python suppressions file from https://github.com/python/cpython/blob/main/Misc/valgrind-python.supp
|
||||
#
|
||||
|
||||
# ============================================
|
||||
# glibc AVX2 optimizations (false positives)
|
||||
# See: https://sourceware.org/bugzilla/show_bug.cgi?id=19796
|
||||
# ============================================
|
||||
|
||||
{
|
||||
glibc_wcpncpy_avx2
|
||||
Memcheck:Addr32
|
||||
fun:__wcpncpy_avx2
|
||||
fun:wcsxfrm_l
|
||||
fun:g_utf8_collate_key
|
||||
...
|
||||
}
|
||||
|
||||
{
|
||||
glibc_wcsxfrm_avx2
|
||||
Memcheck:Addr32
|
||||
...
|
||||
fun:wcsxfrm_l
|
||||
fun:g_utf8_collate_key
|
||||
...
|
||||
}
|
||||
|
||||
# ============================================
|
||||
# Functional tests suppressions (stabber/pthread)
|
||||
# ============================================
|
||||
|
||||
{
|
||||
stabber_pthread_create
|
||||
Memcheck:Leak
|
||||
match-leak-kinds: possible
|
||||
fun:calloc
|
||||
...
|
||||
fun:allocate_dtv
|
||||
fun:_dl_allocate_tls
|
||||
...
|
||||
fun:pthread_create*
|
||||
...
|
||||
fun:server_run
|
||||
...
|
||||
}
|
||||
|
||||
{
|
||||
stabber_server_run
|
||||
Memcheck:Leak
|
||||
match-leak-kinds: possible
|
||||
fun:calloc
|
||||
...
|
||||
fun:pthread_create*
|
||||
...
|
||||
obj:*/libstabber*
|
||||
...
|
||||
}
|
||||
|
||||
{
|
||||
glib_time_zone_cache
|
||||
Memcheck:Leak
|
||||
match-leak-kinds: reachable
|
||||
...
|
||||
fun:g_time_zone_new*
|
||||
...
|
||||
}
|
||||
|
||||
{
|
||||
glib_time_zone_local
|
||||
Memcheck:Leak
|
||||
match-leak-kinds: reachable
|
||||
...
|
||||
fun:g_strdup
|
||||
...
|
||||
fun:g_time_zone_new_identifier
|
||||
fun:g_time_zone_new_local
|
||||
...
|
||||
}
|
||||
|
||||
{
|
||||
glib_date_time_format
|
||||
Memcheck:Leak
|
||||
match-leak-kinds: reachable
|
||||
...
|
||||
fun:g_date_time_format
|
||||
...
|
||||
}
|
||||
|
||||
{
|
||||
glib_date_time_format_locale
|
||||
Memcheck:Leak
|
||||
match-leak-kinds: reachable
|
||||
fun:*alloc
|
||||
...
|
||||
obj:*/libglib*
|
||||
...
|
||||
fun:g_date_time_format
|
||||
...
|
||||
}
|
||||
|
||||
# ============================================
|
||||
# Original suppressions
|
||||
# ============================================
|
||||
|
||||
{
|
||||
_dl_init
|
||||
Memcheck:Leak
|
||||
@@ -2703,3 +2803,284 @@
|
||||
fun:calloc
|
||||
fun:_dl_allocate_tls
|
||||
}
|
||||
|
||||
# pthread TLS allocation in stabber server threads
|
||||
{
|
||||
pthread_create_tls_stabber
|
||||
Memcheck:Leak
|
||||
match-leak-kinds: possible
|
||||
fun:calloc
|
||||
...
|
||||
fun:allocate_dtv
|
||||
fun:_dl_allocate_tls
|
||||
fun:allocate_stack
|
||||
fun:pthread_create*
|
||||
fun:server_run
|
||||
}
|
||||
|
||||
# expect/tcl library allocations
|
||||
{
|
||||
tcl_alloc_expect
|
||||
Memcheck:Leak
|
||||
match-leak-kinds: possible
|
||||
fun:malloc
|
||||
...
|
||||
fun:TclpAlloc
|
||||
fun:Tcl_Alloc
|
||||
...
|
||||
fun:exp_expectl
|
||||
}
|
||||
|
||||
{
|
||||
exp_printify
|
||||
Memcheck:Leak
|
||||
match-leak-kinds: possible
|
||||
fun:malloc
|
||||
...
|
||||
fun:exp_printify
|
||||
...
|
||||
fun:exp_expectl
|
||||
}
|
||||
|
||||
|
||||
# Additional suppressions for functional tests
|
||||
|
||||
# libexpect still reachable allocations
|
||||
{
|
||||
exp_spawnv_malloc
|
||||
Memcheck:Leak
|
||||
match-leak-kinds: reachable
|
||||
fun:malloc
|
||||
...
|
||||
fun:exp_spawnv
|
||||
fun:exp_spawnl
|
||||
}
|
||||
|
||||
{
|
||||
exp_spawnl_realloc
|
||||
Memcheck:Leak
|
||||
match-leak-kinds: reachable
|
||||
fun:realloc
|
||||
...
|
||||
fun:exp_spawnv
|
||||
fun:exp_spawnl
|
||||
}
|
||||
|
||||
# libtcl memory pool allocations (expected)
|
||||
{
|
||||
tcl_alloc_pool
|
||||
Memcheck:Leak
|
||||
match-leak-kinds: reachable
|
||||
fun:malloc
|
||||
...
|
||||
fun:TclpAlloc
|
||||
fun:Tcl_Alloc
|
||||
}
|
||||
|
||||
{
|
||||
tcl_alloc_pool_calloc
|
||||
Memcheck:Leak
|
||||
match-leak-kinds: reachable
|
||||
fun:calloc
|
||||
...
|
||||
fun:TclpAlloc
|
||||
fun:Tcl_Alloc
|
||||
}
|
||||
|
||||
# pthread thread-local storage (normal for multi-threaded programs)
|
||||
{
|
||||
pthread_tls_allocate_dtv
|
||||
Memcheck:Leak
|
||||
match-leak-kinds: possible
|
||||
fun:calloc
|
||||
fun:calloc
|
||||
fun:allocate_dtv
|
||||
fun:_dl_allocate_tls
|
||||
fun:allocate_stack
|
||||
fun:pthread_create*
|
||||
}
|
||||
|
||||
# glib static initializations
|
||||
{
|
||||
glib_hash_table_init
|
||||
Memcheck:Leak
|
||||
match-leak-kinds: reachable
|
||||
fun:malloc
|
||||
fun:g_malloc
|
||||
fun:g_hash_table_new_full
|
||||
...
|
||||
fun:call_init
|
||||
fun:_dl_init
|
||||
}
|
||||
|
||||
{
|
||||
glib_array_init
|
||||
Memcheck:Leak
|
||||
match-leak-kinds: reachable
|
||||
fun:realloc
|
||||
fun:g_realloc
|
||||
...
|
||||
fun:call_init
|
||||
fun:_dl_init
|
||||
}
|
||||
|
||||
# fdopen/fopen allocations (FILE* buffers - normal)
|
||||
{
|
||||
fdopen_file_buffer
|
||||
Memcheck:Leak
|
||||
match-leak-kinds: reachable
|
||||
fun:malloc
|
||||
...
|
||||
fun:fdopen*
|
||||
}
|
||||
|
||||
{
|
||||
fopen_file_buffer
|
||||
Memcheck:Leak
|
||||
match-leak-kinds: reachable
|
||||
fun:malloc
|
||||
...
|
||||
fun:fopen*
|
||||
}
|
||||
|
||||
# stabber log_init (server log file)
|
||||
{
|
||||
stabber_log_init
|
||||
Memcheck:Leak
|
||||
match-leak-kinds: reachable
|
||||
fun:malloc
|
||||
...
|
||||
fun:log_init
|
||||
fun:server_run
|
||||
}
|
||||
|
||||
# glib time zone (static allocation)
|
||||
{
|
||||
g_time_zone_new_local
|
||||
Memcheck:Leak
|
||||
match-leak-kinds: reachable
|
||||
...
|
||||
fun:g_time_zone_new_local
|
||||
}
|
||||
|
||||
{
|
||||
g_time_zone_array
|
||||
Memcheck:Leak
|
||||
match-leak-kinds: reachable
|
||||
fun:realloc
|
||||
fun:g_realloc
|
||||
...
|
||||
fun:g_array_sized_new
|
||||
fun:g_time_zone_new_identifier
|
||||
}
|
||||
|
||||
# stabber log g_date_time_format invalid read - benign race in logging
|
||||
{
|
||||
stabber_g_date_time_format_invalid_read
|
||||
Memcheck:Addr1
|
||||
...
|
||||
fun:g_date_time_format
|
||||
fun:log_println
|
||||
...
|
||||
}
|
||||
|
||||
{
|
||||
stabber_g_date_time_format_invalid_read2
|
||||
Memcheck:Addr2
|
||||
...
|
||||
fun:g_date_time_format
|
||||
fun:log_println
|
||||
...
|
||||
}
|
||||
|
||||
{
|
||||
stabber_g_date_time_format_invalid_read4
|
||||
Memcheck:Addr4
|
||||
...
|
||||
fun:g_date_time_format
|
||||
fun:log_println
|
||||
...
|
||||
}
|
||||
|
||||
{
|
||||
stabber_g_date_time_format_invalid_read8
|
||||
Memcheck:Addr8
|
||||
...
|
||||
fun:g_date_time_format
|
||||
fun:log_println
|
||||
...
|
||||
}
|
||||
|
||||
# More generic suppression for g_date_time_format race condition
|
||||
{
|
||||
g_date_time_format_cond
|
||||
Memcheck:Cond
|
||||
...
|
||||
fun:g_date_time_format
|
||||
...
|
||||
}
|
||||
|
||||
{
|
||||
g_date_time_format_value1
|
||||
Memcheck:Value1
|
||||
...
|
||||
fun:g_date_time_format
|
||||
...
|
||||
}
|
||||
|
||||
{
|
||||
g_date_time_format_value2
|
||||
Memcheck:Value2
|
||||
...
|
||||
fun:g_date_time_format
|
||||
...
|
||||
}
|
||||
|
||||
{
|
||||
g_date_time_format_value4
|
||||
Memcheck:Value4
|
||||
...
|
||||
fun:g_date_time_format
|
||||
...
|
||||
}
|
||||
|
||||
{
|
||||
g_date_time_format_value8
|
||||
Memcheck:Value8
|
||||
...
|
||||
fun:g_date_time_format
|
||||
...
|
||||
}
|
||||
|
||||
# Suppress all Addr errors in log_println
|
||||
{
|
||||
log_println_addr1
|
||||
Memcheck:Addr1
|
||||
...
|
||||
fun:log_println
|
||||
...
|
||||
}
|
||||
|
||||
{
|
||||
log_println_addr2
|
||||
Memcheck:Addr2
|
||||
...
|
||||
fun:log_println
|
||||
...
|
||||
}
|
||||
|
||||
{
|
||||
log_println_addr4
|
||||
Memcheck:Addr4
|
||||
...
|
||||
fun:log_println
|
||||
...
|
||||
}
|
||||
|
||||
{
|
||||
log_println_addr8
|
||||
Memcheck:Addr8
|
||||
...
|
||||
fun:log_println
|
||||
...
|
||||
}
|
||||
|
||||
@@ -86,7 +86,7 @@
|
||||
<xmpp:SupportedXep>
|
||||
<xmpp:xep rdf:resource='https://xmpp.org/extensions/xep-0030.html'/>
|
||||
<xmpp:status>complete</xmpp:status>
|
||||
<xmpp:version>2.5rc3</xmpp:version>
|
||||
<xmpp:version>2.5.0</xmpp:version>
|
||||
<xmpp:since>0.4.5</xmpp:since>
|
||||
<xmpp:note xml:lang='en'>Use /disco items and /disco info</xmpp:note>
|
||||
</xmpp:SupportedXep>
|
||||
@@ -97,7 +97,7 @@
|
||||
<xmpp:SupportedXep>
|
||||
<xmpp:xep rdf:resource='https://xmpp.org/extensions/xep-0045.html'/>
|
||||
<xmpp:status>partial</xmpp:status>
|
||||
<xmpp:version>?</xmpp:version>
|
||||
<xmpp:version>1.35.2</xmpp:version>
|
||||
<xmpp:since>0.4.5</xmpp:since>
|
||||
<xmpp:note xml:lang='en'></xmpp:note>
|
||||
</xmpp:SupportedXep>
|
||||
@@ -141,7 +141,7 @@
|
||||
<xmpp:SupportedXep>
|
||||
<xmpp:xep rdf:resource='https://xmpp.org/extensions/xep-0054.html'/>
|
||||
<xmpp:status>complete</xmpp:status>
|
||||
<xmpp:version>1.2</xmpp:version>
|
||||
<xmpp:version>1.3.0</xmpp:version>
|
||||
<xmpp:since>0.14.0</xmpp:since>
|
||||
</xmpp:SupportedXep>
|
||||
</implements>
|
||||
@@ -161,7 +161,7 @@
|
||||
<xmpp:SupportedXep>
|
||||
<xmpp:xep rdf:resource='https://xmpp.org/extensions/xep-0060.html'/>
|
||||
<xmpp:status>complete</xmpp:status>
|
||||
<xmpp:version>1.15.8</xmpp:version>
|
||||
<xmpp:version>1.30.0</xmpp:version>
|
||||
<xmpp:since>0.7.0</xmpp:since>
|
||||
<xmpp:note xml:lang='en'>Used for OMEMO.</xmpp:note>
|
||||
</xmpp:SupportedXep>
|
||||
@@ -174,7 +174,6 @@
|
||||
<xmpp:status>complete</xmpp:status>
|
||||
<xmpp:version>2.4</xmpp:version>
|
||||
<xmpp:since>0.11.0</xmpp:since>
|
||||
<xmpp:note xml:lang='en'>Only password change. On master also to create an account.</xmpp:note>
|
||||
</xmpp:SupportedXep>
|
||||
</implements>
|
||||
|
||||
@@ -227,7 +226,7 @@
|
||||
<xmpp:SupportedXep>
|
||||
<xmpp:xep rdf:resource='https://xmpp.org/extensions/xep-0115.html'/>
|
||||
<xmpp:status>complete</xmpp:status>
|
||||
<xmpp:version>unknown</xmpp:version>
|
||||
<xmpp:version>1.6.0</xmpp:version>
|
||||
<xmpp:since>0.4.5</xmpp:since>
|
||||
<xmpp:note xml:lang='en'></xmpp:note>
|
||||
</xmpp:SupportedXep>
|
||||
@@ -260,7 +259,7 @@
|
||||
<xmpp:SupportedXep>
|
||||
<xmpp:xep rdf:resource='https://xmpp.org/extensions/xep-0184.html'/>
|
||||
<xmpp:status>complete</xmpp:status>
|
||||
<xmpp:version>1.2</xmpp:version>
|
||||
<xmpp:version>1.4.0</xmpp:version>
|
||||
<xmpp:since>0.4.7</xmpp:since>
|
||||
<xmpp:note xml:lang='en'></xmpp:note>
|
||||
</xmpp:SupportedXep>
|
||||
@@ -282,7 +281,7 @@
|
||||
<xmpp:SupportedXep>
|
||||
<xmpp:xep rdf:resource='https://xmpp.org/extensions/xep-0198.html'/>
|
||||
<xmpp:status>complete</xmpp:status>
|
||||
<xmpp:version>1.6</xmpp:version>
|
||||
<xmpp:version>1.6.3</xmpp:version>
|
||||
<xmpp:since>0.13.0</xmpp:since>
|
||||
<xmpp:note xml:lang='en'></xmpp:note>
|
||||
</xmpp:SupportedXep>
|
||||
@@ -326,7 +325,7 @@
|
||||
<xmpp:SupportedXep>
|
||||
<xmpp:xep rdf:resource='https://xmpp.org/extensions/xep-0249.html'/>
|
||||
<xmpp:status>complete</xmpp:status>
|
||||
<xmpp:version>1.0</xmpp:version>
|
||||
<xmpp:version>1.2</xmpp:version>
|
||||
<xmpp:since>0.4.5</xmpp:since>
|
||||
<xmpp:note xml:lang='en'></xmpp:note>
|
||||
</xmpp:SupportedXep>
|
||||
@@ -348,7 +347,7 @@
|
||||
<xmpp:SupportedXep>
|
||||
<xmpp:xep rdf:resource='https://xmpp.org/extensions/xep-0280.html'/>
|
||||
<xmpp:status>complete</xmpp:status>
|
||||
<xmpp:version>0.12.1</xmpp:version>
|
||||
<xmpp:version>1.0.1</xmpp:version>
|
||||
<xmpp:since>0.4.7</xmpp:since>
|
||||
<xmpp:note xml:lang='en'></xmpp:note>
|
||||
</xmpp:SupportedXep>
|
||||
@@ -370,7 +369,7 @@
|
||||
<xmpp:SupportedXep>
|
||||
<xmpp:xep rdf:resource='https://xmpp.org/extensions/xep-0308.html'/>
|
||||
<xmpp:status>complete</xmpp:status>
|
||||
<xmpp:version>1.2.0</xmpp:version>
|
||||
<xmpp:version>1.2.1</xmpp:version>
|
||||
<xmpp:since>0.9.0</xmpp:since>
|
||||
<xmpp:note xml:lang='en'></xmpp:note>
|
||||
</xmpp:SupportedXep>
|
||||
@@ -381,7 +380,7 @@
|
||||
<xmpp:SupportedXep>
|
||||
<xmpp:xep rdf:resource='https://xmpp.org/extensions/xep-0359.html'/>
|
||||
<xmpp:status>complete</xmpp:status>
|
||||
<xmpp:version>0.6.1</xmpp:version>
|
||||
<xmpp:version>0.7.0</xmpp:version>
|
||||
<xmpp:since>0.8.0</xmpp:since>
|
||||
<xmpp:note xml:lang='en'></xmpp:note>
|
||||
</xmpp:SupportedXep>
|
||||
@@ -392,7 +391,7 @@
|
||||
<xmpp:SupportedXep>
|
||||
<xmpp:xep rdf:resource='https://xmpp.org/extensions/xep-0363.html'/>
|
||||
<xmpp:status>complete</xmpp:status>
|
||||
<xmpp:version>1.0.0</xmpp:version>
|
||||
<xmpp:version>1.2.0</xmpp:version>
|
||||
<xmpp:since>0.5.0</xmpp:since>
|
||||
<xmpp:note xml:lang='en'></xmpp:note>
|
||||
</xmpp:SupportedXep>
|
||||
@@ -436,7 +435,7 @@
|
||||
<xmpp:SupportedXep>
|
||||
<xmpp:xep rdf:resource='https://xmpp.org/extensions/xep-0377.html'/>
|
||||
<xmpp:status>complete</xmpp:status>
|
||||
<xmpp:version>0.3</xmpp:version>
|
||||
<xmpp:version>0.4.0</xmpp:version>
|
||||
<xmpp:since>0.11.0</xmpp:since>
|
||||
<xmpp:note xml:lang='en'></xmpp:note>
|
||||
</xmpp:SupportedXep>
|
||||
@@ -458,7 +457,7 @@
|
||||
<xmpp:SupportedXep>
|
||||
<xmpp:xep rdf:resource='https://xmpp.org/extensions/xep-0392.html'/>
|
||||
<xmpp:status>complete</xmpp:status>
|
||||
<xmpp:version>0.7.0</xmpp:version>
|
||||
<xmpp:version>1.0.1</xmpp:version>
|
||||
<xmpp:since>0.8.0</xmpp:since>
|
||||
<xmpp:note xml:lang='en'></xmpp:note>
|
||||
</xmpp:SupportedXep>
|
||||
@@ -476,9 +475,24 @@
|
||||
</implements>
|
||||
|
||||
<release>
|
||||
<Version>
|
||||
<revision>0.17.0</revision>
|
||||
<created>2026-03-26</created>
|
||||
<file-release rdf:resource='https://profanity-im.github.io/tarballs/profanity-0.17.0.tar.xz'/>
|
||||
</Version>
|
||||
<Version>
|
||||
<revision>0.16.0</revision>
|
||||
<created>2026-02-23</created>
|
||||
<file-release rdf:resource='https://profanity-im.github.io/tarballs/profanity-0.16.0.tar.gz'/>
|
||||
</Version>
|
||||
<Version>
|
||||
<revision>0.15.1</revision>
|
||||
<created>2025-08-22</created>
|
||||
<file-release rdf:resource='https://profanity-im.github.io/tarballs/profanity-0.15.1.tar.gz'/>
|
||||
</Version>
|
||||
<Version>
|
||||
<revision>0.15.0</revision>
|
||||
<created>2024-03-27</created>
|
||||
<created>2025-03-27</created>
|
||||
<file-release rdf:resource='https://profanity-im.github.io/tarballs/profanity-0.15.0.tar.gz'/>
|
||||
</Version>
|
||||
<Version>
|
||||
|
||||
@@ -49,6 +49,8 @@ grlog=true
|
||||
maxsize=1048580
|
||||
rotate=true
|
||||
shared=true
|
||||
# Database backend: on (SQLite), off, redact, flatfile
|
||||
dblog=on
|
||||
|
||||
[otr]
|
||||
warn=true
|
||||
|
||||
35
scripts/README.md
Normal file
35
scripts/README.md
Normal file
@@ -0,0 +1,35 @@
|
||||
# Scripts
|
||||
|
||||
This directory contains various scripts used for development, CI, and maintenance.
|
||||
|
||||
## `lint-and-test.sh`
|
||||
Central validation script mainly for local development.
|
||||
|
||||
- **Purpose:** Runs spelling checks, code formatting, and unit testing.
|
||||
- **Usage:** `./scripts/lint-and-test.sh [options]`
|
||||
- **Options:**
|
||||
- `--fix-formatting`: Automatically apply `clang-format` fixes.
|
||||
- `--tests`: Run unit tests using Meson.
|
||||
- `--hook`: Run in "hook mode" (checks only staged files).
|
||||
- `--install`: Install the script as a git `pre-commit` hook.
|
||||
|
||||
## `build-configuration-matrix.sh`
|
||||
Exhaustive build configuration matrix testing. It ensures that Profanity compiles and passes unit tests across a variety of feature configurations for Meson.
|
||||
|
||||
- **Purpose:** Verifies architectural compatibility by testing many combinations of build flags.
|
||||
- **Usage:** `./scripts/build-configuration-matrix.sh [extra-args]`
|
||||
- **Extra Arguments:** Any arguments are forwarded directly to the configuration command (`meson setup`).
|
||||
- **Environment:** Primarily used in CI (GitHub Actions), but can be run locally to verify all configurations.
|
||||
|
||||
## `changelog-helper.py`
|
||||
Generates a sorted changelog from git commits since the last tag.
|
||||
|
||||
- **Purpose:** Automates the creation of release notes by parsing Conventional Commit messages.
|
||||
- **Usage:** `python3 scripts/changelog-helper.py [--pr]`
|
||||
- **Options:** Use `--pr` to append Pull Request numbers to each entry.
|
||||
|
||||
## `check-new-xeps.py`
|
||||
Checks for updates to XMPP Extension Protocols (XEPs).
|
||||
|
||||
- **Purpose:** Compares the versions of XEPs implemented in Profanity (tracked in `profanity.doap`) against the latest versions available at `xmpp.org`.
|
||||
- **Usage:** `python3 scripts/check-new-xeps.py`
|
||||
113
scripts/build-configuration-matrix.sh
Executable file
113
scripts/build-configuration-matrix.sh
Executable file
@@ -0,0 +1,113 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Exhaustive build configuration matrix testing for Profanity
|
||||
|
||||
set -e
|
||||
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m'
|
||||
|
||||
log_content() {
|
||||
echo
|
||||
if [ -f "$1" ]; then
|
||||
echo "Content of $1:"
|
||||
cat "$1"
|
||||
else
|
||||
echo "Log file $1 not found."
|
||||
fi
|
||||
}
|
||||
|
||||
error_handler() {
|
||||
ERR_CODE=$?
|
||||
echo -e "${RED}Error ${ERR_CODE} with command '${BASH_COMMAND}' on line ${BASH_LINENO[0]}. Exiting.${NC}"
|
||||
|
||||
for log in build_run/meson-logs/testlog.txt build_valgrind/meson-logs/testlog.txt; do
|
||||
if [ -f "$log" ]; then
|
||||
echo "--- Meson Test Log ($log) ---"
|
||||
cat "$log"
|
||||
fi
|
||||
done
|
||||
exit ${ERR_CODE}
|
||||
}
|
||||
|
||||
trap error_handler ERR
|
||||
|
||||
num_cores() {
|
||||
nproc \
|
||||
|| sysctl -n hw.ncpu \
|
||||
|| getconf _NPROCESSORS_ONLN 2>/dev/null \
|
||||
|| echo 2
|
||||
}
|
||||
|
||||
usage() {
|
||||
echo "Usage: $0 [extra-args]"
|
||||
echo ""
|
||||
echo "Run exhaustive build matrix tests."
|
||||
echo ""
|
||||
echo "Extra arguments are passed directly to: meson setup [extra-args]"
|
||||
exit 0
|
||||
}
|
||||
|
||||
if [[ "$1" == "--help" || "$1" == "-h" ]]; then
|
||||
usage
|
||||
fi
|
||||
|
||||
# Compatibility with old script that took autotools|meson as first arg
|
||||
if [[ "$1" == "meson" ]]; then
|
||||
shift
|
||||
elif [[ "$1" == "autotools" ]]; then
|
||||
echo -e "${RED}Error: Autotools is no longer supported.${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
ARCH="$(uname | tr '[:upper:]' '[:lower:]')"
|
||||
EXTRA_ARGS="$*"
|
||||
|
||||
# Build Matrix
|
||||
echo -e "${YELLOW}---> Starting build matrix...${NC}"
|
||||
|
||||
tests=(
|
||||
"-Dnotifications=enabled -Dicons-and-clipboard=enabled -Dotr=enabled -Dpgp=enabled -Domemo=enabled -Dc-plugins=enabled -Dpython-plugins=enabled -Dxscreensaver=enabled -Domemo-qrcode=enabled -Dgdk-pixbuf=enabled"
|
||||
""
|
||||
"-Dnotifications=disabled"
|
||||
"-Dicons-and-clipboard=disabled"
|
||||
"-Dotr=disabled"
|
||||
"-Dpgp=disabled"
|
||||
"-Domemo=disabled -Domemo-qrcode=disabled"
|
||||
"-Dpgp=disabled -Dotr=disabled"
|
||||
"-Dpgp=disabled -Dotr=disabled -Domemo=disabled"
|
||||
"-Dpython-plugins=disabled"
|
||||
"-Dc-plugins=disabled"
|
||||
"-Dc-plugins=disabled -Dpython-plugins=disabled"
|
||||
"-Dxscreensaver=disabled"
|
||||
"-Dgdk-pixbuf=disabled"
|
||||
)
|
||||
|
||||
BACKEND_OPT=""
|
||||
if [ -n "${OMEMO_BACKEND}" ]; then
|
||||
BACKEND_OPT="-Domemo-backend=${OMEMO_BACKEND}"
|
||||
fi
|
||||
|
||||
# Valgrind check (Linux only)
|
||||
if [[ "$ARCH" == linux* ]]; then
|
||||
echo -e "${YELLOW}--> Running Valgrind check with full features ${BACKEND_OPT} ${EXTRA_ARGS}${NC}"
|
||||
rm -rf build_valgrind
|
||||
meson setup build_valgrind ${tests[0]} ${BACKEND_OPT} -Dtests=true -Db_sanitize=undefined ${EXTRA_ARGS}
|
||||
meson compile -C build_valgrind
|
||||
meson test -C build_valgrind "unit tests" --print-errorlogs --wrap=valgrind || echo "Valgrind issues detected"
|
||||
rm -rf build_valgrind
|
||||
fi
|
||||
|
||||
for features in "${tests[@]}"
|
||||
do
|
||||
echo -e "${YELLOW}--> Building with: ${features} ${BACKEND_OPT} ${EXTRA_ARGS}${NC}"
|
||||
rm -rf build_run
|
||||
meson setup build_run ${features} ${BACKEND_OPT} -Dtests=true ${EXTRA_ARGS}
|
||||
meson compile -C build_run
|
||||
meson test -C build_run "unit tests" --print-errorlogs
|
||||
./build_run/profanity -v
|
||||
done
|
||||
|
||||
echo -e "${GREEN}Build configuration matrix testing successful!${NC}"
|
||||
165
scripts/changelog-helper.py
Executable file
165
scripts/changelog-helper.py
Executable file
@@ -0,0 +1,165 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# Since we use Conventional Commits
|
||||
# we can now create our Changelog semi-automatically
|
||||
|
||||
import subprocess
|
||||
import sys
|
||||
import re
|
||||
import argparse
|
||||
from collections import defaultdict
|
||||
|
||||
# Configuration for sections and their order
|
||||
SECTION_CONFIG = [
|
||||
("fix", "Bug Fixes"),
|
||||
("feat", "Features"),
|
||||
("build", "Build System"),
|
||||
("ci", "CI"),
|
||||
("docs", "Documentation"),
|
||||
("perf", "Performance Improvements"),
|
||||
("refactor", "Refactorings"),
|
||||
("cleanup", "Cleanup"),
|
||||
("style", "Style"),
|
||||
("tests", "Tests"),
|
||||
("chore", "Chores"),
|
||||
]
|
||||
SECTIONS = dict(SECTION_CONFIG)
|
||||
TYPE_ORDER = [t for t, _ in SECTION_CONFIG]
|
||||
|
||||
CORRECTIONS = {
|
||||
"ests": "tests",
|
||||
"wleanup": "cleanup",
|
||||
}
|
||||
|
||||
def git_run(args):
|
||||
"""Run a git command and return stripped output lines."""
|
||||
try:
|
||||
result = subprocess.run(["git"] + args, capture_output=True, text=True, check=True)
|
||||
return [line for line in result.stdout.strip().split('\n') if line]
|
||||
except subprocess.CalledProcessError:
|
||||
return []
|
||||
|
||||
def get_last_tag():
|
||||
output = git_run(["describe", "--tags", "--abbrev=0"])
|
||||
return output[0] if output else None
|
||||
|
||||
def get_commits(revision_range):
|
||||
"""Get list of (hash, subject) tuples."""
|
||||
lines = git_run(["log", revision_range, "--format=%H %s"])
|
||||
commits = []
|
||||
for line in lines:
|
||||
parts = line.split(' ', 1)
|
||||
if len(parts) == 2:
|
||||
commits.append(parts)
|
||||
return commits
|
||||
|
||||
def get_pr_mappings(revision_range):
|
||||
"""Map commit hashes to PR numbers found in merge commits."""
|
||||
merge_commits = git_run(["log", revision_range, "--merges", "--format=%H %s"])
|
||||
pr_map = {}
|
||||
pr_re = re.compile(r'Merge pull request #(\d+)')
|
||||
|
||||
for line in merge_commits:
|
||||
parts = line.split(' ', 1)
|
||||
if len(parts) < 2:
|
||||
continue
|
||||
m_hash, m_subject = parts
|
||||
match = pr_re.search(m_subject)
|
||||
if match:
|
||||
pr_num = match.group(1)
|
||||
# Find all commits that are part of this merge branch
|
||||
branch_commits = git_run(["rev-list", f"{m_hash}^1..{m_hash}^2"])
|
||||
for b_hash in branch_commits:
|
||||
pr_map[b_hash] = pr_num
|
||||
return pr_map
|
||||
|
||||
def get_contributors(revision_range):
|
||||
"""Get sorted list of all unique contributors."""
|
||||
cmd = ["log", revision_range, "--format=%an%n%(trailers:key=Co-authored-by,valueonly=true)"]
|
||||
output = git_run(cmd)
|
||||
|
||||
contributors = set()
|
||||
for line in output:
|
||||
# Remove email part if present: "Name <email@example.com>" -> "Name"
|
||||
name = line.split('<')[0].strip()
|
||||
if name:
|
||||
contributors.add(name)
|
||||
|
||||
return sorted(list(contributors))
|
||||
|
||||
def format_description(description):
|
||||
"""Capitalize first letter of description."""
|
||||
description = description.strip()
|
||||
if description and description[0].islower():
|
||||
return description[0].upper() + description[1:]
|
||||
return description
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Generate a sorted changelog from git commits.")
|
||||
parser.add_argument("--pr", action="store_true", help="Append PR number to each commit.")
|
||||
args = parser.parse_args()
|
||||
|
||||
last_tag = get_last_tag()
|
||||
if not last_tag:
|
||||
print("No tags found in the repository.", file=sys.stderr)
|
||||
|
||||
revision_range = f"{last_tag}..HEAD" if last_tag else "HEAD"
|
||||
|
||||
commits = get_commits(revision_range)
|
||||
if not commits:
|
||||
print(f"No commits found since {last_tag if last_tag else 'the beginning'}.")
|
||||
return
|
||||
|
||||
pr_map = get_pr_mappings(revision_range) if args.pr else {}
|
||||
|
||||
# Conventional Commit regex: type(scope): description
|
||||
commit_re = re.compile(r'^(\w+)(?:\(([^)]+)\))?:\s*(.*)$')
|
||||
|
||||
grouped = defaultdict(list)
|
||||
others = []
|
||||
|
||||
for c_hash, c_subject in commits:
|
||||
# Skip merge commits in the output
|
||||
if c_subject.startswith(("Merge pull request", "Merge branch")):
|
||||
continue
|
||||
|
||||
pr_suffix = f" (#{pr_map[c_hash]})" if c_hash in pr_map else ""
|
||||
match = commit_re.match(c_subject)
|
||||
|
||||
if match:
|
||||
ctype = match.group(1).lower()
|
||||
ctype = CORRECTIONS.get(ctype, ctype)
|
||||
description = format_description(match.group(3))
|
||||
grouped[ctype].append(f"{description}{pr_suffix}")
|
||||
else:
|
||||
others.append(f"{c_subject}{pr_suffix}")
|
||||
|
||||
# Output sections in ordered priority
|
||||
all_types = TYPE_ORDER + sorted([t for t in grouped if t not in TYPE_ORDER])
|
||||
first = True
|
||||
for ctype in all_types:
|
||||
if ctype in grouped:
|
||||
if not first:
|
||||
print()
|
||||
section_name = SECTIONS.get(ctype, ctype.capitalize())
|
||||
print(f"{section_name}:")
|
||||
for msg in sorted(grouped[ctype]):
|
||||
print(f"- {msg}")
|
||||
first = False
|
||||
|
||||
if others:
|
||||
if not first:
|
||||
print()
|
||||
print("Others:")
|
||||
for msg in sorted(others):
|
||||
print(f"- {msg}")
|
||||
|
||||
# Contributors section
|
||||
contributors = get_contributors(revision_range)
|
||||
if contributors:
|
||||
print("\nContributors:")
|
||||
for i, name in enumerate(contributors, 1):
|
||||
print(f"{i}. {name}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
136
scripts/check-new-xeps.py
Executable file
136
scripts/check-new-xeps.py
Executable file
@@ -0,0 +1,136 @@
|
||||
#!/usr/bin/env python3
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import urllib.request
|
||||
import xml.etree.ElementTree as ET
|
||||
from itertools import zip_longest
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
|
||||
# Namespaces in DOAP
|
||||
NS = {
|
||||
"rdf": "http://www.w3.org/1999/02/22-rdf-syntax-ns#",
|
||||
"foaf": "http://xmlns.com/foaf/0.1/",
|
||||
"doap": "http://usefulinc.com/ns/doap#",
|
||||
"xmpp": "https://linkmauve.fr/ns/xmpp-doap#",
|
||||
"schema": "https://schema.org/",
|
||||
}
|
||||
|
||||
|
||||
def compare_versions(v1: str, v2: str) -> int:
|
||||
"""
|
||||
Compare two version strings.
|
||||
Returns 1 if v2 > v1, -1 if v1 > v2, 0 if equal.
|
||||
"""
|
||||
def parse_v(v: str) -> List[int]:
|
||||
return [int(x) for x in re.split(r"[^0-9]+", v) if x]
|
||||
|
||||
parts1 = parse_v(v1)
|
||||
parts2 = parse_v(v2)
|
||||
|
||||
for p1, p2 in zip_longest(parts1, parts2, fillvalue=0):
|
||||
if p2 > p1:
|
||||
return 1
|
||||
if p1 > p2:
|
||||
return -1
|
||||
return 0
|
||||
|
||||
|
||||
def find_doap() -> Optional[str]:
|
||||
"""Try to find profanity.doap in current or parent directory."""
|
||||
candidates = ["profanity.doap", "../profanity.doap"]
|
||||
for c in candidates:
|
||||
if os.path.exists(c):
|
||||
return c
|
||||
return None
|
||||
|
||||
|
||||
def get_implemented_xeps(doap_path: str) -> Dict[str, str]:
|
||||
"""Parse DOAP file and return a map of XEP number to version."""
|
||||
implemented_xeps: Dict[str, str] = {}
|
||||
try:
|
||||
tree = ET.parse(doap_path)
|
||||
root = tree.getroot()
|
||||
|
||||
for implements in root.findall(".//doap:implements", NS):
|
||||
supported_xep = implements.find(".//xmpp:SupportedXep", NS)
|
||||
if supported_xep is not None:
|
||||
xep_res = supported_xep.find(".//xmpp:xep", NS)
|
||||
version_elem = supported_xep.find(".//xmpp:version", NS)
|
||||
|
||||
if xep_res is not None and version_elem is not None:
|
||||
resource = xep_res.attrib.get(f"{{{NS['rdf']}}}resource", "")
|
||||
match = re.search(r"xep-(\d+)\.html", resource)
|
||||
if match:
|
||||
xep_num = match.group(1)
|
||||
current_version = version_elem.text.strip() if version_elem.text else ""
|
||||
implemented_xeps[xep_num] = current_version
|
||||
except (ET.ParseError, PermissionError) as e:
|
||||
print(f"Error reading {doap_path}: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
return implemented_xeps
|
||||
|
||||
|
||||
def check_xeps() -> None:
|
||||
"""Main logic for checking XEP updates."""
|
||||
doap_path = find_doap()
|
||||
if not doap_path:
|
||||
print("Error: Could not find DOAP file.")
|
||||
sys.exit(1)
|
||||
|
||||
implemented_xeps = get_implemented_xeps(doap_path)
|
||||
if not implemented_xeps:
|
||||
print("No XEPs found in DOAP file.")
|
||||
return
|
||||
|
||||
print(f"XEPs tracked: {len(implemented_xeps)}")
|
||||
|
||||
try:
|
||||
url = "https://xmpp.org/extensions/xeplist.xml"
|
||||
with urllib.request.urlopen(url, timeout=15) as response:
|
||||
xeplist_xml = response.read()
|
||||
except Exception as e:
|
||||
print(f"Error fetching xeplist.xml: {e}")
|
||||
return
|
||||
|
||||
try:
|
||||
xeplist_tree = ET.fromstring(xeplist_xml)
|
||||
except ET.ParseError as e:
|
||||
print(f"Error parsing xeplist.xml: {e}")
|
||||
return
|
||||
|
||||
print(f"\n{'XEP':<10} | {'Name':<35} | {'Ours':<8} | {'Latest':<8}")
|
||||
print("-" * 80)
|
||||
|
||||
updates_found = 0
|
||||
for xep_node in xeplist_tree.findall("xep"):
|
||||
number_node = xep_node.find("number")
|
||||
title_node = xep_node.find("title")
|
||||
last_revision_node = xep_node.find("last-revision")
|
||||
|
||||
if number_node is None or title_node is None or last_revision_node is None:
|
||||
continue
|
||||
|
||||
number = (number_node.text or "0").zfill(4)
|
||||
name = title_node.text or "Unknown"
|
||||
version_node = last_revision_node.find("version")
|
||||
|
||||
if version_node is None or version_node.text is None:
|
||||
continue
|
||||
|
||||
latest_version = version_node.text
|
||||
|
||||
if number in implemented_xeps:
|
||||
old_version = implemented_xeps[number]
|
||||
if compare_versions(old_version, latest_version) > 0:
|
||||
print(f"XEP-{number:<4} | {name[:35]:<35} | {old_version:<8} | {latest_version:<8}")
|
||||
updates_found += 1
|
||||
|
||||
if updates_found == 0:
|
||||
print("All tracked XEPs are up to date.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
check_xeps()
|
||||
138
scripts/lint-and-test.sh
Executable file
138
scripts/lint-and-test.sh
Executable file
@@ -0,0 +1,138 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Central linting and testing script for Profanity
|
||||
|
||||
set -e
|
||||
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m'
|
||||
|
||||
# Go to project root
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
usage() {
|
||||
echo "Usage: $0 [options]"
|
||||
echo ""
|
||||
echo "Description:"
|
||||
echo " Code quality checks (spelling, formatting, and unit tests)."
|
||||
echo " Works with meson and can be used as a git"
|
||||
echo " pre-commit hook."
|
||||
echo ""
|
||||
echo "Options:"
|
||||
echo " --fix-formatting Apply formatting fixes (default is check only)"
|
||||
echo " --no-format Skip the formatting check/fix entirely. Use this if your local"
|
||||
echo " clang-format version produces different results than the CI."
|
||||
echo " Can also be set via SKIP_FORMAT=1 environment variable."
|
||||
echo " --tests Run unit tests using Meson (meson test)"
|
||||
echo " --hook Git hook mode: Checks only staged files for spelling and"
|
||||
echo " formatting. Does not run tests to ensure fast commits."
|
||||
echo " --install Install this script as a git pre-commit hook"
|
||||
echo " --help Show this help message"
|
||||
}
|
||||
|
||||
run_tests() {
|
||||
local system=$1
|
||||
echo -e "${YELLOW}---> Running unit tests...${NC}"
|
||||
|
||||
if [ "$system" == "meson" ]; then
|
||||
echo "Using Meson..."
|
||||
# Uses build_run
|
||||
meson test -C build_run "unit tests"
|
||||
fi
|
||||
}
|
||||
|
||||
run_format() {
|
||||
local mode=$1
|
||||
local hook_mode=$2
|
||||
local skip=$3
|
||||
local files
|
||||
|
||||
if [ "$skip" = true ]; then
|
||||
echo -e "${YELLOW}---> Skipping formatting check.${NC}"
|
||||
return
|
||||
fi
|
||||
|
||||
if [ "$hook_mode" = true ]; then
|
||||
echo -e "${YELLOW}---> Checking staged files for formatting...${NC}"
|
||||
# Only added/modified C files
|
||||
files=$(git diff --cached --name-only --diff-filter=ACMR | grep -E "\.(c|h)$" || true)
|
||||
else
|
||||
echo -e "${YELLOW}---> Checking all files for formatting...${NC}"
|
||||
files=$(find src tests -name "*.[ch]" || true)
|
||||
fi
|
||||
|
||||
if [ -z "$files" ]; then
|
||||
echo "No C files to check."
|
||||
return
|
||||
fi
|
||||
|
||||
if [ "$mode" = "fix" ]; then
|
||||
clang-format -i $files
|
||||
echo -e "${GREEN}Formatting applied.${NC}"
|
||||
else
|
||||
# --Werror makes it return non-zero on diff
|
||||
if ! clang-format --dry-run --Werror $files; then
|
||||
echo -e "${RED}Error: Style violations found. Run '$0 --fix-formatting' to resolve.${NC}"
|
||||
echo -e "${RED}If this is due to a clang-format version mismatch, use --no-format or SKIP_FORMAT=1.${NC}"
|
||||
exit 1
|
||||
fi
|
||||
echo -e "${GREEN}Style check passed.${NC}"
|
||||
fi
|
||||
}
|
||||
|
||||
run_spell() {
|
||||
echo -e "${YELLOW}---> Running spell check...${NC}"
|
||||
if command -v codespell >/dev/null 2>&1; then
|
||||
codespell
|
||||
echo -e "${GREEN}Spell check passed.${NC}"
|
||||
else
|
||||
echo -e "${YELLOW}Warning: codespell not found, skipping.${NC}"
|
||||
fi
|
||||
}
|
||||
|
||||
MODE="check"
|
||||
BUILD_SYSTEM="none"
|
||||
HOOK=false
|
||||
|
||||
# Support environment variable override
|
||||
if [[ "$SKIP_FORMAT" == "1" || "$SKIP_FORMAT" == "true" ]]; then
|
||||
INTERNAL_SKIP_FORMAT=true
|
||||
else
|
||||
INTERNAL_SKIP_FORMAT=false
|
||||
fi
|
||||
|
||||
while [[ "$#" -gt 0 ]]; do
|
||||
case $1 in
|
||||
--fix-formatting) MODE="fix" ;;
|
||||
--no-format) INTERNAL_SKIP_FORMAT=true ;;
|
||||
--tests) BUILD_SYSTEM="meson" ;;
|
||||
--hook) HOOK=true ;;
|
||||
--install)
|
||||
echo "Installing pre-commit hook..."
|
||||
mkdir -p .git/hooks
|
||||
echo "#!/usr/bin/env bash" > .git/hooks/pre-commit
|
||||
echo "./scripts/lint-and-test.sh --hook" >> .git/hooks/pre-commit
|
||||
chmod +x .git/hooks/pre-commit
|
||||
echo "Hook installed."
|
||||
exit 0
|
||||
;;
|
||||
--help) usage; exit 0 ;;
|
||||
*) echo "Unknown parameter: $1"; usage; exit 1 ;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
|
||||
# Always run spell check
|
||||
run_spell
|
||||
|
||||
# Run format check unless skipped
|
||||
run_format "$MODE" "$HOOK" "$INTERNAL_SKIP_FORMAT"
|
||||
|
||||
# Run tests if a build system was specified
|
||||
if [ "$BUILD_SYSTEM" != "none" ]; then
|
||||
run_tests "$BUILD_SYSTEM"
|
||||
fi
|
||||
|
||||
echo -e "${GREEN}Validation successful!${NC}"
|
||||
1685
src/ai/ai_client.c
Normal file
1685
src/ai/ai_client.c
Normal file
File diff suppressed because it is too large
Load Diff
291
src/ai/ai_client.h
Normal file
291
src/ai/ai_client.h
Normal file
@@ -0,0 +1,291 @@
|
||||
#ifndef AI_CLIENT_H
|
||||
#define AI_CLIENT_H
|
||||
|
||||
#include <glib.h>
|
||||
|
||||
/**
|
||||
* @brief AI message structure for conversation history.
|
||||
*/
|
||||
typedef struct ai_message_t
|
||||
{
|
||||
gchar* role; /* "user" or "assistant" */
|
||||
gchar* content; /* Message content */
|
||||
} AIMessage;
|
||||
|
||||
/**
|
||||
* @brief AI provider configuration.
|
||||
*/
|
||||
typedef struct ai_provider_t
|
||||
{
|
||||
gchar* name; /* Provider name (e.g., "openai", "perplexity") */
|
||||
gchar* api_url; /* API endpoint URL */
|
||||
gchar* project_id; /* Optional project ID (for some providers) */
|
||||
gchar* default_model; /* Default model for this provider */
|
||||
GHashTable* settings; /* Extensible per-provider settings (e.g., tools=enabled, search=disabled) */
|
||||
GList* models; /* Cached models (gchar*) */
|
||||
gboolean models_fresh; /* Whether models cache is current */
|
||||
gint32 ref_count; /* Reference count (atomic) */
|
||||
} AIProvider;
|
||||
|
||||
/**
|
||||
* @brief AI chat session structure.
|
||||
*/
|
||||
typedef struct ai_session_t
|
||||
{
|
||||
pthread_mutex_t lock; /* Protects all session fields below */
|
||||
gchar* provider_name; /* Provider name */
|
||||
AIProvider* provider; /* Provider configuration */
|
||||
gchar* model; /* Model identifier (e.g., "gpt-4", "sonar") */
|
||||
gchar* api_key; /* API key for this session */
|
||||
GList* history; /* Conversation history (GList of AIMessage*) */
|
||||
gint32 ref_count; /* Reference count (atomic) */
|
||||
} AISession;
|
||||
|
||||
/* ========================================================================
|
||||
* Provider Management
|
||||
* ======================================================================== */
|
||||
|
||||
/**
|
||||
* Initialize the AI client and load default providers.
|
||||
*/
|
||||
void ai_client_init(void);
|
||||
|
||||
/**
|
||||
* Shutdown the AI client and free resources.
|
||||
*/
|
||||
void ai_client_shutdown(void);
|
||||
|
||||
/**
|
||||
* Get a provider by name.
|
||||
* @param name The provider name (e.g., "openai", "perplexity")
|
||||
* @return AIProvider*, or NULL if not found
|
||||
*/
|
||||
AIProvider* ai_get_provider(const gchar* name);
|
||||
|
||||
/**
|
||||
* Add or update a provider configuration.
|
||||
* @param name The provider name
|
||||
* @param api_url The API endpoint URL
|
||||
* @return New AIProvider* (caller must unref when done)
|
||||
*/
|
||||
AIProvider* ai_add_provider(const gchar* name, const gchar* api_url);
|
||||
|
||||
/**
|
||||
* Remove a provider by name.
|
||||
* @param name The provider name
|
||||
* @return TRUE if provider was removed, FALSE if not found
|
||||
*/
|
||||
gboolean ai_remove_provider(const gchar* name);
|
||||
|
||||
/**
|
||||
* Increment the reference count of a provider.
|
||||
* @param provider The provider to reference
|
||||
* @return The same provider pointer
|
||||
*/
|
||||
AIProvider* ai_provider_ref(AIProvider* provider);
|
||||
|
||||
/**
|
||||
* Decrement the reference count of a provider.
|
||||
* @param provider The provider to unreference
|
||||
*/
|
||||
void ai_provider_unref(AIProvider* provider);
|
||||
|
||||
/**
|
||||
* List all configured providers.
|
||||
* @return GList of AIProvider* (caller must not free the list or providers,
|
||||
* and must not unref the returned providers)
|
||||
*/
|
||||
GList* ai_list_providers(void);
|
||||
|
||||
/**
|
||||
* Find a provider name for autocomplete.
|
||||
* @param search_str The search string
|
||||
* @param previous Whether to go to previous match
|
||||
* @param context Unused
|
||||
* @return Provider name, or NULL if not found
|
||||
*/
|
||||
gchar* ai_providers_find(const char* const search_str, gboolean previous, void* context);
|
||||
|
||||
/**
|
||||
* Reset the provider autocomplete state.
|
||||
* Called from cmd_ac_reset() to clear autocomplete state.
|
||||
*/
|
||||
void ai_providers_reset_ac(void);
|
||||
|
||||
/**
|
||||
* Get the API key for a provider.
|
||||
* @param provider_name The provider name
|
||||
* @return The API key, or NULL if not set (caller must free)
|
||||
*/
|
||||
gchar* ai_get_provider_key(const gchar* provider_name);
|
||||
|
||||
/**
|
||||
* Set the API key for a provider.
|
||||
* @param provider_name The provider name
|
||||
* @param api_key The API key to set
|
||||
*/
|
||||
void ai_set_provider_key(const gchar* provider_name, const gchar* api_key);
|
||||
|
||||
/**
|
||||
* Set the default model for a provider.
|
||||
* @param provider_name The provider name
|
||||
* @param model The default model name
|
||||
*/
|
||||
void ai_set_provider_default_model(const gchar* provider_name, const gchar* model);
|
||||
|
||||
/**
|
||||
* Get the default model for a provider.
|
||||
* @param provider_name The provider name
|
||||
* @return The default model name (caller must not free), or NULL if not set
|
||||
*/
|
||||
const gchar* ai_get_provider_default_model(const gchar* provider_name);
|
||||
|
||||
/**
|
||||
* Set a custom setting for a provider.
|
||||
* @param provider_name The provider name
|
||||
* @param setting The setting key (e.g., "tools", "search")
|
||||
* @param value The setting value
|
||||
*/
|
||||
void ai_set_provider_setting(const gchar* provider_name, const gchar* setting, const gchar* value);
|
||||
|
||||
/**
|
||||
* Get a custom setting for a provider.
|
||||
* @param provider_name The provider name
|
||||
* @param setting The setting key
|
||||
* @return The setting value (caller must free), or NULL if not set
|
||||
*/
|
||||
gchar* ai_get_provider_setting(const gchar* provider_name, const gchar* setting);
|
||||
|
||||
/**
|
||||
* Fetch available models from a provider's API.
|
||||
* @param provider_name The provider name
|
||||
* @param user_data User data (ProfAiWin* for UI display)
|
||||
* @return TRUE if the request was successfully queued, FALSE otherwise
|
||||
*/
|
||||
gboolean ai_fetch_models(const gchar* provider_name, gpointer user_data);
|
||||
|
||||
/**
|
||||
* Check if models cache is fresh for a provider.
|
||||
* @param provider_name The provider name
|
||||
* @return TRUE if models are fresh, FALSE otherwise
|
||||
*/
|
||||
gboolean ai_models_are_fresh(const gchar* provider_name);
|
||||
|
||||
/* ========================================================================
|
||||
* Parsing helpers (exposed for testing)
|
||||
* ======================================================================== */
|
||||
|
||||
/**
|
||||
* Parse model IDs from an OpenAI-compatible API response.
|
||||
* Expected format: {"object":"list","data":[{"id":"model1",...},...]}
|
||||
* @param provider The provider to add models to
|
||||
* @param json The JSON response from the API
|
||||
*/
|
||||
void ai_parse_models_from_json(AIProvider* provider, const gchar* json);
|
||||
|
||||
/**
|
||||
* Extract assistant content from an LLM response body. Tries Perplexity
|
||||
* /v1/responses "text" first, falls back to OpenAI "content". Decodes
|
||||
* the \" and \n escape sequences only.
|
||||
* @param response_json The JSON body from the provider
|
||||
* @return Newly allocated content string, or NULL if not found (caller frees)
|
||||
*/
|
||||
gchar* ai_parse_response(const gchar* response_json);
|
||||
|
||||
/**
|
||||
* Extract human-readable error message from an API error envelope.
|
||||
* Expected format: {"error":{"message":"...","type":"...","code":...}}
|
||||
* Decodes \" \n \\ \t escapes.
|
||||
* @param error_json The JSON body of the error response
|
||||
* @return Newly allocated error message, or NULL if not parseable (caller frees)
|
||||
*/
|
||||
gchar* ai_parse_error_message(const gchar* error_json);
|
||||
|
||||
/* ========================================================================
|
||||
* Session Management
|
||||
* ======================================================================== */
|
||||
|
||||
/**
|
||||
* Create a new AI session with the specified provider and model.
|
||||
* @param provider_name The provider name (e.g., "openai")
|
||||
* @param model The model identifier (e.g., "gpt-4")
|
||||
* @return New AISession*, or NULL on failure
|
||||
*/
|
||||
AISession* ai_session_create(const gchar* provider_name, const gchar* model);
|
||||
|
||||
/**
|
||||
* Increment the reference count of an AI session.
|
||||
* @param session The session to reference
|
||||
* @return The same session pointer
|
||||
*/
|
||||
AISession* ai_session_ref(AISession* session);
|
||||
|
||||
/**
|
||||
* Decrement the reference count and free the session when it reaches zero.
|
||||
* @param session The session to unreference
|
||||
*/
|
||||
void ai_session_unref(AISession* session);
|
||||
|
||||
/**
|
||||
* Add a message to the session history.
|
||||
* @param session The session
|
||||
* @param role The message role ("user" or "assistant")
|
||||
* @param content The message content
|
||||
*/
|
||||
void ai_session_add_message(AISession* session, const gchar* role, const gchar* content);
|
||||
|
||||
/**
|
||||
* Clear the conversation history.
|
||||
* @param session The session
|
||||
*/
|
||||
void ai_session_clear_history(AISession* session);
|
||||
|
||||
/**
|
||||
* Get the current model for a session.
|
||||
* @param session The session
|
||||
* @return The model name (caller must not free)
|
||||
*/
|
||||
const gchar* ai_session_get_model(AISession* session);
|
||||
|
||||
/**
|
||||
* Set the model for a session.
|
||||
* @param session The session
|
||||
* @param model The model name
|
||||
*/
|
||||
void ai_session_set_model(AISession* session, const gchar* model);
|
||||
|
||||
/**
|
||||
* Atomically switch session provider, model, and API key.
|
||||
* All mutations happen under the session lock to prevent races with
|
||||
* _ai_request_thread() which snapshots session state before making requests.
|
||||
*
|
||||
* @param session The session
|
||||
* @param provider_name New provider name
|
||||
* @param model New model identifier
|
||||
* @param api_key New API key (caller must free after calling this)
|
||||
*/
|
||||
void ai_session_switch(AISession* session, const gchar* provider_name,
|
||||
const gchar* model, gchar* api_key);
|
||||
|
||||
/* ========================================================================
|
||||
* Request Handling
|
||||
* ======================================================================== */
|
||||
|
||||
/**
|
||||
* Send a prompt to the AI provider asynchronously.
|
||||
* @param session The AI session containing provider and model
|
||||
* @param prompt The prompt to send
|
||||
* @param user_data User data (ProfAiWin* for UI display)
|
||||
* @return TRUE if the request was successfully queued, FALSE otherwise
|
||||
*/
|
||||
gboolean ai_send_prompt(AISession* session, const gchar* prompt,
|
||||
gpointer user_data);
|
||||
|
||||
/**
|
||||
* Escape a string for JSON embedding.
|
||||
* @param str The string to escape
|
||||
* @return Newly allocated escaped string (caller must free)
|
||||
*/
|
||||
gchar* ai_json_escape(const gchar* str);
|
||||
|
||||
#endif /* AI_CLIENT_H */
|
||||
405
src/chatlog.c
405
src/chatlog.c
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* log.c
|
||||
* chatlog.c
|
||||
* vim: expandtab:ts=4:sts=4:sw=4
|
||||
*
|
||||
* Copyright (C) 2012 - 2019 James Booth <boothj5@gmail.com>
|
||||
@@ -36,442 +36,81 @@
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <assert.h>
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include "glib.h"
|
||||
#include "glib/gstdio.h"
|
||||
|
||||
#include "log.h"
|
||||
#include "chatlog.h"
|
||||
#include "common.h"
|
||||
#include "config/files.h"
|
||||
#include "config/preferences.h"
|
||||
#include "xmpp/xmpp.h"
|
||||
#include "xmpp/muc.h"
|
||||
|
||||
static GHashTable* logs;
|
||||
static GHashTable* groupchat_logs;
|
||||
|
||||
struct dated_chat_log
|
||||
{
|
||||
gchar* filename;
|
||||
GDateTime* date;
|
||||
};
|
||||
|
||||
static gboolean _log_roll_needed(struct dated_chat_log* dated_log);
|
||||
static struct dated_chat_log* _create_chatlog(const char* const other, const char* const login);
|
||||
static struct dated_chat_log* _create_groupchat_log(const char* const room, const char* const login);
|
||||
static void _free_chat_log(struct dated_chat_log* dated_log);
|
||||
static gboolean _key_equals(void* key1, void* key2);
|
||||
static void _chat_log_chat(const char* const login, const char* const other, const gchar* const msg,
|
||||
chat_log_direction_t direction, GDateTime* timestamp, const char* const resourcepart);
|
||||
static void _groupchat_log_chat(const gchar* const login, const gchar* const room, const gchar* const nick,
|
||||
const gchar* const msg);
|
||||
|
||||
void
|
||||
_chatlog_close(void)
|
||||
{
|
||||
g_hash_table_destroy(logs);
|
||||
g_hash_table_destroy(groupchat_logs);
|
||||
}
|
||||
/*
|
||||
* Background message-logging to per-day plain-text files under
|
||||
* $XDG_DATA_HOME/profanity/chatlogs/ is disabled. These files were never
|
||||
* read back by the application — history replay reads from the database
|
||||
* (log_database_get_previous_chat) — so this subsystem only ever wrote to
|
||||
* disk for users to read with external tools.
|
||||
*
|
||||
* All public entry points are kept as no-ops to avoid touching the ~20
|
||||
* call sites in event/, otr/ and profanity.c until the API itself is
|
||||
* removed.
|
||||
*/
|
||||
|
||||
void
|
||||
chatlog_init(void)
|
||||
{
|
||||
log_info("Initialising chat logs");
|
||||
|
||||
prof_add_shutdown_routine(_chatlog_close);
|
||||
|
||||
logs = g_hash_table_new_full(g_str_hash, (GEqualFunc)_key_equals, free,
|
||||
(GDestroyNotify)_free_chat_log);
|
||||
groupchat_logs = g_hash_table_new_full(g_str_hash, (GEqualFunc)_key_equals, free,
|
||||
(GDestroyNotify)_free_chat_log);
|
||||
}
|
||||
|
||||
void
|
||||
chat_log_msg_out(const char* const barejid, const char* const msg, const char* const resource)
|
||||
{
|
||||
if (prefs_get_boolean(PREF_CHLOG)) {
|
||||
_chat_log_chat(connection_get_barejid(), barejid, msg, PROF_OUT_LOG, NULL, resource);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
chat_log_otr_msg_out(const char* const barejid, const char* const msg, const char* const resource)
|
||||
{
|
||||
if (prefs_get_boolean(PREF_CHLOG)) {
|
||||
const char* mybarejid = connection_get_barejid();
|
||||
auto_gchar gchar* pref_otr_log = prefs_get_string(PREF_OTR_LOG);
|
||||
if (strcmp(pref_otr_log, "on") == 0) {
|
||||
_chat_log_chat(mybarejid, barejid, msg, PROF_OUT_LOG, NULL, resource);
|
||||
} else if (strcmp(pref_otr_log, "redact") == 0) {
|
||||
_chat_log_chat(mybarejid, barejid, "[redacted]", PROF_OUT_LOG, NULL, resource);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
chat_log_pgp_msg_out(const char* const barejid, const char* const msg, const char* const resource)
|
||||
{
|
||||
if (prefs_get_boolean(PREF_CHLOG)) {
|
||||
const char* mybarejid = connection_get_barejid();
|
||||
auto_gchar gchar* pref_pgp_log = prefs_get_string(PREF_PGP_LOG);
|
||||
if (strcmp(pref_pgp_log, "on") == 0) {
|
||||
_chat_log_chat(mybarejid, barejid, msg, PROF_OUT_LOG, NULL, resource);
|
||||
} else if (strcmp(pref_pgp_log, "redact") == 0) {
|
||||
_chat_log_chat(mybarejid, barejid, "[redacted]", PROF_OUT_LOG, NULL, resource);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
chat_log_omemo_msg_out(const char* const barejid, const char* const msg, const char* const resource)
|
||||
{
|
||||
if (prefs_get_boolean(PREF_CHLOG)) {
|
||||
const char* mybarejid = connection_get_barejid();
|
||||
auto_gchar gchar* pref_omemo_log = prefs_get_string(PREF_OMEMO_LOG);
|
||||
if (strcmp(pref_omemo_log, "on") == 0) {
|
||||
_chat_log_chat(mybarejid, barejid, msg, PROF_OUT_LOG, NULL, resource);
|
||||
} else if (strcmp(pref_omemo_log, "redact") == 0) {
|
||||
_chat_log_chat(mybarejid, barejid, "[redacted]", PROF_OUT_LOG, NULL, resource);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
chat_log_otr_msg_in(ProfMessage* message)
|
||||
{
|
||||
if (prefs_get_boolean(PREF_CHLOG)) {
|
||||
const char* mybarejid = connection_get_barejid();
|
||||
auto_gchar gchar* pref_otr_log = prefs_get_string(PREF_OTR_LOG);
|
||||
if (message->enc == PROF_MSG_ENC_NONE || (strcmp(pref_otr_log, "on") == 0)) {
|
||||
if (message->type == PROF_MSG_TYPE_MUCPM) {
|
||||
_chat_log_chat(mybarejid, message->from_jid->barejid, message->plain, PROF_IN_LOG, message->timestamp, message->from_jid->resourcepart);
|
||||
} else {
|
||||
_chat_log_chat(mybarejid, message->from_jid->barejid, message->plain, PROF_IN_LOG, message->timestamp, NULL);
|
||||
}
|
||||
} else if (strcmp(pref_otr_log, "redact") == 0) {
|
||||
if (message->type == PROF_MSG_TYPE_MUCPM) {
|
||||
_chat_log_chat(mybarejid, message->from_jid->barejid, "[redacted]", PROF_IN_LOG, message->timestamp, message->from_jid->resourcepart);
|
||||
} else {
|
||||
_chat_log_chat(mybarejid, message->from_jid->barejid, "[redacted]", PROF_IN_LOG, message->timestamp, NULL);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
chat_log_pgp_msg_in(ProfMessage* message)
|
||||
{
|
||||
if (prefs_get_boolean(PREF_CHLOG)) {
|
||||
const char* mybarejid = connection_get_barejid();
|
||||
auto_gchar gchar* pref_pgp_log = prefs_get_string(PREF_PGP_LOG);
|
||||
if (strcmp(pref_pgp_log, "on") == 0) {
|
||||
if (message->type == PROF_MSG_TYPE_MUCPM) {
|
||||
_chat_log_chat(mybarejid, message->from_jid->barejid, message->plain, PROF_IN_LOG, message->timestamp, message->from_jid->resourcepart);
|
||||
} else {
|
||||
_chat_log_chat(mybarejid, message->from_jid->barejid, message->plain, PROF_IN_LOG, message->timestamp, NULL);
|
||||
}
|
||||
} else if (strcmp(pref_pgp_log, "redact") == 0) {
|
||||
if (message->type == PROF_MSG_TYPE_MUCPM) {
|
||||
_chat_log_chat(mybarejid, message->from_jid->barejid, "[redacted]", PROF_IN_LOG, message->timestamp, message->from_jid->resourcepart);
|
||||
} else {
|
||||
_chat_log_chat(mybarejid, message->from_jid->barejid, "[redacted]", PROF_IN_LOG, message->timestamp, NULL);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
chat_log_omemo_msg_in(ProfMessage* message)
|
||||
{
|
||||
if (prefs_get_boolean(PREF_CHLOG)) {
|
||||
const char* mybarejid = connection_get_barejid();
|
||||
auto_gchar gchar* pref_omemo_log = prefs_get_string(PREF_OMEMO_LOG);
|
||||
if (strcmp(pref_omemo_log, "on") == 0) {
|
||||
if (message->type == PROF_MSG_TYPE_MUCPM) {
|
||||
_chat_log_chat(mybarejid, message->from_jid->barejid, message->plain, PROF_IN_LOG, message->timestamp, message->from_jid->resourcepart);
|
||||
} else {
|
||||
_chat_log_chat(mybarejid, message->from_jid->barejid, message->plain, PROF_IN_LOG, message->timestamp, NULL);
|
||||
}
|
||||
} else if (strcmp(pref_omemo_log, "redact") == 0) {
|
||||
if (message->type == PROF_MSG_TYPE_MUCPM) {
|
||||
_chat_log_chat(mybarejid, message->from_jid->barejid, "[redacted]", PROF_IN_LOG, message->timestamp, message->from_jid->resourcepart);
|
||||
} else {
|
||||
_chat_log_chat(mybarejid, message->from_jid->barejid, "[redacted]", PROF_IN_LOG, message->timestamp, message->from_jid->resourcepart);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
chat_log_msg_in(ProfMessage* message)
|
||||
{
|
||||
if (prefs_get_boolean(PREF_CHLOG)) {
|
||||
const char* mybarejid = connection_get_barejid();
|
||||
|
||||
if (message->type == PROF_MSG_TYPE_MUCPM) {
|
||||
_chat_log_chat(mybarejid, message->from_jid->barejid, message->plain, PROF_IN_LOG, message->timestamp, message->from_jid->resourcepart);
|
||||
} else {
|
||||
_chat_log_chat(mybarejid, message->from_jid->barejid, message->plain, PROF_IN_LOG, message->timestamp, NULL);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void
|
||||
_chat_log_chat(const char* const login, const char* const other, const char* msg,
|
||||
chat_log_direction_t direction, GDateTime* timestamp, const char* const resourcepart)
|
||||
void
|
||||
chat_log_otr_msg_in(ProfMessage* message)
|
||||
{
|
||||
auto_gchar gchar* pref_dblog = prefs_get_string(PREF_DBLOG);
|
||||
if (g_strcmp0(pref_dblog, "redact") == 0) {
|
||||
msg = "[REDACTED]";
|
||||
}
|
||||
char* other_name;
|
||||
GString* other_str = NULL;
|
||||
}
|
||||
|
||||
if (resourcepart) {
|
||||
other_str = g_string_new(other);
|
||||
g_string_append(other_str, "_");
|
||||
g_string_append(other_str, resourcepart);
|
||||
void
|
||||
chat_log_pgp_msg_in(ProfMessage* message)
|
||||
{
|
||||
}
|
||||
|
||||
other_name = other_str->str;
|
||||
} else {
|
||||
other_name = (char*)other;
|
||||
}
|
||||
|
||||
struct dated_chat_log* dated_log = g_hash_table_lookup(logs, other_name);
|
||||
|
||||
// no log for user
|
||||
if (dated_log == NULL) {
|
||||
dated_log = _create_chatlog(other_name, login);
|
||||
g_hash_table_insert(logs, strdup(other_name), dated_log);
|
||||
|
||||
// log entry exists but file removed
|
||||
} else if (!g_file_test(dated_log->filename, G_FILE_TEST_EXISTS)) {
|
||||
dated_log = _create_chatlog(other_name, login);
|
||||
g_hash_table_replace(logs, strdup(other_name), dated_log);
|
||||
|
||||
// log file needs rolling
|
||||
} else if (_log_roll_needed(dated_log)) {
|
||||
dated_log = _create_chatlog(other_name, login);
|
||||
g_hash_table_replace(logs, strdup(other_name), dated_log);
|
||||
}
|
||||
|
||||
if (resourcepart) {
|
||||
g_string_free(other_str, TRUE);
|
||||
}
|
||||
|
||||
if (timestamp == NULL) {
|
||||
timestamp = g_date_time_new_now_local();
|
||||
} else {
|
||||
g_date_time_ref(timestamp);
|
||||
}
|
||||
|
||||
auto_gchar gchar* date_fmt = g_date_time_format_iso8601(timestamp);
|
||||
FILE* chatlogp = fopen(dated_log->filename, "a");
|
||||
g_chmod(dated_log->filename, S_IRUSR | S_IWUSR);
|
||||
if (chatlogp) {
|
||||
if (direction == PROF_IN_LOG) {
|
||||
if (strncmp(msg, "/me ", 4) == 0) {
|
||||
if (resourcepart) {
|
||||
fprintf(chatlogp, "%s - *%s %s\n", date_fmt, resourcepart, msg + 4);
|
||||
} else {
|
||||
fprintf(chatlogp, "%s - *%s %s\n", date_fmt, other, msg + 4);
|
||||
}
|
||||
} else {
|
||||
if (resourcepart) {
|
||||
fprintf(chatlogp, "%s - %s: %s\n", date_fmt, resourcepart, msg);
|
||||
} else {
|
||||
fprintf(chatlogp, "%s - %s: %s\n", date_fmt, other, msg);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (strncmp(msg, "/me ", 4) == 0) {
|
||||
fprintf(chatlogp, "%s - *me %s\n", date_fmt, msg + 4);
|
||||
} else {
|
||||
fprintf(chatlogp, "%s - me: %s\n", date_fmt, msg);
|
||||
}
|
||||
}
|
||||
fflush(chatlogp);
|
||||
int result = fclose(chatlogp);
|
||||
if (result == EOF) {
|
||||
log_error("Error closing file %s, errno = %d", dated_log->filename, errno);
|
||||
}
|
||||
}
|
||||
|
||||
g_date_time_unref(timestamp);
|
||||
void
|
||||
chat_log_omemo_msg_in(ProfMessage* message)
|
||||
{
|
||||
}
|
||||
|
||||
void
|
||||
groupchat_log_msg_out(const gchar* const room, const gchar* const msg)
|
||||
{
|
||||
if (prefs_get_boolean(PREF_GRLOG)) {
|
||||
const char* const mynick = muc_nick(room);
|
||||
_groupchat_log_chat(connection_get_barejid(), room, mynick, msg);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
groupchat_log_msg_in(const gchar* const room, const gchar* const nick, const gchar* const msg)
|
||||
{
|
||||
if (prefs_get_boolean(PREF_GRLOG)) {
|
||||
_groupchat_log_chat(connection_get_barejid(), room, nick, msg);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
groupchat_log_omemo_msg_out(const gchar* const room, const gchar* const msg)
|
||||
{
|
||||
if (prefs_get_boolean(PREF_CHLOG)) {
|
||||
const char* mybarejid = connection_get_barejid();
|
||||
auto_gchar gchar* pref_omemo_log = prefs_get_string(PREF_OMEMO_LOG);
|
||||
const char* const mynick = muc_nick(room);
|
||||
|
||||
if (strcmp(pref_omemo_log, "on") == 0) {
|
||||
_groupchat_log_chat(mybarejid, room, mynick, msg);
|
||||
} else if (strcmp(pref_omemo_log, "redact") == 0) {
|
||||
_groupchat_log_chat(mybarejid, room, mynick, "[redacted]");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
groupchat_log_omemo_msg_in(const gchar* const room, const gchar* const nick, const gchar* const msg)
|
||||
{
|
||||
if (prefs_get_boolean(PREF_CHLOG)) {
|
||||
const char* mybarejid = connection_get_barejid();
|
||||
auto_gchar gchar* pref_omemo_log = prefs_get_string(PREF_OMEMO_LOG);
|
||||
|
||||
if (strcmp(pref_omemo_log, "on") == 0) {
|
||||
_groupchat_log_chat(mybarejid, room, nick, msg);
|
||||
} else if (strcmp(pref_omemo_log, "redact") == 0) {
|
||||
_groupchat_log_chat(mybarejid, room, nick, "[redacted]");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
_groupchat_log_chat(const gchar* const login, const gchar* const room, const gchar* const nick,
|
||||
const gchar* const msg)
|
||||
{
|
||||
struct dated_chat_log* dated_log = g_hash_table_lookup(groupchat_logs, room);
|
||||
|
||||
// no log for room
|
||||
if (dated_log == NULL) {
|
||||
dated_log = _create_groupchat_log(room, login);
|
||||
g_hash_table_insert(groupchat_logs, strdup(room), dated_log);
|
||||
|
||||
// log exists but needs rolling
|
||||
} else if (_log_roll_needed(dated_log)) {
|
||||
dated_log = _create_groupchat_log(room, login);
|
||||
g_hash_table_replace(logs, strdup(room), dated_log);
|
||||
}
|
||||
|
||||
GDateTime* dt_tmp = g_date_time_new_now_local();
|
||||
|
||||
auto_gchar gchar* date_fmt = g_date_time_format_iso8601(dt_tmp);
|
||||
|
||||
FILE* grpchatlogp = fopen(dated_log->filename, "a");
|
||||
g_chmod(dated_log->filename, S_IRUSR | S_IWUSR);
|
||||
if (grpchatlogp) {
|
||||
if (strncmp(msg, "/me ", 4) == 0) {
|
||||
fprintf(grpchatlogp, "%s - *%s %s\n", date_fmt, nick, msg + 4);
|
||||
} else {
|
||||
fprintf(grpchatlogp, "%s - %s: %s\n", date_fmt, nick, msg);
|
||||
}
|
||||
|
||||
fflush(grpchatlogp);
|
||||
int result = fclose(grpchatlogp);
|
||||
if (result == EOF) {
|
||||
log_error("Error closing file %s, errno = %d", dated_log->filename, errno);
|
||||
}
|
||||
}
|
||||
|
||||
g_date_time_unref(dt_tmp);
|
||||
}
|
||||
|
||||
static char*
|
||||
_get_log_filename(const char* const other, const char* const login, GDateTime* dt, gboolean is_room)
|
||||
{
|
||||
auto_gchar gchar* chatlogs_dir = files_file_in_account_data_path(DIR_CHATLOGS, login, is_room ? "rooms" : NULL);
|
||||
auto_gchar gchar* logfile_name = g_date_time_format(dt, "%Y_%m_%d.log");
|
||||
auto_char char* other_ = str_replace(other, "@", "_at_");
|
||||
auto_gchar gchar* logs_path = g_strdup_printf("%s/%s", chatlogs_dir, other_);
|
||||
gchar* logfile_path = NULL;
|
||||
|
||||
if (create_dir(logs_path)) {
|
||||
logfile_path = g_strdup_printf("%s/%s", logs_path, logfile_name);
|
||||
}
|
||||
|
||||
return logfile_path;
|
||||
}
|
||||
|
||||
static struct dated_chat_log*
|
||||
_create_chatlog(const char* const other, const char* const login)
|
||||
{
|
||||
GDateTime* now = g_date_time_new_now_local();
|
||||
auto_char char* filename = _get_log_filename(other, login, now, FALSE);
|
||||
|
||||
struct dated_chat_log* new_log = malloc(sizeof(struct dated_chat_log));
|
||||
new_log->filename = strdup(filename);
|
||||
new_log->date = now;
|
||||
|
||||
return new_log;
|
||||
}
|
||||
|
||||
static struct dated_chat_log*
|
||||
_create_groupchat_log(const char* const room, const char* const login)
|
||||
{
|
||||
GDateTime* now = g_date_time_new_now_local();
|
||||
auto_char char* filename = _get_log_filename(room, login, now, TRUE);
|
||||
|
||||
struct dated_chat_log* new_log = malloc(sizeof(struct dated_chat_log));
|
||||
new_log->filename = strdup(filename);
|
||||
new_log->date = now;
|
||||
|
||||
return new_log;
|
||||
}
|
||||
|
||||
static gboolean
|
||||
_log_roll_needed(struct dated_chat_log* dated_log)
|
||||
{
|
||||
gboolean result = FALSE;
|
||||
GDateTime* now = g_date_time_new_now_local();
|
||||
if (g_date_time_get_day_of_year(dated_log->date) != g_date_time_get_day_of_year(now)) {
|
||||
result = TRUE;
|
||||
}
|
||||
g_date_time_unref(now);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
static void
|
||||
_free_chat_log(struct dated_chat_log* dated_log)
|
||||
{
|
||||
if (dated_log) {
|
||||
if (dated_log->filename) {
|
||||
g_free(dated_log->filename);
|
||||
dated_log->filename = NULL;
|
||||
}
|
||||
if (dated_log->date) {
|
||||
g_date_time_unref(dated_log->date);
|
||||
dated_log->date = NULL;
|
||||
}
|
||||
free(dated_log);
|
||||
}
|
||||
}
|
||||
|
||||
static gboolean
|
||||
_key_equals(void* key1, void* key2)
|
||||
{
|
||||
gchar* str1 = (gchar*)key1;
|
||||
gchar* str2 = (gchar*)key2;
|
||||
|
||||
return (g_strcmp0(str1, str2) == 0);
|
||||
}
|
||||
|
||||
@@ -4,33 +4,7 @@
|
||||
*
|
||||
* Copyright (C) 2012 - 2019 James Booth <boothj5@gmail.com>
|
||||
*
|
||||
* This file is part of Profanity.
|
||||
*
|
||||
* Profanity is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Profanity is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Profanity. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* In addition, as a special exception, the copyright holders give permission to
|
||||
* link the code of portions of this program with the OpenSSL library under
|
||||
* certain conditions as described in each individual source file, and
|
||||
* distribute linked combinations including the two.
|
||||
*
|
||||
* You must obey the GNU General Public License in all respects for all of the
|
||||
* code used other than OpenSSL. If you modify file(s) with this exception, you
|
||||
* may extend this exception to your version of the file(s), but you are not
|
||||
* obligated to do so. If you do not wish to do so, delete this exception
|
||||
* statement from your version. If you delete this exception statement from all
|
||||
* source files in the program, then also delete it here.
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later WITH OpenSSL-exception
|
||||
*/
|
||||
|
||||
#ifndef CHATLOG_H
|
||||
|
||||
@@ -3,35 +3,9 @@
|
||||
* vim: expandtab:ts=4:sts=4:sw=4
|
||||
*
|
||||
* Copyright (C) 2012 - 2019 James Booth <boothj5@gmail.com>
|
||||
* Copyright (C) 2019 - 2025 Michael Vetter <jubalh@iodoru.org>
|
||||
*
|
||||
* This file is part of Profanity.
|
||||
*
|
||||
* Profanity is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Profanity is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Profanity. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* In addition, as a special exception, the copyright holders give permission to
|
||||
* link the code of portions of this program with the OpenSSL library under
|
||||
* certain conditions as described in each individual source file, and
|
||||
* distribute linked combinations including the two.
|
||||
*
|
||||
* You must obey the GNU General Public License in all respects for all of the
|
||||
* code used other than OpenSSL. If you modify file(s) with this exception, you
|
||||
* may extend this exception to your version of the file(s), but you are not
|
||||
* obligated to do so. If you do not wish to do so, delete this exception
|
||||
* statement from your version. If you delete this exception statement from all
|
||||
* source files in the program, then also delete it here.
|
||||
* Copyright (C) 2019 - 2026 Michael Vetter <jubalh@iodoru.org>
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later WITH OpenSSL-exception
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
@@ -66,9 +40,12 @@
|
||||
#include "omemo/omemo.h"
|
||||
#endif
|
||||
|
||||
#include "ai/ai_client.h"
|
||||
|
||||
static char* _sub_autocomplete(ProfWin* window, const char* const input, gboolean previous);
|
||||
static char* _notify_autocomplete(ProfWin* window, const char* const input, gboolean previous);
|
||||
static char* _theme_autocomplete(ProfWin* window, const char* const input, gboolean previous);
|
||||
static char* _spellcheck_autocomplete(ProfWin* window, const char* const input, gboolean previous);
|
||||
static char* _autoaway_autocomplete(ProfWin* window, const char* const input, gboolean previous);
|
||||
static char* _autoconnect_autocomplete(ProfWin* window, const char* const input, gboolean previous);
|
||||
static char* _account_autocomplete(ProfWin* window, const char* const input, gboolean previous);
|
||||
@@ -134,8 +111,11 @@ static char* _lastactivity_autocomplete(ProfWin* window, const char* const input
|
||||
static char* _intype_autocomplete(ProfWin* window, const char* const input, gboolean previous);
|
||||
static char* _mood_autocomplete(ProfWin* window, const char* const input, gboolean previous);
|
||||
static char* _strophe_autocomplete(ProfWin* window, const char* const input, gboolean previous);
|
||||
static char* _stamp_autocomplete(ProfWin* window, const char* const input, gboolean previous);
|
||||
static char* _adhoc_cmd_autocomplete(ProfWin* window, const char* const input, gboolean previous);
|
||||
static char* _vcard_autocomplete(ProfWin* window, const char* const input, gboolean previous);
|
||||
static char* _force_encryption_autocomplete(ProfWin* window, const char* const input, gboolean previous);
|
||||
static char* _ai_autocomplete(ProfWin* window, const char* const input, gboolean previous);
|
||||
|
||||
static char* _script_autocomplete_func(const char* const prefix, gboolean previous, void* context);
|
||||
|
||||
@@ -163,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;
|
||||
@@ -274,18 +255,27 @@ static Autocomplete logging_ac;
|
||||
static Autocomplete logging_group_ac;
|
||||
static Autocomplete privacy_ac;
|
||||
static Autocomplete privacy_log_ac;
|
||||
static Autocomplete history_ac;
|
||||
static Autocomplete history_switch_ac;
|
||||
static Autocomplete color_ac;
|
||||
static Autocomplete correction_ac;
|
||||
static Autocomplete avatar_ac;
|
||||
static Autocomplete ai_subcommands_ac;
|
||||
static Autocomplete ai_set_subcommands_ac;
|
||||
static Autocomplete ai_set_custom_subcommands_ac;
|
||||
static Autocomplete ai_remove_subcommands_ac;
|
||||
static Autocomplete url_ac;
|
||||
static Autocomplete executable_ac;
|
||||
static Autocomplete executable_param_ac;
|
||||
static Autocomplete intype_ac;
|
||||
static Autocomplete mood_ac;
|
||||
static Autocomplete ai_models_ac;
|
||||
static Autocomplete mood_type_ac;
|
||||
static Autocomplete strophe_ac;
|
||||
static Autocomplete strophe_sm_ac;
|
||||
static Autocomplete strophe_verbosity_ac;
|
||||
static Autocomplete stamp_ac;
|
||||
static Autocomplete stamp_unset_ac;
|
||||
static Autocomplete adhoc_cmd_ac;
|
||||
static Autocomplete lastactivity_ac;
|
||||
static Autocomplete vcard_ac;
|
||||
@@ -296,6 +286,10 @@ static Autocomplete vcard_name_ac;
|
||||
static Autocomplete vcard_set_param_ac;
|
||||
static Autocomplete vcard_togglable_param_ac;
|
||||
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,
|
||||
@@ -319,6 +313,7 @@ static Autocomplete* all_acs[] = {
|
||||
&autoconnect_ac,
|
||||
&wintitle_ac,
|
||||
&theme_ac,
|
||||
&spellcheck_ac,
|
||||
&account_ac,
|
||||
&account_set_ac,
|
||||
&account_clear_ac,
|
||||
@@ -426,6 +421,8 @@ static Autocomplete* all_acs[] = {
|
||||
&logging_group_ac,
|
||||
&privacy_ac,
|
||||
&privacy_log_ac,
|
||||
&history_ac,
|
||||
&history_switch_ac,
|
||||
&color_ac,
|
||||
&correction_ac,
|
||||
&avatar_ac,
|
||||
@@ -438,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,
|
||||
@@ -448,6 +447,13 @@ static Autocomplete* all_acs[] = {
|
||||
&vcard_set_param_ac,
|
||||
&vcard_togglable_param_ac,
|
||||
&vcard_address_type_ac,
|
||||
&force_encryption_ac,
|
||||
&force_encryption_policy_ac,
|
||||
&ai_subcommands_ac,
|
||||
&ai_set_subcommands_ac,
|
||||
&ai_set_custom_subcommands_ac,
|
||||
&ai_remove_subcommands_ac,
|
||||
&ai_models_ac
|
||||
};
|
||||
|
||||
static GHashTable* ac_funcs = NULL;
|
||||
@@ -582,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");
|
||||
|
||||
@@ -853,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");
|
||||
@@ -1110,6 +1122,7 @@ cmd_ac_init(void)
|
||||
autocomplete_add(statusbar_show_ac, "name");
|
||||
autocomplete_add(statusbar_show_ac, "number");
|
||||
autocomplete_add(statusbar_show_ac, "read");
|
||||
autocomplete_add(statusbar_show_ac, "dbbackend");
|
||||
|
||||
autocomplete_add(statusbar_tabmode_ac, "actlist");
|
||||
autocomplete_add(statusbar_tabmode_ac, "dynamic");
|
||||
@@ -1124,15 +1137,24 @@ cmd_ac_init(void)
|
||||
autocomplete_add(status_state_ac, "xa");
|
||||
autocomplete_add(status_state_ac, "dnd");
|
||||
|
||||
autocomplete_add(logging_ac, "chat");
|
||||
autocomplete_add(logging_ac, "group");
|
||||
|
||||
autocomplete_add(privacy_ac, "logging");
|
||||
autocomplete_add(privacy_ac, "os");
|
||||
|
||||
autocomplete_add(privacy_log_ac, "on");
|
||||
autocomplete_add(privacy_log_ac, "off");
|
||||
autocomplete_add(privacy_log_ac, "redact");
|
||||
autocomplete_add(privacy_log_ac, "flatfile");
|
||||
|
||||
autocomplete_add(history_ac, "on");
|
||||
autocomplete_add(history_ac, "off");
|
||||
autocomplete_add(history_ac, "backend");
|
||||
autocomplete_add(history_ac, "switch");
|
||||
autocomplete_add(history_ac, "verify");
|
||||
autocomplete_add(history_ac, "export");
|
||||
autocomplete_add(history_ac, "import");
|
||||
|
||||
autocomplete_add(history_switch_ac, "sqlite");
|
||||
autocomplete_add(history_switch_ac, "flatfile");
|
||||
|
||||
autocomplete_add(logging_group_ac, "on");
|
||||
autocomplete_add(logging_group_ac, "off");
|
||||
@@ -1148,6 +1170,26 @@ cmd_ac_init(void)
|
||||
autocomplete_add(correction_ac, "off");
|
||||
autocomplete_add(correction_ac, "char");
|
||||
|
||||
autocomplete_add_unsorted(ai_subcommands_ac, "set", FALSE);
|
||||
autocomplete_add_unsorted(ai_subcommands_ac, "remove", FALSE);
|
||||
autocomplete_add_unsorted(ai_subcommands_ac, "start", FALSE);
|
||||
autocomplete_add_unsorted(ai_subcommands_ac, "clear", FALSE);
|
||||
autocomplete_add_unsorted(ai_subcommands_ac, "providers", FALSE);
|
||||
autocomplete_add_unsorted(ai_subcommands_ac, "switch", FALSE);
|
||||
autocomplete_add_unsorted(ai_subcommands_ac, "models", FALSE);
|
||||
|
||||
autocomplete_add_unsorted(ai_set_subcommands_ac, "provider", FALSE);
|
||||
autocomplete_add_unsorted(ai_set_subcommands_ac, "token", FALSE);
|
||||
autocomplete_add_unsorted(ai_set_subcommands_ac, "default-model", FALSE);
|
||||
autocomplete_add_unsorted(ai_set_subcommands_ac, "custom", FALSE);
|
||||
|
||||
autocomplete_add_unsorted(ai_set_custom_subcommands_ac, "tools", FALSE);
|
||||
autocomplete_add_unsorted(ai_set_custom_subcommands_ac, "search", FALSE);
|
||||
autocomplete_add_unsorted(ai_set_custom_subcommands_ac, "memory", FALSE);
|
||||
autocomplete_add_unsorted(ai_set_custom_subcommands_ac, "plugins", FALSE);
|
||||
|
||||
autocomplete_add_unsorted(ai_remove_subcommands_ac, "provider", FALSE);
|
||||
|
||||
autocomplete_add(avatar_ac, "set");
|
||||
autocomplete_add(avatar_ac, "disable");
|
||||
autocomplete_add(avatar_ac, "get");
|
||||
@@ -1180,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");
|
||||
@@ -1343,6 +1392,13 @@ cmd_ac_init(void)
|
||||
autocomplete_add(vcard_address_type_ac, "domestic");
|
||||
autocomplete_add(vcard_address_type_ac, "international");
|
||||
|
||||
autocomplete_add(force_encryption_ac, "on");
|
||||
autocomplete_add(force_encryption_ac, "off");
|
||||
autocomplete_add(force_encryption_ac, "policy");
|
||||
|
||||
autocomplete_add(force_encryption_policy_ac, "block");
|
||||
autocomplete_add(force_encryption_policy_ac, "resend-to-confirm");
|
||||
|
||||
if (ac_funcs != NULL)
|
||||
g_hash_table_destroy(ac_funcs);
|
||||
ac_funcs = g_hash_table_new(g_str_hash, g_str_equal);
|
||||
@@ -1402,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);
|
||||
@@ -1415,6 +1472,8 @@ cmd_ac_init(void)
|
||||
g_hash_table_insert(ac_funcs, "/win", _win_autocomplete);
|
||||
g_hash_table_insert(ac_funcs, "/wins", _wins_autocomplete);
|
||||
g_hash_table_insert(ac_funcs, "/wintitle", _wintitle_autocomplete);
|
||||
g_hash_table_insert(ac_funcs, "/force-encryption", _force_encryption_autocomplete);
|
||||
g_hash_table_insert(ac_funcs, "/ai", _ai_autocomplete);
|
||||
}
|
||||
|
||||
void
|
||||
@@ -1544,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, ' '))) {
|
||||
@@ -1633,6 +1696,7 @@ cmd_ac_reset(ProfWin* window)
|
||||
win_reset_search_attempts();
|
||||
win_close_reset_search_attempts();
|
||||
plugins_reset_autocomplete();
|
||||
ai_providers_reset_ac();
|
||||
}
|
||||
|
||||
void
|
||||
@@ -1647,67 +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;
|
||||
|
||||
// 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);
|
||||
auto_gchar gchar* inpcp = g_strdup(inpcp_ptr);
|
||||
if (!inpcp) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// strip quotes
|
||||
if (*inpcp == '"') {
|
||||
tmp = strchr(inpcp + 1, '"');
|
||||
if (tmp) {
|
||||
*tmp = '\0';
|
||||
if (inpcp[0] == '"') {
|
||||
char* last_quote = strrchr(inpcp + 1, '"');
|
||||
if (last_quote) {
|
||||
*last_quote = '\0';
|
||||
}
|
||||
tmp = strdup(inpcp + 1);
|
||||
free(inpcp);
|
||||
inpcp = tmp;
|
||||
gchar* unquoted = g_strdup(inpcp + 1);
|
||||
g_free(inpcp);
|
||||
inpcp = unquoted;
|
||||
}
|
||||
|
||||
// expand ~ to $HOME
|
||||
if (inpcp[0] == '~' && inpcp[1] == '/') {
|
||||
tmp = g_strdup_printf("%s/%sfoo", getenv("HOME"), inpcp + 2);
|
||||
if (!tmp) {
|
||||
free(inpcp);
|
||||
return NULL;
|
||||
}
|
||||
output_off = strlen(getenv("HOME")) + 1;
|
||||
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 {
|
||||
tmp = g_strdup_printf("%sfoo", inpcp);
|
||||
if (!tmp) {
|
||||
free(inpcp);
|
||||
return NULL;
|
||||
}
|
||||
user_dir_part = g_strdup("");
|
||||
}
|
||||
free(inpcp);
|
||||
inpcp = tmp;
|
||||
|
||||
char* inpcp2 = strdup(inpcp);
|
||||
char* foofile = strdup(basename(inpcp2));
|
||||
char* directory = strdup(dirname(inpcp));
|
||||
free(inpcp);
|
||||
free(inpcp2);
|
||||
|
||||
GArray* files = g_array_new(TRUE, FALSE, sizeof(char*));
|
||||
GArray* files = g_array_new(TRUE, FALSE, sizeof(gchar*));
|
||||
g_array_set_clear_func(files, (GDestroyNotify)_filepath_item_free);
|
||||
|
||||
DIR* d = opendir(directory);
|
||||
@@ -1715,61 +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;
|
||||
if (output_off) {
|
||||
tmp = g_strdup_printf("%s/%s", directory, dir->d_name);
|
||||
if (!tmp) {
|
||||
free(directory);
|
||||
free(foofile);
|
||||
return NULL;
|
||||
}
|
||||
acstring = g_strdup_printf("~/%s", tmp + output_off);
|
||||
if (!acstring) {
|
||||
free(directory);
|
||||
free(foofile);
|
||||
return NULL;
|
||||
}
|
||||
free(tmp);
|
||||
} else if (strcmp(directory, "/") == 0) {
|
||||
acstring = g_strdup_printf("/%s", dir->d_name);
|
||||
if (!acstring) {
|
||||
free(directory);
|
||||
free(foofile);
|
||||
return NULL;
|
||||
}
|
||||
} else {
|
||||
acstring = g_strdup_printf("%s/%s", directory, dir->d_name);
|
||||
if (!acstring) {
|
||||
free(directory);
|
||||
free(foofile);
|
||||
return NULL;
|
||||
}
|
||||
// check if it matches prefix
|
||||
if (!find_all && !g_str_has_prefix(dir->d_name, foofile)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
char* acstring_cpy = strdup(acstring);
|
||||
g_array_append_val(files, acstring_cpy);
|
||||
free(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)
|
||||
{
|
||||
@@ -1779,16 +1837,39 @@ _cmd_ac_complete_params(ProfWin* window, const char* const input, gboolean previ
|
||||
|
||||
// autocomplete boolean settings
|
||||
gchar* boolean_choices[] = { "/beep", "/states", "/outtype", "/flash", "/splash",
|
||||
"/history", "/vercheck", "/privileges", "/wrap",
|
||||
"/vercheck", "/privileges", "/wrap",
|
||||
"/carbons", "/slashguard", "/mam", "/silence" };
|
||||
|
||||
for (int i = 0; i < ARRAY_SIZE(boolean_choices); i++) {
|
||||
for (size_t i = 0; i < ARRAY_SIZE(boolean_choices); i++) {
|
||||
result = autocomplete_param_with_func(input, boolean_choices[i], prefs_autocomplete_boolean_choice, previous, NULL);
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
result = autocomplete_param_with_ac(input, "/history switch", history_switch_ac, TRUE, previous);
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
|
||||
gchar* history_jid_subcmds[] = { "/history verify", "/history export", "/history import" };
|
||||
for (size_t i = 0; i < ARRAY_SIZE(history_jid_subcmds); i++) {
|
||||
result = autocomplete_param_with_func(input, history_jid_subcmds[i], roster_barejid_autocomplete, previous, NULL);
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
result = autocomplete_param_with_ac(input, "/history", history_ac, TRUE, previous);
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
|
||||
result = _spellcheck_autocomplete(window, input, previous);
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
|
||||
// autocomplete nickname in chat rooms
|
||||
if (window->type == WIN_MUC) {
|
||||
ProfMucWin* mucwin = (ProfMucWin*)window;
|
||||
@@ -1799,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);
|
||||
@@ -1814,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);
|
||||
@@ -1829,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;
|
||||
@@ -1838,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;
|
||||
@@ -1858,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;
|
||||
@@ -1878,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) {
|
||||
@@ -1947,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;
|
||||
@@ -2269,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;
|
||||
@@ -2302,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;
|
||||
@@ -3748,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;
|
||||
@@ -4013,11 +4095,6 @@ _logging_autocomplete(ProfWin* window, const char* const input, gboolean previou
|
||||
return result;
|
||||
}
|
||||
|
||||
result = autocomplete_param_with_func(input, "/logging chat", prefs_autocomplete_boolean_choice, previous, NULL);
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
|
||||
result = autocomplete_param_with_ac(input, "/logging group", logging_group_ac, TRUE, previous);
|
||||
return result;
|
||||
}
|
||||
@@ -4265,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)
|
||||
{
|
||||
@@ -4289,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;
|
||||
@@ -4406,3 +4497,176 @@ _vcard_autocomplete(ProfWin* window, const char* const input, gboolean previous)
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
static char*
|
||||
_force_encryption_autocomplete(ProfWin* window, const char* const input, gboolean previous)
|
||||
{
|
||||
char* result = NULL;
|
||||
|
||||
result = autocomplete_param_with_ac(input, "/force-encryption policy", force_encryption_policy_ac, TRUE, previous);
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
|
||||
result = autocomplete_param_with_ac(input, "/force-encryption", force_encryption_ac, TRUE, previous);
|
||||
return result;
|
||||
}
|
||||
|
||||
/* Forward declaration */
|
||||
static char* _ai_provider_and_model_autocomplete(ProfWin* window, const char* const input,
|
||||
gboolean previous, const char* cmd);
|
||||
|
||||
static char*
|
||||
_ai_autocomplete(ProfWin* window, const char* const input, gboolean previous)
|
||||
{
|
||||
char* result = NULL;
|
||||
|
||||
/* Parse once for reuse - /ai <subcommand> [<arg1>] [<arg2>] */
|
||||
gboolean parse_result = FALSE;
|
||||
auto_gcharv gchar** args = parse_args(input, 1, 4, &parse_result);
|
||||
int num_args = g_strv_length(args);
|
||||
|
||||
/* Top-level /ai <subcommand> autocomplete (e.g., /ai s<tab> -> /ai set) */
|
||||
result = autocomplete_param_with_func(input, "/ai set provider", ai_providers_find, previous, NULL);
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
|
||||
// /ai set token <provider> <token> - autocomplete provider names
|
||||
result = autocomplete_param_with_func(input, "/ai set token", ai_providers_find, previous, NULL);
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
|
||||
// /ai set default-model <provider> <model> - autocomplete provider names
|
||||
result = autocomplete_param_with_func(input, "/ai set default-model", ai_providers_find, previous, NULL);
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
|
||||
// /ai set custom <provider> <setting> <value> - autocomplete provider names
|
||||
result = autocomplete_param_with_func(input, "/ai set custom", ai_providers_find, previous, NULL);
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
|
||||
// /ai set custom <provider> <setting> - autocomplete settings
|
||||
/* args[0]="set", args[1]="custom", args[2]=provider (if typed), args[3]=setting (if typed) */
|
||||
if (num_args == 3 && g_strcmp0(args[1], "custom") == 0) {
|
||||
/* /ai set custom <provider> - check if provider is valid */
|
||||
if (ai_get_provider(args[2])) {
|
||||
/* Valid provider, try settings autocomplete */
|
||||
result = autocomplete_param_with_ac(input, "/ai set custom ", ai_set_custom_subcommands_ac, TRUE, previous);
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
// /ai set <subcommand> - autocomplete subcommands
|
||||
result = autocomplete_param_with_ac(input, "/ai set", ai_set_subcommands_ac, TRUE, previous);
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
|
||||
// /ai remove provider <name> - autocomplete provider names
|
||||
result = autocomplete_param_with_func(input, "/ai remove provider", ai_providers_find, previous, NULL);
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
|
||||
result = autocomplete_param_with_ac(input, "/ai remove", ai_remove_subcommands_ac, TRUE, previous);
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
|
||||
// /ai start <provider> <model> - autocomplete provider names and model names
|
||||
result = _ai_provider_and_model_autocomplete(window, input, previous, "/ai start");
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
|
||||
// /ai switch <provider> <model> - autocomplete provider names and model names
|
||||
result = _ai_provider_and_model_autocomplete(window, input, previous, "/ai switch");
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
|
||||
// /ai set default-model <provider> <model> - autocomplete provider names and model names
|
||||
result = _ai_provider_and_model_autocomplete(window, input, previous, "/ai set default-model");
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
|
||||
// /ai models <provider> - autocomplete provider names
|
||||
result = autocomplete_param_with_func(input, "/ai models", ai_providers_find, previous, NULL);
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
|
||||
// /ai clear - no autocomplete
|
||||
// /ai providers - no autocomplete
|
||||
|
||||
result = autocomplete_param_with_ac(input, "/ai", ai_subcommands_ac, FALSE, previous);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Autocomplete provider names and model names for /ai <cmd> <provider> <model> patterns.
|
||||
* First tries to autocomplete provider names, then model names if provider is valid.
|
||||
* Uses static ai_models_ac to preserve cycling state (last_found) across calls.
|
||||
* The cmd_prefix should NOT include trailing space (e.g., "/ai start" not "/ai start ").
|
||||
*/
|
||||
static char*
|
||||
_ai_provider_and_model_autocomplete(ProfWin* window, const char* const input, gboolean previous, const char* cmd)
|
||||
{
|
||||
char* result;
|
||||
|
||||
/* First, try provider name autocomplete */
|
||||
result = autocomplete_param_with_func(input, cmd, ai_providers_find, previous, NULL);
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
|
||||
/* Provider was specified, try model name autocomplete */
|
||||
/* Build the full command prefix with space */
|
||||
auto_gchar gchar* cmd_prefix = g_strdup_printf("%s ", cmd);
|
||||
|
||||
if (!g_str_has_prefix(input, cmd_prefix)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* Extract provider name from input (after cmd_prefix) */
|
||||
auto_gchar gchar* rest = g_strdup(input + strlen(cmd_prefix));
|
||||
char* space = strchr(rest, ' ');
|
||||
if (space) {
|
||||
*space = '\0';
|
||||
}
|
||||
|
||||
/* Look up the provider by name and get its models */
|
||||
AIProvider* prov = ai_get_provider(rest);
|
||||
if (!prov || !prov->models) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (!ai_models_ac) {
|
||||
ai_models_ac = autocomplete_new();
|
||||
}
|
||||
/* Convert GList* to char** for autocomplete_update */
|
||||
int model_count = g_list_length(prov->models);
|
||||
char** model_array = g_new0(char*, model_count + 1);
|
||||
GList* curr = prov->models;
|
||||
int i = 0;
|
||||
while (curr) {
|
||||
model_array[i++] = curr->data;
|
||||
curr = g_list_next(curr);
|
||||
}
|
||||
|
||||
autocomplete_update(ai_models_ac, model_array);
|
||||
g_free(model_array);
|
||||
|
||||
auto_gchar gchar* full_prefix = g_strdup_printf("%s%s", cmd_prefix, rest);
|
||||
|
||||
result = autocomplete_param_with_ac(input, full_prefix, ai_models_ac, TRUE, previous);
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -4,33 +4,7 @@
|
||||
*
|
||||
* Copyright (C) 2012 - 2019 James Booth <boothj5@gmail.com>
|
||||
*
|
||||
* This file is part of Profanity.
|
||||
*
|
||||
* Profanity is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Profanity is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Profanity. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* In addition, as a special exception, the copyright holders give permission to
|
||||
* link the code of portions of this program with the OpenSSL library under
|
||||
* certain conditions as described in each individual source file, and
|
||||
* distribute linked combinations including the two.
|
||||
*
|
||||
* You must obey the GNU General Public License in all respects for all of the
|
||||
* code used other than OpenSSL. If you modify file(s) with this exception, you
|
||||
* may extend this exception to your version of the file(s), but you are not
|
||||
* obligated to do so. If you do not wish to do so, delete this exception
|
||||
* statement from your version. If you delete this exception statement from all
|
||||
* source files in the program, then also delete it here.
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later WITH OpenSSL-exception
|
||||
*/
|
||||
|
||||
#ifndef COMMAND_CMD_AC_H
|
||||
|
||||
@@ -3,35 +3,9 @@
|
||||
* vim: expandtab:ts=4:sts=4:sw=4
|
||||
*
|
||||
* Copyright (C) 2012 - 2019 James Booth <boothj5@gmail.com>
|
||||
* Copyright (C) 2019 - 2025 Michael Vetter <jubalh@iodoru.org>
|
||||
*
|
||||
* This file is part of Profanity.
|
||||
*
|
||||
* Profanity is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Profanity is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Profanity. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* In addition, as a special exception, the copyright holders give permission to
|
||||
* link the code of portions of this program with the OpenSSL library under
|
||||
* certain conditions as described in each individual source file, and
|
||||
* distribute linked combinations including the two.
|
||||
*
|
||||
* You must obey the GNU General Public License in all respects for all of the
|
||||
* code used other than OpenSSL. If you modify file(s) with this exception, you
|
||||
* may extend this exception to your version of the file(s), but you are not
|
||||
* obligated to do so. If you do not wish to do so, delete this exception
|
||||
* statement from your version. If you delete this exception statement from all
|
||||
* source files in the program, then also delete it here.
|
||||
* Copyright (C) 2019 - 2026 Michael Vetter <jubalh@iodoru.org>
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later WITH OpenSSL-exception
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
@@ -105,6 +79,9 @@ static gboolean _cmd_has_tag(Command* pcmd, const char* const tag);
|
||||
* Command list
|
||||
*/
|
||||
|
||||
#define CMD_TLS_DIRECT "Use direct TLS for the connection. It means TLS handshake is started right after TCP connection is established."
|
||||
#define CMD_TLS_LEGACY "Alternative keyword for 'direct', which was created when one still thought that 'STARTTLS' is the future."
|
||||
|
||||
// clang-format off
|
||||
static const struct cmd_t command_defs[] = {
|
||||
{ CMD_PREAMBLE("/help",
|
||||
@@ -139,14 +116,15 @@ static const struct cmd_t command_defs[] = {
|
||||
"Show version and license information.")
|
||||
},
|
||||
|
||||
// Max args: account + server <s> + port <p> + tls <t> + auth <a> = 9
|
||||
{ CMD_PREAMBLE("/connect",
|
||||
parse_args, 0, 7, NULL)
|
||||
parse_args, 0, 9, NULL)
|
||||
CMD_MAINFUNC(cmd_connect)
|
||||
CMD_TAGS(
|
||||
CMD_TAG_CONNECTION)
|
||||
CMD_SYN(
|
||||
"/connect [<account>]",
|
||||
"/connect <account> [server <server>] [port <port>] [tls force|allow|trust|legacy|disable] [auth default|legacy]",
|
||||
"/connect <account> [server <server>] [port <port>] [tls force|allow|trust|direct|disable|legacy] [auth default|legacy]",
|
||||
"/connect <server>")
|
||||
CMD_DESC(
|
||||
"Login to a chat service. "
|
||||
@@ -161,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(
|
||||
@@ -403,15 +382,17 @@ static const struct cmd_t command_defs[] = {
|
||||
CMD_DESC(
|
||||
"Manage blocked users (XEP-0191), calling with no arguments shows the current list of blocked users. "
|
||||
"To blog a certain user in a MUC use the following as jid: room@conference.example.org/spammy-user"
|
||||
"It is also possible to block and report (XEP-0377) a user with the report-abuse and report-spam commands.")
|
||||
"It is also possible to block and report (XEP-0377) a user with the report-abuse and report-spam commands. "
|
||||
"For spam reporting, please include the content of the spam message as evidence for the service operator.")
|
||||
CMD_ARGS(
|
||||
{ "add [<jid>]", "Block the specified Jabber ID. If in a chat window and no jid is specified, the current recipient will be blocked." },
|
||||
{ "remove <jid>", "Remove the specified Jabber ID from the blocked list." },
|
||||
{ "report-abuse <jid> [<message>]", "Report the jid as abuse with an optional message to the service operator." },
|
||||
{ "report-spam <jid> [<message>]", "Report the jid as spam with an optional message to the service operator." })
|
||||
{ "report-abuse <jid> [<message>]", "Report a user for abuse with an optional description of the behavior." },
|
||||
{ "report-spam <jid> [<message>]", "Report a user for spam, including the actual spam message as evidence." })
|
||||
CMD_EXAMPLES(
|
||||
"/blocked add hel@helheim.edda",
|
||||
"/blocked report-spam hel@helheim.edda Very annoying guy",
|
||||
"/blocked report-abuse hel@helheim.edda Harassing me in MUC",
|
||||
"/blocked report-spam spambot@example.com \"You won a free prize!\"",
|
||||
"/blocked add profanity@rooms.dismail.de/spammy-user")
|
||||
},
|
||||
|
||||
@@ -433,7 +414,7 @@ static const struct cmd_t command_defs[] = {
|
||||
{ "<contact>", "The contact you wish to view information about." },
|
||||
{ "<nick>", "When in a chat room, the occupant you wish to view information about." })
|
||||
CMD_EXAMPLES(
|
||||
"/info thor@aasgard.server.org",
|
||||
"/info thor@asgard.server.org",
|
||||
"/info heimdall")
|
||||
},
|
||||
|
||||
@@ -1288,8 +1269,8 @@ static const struct cmd_t command_defs[] = {
|
||||
CMD_TAGS(
|
||||
CMD_TAG_UI)
|
||||
CMD_SYN(
|
||||
"/statusbar show name|number|read",
|
||||
"/statusbar hide name|number|read",
|
||||
"/statusbar show name|number|read|dbbackend",
|
||||
"/statusbar hide name|number|read|dbbackend",
|
||||
"/statusbar maxtabs <value>",
|
||||
"/statusbar tablen <value>",
|
||||
"/statusbar tabmode default|dynamic|actlist",
|
||||
@@ -1307,6 +1288,7 @@ static const struct cmd_t command_defs[] = {
|
||||
{ "show|hide name", "Show or hide names in tabs." },
|
||||
{ "show|hide number", "Show or hide numbers in tabs." },
|
||||
{ "show|hide read", "Show or hide inactive tabs." },
|
||||
{ "show|hide dbbackend", "Show or hide the database backend indicator [sqlite] or [flatfile]." },
|
||||
{ "self user|barejid|fulljid", "Show account user name, barejid, fulljid as status bar title." },
|
||||
{ "self off", "Disable showing self as status bar title." },
|
||||
{ "chat user|jid", "Show users name, or fulljid. Change needs a redraw/restart to take effect." },
|
||||
@@ -1471,6 +1453,23 @@ static const struct cmd_t command_defs[] = {
|
||||
{ "on|off", "Enable or disable splash logo." })
|
||||
},
|
||||
|
||||
{ CMD_PREAMBLE("/spellcheck",
|
||||
parse_args, 1, 2, &cons_spellcheck_setting)
|
||||
CMD_MAINFUNC(cmd_spellcheck)
|
||||
CMD_TAGS(
|
||||
CMD_TAG_UI)
|
||||
CMD_SYN(
|
||||
"/spellcheck on|off",
|
||||
"/spellcheck list",
|
||||
"/spellcheck lang <locale>")
|
||||
CMD_DESC(
|
||||
"Enable or disable spellchecking, or set the language.")
|
||||
CMD_ARGS(
|
||||
{ "on|off", "Enable or disable spellchecking." },
|
||||
{ "list", "List available dictionaries recognized by Enchant." },
|
||||
{ "lang <locale>", "Set the spellcheck language (en_US)." })
|
||||
},
|
||||
|
||||
{ CMD_PREAMBLE("/autoconnect",
|
||||
parse_args, 1, 2, &cons_autoconnect_setting)
|
||||
CMD_MAINFUNC(cmd_autoconnect)
|
||||
@@ -1658,23 +1657,14 @@ static const struct cmd_t command_defs[] = {
|
||||
},
|
||||
|
||||
{ CMD_PREAMBLE("/logging",
|
||||
parse_args, 2, 3, &cons_logging_setting)
|
||||
parse_args, 0, 0, NULL)
|
||||
CMD_MAINFUNC(cmd_logging)
|
||||
CMD_TAGS(
|
||||
CMD_TAG_CHAT)
|
||||
CMD_SYN(
|
||||
"/logging chat|group on|off")
|
||||
"/logging")
|
||||
CMD_DESC(
|
||||
"Configure chat logging. "
|
||||
"Switch logging on or off. "
|
||||
"Chat logging will be enabled if /history is set to on. "
|
||||
"When disabling this option, /history will also be disabled. ")
|
||||
CMD_ARGS(
|
||||
{ "chat on|off", "Enable/Disable regular chat logging." },
|
||||
{ "group on|off", "Enable/Disable groupchat (room) logging." })
|
||||
CMD_EXAMPLES(
|
||||
"/logging chat on",
|
||||
"/logging group off")
|
||||
"Deprecated. Use '/history' instead.")
|
||||
},
|
||||
|
||||
{ CMD_PREAMBLE("/states",
|
||||
@@ -1874,18 +1864,34 @@ static const struct cmd_t command_defs[] = {
|
||||
},
|
||||
|
||||
{ CMD_PREAMBLE("/history",
|
||||
parse_args, 1, 1, &cons_history_setting)
|
||||
parse_args, 1, 2, &cons_history_setting)
|
||||
CMD_MAINFUNC(cmd_history)
|
||||
CMD_TAGS(
|
||||
CMD_TAG_UI,
|
||||
CMD_TAG_CHAT)
|
||||
CMD_SYN(
|
||||
"/history on|off")
|
||||
"/history on|off",
|
||||
"/history backend",
|
||||
"/history switch sqlite|flatfile",
|
||||
"/history verify [<jid>]",
|
||||
"/history export [<jid>]",
|
||||
"/history import [<jid>]")
|
||||
CMD_DESC(
|
||||
"Switch chat history on or off, /logging chat will automatically be enabled when this setting is on. "
|
||||
"When history is enabled, previous messages are shown in chat windows.")
|
||||
"Enable or disable chat history. "
|
||||
"When on, messages are persisted via the active database backend and shown in chat windows. "
|
||||
"When off, persistence stops and old history is hidden on chat-window open. "
|
||||
"Use 'backend' to show the active database backend. "
|
||||
"Use 'switch' to change the active database backend at runtime without reconnecting. "
|
||||
"Use 'verify' to check integrity of stored message history. "
|
||||
"Use 'export' to copy messages from SQLite to flat-file format, or 'import' to copy from flat-file to SQLite. "
|
||||
"Both export and import merge with existing data (duplicates are skipped).")
|
||||
CMD_ARGS(
|
||||
{ "on|off", "Enable or disable showing chat history." })
|
||||
{ "on|off", "Enable or disable persistence and showing chat history." },
|
||||
{ "backend", "Show the name of the active database backend." },
|
||||
{ "switch sqlite|flatfile", "Switch the active database backend at runtime." },
|
||||
{ "verify [<jid>]", "Verify integrity of message history. Optionally specify a JID to check only one contact." },
|
||||
{ "export [<jid>]", "Export SQLite history to flat-file format. Optionally specify a JID to export only one contact." },
|
||||
{ "import [<jid>]", "Import flat-file history into SQLite. Optionally specify a JID to import only one contact." })
|
||||
},
|
||||
|
||||
{ CMD_PREAMBLE("/log",
|
||||
@@ -1902,9 +1908,9 @@ static const struct cmd_t command_defs[] = {
|
||||
CMD_ARGS(
|
||||
{ "where", "Show the current log file location." },
|
||||
{ "rotate on|off", "Rotate log, default on. Does not take effect if you specified a filename yourself when starting Profanity." },
|
||||
{ "maxsize <bytes>", "With rotate enabled, specifies the max log size, defaults to 10485760 (10MB)." },
|
||||
{ "maxsize <bytes>", "With rotate enabled, specifies the max log size, defaults to 10485760 (10MiB)." },
|
||||
{ "shared on|off", "Share logs between all instances, default: on. When off, the process id will be included in the log filename. Does not take effect if you specified a filename yourself when starting Profanity." },
|
||||
{"level INFO|DEBUG|WARN|ERROR", "Set the log level. Default is INFO. Only works with default log file, not with user provided log file during startup via -f." })
|
||||
{ "level INFO|DEBUG|WARN|ERROR", "Set the log level. Default is INFO." })
|
||||
},
|
||||
|
||||
{ CMD_PREAMBLE("/carbons",
|
||||
@@ -2070,7 +2076,7 @@ static const struct cmd_t command_defs[] = {
|
||||
"/account set <account> pgpkeyid <pgpkeyid>",
|
||||
"/account set <account> startscript <script>",
|
||||
"/account set <account> clientid \"<name> <version>\"",
|
||||
"/account set <account> tls force|allow|trust|legacy|disable",
|
||||
"/account set <account> tls force|allow|trust|direct|disable|legacy",
|
||||
"/account set <account> auth default|legacy",
|
||||
"/account set <account> theme <theme>",
|
||||
"/account set <account> session_alarm <max_sessions>",
|
||||
@@ -2116,8 +2122,9 @@ static const struct cmd_t command_defs[] = {
|
||||
{ "set <account> tls force", "Force TLS connection, and fail if one cannot be established, this is default behaviour." },
|
||||
{ "set <account> tls allow", "Use TLS for the connection if it is available." },
|
||||
{ "set <account> tls trust", "Force TLS connection and trust server's certificate." },
|
||||
{ "set <account> tls legacy", "Use legacy TLS for the connection. It means server doesn't support STARTTLS and TLS is forced just after TCP connection is established." },
|
||||
{ "set <account> tls direct", CMD_TLS_DIRECT },
|
||||
{ "set <account> tls disable", "Disable TLS for the connection." },
|
||||
{ "set <account> tls legacy", CMD_TLS_LEGACY },
|
||||
{ "set <account> auth default", "Use default authentication process." },
|
||||
{ "set <account> auth legacy", "Allow legacy authentication." },
|
||||
{ "set <account> theme <theme>", "Set the UI theme for the account." },
|
||||
@@ -2144,6 +2151,7 @@ static const struct cmd_t command_defs[] = {
|
||||
"/account set me status dnd",
|
||||
"/account set me dnd -1",
|
||||
"/account set me clientid \"Profanity 0.42 (Dev)\"",
|
||||
"/account set me eval_password \"pass \\\"Test Accounts/my user\\\"\"",
|
||||
"/account rename me chattyme",
|
||||
"/account clear me pgpkeyid")
|
||||
},
|
||||
@@ -2327,13 +2335,18 @@ static const struct cmd_t command_defs[] = {
|
||||
"/omemo clear_device_list",
|
||||
"/omemo qrcode")
|
||||
CMD_DESC(
|
||||
"OMEMO commands to manage keys, and perform encryption during chat sessions.")
|
||||
"OMEMO commands to manage keys, and perform encryption during chat sessions.\n"
|
||||
"The title bar will show the OMEMO session status:\n"
|
||||
"[OMEMO][trusted] - All active devices for the contact are trusted.\n"
|
||||
"[OMEMO][untrusted] - One or more active devices for the contact are untrusted.\n")
|
||||
CMD_ARGS(
|
||||
{ "gen", "Generate OMEMO cryptographic materials for current account." },
|
||||
{ "start [<contact>]", "Start an OMEMO session with contact, or current recipient if omitted." },
|
||||
{ "end", "End the current OMEMO session." },
|
||||
{ "log on|off", "Enable or disable plaintext logging of OMEMO encrypted messages." },
|
||||
{ "log redact", "Log OMEMO encrypted messages, but replace the contents with [redacted]." },
|
||||
{ "trust [<contact>] <fp>", "Trust a fingerprint for a contact, or current recipient if omitted. If all active devices are trusted, the title bar will show [trusted]. Otherwise, it will show [untrusted]." },
|
||||
{ "untrust [<contact>] <fp>","Untrust a fingerprint for a contact, or current recipient if omitted." },
|
||||
{ "fingerprint [<contact>]", "Show contact's fingerprints, or current recipient's if omitted." },
|
||||
{ "char <char>", "Set the character to be displayed next to OMEMO encrypted messages." },
|
||||
{ "trustmode manual", "Set the global OMEMO trust mode to manual, OMEMO keys has to be trusted manually." },
|
||||
@@ -2352,6 +2365,15 @@ static const struct cmd_t command_defs[] = {
|
||||
"/omemo char *")
|
||||
},
|
||||
|
||||
{ CMD_PREAMBLE("/changes",
|
||||
parse_args, 0, 0, NULL)
|
||||
CMD_MAINFUNC(cmd_changes)
|
||||
CMD_SYN(
|
||||
"/changes")
|
||||
CMD_DESC(
|
||||
"Show changes from saved configuration file.")
|
||||
},
|
||||
|
||||
{ CMD_PREAMBLE("/save",
|
||||
parse_args, 0, 0, NULL)
|
||||
CMD_MAINFUNC(cmd_save)
|
||||
@@ -2410,7 +2432,7 @@ static const struct cmd_t command_defs[] = {
|
||||
"/stamp unset outgoing|incoming")
|
||||
CMD_DESC("Set chat window stamp. "
|
||||
"The format of line in the chat window is: \"<timestamp> <encryption sign> <stamp> <message>\" "
|
||||
"where <stamp> is \"me:\" for incoming messages or \"username@server/resource\" for outgoing messages. "
|
||||
"where <stamp> is \"me:\" for outgoing messages or \"username@server/resource\" for incoming messages. "
|
||||
"This command allows to change <stamp> value.")
|
||||
CMD_ARGS({ "outgoing", "Set outgoing stamp" },
|
||||
{ "incoming", "Set incoming stamp"},
|
||||
@@ -2616,7 +2638,8 @@ static const struct cmd_t command_defs[] = {
|
||||
CMD_DESC(
|
||||
"Spawn external editor to edit message. "
|
||||
"After editing the inputline may appear empty. Press enter to send the text anyways. "
|
||||
"Use /executable to set your favourite editor." )
|
||||
"Use /executable to set your favourite editor. "
|
||||
"Ctrl-Z in the editor aborts the session and returns to Profanity." )
|
||||
},
|
||||
|
||||
{ CMD_PREAMBLE("/correct-editor",
|
||||
@@ -2650,7 +2673,7 @@ static const struct cmd_t command_defs[] = {
|
||||
CMD_TAGS(
|
||||
CMD_TAG_CONNECTION)
|
||||
CMD_SYN(
|
||||
"/register <username> <server> [port <port>] [tls force|allow|trust|legacy|disable]")
|
||||
"/register <username> <server> [port <port>] [tls force|allow|trust|direct|disable|legacy]")
|
||||
CMD_DESC(
|
||||
"Register an account on a server.")
|
||||
CMD_ARGS(
|
||||
@@ -2660,8 +2683,9 @@ static const struct cmd_t command_defs[] = {
|
||||
{ "tls force", "Force TLS connection, and fail if one cannot be established. This is the default behavior." },
|
||||
{ "tls allow", "Use TLS for the connection if it is available." },
|
||||
{ "tls trust", "Force TLS connection and trust the server's certificate." },
|
||||
{ "tls legacy", "Use legacy TLS for the connection. This forces TLS just after the TCP connection is established. Use when a server doesn't support STARTTLS." },
|
||||
{ "tls disable", "Disable TLS for the connection." })
|
||||
{ "tls direct", CMD_TLS_DIRECT },
|
||||
{ "tls disable", "Disable TLS for the connection." },
|
||||
{ "tls legacy", CMD_TLS_LEGACY })
|
||||
CMD_EXAMPLES(
|
||||
"/register odin valhalla.edda ",
|
||||
"/register freyr vanaheimr.edda port 5678",
|
||||
@@ -2717,7 +2741,7 @@ static const struct cmd_t command_defs[] = {
|
||||
CMD_TAG_CHAT,
|
||||
CMD_TAG_DISCOVERY)
|
||||
CMD_SYN(
|
||||
"/privacy logging on|redact|off",
|
||||
"/privacy logging on|redact|off|flatfile",
|
||||
"/privacy os on|off")
|
||||
CMD_DESC(
|
||||
"Configure privacy settings. "
|
||||
@@ -2725,11 +2749,13 @@ static const struct cmd_t command_defs[] = {
|
||||
"clientid to set the client identification name "
|
||||
"session_alarm to configure an alarm when more clients log in.")
|
||||
CMD_ARGS(
|
||||
{ "logging on|redact|off", "Switch chat logging. This will also disable logging in the internally used SQL database. Your messages will not be saved anywhere locally. This might have unintended consequences, such as not being able to decrypt OMEMO encrypted messages received later via MAM, and should be used with caution." },
|
||||
{ "logging on|redact|off|flatfile", "Switch chat logging backend. 'on' uses the SQLite database (default). 'flatfile' stores messages as plain-text files under ~/.local/share/profanity/flatlog/ that can be edited with any text editor. 'off' disables logging entirely. 'redact' keeps logging but replaces every message body with '[REDACTED]'. Note: 'off' may have side effects such as not being able to decrypt OMEMO messages received later via MAM. Backend changes ('on' / 'flatfile') take effect immediately when connected, otherwise on next connect." },
|
||||
{ "os on|off", "Choose whether to include the OS name if a user asks for software information (XEP-0092)." }
|
||||
)
|
||||
CMD_EXAMPLES(
|
||||
"/privacy",
|
||||
"/privacy logging off",
|
||||
"/privacy logging flatfile",
|
||||
"/privacy os off")
|
||||
},
|
||||
|
||||
@@ -2744,6 +2770,92 @@ static const struct cmd_t command_defs[] = {
|
||||
"Redraw user interface. Can be used when some other program interrupted profanity or wrote to the same terminal and the interface looks \"broken\"." )
|
||||
},
|
||||
|
||||
{ CMD_PREAMBLE("/force-encryption",
|
||||
parse_args, 1, 3, &cons_encryption_setting)
|
||||
CMD_MAINFUNC(cmd_force_encryption)
|
||||
CMD_TAGS(
|
||||
CMD_TAG_CHAT)
|
||||
CMD_SYN(
|
||||
"/force-encryption",
|
||||
"/force-encryption on|off",
|
||||
"/force-encryption policy resend-to-confirm|block")
|
||||
CMD_DESC(
|
||||
"Configure forced encryption for outgoing messages in CProof. "
|
||||
"When enabled, restricts sending unencrypted messages based on the specified policy. "
|
||||
"Run without arguments to display current settings.")
|
||||
CMD_ARGS(
|
||||
{ "on|off", "Enable or disable forced encryption. When enabled, restricts unencrypted messages per the policy. When disabled, allows sending unencrypted messages without restrictions." },
|
||||
{ "policy resend-to-confirm|block", "Set the policy for unencrypted messages when forced encryption is enabled. 'resend-to-confirm' requires pressing Enter again to confirm sending an unencrypted message (default). 'block' prevents sending unencrypted messages entirely." }
|
||||
)
|
||||
CMD_EXAMPLES(
|
||||
"/force-encryption",
|
||||
"/force-encryption on",
|
||||
"/force-encryption off",
|
||||
"/force-encryption policy resend-to-confirm",
|
||||
"/force-encryption policy block")
|
||||
},
|
||||
|
||||
{ CMD_PREAMBLE("/ai",
|
||||
parse_args, 0, 5, NULL)
|
||||
CMD_SUBFUNCS(
|
||||
{ "set", cmd_ai_set },
|
||||
{ "remove", cmd_ai_remove },
|
||||
{ "start", cmd_ai_start },
|
||||
{ "clear", cmd_ai_clear },
|
||||
{ "providers", cmd_ai_providers },
|
||||
{ "switch", cmd_ai_switch },
|
||||
{ "models", cmd_ai_models })
|
||||
CMD_MAINFUNC(cmd_ai)
|
||||
CMD_TAGS(
|
||||
CMD_TAG_CHAT)
|
||||
CMD_SYN(
|
||||
"/ai",
|
||||
"/ai set provider <name> <url>",
|
||||
"/ai set token <provider> <token>",
|
||||
"/ai set default-model <provider> <model>",
|
||||
"/ai set custom <provider> <setting> <value>",
|
||||
"/ai remove provider <name>",
|
||||
"/ai providers",
|
||||
"/ai start [<provider>] [<model>]",
|
||||
"/ai switch <provider> [<model>]",
|
||||
"/ai switch <model>",
|
||||
"/ai models <provider>",
|
||||
"/ai clear")
|
||||
CMD_DESC(
|
||||
"Interact with AI models via OpenAI-compatible APIs. "
|
||||
"Supports multiple providers (openai, perplexity, custom). "
|
||||
"Each provider has its own API key, endpoint, default model, and settings. "
|
||||
"Chat history is maintained per session and not persisted locally.")
|
||||
CMD_ARGS(
|
||||
{ "", "Display current AI settings and configured providers" },
|
||||
{ "set provider <name> <url>", "Add or update a provider with custom API endpoint" },
|
||||
{ "set token <provider> <token>", "Set API token for a provider (e.g., openai, perplexity)" },
|
||||
{ "set default-model <provider> <model>", "Set default model for a provider" },
|
||||
{ "set custom <provider> <setting> <value>", "Set provider-level setting (e.g., tools, search)" },
|
||||
{ "remove provider <name>", "Remove a custom provider" },
|
||||
{ "providers", "List configured providers with full details" },
|
||||
{ "start <provider> [<model>]", "Start new AI chat (space-separated, uses defaults if omitted)" },
|
||||
{ "switch <provider> [<model>]", "Switch to different provider (and optionally model)" },
|
||||
{ "models <provider>", "Fetch and display available models for provider" },
|
||||
{ "clear", "Clear current chat history" })
|
||||
CMD_EXAMPLES(
|
||||
"/ai",
|
||||
"/ai set token openai sk-xxx",
|
||||
"/ai set token perplexity pplx-xxx",
|
||||
"/ai set provider custom https://my-api.com/v1/chat/completions",
|
||||
"/ai set default-model perplexity sonar",
|
||||
"/ai set custom perplexity tools enabled",
|
||||
"/ai remove provider custom",
|
||||
"/ai start",
|
||||
"/ai start perplexity",
|
||||
"/ai start perplexity sonar",
|
||||
"/ai start openai gpt-4o",
|
||||
"/ai switch gpt-4o",
|
||||
"/ai switch openai gpt-5.4-nano",
|
||||
"/ai models perplexity",
|
||||
"/ai clear")
|
||||
},
|
||||
|
||||
// NEXT-COMMAND (search helper)
|
||||
};
|
||||
|
||||
@@ -2781,7 +2893,7 @@ _cmd_index(const Command* cmd)
|
||||
g_string_free(index_source, TRUE);
|
||||
|
||||
GString* index = g_string_new("");
|
||||
for (int i = 0; i < g_strv_length(tokens); i++) {
|
||||
for (guint i = 0; i < g_strv_length(tokens); i++) {
|
||||
index = g_string_append(index, tokens[i]);
|
||||
index = g_string_append(index, " ");
|
||||
}
|
||||
@@ -2798,16 +2910,15 @@ cmd_search_index_any(char* term)
|
||||
int terms_len = g_strv_length(processed_terms);
|
||||
|
||||
for (int i = 0; i < terms_len; i++) {
|
||||
GList* index_keys = g_hash_table_get_keys(search_index);
|
||||
GList* curr = index_keys;
|
||||
while (curr) {
|
||||
char* index_entry = g_hash_table_lookup(search_index, curr->data);
|
||||
GHashTableIter iter;
|
||||
gpointer key, value;
|
||||
g_hash_table_iter_init(&iter, search_index);
|
||||
while (g_hash_table_iter_next(&iter, &key, &value)) {
|
||||
char* index_entry = (char*)value;
|
||||
if (g_str_match_string(processed_terms[i], index_entry, FALSE)) {
|
||||
results = g_list_append(results, curr->data);
|
||||
results = g_list_append(results, key);
|
||||
}
|
||||
curr = g_list_next(curr);
|
||||
}
|
||||
g_list_free(index_keys);
|
||||
}
|
||||
|
||||
return results;
|
||||
@@ -2821,13 +2932,14 @@ cmd_search_index_all(char* term)
|
||||
auto_gcharv gchar** terms = g_str_tokenize_and_fold(term, NULL, NULL);
|
||||
int terms_len = g_strv_length(terms);
|
||||
|
||||
GList* commands = g_hash_table_get_keys(search_index);
|
||||
GList* curr = commands;
|
||||
while (curr) {
|
||||
char* command = curr->data;
|
||||
GHashTableIter iter;
|
||||
gpointer key, value;
|
||||
g_hash_table_iter_init(&iter, search_index);
|
||||
while (g_hash_table_iter_next(&iter, &key, &value)) {
|
||||
char* command = (char*)key;
|
||||
char* command_index = (char*)value;
|
||||
int matches = 0;
|
||||
for (int i = 0; i < terms_len; i++) {
|
||||
char* command_index = g_hash_table_lookup(search_index, command);
|
||||
if (g_str_match_string(terms[i], command_index, FALSE)) {
|
||||
matches++;
|
||||
}
|
||||
@@ -2835,11 +2947,8 @@ cmd_search_index_all(char* term)
|
||||
if (matches == terms_len) {
|
||||
results = g_list_append(results, command);
|
||||
}
|
||||
curr = g_list_next(curr);
|
||||
}
|
||||
|
||||
g_list_free(commands);
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
@@ -2961,8 +3070,14 @@ command_docgen(void)
|
||||
cmds = g_list_insert_sorted(cmds, (gpointer)pcmd, (GCompareFunc)_cmp_command);
|
||||
}
|
||||
|
||||
FILE* toc_fragment = fopen("toc_fragment.html", "w");
|
||||
FILE* main_fragment = fopen("main_fragment.html", "w");
|
||||
auto_FILE FILE* toc_fragment = fopen("toc_fragment.html", "w");
|
||||
auto_FILE FILE* main_fragment = fopen("main_fragment.html", "w");
|
||||
|
||||
if (!toc_fragment || !main_fragment) {
|
||||
log_error("command_docgen(): unable to open html files: %s", g_strerror(errno));
|
||||
g_list_free(cmds);
|
||||
return;
|
||||
}
|
||||
|
||||
fputs("<ul><li><ul><li>\n", toc_fragment);
|
||||
fputs("<hr>\n", main_fragment);
|
||||
@@ -3030,9 +3145,7 @@ command_docgen(void)
|
||||
|
||||
fputs("</ul></ul>\n", toc_fragment);
|
||||
|
||||
fclose(toc_fragment);
|
||||
fclose(main_fragment);
|
||||
printf("\nProcessed %d commands.\n\n", g_list_length(cmds));
|
||||
printf("\nProcessed %u commands.\n\n", g_list_length(cmds));
|
||||
g_list_free(cmds);
|
||||
}
|
||||
|
||||
@@ -3067,6 +3180,11 @@ command_mangen(void)
|
||||
return;
|
||||
}
|
||||
FILE* manpage = fopen(filename, "w");
|
||||
if (!manpage) {
|
||||
log_error("command_mangen(): unable to open %s for writing: %s", filename, g_strerror(errno));
|
||||
curr = g_list_next(curr);
|
||||
continue;
|
||||
}
|
||||
|
||||
fprintf(manpage, "%s\n", header);
|
||||
fputs(".SH NAME\n", manpage);
|
||||
@@ -3105,7 +3223,7 @@ command_mangen(void)
|
||||
curr = g_list_next(curr);
|
||||
}
|
||||
|
||||
printf("\nProcessed %d commands.\n\n", g_list_length(cmds));
|
||||
printf("\nProcessed %u commands.\n\n", g_list_length(cmds));
|
||||
|
||||
g_list_free(cmds);
|
||||
}
|
||||
|
||||
@@ -3,35 +3,9 @@
|
||||
* vim: expandtab:ts=4:sts=4:sw=4
|
||||
*
|
||||
* Copyright (C) 2012 - 2019 James Booth <boothj5@gmail.com>
|
||||
* Copyright (C) 2019 - 2025 Michael Vetter <jubalh@iodoru.org>
|
||||
*
|
||||
* This file is part of Profanity.
|
||||
*
|
||||
* Profanity is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Profanity is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Profanity. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* In addition, as a special exception, the copyright holders give permission to
|
||||
* link the code of portions of this program with the OpenSSL library under
|
||||
* certain conditions as described in each individual source file, and
|
||||
* distribute linked combinations including the two.
|
||||
*
|
||||
* You must obey the GNU General Public License in all respects for all of the
|
||||
* code used other than OpenSSL. If you modify file(s) with this exception, you
|
||||
* may extend this exception to your version of the file(s), but you are not
|
||||
* obligated to do so. If you do not wish to do so, delete this exception
|
||||
* statement from your version. If you delete this exception statement from all
|
||||
* source files in the program, then also delete it here.
|
||||
* Copyright (C) 2019 - 2026 Michael Vetter <jubalh@iodoru.org>
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later WITH OpenSSL-exception
|
||||
*/
|
||||
|
||||
#ifndef COMMAND_CMD_DEFS_H
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3,35 +3,9 @@
|
||||
* vim: expandtab:ts=4:sts=4:sw=4
|
||||
*
|
||||
* Copyright (C) 2012 - 2019 James Booth <boothj5@gmail.com>
|
||||
* Copyright (C) 2019 - 2025 Michael Vetter <jubalh@iodoru.org>
|
||||
*
|
||||
* This file is part of Profanity.
|
||||
*
|
||||
* Profanity is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Profanity is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Profanity. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* In addition, as a special exception, the copyright holders give permission to
|
||||
* link the code of portions of this program with the OpenSSL library under
|
||||
* certain conditions as described in each individual source file, and
|
||||
* distribute linked combinations including the two.
|
||||
*
|
||||
* You must obey the GNU General Public License in all respects for all of the
|
||||
* code used other than OpenSSL. If you modify file(s) with this exception, you
|
||||
* may extend this exception to your version of the file(s), but you are not
|
||||
* obligated to do so. If you do not wish to do so, delete this exception
|
||||
* statement from your version. If you delete this exception statement from all
|
||||
* source files in the program, then also delete it here.
|
||||
* Copyright (C) 2019 - 2026 Michael Vetter <jubalh@iodoru.org>
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later WITH OpenSSL-exception
|
||||
*/
|
||||
|
||||
#ifndef COMMAND_CMD_FUNCS_H
|
||||
@@ -129,6 +103,7 @@ gboolean cmd_bookmark_ignore(ProfWin* window, const char* const command, gchar**
|
||||
gboolean cmd_roster(ProfWin* window, const char* const command, gchar** args);
|
||||
gboolean cmd_software(ProfWin* window, const char* const command, gchar** args);
|
||||
gboolean cmd_splash(ProfWin* window, const char* const command, gchar** args);
|
||||
gboolean cmd_spellcheck(ProfWin* window, const char* const command, gchar** args);
|
||||
gboolean cmd_states(ProfWin* window, const char* const command, gchar** args);
|
||||
gboolean cmd_status_get(ProfWin* window, const char* const command, gchar** args);
|
||||
gboolean cmd_status_set(ProfWin* window, const char* const command, gchar** args);
|
||||
@@ -166,6 +141,14 @@ gboolean cmd_console(ProfWin* window, const char* const command, gchar** args);
|
||||
gboolean cmd_command_list(ProfWin* window, const char* const command, gchar** args);
|
||||
gboolean cmd_command_exec(ProfWin* window, const char* const command, gchar** args);
|
||||
gboolean cmd_change_password(ProfWin* window, const char* const command, gchar** args);
|
||||
gboolean cmd_ai(ProfWin* window, const char* const command, gchar** args);
|
||||
gboolean cmd_ai_set(ProfWin* window, const char* const command, gchar** args);
|
||||
gboolean cmd_ai_remove(ProfWin* window, const char* const command, gchar** args);
|
||||
gboolean cmd_ai_start(ProfWin* window, const char* const command, gchar** args);
|
||||
gboolean cmd_ai_clear(ProfWin* window, const char* const command, gchar** args);
|
||||
gboolean cmd_ai_providers(ProfWin* window, const char* const command, gchar** args);
|
||||
gboolean cmd_ai_switch(ProfWin* window, const char* const command, gchar** args);
|
||||
gboolean cmd_ai_models(ProfWin* window, const char* const command, gchar** args);
|
||||
|
||||
gboolean cmd_plugins(ProfWin* window, const char* const command, gchar** args);
|
||||
gboolean cmd_plugins_sourcepath(ProfWin* window, const char* const command, gchar** args);
|
||||
@@ -234,6 +217,7 @@ gboolean cmd_omemo_policy(ProfWin* window, const char* const command, gchar** ar
|
||||
gboolean cmd_omemo_clear_device_list(ProfWin* window, const char* const command, gchar** args);
|
||||
gboolean cmd_omemo_qrcode(ProfWin* window, const char* const command, gchar** args);
|
||||
|
||||
gboolean cmd_changes(ProfWin* window, const char* const command, gchar** args);
|
||||
gboolean cmd_save(ProfWin* window, const char* const command, gchar** args);
|
||||
gboolean cmd_reload(ProfWin* window, const char* const command, gchar** args);
|
||||
|
||||
@@ -269,5 +253,6 @@ gboolean cmd_vcard_photo(ProfWin* window, const char* const command, gchar** arg
|
||||
gboolean cmd_vcard_refresh(ProfWin* window, const char* const command, gchar** args);
|
||||
gboolean cmd_vcard_set(ProfWin* window, const char* const command, gchar** args);
|
||||
gboolean cmd_vcard_save(ProfWin* window, const char* const command, gchar** args);
|
||||
gboolean cmd_force_encryption(ProfWin* window, const char* const command, gchar** args);
|
||||
|
||||
#endif
|
||||
|
||||
297
src/common.c
297
src/common.c
@@ -3,35 +3,9 @@
|
||||
* vim: expandtab:ts=4:sts=4:sw=4
|
||||
*
|
||||
* Copyright (C) 2012 - 2019 James Booth <boothj5@gmail.com>
|
||||
* Copyright (C) 2019 - 2025 Michael Vetter <jubalh@iodoru.org>
|
||||
*
|
||||
* This file is part of Profanity.
|
||||
*
|
||||
* Profanity is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Profanity is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Profanity. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* In addition, as a special exception, the copyright holders give permission to
|
||||
* link the code of portions of this program with the OpenSSL library under
|
||||
* certain conditions as described in each individual source file, and
|
||||
* distribute linked combinations including the two.
|
||||
*
|
||||
* You must obey the GNU General Public License in all respects for all of the
|
||||
* code used other than OpenSSL. If you modify file(s) with this exception, you
|
||||
* may extend this exception to your version of the file(s), but you are not
|
||||
* obligated to do so. If you do not wish to do so, delete this exception
|
||||
* statement from your version. If you delete this exception statement from all
|
||||
* source files in the program, then also delete it here.
|
||||
* Copyright (C) 2019 - 2026 Michael Vetter <jubalh@iodoru.org>
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later WITH OpenSSL-exception
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
@@ -45,6 +19,7 @@
|
||||
#include <string.h>
|
||||
#include <sys/types.h>
|
||||
#include <sys/stat.h>
|
||||
#include <fcntl.h>
|
||||
|
||||
#include <curl/curl.h>
|
||||
#include <curl/easy.h>
|
||||
@@ -63,6 +38,7 @@
|
||||
#include "log.h"
|
||||
#include "common.h"
|
||||
#include "config/files.h"
|
||||
#include "ui/ui.h"
|
||||
|
||||
#ifdef HAVE_GIT_VERSION
|
||||
#include "gitversion.h"
|
||||
@@ -142,7 +118,7 @@ auto_close_gfd(gint* fd)
|
||||
return;
|
||||
|
||||
if (close(*fd) == EOF)
|
||||
log_error(g_strerror(errno));
|
||||
log_error("%s", g_strerror(errno));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -157,23 +133,30 @@ auto_close_FILE(FILE** fd)
|
||||
return;
|
||||
|
||||
if (fclose(*fd) == EOF)
|
||||
log_error(g_strerror(errno));
|
||||
log_error("%s", g_strerror(errno));
|
||||
}
|
||||
|
||||
void
|
||||
auto_free_gerror(GError** err)
|
||||
{
|
||||
if (err == NULL)
|
||||
return;
|
||||
|
||||
PROF_GERROR_FREE(*err);
|
||||
}
|
||||
|
||||
static gboolean
|
||||
_load_keyfile(prof_keyfile_t* keyfile)
|
||||
{
|
||||
GError* error = NULL;
|
||||
auto_gerror GError* error = NULL;
|
||||
keyfile->keyfile = g_key_file_new();
|
||||
|
||||
if (g_key_file_load_from_file(keyfile->keyfile, keyfile->filename, G_KEY_FILE_KEEP_COMMENTS | G_KEY_FILE_KEEP_TRANSLATIONS, &error)) {
|
||||
return TRUE;
|
||||
} else if (error->code != G_FILE_ERROR_NOENT) {
|
||||
} else if (error && error->code != G_FILE_ERROR_NOENT) {
|
||||
log_warning("[Keyfile] error loading %s: %s", keyfile->filename, error->message);
|
||||
g_error_free(error);
|
||||
} else {
|
||||
log_warning("[Keyfile] no such file: %s", keyfile->filename);
|
||||
g_error_free(error);
|
||||
}
|
||||
return FALSE;
|
||||
}
|
||||
@@ -199,6 +182,10 @@ load_custom_keyfile(prof_keyfile_t* keyfile, gchar* filename)
|
||||
|
||||
if (g_file_test(keyfile->filename, G_FILE_TEST_EXISTS)) {
|
||||
g_chmod(keyfile->filename, S_IRUSR | S_IWUSR);
|
||||
} else {
|
||||
int fno = g_creat(keyfile->filename, S_IRUSR | S_IWUSR);
|
||||
if (fno != -1)
|
||||
g_close(fno, NULL);
|
||||
}
|
||||
|
||||
return _load_keyfile(keyfile);
|
||||
@@ -207,10 +194,9 @@ load_custom_keyfile(prof_keyfile_t* keyfile, gchar* filename)
|
||||
gboolean
|
||||
save_keyfile(prof_keyfile_t* keyfile)
|
||||
{
|
||||
GError* error = NULL;
|
||||
auto_gerror GError* error = NULL;
|
||||
if (!g_key_file_save_to_file(keyfile->keyfile, keyfile->filename, &error)) {
|
||||
log_error("[Keyfile]: saving file %s failed! %s", keyfile->filename, error->message);
|
||||
g_error_free(error);
|
||||
log_error("[Keyfile]: saving file %s failed! %s", STR_MAYBE_NULL(keyfile->filename), PROF_GERROR_MESSAGE(error));
|
||||
return FALSE;
|
||||
}
|
||||
g_chmod(keyfile->filename, S_IRUSR | S_IWUSR);
|
||||
@@ -249,11 +235,8 @@ copy_file(const char* const sourcepath, const char* const targetpath, const gboo
|
||||
{
|
||||
GFile* source = g_file_new_for_path(sourcepath);
|
||||
GFile* dest = g_file_new_for_path(targetpath);
|
||||
GError* error = NULL;
|
||||
GFileCopyFlags flags = overwrite_existing ? G_FILE_COPY_OVERWRITE : G_FILE_COPY_NONE;
|
||||
gboolean success = g_file_copy(source, dest, flags, NULL, NULL, NULL, &error);
|
||||
if (error != NULL)
|
||||
g_error_free(error);
|
||||
gboolean success = g_file_copy(source, dest, flags, NULL, NULL, NULL, NULL);
|
||||
g_object_unref(source);
|
||||
g_object_unref(dest);
|
||||
return success;
|
||||
@@ -311,7 +294,7 @@ str_replace(const char* string, const char* substr,
|
||||
wr = newstr;
|
||||
|
||||
while ((tok = strstr(head, substr))) {
|
||||
size_t l = tok - head;
|
||||
size_t l = g_diff_to_gsize(tok, head);
|
||||
memcpy(wr, head, l);
|
||||
wr += l;
|
||||
memcpy(wr, replacement, len_replacement);
|
||||
@@ -326,33 +309,103 @@ str_replace(const char* string, const char* substr,
|
||||
return newstr;
|
||||
}
|
||||
|
||||
gsize
|
||||
g_diff_to_gsize(const void* end, const void* start)
|
||||
{
|
||||
if (end < start) {
|
||||
log_error("g_diff_to_gsize: end < start (%p < %p)", end, start);
|
||||
return 0;
|
||||
}
|
||||
return (gsize)((const char*)end - (const char*)start);
|
||||
}
|
||||
|
||||
gboolean
|
||||
strtoi_range(const char* str, int* saveptr, int min, int max, gchar** err_msg)
|
||||
{
|
||||
char* ptr;
|
||||
int val;
|
||||
long lval;
|
||||
if (str == NULL) {
|
||||
if (err_msg)
|
||||
*err_msg = g_strdup_printf("'str' input pointer can not be NULL");
|
||||
return FALSE;
|
||||
}
|
||||
errno = 0;
|
||||
val = (int)strtol(str, &ptr, 0);
|
||||
lval = strtol(str, &ptr, 0);
|
||||
if (errno != 0 || *str == '\0' || *ptr != '\0') {
|
||||
if (err_msg)
|
||||
*err_msg = g_strdup_printf("Could not convert \"%s\" to a number.", str);
|
||||
return FALSE;
|
||||
} else if (val < min || val > max) {
|
||||
} else if (lval < (long)min || lval > (long)max) {
|
||||
if (err_msg)
|
||||
*err_msg = g_strdup_printf("Value %s out of range. Must be in %d..%d.", str, min, max);
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
*saveptr = val;
|
||||
*saveptr = (int)lval;
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
gboolean
|
||||
string_matches_one_of(const char* what, const char* is, gboolean is_can_be_null, const char* first, ...)
|
||||
{
|
||||
gboolean ret = FALSE;
|
||||
va_list ap;
|
||||
const char* cur = first;
|
||||
if (!is)
|
||||
return is_can_be_null;
|
||||
|
||||
va_start(ap, first);
|
||||
while (cur != NULL) {
|
||||
if (g_strcmp0(is, cur) == 0) {
|
||||
ret = TRUE;
|
||||
break;
|
||||
}
|
||||
cur = va_arg(ap, const char*);
|
||||
}
|
||||
va_end(ap);
|
||||
if (!ret && what) {
|
||||
cons_show("Invalid %s: '%s'", what, is);
|
||||
char errmsg[256] = { 0 };
|
||||
size_t sz = 0;
|
||||
int s = snprintf(errmsg, sizeof(errmsg) - sz, "%s must be one of:", what);
|
||||
if (s < 0 || (size_t)s + sz >= sizeof(errmsg))
|
||||
return ret;
|
||||
sz += (size_t)s;
|
||||
|
||||
cur = first;
|
||||
va_start(ap, first);
|
||||
while (cur != NULL) {
|
||||
const char* next = va_arg(ap, const char*);
|
||||
if (next) {
|
||||
s = snprintf(errmsg + sz, sizeof(errmsg) - sz, " '%s',", cur);
|
||||
} else {
|
||||
/* remove last ',' */
|
||||
sz--;
|
||||
errmsg[sz] = '\0';
|
||||
s = snprintf(errmsg + sz, sizeof(errmsg) - sz, " or '%s'.", cur);
|
||||
}
|
||||
if (s < 0 || (size_t)s + sz >= sizeof(errmsg)) {
|
||||
log_debug("Error message too long or some other error occurred (%d).", s);
|
||||
s = -1;
|
||||
break;
|
||||
}
|
||||
sz += (size_t)s;
|
||||
cur = next;
|
||||
}
|
||||
va_end(ap);
|
||||
if (s > 0)
|
||||
cons_show("%s", errmsg);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
gboolean
|
||||
valid_tls_policy_option(const char* is)
|
||||
{
|
||||
return string_matches_one_of("TLS policy", is, TRUE, "force", "allow", "trust", "disable", "legacy", "direct", NULL);
|
||||
}
|
||||
|
||||
int
|
||||
utf8_display_len(const char* const str)
|
||||
{
|
||||
@@ -375,10 +428,37 @@ utf8_display_len(const char* const str)
|
||||
return len;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes illegal XML 1.0 characters from a string.
|
||||
*
|
||||
* This function creates a new string that excludes characters in the range
|
||||
* U+0000 to U+001F, except for U+0009 (TAB), U+000A (LF), and U+000D (CR).
|
||||
*/
|
||||
gchar*
|
||||
str_xml_sanitize(const char* const str)
|
||||
{
|
||||
if (str == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
GString* sanitized = g_string_new_len(NULL, (gssize)strlen(str));
|
||||
const char* curr = str;
|
||||
|
||||
while (*curr != '\0') {
|
||||
gunichar c = g_utf8_get_char(curr);
|
||||
if ((c >= 0x20) || (c == 0x09) || (c == 0x0A) || (c == 0x0D)) {
|
||||
g_string_append_unichar(sanitized, c);
|
||||
}
|
||||
curr = g_utf8_next_char(curr);
|
||||
}
|
||||
|
||||
return g_string_free(sanitized, FALSE);
|
||||
}
|
||||
|
||||
char*
|
||||
release_get_latest(void)
|
||||
{
|
||||
char* url = "https://profanity-im.github.io/profanity_version.txt";
|
||||
const char* url = "https://profanity-im.github.io/profanity_version.txt";
|
||||
|
||||
CURL* handle = curl_easy_init();
|
||||
struct curl_data_t output;
|
||||
@@ -402,11 +482,15 @@ release_get_latest(void)
|
||||
}
|
||||
|
||||
gboolean
|
||||
release_is_new(char* found_version)
|
||||
release_is_new(const char* const curr_version, const char* const found_version)
|
||||
{
|
||||
if (!curr_version || !found_version) {
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
int curr_maj, curr_min, curr_patch, found_maj, found_min, found_patch;
|
||||
|
||||
int parse_curr = sscanf(PACKAGE_VERSION, "%d.%d.%d", &curr_maj, &curr_min,
|
||||
int parse_curr = sscanf(curr_version, "%d.%d.%d", &curr_maj, &curr_min,
|
||||
&curr_patch);
|
||||
int parse_found = sscanf(found_version, "%d.%d.%d", &found_maj, &found_min,
|
||||
&found_patch);
|
||||
@@ -469,23 +553,32 @@ _get_file_or_linked(gchar* loc)
|
||||
char*
|
||||
strip_arg_quotes(const char* const input)
|
||||
{
|
||||
char* unquoted = strdup(input);
|
||||
if (input == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Remove starting quote if it exists
|
||||
if (strchr(unquoted, '"')) {
|
||||
if (strchr(unquoted, ' ') + 1 == strchr(unquoted, '"')) {
|
||||
memmove(strchr(unquoted, '"'), strchr(unquoted, '"') + 1, strchr(unquoted, '\0') - strchr(unquoted, '"'));
|
||||
// Unescape and strip quotes
|
||||
GString* unescaped = g_string_new("");
|
||||
for (const char* p = input; *p; p++) {
|
||||
if (*p == '\\' && (*(p + 1) != '\0')) {
|
||||
p++;
|
||||
g_string_append_c(unescaped, *p);
|
||||
} else if (*p == '"') {
|
||||
// Only strip if it's the first char or preceded by a space
|
||||
if (p == input || *(p - 1) == ' ') {
|
||||
continue;
|
||||
}
|
||||
// Or if it's the last char
|
||||
if (*(p + 1) == '\0') {
|
||||
continue;
|
||||
}
|
||||
g_string_append_c(unescaped, *p);
|
||||
} else {
|
||||
g_string_append_c(unescaped, *p);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove ending quote if it exists
|
||||
if (strchr(unquoted, '"')) {
|
||||
if (strchr(unquoted, '\0') - 1 == strchr(unquoted, '"')) {
|
||||
memmove(strchr(unquoted, '"'), strchr(unquoted, '"') + 1, strchr(unquoted, '\0') - strchr(unquoted, '"'));
|
||||
}
|
||||
}
|
||||
|
||||
return unquoted;
|
||||
return g_string_free(unescaped, FALSE);
|
||||
}
|
||||
|
||||
gboolean
|
||||
@@ -509,36 +602,48 @@ is_notify_enabled(void)
|
||||
GSList*
|
||||
prof_occurrences(const char* const needle, const char* const haystack, int offset, gboolean whole_word, GSList** result)
|
||||
{
|
||||
if (needle == NULL || haystack == NULL) {
|
||||
if (needle == NULL || haystack == NULL || *needle == '\0') {
|
||||
return *result;
|
||||
}
|
||||
|
||||
do {
|
||||
gchar* haystack_curr = g_utf8_offset_to_pointer(haystack, offset);
|
||||
if (g_str_has_prefix(haystack_curr, needle)) {
|
||||
size_t needle_len = strlen(needle);
|
||||
gchar* p = g_utf8_offset_to_pointer(haystack, offset);
|
||||
GSList* matches = NULL;
|
||||
|
||||
while (p) {
|
||||
if (g_str_has_prefix(p, needle)) {
|
||||
if (whole_word) {
|
||||
gunichar before = 0;
|
||||
gchar* haystack_before_ch = g_utf8_find_prev_char(haystack, haystack_curr);
|
||||
gchar* haystack_before_ch = g_utf8_find_prev_char(haystack, p);
|
||||
if (haystack_before_ch) {
|
||||
before = g_utf8_get_char(haystack_before_ch);
|
||||
}
|
||||
|
||||
gunichar after = 0;
|
||||
gchar* haystack_after_ch = haystack_curr + strlen(needle);
|
||||
if (haystack_after_ch[0] != '\0') {
|
||||
gchar* haystack_after_ch = p + needle_len;
|
||||
if (*haystack_after_ch != '\0') {
|
||||
after = g_utf8_get_char(haystack_after_ch);
|
||||
}
|
||||
|
||||
if (!g_unichar_isalnum(before) && !g_unichar_isalnum(after)) {
|
||||
*result = g_slist_append(*result, GINT_TO_POINTER(offset));
|
||||
matches = g_slist_prepend(matches, GINT_TO_POINTER(offset));
|
||||
}
|
||||
} else {
|
||||
*result = g_slist_append(*result, GINT_TO_POINTER(offset));
|
||||
matches = g_slist_prepend(matches, GINT_TO_POINTER(offset));
|
||||
}
|
||||
}
|
||||
|
||||
if (*p == '\0') {
|
||||
break;
|
||||
}
|
||||
|
||||
p = g_utf8_next_char(p);
|
||||
offset++;
|
||||
} while (g_strcmp0(g_utf8_offset_to_pointer(haystack, offset), "\0") != 0);
|
||||
}
|
||||
|
||||
if (matches) {
|
||||
*result = g_slist_concat(*result, g_slist_reverse(matches));
|
||||
}
|
||||
|
||||
return *result;
|
||||
}
|
||||
@@ -594,19 +699,19 @@ get_file_paths_recursive(const char* path, GSList** contents)
|
||||
}
|
||||
}
|
||||
|
||||
char*
|
||||
get_random_string(int length)
|
||||
gchar*
|
||||
get_random_string(size_t length)
|
||||
{
|
||||
GRand* prng;
|
||||
char* rand;
|
||||
char alphabet[] = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
||||
gchar* rand;
|
||||
gchar alphabet[] = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
||||
int endrange = sizeof(alphabet) - 1;
|
||||
|
||||
rand = calloc(length + 1, sizeof(char));
|
||||
rand = g_malloc0(length + 1);
|
||||
|
||||
prng = g_rand_new();
|
||||
|
||||
for (int i = 0; i < length; i++) {
|
||||
for (size_t i = 0; i < length; i++) {
|
||||
rand[i] = alphabet[g_rand_int_range(prng, 0, endrange)];
|
||||
}
|
||||
g_rand_free(prng);
|
||||
@@ -629,7 +734,7 @@ get_mentions(gboolean whole_word, gboolean case_sensitive, const char* const mes
|
||||
gboolean
|
||||
call_external(gchar** argv)
|
||||
{
|
||||
GError* spawn_error;
|
||||
auto_gerror GError* spawn_error = NULL;
|
||||
gboolean is_successful;
|
||||
|
||||
GSpawnFlags flags = G_SPAWN_SEARCH_PATH | G_SPAWN_STDOUT_TO_DEV_NULL | G_SPAWN_STDERR_TO_DEV_NULL;
|
||||
@@ -642,9 +747,7 @@ call_external(gchar** argv)
|
||||
&spawn_error);
|
||||
if (!is_successful) {
|
||||
auto_gchar gchar* cmd = g_strjoinv(" ", argv);
|
||||
log_error("Spawning '%s' failed with error '%s'", cmd, spawn_error ? spawn_error->message : "Unknown, spawn_error is NULL");
|
||||
|
||||
g_error_free(spawn_error);
|
||||
log_error("Spawning '%s' failed with error '%s'", cmd, PROF_GERROR_MESSAGE(spawn_error));
|
||||
}
|
||||
|
||||
return is_successful;
|
||||
@@ -655,17 +758,17 @@ call_external(gchar** argv)
|
||||
*
|
||||
* This function constructs an argument vector (argv) based on the provided template string, replacing placeholders ("%u" and "%p") with the provided URL and filename, respectively.
|
||||
*
|
||||
* @param template The template string with placeholders.
|
||||
* @param url The URL to replace "%u" (or NULL to skip).
|
||||
* @param filename The filename to replace "%p" (or NULL to skip).
|
||||
* @return The constructed argument vector (argv) as a null-terminated array of strings.
|
||||
* @param template_fmt The template string with placeholders.
|
||||
* @param url The URL to replace "%u" (or NULL to skip).
|
||||
* @param filename The filename to replace "%p" (or NULL to skip).
|
||||
* @return The constructed argument vector (argv) as a null-terminated array of strings.
|
||||
*
|
||||
* @note Remember to free the returned argument vector using `auto_gcharv` or `g_strfreev()`.
|
||||
*/
|
||||
gchar**
|
||||
format_call_external_argv(const char* template, const char* url, const char* filename)
|
||||
format_call_external_argv(const char* template_fmt, const char* url, const char* filename)
|
||||
{
|
||||
gchar** argv = g_strsplit(template, " ", 0);
|
||||
gchar** argv = g_strsplit(template_fmt, " ", 0);
|
||||
|
||||
guint num_args = 0;
|
||||
while (argv[num_args]) {
|
||||
@@ -674,7 +777,7 @@ format_call_external_argv(const char* template, const char* url, const char* fil
|
||||
argv[num_args] = g_strdup(url);
|
||||
} else if (0 == g_strcmp0(argv[num_args], "%p") && filename != NULL) {
|
||||
g_free(argv[num_args]);
|
||||
argv[num_args] = strdup(filename);
|
||||
argv[num_args] = g_strdup(filename);
|
||||
}
|
||||
num_args++;
|
||||
}
|
||||
@@ -706,7 +809,7 @@ _unique_filename(const char* filename)
|
||||
return unique;
|
||||
}
|
||||
|
||||
static bool
|
||||
static gboolean
|
||||
_has_directory_suffix(const char* path)
|
||||
{
|
||||
return (g_str_has_suffix(path, ".")
|
||||
@@ -804,6 +907,20 @@ unique_filename_from_url(const char* url, const char* path)
|
||||
return unique_filename;
|
||||
}
|
||||
|
||||
/* This returns a timestamp formatted in ISO8601 format.
|
||||
* Either it returns the current time if `dt` is NULL, or
|
||||
* it returns the formatted time value passed in `dt`.
|
||||
*/
|
||||
gchar*
|
||||
prof_date_time_format_iso8601(GDateTime* dt)
|
||||
{
|
||||
GDateTime* dt_ = (dt == NULL) ? g_date_time_new_now_local() : dt;
|
||||
gchar* ret = g_date_time_format_iso8601(dt_);
|
||||
if (dt == NULL)
|
||||
g_date_time_unref(dt_);
|
||||
return ret;
|
||||
}
|
||||
|
||||
/* build profanity version string.
|
||||
* example: 0.13.1dev.master.69d8c1f9
|
||||
*/
|
||||
|
||||
56
src/common.h
56
src/common.h
@@ -4,36 +4,7 @@
|
||||
*
|
||||
* Copyright (C) 2012 - 2019 James Booth <boothj5@gmail.com>
|
||||
*
|
||||
* This file is part of Profanity.
|
||||
*
|
||||
* Profanity is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Profanity is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Profanity. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* In addition, as a special exception, the copyright holders give permission to
|
||||
* link the code of portions of this program with the OpenSSL library under
|
||||
* certain conditions as described in each individual source file, and
|
||||
* distribute linked combinations including the two.
|
||||
*
|
||||
* You must obey the GNU General Public License in all respects for all of the
|
||||
* code used other than OpenSSL. If you modify file(s) with this exception, you
|
||||
* may extend this exception to your version of the file(s), but you are not
|
||||
* obligated to do so. If you do not wish to do so, delete this exception
|
||||
* statement from your version. If you delete this exception statement from all
|
||||
* source files in the program, then also delete it here.
|
||||
*
|
||||
* @file common.h
|
||||
*
|
||||
* @brief Common utilities for the project.
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later WITH OpenSSL-exception
|
||||
*/
|
||||
|
||||
#ifndef COMMON_H
|
||||
@@ -112,12 +83,22 @@ void auto_close_gfd(gint* fd);
|
||||
|
||||
void auto_close_FILE(FILE** fd);
|
||||
|
||||
void auto_free_gerror(GError** err);
|
||||
#define auto_gerror __attribute__((__cleanup__(auto_free_gerror)))
|
||||
|
||||
#if defined(__OpenBSD__)
|
||||
#define STR_MAYBE_NULL(p) (p) ?: "(null)"
|
||||
#else
|
||||
#define STR_MAYBE_NULL(p) (p)
|
||||
#endif
|
||||
|
||||
#define PROF_GERROR_MESSAGE(err) (err) ? (err)->message : "error message missing"
|
||||
#define PROF_GERROR_FREE(err) \
|
||||
do { \
|
||||
if (err) \
|
||||
g_error_free(err); \
|
||||
} while (0)
|
||||
|
||||
typedef struct prof_keyfile_t
|
||||
{
|
||||
gchar* filename;
|
||||
@@ -171,18 +152,21 @@ typedef enum {
|
||||
RESOURCE_XA
|
||||
} resource_presence_t;
|
||||
|
||||
extern gboolean background_mode;
|
||||
|
||||
gboolean string_to_verbosity(const char* cmd, int* verbosity, gchar** err_msg);
|
||||
|
||||
gboolean create_dir(const char* name);
|
||||
gboolean copy_file(const char* const src, const char* const target, const gboolean overwrite_existing);
|
||||
char* str_replace(const char* string, const char* substr, const char* replacement);
|
||||
gboolean strtoi_range(const char* str, int* saveptr, int min, int max, char** err_msg);
|
||||
gsize g_diff_to_gsize(const void* end, const void* start);
|
||||
int utf8_display_len(const char* const str);
|
||||
gchar* str_xml_sanitize(const char* const str);
|
||||
|
||||
gboolean string_matches_one_of(const char* what, const char* is, gboolean is_can_be_null, const char* first, ...) __attribute__((sentinel));
|
||||
gboolean valid_tls_policy_option(const char* is);
|
||||
|
||||
char* release_get_latest(void);
|
||||
gboolean release_is_new(char* found_version);
|
||||
gboolean release_is_new(const char* const curr_version, const char* const found_version);
|
||||
|
||||
char* strip_arg_quotes(const char* const input);
|
||||
gboolean is_notify_enabled(void);
|
||||
@@ -195,16 +179,18 @@ int is_regular_file(const char* path);
|
||||
int is_dir(const char* path);
|
||||
void get_file_paths_recursive(const char* directory, GSList** contents);
|
||||
|
||||
char* get_random_string(int length);
|
||||
gchar* get_random_string(size_t length);
|
||||
|
||||
gboolean call_external(gchar** argv);
|
||||
gchar** format_call_external_argv(const char* template, const char* url, const char* filename);
|
||||
gchar** format_call_external_argv(const char* template_fmt, const char* url, const char* filename);
|
||||
|
||||
gchar* unique_filename_from_url(const char* url, const char* path);
|
||||
gchar* get_expanded_path(const char* path);
|
||||
|
||||
char* basename_from_url(const char* url);
|
||||
|
||||
gchar* prof_date_time_format_iso8601(GDateTime* dt);
|
||||
|
||||
gchar* prof_get_version(void);
|
||||
void prof_add_shutdown_routine(void (*routine)(void));
|
||||
|
||||
|
||||
@@ -4,33 +4,7 @@
|
||||
*
|
||||
* Copyright (C) 2012 - 2019 James Booth <boothj5@gmail.com>
|
||||
*
|
||||
* This file is part of Profanity.
|
||||
*
|
||||
* Profanity is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Profanity is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Profanity. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* In addition, as a special exception, the copyright holders give permission to
|
||||
* link the code of portions of this program with the OpenSSL library under
|
||||
* certain conditions as described in each individual source file, and
|
||||
* distribute linked combinations including the two.
|
||||
*
|
||||
* You must obey the GNU General Public License in all respects for all of the
|
||||
* code used other than OpenSSL. If you modify file(s) with this exception, you
|
||||
* may extend this exception to your version of the file(s), but you are not
|
||||
* obligated to do so. If you do not wish to do so, delete this exception
|
||||
* statement from your version. If you delete this exception statement from all
|
||||
* source files in the program, then also delete it here.
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later WITH OpenSSL-exception
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
@@ -39,6 +13,7 @@
|
||||
#include <string.h>
|
||||
#include <assert.h>
|
||||
#include <errno.h>
|
||||
#include <sys/wait.h>
|
||||
|
||||
#include <glib.h>
|
||||
|
||||
@@ -59,7 +34,7 @@ account_new(gchar* name, gchar* jid, gchar* password, gchar* eval_password, gboo
|
||||
gchar* startscript, gchar* theme, gchar* tls_policy, gchar* auth_policy,
|
||||
gchar* client, int max_sessions)
|
||||
{
|
||||
ProfAccount* new_account = calloc(1, sizeof(ProfAccount));
|
||||
ProfAccount* new_account = g_new0(ProfAccount, 1);
|
||||
|
||||
new_account->name = name;
|
||||
|
||||
@@ -144,13 +119,13 @@ account_new(gchar* name, gchar* jid, gchar* password, gchar* eval_password, gboo
|
||||
return new_account;
|
||||
}
|
||||
|
||||
char*
|
||||
gchar*
|
||||
account_create_connect_jid(ProfAccount* account)
|
||||
{
|
||||
if (account->resource) {
|
||||
return create_fulljid(account->jid, account->resource);
|
||||
} else {
|
||||
return strdup(account->jid);
|
||||
return g_strdup(account->jid);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -160,53 +135,41 @@ account_eval_password(ProfAccount* account)
|
||||
assert(account != NULL);
|
||||
assert(account->eval_password != NULL);
|
||||
|
||||
errno = 0;
|
||||
gchar* stdout_buf = NULL;
|
||||
GError* error = NULL;
|
||||
gint exit_status = 0;
|
||||
gchar** argv = NULL;
|
||||
|
||||
FILE* stream = popen(account->eval_password, "r");
|
||||
if (stream == NULL) {
|
||||
const char* errmsg = strerror(errno);
|
||||
if (errmsg) {
|
||||
log_error("Could not execute `eval_password` command (%s).",
|
||||
errmsg);
|
||||
} else {
|
||||
log_error("Failed to allocate memory for `eval_password` command.");
|
||||
if (!g_shell_parse_argv(account->eval_password, NULL, &argv, &error)) {
|
||||
log_error("Failed to parse `eval_password` command: %s", error->message);
|
||||
g_error_free(error);
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
if (!g_spawn_sync(NULL, argv, NULL, G_SPAWN_SEARCH_PATH | G_SPAWN_CHILD_INHERITS_STDIN, NULL, NULL, &stdout_buf, NULL, &exit_status, &error)) {
|
||||
log_error("Failed to execute `eval_password` command: %s", error->message);
|
||||
g_strfreev(argv);
|
||||
g_error_free(error);
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
g_strfreev(argv);
|
||||
|
||||
if (WIFEXITED(exit_status) && WEXITSTATUS(exit_status) == 0) {
|
||||
account->password = stdout_buf;
|
||||
g_strstrip(account->password);
|
||||
if (account->password[0] == '\0') {
|
||||
log_error("Empty password returned by `eval_password` command.");
|
||||
g_free(account->password);
|
||||
account->password = NULL;
|
||||
return FALSE;
|
||||
}
|
||||
return TRUE;
|
||||
} else {
|
||||
log_error("`eval_password` command failed with status %d", exit_status);
|
||||
g_free(stdout_buf);
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
account->password = g_malloc(READ_BUF_SIZE);
|
||||
if (!account->password) {
|
||||
log_error("Failed to allocate enough memory to read `eval_password` "
|
||||
"output.");
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
account->password = fgets(account->password, READ_BUF_SIZE, stream);
|
||||
if (!account->password) {
|
||||
log_error("Failed to read password from stream.");
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
int exit_status = pclose(stream);
|
||||
if (exit_status > 0) {
|
||||
log_error("Command for `eval_password` returned error status (%s).",
|
||||
exit_status);
|
||||
return FALSE;
|
||||
} else if (exit_status < 0) {
|
||||
log_error("Failed to close stream for `eval_password` command output "
|
||||
"(%s).",
|
||||
strerror(errno));
|
||||
return FALSE;
|
||||
};
|
||||
|
||||
// Remove leading and trailing whitespace from output.
|
||||
g_strstrip(account->password);
|
||||
if (!account->password) {
|
||||
log_error("Empty password returned by `eval_password` command.");
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
void
|
||||
|
||||
@@ -4,33 +4,7 @@
|
||||
*
|
||||
* Copyright (C) 2012 - 2019 James Booth <boothj5@gmail.com>
|
||||
*
|
||||
* This file is part of Profanity.
|
||||
*
|
||||
* Profanity is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Profanity is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Profanity. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* In addition, as a special exception, the copyright holders give permission to
|
||||
* link the code of portions of this program with the OpenSSL library under
|
||||
* certain conditions as described in each individual source file, and
|
||||
* distribute linked combinations including the two.
|
||||
*
|
||||
* You must obey the GNU General Public License in all respects for all of the
|
||||
* code used other than OpenSSL. If you modify file(s) with this exception, you
|
||||
* may extend this exception to your version of the file(s), but you are not
|
||||
* obligated to do so. If you do not wish to do so, delete this exception
|
||||
* statement from your version. If you delete this exception statement from all
|
||||
* source files in the program, then also delete it here.
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later WITH OpenSSL-exception
|
||||
*/
|
||||
|
||||
#ifndef CONFIG_ACCOUNT_H
|
||||
@@ -84,7 +58,7 @@ ProfAccount* account_new(gchar* name, gchar* jid, gchar* password, gchar* eval_p
|
||||
GList* ox_enabled, GList* pgp_enabled, gchar* pgp_keyid,
|
||||
gchar* startscript, gchar* theme, gchar* tls_policy, gchar* auth_policy,
|
||||
gchar* client, int max_sessions);
|
||||
char* account_create_connect_jid(ProfAccount* account);
|
||||
gchar* account_create_connect_jid(ProfAccount* account);
|
||||
gboolean account_eval_password(ProfAccount* account);
|
||||
void account_free(ProfAccount* account);
|
||||
void account_set_server(ProfAccount* account, const char* server);
|
||||
|
||||
@@ -3,35 +3,9 @@
|
||||
* vim: expandtab:ts=4:sts=4:sw=4
|
||||
*
|
||||
* Copyright (C) 2012 - 2019 James Booth <boothj5@gmail.com>
|
||||
* Copyright (C) 2019 - 2025 Michael Vetter <jubalh@iodoru.org>
|
||||
*
|
||||
* This file is part of Profanity.
|
||||
*
|
||||
* Profanity is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Profanity is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Profanity. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* In addition, as a special exception, the copyright holders give permission to
|
||||
* link the code of portions of this program with the OpenSSL library under
|
||||
* certain conditions as described in each individual source file, and
|
||||
* distribute linked combinations including the two.
|
||||
*
|
||||
* You must obey the GNU General Public License in all respects for all of the
|
||||
* code used other than OpenSSL. If you modify file(s) with this exception, you
|
||||
* may extend this exception to your version of the file(s), but you are not
|
||||
* obligated to do so. If you do not wish to do so, delete this exception
|
||||
* statement from your version. If you delete this exception statement from all
|
||||
* source files in the program, then also delete it here.
|
||||
* Copyright (C) 2019 - 2026 Michael Vetter <jubalh@iodoru.org>
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later WITH OpenSSL-exception
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
@@ -59,7 +33,74 @@ static GKeyFile* accounts;
|
||||
static Autocomplete all_ac;
|
||||
static Autocomplete enabled_ac;
|
||||
|
||||
static void _save_accounts(void);
|
||||
static gchar*
|
||||
_sanitize_account_name(const char* const name)
|
||||
{
|
||||
if (!name) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
gchar* sanitized = g_strdup(name);
|
||||
gchar* p = sanitized;
|
||||
while (*p) {
|
||||
// GKeyFile group header forbids these characters.
|
||||
if (*p == '[' || *p == ']' || *p == '\n' || *p == '\r') {
|
||||
*p = '_';
|
||||
}
|
||||
p++;
|
||||
}
|
||||
|
||||
return sanitized;
|
||||
}
|
||||
|
||||
static gboolean
|
||||
_accounts_has_group(const char* account_name)
|
||||
{
|
||||
if (!account_name || !accounts)
|
||||
return FALSE;
|
||||
|
||||
auto_gchar gchar* sanitized = _sanitize_account_name(account_name);
|
||||
return g_key_file_has_group(accounts, sanitized);
|
||||
}
|
||||
|
||||
static void
|
||||
_accounts_save(const char* account_name)
|
||||
{
|
||||
prof_keyfile_t current;
|
||||
|
||||
if (!load_data_keyfile(¤t, FILE_ACCOUNTS)) {
|
||||
log_error("Could not load accounts");
|
||||
return;
|
||||
}
|
||||
|
||||
auto_gchar gchar* sanitized = _sanitize_account_name(account_name);
|
||||
|
||||
if (_accounts_has_group(sanitized)) {
|
||||
// Remove keys from file that are no longer in memory
|
||||
gsize nkeys_disk;
|
||||
auto_gcharv gchar** keys_disk = g_key_file_get_keys(current.keyfile, sanitized, &nkeys_disk, NULL);
|
||||
if (keys_disk) {
|
||||
for (gsize j = 0; j < nkeys_disk; ++j) {
|
||||
if (!g_key_file_has_key(accounts_prof_keyfile.keyfile, sanitized, keys_disk[j], NULL)) {
|
||||
g_key_file_remove_key(current.keyfile, sanitized, keys_disk[j], NULL);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
gsize nkeys;
|
||||
auto_gcharv gchar** keys = g_key_file_get_keys(accounts_prof_keyfile.keyfile, sanitized, &nkeys, NULL);
|
||||
if (keys) {
|
||||
for (gsize j = 0; j < nkeys; ++j) {
|
||||
auto_gchar gchar* new_value = g_key_file_get_value(accounts_prof_keyfile.keyfile, sanitized, keys[j], NULL);
|
||||
g_key_file_set_value(current.keyfile, sanitized, keys[j], new_value);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
g_key_file_remove_group(current.keyfile, sanitized, NULL);
|
||||
}
|
||||
save_keyfile(¤t);
|
||||
free_keyfile(¤t);
|
||||
}
|
||||
|
||||
static void
|
||||
_accounts_close(void)
|
||||
@@ -79,7 +120,9 @@ accounts_load(void)
|
||||
|
||||
all_ac = autocomplete_new();
|
||||
enabled_ac = autocomplete_new();
|
||||
load_data_keyfile(&accounts_prof_keyfile, FILE_ACCOUNTS);
|
||||
if (!load_data_keyfile(&accounts_prof_keyfile, FILE_ACCOUNTS)) {
|
||||
log_error("Could not load accounts");
|
||||
}
|
||||
accounts = accounts_prof_keyfile.keyfile;
|
||||
|
||||
// create the logins searchable list for autocompletion
|
||||
@@ -132,54 +175,58 @@ accounts_add(const char* account_name, const char* altdomain, const int port, co
|
||||
}
|
||||
}
|
||||
|
||||
if (g_key_file_has_group(accounts, account_name)) {
|
||||
auto_gchar gchar* sanitized_account_name = _sanitize_account_name(account_name);
|
||||
|
||||
if (_accounts_has_group(sanitized_account_name)) {
|
||||
log_error("Can't add account \"%s\", it already exists.", account_name);
|
||||
return;
|
||||
}
|
||||
|
||||
g_key_file_set_boolean(accounts, account_name, "enabled", TRUE);
|
||||
g_key_file_set_string(accounts, account_name, "jid", barejid);
|
||||
g_key_file_set_string(accounts, account_name, "resource", resource);
|
||||
g_key_file_set_boolean(accounts, sanitized_account_name, "enabled", TRUE);
|
||||
g_key_file_set_string(accounts, sanitized_account_name, "jid", barejid);
|
||||
g_key_file_set_string(accounts, sanitized_account_name, "resource", resource);
|
||||
if (altdomain) {
|
||||
g_key_file_set_string(accounts, account_name, "server", altdomain);
|
||||
g_key_file_set_string(accounts, sanitized_account_name, "server", altdomain);
|
||||
}
|
||||
if (port != 0) {
|
||||
g_key_file_set_integer(accounts, account_name, "port", port);
|
||||
g_key_file_set_integer(accounts, sanitized_account_name, "port", port);
|
||||
}
|
||||
if (tls_policy) {
|
||||
g_key_file_set_string(accounts, account_name, "tls.policy", tls_policy);
|
||||
g_key_file_set_string(accounts, sanitized_account_name, "tls.policy", tls_policy);
|
||||
}
|
||||
if (auth_policy) {
|
||||
g_key_file_set_string(accounts, account_name, "auth.policy", auth_policy);
|
||||
g_key_file_set_string(accounts, sanitized_account_name, "auth.policy", auth_policy);
|
||||
}
|
||||
|
||||
auto_jid Jid* jidp = jid_create(barejid);
|
||||
|
||||
if (jidp->localpart == NULL) {
|
||||
g_key_file_set_string(accounts, account_name, "muc.nick", jidp->domainpart);
|
||||
g_key_file_set_string(accounts, sanitized_account_name, "muc.nick", jidp->domainpart);
|
||||
} else {
|
||||
g_key_file_set_string(accounts, account_name, "muc.nick", jidp->localpart);
|
||||
g_key_file_set_string(accounts, sanitized_account_name, "muc.nick", jidp->localpart);
|
||||
}
|
||||
|
||||
g_key_file_set_string(accounts, account_name, "presence.last", "online");
|
||||
g_key_file_set_string(accounts, account_name, "presence.login", "online");
|
||||
g_key_file_set_integer(accounts, account_name, "priority.online", 0);
|
||||
g_key_file_set_integer(accounts, account_name, "priority.chat", 0);
|
||||
g_key_file_set_integer(accounts, account_name, "priority.away", 0);
|
||||
g_key_file_set_integer(accounts, account_name, "priority.xa", 0);
|
||||
g_key_file_set_integer(accounts, account_name, "priority.dnd", 0);
|
||||
g_key_file_set_string(accounts, sanitized_account_name, "presence.last", "online");
|
||||
g_key_file_set_string(accounts, sanitized_account_name, "presence.login", "online");
|
||||
g_key_file_set_integer(accounts, sanitized_account_name, "priority.online", 0);
|
||||
g_key_file_set_integer(accounts, sanitized_account_name, "priority.chat", 0);
|
||||
g_key_file_set_integer(accounts, sanitized_account_name, "priority.away", 0);
|
||||
g_key_file_set_integer(accounts, sanitized_account_name, "priority.xa", 0);
|
||||
g_key_file_set_integer(accounts, sanitized_account_name, "priority.dnd", 0);
|
||||
|
||||
_save_accounts();
|
||||
autocomplete_add(all_ac, account_name);
|
||||
autocomplete_add(enabled_ac, account_name);
|
||||
_accounts_save(account_name);
|
||||
autocomplete_add(all_ac, sanitized_account_name);
|
||||
autocomplete_add(enabled_ac, sanitized_account_name);
|
||||
}
|
||||
|
||||
int
|
||||
gboolean
|
||||
accounts_remove(const char* account_name)
|
||||
{
|
||||
int r = g_key_file_remove_group(accounts, account_name, NULL);
|
||||
_save_accounts();
|
||||
autocomplete_remove(all_ac, account_name);
|
||||
autocomplete_remove(enabled_ac, account_name);
|
||||
auto_gchar gchar* sanitized_account_name = _sanitize_account_name(account_name);
|
||||
gboolean r = g_key_file_remove_group(accounts, sanitized_account_name, NULL);
|
||||
_accounts_save(account_name);
|
||||
autocomplete_remove(all_ac, sanitized_account_name);
|
||||
autocomplete_remove(enabled_ac, sanitized_account_name);
|
||||
return r;
|
||||
}
|
||||
|
||||
@@ -189,167 +236,123 @@ accounts_get_list(void)
|
||||
return g_key_file_get_groups(accounts, NULL);
|
||||
}
|
||||
|
||||
ProfAccount*
|
||||
accounts_get_account(const char* const name)
|
||||
static GList*
|
||||
_g_strv_to_glist(gchar** in, gsize length)
|
||||
{
|
||||
if (!g_key_file_has_group(accounts, name)) {
|
||||
if (in == NULL)
|
||||
return NULL;
|
||||
GList* out = NULL;
|
||||
for (gsize i = 0; i < length; i++) {
|
||||
out = g_list_append(out, in[i]);
|
||||
}
|
||||
g_free(in);
|
||||
return out;
|
||||
}
|
||||
|
||||
static GList*
|
||||
_accounts_get_glist(const gchar* sanitized_account_name,
|
||||
const gchar* key)
|
||||
{
|
||||
gsize length = 0;
|
||||
gchar** list = g_key_file_get_string_list(accounts, sanitized_account_name, key, &length, NULL);
|
||||
return _g_strv_to_glist(list, length);
|
||||
}
|
||||
|
||||
ProfAccount*
|
||||
accounts_get_account(const char* const account_name)
|
||||
{
|
||||
auto_gchar gchar* sanitized_account_name = _sanitize_account_name(account_name);
|
||||
if (!_accounts_has_group(sanitized_account_name)) {
|
||||
return NULL;
|
||||
} else {
|
||||
gchar* jid = g_key_file_get_string(accounts, name, "jid", NULL);
|
||||
gchar* jid = g_key_file_get_string(accounts, sanitized_account_name, "jid", NULL);
|
||||
|
||||
// fix accounts that have no jid property by setting to name
|
||||
// fix accounts that have no jid property by setting to account_name
|
||||
if (jid == NULL) {
|
||||
g_key_file_set_string(accounts, name, "jid", name);
|
||||
_save_accounts();
|
||||
g_key_file_set_string(accounts, sanitized_account_name, "jid", account_name);
|
||||
_accounts_save(account_name);
|
||||
}
|
||||
|
||||
gchar* password = g_key_file_get_string(accounts, name, "password", NULL);
|
||||
gchar* eval_password = g_key_file_get_string(accounts, name, "eval_password", NULL);
|
||||
gboolean enabled = g_key_file_get_boolean(accounts, name, "enabled", NULL);
|
||||
gchar* password = g_key_file_get_string(accounts, sanitized_account_name, "password", NULL);
|
||||
gchar* eval_password = g_key_file_get_string(accounts, sanitized_account_name, "eval_password", NULL);
|
||||
gboolean enabled = g_key_file_get_boolean(accounts, sanitized_account_name, "enabled", NULL);
|
||||
|
||||
gchar* server = g_key_file_get_string(accounts, name, "server", NULL);
|
||||
gchar* resource = g_key_file_get_string(accounts, name, "resource", NULL);
|
||||
int port = g_key_file_get_integer(accounts, name, "port", NULL);
|
||||
gchar* server = g_key_file_get_string(accounts, sanitized_account_name, "server", NULL);
|
||||
gchar* resource = g_key_file_get_string(accounts, sanitized_account_name, "resource", NULL);
|
||||
int port = g_key_file_get_integer(accounts, sanitized_account_name, "port", NULL);
|
||||
|
||||
gchar* last_presence = g_key_file_get_string(accounts, name, "presence.last", NULL);
|
||||
gchar* login_presence = g_key_file_get_string(accounts, name, "presence.login", NULL);
|
||||
gchar* last_presence = g_key_file_get_string(accounts, sanitized_account_name, "presence.last", NULL);
|
||||
gchar* login_presence = g_key_file_get_string(accounts, sanitized_account_name, "presence.login", NULL);
|
||||
|
||||
int priority_online = g_key_file_get_integer(accounts, name, "priority.online", NULL);
|
||||
int priority_chat = g_key_file_get_integer(accounts, name, "priority.chat", NULL);
|
||||
int priority_away = g_key_file_get_integer(accounts, name, "priority.away", NULL);
|
||||
int priority_xa = g_key_file_get_integer(accounts, name, "priority.xa", NULL);
|
||||
int priority_dnd = g_key_file_get_integer(accounts, name, "priority.dnd", NULL);
|
||||
int priority_online = g_key_file_get_integer(accounts, sanitized_account_name, "priority.online", NULL);
|
||||
int priority_chat = g_key_file_get_integer(accounts, sanitized_account_name, "priority.chat", NULL);
|
||||
int priority_away = g_key_file_get_integer(accounts, sanitized_account_name, "priority.away", NULL);
|
||||
int priority_xa = g_key_file_get_integer(accounts, sanitized_account_name, "priority.xa", NULL);
|
||||
int priority_dnd = g_key_file_get_integer(accounts, sanitized_account_name, "priority.dnd", NULL);
|
||||
|
||||
gchar* muc_service = NULL;
|
||||
if (g_key_file_has_key(accounts, name, "muc.service", NULL)) {
|
||||
muc_service = g_key_file_get_string(accounts, name, "muc.service", NULL);
|
||||
if (g_key_file_has_key(accounts, sanitized_account_name, "muc.service", NULL)) {
|
||||
muc_service = g_key_file_get_string(accounts, sanitized_account_name, "muc.service", NULL);
|
||||
} else {
|
||||
jabber_conn_status_t conn_status = connection_get_status();
|
||||
if (conn_status == JABBER_CONNECTED) {
|
||||
const char* conf_jid = connection_jid_for_feature(XMPP_FEATURE_MUC);
|
||||
if (conf_jid) {
|
||||
muc_service = strdup(conf_jid);
|
||||
muc_service = g_strdup(conf_jid);
|
||||
}
|
||||
}
|
||||
}
|
||||
gchar* muc_nick = g_key_file_get_string(accounts, name, "muc.nick", NULL);
|
||||
gchar* muc_nick = g_key_file_get_string(accounts, sanitized_account_name, "muc.nick", NULL);
|
||||
|
||||
gchar* otr_policy = NULL;
|
||||
if (g_key_file_has_key(accounts, name, "otr.policy", NULL)) {
|
||||
otr_policy = g_key_file_get_string(accounts, name, "otr.policy", NULL);
|
||||
}
|
||||
gchar* otr_policy = g_key_file_get_string(accounts, sanitized_account_name, "otr.policy", NULL);
|
||||
GList* otr_manual = _accounts_get_glist(sanitized_account_name, "otr.manual");
|
||||
GList* otr_opportunistic = _accounts_get_glist(sanitized_account_name, "otr.opportunistic");
|
||||
GList* otr_always = _accounts_get_glist(sanitized_account_name, "otr.always");
|
||||
|
||||
gsize length;
|
||||
GList* otr_manual = NULL;
|
||||
auto_gcharv gchar** manual = g_key_file_get_string_list(accounts, name, "otr.manual", &length, NULL);
|
||||
if (manual) {
|
||||
for (int i = 0; i < length; i++) {
|
||||
otr_manual = g_list_append(otr_manual, strdup(manual[i]));
|
||||
}
|
||||
}
|
||||
gchar* omemo_policy = g_key_file_get_string(accounts, sanitized_account_name, "omemo.policy", NULL);
|
||||
GList* omemo_enabled = _accounts_get_glist(sanitized_account_name, "omemo.enabled");
|
||||
GList* omemo_disabled = _accounts_get_glist(sanitized_account_name, "omemo.disabled");
|
||||
|
||||
GList* otr_opportunistic = NULL;
|
||||
auto_gcharv gchar** opportunistic = g_key_file_get_string_list(accounts, name, "otr.opportunistic", &length, NULL);
|
||||
if (opportunistic) {
|
||||
for (int i = 0; i < length; i++) {
|
||||
otr_opportunistic = g_list_append(otr_opportunistic, strdup(opportunistic[i]));
|
||||
}
|
||||
}
|
||||
GList* ox_enabled = _accounts_get_glist(sanitized_account_name, "ox.enabled");
|
||||
|
||||
GList* otr_always = NULL;
|
||||
auto_gcharv gchar** always = g_key_file_get_string_list(accounts, name, "otr.always", &length, NULL);
|
||||
if (always) {
|
||||
for (int i = 0; i < length; i++) {
|
||||
otr_always = g_list_append(otr_always, strdup(always[i]));
|
||||
}
|
||||
}
|
||||
GList* pgp_enabled = _accounts_get_glist(sanitized_account_name, "pgp.enabled");
|
||||
|
||||
gchar* omemo_policy = NULL;
|
||||
if (g_key_file_has_key(accounts, name, "omemo.policy", NULL)) {
|
||||
omemo_policy = g_key_file_get_string(accounts, name, "omemo.policy", NULL);
|
||||
}
|
||||
gchar* pgp_keyid = g_key_file_get_string(accounts, sanitized_account_name, "pgp.keyid", NULL);
|
||||
|
||||
GList* omemo_enabled = NULL;
|
||||
auto_gcharv gchar** omemo_enabled_list = g_key_file_get_string_list(accounts, name, "omemo.enabled", &length, NULL);
|
||||
if (omemo_enabled_list) {
|
||||
for (int i = 0; i < length; i++) {
|
||||
omemo_enabled = g_list_append(omemo_enabled, strdup(omemo_enabled_list[i]));
|
||||
}
|
||||
}
|
||||
gchar* startscript = g_key_file_get_string(accounts, sanitized_account_name, "script.start", NULL);
|
||||
|
||||
GList* omemo_disabled = NULL;
|
||||
auto_gcharv gchar** omemo_disabled_list = g_key_file_get_string_list(accounts, name, "omemo.disabled", &length, NULL);
|
||||
if (omemo_disabled_list) {
|
||||
for (int i = 0; i < length; i++) {
|
||||
omemo_disabled = g_list_append(omemo_disabled, strdup(omemo_disabled_list[i]));
|
||||
}
|
||||
}
|
||||
gchar* client = g_key_file_get_string(accounts, sanitized_account_name, "client.account_name", NULL);
|
||||
|
||||
GList* ox_enabled = NULL;
|
||||
auto_gcharv gchar** ox_enabled_list = g_key_file_get_string_list(accounts, name, "ox.enabled", &length, NULL);
|
||||
if (ox_enabled_list) {
|
||||
for (int i = 0; i < length; i++) {
|
||||
ox_enabled = g_list_append(ox_enabled, strdup(ox_enabled_list[i]));
|
||||
}
|
||||
}
|
||||
gchar* theme = g_key_file_get_string(accounts, sanitized_account_name, "theme", NULL);
|
||||
|
||||
GList* pgp_enabled = NULL;
|
||||
auto_gcharv gchar** pgp_enabled_list = g_key_file_get_string_list(accounts, name, "pgp.enabled", &length, NULL);
|
||||
if (pgp_enabled_list) {
|
||||
for (int i = 0; i < length; i++) {
|
||||
pgp_enabled = g_list_append(pgp_enabled, strdup(pgp_enabled_list[i]));
|
||||
}
|
||||
}
|
||||
|
||||
gchar* pgp_keyid = NULL;
|
||||
if (g_key_file_has_key(accounts, name, "pgp.keyid", NULL)) {
|
||||
pgp_keyid = g_key_file_get_string(accounts, name, "pgp.keyid", NULL);
|
||||
}
|
||||
|
||||
gchar* startscript = NULL;
|
||||
if (g_key_file_has_key(accounts, name, "script.start", NULL)) {
|
||||
startscript = g_key_file_get_string(accounts, name, "script.start", NULL);
|
||||
}
|
||||
|
||||
gchar* client = NULL;
|
||||
if (g_key_file_has_key(accounts, name, "client.name", NULL)) {
|
||||
client = g_key_file_get_string(accounts, name, "client.name", NULL);
|
||||
}
|
||||
|
||||
gchar* theme = NULL;
|
||||
if (g_key_file_has_key(accounts, name, "theme", NULL)) {
|
||||
theme = g_key_file_get_string(accounts, name, "theme", NULL);
|
||||
}
|
||||
|
||||
gchar* tls_policy = g_key_file_get_string(accounts, name, "tls.policy", NULL);
|
||||
if (tls_policy && ((g_strcmp0(tls_policy, "force") != 0) && (g_strcmp0(tls_policy, "allow") != 0) && (g_strcmp0(tls_policy, "trust") != 0) && (g_strcmp0(tls_policy, "disable") != 0) && (g_strcmp0(tls_policy, "legacy") != 0))) {
|
||||
gchar* tls_policy = g_key_file_get_string(accounts, sanitized_account_name, "tls.policy", NULL);
|
||||
if (tls_policy && !valid_tls_policy_option(tls_policy)) {
|
||||
g_free(tls_policy);
|
||||
tls_policy = NULL;
|
||||
}
|
||||
|
||||
gchar* auth_policy = g_key_file_get_string(accounts, name, "auth.policy", NULL);
|
||||
gchar* auth_policy = g_key_file_get_string(accounts, sanitized_account_name, "auth.policy", NULL);
|
||||
|
||||
int max_sessions = g_key_file_get_integer(accounts, name, "max.sessions", 0);
|
||||
int max_sessions = g_key_file_get_integer(accounts, sanitized_account_name, "max.sessions", 0);
|
||||
|
||||
ProfAccount* new_account = account_new(g_strdup(name), jid, password, eval_password, enabled,
|
||||
server, port, resource, last_presence, login_presence,
|
||||
priority_online, priority_chat, priority_away, priority_xa,
|
||||
priority_dnd, muc_service, muc_nick, otr_policy, otr_manual,
|
||||
otr_opportunistic, otr_always, omemo_policy, omemo_enabled,
|
||||
omemo_disabled, ox_enabled, pgp_enabled, pgp_keyid,
|
||||
startscript, theme, tls_policy, auth_policy, client, max_sessions);
|
||||
|
||||
return new_account;
|
||||
return account_new(g_strdup(account_name), jid, password, eval_password, enabled,
|
||||
server, port, resource, last_presence, login_presence,
|
||||
priority_online, priority_chat, priority_away, priority_xa,
|
||||
priority_dnd, muc_service, muc_nick, otr_policy, otr_manual,
|
||||
otr_opportunistic, otr_always, omemo_policy, omemo_enabled,
|
||||
omemo_disabled, ox_enabled, pgp_enabled, pgp_keyid,
|
||||
startscript, theme, tls_policy, auth_policy, client, max_sessions);
|
||||
}
|
||||
}
|
||||
|
||||
gboolean
|
||||
accounts_enable(const char* const name)
|
||||
accounts_enable(const char* const account_name)
|
||||
{
|
||||
if (g_key_file_has_group(accounts, name)) {
|
||||
g_key_file_set_boolean(accounts, name, "enabled", TRUE);
|
||||
_save_accounts();
|
||||
autocomplete_add(enabled_ac, name);
|
||||
auto_gchar gchar* sanitized_account_name = _sanitize_account_name(account_name);
|
||||
if (_accounts_has_group(sanitized_account_name)) {
|
||||
g_key_file_set_boolean(accounts, sanitized_account_name, "enabled", TRUE);
|
||||
_accounts_save(account_name);
|
||||
autocomplete_add(enabled_ac, sanitized_account_name);
|
||||
return TRUE;
|
||||
} else {
|
||||
return FALSE;
|
||||
@@ -357,12 +360,13 @@ accounts_enable(const char* const name)
|
||||
}
|
||||
|
||||
gboolean
|
||||
accounts_disable(const char* const name)
|
||||
accounts_disable(const char* const account_name)
|
||||
{
|
||||
if (g_key_file_has_group(accounts, name)) {
|
||||
g_key_file_set_boolean(accounts, name, "enabled", FALSE);
|
||||
_save_accounts();
|
||||
autocomplete_remove(enabled_ac, name);
|
||||
auto_gchar gchar* sanitized_account_name = _sanitize_account_name(account_name);
|
||||
if (_accounts_has_group(sanitized_account_name)) {
|
||||
g_key_file_set_boolean(accounts, sanitized_account_name, "enabled", FALSE);
|
||||
_accounts_save(account_name);
|
||||
autocomplete_remove(enabled_ac, sanitized_account_name);
|
||||
return TRUE;
|
||||
} else {
|
||||
return FALSE;
|
||||
@@ -372,63 +376,33 @@ accounts_disable(const char* const name)
|
||||
gboolean
|
||||
accounts_rename(const char* const account_name, const char* const new_name)
|
||||
{
|
||||
if (g_key_file_has_group(accounts, new_name)) {
|
||||
auto_gchar gchar* sanitized_account_name = _sanitize_account_name(account_name);
|
||||
auto_gchar gchar* sanitized_new_account_name = _sanitize_account_name(new_name);
|
||||
|
||||
if (_accounts_has_group(sanitized_new_account_name)) {
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
if (!g_key_file_has_group(accounts, account_name)) {
|
||||
if (!_accounts_has_group(sanitized_account_name)) {
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
// treat all properties as strings for copy
|
||||
gchar* string_keys[] = {
|
||||
"enabled",
|
||||
"jid",
|
||||
"server",
|
||||
"port",
|
||||
"resource",
|
||||
"password",
|
||||
"eval_password",
|
||||
"presence.last",
|
||||
"presence.laststatus",
|
||||
"presence.login",
|
||||
"priority.online",
|
||||
"priority.chat",
|
||||
"priority.away",
|
||||
"priority.xa",
|
||||
"priority.dnd",
|
||||
"muc.service",
|
||||
"muc.nick",
|
||||
"otr.policy",
|
||||
"otr.manual",
|
||||
"otr.opportunistic",
|
||||
"otr.always",
|
||||
"omemo.policy",
|
||||
"omemo.enabled",
|
||||
"omemo.disabled",
|
||||
"ox.enabled",
|
||||
"pgp.enabled",
|
||||
"pgp.keyid",
|
||||
"last.activity",
|
||||
"script.start",
|
||||
"tls.policy"
|
||||
};
|
||||
|
||||
for (int i = 0; i < ARRAY_SIZE(string_keys); i++) {
|
||||
auto_gchar gchar* value = g_key_file_get_string(accounts, account_name, string_keys[i], NULL);
|
||||
if (value) {
|
||||
g_key_file_set_string(accounts, new_name, string_keys[i], value);
|
||||
}
|
||||
gsize nkeys;
|
||||
auto_gcharv gchar** keys = g_key_file_get_keys(accounts, sanitized_account_name, &nkeys, NULL);
|
||||
for (gsize i = 0; i < nkeys; i++) {
|
||||
auto_gchar gchar* new_value = g_key_file_get_value(accounts, sanitized_account_name, keys[i], NULL);
|
||||
g_key_file_set_value(accounts, sanitized_new_account_name, keys[i], new_value);
|
||||
}
|
||||
|
||||
g_key_file_remove_group(accounts, account_name, NULL);
|
||||
_save_accounts();
|
||||
g_key_file_remove_group(accounts, sanitized_account_name, NULL);
|
||||
_accounts_save(account_name);
|
||||
_accounts_save(new_name);
|
||||
|
||||
autocomplete_remove(all_ac, account_name);
|
||||
autocomplete_add(all_ac, new_name);
|
||||
if (g_key_file_get_boolean(accounts, new_name, "enabled", NULL)) {
|
||||
autocomplete_remove(enabled_ac, account_name);
|
||||
autocomplete_add(enabled_ac, new_name);
|
||||
autocomplete_remove(all_ac, sanitized_account_name);
|
||||
autocomplete_remove(enabled_ac, sanitized_account_name);
|
||||
autocomplete_add(all_ac, sanitized_new_account_name);
|
||||
if (g_key_file_get_boolean(accounts, sanitized_new_account_name, "enabled", NULL)) {
|
||||
autocomplete_add(enabled_ac, sanitized_new_account_name);
|
||||
}
|
||||
|
||||
return TRUE;
|
||||
@@ -437,7 +411,7 @@ accounts_rename(const char* const account_name, const char* const new_name)
|
||||
gboolean
|
||||
accounts_account_exists(const char* const account_name)
|
||||
{
|
||||
return g_key_file_has_group(accounts, account_name);
|
||||
return _accounts_has_group(account_name);
|
||||
}
|
||||
|
||||
void
|
||||
@@ -445,19 +419,20 @@ accounts_set_jid(const char* const account_name, const char* const value)
|
||||
{
|
||||
auto_jid Jid* jid = jid_create(value);
|
||||
if (jid) {
|
||||
if (accounts_account_exists(account_name)) {
|
||||
g_key_file_set_string(accounts, account_name, "jid", jid->barejid);
|
||||
auto_gchar gchar* sanitized_account_name = _sanitize_account_name(account_name);
|
||||
if (_accounts_has_group(sanitized_account_name)) {
|
||||
g_key_file_set_string(accounts, sanitized_account_name, "jid", jid->barejid);
|
||||
if (jid->resourcepart) {
|
||||
g_key_file_set_string(accounts, account_name, "resource", jid->resourcepart);
|
||||
g_key_file_set_string(accounts, sanitized_account_name, "resource", jid->resourcepart);
|
||||
}
|
||||
|
||||
if (jid->localpart == NULL) {
|
||||
g_key_file_set_string(accounts, account_name, "muc.nick", jid->domainpart);
|
||||
g_key_file_set_string(accounts, sanitized_account_name, "muc.nick", jid->domainpart);
|
||||
} else {
|
||||
g_key_file_set_string(accounts, account_name, "muc.nick", jid->localpart);
|
||||
g_key_file_set_string(accounts, sanitized_account_name, "muc.nick", jid->localpart);
|
||||
}
|
||||
|
||||
_save_accounts();
|
||||
_accounts_save(account_name);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -465,9 +440,10 @@ accounts_set_jid(const char* const account_name, const char* const value)
|
||||
void
|
||||
accounts_set_server(const char* const account_name, const char* const value)
|
||||
{
|
||||
if (accounts_account_exists(account_name)) {
|
||||
g_key_file_set_string(accounts, account_name, "server", value);
|
||||
_save_accounts();
|
||||
auto_gchar gchar* sanitized_account_name = _sanitize_account_name(account_name);
|
||||
if (_accounts_has_group(sanitized_account_name)) {
|
||||
g_key_file_set_string(accounts, sanitized_account_name, "server", value);
|
||||
_accounts_save(account_name);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -475,35 +451,41 @@ void
|
||||
accounts_set_port(const char* const account_name, const int value)
|
||||
{
|
||||
if (value != 0) {
|
||||
g_key_file_set_integer(accounts, account_name, "port", value);
|
||||
_save_accounts();
|
||||
auto_gchar gchar* sanitized_account_name = _sanitize_account_name(account_name);
|
||||
if (_accounts_has_group(sanitized_account_name)) {
|
||||
g_key_file_set_integer(accounts, sanitized_account_name, "port", value);
|
||||
_accounts_save(account_name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void
|
||||
_accounts_set_string_option(const char* account_name, const char* const option, const char* const value)
|
||||
{
|
||||
if (accounts_account_exists(account_name)) {
|
||||
g_key_file_set_string(accounts, account_name, option, value ?: "");
|
||||
_save_accounts();
|
||||
auto_gchar gchar* sanitized_account_name = _sanitize_account_name(account_name);
|
||||
if (_accounts_has_group(sanitized_account_name)) {
|
||||
g_key_file_set_string(accounts, sanitized_account_name, option, value ?: "");
|
||||
_accounts_save(account_name);
|
||||
}
|
||||
}
|
||||
|
||||
static void
|
||||
_accounts_set_int_option(const char* account_name, const char* const option, int value)
|
||||
{
|
||||
if (accounts_account_exists(account_name)) {
|
||||
g_key_file_set_integer(accounts, account_name, option, value);
|
||||
_save_accounts();
|
||||
auto_gchar gchar* sanitized_account_name = _sanitize_account_name(account_name);
|
||||
if (_accounts_has_group(sanitized_account_name)) {
|
||||
g_key_file_set_integer(accounts, sanitized_account_name, option, value);
|
||||
_accounts_save(account_name);
|
||||
}
|
||||
}
|
||||
|
||||
static void
|
||||
_accounts_clear_string_option(const char* account_name, const char* const option)
|
||||
{
|
||||
if (accounts_account_exists(account_name)) {
|
||||
g_key_file_remove_key(accounts, account_name, option, NULL);
|
||||
_save_accounts();
|
||||
auto_gchar gchar* sanitized_account_name = _sanitize_account_name(account_name);
|
||||
if (_accounts_has_group(sanitized_account_name)) {
|
||||
g_key_file_remove_key(accounts, sanitized_account_name, option, NULL);
|
||||
_accounts_save(account_name);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -630,83 +612,88 @@ accounts_clear_max_sessions(const char* const account_name)
|
||||
void
|
||||
accounts_add_otr_policy(const char* const account_name, const char* const contact_jid, const char* const policy)
|
||||
{
|
||||
if (accounts_account_exists(account_name)) {
|
||||
auto_gchar gchar* sanitized_account_name = _sanitize_account_name(account_name);
|
||||
if (_accounts_has_group(sanitized_account_name)) {
|
||||
GString* key = g_string_new("otr.");
|
||||
g_string_append(key, policy);
|
||||
conf_string_list_add(accounts, account_name, key->str, contact_jid);
|
||||
conf_string_list_add(accounts, sanitized_account_name, key->str, contact_jid);
|
||||
g_string_free(key, TRUE);
|
||||
|
||||
// check for and remove from other lists
|
||||
if (strcmp(policy, "manual") == 0) {
|
||||
conf_string_list_remove(accounts, account_name, "otr.opportunistic", contact_jid);
|
||||
conf_string_list_remove(accounts, account_name, "otr.always", contact_jid);
|
||||
conf_string_list_remove(accounts, sanitized_account_name, "otr.opportunistic", contact_jid);
|
||||
conf_string_list_remove(accounts, sanitized_account_name, "otr.always", contact_jid);
|
||||
}
|
||||
if (strcmp(policy, "opportunistic") == 0) {
|
||||
conf_string_list_remove(accounts, account_name, "otr.manual", contact_jid);
|
||||
conf_string_list_remove(accounts, account_name, "otr.always", contact_jid);
|
||||
conf_string_list_remove(accounts, sanitized_account_name, "otr.manual", contact_jid);
|
||||
conf_string_list_remove(accounts, sanitized_account_name, "otr.always", contact_jid);
|
||||
}
|
||||
if (strcmp(policy, "always") == 0) {
|
||||
conf_string_list_remove(accounts, account_name, "otr.opportunistic", contact_jid);
|
||||
conf_string_list_remove(accounts, account_name, "otr.manual", contact_jid);
|
||||
conf_string_list_remove(accounts, sanitized_account_name, "otr.opportunistic", contact_jid);
|
||||
conf_string_list_remove(accounts, sanitized_account_name, "otr.manual", contact_jid);
|
||||
}
|
||||
|
||||
_save_accounts();
|
||||
_accounts_save(account_name);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
accounts_add_omemo_state(const char* const account_name, const char* const contact_jid, gboolean enabled)
|
||||
{
|
||||
if (accounts_account_exists(account_name)) {
|
||||
auto_gchar gchar* sanitized_account_name = _sanitize_account_name(account_name);
|
||||
if (_accounts_has_group(sanitized_account_name)) {
|
||||
if (enabled) {
|
||||
conf_string_list_add(accounts, account_name, "omemo.enabled", contact_jid);
|
||||
conf_string_list_remove(accounts, account_name, "omemo.disabled", contact_jid);
|
||||
conf_string_list_add(accounts, sanitized_account_name, "omemo.enabled", contact_jid);
|
||||
conf_string_list_remove(accounts, sanitized_account_name, "omemo.disabled", contact_jid);
|
||||
} else {
|
||||
conf_string_list_add(accounts, account_name, "omemo.disabled", contact_jid);
|
||||
conf_string_list_remove(accounts, account_name, "omemo.enabled", contact_jid);
|
||||
conf_string_list_add(accounts, sanitized_account_name, "omemo.disabled", contact_jid);
|
||||
conf_string_list_remove(accounts, sanitized_account_name, "omemo.enabled", contact_jid);
|
||||
}
|
||||
|
||||
_save_accounts();
|
||||
_accounts_save(account_name);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
accounts_add_ox_state(const char* const account_name, const char* const contact_jid, gboolean enabled)
|
||||
{
|
||||
if (accounts_account_exists(account_name)) {
|
||||
auto_gchar gchar* sanitized_account_name = _sanitize_account_name(account_name);
|
||||
if (_accounts_has_group(sanitized_account_name)) {
|
||||
if (enabled) {
|
||||
conf_string_list_add(accounts, account_name, "ox.enabled", contact_jid);
|
||||
conf_string_list_add(accounts, sanitized_account_name, "ox.enabled", contact_jid);
|
||||
} else {
|
||||
conf_string_list_remove(accounts, account_name, "ox.enabled", contact_jid);
|
||||
conf_string_list_remove(accounts, sanitized_account_name, "ox.enabled", contact_jid);
|
||||
}
|
||||
|
||||
_save_accounts();
|
||||
_accounts_save(account_name);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
accounts_add_pgp_state(const char* const account_name, const char* const contact_jid, gboolean enabled)
|
||||
{
|
||||
if (accounts_account_exists(account_name)) {
|
||||
auto_gchar gchar* sanitized_account_name = _sanitize_account_name(account_name);
|
||||
if (_accounts_has_group(sanitized_account_name)) {
|
||||
if (enabled) {
|
||||
conf_string_list_add(accounts, account_name, "pgp.enabled", contact_jid);
|
||||
conf_string_list_remove(accounts, account_name, "pgp.disabled", contact_jid);
|
||||
conf_string_list_add(accounts, sanitized_account_name, "pgp.enabled", contact_jid);
|
||||
conf_string_list_remove(accounts, sanitized_account_name, "pgp.disabled", contact_jid);
|
||||
} else {
|
||||
conf_string_list_add(accounts, account_name, "pgp.disabled", contact_jid);
|
||||
conf_string_list_remove(accounts, account_name, "pgp.enabled", contact_jid);
|
||||
conf_string_list_add(accounts, sanitized_account_name, "pgp.disabled", contact_jid);
|
||||
conf_string_list_remove(accounts, sanitized_account_name, "pgp.enabled", contact_jid);
|
||||
}
|
||||
|
||||
_save_accounts();
|
||||
_accounts_save(account_name);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
accounts_clear_omemo_state(const char* const account_name, const char* const contact_jid)
|
||||
{
|
||||
if (accounts_account_exists(account_name)) {
|
||||
conf_string_list_remove(accounts, account_name, "omemo.enabled", contact_jid);
|
||||
conf_string_list_remove(accounts, account_name, "omemo.disabled", contact_jid);
|
||||
_save_accounts();
|
||||
auto_gchar gchar* sanitized_account_name = _sanitize_account_name(account_name);
|
||||
if (_accounts_has_group(sanitized_account_name)) {
|
||||
conf_string_list_remove(accounts, sanitized_account_name, "omemo.enabled", contact_jid);
|
||||
conf_string_list_remove(accounts, sanitized_account_name, "omemo.disabled", contact_jid);
|
||||
_accounts_save(account_name);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -779,7 +766,8 @@ accounts_set_priority_dnd(const char* const account_name, const gint value)
|
||||
void
|
||||
accounts_set_priority_all(const char* const account_name, const gint value)
|
||||
{
|
||||
if (accounts_account_exists(account_name)) {
|
||||
auto_gchar gchar* sanitized_account_name = _sanitize_account_name(account_name);
|
||||
if (_accounts_has_group(sanitized_account_name)) {
|
||||
accounts_set_priority_online(account_name, value);
|
||||
accounts_set_priority_chat(account_name, value);
|
||||
accounts_set_priority_away(account_name, value);
|
||||
@@ -793,22 +781,23 @@ accounts_get_priority_for_presence_type(const char* const account_name,
|
||||
resource_presence_t presence_type)
|
||||
{
|
||||
gint result;
|
||||
auto_gchar gchar* sanitized_account_name = _sanitize_account_name(account_name);
|
||||
|
||||
switch (presence_type) {
|
||||
case (RESOURCE_ONLINE):
|
||||
result = g_key_file_get_integer(accounts, account_name, "priority.online", NULL);
|
||||
result = g_key_file_get_integer(accounts, sanitized_account_name, "priority.online", NULL);
|
||||
break;
|
||||
case (RESOURCE_CHAT):
|
||||
result = g_key_file_get_integer(accounts, account_name, "priority.chat", NULL);
|
||||
result = g_key_file_get_integer(accounts, sanitized_account_name, "priority.chat", NULL);
|
||||
break;
|
||||
case (RESOURCE_AWAY):
|
||||
result = g_key_file_get_integer(accounts, account_name, "priority.away", NULL);
|
||||
result = g_key_file_get_integer(accounts, sanitized_account_name, "priority.away", NULL);
|
||||
break;
|
||||
case (RESOURCE_XA):
|
||||
result = g_key_file_get_integer(accounts, account_name, "priority.xa", NULL);
|
||||
result = g_key_file_get_integer(accounts, sanitized_account_name, "priority.xa", NULL);
|
||||
break;
|
||||
default:
|
||||
result = g_key_file_get_integer(accounts, account_name, "priority.dnd", NULL);
|
||||
result = g_key_file_get_integer(accounts, sanitized_account_name, "priority.dnd", NULL);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -833,7 +822,8 @@ accounts_set_last_status(const char* const account_name, const char* const value
|
||||
void
|
||||
accounts_set_last_activity(const char* const account_name)
|
||||
{
|
||||
if (accounts_account_exists(account_name)) {
|
||||
auto_gchar gchar* sanitized_account_name = _sanitize_account_name(account_name);
|
||||
if (_accounts_has_group(sanitized_account_name)) {
|
||||
GDateTime* nowdt = g_date_time_new_now_utc();
|
||||
GTimeVal nowtv;
|
||||
gboolean res = g_date_time_to_timeval(nowdt, &nowtv);
|
||||
@@ -841,8 +831,8 @@ accounts_set_last_activity(const char* const account_name)
|
||||
|
||||
if (res) {
|
||||
auto_char char* timestr = g_time_val_to_iso8601(&nowtv);
|
||||
g_key_file_set_string(accounts, account_name, "last.activity", timestr);
|
||||
_save_accounts();
|
||||
g_key_file_set_string(accounts, sanitized_account_name, "last.activity", timestr);
|
||||
_accounts_save(account_name);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -850,8 +840,9 @@ accounts_set_last_activity(const char* const account_name)
|
||||
gchar*
|
||||
accounts_get_last_activity(const char* const account_name)
|
||||
{
|
||||
if (accounts_account_exists(account_name)) {
|
||||
return g_key_file_get_string(accounts, account_name, "last.activity", NULL);
|
||||
auto_gchar gchar* sanitized_account_name = _sanitize_account_name(account_name);
|
||||
if (_accounts_has_group(sanitized_account_name)) {
|
||||
return g_key_file_get_string(accounts, sanitized_account_name, "last.activity", NULL);
|
||||
} else {
|
||||
return NULL;
|
||||
}
|
||||
@@ -860,27 +851,30 @@ accounts_get_last_activity(const char* const account_name)
|
||||
gchar*
|
||||
accounts_get_resource(const char* const account_name)
|
||||
{
|
||||
if (!accounts_account_exists(account_name)) {
|
||||
auto_gchar gchar* sanitized_account_name = _sanitize_account_name(account_name);
|
||||
if (!_accounts_has_group(sanitized_account_name)) {
|
||||
return NULL;
|
||||
}
|
||||
return g_key_file_get_string(accounts, account_name, "resource", NULL);
|
||||
return g_key_file_get_string(accounts, sanitized_account_name, "resource", NULL);
|
||||
}
|
||||
|
||||
int
|
||||
accounts_get_max_sessions(const char* const account_name)
|
||||
{
|
||||
if (!accounts_account_exists(account_name)) {
|
||||
auto_gchar gchar* sanitized_account_name = _sanitize_account_name(account_name);
|
||||
if (!_accounts_has_group(sanitized_account_name)) {
|
||||
return 0;
|
||||
}
|
||||
return g_key_file_get_integer(accounts, account_name, "max.sessions", 0);
|
||||
return g_key_file_get_integer(accounts, sanitized_account_name, "max.sessions", 0);
|
||||
}
|
||||
|
||||
void
|
||||
accounts_set_login_presence(const char* const account_name, const char* const value)
|
||||
{
|
||||
if (accounts_account_exists(account_name)) {
|
||||
g_key_file_set_string(accounts, account_name, "presence.login", value);
|
||||
_save_accounts();
|
||||
auto_gchar gchar* sanitized_account_name = _sanitize_account_name(account_name);
|
||||
if (_accounts_has_group(sanitized_account_name)) {
|
||||
g_key_file_set_string(accounts, sanitized_account_name, "presence.login", value);
|
||||
_accounts_save(account_name);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -888,7 +882,8 @@ resource_presence_t
|
||||
accounts_get_last_presence(const char* const account_name)
|
||||
{
|
||||
resource_presence_t result;
|
||||
auto_gchar gchar* setting = g_key_file_get_string(accounts, account_name, "presence.last", NULL);
|
||||
auto_gchar gchar* sanitized_account_name = _sanitize_account_name(account_name);
|
||||
auto_gchar gchar* setting = g_key_file_get_string(accounts, sanitized_account_name, "presence.last", NULL);
|
||||
|
||||
if (setting == NULL || (strcmp(setting, "online") == 0)) {
|
||||
result = RESOURCE_ONLINE;
|
||||
@@ -912,14 +907,16 @@ accounts_get_last_presence(const char* const account_name)
|
||||
char*
|
||||
accounts_get_last_status(const char* const account_name)
|
||||
{
|
||||
return g_key_file_get_string(accounts, account_name, "presence.laststatus", NULL);
|
||||
auto_gchar gchar* sanitized_account_name = _sanitize_account_name(account_name);
|
||||
return g_key_file_get_string(accounts, sanitized_account_name, "presence.laststatus", NULL);
|
||||
}
|
||||
|
||||
resource_presence_t
|
||||
accounts_get_login_presence(const char* const account_name)
|
||||
{
|
||||
resource_presence_t result;
|
||||
auto_gchar gchar* setting = g_key_file_get_string(accounts, account_name, "presence.login", NULL);
|
||||
auto_gchar gchar* sanitized_account_name = _sanitize_account_name(account_name);
|
||||
auto_gchar gchar* setting = g_key_file_get_string(accounts, sanitized_account_name, "presence.login", NULL);
|
||||
|
||||
if (setting == NULL || (strcmp(setting, "online") == 0)) {
|
||||
result = RESOURCE_ONLINE;
|
||||
@@ -944,7 +941,8 @@ accounts_get_login_presence(const char* const account_name)
|
||||
char*
|
||||
accounts_get_login_status(const char* const account_name)
|
||||
{
|
||||
auto_gchar gchar* setting = g_key_file_get_string(accounts, account_name, "presence.login", NULL);
|
||||
auto_gchar gchar* sanitized_account_name = _sanitize_account_name(account_name);
|
||||
auto_gchar gchar* setting = g_key_file_get_string(accounts, sanitized_account_name, "presence.login", NULL);
|
||||
gchar* status = NULL;
|
||||
|
||||
if (g_strcmp0(setting, "last") == 0) {
|
||||
@@ -953,9 +951,3 @@ accounts_get_login_status(const char* const account_name)
|
||||
|
||||
return status;
|
||||
}
|
||||
|
||||
static void
|
||||
_save_accounts(void)
|
||||
{
|
||||
save_keyfile(&accounts_prof_keyfile);
|
||||
}
|
||||
|
||||
@@ -4,33 +4,7 @@
|
||||
*
|
||||
* Copyright (C) 2012 - 2019 James Booth <boothj5@gmail.com>
|
||||
*
|
||||
* This file is part of Profanity.
|
||||
*
|
||||
* Profanity is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Profanity is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Profanity. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* In addition, as a special exception, the copyright holders give permission to
|
||||
* link the code of portions of this program with the OpenSSL library under
|
||||
* certain conditions as described in each individual source file, and
|
||||
* distribute linked combinations including the two.
|
||||
*
|
||||
* You must obey the GNU General Public License in all respects for all of the
|
||||
* code used other than OpenSSL. If you modify file(s) with this exception, you
|
||||
* may extend this exception to your version of the file(s), but you are not
|
||||
* obligated to do so. If you do not wish to do so, delete this exception
|
||||
* statement from your version. If you delete this exception statement from all
|
||||
* source files in the program, then also delete it here.
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later WITH OpenSSL-exception
|
||||
*/
|
||||
|
||||
#ifndef CONFIG_ACCOUNTS_H
|
||||
@@ -48,7 +22,7 @@ char* accounts_find_enabled(const char* const prefix, gboolean previous, void* c
|
||||
void accounts_reset_all_search(void);
|
||||
void accounts_reset_enabled_search(void);
|
||||
void accounts_add(const char* jid, const char* altdomain, const int port, const char* const tls_policy, const char* const auth_policy);
|
||||
int accounts_remove(const char* jid);
|
||||
gboolean accounts_remove(const char* jid);
|
||||
gchar** accounts_get_list(void);
|
||||
ProfAccount* accounts_get_account(const char* const name);
|
||||
gboolean accounts_enable(const char* const name);
|
||||
|
||||
@@ -4,33 +4,7 @@
|
||||
*
|
||||
* Copyright (C) 2022 Steffen Jaeckel <jaeckel-floss@eyet-services.de>
|
||||
*
|
||||
* This file is part of Profanity.
|
||||
*
|
||||
* Profanity is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Profanity is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Profanity. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* In addition, as a special exception, the copyright holders give permission to
|
||||
* link the code of portions of this program with the OpenSSL library under
|
||||
* certain conditions as described in each individual source file, and
|
||||
* distribute linked combinations including the two.
|
||||
*
|
||||
* You must obey the GNU General Public License in all respects for all of the
|
||||
* code used other than OpenSSL. If you modify file(s) with this exception, you
|
||||
* may extend this exception to your version of the file(s), but you are not
|
||||
* obligated to do so. If you do not wish to do so, delete this exception
|
||||
* statement from your version. If you delete this exception statement from all
|
||||
* source files in the program, then also delete it here.
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later WITH OpenSSL-exception
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
@@ -53,8 +27,7 @@ _cafile_name(void)
|
||||
if (!create_dir(certs_dir)) {
|
||||
return NULL;
|
||||
}
|
||||
gchar* filename = g_strdup_printf("%s/CAfile.pem", certs_dir);
|
||||
return filename;
|
||||
return g_strdup_printf("%s/CAfile.pem", certs_dir);
|
||||
}
|
||||
|
||||
void
|
||||
@@ -70,21 +43,25 @@ cafile_add(const TLSCertificate* cert)
|
||||
auto_gchar gchar* contents = NULL;
|
||||
auto_gchar gchar* new_contents = NULL;
|
||||
gsize length;
|
||||
GError* glib_error = NULL;
|
||||
auto_gerror GError* glib_error = NULL;
|
||||
if (g_file_test(cafile, G_FILE_TEST_EXISTS)) {
|
||||
if (!g_file_get_contents(cafile, &contents, &length, &glib_error)) {
|
||||
log_error("[CAfile] could not read from %s: %s", cafile, glib_error ? glib_error->message : "No GLib error given");
|
||||
log_error("[CAfile] could not read from %s: %s", cafile, PROF_GERROR_MESSAGE(glib_error));
|
||||
return;
|
||||
}
|
||||
if (strstr(contents, cert->fingerprint)) {
|
||||
log_debug("[CAfile] fingerprint %s already stored", cert->fingerprint);
|
||||
return;
|
||||
}
|
||||
if (strstr(contents, cert->fingerprint_sha1)) {
|
||||
log_debug("[CAfile] fingerprint %s already stored", cert->fingerprint_sha1);
|
||||
return;
|
||||
}
|
||||
}
|
||||
const char* header = "# Profanity CAfile\n# DO NOT EDIT - this file is automatically generated";
|
||||
new_contents = g_strdup_printf("%s\n\n# %s\n%s", contents ? contents : header, cert->fingerprint, cert->pem);
|
||||
new_contents = g_strdup_printf("%s\n# %s\n%s", contents ? contents : header, cert->fingerprint, cert->pem);
|
||||
if (!g_file_set_contents(cafile, new_contents, -1, &glib_error))
|
||||
log_error("[CAfile] could not write to %s: %s", cafile, glib_error ? glib_error->message : "No GLib error given");
|
||||
log_error("[CAfile] could not write to %s: %s", cafile, PROF_GERROR_MESSAGE(glib_error));
|
||||
}
|
||||
|
||||
gchar*
|
||||
|
||||
@@ -4,33 +4,7 @@
|
||||
*
|
||||
* Copyright (C) 2022 Steffen Jaeckel <jaeckel-floss@eyet-services.de>
|
||||
*
|
||||
* This file is part of Profanity.
|
||||
*
|
||||
* Profanity is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Profanity is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Profanity. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* In addition, as a special exception, the copyright holders give permission to
|
||||
* link the code of portions of this program with the OpenSSL library under
|
||||
* certain conditions as described in each individual source file, and
|
||||
* distribute linked combinations including the two.
|
||||
*
|
||||
* You must obey the GNU General Public License in all respects for all of the
|
||||
* code used other than OpenSSL. If you modify file(s) with this exception, you
|
||||
* may extend this exception to your version of the file(s), but you are not
|
||||
* obligated to do so. If you do not wish to do so, delete this exception
|
||||
* statement from your version. If you delete this exception statement from all
|
||||
* source files in the program, then also delete it here.
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later WITH OpenSSL-exception
|
||||
*/
|
||||
|
||||
#ifndef CONFIG_CAFILE_H
|
||||
|
||||
@@ -3,35 +3,9 @@
|
||||
* vim: expandtab:ts=4:sts=4:sw=4
|
||||
*
|
||||
* Copyright (C) 2019 Aurelien Aptel <aurelien.aptel@gmail.com>
|
||||
* Copyright (C) 2019 - 2025 Michael Vetter <jubalh@iodoru.org>
|
||||
*
|
||||
* This file is part of Profanity.
|
||||
*
|
||||
* Profanity is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Profanity is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Profanity. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* In addition, as a special exception, the copyright holders give permission to
|
||||
* link the code of portions of this program with the OpenSSL library under
|
||||
* certain conditions as described in each individual source file, and
|
||||
* distribute linked combinations including the two.
|
||||
*
|
||||
* You must obey the GNU General Public License in all respects for all of the
|
||||
* code used other than OpenSSL. If you modify file(s) with this exception, you
|
||||
* may extend this exception to your version of the file(s), but you are not
|
||||
* obligated to do so. If you do not wish to do so, delete this exception
|
||||
* statement from your version. If you delete this exception statement from all
|
||||
* source files in the program, then also delete it here.
|
||||
* Copyright (C) 2019 - 2026 Michael Vetter <jubalh@iodoru.org>
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later WITH OpenSSL-exception
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
@@ -59,7 +33,7 @@ static struct color_pair_cache
|
||||
{
|
||||
struct
|
||||
{
|
||||
int16_t fg, bg;
|
||||
short fg, bg;
|
||||
}* pairs;
|
||||
int size;
|
||||
int capacity;
|
||||
@@ -341,14 +315,18 @@ color_distance(const struct color_def* a, const struct color_def* b)
|
||||
return h * h + s * s + l * l;
|
||||
}
|
||||
|
||||
static int
|
||||
/* Indices into the 256-colour palette and ncurses palette IDs are
|
||||
* 16-bit (the size_t-or-short distinction in ncurses init_pair).
|
||||
* Use `short` end-to-end so the caller boundaries don't need casts.
|
||||
*/
|
||||
static short
|
||||
find_closest_col(int h, int s, int l)
|
||||
{
|
||||
struct color_def a = { h, s, l };
|
||||
int min = 0;
|
||||
struct color_def a = { (uint16_t)h, (uint8_t)s, (uint8_t)l, NULL };
|
||||
short min = 0;
|
||||
int dmin = color_distance(&a, &color_names[0]);
|
||||
|
||||
for (int i = 1; i < COLOR_NAME_SIZE; i++) {
|
||||
for (short i = 1; i < COLOR_NAME_SIZE; i++) {
|
||||
int d = color_distance(&a, &color_names[i]);
|
||||
if (d < dmin) {
|
||||
dmin = d;
|
||||
@@ -358,8 +336,8 @@ find_closest_col(int h, int s, int l)
|
||||
return min;
|
||||
}
|
||||
|
||||
static int
|
||||
find_col(const char* col_name, int n)
|
||||
static short
|
||||
find_col(const char* col_name, size_t n)
|
||||
{
|
||||
char name[32] = { 0 };
|
||||
|
||||
@@ -371,7 +349,7 @@ find_col(const char* col_name, int n)
|
||||
|
||||
if (n >= sizeof(name)) {
|
||||
/* truncate */
|
||||
log_error("Color: <%s,%d> bigger than %zu", col_name, n, sizeof(name));
|
||||
log_error("Color: <%s,%zu> bigger than %zu", col_name, n, sizeof(name));
|
||||
n = sizeof(name) - 1;
|
||||
}
|
||||
memcpy(name, col_name, n);
|
||||
@@ -380,7 +358,7 @@ find_col(const char* col_name, int n)
|
||||
return -1;
|
||||
}
|
||||
|
||||
for (int i = 0; i < COLOR_NAME_SIZE; i++) {
|
||||
for (short i = 0; i < COLOR_NAME_SIZE; i++) {
|
||||
if (g_ascii_strcasecmp(name, color_names[i].name) == 0) {
|
||||
return i;
|
||||
}
|
||||
@@ -389,13 +367,13 @@ find_col(const char* col_name, int n)
|
||||
return COL_ERR;
|
||||
}
|
||||
|
||||
static int
|
||||
static short
|
||||
color_hash(const char* str, color_profile profile)
|
||||
{
|
||||
GChecksum* cs = NULL;
|
||||
guint8 buf[256] = { 0 };
|
||||
gsize len = 256;
|
||||
int rc = -1; /* default ncurse color */
|
||||
short rc = -1; /* default ncurse color */
|
||||
|
||||
cs = g_checksum_new(G_CHECKSUM_SHA1);
|
||||
if (!cs)
|
||||
@@ -464,7 +442,7 @@ color_pair_cache_reset(void)
|
||||
}
|
||||
|
||||
static int
|
||||
_color_pair_cache_get(int fg, int bg)
|
||||
_color_pair_cache_get(short fg, short bg)
|
||||
{
|
||||
if (COLORS < 256) {
|
||||
if (fg > 7 || bg > 7) {
|
||||
@@ -489,10 +467,13 @@ _color_pair_cache_get(int fg, int bg)
|
||||
}
|
||||
|
||||
int i = cache.size;
|
||||
// TODO: init_pair uses 15-bit palette indices. Terminals exposing
|
||||
// extended colour palettes (>32767) need init_extended_pair here.
|
||||
// Upstream-inherited limitation.
|
||||
cache.pairs[i].fg = fg;
|
||||
cache.pairs[i].bg = bg;
|
||||
/* (re-)define the new pair in curses */
|
||||
init_pair(i, fg, bg);
|
||||
init_pair((short)i, fg, bg);
|
||||
|
||||
cache.size++;
|
||||
|
||||
@@ -511,8 +492,8 @@ _color_pair_cache_get(int fg, int bg)
|
||||
int
|
||||
color_pair_cache_hash_str(const char* str, color_profile profile)
|
||||
{
|
||||
int fg = color_hash(str, profile);
|
||||
int bg = -1;
|
||||
short fg = color_hash(str, profile);
|
||||
short bg = -1;
|
||||
|
||||
auto_char char* bkgnd = theme_get_bkgnd();
|
||||
if (bkgnd) {
|
||||
@@ -532,7 +513,7 @@ int
|
||||
color_pair_cache_get(const char* pair_name)
|
||||
{
|
||||
const char* sep;
|
||||
int fg, bg;
|
||||
short fg, bg;
|
||||
|
||||
sep = strchr(pair_name, '_');
|
||||
if (!sep) {
|
||||
@@ -540,7 +521,7 @@ color_pair_cache_get(const char* pair_name)
|
||||
return -1;
|
||||
}
|
||||
|
||||
fg = find_col(pair_name, sep - pair_name);
|
||||
fg = find_col(pair_name, (size_t)(sep - pair_name));
|
||||
bg = find_col(sep + 1, strlen(sep));
|
||||
if (fg == COL_ERR || bg == COL_ERR) {
|
||||
log_error("Color: bad color name %s", pair_name);
|
||||
|
||||
@@ -4,33 +4,7 @@
|
||||
*
|
||||
* Copyright (C) 2019 Aurelien Aptel <aurelien.aptel@gmail.com>
|
||||
*
|
||||
* This file is part of Profanity.
|
||||
*
|
||||
* Profanity is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Profanity is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Profanity. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* In addition, as a special exception, the copyright holders give permission to
|
||||
* link the code of portions of this program with the OpenSSL library under
|
||||
* certain conditions as described in each individual source file, and
|
||||
* distribute linked combinations including the two.
|
||||
*
|
||||
* You must obey the GNU General Public License in all respects for all of the
|
||||
* code used other than OpenSSL. If you modify file(s) with this exception, you
|
||||
* may extend this exception to your version of the file(s), but you are not
|
||||
* obligated to do so. If you do not wish to do so, delete this exception
|
||||
* statement from your version. If you delete this exception statement from all
|
||||
* source files in the program, then also delete it here.
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later WITH OpenSSL-exception
|
||||
*/
|
||||
|
||||
#ifndef _COLOR_H_
|
||||
|
||||
@@ -4,33 +4,7 @@
|
||||
*
|
||||
* Copyright (C) 2012 - 2019 James Booth <boothj5@gmail.com>
|
||||
*
|
||||
* This file is part of Profanity.
|
||||
*
|
||||
* Profanity is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Profanity is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Profanity. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* In addition, as a special exception, the copyright holders give permission to
|
||||
* link the code of portions of this program with the OpenSSL library under
|
||||
* certain conditions as described in each individual source file, and
|
||||
* distribute linked combinations including the two.
|
||||
*
|
||||
* You must obey the GNU General Public License in all respects for all of the
|
||||
* code used other than OpenSSL. If you modify file(s) with this exception, you
|
||||
* may extend this exception to your version of the file(s), but you are not
|
||||
* obligated to do so. If you do not wish to do so, delete this exception
|
||||
* statement from your version. If you delete this exception statement from all
|
||||
* source files in the program, then also delete it here.
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later WITH OpenSSL-exception
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
@@ -66,9 +40,7 @@ conf_string_list_add(GKeyFile* keyfile, const char* const group, const char* con
|
||||
|
||||
// Add item to the existing list
|
||||
const gchar** new_list = g_new(const gchar*, length + 2);
|
||||
for (gsize i = 0; i < length; ++i) {
|
||||
new_list[i] = list[i];
|
||||
}
|
||||
memcpy(new_list, list, sizeof(list[0]) * length);
|
||||
new_list[length] = item;
|
||||
new_list[length + 1] = NULL;
|
||||
|
||||
@@ -89,33 +61,28 @@ conf_string_list_remove(GKeyFile* keyfile, const char* const group, const char*
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
GList* glist = NULL;
|
||||
gsize new_length = 0;
|
||||
const gchar** new_list = g_new(const gchar*, length + 1);
|
||||
gboolean deleted = FALSE;
|
||||
|
||||
for (int i = 0; i < length; i++) {
|
||||
for (gsize i = 0; i < length; i++) {
|
||||
if (strcmp(list[i], item) == 0) {
|
||||
deleted = TRUE;
|
||||
continue;
|
||||
}
|
||||
glist = g_list_append(glist, strdup(list[i]));
|
||||
new_list[new_length++] = list[i];
|
||||
}
|
||||
new_list[new_length] = NULL;
|
||||
|
||||
if (deleted) {
|
||||
if (g_list_length(glist) == 0) {
|
||||
if (new_length == 0) {
|
||||
g_key_file_remove_key(keyfile, group, key, NULL);
|
||||
} else {
|
||||
const gchar* new_list[g_list_length(glist) + 1];
|
||||
int i = 0;
|
||||
|
||||
for (GList* curr = glist; curr; curr = g_list_next(curr)) {
|
||||
new_list[i++] = curr->data;
|
||||
}
|
||||
|
||||
new_list[i] = NULL;
|
||||
g_key_file_set_string_list(keyfile, group, key, new_list, g_list_length(glist));
|
||||
g_key_file_set_string_list(keyfile, group, key, new_list, new_length);
|
||||
}
|
||||
}
|
||||
|
||||
g_list_free_full(glist, g_free);
|
||||
g_free(new_list);
|
||||
|
||||
return deleted;
|
||||
}
|
||||
|
||||
@@ -4,33 +4,7 @@
|
||||
*
|
||||
* Copyright (C) 2012 - 2019 James Booth <boothj5@gmail.com>
|
||||
*
|
||||
* This file is part of Profanity.
|
||||
*
|
||||
* Profanity is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Profanity is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Profanity. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* In addition, as a special exception, the copyright holders give permission to
|
||||
* link the code of portions of this program with the OpenSSL library under
|
||||
* certain conditions as described in each individual source file, and
|
||||
* distribute linked combinations including the two.
|
||||
*
|
||||
* You must obey the GNU General Public License in all respects for all of the
|
||||
* code used other than OpenSSL. If you modify file(s) with this exception, you
|
||||
* may extend this exception to your version of the file(s), but you are not
|
||||
* obligated to do so. If you do not wish to do so, delete this exception
|
||||
* statement from your version. If you delete this exception statement from all
|
||||
* source files in the program, then also delete it here.
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later WITH OpenSSL-exception
|
||||
*/
|
||||
|
||||
#ifndef CONFIG_CONFLISTS_H
|
||||
|
||||
@@ -3,35 +3,9 @@
|
||||
* vim: expandtab:ts=4:sts=4:sw=4
|
||||
*
|
||||
* Copyright (C) 2012 - 2019 James Booth <boothj5@gmail.com>
|
||||
* Copyright (C) 2020 - 2025 Michael Vetter <jubalh@iodoru.org>
|
||||
*
|
||||
* This file is part of Profanity.
|
||||
*
|
||||
* Profanity is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Profanity is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Profanity. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* In addition, as a special exception, the copyright holders give permission to
|
||||
* link the code of portions of this program with the OpenSSL library under
|
||||
* certain conditions as described in each individual source file, and
|
||||
* distribute linked combinations including the two.
|
||||
*
|
||||
* You must obey the GNU General Public License in all respects for all of the
|
||||
* code used other than OpenSSL. If you modify file(s) with this exception, you
|
||||
* may extend this exception to your version of the file(s), but you are not
|
||||
* obligated to do so. If you do not wish to do so, delete this exception
|
||||
* statement from your version. If you delete this exception statement from all
|
||||
* source files in the program, then also delete it here.
|
||||
* Copyright (C) 2020 - 2026 Michael Vetter <jubalh@iodoru.org>
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later WITH OpenSSL-exception
|
||||
*/
|
||||
#include "config.h"
|
||||
|
||||
@@ -114,30 +88,26 @@ gchar*
|
||||
files_get_log_file(const char* const log_file)
|
||||
{
|
||||
auto_gchar gchar* xdg_data = _files_get_xdg_data_home();
|
||||
GString* logfile;
|
||||
|
||||
if (log_file) {
|
||||
auto_gchar gchar* log_path = g_path_get_dirname(log_file);
|
||||
if (!create_dir(log_path)) {
|
||||
log_error("Error while creating directory %s", log_path);
|
||||
}
|
||||
|
||||
logfile = g_string_new(log_file);
|
||||
} else {
|
||||
logfile = g_string_new(xdg_data);
|
||||
g_string_append(logfile, "/profanity/logs/profanity");
|
||||
|
||||
if (!prefs_get_boolean(PREF_LOG_SHARED)) {
|
||||
g_string_append_printf(logfile, "%d", getpid());
|
||||
}
|
||||
|
||||
g_string_append(logfile, ".log");
|
||||
return g_strdup(log_file);
|
||||
}
|
||||
|
||||
GString* logfile = g_string_new(xdg_data);
|
||||
g_string_append(logfile, "/profanity/logs/profanity");
|
||||
|
||||
if (!prefs_get_boolean(PREF_LOG_SHARED)) {
|
||||
g_string_append_printf(logfile, "%d", getpid());
|
||||
}
|
||||
|
||||
g_string_append(logfile, ".log");
|
||||
|
||||
gchar* result = g_strdup(logfile->str);
|
||||
|
||||
g_string_free(logfile, TRUE);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,35 +3,9 @@
|
||||
* vim: expandtab:ts=4:sts=4:sw=4
|
||||
*
|
||||
* Copyright (C) 2012 - 2019 James Booth <boothj5@gmail.com>
|
||||
* Copyright (C) 2018 - 2025 Michael Vetter <jubalh@iodoru.org>
|
||||
*
|
||||
* This file is part of Profanity.
|
||||
*
|
||||
* Profanity is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Profanity is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Profanity. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* In addition, as a special exception, the copyright holders give permission to
|
||||
* link the code of portions of this program with the OpenSSL library under
|
||||
* certain conditions as described in each individual source file, and
|
||||
* distribute linked combinations including the two.
|
||||
*
|
||||
* You must obey the GNU General Public License in all respects for all of the
|
||||
* code used other than OpenSSL. If you modify file(s) with this exception, you
|
||||
* may extend this exception to your version of the file(s), but you are not
|
||||
* obligated to do so. If you do not wish to do so, delete this exception
|
||||
* statement from your version. If you delete this exception statement from all
|
||||
* source files in the program, then also delete it here.
|
||||
* Copyright (C) 2018 - 2026 Michael Vetter <jubalh@iodoru.org>
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later WITH OpenSSL-exception
|
||||
*/
|
||||
|
||||
#ifndef CONFIG_FILES_H
|
||||
@@ -57,6 +31,7 @@
|
||||
#define DIR_OMEMO "omemo"
|
||||
#define DIR_PLUGINS "plugins"
|
||||
#define DIR_DATABASE "database"
|
||||
#define DIR_FLATLOG "flatlog"
|
||||
#define DIR_DOWNLOADS "downloads"
|
||||
#define DIR_EDITOR "editor"
|
||||
#define DIR_CERTS "certs"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3,35 +3,9 @@
|
||||
* vim: expandtab:ts=4:sts=4:sw=4
|
||||
*
|
||||
* Copyright (C) 2012 - 2019 James Booth <boothj5@gmail.com>
|
||||
* Copyright (C) 2019 - 2025 Michael Vetter <jubalh@iodoru.org>
|
||||
*
|
||||
* This file is part of Profanity.
|
||||
*
|
||||
* Profanity is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Profanity is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Profanity. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* In addition, as a special exception, the copyright holders give permission to
|
||||
* link the code of portions of this program with the OpenSSL library under
|
||||
* certain conditions as described in each individual source file, and
|
||||
* distribute linked combinations including the two.
|
||||
*
|
||||
* You must obey the GNU General Public License in all respects for all of the
|
||||
* code used other than OpenSSL. If you modify file(s) with this exception, you
|
||||
* may extend this exception to your version of the file(s), but you are not
|
||||
* obligated to do so. If you do not wish to do so, delete this exception
|
||||
* statement from your version. If you delete this exception statement from all
|
||||
* source files in the program, then also delete it here.
|
||||
* Copyright (C) 2019 - 2026 Michael Vetter <jubalh@iodoru.org>
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later WITH OpenSSL-exception
|
||||
*/
|
||||
|
||||
#ifndef CONFIG_PREFERENCES_H
|
||||
@@ -162,6 +136,7 @@ typedef enum {
|
||||
PREF_STATUSBAR_SHOW_NAME,
|
||||
PREF_STATUSBAR_SHOW_NUMBER,
|
||||
PREF_STATUSBAR_SHOW_READ,
|
||||
PREF_STATUSBAR_SHOW_DBBACKEND,
|
||||
PREF_STATUSBAR_SELF,
|
||||
PREF_STATUSBAR_CHAT,
|
||||
PREF_STATUSBAR_ROOM_TITLE,
|
||||
@@ -183,10 +158,17 @@ typedef enum {
|
||||
PREF_OX_LOG,
|
||||
PREF_MOOD,
|
||||
PREF_STROPHE_VERBOSITY,
|
||||
PREF_AI_PROVIDER,
|
||||
PREF_AI_API_KEY,
|
||||
PREF_AI_DEFAULT_MODEL,
|
||||
PREF_STROPHE_SM_ENABLED,
|
||||
PREF_STROPHE_SM_RESEND,
|
||||
PREF_VCARD_PHOTO_CMD,
|
||||
PREF_STATUSBAR_TABMODE,
|
||||
PREF_FORCE_ENCRYPTION,
|
||||
PREF_FORCE_ENCRYPTION_MODE,
|
||||
PREF_SPELLCHECK_ENABLE,
|
||||
PREF_SPELLCHECK_LANG,
|
||||
} preference_t;
|
||||
|
||||
typedef struct prof_alias_t
|
||||
@@ -203,15 +185,16 @@ typedef struct prof_winplacement_t
|
||||
int inputwin_pos;
|
||||
} ProfWinPlacement;
|
||||
|
||||
void prefs_load(const char* config_file);
|
||||
void prefs_load(const gchar* config_file);
|
||||
void prefs_changes(void);
|
||||
void prefs_save(void);
|
||||
void prefs_close(void);
|
||||
void prefs_reload(void);
|
||||
|
||||
gchar* prefs_autocomplete_boolean_choice(const char* const prefix, gboolean previous, void* context);
|
||||
gchar* prefs_autocomplete_boolean_choice(const gchar* const prefix, gboolean previous, void* context);
|
||||
void prefs_reset_boolean_choice(void);
|
||||
|
||||
gchar* prefs_autocomplete_room_trigger(const char* const prefix, gboolean previous, void* context);
|
||||
gchar* prefs_autocomplete_room_trigger(const gchar* const prefix, gboolean previous, void* context);
|
||||
void prefs_reset_room_trigger_ac(void);
|
||||
|
||||
gint prefs_get_gone(void);
|
||||
@@ -248,42 +231,42 @@ gint prefs_get_autoxa_time(void);
|
||||
void prefs_set_autoxa_time(gint value);
|
||||
|
||||
gchar** prefs_get_plugins(void);
|
||||
void prefs_add_plugin(const char* const name);
|
||||
void prefs_remove_plugin(const char* const name);
|
||||
void prefs_add_plugin(const gchar* const name);
|
||||
void prefs_remove_plugin(const gchar* const name);
|
||||
|
||||
gchar* prefs_get_otr_char(void);
|
||||
gboolean prefs_set_otr_char(char* ch);
|
||||
gboolean prefs_set_otr_char(gchar* ch);
|
||||
gchar* prefs_get_pgp_char(void);
|
||||
gboolean prefs_set_pgp_char(char* ch);
|
||||
gboolean prefs_set_pgp_char(gchar* ch);
|
||||
gchar* prefs_get_omemo_char(void);
|
||||
gboolean prefs_set_omemo_char(char* ch);
|
||||
gboolean prefs_set_omemo_char(gchar* ch);
|
||||
// XEP-0373: OpenPGP for XMPP
|
||||
char* prefs_get_ox_char(void);
|
||||
gboolean prefs_set_ox_char(char* ch);
|
||||
gboolean prefs_set_ox_char(gchar* ch);
|
||||
|
||||
gchar* prefs_get_roster_header_char(void);
|
||||
void prefs_set_roster_header_char(char* ch);
|
||||
void prefs_set_roster_header_char(gchar* ch);
|
||||
void prefs_clear_roster_header_char(void);
|
||||
gchar* prefs_get_roster_contact_char(void);
|
||||
void prefs_set_roster_contact_char(char* ch);
|
||||
void prefs_set_roster_contact_char(gchar* ch);
|
||||
void prefs_clear_roster_contact_char(void);
|
||||
gchar* prefs_get_roster_resource_char(void);
|
||||
void prefs_set_roster_resource_char(char* ch);
|
||||
void prefs_set_roster_resource_char(gchar* ch);
|
||||
void prefs_clear_roster_resource_char(void);
|
||||
gchar* prefs_get_roster_private_char(void);
|
||||
void prefs_set_roster_private_char(char* ch);
|
||||
void prefs_set_roster_private_char(gchar* ch);
|
||||
void prefs_clear_roster_private_char(void);
|
||||
gchar* prefs_get_roster_room_char(void);
|
||||
void prefs_set_roster_room_char(char* ch);
|
||||
void prefs_set_roster_room_char(gchar* ch);
|
||||
void prefs_clear_roster_room_char(void);
|
||||
gchar* prefs_get_roster_room_private_char(void);
|
||||
void prefs_set_roster_room_private_char(char* ch);
|
||||
void prefs_set_roster_room_private_char(gchar* ch);
|
||||
void prefs_clear_roster_room_private_char(void);
|
||||
gchar* prefs_get_occupants_char(void);
|
||||
void prefs_set_occupants_char(char* ch);
|
||||
void prefs_set_occupants_char(gchar* ch);
|
||||
void prefs_clear_occupants_char(void);
|
||||
gchar* prefs_get_occupants_header_char(void);
|
||||
void prefs_set_occupants_header_char(char* ch);
|
||||
void prefs_set_occupants_header_char(gchar* ch);
|
||||
void prefs_clear_occupants_header_char(void);
|
||||
|
||||
gint prefs_get_roster_contact_indent(void);
|
||||
@@ -296,21 +279,21 @@ gint prefs_get_occupants_indent(void);
|
||||
void prefs_set_occupants_indent(gint value);
|
||||
|
||||
gchar* prefs_get_correction_char(void);
|
||||
void prefs_set_correction_char(char* ch);
|
||||
void prefs_set_correction_char(gchar* ch);
|
||||
|
||||
void prefs_add_login(const char* jid);
|
||||
void prefs_add_login(const gchar* jid);
|
||||
|
||||
void prefs_set_tray_timer(gint value);
|
||||
gint prefs_get_tray_timer(void);
|
||||
|
||||
gboolean prefs_add_alias(const char* const name, const char* const value);
|
||||
gboolean prefs_remove_alias(const char* const name);
|
||||
gchar* prefs_get_alias(const char* const name);
|
||||
gboolean prefs_add_alias(const gchar* const name, const gchar* const value);
|
||||
gboolean prefs_remove_alias(const gchar* const name);
|
||||
gchar* prefs_get_alias(const gchar* const name);
|
||||
GList* prefs_get_aliases(void);
|
||||
void prefs_free_aliases(GList* aliases);
|
||||
|
||||
gboolean prefs_add_room_notify_trigger(const char* const text);
|
||||
gboolean prefs_remove_room_notify_trigger(const char* const text);
|
||||
gboolean prefs_add_room_notify_trigger(const gchar* const text);
|
||||
gboolean prefs_remove_room_notify_trigger(const gchar* const text);
|
||||
GList* prefs_get_room_notify_triggers(void);
|
||||
|
||||
ProfWinPlacement* prefs_get_win_placement(void);
|
||||
@@ -354,4 +337,42 @@ gboolean prefs_get_room_notify(const char* const roomjid);
|
||||
gboolean prefs_get_room_notify_mention(const char* const roomjid);
|
||||
gboolean prefs_get_room_notify_trigger(const char* const roomjid);
|
||||
|
||||
/* AI token management */
|
||||
gboolean prefs_ai_set_token(const char* const provider, const char* const token);
|
||||
gboolean prefs_ai_remove_token(const char* const provider);
|
||||
char* prefs_ai_get_token(const char* const provider);
|
||||
GList* prefs_ai_list_tokens(void);
|
||||
void prefs_free_ai_tokens(GList* tokens);
|
||||
|
||||
/* AI provider management */
|
||||
gboolean prefs_ai_set_provider(const char* const provider, const char* const url);
|
||||
gboolean prefs_ai_remove_provider(const char* const provider);
|
||||
char* prefs_ai_get_provider_url(const char* const provider);
|
||||
GList* prefs_ai_list_providers(void);
|
||||
void prefs_free_ai_providers(GList* providers);
|
||||
GList* prefs_ai_get_providers(void);
|
||||
|
||||
/* Wipe the whole [ai/<provider>] subsection (url, key, models, default_model,
|
||||
* custom settings) in one call. */
|
||||
gboolean prefs_ai_remove_section(const char* const provider);
|
||||
|
||||
/* AI model cache management */
|
||||
gboolean prefs_ai_set_models(const char* const provider, const gchar* const* models, gint count);
|
||||
gboolean prefs_ai_remove_models(const char* const provider);
|
||||
GList* prefs_ai_get_models(const char* const provider);
|
||||
void prefs_free_ai_models(GList* models);
|
||||
|
||||
/* AI default model per provider */
|
||||
gboolean prefs_ai_set_default_model(const char* const provider, const char* const model);
|
||||
gboolean prefs_ai_remove_default_model(const char* const provider);
|
||||
char* prefs_ai_get_default_model(const char* const provider);
|
||||
|
||||
/* AI per-provider custom settings (e.g. tools, search) — persisted as
|
||||
* setting.<name>=<value> inside [ai/<provider>]. Pass NULL value to remove. */
|
||||
gboolean prefs_ai_set_setting(const char* const provider, const char* const setting, const char* const value);
|
||||
gboolean prefs_ai_remove_setting(const char* const provider, const char* const setting);
|
||||
char* prefs_ai_get_setting(const char* const provider, const char* const setting);
|
||||
GList* prefs_ai_list_settings(const char* const provider);
|
||||
void prefs_free_ai_settings(GList* settings);
|
||||
|
||||
#endif
|
||||
|
||||
@@ -4,33 +4,7 @@
|
||||
*
|
||||
* Copyright (C) 2012 - 2019 James Booth <boothj5@gmail.com>
|
||||
*
|
||||
* This file is part of Profanity.
|
||||
*
|
||||
* Profanity is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Profanity is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Profanity. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* In addition, as a special exception, the copyright holders give permission to
|
||||
* link the code of portions of this program with the OpenSSL library under
|
||||
* certain conditions as described in each individual source file, and
|
||||
* distribute linked combinations including the two.
|
||||
*
|
||||
* You must obey the GNU General Public License in all respects for all of the
|
||||
* code used other than OpenSSL. If you modify file(s) with this exception, you
|
||||
* may extend this exception to your version of the file(s), but you are not
|
||||
* obligated to do so. If you do not wish to do so, delete this exception
|
||||
* statement from your version. If you delete this exception statement from all
|
||||
* source files in the program, then also delete it here.
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later WITH OpenSSL-exception
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
|
||||
@@ -4,33 +4,7 @@
|
||||
*
|
||||
* Copyright (C) 2012 - 2019 James Booth <boothj5@gmail.com>
|
||||
*
|
||||
* This file is part of Profanity.
|
||||
*
|
||||
* Profanity is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Profanity is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Profanity. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* In addition, as a special exception, the copyright holders give permission to
|
||||
* link the code of portions of this program with the OpenSSL library under
|
||||
* certain conditions as described in each individual source file, and
|
||||
* distribute linked combinations including the two.
|
||||
*
|
||||
* You must obey the GNU General Public License in all respects for all of the
|
||||
* code used other than OpenSSL. If you modify file(s) with this exception, you
|
||||
* may extend this exception to your version of the file(s), but you are not
|
||||
* obligated to do so. If you do not wish to do so, delete this exception
|
||||
* statement from your version. If you delete this exception statement from all
|
||||
* source files in the program, then also delete it here.
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later WITH OpenSSL-exception
|
||||
*/
|
||||
|
||||
#ifndef CONFIG_SCRIPTS_H
|
||||
|
||||
@@ -3,35 +3,9 @@
|
||||
* vim: expandtab:ts=4:sts=4:sw=4
|
||||
*
|
||||
* Copyright (C) 2012 - 2019 James Booth <boothj5@gmail.com>
|
||||
* Copyright (C) 2019 - 2025 Michael Vetter <jubalh@iodoru.org>
|
||||
*
|
||||
* This file is part of Profanity.
|
||||
*
|
||||
* Profanity is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Profanity is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Profanity. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* In addition, as a special exception, the copyright holders give permission to
|
||||
* link the code of portions of this program with the OpenSSL library under
|
||||
* certain conditions as described in each individual source file, and
|
||||
* distribute linked combinations including the two.
|
||||
*
|
||||
* You must obey the GNU General Public License in all respects for all of the
|
||||
* code used other than OpenSSL. If you modify file(s) with this exception, you
|
||||
* may extend this exception to your version of the file(s), but you are not
|
||||
* obligated to do so. If you do not wish to do so, delete this exception
|
||||
* statement from your version. If you delete this exception statement from all
|
||||
* source files in the program, then also delete it here.
|
||||
* Copyright (C) 2019 - 2026 Michael Vetter <jubalh@iodoru.org>
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later WITH OpenSSL-exception
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
@@ -117,6 +91,7 @@ theme_init(const char* const theme_name)
|
||||
g_hash_table_insert(defaults, strdup("mention"), strdup("yellow"));
|
||||
g_hash_table_insert(defaults, strdup("trigger"), strdup("yellow"));
|
||||
g_hash_table_insert(defaults, strdup("input.text"), strdup("default"));
|
||||
g_hash_table_insert(defaults, strdup("input.misspelled"), strdup("red"));
|
||||
g_hash_table_insert(defaults, strdup("main.time"), strdup("default"));
|
||||
g_hash_table_insert(defaults, strdup("titlebar.text"), strdup("white"));
|
||||
g_hash_table_insert(defaults, strdup("titlebar.brackets"), strdup("cyan"));
|
||||
@@ -533,7 +508,7 @@ _theme_find(const char* const theme_name)
|
||||
g_string_append(path, "/");
|
||||
g_string_append(path, theme_name);
|
||||
if (!g_file_test(path->str, G_FILE_TEST_EXISTS)) {
|
||||
g_string_free(path, true);
|
||||
g_string_free(path, TRUE);
|
||||
path = NULL;
|
||||
}
|
||||
}
|
||||
@@ -667,25 +642,17 @@ _theme_prep_fgnd(char* setting, GString* lookup_str, gboolean* bold)
|
||||
}
|
||||
}
|
||||
|
||||
char*
|
||||
gchar*
|
||||
theme_get_string(char* str)
|
||||
{
|
||||
gchar* res = g_key_file_get_string(theme, "colours", str, NULL);
|
||||
if (!res) {
|
||||
return strdup(g_hash_table_lookup(defaults, str));
|
||||
return g_strdup(g_hash_table_lookup(defaults, str));
|
||||
} else {
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
theme_free_string(char* str)
|
||||
{
|
||||
if (str) {
|
||||
g_free(str);
|
||||
}
|
||||
}
|
||||
|
||||
int
|
||||
theme_hash_attrs(const char* str)
|
||||
{
|
||||
@@ -748,6 +715,9 @@ theme_attrs(theme_item_t attrs)
|
||||
case THEME_INPUT_TEXT:
|
||||
_theme_prep_fgnd("input.text", lookup_str, &bold);
|
||||
break;
|
||||
case THEME_INPUT_MISSPELLED:
|
||||
_theme_prep_fgnd("input.misspelled", lookup_str, &bold);
|
||||
break;
|
||||
case THEME_TIME:
|
||||
_theme_prep_fgnd("main.time", lookup_str, &bold);
|
||||
break;
|
||||
|
||||
@@ -4,33 +4,7 @@
|
||||
*
|
||||
* Copyright (C) 2012 - 2019 James Booth <boothj5@gmail.com>
|
||||
*
|
||||
* This file is part of Profanity.
|
||||
*
|
||||
* Profanity is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Profanity is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Profanity. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* In addition, as a special exception, the copyright holders give permission to
|
||||
* link the code of portions of this program with the OpenSSL library under
|
||||
* certain conditions as described in each individual source file, and
|
||||
* distribute linked combinations including the two.
|
||||
*
|
||||
* You must obey the GNU General Public License in all respects for all of the
|
||||
* code used other than OpenSSL. If you modify file(s) with this exception, you
|
||||
* may extend this exception to your version of the file(s), but you are not
|
||||
* obligated to do so. If you do not wish to do so, delete this exception
|
||||
* statement from your version. If you delete this exception statement from all
|
||||
* source files in the program, then also delete it here.
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later WITH OpenSSL-exception
|
||||
*/
|
||||
|
||||
#ifndef CONFIG_THEME_H
|
||||
@@ -53,6 +27,7 @@ typedef enum {
|
||||
THEME_MENTION,
|
||||
THEME_TRIGGER,
|
||||
THEME_INPUT_TEXT,
|
||||
THEME_INPUT_MISSPELLED,
|
||||
THEME_TIME,
|
||||
THEME_TITLE_TEXT,
|
||||
THEME_TITLE_BRACKET,
|
||||
@@ -152,7 +127,6 @@ void theme_close(void);
|
||||
int theme_hash_attrs(const char* str);
|
||||
int theme_attrs(theme_item_t attrs);
|
||||
char* theme_get_string(char* str);
|
||||
void theme_free_string(char* str);
|
||||
theme_item_t theme_main_presence_attrs(const char* const presence);
|
||||
theme_item_t theme_roster_unread_presence_attrs(const char* const presence);
|
||||
theme_item_t theme_roster_active_presence_attrs(const char* const presence);
|
||||
|
||||
@@ -4,33 +4,7 @@
|
||||
*
|
||||
* Copyright (C) 2012 - 2019 James Booth <boothj5@gmail.com>
|
||||
*
|
||||
* This file is part of Profanity.
|
||||
*
|
||||
* Profanity is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Profanity is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Profanity. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* In addition, as a special exception, the copyright holders give permission to
|
||||
* link the code of portions of this program with the OpenSSL library under
|
||||
* certain conditions as described in each individual source file, and
|
||||
* distribute linked combinations including the two.
|
||||
*
|
||||
* You must obey the GNU General Public License in all respects for all of the
|
||||
* code used other than OpenSSL. If you modify file(s) with this exception, you
|
||||
* may extend this exception to your version of the file(s), but you are not
|
||||
* obligated to do so. If you do not wish to do so, delete this exception
|
||||
* statement from your version. If you delete this exception statement from all
|
||||
* source files in the program, then also delete it here.
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later WITH OpenSSL-exception
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
@@ -54,7 +28,7 @@ static void _save_tlscerts(void);
|
||||
|
||||
static Autocomplete certs_ac;
|
||||
|
||||
static char* current_fp;
|
||||
static gchar* current_fp;
|
||||
|
||||
static void
|
||||
_tlscerts_close(void)
|
||||
@@ -62,7 +36,7 @@ _tlscerts_close(void)
|
||||
free_keyfile(&tlscerts_prof_keyfile);
|
||||
tlscerts = NULL;
|
||||
|
||||
free(current_fp);
|
||||
g_free(current_fp);
|
||||
current_fp = NULL;
|
||||
|
||||
autocomplete_free(certs_ac);
|
||||
@@ -82,7 +56,7 @@ tlscerts_init(void)
|
||||
gsize len = 0;
|
||||
auto_gcharv gchar** groups = g_key_file_get_groups(tlscerts, &len);
|
||||
|
||||
for (int i = 0; i < g_strv_length(groups); i++) {
|
||||
for (guint i = 0; i < g_strv_length(groups); i++) {
|
||||
autocomplete_add(certs_ac, groups[i]);
|
||||
}
|
||||
|
||||
@@ -90,33 +64,116 @@ tlscerts_init(void)
|
||||
}
|
||||
|
||||
void
|
||||
tlscerts_set_current(const char* const fp)
|
||||
tlscerts_set_current(const TLSCertificate* cert)
|
||||
{
|
||||
if (current_fp) {
|
||||
free(current_fp);
|
||||
g_free(current_fp);
|
||||
}
|
||||
current_fp = strdup(fp);
|
||||
current_fp = g_strdup(cert->fingerprint);
|
||||
}
|
||||
|
||||
char*
|
||||
tlscerts_get_current(void)
|
||||
gboolean
|
||||
tlscerts_current_fingerprint_equals(const TLSCertificate* cert)
|
||||
{
|
||||
return current_fp;
|
||||
return g_strcmp0(current_fp, cert->fingerprint) == 0;
|
||||
}
|
||||
|
||||
void
|
||||
tlscerts_clear_current(void)
|
||||
{
|
||||
if (current_fp) {
|
||||
free(current_fp);
|
||||
g_free(current_fp);
|
||||
current_fp = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
gboolean
|
||||
tlscerts_exists(const char* const fingerprint)
|
||||
tlscerts_exists(const TLSCertificate* cert)
|
||||
{
|
||||
return g_key_file_has_group(tlscerts, fingerprint);
|
||||
return g_key_file_has_group(tlscerts, cert->fingerprint);
|
||||
}
|
||||
|
||||
#define _name_to_tlscert_name(in, cert, which) \
|
||||
do { \
|
||||
_name_to_tlscert_name_(in, &cert->which, #which); \
|
||||
} while (0)
|
||||
static void
|
||||
_name_to_tlscert_name_(const char* in, tls_cert_name_t* out, const char* name);
|
||||
|
||||
static TLSCertificate*
|
||||
_tlscerts_new(gchar* fingerprint_sha1, int version, gchar* serialnumber, gchar* subjectname,
|
||||
gchar* issuername, gchar* notbefore, gchar* notafter,
|
||||
gchar* key_alg, gchar* signature_alg, gchar* pem,
|
||||
gchar* fingerprint_sha256, gchar* pubkey_fingerprint)
|
||||
{
|
||||
TLSCertificate* cert = g_new0(TLSCertificate, 1);
|
||||
|
||||
if (fingerprint_sha256 && fingerprint_sha1) {
|
||||
cert->fingerprint_sha256 = fingerprint_sha256;
|
||||
cert->fingerprint_sha1 = fingerprint_sha1;
|
||||
cert->fingerprint = cert->fingerprint_sha256;
|
||||
} else {
|
||||
if (fingerprint_sha256) {
|
||||
size_t s = strlen(fingerprint_sha256);
|
||||
switch (s) {
|
||||
case 40:
|
||||
cert->fingerprint_sha1 = fingerprint_sha256;
|
||||
cert->fingerprint = cert->fingerprint_sha1;
|
||||
break;
|
||||
case 64:
|
||||
cert->fingerprint_sha256 = fingerprint_sha256;
|
||||
cert->fingerprint = cert->fingerprint_sha256;
|
||||
break;
|
||||
default:
|
||||
log_error("fingerprint_sha256 of length %zu unexpected: %s", s, fingerprint_sha256);
|
||||
}
|
||||
} else {
|
||||
cert->fingerprint_sha1 = fingerprint_sha1;
|
||||
cert->fingerprint = cert->fingerprint_sha1;
|
||||
}
|
||||
}
|
||||
|
||||
cert->version = version;
|
||||
cert->serialnumber = serialnumber;
|
||||
cert->subjectname = subjectname;
|
||||
cert->issuername = issuername;
|
||||
cert->notbefore = notbefore;
|
||||
cert->notafter = notafter;
|
||||
cert->key_alg = key_alg;
|
||||
cert->signature_alg = signature_alg;
|
||||
cert->pem = pem;
|
||||
cert->pubkey_fingerprint = pubkey_fingerprint;
|
||||
|
||||
/* Do this check after assigning all values, in order to not leak them,
|
||||
* even though the situation is most likely already FUBAR */
|
||||
if (!fingerprint_sha256 && !fingerprint_sha1) {
|
||||
log_error("No TLSCertificate w/o fingerprint");
|
||||
tlscerts_free(cert);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
_name_to_tlscert_name(subjectname, cert, subject);
|
||||
_name_to_tlscert_name(issuername, cert, issuer);
|
||||
|
||||
return cert;
|
||||
}
|
||||
|
||||
static TLSCertificate*
|
||||
_get_TLSCertificate(const gchar* fingerprint)
|
||||
{
|
||||
int version = g_key_file_get_integer(tlscerts, fingerprint, "version", NULL);
|
||||
gchar* serialnumber = g_key_file_get_string(tlscerts, fingerprint, "serialnumber", NULL);
|
||||
gchar* subjectname = g_key_file_get_string(tlscerts, fingerprint, "subjectname", NULL);
|
||||
gchar* issuername = g_key_file_get_string(tlscerts, fingerprint, "issuername", NULL);
|
||||
gchar* notbefore = g_key_file_get_string(tlscerts, fingerprint, "start", NULL);
|
||||
gchar* notafter = g_key_file_get_string(tlscerts, fingerprint, "end", NULL);
|
||||
gchar* keyalg = g_key_file_get_string(tlscerts, fingerprint, "keyalg", NULL);
|
||||
gchar* signaturealg = g_key_file_get_string(tlscerts, fingerprint, "signaturealg", NULL);
|
||||
gchar* fingerprint_sha1 = g_key_file_get_string(tlscerts, fingerprint, "fingerprint_sha1", NULL);
|
||||
gchar* pubkey_fingerprint = g_key_file_get_string(tlscerts, fingerprint, "pubkey_fingerprint", NULL);
|
||||
|
||||
return _tlscerts_new(fingerprint_sha1, version, serialnumber, subjectname, issuername, notbefore,
|
||||
notafter, keyalg, signaturealg, NULL, g_strdup(fingerprint), pubkey_fingerprint);
|
||||
}
|
||||
|
||||
GList*
|
||||
@@ -126,125 +183,81 @@ tlscerts_list(void)
|
||||
gsize len = 0;
|
||||
auto_gcharv gchar** groups = g_key_file_get_groups(tlscerts, &len);
|
||||
|
||||
for (int i = 0; i < g_strv_length(groups); i++) {
|
||||
char* fingerprint = strdup(groups[i]);
|
||||
int version = g_key_file_get_integer(tlscerts, fingerprint, "version", NULL);
|
||||
auto_gchar gchar* serialnumber = g_key_file_get_string(tlscerts, fingerprint, "serialnumber", NULL);
|
||||
auto_gchar gchar* subjectname = g_key_file_get_string(tlscerts, fingerprint, "subjectname", NULL);
|
||||
auto_gchar gchar* issuername = g_key_file_get_string(tlscerts, fingerprint, "issuername", NULL);
|
||||
auto_gchar gchar* notbefore = g_key_file_get_string(tlscerts, fingerprint, "start", NULL);
|
||||
auto_gchar gchar* notafter = g_key_file_get_string(tlscerts, fingerprint, "end", NULL);
|
||||
auto_gchar gchar* keyalg = g_key_file_get_string(tlscerts, fingerprint, "keyalg", NULL);
|
||||
auto_gchar gchar* signaturealg = g_key_file_get_string(tlscerts, fingerprint, "signaturealg", NULL);
|
||||
|
||||
TLSCertificate* cert = tlscerts_new(fingerprint, version, serialnumber, subjectname, issuername, notbefore,
|
||||
notafter, keyalg, signaturealg, NULL);
|
||||
|
||||
res = g_list_append(res, cert);
|
||||
for (gsize i = 0; i < len; i++) {
|
||||
res = g_list_append(res, _get_TLSCertificate(groups[i]));
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
TLSCertificate*
|
||||
tlscerts_new(const char* const fingerprint, int version, const char* const serialnumber, const char* const subjectname,
|
||||
const char* const issuername, const char* const notbefore, const char* const notafter,
|
||||
const char* const key_alg, const char* const signature_alg, const char* const pem)
|
||||
static void
|
||||
_name_to_tlscert_name_(const char* in, tls_cert_name_t* out, const char* name)
|
||||
{
|
||||
TLSCertificate* cert = calloc(1, sizeof(TLSCertificate));
|
||||
|
||||
if (fingerprint) {
|
||||
cert->fingerprint = strdup(fingerprint);
|
||||
}
|
||||
cert->version = version;
|
||||
if (serialnumber) {
|
||||
cert->serialnumber = strdup(serialnumber);
|
||||
}
|
||||
if (subjectname) {
|
||||
cert->subjectname = strdup(subjectname);
|
||||
}
|
||||
if (issuername) {
|
||||
cert->issuername = strdup(issuername);
|
||||
}
|
||||
if (notbefore) {
|
||||
cert->notbefore = strdup(notbefore);
|
||||
}
|
||||
if (notafter) {
|
||||
cert->notafter = strdup(notafter);
|
||||
}
|
||||
if (key_alg) {
|
||||
cert->key_alg = strdup(key_alg);
|
||||
}
|
||||
if (signature_alg) {
|
||||
cert->signature_alg = strdup(signature_alg);
|
||||
}
|
||||
if (pem) {
|
||||
cert->pem = strdup(pem);
|
||||
}
|
||||
|
||||
auto_gcharv gchar** fields = g_strsplit(subjectname, "/", 0);
|
||||
for (int i = 0; i < g_strv_length(fields); i++) {
|
||||
auto_gcharv gchar** fields = g_strsplit(in, "/", 0);
|
||||
const guint fields_num = g_strv_length(fields);
|
||||
for (guint i = 0; i < fields_num; i++) {
|
||||
auto_gcharv gchar** keyval = g_strsplit(fields[i], "=", 2);
|
||||
if (g_strv_length(keyval) == 2) {
|
||||
|
||||
#define tls_cert_name_set(which) \
|
||||
do { \
|
||||
if (out->which) { \
|
||||
log_warning("%s." #which " already set, skip %s", name, keyval[1]); \
|
||||
continue; \
|
||||
} \
|
||||
out->which = g_strdup(keyval[1]); \
|
||||
} while (0)
|
||||
|
||||
if ((g_strcmp0(keyval[0], "C") == 0) || (g_strcmp0(keyval[0], "countryName") == 0)) {
|
||||
cert->subject_country = strdup(keyval[1]);
|
||||
tls_cert_name_set(country);
|
||||
}
|
||||
if ((g_strcmp0(keyval[0], "ST") == 0) || (g_strcmp0(keyval[0], "stateOrProvinceName") == 0)) {
|
||||
cert->subject_state = strdup(keyval[1]);
|
||||
tls_cert_name_set(state);
|
||||
}
|
||||
if (g_strcmp0(keyval[0], "dnQualifier") == 0) {
|
||||
cert->subject_distinguishedname = strdup(keyval[1]);
|
||||
tls_cert_name_set(distinguishedname);
|
||||
}
|
||||
if (g_strcmp0(keyval[0], "serialnumber") == 0) {
|
||||
cert->subject_serialnumber = strdup(keyval[1]);
|
||||
tls_cert_name_set(serialnumber);
|
||||
}
|
||||
if ((g_strcmp0(keyval[0], "CN") == 0) || (g_strcmp0(keyval[0], "commonName") == 0)) {
|
||||
cert->subject_commonname = strdup(keyval[1]);
|
||||
tls_cert_name_set(commonname);
|
||||
}
|
||||
if ((g_strcmp0(keyval[0], "O") == 0) || (g_strcmp0(keyval[0], "organizationName") == 0)) {
|
||||
cert->subject_organisation = strdup(keyval[1]);
|
||||
tls_cert_name_set(organisation);
|
||||
}
|
||||
if ((g_strcmp0(keyval[0], "OU") == 0) || (g_strcmp0(keyval[0], "organizationalUnitName") == 0)) {
|
||||
cert->subject_organisation_unit = strdup(keyval[1]);
|
||||
tls_cert_name_set(organisation_unit);
|
||||
}
|
||||
if (g_strcmp0(keyval[0], "emailAddress") == 0) {
|
||||
cert->subject_email = strdup(keyval[1]);
|
||||
tls_cert_name_set(email);
|
||||
}
|
||||
#undef tls_cert_name_set
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
auto_gcharv gchar** fields2 = g_strsplit(issuername, "/", 0);
|
||||
for (int i = 0; i < g_strv_length(fields2); i++) {
|
||||
auto_gcharv gchar** keyval = g_strsplit(fields2[i], "=", 2);
|
||||
if (g_strv_length(keyval) == 2) {
|
||||
if ((g_strcmp0(keyval[0], "C") == 0) || (g_strcmp0(keyval[0], "countryName") == 0)) {
|
||||
cert->issuer_country = strdup(keyval[1]);
|
||||
}
|
||||
if ((g_strcmp0(keyval[0], "ST") == 0) || (g_strcmp0(keyval[0], "stateOrProvinceName") == 0)) {
|
||||
cert->issuer_state = strdup(keyval[1]);
|
||||
}
|
||||
if (g_strcmp0(keyval[0], "dnQualifier") == 0) {
|
||||
cert->issuer_distinguishedname = strdup(keyval[1]);
|
||||
}
|
||||
if (g_strcmp0(keyval[0], "serialnumber") == 0) {
|
||||
cert->issuer_serialnumber = strdup(keyval[1]);
|
||||
}
|
||||
if ((g_strcmp0(keyval[0], "CN") == 0) || (g_strcmp0(keyval[0], "commonName") == 0)) {
|
||||
cert->issuer_commonname = strdup(keyval[1]);
|
||||
}
|
||||
if ((g_strcmp0(keyval[0], "O") == 0) || (g_strcmp0(keyval[0], "organizationName") == 0)) {
|
||||
cert->issuer_organisation = strdup(keyval[1]);
|
||||
}
|
||||
if ((g_strcmp0(keyval[0], "OU") == 0) || (g_strcmp0(keyval[0], "organizationalUnitName") == 0)) {
|
||||
cert->issuer_organisation_unit = strdup(keyval[1]);
|
||||
}
|
||||
if (g_strcmp0(keyval[0], "emailAddress") == 0) {
|
||||
cert->issuer_email = strdup(keyval[1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
TLSCertificate*
|
||||
tlscerts_new(const char* fingerprint_sha1, int version, const char* serialnumber, const char* subjectname,
|
||||
const char* issuername, const char* notbefore, const char* notafter,
|
||||
const char* key_alg, const char* signature_alg, const char* pem,
|
||||
const char* fingerprint_sha256, const char* pubkey_fingerprint)
|
||||
{
|
||||
return _tlscerts_new(g_strdup(fingerprint_sha1), version, g_strdup(serialnumber), g_strdup(subjectname),
|
||||
g_strdup(issuername), g_strdup(notbefore), g_strdup(notafter),
|
||||
g_strdup(key_alg), g_strdup(signature_alg), g_strdup(pem),
|
||||
g_strdup(fingerprint_sha256), g_strdup(pubkey_fingerprint));
|
||||
}
|
||||
|
||||
return cert;
|
||||
static void
|
||||
_checked_g_key_file_set_string(GKeyFile* key_file,
|
||||
const gchar* group_name,
|
||||
const gchar* key,
|
||||
const gchar* string)
|
||||
{
|
||||
if (string == NULL)
|
||||
return;
|
||||
g_key_file_set_string(key_file, group_name, key, string);
|
||||
}
|
||||
|
||||
void
|
||||
@@ -261,33 +274,21 @@ tlscerts_add(const TLSCertificate* cert)
|
||||
autocomplete_add(certs_ac, cert->fingerprint);
|
||||
|
||||
g_key_file_set_integer(tlscerts, cert->fingerprint, "version", cert->version);
|
||||
if (cert->serialnumber) {
|
||||
g_key_file_set_string(tlscerts, cert->fingerprint, "serialnumber", cert->serialnumber);
|
||||
}
|
||||
if (cert->subjectname) {
|
||||
g_key_file_set_string(tlscerts, cert->fingerprint, "subjectname", cert->subjectname);
|
||||
}
|
||||
if (cert->issuername) {
|
||||
g_key_file_set_string(tlscerts, cert->fingerprint, "issuername", cert->issuername);
|
||||
}
|
||||
if (cert->notbefore) {
|
||||
g_key_file_set_string(tlscerts, cert->fingerprint, "start", cert->notbefore);
|
||||
}
|
||||
if (cert->notafter) {
|
||||
g_key_file_set_string(tlscerts, cert->fingerprint, "end", cert->notafter);
|
||||
}
|
||||
if (cert->key_alg) {
|
||||
g_key_file_set_string(tlscerts, cert->fingerprint, "keyalg", cert->key_alg);
|
||||
}
|
||||
if (cert->signature_alg) {
|
||||
g_key_file_set_string(tlscerts, cert->fingerprint, "signaturealg", cert->signature_alg);
|
||||
}
|
||||
_checked_g_key_file_set_string(tlscerts, cert->fingerprint, "serialnumber", cert->serialnumber);
|
||||
_checked_g_key_file_set_string(tlscerts, cert->fingerprint, "subjectname", cert->subjectname);
|
||||
_checked_g_key_file_set_string(tlscerts, cert->fingerprint, "issuername", cert->issuername);
|
||||
_checked_g_key_file_set_string(tlscerts, cert->fingerprint, "start", cert->notbefore);
|
||||
_checked_g_key_file_set_string(tlscerts, cert->fingerprint, "end", cert->notafter);
|
||||
_checked_g_key_file_set_string(tlscerts, cert->fingerprint, "keyalg", cert->key_alg);
|
||||
_checked_g_key_file_set_string(tlscerts, cert->fingerprint, "signaturealg", cert->signature_alg);
|
||||
_checked_g_key_file_set_string(tlscerts, cert->fingerprint, "fingerprint_sha1", cert->fingerprint_sha1);
|
||||
_checked_g_key_file_set_string(tlscerts, cert->fingerprint, "pubkey_fingerprint", cert->pubkey_fingerprint);
|
||||
|
||||
_save_tlscerts();
|
||||
}
|
||||
|
||||
gboolean
|
||||
tlscerts_revoke(const char* const fingerprint)
|
||||
tlscerts_revoke(const char* fingerprint)
|
||||
{
|
||||
gboolean result = g_key_file_remove_group(tlscerts, fingerprint, NULL);
|
||||
if (result) {
|
||||
@@ -300,28 +301,24 @@ tlscerts_revoke(const char* const fingerprint)
|
||||
}
|
||||
|
||||
TLSCertificate*
|
||||
tlscerts_get_trusted(const char* const fingerprint)
|
||||
tlscerts_get_trusted(const gchar* fingerprint)
|
||||
{
|
||||
if (!g_key_file_has_group(tlscerts, fingerprint)) {
|
||||
gsize len = 0;
|
||||
auto_gcharv gchar** groups = g_key_file_get_groups(tlscerts, &len);
|
||||
for (gsize i = 0; i < len; i++) {
|
||||
auto_gchar gchar* fingerprint_sha1 = g_key_file_get_string(tlscerts, groups[i], "fingerprint_sha1", NULL);
|
||||
if (g_strcmp0(fingerprint, fingerprint_sha1) == 0) {
|
||||
return _get_TLSCertificate(fingerprint_sha1);
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
int version = g_key_file_get_integer(tlscerts, fingerprint, "version", NULL);
|
||||
auto_gchar gchar* serialnumber = g_key_file_get_string(tlscerts, fingerprint, "serialnumber", NULL);
|
||||
auto_gchar gchar* subjectname = g_key_file_get_string(tlscerts, fingerprint, "subjectname", NULL);
|
||||
auto_gchar gchar* issuername = g_key_file_get_string(tlscerts, fingerprint, "issuername", NULL);
|
||||
auto_gchar gchar* notbefore = g_key_file_get_string(tlscerts, fingerprint, "start", NULL);
|
||||
auto_gchar gchar* notafter = g_key_file_get_string(tlscerts, fingerprint, "end", NULL);
|
||||
auto_gchar gchar* keyalg = g_key_file_get_string(tlscerts, fingerprint, "keyalg", NULL);
|
||||
auto_gchar gchar* signaturealg = g_key_file_get_string(tlscerts, fingerprint, "signaturealg", NULL);
|
||||
|
||||
TLSCertificate* cert = tlscerts_new(fingerprint, version, serialnumber, subjectname, issuername, notbefore,
|
||||
notafter, keyalg, signaturealg, NULL);
|
||||
return cert;
|
||||
return _get_TLSCertificate(fingerprint);
|
||||
}
|
||||
|
||||
char*
|
||||
tlscerts_complete(const char* const prefix, gboolean previous, void* context)
|
||||
tlscerts_complete(const char* prefix, gboolean previous, void* context)
|
||||
{
|
||||
return autocomplete_complete(certs_ac, prefix, TRUE, previous);
|
||||
}
|
||||
@@ -332,40 +329,37 @@ tlscerts_reset_ac(void)
|
||||
autocomplete_reset(certs_ac);
|
||||
}
|
||||
|
||||
static void
|
||||
_tls_cert_name_free(tls_cert_name_t* name)
|
||||
{
|
||||
g_free(name->country);
|
||||
g_free(name->state);
|
||||
g_free(name->distinguishedname);
|
||||
g_free(name->serialnumber);
|
||||
g_free(name->commonname);
|
||||
g_free(name->organisation);
|
||||
g_free(name->organisation_unit);
|
||||
g_free(name->email);
|
||||
}
|
||||
|
||||
void
|
||||
tlscerts_free(TLSCertificate* cert)
|
||||
{
|
||||
if (cert) {
|
||||
free(cert->serialnumber);
|
||||
_tls_cert_name_free(&cert->subject);
|
||||
_tls_cert_name_free(&cert->issuer);
|
||||
|
||||
free(cert->subjectname);
|
||||
free(cert->subject_country);
|
||||
free(cert->subject_state);
|
||||
free(cert->subject_distinguishedname);
|
||||
free(cert->subject_serialnumber);
|
||||
free(cert->subject_commonname);
|
||||
free(cert->subject_organisation);
|
||||
free(cert->subject_organisation_unit);
|
||||
free(cert->subject_email);
|
||||
|
||||
free(cert->issuername);
|
||||
free(cert->issuer_country);
|
||||
free(cert->issuer_state);
|
||||
free(cert->issuer_distinguishedname);
|
||||
free(cert->issuer_serialnumber);
|
||||
free(cert->issuer_commonname);
|
||||
free(cert->issuer_organisation);
|
||||
free(cert->issuer_organisation_unit);
|
||||
free(cert->issuer_email);
|
||||
|
||||
free(cert->notbefore);
|
||||
free(cert->notafter);
|
||||
free(cert->fingerprint);
|
||||
|
||||
free(cert->key_alg);
|
||||
free(cert->signature_alg);
|
||||
|
||||
free(cert->pem);
|
||||
g_free(cert->pubkey_fingerprint);
|
||||
g_free(cert->pem);
|
||||
g_free(cert->signature_alg);
|
||||
g_free(cert->key_alg);
|
||||
g_free(cert->notafter);
|
||||
g_free(cert->notbefore);
|
||||
g_free(cert->issuername);
|
||||
g_free(cert->subjectname);
|
||||
g_free(cert->serialnumber);
|
||||
g_free(cert->fingerprint_sha1);
|
||||
g_free(cert->fingerprint_sha256);
|
||||
|
||||
free(cert);
|
||||
}
|
||||
|
||||
@@ -4,33 +4,7 @@
|
||||
*
|
||||
* Copyright (C) 2012 - 2019 James Booth <boothj5@gmail.com>
|
||||
*
|
||||
* This file is part of Profanity.
|
||||
*
|
||||
* Profanity is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Profanity is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Profanity. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* In addition, as a special exception, the copyright holders give permission to
|
||||
* link the code of portions of this program with the OpenSSL library under
|
||||
* certain conditions as described in each individual source file, and
|
||||
* distribute linked combinations including the two.
|
||||
*
|
||||
* You must obey the GNU General Public License in all respects for all of the
|
||||
* code used other than OpenSSL. If you modify file(s) with this exception, you
|
||||
* may extend this exception to your version of the file(s), but you are not
|
||||
* obligated to do so. If you do not wish to do so, delete this exception
|
||||
* statement from your version. If you delete this exception statement from all
|
||||
* source files in the program, then also delete it here.
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later WITH OpenSSL-exception
|
||||
*/
|
||||
|
||||
#ifndef CONFIG_TLSCERTS_H
|
||||
@@ -38,61 +12,63 @@
|
||||
|
||||
#include <glib.h>
|
||||
|
||||
typedef struct tls_cert_name_t
|
||||
{
|
||||
gchar* country;
|
||||
gchar* state;
|
||||
gchar* distinguishedname;
|
||||
gchar* serialnumber;
|
||||
gchar* commonname;
|
||||
gchar* organisation;
|
||||
gchar* organisation_unit;
|
||||
gchar* email;
|
||||
} tls_cert_name_t;
|
||||
|
||||
typedef struct tls_cert_t
|
||||
{
|
||||
int version;
|
||||
char* serialnumber;
|
||||
char* subjectname;
|
||||
char* subject_country;
|
||||
char* subject_state;
|
||||
char* subject_distinguishedname;
|
||||
char* subject_serialnumber;
|
||||
char* subject_commonname;
|
||||
char* subject_organisation;
|
||||
char* subject_organisation_unit;
|
||||
char* subject_email;
|
||||
char* issuername;
|
||||
char* issuer_country;
|
||||
char* issuer_state;
|
||||
char* issuer_distinguishedname;
|
||||
char* issuer_serialnumber;
|
||||
char* issuer_commonname;
|
||||
char* issuer_organisation;
|
||||
char* issuer_organisation_unit;
|
||||
char* issuer_email;
|
||||
char* notbefore;
|
||||
char* notafter;
|
||||
char* fingerprint;
|
||||
char* key_alg;
|
||||
char* signature_alg;
|
||||
char* pem;
|
||||
const gchar* fingerprint;
|
||||
gchar* serialnumber;
|
||||
gchar* subjectname;
|
||||
tls_cert_name_t subject;
|
||||
gchar* issuername;
|
||||
tls_cert_name_t issuer;
|
||||
gchar* notbefore;
|
||||
gchar* notafter;
|
||||
gchar* fingerprint_sha1;
|
||||
gchar* fingerprint_sha256;
|
||||
gchar* key_alg;
|
||||
gchar* signature_alg;
|
||||
gchar* pem;
|
||||
gchar* pubkey_fingerprint;
|
||||
} TLSCertificate;
|
||||
|
||||
void tlscerts_init(void);
|
||||
|
||||
TLSCertificate* tlscerts_new(const char* const fingerprint, int version, const char* const serialnumber, const char* const subjectname,
|
||||
const char* const issuername, const char* const notbefore, const char* const notafter,
|
||||
const char* const key_alg, const char* const signature_alg, const char* const pem);
|
||||
TLSCertificate* tlscerts_new(const char* fingerprint_sha1, int version, const char* serialnumber, const char* subjectname,
|
||||
const char* issuername, const char* notbefore, const char* notafter,
|
||||
const char* key_alg, const char* signature_alg, const char* pem,
|
||||
const char* fingerprint_sha256, const char* pubkey_fingerprint);
|
||||
|
||||
void tlscerts_set_current(const char* const fp);
|
||||
void tlscerts_set_current(const TLSCertificate* cert);
|
||||
|
||||
char* tlscerts_get_current(void);
|
||||
gboolean tlscerts_current_fingerprint_equals(const TLSCertificate* cert);
|
||||
|
||||
void tlscerts_clear_current(void);
|
||||
|
||||
gboolean tlscerts_exists(const char* const fingerprint);
|
||||
gboolean tlscerts_exists(const TLSCertificate* cert);
|
||||
|
||||
void tlscerts_add(const TLSCertificate* cert);
|
||||
|
||||
gboolean tlscerts_revoke(const char* const fingerprint);
|
||||
gboolean tlscerts_revoke(const char* fingerprint);
|
||||
|
||||
TLSCertificate* tlscerts_get_trusted(const char* const fingerprint);
|
||||
TLSCertificate* tlscerts_get_trusted(const char* fingerprint);
|
||||
|
||||
void tlscerts_free(TLSCertificate* cert);
|
||||
|
||||
GList* tlscerts_list(void);
|
||||
|
||||
char* tlscerts_complete(const char* const prefix, gboolean previous, void* context);
|
||||
char* tlscerts_complete(const char* prefix, gboolean previous, void* context);
|
||||
|
||||
void tlscerts_reset_ac(void);
|
||||
|
||||
|
||||
709
src/database.c
709
src/database.c
@@ -35,664 +35,187 @@
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <sys/stat.h>
|
||||
#include <sys/statvfs.h>
|
||||
#include <sqlite3.h>
|
||||
#include <glib.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <errno.h>
|
||||
|
||||
#include "log.h"
|
||||
#include "common.h"
|
||||
#include "config/files.h"
|
||||
#include "config.h"
|
||||
#include "database.h"
|
||||
#include "config/preferences.h"
|
||||
#include "config/accounts.h"
|
||||
#include "ui/ui.h"
|
||||
#include "xmpp/xmpp.h"
|
||||
#include "xmpp/message.h"
|
||||
|
||||
static sqlite3* g_chatlog_database;
|
||||
db_backend_t* active_db_backend = NULL;
|
||||
|
||||
static void _add_to_db(ProfMessage* message, char* type, const Jid* const from_jid, const Jid* const to_jid);
|
||||
static char* _get_db_filename(ProfAccount* account);
|
||||
static prof_msg_type_t _get_message_type_type(const char* const type);
|
||||
static prof_enc_t _get_message_enc_type(const char* const encstr);
|
||||
static int _get_db_version(void);
|
||||
static gboolean _migrate_to_v2(void);
|
||||
static gboolean _check_available_space_for_db_migration(char* path_to_db);
|
||||
|
||||
static const int latest_version = 2;
|
||||
|
||||
static char*
|
||||
_db_strdup(const char* str)
|
||||
void
|
||||
integrity_issue_free(integrity_issue_t* issue)
|
||||
{
|
||||
return str ? strdup(str) : NULL;
|
||||
if (issue) {
|
||||
g_free(issue->file);
|
||||
g_free(issue->message);
|
||||
g_free(issue);
|
||||
}
|
||||
}
|
||||
|
||||
#define auto_sqlite __attribute__((__cleanup__(auto_free_sqlite)))
|
||||
|
||||
static void
|
||||
auto_free_sqlite(gchar** str)
|
||||
gboolean
|
||||
log_database_can_recover_messages(void)
|
||||
{
|
||||
if (str == NULL)
|
||||
return;
|
||||
sqlite3_free(*str);
|
||||
}
|
||||
|
||||
static char*
|
||||
_get_db_filename(ProfAccount* account)
|
||||
{
|
||||
return files_file_in_account_data_path(DIR_DATABASE, account->jid, "chatlog.db");
|
||||
if (!active_db_backend) {
|
||||
return FALSE;
|
||||
}
|
||||
auto_gchar gchar* pref_dblog = prefs_get_string(PREF_DBLOG);
|
||||
if (g_strcmp0(pref_dblog, "off") == 0) {
|
||||
return FALSE;
|
||||
}
|
||||
if (g_strcmp0(pref_dblog, "redact") == 0) {
|
||||
return FALSE;
|
||||
}
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
gboolean
|
||||
log_database_init(ProfAccount* account)
|
||||
{
|
||||
int ret = sqlite3_initialize();
|
||||
if (ret != SQLITE_OK) {
|
||||
log_error("Error initializing SQLite database: %d", ret);
|
||||
return FALSE;
|
||||
}
|
||||
auto_gchar gchar* pref_dblog = prefs_get_string(PREF_DBLOG);
|
||||
|
||||
auto_char char* filename = _get_db_filename(account);
|
||||
if (!filename) {
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
ret = sqlite3_open(filename, &g_chatlog_database);
|
||||
if (ret != SQLITE_OK) {
|
||||
const char* err_msg = sqlite3_errmsg(g_chatlog_database);
|
||||
log_error("Error opening SQLite database: %s", err_msg);
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
char* err_msg;
|
||||
|
||||
int db_version = _get_db_version();
|
||||
if (db_version == latest_version) {
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
// ChatLogs Table
|
||||
// Contains all chat messages
|
||||
//
|
||||
// id is primary key
|
||||
// from_jid is the sender's jid
|
||||
// to_jid is the receiver's jid
|
||||
// from_resource is the sender's resource
|
||||
// to_resource is the receiver's resource
|
||||
// message is the message's text
|
||||
// timestamp is the timestamp like "2020/03/24 11:12:14"
|
||||
// type is there to distinguish: message (chat), MUC message (muc), muc pm (mucpm)
|
||||
// stanza_id is the ID in <message>
|
||||
// archive_id is the stanza-id from from XEP-0359: Unique and Stable Stanza IDs used for XEP-0313: Message Archive Management
|
||||
// encryption is to distinguish: none, omemo, otr, pgp
|
||||
// marked_read is 0/1 whether a message has been marked as read via XEP-0333: Chat Markers
|
||||
// replace_id is the ID from XEP-0308: Last Message Correction
|
||||
// replaces_db_id is ID (primary key) of the original message that LMC message corrects/replaces
|
||||
// replaced_by_db_id is ID (primary key) of the last correcting (LMC) message for the original message
|
||||
char* query = "CREATE TABLE IF NOT EXISTS `ChatLogs` ("
|
||||
"`id` INTEGER PRIMARY KEY AUTOINCREMENT, "
|
||||
"`from_jid` TEXT NOT NULL, "
|
||||
"`to_jid` TEXT NOT NULL, "
|
||||
"`from_resource` TEXT, "
|
||||
"`to_resource` TEXT, "
|
||||
"`message` TEXT, "
|
||||
"`timestamp` TEXT, "
|
||||
"`type` TEXT, "
|
||||
"`stanza_id` TEXT, "
|
||||
"`archive_id` TEXT, "
|
||||
"`encryption` TEXT, "
|
||||
"`marked_read` INTEGER, "
|
||||
"`replace_id` TEXT, "
|
||||
"`replaces_db_id` INTEGER, "
|
||||
"`replaced_by_db_id` INTEGER)";
|
||||
if (SQLITE_OK != sqlite3_exec(g_chatlog_database, query, NULL, 0, &err_msg)) {
|
||||
goto out;
|
||||
}
|
||||
|
||||
query = "CREATE TRIGGER IF NOT EXISTS update_corrected_message "
|
||||
"AFTER INSERT ON ChatLogs "
|
||||
"FOR EACH ROW "
|
||||
"WHEN NEW.replaces_db_id IS NOT NULL "
|
||||
"BEGIN "
|
||||
"UPDATE ChatLogs "
|
||||
"SET replaced_by_db_id = NEW.id "
|
||||
"WHERE id = NEW.replaces_db_id; "
|
||||
"END;";
|
||||
if (SQLITE_OK != sqlite3_exec(g_chatlog_database, query, NULL, 0, &err_msg)) {
|
||||
log_error("Unable to add `update_corrected_message` trigger.");
|
||||
goto out;
|
||||
}
|
||||
|
||||
query = "CREATE INDEX IF NOT EXISTS ChatLogs_timestamp_IDX ON `ChatLogs` (`timestamp`)";
|
||||
if (SQLITE_OK != sqlite3_exec(g_chatlog_database, query, NULL, 0, &err_msg)) {
|
||||
log_error("Unable to create index for timestamp.");
|
||||
goto out;
|
||||
}
|
||||
query = "CREATE INDEX IF NOT EXISTS ChatLogs_to_from_jid_IDX ON `ChatLogs` (`to_jid`, `from_jid`)";
|
||||
if (SQLITE_OK != sqlite3_exec(g_chatlog_database, query, NULL, 0, &err_msg)) {
|
||||
log_error("Unable to create index for to_jid.");
|
||||
goto out;
|
||||
}
|
||||
|
||||
query = "CREATE TABLE IF NOT EXISTS `DbVersion` (`dv_id` INTEGER PRIMARY KEY, `version` INTEGER UNIQUE)";
|
||||
if (SQLITE_OK != sqlite3_exec(g_chatlog_database, query, NULL, 0, &err_msg)) {
|
||||
goto out;
|
||||
}
|
||||
|
||||
if (db_version == -1) {
|
||||
query = "INSERT OR IGNORE INTO `DbVersion` (`version`) VALUES ('2')";
|
||||
if (SQLITE_OK != sqlite3_exec(g_chatlog_database, query, NULL, 0, &err_msg)) {
|
||||
goto out;
|
||||
}
|
||||
db_version = _get_db_version();
|
||||
}
|
||||
|
||||
// Unlikely event, but we don't want to migrate if we are just unable to determine the DB version
|
||||
if (db_version == -1) {
|
||||
cons_show_error("DB Initialization Error: Unable to check DB version.");
|
||||
goto out;
|
||||
}
|
||||
|
||||
if (db_version < latest_version) {
|
||||
cons_show("Migrating database schema. This operation may take a while...");
|
||||
if (db_version < 2 && (!_check_available_space_for_db_migration(filename) || !_migrate_to_v2())) {
|
||||
cons_show_error("Database Initialization Error: Unable to migrate database to version 2. Please, check error logs for details.");
|
||||
goto out;
|
||||
}
|
||||
cons_show("Database schema migration was successful.");
|
||||
}
|
||||
|
||||
log_debug("Initialized SQLite database: %s", filename);
|
||||
return TRUE;
|
||||
|
||||
out:
|
||||
if (err_msg) {
|
||||
log_error("SQLite error in log_database_init(): %s", err_msg);
|
||||
sqlite3_free(err_msg);
|
||||
// Select backend based on preference
|
||||
if (g_strcmp0(pref_dblog, "flatfile") == 0) {
|
||||
active_db_backend = db_backend_flatfile();
|
||||
log_info("Using flat-file database backend");
|
||||
} else {
|
||||
log_error("Unknown SQLite error in log_database_init().");
|
||||
#ifdef HAVE_SQLITE
|
||||
active_db_backend = db_backend_sqlite();
|
||||
log_info("Using SQLite database backend");
|
||||
#else
|
||||
log_warning("SQLite not compiled in, falling back to flat-file backend");
|
||||
active_db_backend = db_backend_flatfile();
|
||||
#endif
|
||||
}
|
||||
return FALSE;
|
||||
|
||||
if (!active_db_backend) {
|
||||
log_error("log_database_init: no backend available");
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
return active_db_backend->init(account);
|
||||
}
|
||||
|
||||
void
|
||||
log_database_close(void)
|
||||
{
|
||||
log_debug("log_database_close() called");
|
||||
if (g_chatlog_database) {
|
||||
sqlite3_close(g_chatlog_database);
|
||||
sqlite3_shutdown();
|
||||
g_chatlog_database = NULL;
|
||||
if (active_db_backend) {
|
||||
active_db_backend->close();
|
||||
}
|
||||
active_db_backend = NULL;
|
||||
}
|
||||
|
||||
gboolean
|
||||
log_database_switch_backend(const char* new_backend)
|
||||
{
|
||||
// Close current backend
|
||||
log_database_close();
|
||||
|
||||
// Update preference (user must /save to persist across restarts)
|
||||
prefs_set_string(PREF_DBLOG, new_backend);
|
||||
|
||||
// Get current account to reinitialize
|
||||
const char* account_name = session_get_account_name();
|
||||
if (!account_name) {
|
||||
log_error("log_database_switch_backend: no active session");
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
ProfAccount* account = accounts_get_account(account_name);
|
||||
if (!account) {
|
||||
log_error("log_database_switch_backend: account '%s' not found", account_name);
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
gboolean result = log_database_init(account);
|
||||
account_free(account);
|
||||
return result;
|
||||
}
|
||||
|
||||
void
|
||||
log_database_add_incoming(ProfMessage* message)
|
||||
{
|
||||
if (message->to_jid) {
|
||||
_add_to_db(message, NULL, message->from_jid, message->to_jid);
|
||||
if (active_db_backend && active_db_backend->add_incoming) {
|
||||
active_db_backend->add_incoming(message);
|
||||
} else {
|
||||
_add_to_db(message, NULL, message->from_jid, connection_get_jid());
|
||||
log_warning("log_database_add_incoming: no backend or method available");
|
||||
}
|
||||
}
|
||||
|
||||
static void
|
||||
_log_database_add_outgoing(char* type, const char* const id, const char* const barejid, const char* const message, const char* const replace_id, prof_enc_t enc)
|
||||
{
|
||||
ProfMessage* msg = message_init();
|
||||
|
||||
msg->id = _db_strdup(id);
|
||||
msg->from_jid = jid_create(barejid);
|
||||
msg->plain = _db_strdup(message);
|
||||
msg->replace_id = _db_strdup(replace_id);
|
||||
msg->timestamp = g_date_time_new_now_local(); // TODO: get from outside. best to have whole ProfMessage from outside
|
||||
msg->enc = enc;
|
||||
|
||||
_add_to_db(msg, type, connection_get_jid(), msg->from_jid); // TODO: myjid now in profmessage
|
||||
|
||||
message_free(msg);
|
||||
}
|
||||
|
||||
void
|
||||
log_database_add_outgoing_chat(const char* const id, const char* const barejid, const char* const message, const char* const replace_id, prof_enc_t enc)
|
||||
{
|
||||
_log_database_add_outgoing("chat", id, barejid, message, replace_id, enc);
|
||||
if (active_db_backend && active_db_backend->add_outgoing_chat) {
|
||||
active_db_backend->add_outgoing_chat(id, barejid, message, replace_id, enc);
|
||||
} else {
|
||||
log_warning("log_database_add_outgoing_chat: no backend or method available");
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
log_database_add_outgoing_muc(const char* const id, const char* const barejid, const char* const message, const char* const replace_id, prof_enc_t enc)
|
||||
{
|
||||
_log_database_add_outgoing("muc", id, barejid, message, replace_id, enc);
|
||||
if (active_db_backend && active_db_backend->add_outgoing_muc) {
|
||||
active_db_backend->add_outgoing_muc(id, barejid, message, replace_id, enc);
|
||||
} else {
|
||||
log_warning("log_database_add_outgoing_muc: no backend or method available");
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
log_database_add_outgoing_muc_pm(const char* const id, const char* const barejid, const char* const message, const char* const replace_id, prof_enc_t enc)
|
||||
{
|
||||
_log_database_add_outgoing("mucpm", id, barejid, message, replace_id, enc);
|
||||
if (active_db_backend && active_db_backend->add_outgoing_muc_pm) {
|
||||
active_db_backend->add_outgoing_muc_pm(id, barejid, message, replace_id, enc);
|
||||
} else {
|
||||
log_warning("log_database_add_outgoing_muc_pm: no backend or method available");
|
||||
}
|
||||
}
|
||||
|
||||
// Get info (timestamp and stanza_id) of the first or last message in db
|
||||
ProfMessage*
|
||||
log_database_get_limits_info(const gchar* const contact_barejid, gboolean is_last)
|
||||
{
|
||||
sqlite3_stmt* stmt = NULL;
|
||||
const Jid* myjid = connection_get_jid();
|
||||
if (!myjid->str)
|
||||
return NULL;
|
||||
|
||||
const char* order = is_last ? "DESC" : "ASC";
|
||||
auto_sqlite char* query = sqlite3_mprintf("SELECT `archive_id`, `timestamp` FROM `ChatLogs` WHERE "
|
||||
"(`from_jid` = %Q AND `to_jid` = %Q) OR "
|
||||
"(`from_jid` = %Q AND `to_jid` = %Q) "
|
||||
"ORDER BY `timestamp` %s LIMIT 1;",
|
||||
contact_barejid, myjid->barejid, myjid->barejid, contact_barejid, order);
|
||||
|
||||
if (!query) {
|
||||
log_error("Could not allocate memory for SQL query in log_database_get_limits_info()");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
int rc = sqlite3_prepare_v2(g_chatlog_database, query, -1, &stmt, NULL);
|
||||
if (rc != SQLITE_OK) {
|
||||
log_error("Unknown SQLite error in log_database_get_last_info().");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
ProfMessage* msg = message_init();
|
||||
|
||||
if (sqlite3_step(stmt) == SQLITE_ROW) {
|
||||
char* archive_id = (char*)sqlite3_column_text(stmt, 0);
|
||||
char* date = (char*)sqlite3_column_text(stmt, 1);
|
||||
|
||||
msg->stanzaid = _db_strdup(archive_id);
|
||||
msg->timestamp = g_date_time_new_from_iso8601(date, NULL);
|
||||
}
|
||||
sqlite3_finalize(stmt);
|
||||
|
||||
return msg;
|
||||
}
|
||||
|
||||
// Query previous chats, constraints start_time and end_time. If end_time is
|
||||
// null the current time is used. from_start gets first few messages if true
|
||||
// otherwise the last ones. Flip flips the order of the results
|
||||
db_history_result_t
|
||||
log_database_get_previous_chat(const gchar* const contact_barejid, const gchar* start_time, const gchar* end_time, gboolean from_start, gboolean flip, GSList** result)
|
||||
{
|
||||
if (!g_chatlog_database) {
|
||||
log_warning("log_database_get_previous_chat() called but db is not initialized");
|
||||
return DB_RESPONSE_ERROR;
|
||||
if (active_db_backend && active_db_backend->get_previous_chat) {
|
||||
return active_db_backend->get_previous_chat(contact_barejid, start_time, end_time, from_start, flip, result);
|
||||
}
|
||||
sqlite3_stmt* stmt = NULL;
|
||||
const Jid* myjid = connection_get_jid();
|
||||
if (!myjid->str) {
|
||||
log_warning("log_database_get_previous_chat() called but no connection detected.");
|
||||
return DB_RESPONSE_ERROR;
|
||||
}
|
||||
|
||||
// Flip order when querying older pages
|
||||
gchar* sort1 = from_start ? "ASC" : "DESC";
|
||||
gchar* sort2 = !flip ? "ASC" : "DESC";
|
||||
GDateTime* now = g_date_time_new_now_local();
|
||||
auto_gchar gchar* end_date_fmt = end_time ? g_strdup(end_time) : g_date_time_format_iso8601(now);
|
||||
auto_sqlite gchar* query = sqlite3_mprintf("SELECT * FROM ("
|
||||
"SELECT COALESCE(B.`message`, A.`message`) AS message, "
|
||||
"A.`timestamp`, A.`from_jid`, A.`from_resource`, A.`to_jid`, A.`to_resource`, A.`type`, A.`encryption`, A.`stanza_id` FROM `ChatLogs` AS A "
|
||||
"LEFT JOIN `ChatLogs` AS B ON (A.`replaced_by_db_id` = B.`id` AND A.`from_jid` = B.`from_jid`) "
|
||||
"WHERE (A.`replaces_db_id` IS NULL) "
|
||||
"AND ((A.`from_jid` = %Q AND A.`to_jid` = %Q) OR (A.`from_jid` = %Q AND A.`to_jid` = %Q)) "
|
||||
"AND A.`timestamp` < %Q "
|
||||
"AND (%Q IS NULL OR A.`timestamp` > %Q) "
|
||||
"ORDER BY A.`timestamp` %s LIMIT %d) "
|
||||
"ORDER BY `timestamp` %s;",
|
||||
contact_barejid, myjid->barejid, myjid->barejid, contact_barejid, end_date_fmt, start_time, start_time, sort1, MESSAGES_TO_RETRIEVE, sort2);
|
||||
|
||||
g_date_time_unref(now);
|
||||
|
||||
if (!query) {
|
||||
log_error("Could not allocate memory.");
|
||||
return DB_RESPONSE_ERROR;
|
||||
}
|
||||
|
||||
int rc = sqlite3_prepare_v2(g_chatlog_database, query, -1, &stmt, NULL);
|
||||
if (rc != SQLITE_OK) {
|
||||
log_error("SQLite error in log_database_get_previous_chat(): (error code: %d) %s", rc, sqlite3_errmsg(g_chatlog_database));
|
||||
return DB_RESPONSE_ERROR;
|
||||
}
|
||||
|
||||
while (sqlite3_step(stmt) == SQLITE_ROW) {
|
||||
char* message = (char*)sqlite3_column_text(stmt, 0);
|
||||
char* date = (char*)sqlite3_column_text(stmt, 1);
|
||||
char* from_jid = (char*)sqlite3_column_text(stmt, 2);
|
||||
char* from_resource = (char*)sqlite3_column_text(stmt, 3);
|
||||
char* to_jid = (char*)sqlite3_column_text(stmt, 4);
|
||||
char* to_resource = (char*)sqlite3_column_text(stmt, 5);
|
||||
char* type = (char*)sqlite3_column_text(stmt, 6);
|
||||
char* encryption = (char*)sqlite3_column_text(stmt, 7);
|
||||
char* id = (char*)sqlite3_column_text(stmt, 8);
|
||||
|
||||
ProfMessage* msg = message_init();
|
||||
msg->id = id ? strdup(id) : NULL;
|
||||
msg->from_jid = jid_create_from_bare_and_resource(from_jid, from_resource);
|
||||
msg->to_jid = jid_create_from_bare_and_resource(to_jid, to_resource);
|
||||
msg->plain = strdup(message ?: "");
|
||||
msg->timestamp = g_date_time_new_from_iso8601(date, NULL);
|
||||
msg->type = _get_message_type_type(type);
|
||||
msg->enc = _get_message_enc_type(encryption);
|
||||
|
||||
*result = g_slist_append(*result, msg);
|
||||
}
|
||||
sqlite3_finalize(stmt);
|
||||
|
||||
return g_slist_length(*result) != 0 ? DB_RESPONSE_SUCCESS : DB_RESPONSE_EMPTY;
|
||||
return DB_RESPONSE_ERROR;
|
||||
}
|
||||
|
||||
static const char*
|
||||
_get_message_type_str(prof_msg_type_t type)
|
||||
ProfMessage*
|
||||
log_database_get_limits_info(const gchar* const contact_barejid, gboolean is_last)
|
||||
{
|
||||
switch (type) {
|
||||
case PROF_MSG_TYPE_CHAT:
|
||||
return "chat";
|
||||
case PROF_MSG_TYPE_MUC:
|
||||
return "muc";
|
||||
case PROF_MSG_TYPE_MUCPM:
|
||||
return "mucpm";
|
||||
case PROF_MSG_TYPE_UNINITIALIZED:
|
||||
return NULL;
|
||||
if (active_db_backend && active_db_backend->get_limits_info) {
|
||||
return active_db_backend->get_limits_info(contact_barejid, is_last);
|
||||
}
|
||||
return NULL;
|
||||
// Fallback: return an empty message with sane defaults
|
||||
ProfMessage* msg = message_init();
|
||||
if (is_last) {
|
||||
msg->timestamp = g_date_time_new_now_utc();
|
||||
}
|
||||
return msg;
|
||||
}
|
||||
|
||||
static prof_msg_type_t
|
||||
_get_message_type_type(const char* const type)
|
||||
GSList*
|
||||
log_database_verify_integrity(const gchar* const contact_barejid)
|
||||
{
|
||||
if (g_strcmp0(type, "chat") == 0) {
|
||||
return PROF_MSG_TYPE_CHAT;
|
||||
} else if (g_strcmp0(type, "muc") == 0) {
|
||||
return PROF_MSG_TYPE_MUC;
|
||||
} else if (g_strcmp0(type, "mucpm") == 0) {
|
||||
return PROF_MSG_TYPE_MUCPM;
|
||||
} else {
|
||||
return PROF_MSG_TYPE_UNINITIALIZED;
|
||||
}
|
||||
}
|
||||
|
||||
static const char*
|
||||
_get_message_enc_str(prof_enc_t enc)
|
||||
{
|
||||
switch (enc) {
|
||||
case PROF_MSG_ENC_OX:
|
||||
return "ox";
|
||||
case PROF_MSG_ENC_PGP:
|
||||
return "pgp";
|
||||
case PROF_MSG_ENC_OTR:
|
||||
return "otr";
|
||||
case PROF_MSG_ENC_OMEMO:
|
||||
return "omemo";
|
||||
case PROF_MSG_ENC_NONE:
|
||||
return "none";
|
||||
}
|
||||
|
||||
return "none";
|
||||
}
|
||||
|
||||
static prof_enc_t
|
||||
_get_message_enc_type(const char* const encstr)
|
||||
{
|
||||
if (g_strcmp0(encstr, "ox") == 0) {
|
||||
return PROF_MSG_ENC_OX;
|
||||
} else if (g_strcmp0(encstr, "pgp") == 0) {
|
||||
return PROF_MSG_ENC_PGP;
|
||||
} else if (g_strcmp0(encstr, "otr") == 0) {
|
||||
return PROF_MSG_ENC_OTR;
|
||||
} else if (g_strcmp0(encstr, "omemo") == 0) {
|
||||
return PROF_MSG_ENC_OMEMO;
|
||||
}
|
||||
|
||||
return PROF_MSG_ENC_NONE;
|
||||
}
|
||||
|
||||
static void
|
||||
_add_to_db(ProfMessage* message, char* type, const Jid* const from_jid, const Jid* const to_jid)
|
||||
{
|
||||
auto_gchar gchar* pref_dblog = prefs_get_string(PREF_DBLOG);
|
||||
sqlite_int64 original_message_id = -1;
|
||||
|
||||
if (g_strcmp0(pref_dblog, "off") == 0) {
|
||||
return;
|
||||
} else if (g_strcmp0(pref_dblog, "redact") == 0) {
|
||||
if (message->plain) {
|
||||
free(message->plain);
|
||||
}
|
||||
message->plain = strdup("[REDACTED]");
|
||||
}
|
||||
|
||||
if (!g_chatlog_database) {
|
||||
log_debug("log_database_add() called but db is not initialized");
|
||||
return;
|
||||
}
|
||||
|
||||
char* err_msg;
|
||||
auto_gchar gchar* date_fmt = NULL;
|
||||
|
||||
if (message->timestamp) {
|
||||
date_fmt = g_date_time_format_iso8601(message->timestamp);
|
||||
} else {
|
||||
GDateTime* dt = g_date_time_new_now_local();
|
||||
date_fmt = g_date_time_format_iso8601(dt);
|
||||
g_date_time_unref(dt);
|
||||
}
|
||||
|
||||
const char* enc = _get_message_enc_str(message->enc);
|
||||
|
||||
if (!type) {
|
||||
type = (char*)_get_message_type_str(message->type);
|
||||
}
|
||||
|
||||
// Apply LMC and check its validity (XEP-0308)
|
||||
if (message->replace_id) {
|
||||
auto_sqlite char* replace_check_query = sqlite3_mprintf("SELECT `id`, `from_jid`, `replaces_db_id` FROM `ChatLogs` WHERE `stanza_id` = %Q ORDER BY `timestamp` DESC LIMIT 1",
|
||||
message->replace_id);
|
||||
|
||||
if (!replace_check_query) {
|
||||
log_error("Could not allocate memory for SQL replace query in log_database_add()");
|
||||
return;
|
||||
}
|
||||
|
||||
sqlite3_stmt* lmc_stmt = NULL;
|
||||
|
||||
if (SQLITE_OK != sqlite3_prepare_v2(g_chatlog_database, replace_check_query, -1, &lmc_stmt, NULL)) {
|
||||
log_error("SQLite error in _add_to_db() on selecting original message: %s", sqlite3_errmsg(g_chatlog_database));
|
||||
return;
|
||||
}
|
||||
|
||||
if (sqlite3_step(lmc_stmt) == SQLITE_ROW) {
|
||||
original_message_id = sqlite3_column_int64(lmc_stmt, 0);
|
||||
const char* from_jid_orig = (const char*)sqlite3_column_text(lmc_stmt, 1);
|
||||
|
||||
// Handle non-XEP-compliant replacement messages (edit->edit->original)
|
||||
sqlite_int64 tmp = sqlite3_column_int64(lmc_stmt, 2);
|
||||
original_message_id = tmp ? tmp : original_message_id;
|
||||
|
||||
if (g_strcmp0(from_jid_orig, from_jid->barejid) != 0) {
|
||||
log_error("Mismatch in sender JIDs when trying to do LMC. Corrected message sender: %s. Original message sender: %s. Replace-ID: %s. Message: %s", from_jid->barejid, from_jid_orig, message->replace_id, message->plain);
|
||||
cons_show_error("%s sent a message correction with mismatched sender. See log for details.", from_jid->barejid);
|
||||
sqlite3_finalize(lmc_stmt);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
log_warning("Got LMC message that does not have original message counterpart in the database from %s", message->from_jid->fulljid);
|
||||
}
|
||||
sqlite3_finalize(lmc_stmt);
|
||||
}
|
||||
|
||||
// stanza-id (XEP-0359) doesn't have to be present in the message.
|
||||
// But if it's duplicated, it's a serious server-side problem, so we better track it.
|
||||
// Unless it's MAM, in that case it's expected behaviour.
|
||||
if (message->stanzaid && !message->is_mam) {
|
||||
auto_sqlite char* duplicate_check_query = sqlite3_mprintf("SELECT 1 FROM `ChatLogs` WHERE (`archive_id` = %Q)",
|
||||
message->stanzaid);
|
||||
|
||||
if (!duplicate_check_query) {
|
||||
log_error("Could not allocate memory for SQL duplicate query in log_database_add()");
|
||||
return;
|
||||
}
|
||||
|
||||
sqlite3_stmt* stmt;
|
||||
|
||||
if (SQLITE_OK == sqlite3_prepare_v2(g_chatlog_database, duplicate_check_query, -1, &stmt, NULL)) {
|
||||
if (sqlite3_step(stmt) == SQLITE_ROW) {
|
||||
log_error("Duplicate stanza-id found for the message. stanza_id: %s; archive_id: %s; sender: %s; content: %s", message->id, message->stanzaid, from_jid->barejid, message->plain);
|
||||
cons_show_error("Got a message with duplicate (server-generated) stanza-id from %s.", from_jid->fulljid);
|
||||
}
|
||||
sqlite3_finalize(stmt);
|
||||
}
|
||||
}
|
||||
|
||||
auto_sqlite char* orig_message_id = original_message_id == -1 ? NULL : sqlite3_mprintf("%d", original_message_id);
|
||||
|
||||
auto_sqlite char* query = sqlite3_mprintf("INSERT INTO `ChatLogs` "
|
||||
"(`from_jid`, `from_resource`, `to_jid`, `to_resource`, "
|
||||
"`message`, `timestamp`, `stanza_id`, `archive_id`, "
|
||||
"`replaces_db_id`, `replace_id`, `type`, `encryption`) "
|
||||
"VALUES (%Q, %Q, %Q, %Q, %Q, %Q, %Q, %Q, %Q, %Q, %Q, %Q)",
|
||||
from_jid->barejid,
|
||||
from_jid->resourcepart,
|
||||
to_jid->barejid,
|
||||
to_jid->resourcepart,
|
||||
message->plain,
|
||||
date_fmt,
|
||||
message->id,
|
||||
message->stanzaid,
|
||||
orig_message_id,
|
||||
message->replace_id,
|
||||
type,
|
||||
enc);
|
||||
if (!query) {
|
||||
log_error("Could not allocate memory for SQL insert query in log_database_add()");
|
||||
return;
|
||||
}
|
||||
|
||||
log_debug("Writing to DB. Query: %s", query);
|
||||
|
||||
if (SQLITE_OK != sqlite3_exec(g_chatlog_database, query, NULL, 0, &err_msg)) {
|
||||
if (err_msg) {
|
||||
log_error("SQLite error in _add_to_db(): %s", err_msg);
|
||||
sqlite3_free(err_msg);
|
||||
} else {
|
||||
log_error("Unknown SQLite error in _add_to_db().");
|
||||
}
|
||||
} else {
|
||||
int inserted_rows_count = sqlite3_changes(g_chatlog_database);
|
||||
if (inserted_rows_count < 1) {
|
||||
log_error("SQLite did not insert message (rows: %d, id: %s, content: %s)", inserted_rows_count, message->id, message->plain);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static int
|
||||
_get_db_version(void)
|
||||
{
|
||||
int current_version = -1;
|
||||
const char* query = "SELECT `version` FROM `DbVersion` LIMIT 1";
|
||||
sqlite3_stmt* statement;
|
||||
|
||||
if (sqlite3_prepare_v2(g_chatlog_database, query, -1, &statement, NULL) == SQLITE_OK) {
|
||||
if (sqlite3_step(statement) == SQLITE_ROW) {
|
||||
current_version = sqlite3_column_int(statement, 0);
|
||||
}
|
||||
sqlite3_finalize(statement);
|
||||
}
|
||||
return current_version;
|
||||
}
|
||||
|
||||
/**
|
||||
* Migration to version 2 introduces new columns. Returns TRUE on success.
|
||||
*
|
||||
* New columns:
|
||||
* `replaces_db_id` database ID for correcting message of the original message
|
||||
* `replaced_by_db_id` database ID for original message of the last correcting message
|
||||
*/
|
||||
static gboolean
|
||||
_migrate_to_v2(void)
|
||||
{
|
||||
char* err_msg = NULL;
|
||||
|
||||
// from_resource, to_resource, message, timestamp, stanza_id, archive_id, replace_id, type, encryption
|
||||
const char* sql_statements[] = {
|
||||
"BEGIN TRANSACTION",
|
||||
"ALTER TABLE `ChatLogs` ADD COLUMN `replaces_db_id` INTEGER;",
|
||||
"ALTER TABLE `ChatLogs` ADD COLUMN `replaced_by_db_id` INTEGER;",
|
||||
"UPDATE `ChatLogs` AS A "
|
||||
"SET `replaces_db_id` = B.`id` "
|
||||
"FROM `ChatLogs` AS B "
|
||||
"WHERE A.`replace_id` IS NOT NULL AND A.`replace_id` != '' "
|
||||
"AND A.`replace_id` = B.`stanza_id` "
|
||||
"AND A.`from_jid` = B.`from_jid` AND A.`to_jid` = B.`to_jid`;",
|
||||
"UPDATE `ChatLogs` AS A "
|
||||
"SET `replaced_by_db_id` = B.`id` "
|
||||
"FROM `ChatLogs` AS B "
|
||||
"WHERE (A.`replace_id` IS NULL OR A.`replace_id` = '') "
|
||||
"AND A.`id` = B.`replaces_db_id` "
|
||||
"AND A.`from_jid` = B.`from_jid`;",
|
||||
"UPDATE ChatLogs SET "
|
||||
"from_resource = COALESCE(NULLIF(from_resource, ''), NULL), "
|
||||
"to_resource = COALESCE(NULLIF(to_resource, ''), NULL), "
|
||||
"message = COALESCE(NULLIF(message, ''), NULL), "
|
||||
"timestamp = COALESCE(NULLIF(timestamp, ''), NULL), "
|
||||
"stanza_id = COALESCE(NULLIF(stanza_id, ''), NULL), "
|
||||
"archive_id = COALESCE(NULLIF(archive_id, ''), NULL), "
|
||||
"replace_id = COALESCE(NULLIF(replace_id, ''), NULL), "
|
||||
"type = COALESCE(NULLIF(type, ''), NULL), "
|
||||
"encryption = COALESCE(NULLIF(encryption, ''), NULL);",
|
||||
"UPDATE `DbVersion` SET `version` = 2;",
|
||||
"END TRANSACTION"
|
||||
};
|
||||
|
||||
int statements_count = sizeof(sql_statements) / sizeof(sql_statements[0]);
|
||||
|
||||
for (int i = 0; i < statements_count; i++) {
|
||||
if (SQLITE_OK != sqlite3_exec(g_chatlog_database, sql_statements[i], NULL, 0, &err_msg)) {
|
||||
log_error("SQLite error in _migrate_to_v2() on statement %d: %s", i, err_msg);
|
||||
if (err_msg) {
|
||||
sqlite3_free(err_msg);
|
||||
err_msg = NULL;
|
||||
}
|
||||
goto cleanup;
|
||||
}
|
||||
}
|
||||
|
||||
return TRUE;
|
||||
|
||||
cleanup:
|
||||
if (SQLITE_OK != sqlite3_exec(g_chatlog_database, "ROLLBACK;", NULL, 0, &err_msg)) {
|
||||
log_error("[DB Migration] Unable to ROLLBACK: %s", err_msg);
|
||||
if (err_msg) {
|
||||
sqlite3_free(err_msg);
|
||||
}
|
||||
}
|
||||
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
// Checks if there is more system storage space available than current database takes + 40% (for indexing and other potential size increases)
|
||||
static gboolean
|
||||
_check_available_space_for_db_migration(char* path_to_db)
|
||||
{
|
||||
struct stat file_stat;
|
||||
struct statvfs fs_stat;
|
||||
|
||||
if (statvfs(path_to_db, &fs_stat) == 0 && stat(path_to_db, &file_stat) == 0) {
|
||||
unsigned long long file_size = file_stat.st_size / 1024;
|
||||
unsigned long long available_space_kb = fs_stat.f_frsize * fs_stat.f_bavail / 1024;
|
||||
log_debug("_check_available_space_for_db_migration(): Available space on disk: %llu KB; DB size: %llu KB", available_space_kb, file_size);
|
||||
|
||||
return (available_space_kb >= (file_size + (file_size * 10 / 4)));
|
||||
} else {
|
||||
log_error("Error checking available space.");
|
||||
return FALSE;
|
||||
if (active_db_backend && active_db_backend->verify_integrity) {
|
||||
return active_db_backend->verify_integrity(contact_barejid);
|
||||
}
|
||||
|
||||
GSList* issues = NULL;
|
||||
integrity_issue_t* issue = g_malloc0(sizeof(integrity_issue_t));
|
||||
issue->level = INTEGRITY_WARNING;
|
||||
issue->file = g_strdup("N/A");
|
||||
issue->line = 0;
|
||||
issue->message = g_strdup("Active backend does not support integrity verification");
|
||||
issues = g_slist_append(issues, issue);
|
||||
return issues;
|
||||
}
|
||||
|
||||
@@ -2,35 +2,9 @@
|
||||
* database.h
|
||||
* vim: expandtab:ts=4:sts=4:sw=4
|
||||
*
|
||||
* Copyright (C) 2020 - 2025 Michael Vetter <jubalh@iodoru.org>
|
||||
*
|
||||
* This file is part of Profanity.
|
||||
*
|
||||
* Profanity is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Profanity is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Profanity. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* In addition, as a special exception, the copyright holders give permission to
|
||||
* link the code of portions of this program with the OpenSSL library under
|
||||
* certain conditions as described in each individual source file, and
|
||||
* distribute linked combinations including the two.
|
||||
*
|
||||
* You must obey the GNU General Public License in all respects for all of the
|
||||
* code used other than OpenSSL. If you modify file(s) with this exception, you
|
||||
* may extend this exception to your version of the file(s), but you are not
|
||||
* obligated to do so. If you do not wish to do so, delete this exception
|
||||
* statement from your version. If you delete this exception statement from all
|
||||
* source files in the program, then also delete it here.
|
||||
* Copyright (C) 2020 - 2026 Michael Vetter <jubalh@iodoru.org>
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later WITH OpenSSL-exception
|
||||
*/
|
||||
|
||||
#ifndef DATABASE_H
|
||||
@@ -48,7 +22,58 @@ typedef enum {
|
||||
DB_RESPONSE_SUCCESS
|
||||
} db_history_result_t;
|
||||
|
||||
// Integrity verification issue levels
|
||||
typedef enum {
|
||||
INTEGRITY_ERROR,
|
||||
INTEGRITY_WARNING,
|
||||
INTEGRITY_INFO
|
||||
} integrity_level_t;
|
||||
|
||||
// A single integrity issue found during verification
|
||||
typedef struct
|
||||
{
|
||||
integrity_level_t level;
|
||||
char* file;
|
||||
int line;
|
||||
char* message;
|
||||
} integrity_issue_t;
|
||||
|
||||
void integrity_issue_free(integrity_issue_t* issue);
|
||||
|
||||
// Backend vtable: pluggable storage backends implement this interface
|
||||
typedef struct db_backend_t
|
||||
{
|
||||
const char* name;
|
||||
gboolean (*init)(ProfAccount* account);
|
||||
void (*close)(void);
|
||||
void (*add_incoming)(ProfMessage* message);
|
||||
void (*add_outgoing_chat)(const char* const id, const char* const barejid, const char* const message, const char* const replace_id, prof_enc_t enc);
|
||||
void (*add_outgoing_muc)(const char* const id, const char* const barejid, const char* const message, const char* const replace_id, prof_enc_t enc);
|
||||
void (*add_outgoing_muc_pm)(const char* const id, const char* const barejid, const char* const message, const char* const replace_id, prof_enc_t enc);
|
||||
db_history_result_t (*get_previous_chat)(const gchar* const contact_barejid, const gchar* start_time, const gchar* end_time, gboolean from_start, gboolean flip, GSList** result);
|
||||
ProfMessage* (*get_limits_info)(const gchar* const contact_barejid, gboolean is_last);
|
||||
GSList* (*verify_integrity)(const gchar* const contact_barejid);
|
||||
} db_backend_t;
|
||||
|
||||
// Active backend (set during init based on PREF_DBLOG)
|
||||
extern db_backend_t* active_db_backend;
|
||||
|
||||
// Backend registry
|
||||
#ifdef HAVE_SQLITE
|
||||
db_backend_t* db_backend_sqlite(void);
|
||||
GSList* db_sqlite_list_contacts(void);
|
||||
GSList* db_sqlite_get_all_chat(const gchar* const contact_barejid);
|
||||
void db_sqlite_begin_transaction(void);
|
||||
void db_sqlite_end_transaction(void);
|
||||
void db_sqlite_rollback_transaction(void);
|
||||
int db_sqlite_last_changes(void);
|
||||
gboolean db_sqlite_is_open(void);
|
||||
#endif
|
||||
db_backend_t* db_backend_flatfile(void);
|
||||
|
||||
// Public API (dispatches to active_db_backend)
|
||||
gboolean log_database_init(ProfAccount* account);
|
||||
gboolean log_database_switch_backend(const char* new_backend);
|
||||
void log_database_add_incoming(ProfMessage* message);
|
||||
void log_database_add_outgoing_chat(const char* const id, const char* const barejid, const char* const message, const char* const replace_id, prof_enc_t enc);
|
||||
void log_database_add_outgoing_muc(const char* const id, const char* const barejid, const char* const message, const char* const replace_id, prof_enc_t enc);
|
||||
@@ -56,5 +81,15 @@ void log_database_add_outgoing_muc_pm(const char* const id, const char* const ba
|
||||
db_history_result_t log_database_get_previous_chat(const gchar* const contact_barejid, const gchar* start_time, const gchar* end_time, gboolean from_start, gboolean flip, GSList** result);
|
||||
ProfMessage* log_database_get_limits_info(const gchar* const contact_barejid, gboolean is_last);
|
||||
void log_database_close(void);
|
||||
GSList* log_database_verify_integrity(const gchar* const contact_barejid);
|
||||
|
||||
// FALSE if no backend, or PREF_DBLOG is "off" / "redact".
|
||||
gboolean log_database_can_recover_messages(void);
|
||||
|
||||
// Cross-backend export/import (requires HAVE_SQLITE)
|
||||
#ifdef HAVE_SQLITE
|
||||
int log_database_export_to_flatfile(const gchar* const contact_jid);
|
||||
int log_database_import_from_flatfile(const gchar* const contact_jid);
|
||||
#endif
|
||||
|
||||
#endif // DATABASE_H
|
||||
|
||||
680
src/database_export.c
Normal file
680
src/database_export.c
Normal file
@@ -0,0 +1,680 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
//
|
||||
// This file is part of CProof.
|
||||
// See LICENSE for the full GPLv3 text and the special OpenSSL linking exception.
|
||||
|
||||
/*
|
||||
* database_export.c
|
||||
* vim: expandtab:ts=4:sts=4:sw=4
|
||||
*
|
||||
* Cross-backend export/import for message history.
|
||||
* Reads from one backend, writes to the other, with merge + dedup.
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <glib.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <unistd.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/file.h>
|
||||
|
||||
#ifdef HAVE_SQLITE
|
||||
#include <sqlite3.h>
|
||||
#endif
|
||||
|
||||
#include "log.h"
|
||||
#include "common.h"
|
||||
#include "config/files.h"
|
||||
#include "config/accounts.h"
|
||||
#include "database.h"
|
||||
#include "database_flatfile.h"
|
||||
#include "config/preferences.h"
|
||||
#include "ui/ui.h"
|
||||
#include "xmpp/session.h"
|
||||
#include "xmpp/xmpp.h"
|
||||
#include "xmpp/message.h"
|
||||
|
||||
// =========================================================================
|
||||
// Everything below is only used when HAVE_SQLITE is defined
|
||||
// =========================================================================
|
||||
|
||||
#ifdef HAVE_SQLITE
|
||||
|
||||
// Print progress to console every N messages during export/import
|
||||
#define EXPORT_PROGRESS_INTERVAL 500
|
||||
|
||||
// Open SQLite if it isn't already (e.g. when flatfile is the active backend
|
||||
// and sqlite was never initialised this session, or was closed by /history
|
||||
// switch). Returns 1 if we opened it (caller must close), 0 if it was already
|
||||
// open, -1 on failure.
|
||||
static int
|
||||
_ensure_sqlite_open(void)
|
||||
{
|
||||
if (db_sqlite_is_open())
|
||||
return 0;
|
||||
|
||||
db_backend_t* sqlite_be = db_backend_sqlite();
|
||||
if (!sqlite_be || !sqlite_be->init)
|
||||
return -1;
|
||||
|
||||
const char* account_name = session_get_account_name();
|
||||
if (!account_name)
|
||||
return -1;
|
||||
|
||||
ProfAccount* account = accounts_get_account(account_name);
|
||||
if (!account)
|
||||
return -1;
|
||||
|
||||
gboolean ok = sqlite_be->init(account);
|
||||
account_free(account);
|
||||
return ok ? 1 : -1;
|
||||
}
|
||||
|
||||
static void
|
||||
_close_temp_sqlite(int opened)
|
||||
{
|
||||
if (opened != 1)
|
||||
return;
|
||||
db_backend_t* sqlite_be = db_backend_sqlite();
|
||||
if (sqlite_be && sqlite_be->close)
|
||||
sqlite_be->close();
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Dedup key: stanza_id if present, else hash of timestamp+from+body
|
||||
// =========================================================================
|
||||
|
||||
// Returns a g_strdup'd key — callers free with g_free (gchar* convention).
|
||||
//
|
||||
// stanza_id is mixed into the hash but is NOT used as the sole key, since
|
||||
// some clients (older Pidgin, Adium) reuse incremental ids per session and
|
||||
// servers echo them back unchanged. Trusting stanza_id alone silently drops
|
||||
// legitimate distinct messages on collision. By hashing id + timestamp +
|
||||
// from_jid + body together we preserve id as a disambiguator without making
|
||||
// it a single point of failure: true exact duplicates still dedup, but two
|
||||
// messages that share an id with different content (or different timestamps)
|
||||
// stay distinct.
|
||||
static gchar*
|
||||
_make_dedup_key(const char* stanza_id, const char* timestamp, const char* from_jid, const char* body)
|
||||
{
|
||||
GChecksum* sum = g_checksum_new(G_CHECKSUM_SHA256);
|
||||
g_checksum_update(sum, (const guchar*)(stanza_id ? stanza_id : ""), -1);
|
||||
g_checksum_update(sum, (const guchar*)"|", 1);
|
||||
g_checksum_update(sum, (const guchar*)(timestamp ? timestamp : ""), -1);
|
||||
g_checksum_update(sum, (const guchar*)"|", 1);
|
||||
g_checksum_update(sum, (const guchar*)(from_jid ? from_jid : ""), -1);
|
||||
g_checksum_update(sum, (const guchar*)"|", 1);
|
||||
g_checksum_update(sum, (const guchar*)(body ? body : ""), -1);
|
||||
gchar* key = g_strdup(g_checksum_get_string(sum));
|
||||
g_checksum_free(sum);
|
||||
return key;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Helper: reverse ff_jid_to_dir (best-effort)
|
||||
// '_at_' -> '@'
|
||||
// Returns a g_strdup'd jid — callers free with g_free (gchar* convention).
|
||||
// str_replace allocates via libc malloc, so we hand over to g_-allocated.
|
||||
// =========================================================================
|
||||
|
||||
static gchar*
|
||||
_dir_to_jid(const char* dirname)
|
||||
{
|
||||
if (!dirname)
|
||||
return NULL;
|
||||
char* raw = str_replace(dirname, "_at_", "@");
|
||||
if (!raw)
|
||||
return NULL;
|
||||
gchar* gres = g_strdup(raw);
|
||||
free(raw);
|
||||
return gres;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Helper: list all contact JIDs from flatfile directory
|
||||
// Returns a GSList of g_strdup'd JID strings. Caller frees with g_slist_free_full(list, g_free).
|
||||
// =========================================================================
|
||||
|
||||
static GSList*
|
||||
_ff_list_contacts(void)
|
||||
{
|
||||
GSList* contacts = NULL;
|
||||
|
||||
if (!g_flatfile_account_jid)
|
||||
return NULL;
|
||||
|
||||
auto_gchar gchar* data_path = files_get_data_path(DIR_FLATLOG);
|
||||
auto_gchar gchar* my_dir = ff_jid_to_dir(g_flatfile_account_jid);
|
||||
auto_gchar gchar* base_dir = g_strdup_printf("%s/%s", data_path, my_dir);
|
||||
|
||||
GError* err = NULL;
|
||||
GDir* dir = g_dir_open(base_dir, 0, &err);
|
||||
if (!dir) {
|
||||
if (err) {
|
||||
log_warning("export: cannot open flatlog dir %s: %s", base_dir, err->message);
|
||||
g_error_free(err);
|
||||
} else {
|
||||
log_warning("export: cannot open flatlog dir %s (no error details)", base_dir);
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
const gchar* entry;
|
||||
while ((entry = g_dir_read_name(dir)) != NULL) {
|
||||
auto_gchar gchar* full_path = g_strdup_printf("%s/%s/history.log", base_dir, entry);
|
||||
if (g_file_test(full_path, G_FILE_TEST_IS_REGULAR)) {
|
||||
gchar* jid = _dir_to_jid(entry);
|
||||
if (jid) {
|
||||
contacts = g_slist_prepend(contacts, jid);
|
||||
}
|
||||
}
|
||||
}
|
||||
g_dir_close(dir);
|
||||
|
||||
return g_slist_reverse(contacts);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Helper: read ALL parsed lines from a flatfile for a contact
|
||||
// Returns GSList<ff_parsed_line_t*>. Caller frees with g_slist_free_full(list, (GDestroyNotify)ff_parsed_line_free).
|
||||
// =========================================================================
|
||||
|
||||
// Hard ceiling on the total size of a single contact's history.log that
|
||||
// _ff_read_all_lines is willing to slurp into memory. Each line expands
|
||||
// ~10x on parse (12 g_strdup'd fields + GDateTime), so 2 GB on disk
|
||||
// becomes ~20 GB peak heap — well past any realistic working set.
|
||||
#define FF_EXPORT_MAX_FILE_BYTES (2LL * 1024 * 1024 * 1024)
|
||||
|
||||
static GSList*
|
||||
_ff_read_all_lines(const char* contact_barejid)
|
||||
{
|
||||
GSList* lines = NULL;
|
||||
auto_gchar gchar* log_path = ff_get_log_path(contact_barejid);
|
||||
if (!log_path)
|
||||
return NULL;
|
||||
|
||||
// Refuse upfront if the file is so large that parsing will OOM the
|
||||
// process. We have no streaming-parse path; the whole file becomes
|
||||
// a GSList of ff_parsed_line_t in memory. Surface a clear error so
|
||||
// the user knows what hit them, rather than the kernel killing prof.
|
||||
struct stat st;
|
||||
if (stat(log_path, &st) == 0 && st.st_size > FF_EXPORT_MAX_FILE_BYTES) {
|
||||
log_error("export: %s is %lld bytes (> %lld limit); refusing to load.",
|
||||
log_path, (long long)st.st_size,
|
||||
(long long)FF_EXPORT_MAX_FILE_BYTES);
|
||||
cons_show_error("Flat-file for %s exceeds %lld MB load limit; cannot export.",
|
||||
contact_barejid, (long long)(FF_EXPORT_MAX_FILE_BYTES / (1024 * 1024)));
|
||||
return NULL;
|
||||
}
|
||||
|
||||
FILE* fp = fopen(log_path, "r");
|
||||
if (!fp)
|
||||
return NULL;
|
||||
|
||||
while (1) {
|
||||
gboolean truncated = FALSE;
|
||||
auto_char char* buf = ff_readline(fp, &truncated);
|
||||
if (!buf)
|
||||
break;
|
||||
if (truncated)
|
||||
break;
|
||||
if (buf[0] == '\0' || buf[0] == '#')
|
||||
continue;
|
||||
|
||||
ff_parsed_line_t* pl = ff_parse_line(buf);
|
||||
if (pl) {
|
||||
lines = g_slist_prepend(lines, pl);
|
||||
}
|
||||
}
|
||||
fclose(fp);
|
||||
|
||||
return g_slist_reverse(lines);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Comparator for sorting ff_parsed_line_t by timestamp (ISO8601 is lexicographic)
|
||||
// =========================================================================
|
||||
|
||||
static gint
|
||||
_compare_parsed_lines_by_timestamp(gconstpointer a, gconstpointer b)
|
||||
{
|
||||
const ff_parsed_line_t* la = a;
|
||||
const ff_parsed_line_t* lb = b;
|
||||
int cmp = g_strcmp0(la->timestamp_str, lb->timestamp_str);
|
||||
if (cmp != 0)
|
||||
return cmp;
|
||||
// Secondary key: stanza_id (stabilises sort for same-second messages)
|
||||
cmp = g_strcmp0(la->stanza_id, lb->stanza_id);
|
||||
if (cmp != 0)
|
||||
return cmp;
|
||||
return g_strcmp0(la->from_jid, lb->from_jid);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Export: SQLite -> flatfile (merge)
|
||||
// =========================================================================
|
||||
|
||||
// Callback data for the SQLite query
|
||||
typedef struct
|
||||
{
|
||||
GHashTable* seen_keys; // dedup set
|
||||
FILE* fp; // output file
|
||||
int exported; // counter
|
||||
int skipped; // counter
|
||||
} export_ctx_t;
|
||||
|
||||
// Export a single contact's messages from SQLite to flatfile.
|
||||
// Returns the number of newly exported messages, or -1 on error/skip.
|
||||
static int
|
||||
_export_one_contact(const char* contact)
|
||||
{
|
||||
// 1. Read existing flatfile lines for dedup
|
||||
GHashTable* seen_keys = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL);
|
||||
|
||||
GSList* existing = _ff_read_all_lines(contact);
|
||||
for (GSList* l = existing; l; l = l->next) {
|
||||
ff_parsed_line_t* pl = l->data;
|
||||
gchar* key = _make_dedup_key(pl->stanza_id, pl->timestamp_str, pl->from_jid, pl->message);
|
||||
g_hash_table_add(seen_keys, key);
|
||||
}
|
||||
int existing_count = (int)g_slist_length(existing);
|
||||
|
||||
// 2. Query SQLite for this contact — get ALL messages via direct SQL
|
||||
GSList* sqlite_lines = db_sqlite_get_all_chat(contact);
|
||||
|
||||
if (!sqlite_lines) {
|
||||
log_debug("export: no SQLite messages for %s", contact);
|
||||
g_hash_table_destroy(seen_keys);
|
||||
g_slist_free_full(existing, (GDestroyNotify)ff_parsed_line_free);
|
||||
return -1;
|
||||
}
|
||||
|
||||
// 3. Merge: write existing flatfile lines + new SQLite lines to temp file
|
||||
auto_gchar gchar* log_path = ff_get_log_path(contact);
|
||||
if (!log_path) {
|
||||
g_hash_table_destroy(seen_keys);
|
||||
g_slist_free_full(existing, (GDestroyNotify)ff_parsed_line_free);
|
||||
g_slist_free_full(sqlite_lines, (GDestroyNotify)message_free);
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Ensure directory
|
||||
auto_gchar gchar* dir = g_path_get_dirname(log_path);
|
||||
ff_ensure_dir(dir);
|
||||
|
||||
// Use mkstemp() to create a unique-named temp file. The previous fixed
|
||||
// "{log_path}.export.tmp" name + unlink-retry was vulnerable to two
|
||||
// concurrent exports clobbering each other's in-flight tmp files
|
||||
// (process B unlinks A's mid-write tmp, both race to write, second
|
||||
// rename wins). Random suffix eliminates that.
|
||||
//
|
||||
// mkstemp on glibc creates the file with mode 0600. We fchmod to be
|
||||
// sure across libc implementations. mkstemp uses O_RDWR|O_CREAT|O_EXCL
|
||||
// internally; symlink attacks aren't possible because the name was
|
||||
// just generated and no other process knows it.
|
||||
auto_gchar gchar* tmp_path = g_strdup_printf("%s.export.XXXXXX", log_path);
|
||||
int tmp_fd = mkstemp(tmp_path);
|
||||
if (tmp_fd < 0) {
|
||||
log_error("export: mkstemp(%s) failed: %s", tmp_path, strerror(errno));
|
||||
g_hash_table_destroy(seen_keys);
|
||||
g_slist_free_full(existing, (GDestroyNotify)ff_parsed_line_free);
|
||||
g_slist_free_full(sqlite_lines, (GDestroyNotify)message_free);
|
||||
return -1;
|
||||
}
|
||||
if (fchmod(tmp_fd, S_IRUSR | S_IWUSR) != 0) {
|
||||
log_warning("export: fchmod(%s, 0600) failed: %s", tmp_path, strerror(errno));
|
||||
}
|
||||
FILE* fp = fdopen(tmp_fd, "w");
|
||||
if (!fp) {
|
||||
log_error("export: fdopen failed for %s: %s", tmp_path, strerror(errno));
|
||||
close(tmp_fd);
|
||||
unlink(tmp_path);
|
||||
g_hash_table_destroy(seen_keys);
|
||||
g_slist_free_full(existing, (GDestroyNotify)ff_parsed_line_free);
|
||||
g_slist_free_full(sqlite_lines, (GDestroyNotify)message_free);
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Lock the temp file
|
||||
int fd = fileno(fp);
|
||||
if (flock(fd, LOCK_EX) != 0) {
|
||||
log_warning("export: flock(%s) failed: %s", tmp_path, strerror(errno));
|
||||
}
|
||||
|
||||
fprintf(fp, "%s", FLATFILE_HEADER);
|
||||
|
||||
int contact_exported = 0;
|
||||
int contact_skipped = 0;
|
||||
|
||||
// Collect ALL lines into a merged list for sorted output.
|
||||
// Transfer ownership of every parsed line from `existing` directly into
|
||||
// `merged` instead of deep-copying — saves ~12 g_strdup calls per row,
|
||||
// which dominate wall time at 100k+ rows. After splicing we free the
|
||||
// list nodes (not the data) and NULL out existing so the final cleanup
|
||||
// doesn't double-free.
|
||||
GSList* merged = NULL;
|
||||
for (GSList* l = existing; l; l = l->next) {
|
||||
merged = g_slist_prepend(merged, l->data);
|
||||
}
|
||||
g_slist_free(existing);
|
||||
existing = NULL;
|
||||
|
||||
// Add SQLite messages that aren't already in the flatfile
|
||||
for (GSList* l = sqlite_lines; l; l = l->next) {
|
||||
ProfMessage* msg = l->data;
|
||||
if (!msg || !msg->timestamp)
|
||||
continue;
|
||||
|
||||
auto_gchar gchar* ts = g_date_time_format_iso8601(msg->timestamp);
|
||||
const char* from_jid = msg->from_jid ? msg->from_jid->barejid : "unknown";
|
||||
const char* body = msg->plain ? msg->plain : "";
|
||||
const char* sid = msg->id ? msg->id : "";
|
||||
|
||||
gchar* key = _make_dedup_key(sid, ts, from_jid, body);
|
||||
if (g_hash_table_contains(seen_keys, key)) {
|
||||
g_free(key);
|
||||
contact_skipped++;
|
||||
continue;
|
||||
}
|
||||
g_hash_table_add(seen_keys, key);
|
||||
|
||||
ff_parsed_line_t* pl = g_malloc0(sizeof(ff_parsed_line_t));
|
||||
pl->timestamp_str = g_strdup(ts);
|
||||
pl->timestamp = g_date_time_ref(msg->timestamp);
|
||||
pl->type = g_strdup(ff_get_message_type_str(msg->type));
|
||||
pl->enc = g_strdup(ff_get_message_enc_str(msg->enc));
|
||||
pl->stanza_id = g_strdup(sid);
|
||||
pl->archive_id = msg->stanzaid ? g_strdup(msg->stanzaid) : NULL;
|
||||
pl->replace_id = NULL;
|
||||
pl->from_jid = g_strdup(from_jid);
|
||||
pl->from_resource = msg->from_jid ? g_strdup(msg->from_jid->resourcepart) : NULL;
|
||||
pl->to_jid = msg->to_jid ? g_strdup(msg->to_jid->barejid) : NULL;
|
||||
pl->to_resource = msg->to_jid ? g_strdup(msg->to_jid->resourcepart) : NULL;
|
||||
pl->marked_read = msg->marked_read;
|
||||
pl->message = g_strdup(body);
|
||||
merged = g_slist_prepend(merged, pl);
|
||||
contact_exported++;
|
||||
}
|
||||
|
||||
// Sort merged list by timestamp (ISO8601 is lexicographically sortable)
|
||||
merged = g_slist_sort(merged, _compare_parsed_lines_by_timestamp);
|
||||
|
||||
// Cache the total length once — calling g_slist_length() inside the
|
||||
// write loop is O(n) per call, turning the loop into O(n²) wall time
|
||||
// (catastrophic at >100k rows).
|
||||
int merged_total = (int)g_slist_length(merged);
|
||||
|
||||
// Write all lines sorted
|
||||
int written = 0;
|
||||
for (GSList* l = merged; l; l = l->next) {
|
||||
ff_parsed_line_t* pl = l->data;
|
||||
ff_write_line(fp, pl->timestamp_str,
|
||||
pl->type, pl->enc,
|
||||
pl->stanza_id, pl->archive_id, pl->replace_id,
|
||||
pl->from_jid, pl->from_resource,
|
||||
pl->to_jid, pl->to_resource, pl->marked_read,
|
||||
pl->message);
|
||||
written++;
|
||||
if (written % EXPORT_PROGRESS_INTERVAL == 0) {
|
||||
cons_show(" ... %s: %d/%d written so far", contact, written, merged_total);
|
||||
}
|
||||
}
|
||||
|
||||
g_slist_free_full(merged, (GDestroyNotify)ff_parsed_line_free);
|
||||
|
||||
fflush(fp);
|
||||
fsync(fd); // ensure data is on disk before atomic rename
|
||||
fclose(fp); // also releases flock
|
||||
|
||||
// Atomic rename
|
||||
int result = -1;
|
||||
if (rename(tmp_path, log_path) != 0) {
|
||||
log_error("export: rename %s -> %s failed: %s", tmp_path, log_path, strerror(errno));
|
||||
cons_show_error("Export failed for %s: could not rename temp file (%s)", contact, strerror(errno));
|
||||
unlink(tmp_path);
|
||||
} else {
|
||||
cons_show("Exported %s: %d new, %d skipped (already existed: %d)",
|
||||
contact, contact_exported, contact_skipped, existing_count);
|
||||
result = contact_exported;
|
||||
}
|
||||
|
||||
g_hash_table_destroy(seen_keys);
|
||||
g_slist_free_full(existing, (GDestroyNotify)ff_parsed_line_free);
|
||||
g_slist_free_full(sqlite_lines, (GDestroyNotify)message_free);
|
||||
return result;
|
||||
}
|
||||
|
||||
int
|
||||
log_database_export_to_flatfile(const gchar* const contact_jid)
|
||||
{
|
||||
const Jid* myjid = connection_get_jid();
|
||||
if (!myjid || !myjid->barejid) {
|
||||
cons_show_error("Export failed: not connected.");
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (!g_flatfile_account_jid) {
|
||||
// Flatfile backend not initialized — init it temporarily
|
||||
// We need g_flatfile_account_jid for path construction
|
||||
g_flatfile_account_jid = g_strdup(myjid->barejid);
|
||||
}
|
||||
|
||||
// Get the SQLite backend
|
||||
db_backend_t* sqlite_be = db_backend_sqlite();
|
||||
if (!sqlite_be) {
|
||||
cons_show_error("Export failed: SQLite backend not available.");
|
||||
return -1;
|
||||
}
|
||||
|
||||
// SQLite must be open for db_sqlite_list_contacts/db_sqlite_get_all_chat
|
||||
// to return rows. When the flatfile backend is active, sqlite is closed —
|
||||
// queries silently return NULL and the export reports zero changes.
|
||||
int sqlite_opened = _ensure_sqlite_open();
|
||||
if (sqlite_opened < 0) {
|
||||
cons_show_error("Export failed: could not open SQLite database.");
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Determine contact list
|
||||
GSList* contacts = NULL;
|
||||
if (contact_jid) {
|
||||
contacts = g_slist_append(contacts, g_strdup(contact_jid));
|
||||
} else {
|
||||
contacts = db_sqlite_list_contacts();
|
||||
if (!contacts) {
|
||||
cons_show("No contacts found in SQLite database.");
|
||||
_close_temp_sqlite(sqlite_opened);
|
||||
return 0;
|
||||
}
|
||||
cons_show("Exporting %d contact(s) to flat-file format...", g_slist_length(contacts));
|
||||
}
|
||||
|
||||
int total_exported = 0;
|
||||
|
||||
for (GSList* c = contacts; c; c = c->next) {
|
||||
const char* contact = c->data;
|
||||
int exported = _export_one_contact(contact);
|
||||
if (exported > 0)
|
||||
total_exported += exported;
|
||||
}
|
||||
|
||||
g_slist_free_full(contacts, g_free);
|
||||
_close_temp_sqlite(sqlite_opened);
|
||||
return total_exported;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Import: flatfile -> SQLite (merge)
|
||||
// =========================================================================
|
||||
|
||||
int
|
||||
log_database_import_from_flatfile(const gchar* const contact_jid)
|
||||
{
|
||||
const Jid* myjid = connection_get_jid();
|
||||
if (!myjid || !myjid->barejid) {
|
||||
cons_show_error("Import failed: not connected.");
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (!g_flatfile_account_jid) {
|
||||
g_flatfile_account_jid = g_strdup(myjid->barejid);
|
||||
}
|
||||
|
||||
db_backend_t* sqlite_be = db_backend_sqlite();
|
||||
if (!sqlite_be) {
|
||||
cons_show_error("Import failed: SQLite backend not available.");
|
||||
return -1;
|
||||
}
|
||||
|
||||
int sqlite_opened = _ensure_sqlite_open();
|
||||
if (sqlite_opened < 0) {
|
||||
cons_show_error("Import failed: could not open SQLite database.");
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Determine contacts
|
||||
GSList* contacts = NULL;
|
||||
if (contact_jid) {
|
||||
contacts = g_slist_append(contacts, g_strdup(contact_jid));
|
||||
} else {
|
||||
contacts = _ff_list_contacts();
|
||||
if (!contacts) {
|
||||
cons_show("No flat-file history found to import.");
|
||||
_close_temp_sqlite(sqlite_opened);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
int total_imported = 0;
|
||||
|
||||
for (GSList* c = contacts; c; c = c->next) {
|
||||
const char* contact = c->data;
|
||||
|
||||
// 1. Read all flatfile lines
|
||||
GSList* ff_lines = _ff_read_all_lines(contact);
|
||||
if (!ff_lines) {
|
||||
log_debug("import: no flatfile messages for %s", contact);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 2. Build dedup set from SQLite (existing stanza_ids + fallback keys)
|
||||
GHashTable* seen_keys = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL);
|
||||
|
||||
// Read all SQLite messages for this contact via direct SQL query
|
||||
GSList* existing_msgs = db_sqlite_get_all_chat(contact);
|
||||
for (GSList* l = existing_msgs; l; l = l->next) {
|
||||
ProfMessage* msg = l->data;
|
||||
if (!msg)
|
||||
continue;
|
||||
auto_gchar gchar* ts = msg->timestamp ? g_date_time_format_iso8601(msg->timestamp) : g_strdup("");
|
||||
gchar* key = _make_dedup_key(msg->id, ts, msg->from_jid ? msg->from_jid->barejid : "", msg->plain);
|
||||
g_hash_table_add(seen_keys, key);
|
||||
}
|
||||
g_slist_free_full(existing_msgs, (GDestroyNotify)message_free);
|
||||
|
||||
// 3. For each flatfile line not in seen_keys, insert via add_incoming
|
||||
// Wrap in a transaction for atomicity and performance
|
||||
int contact_imported = 0;
|
||||
int contact_skipped = 0;
|
||||
int total_lines = (int)g_slist_length(ff_lines);
|
||||
|
||||
db_sqlite_begin_transaction();
|
||||
gboolean import_ok = TRUE;
|
||||
|
||||
for (GSList* l = ff_lines; l; l = l->next) {
|
||||
ff_parsed_line_t* pl = l->data;
|
||||
if (!pl || !pl->timestamp)
|
||||
continue;
|
||||
|
||||
auto_gchar gchar* ts = g_date_time_format_iso8601(pl->timestamp);
|
||||
gchar* key = _make_dedup_key(pl->stanza_id, ts, pl->from_jid, pl->message);
|
||||
|
||||
if (g_hash_table_contains(seen_keys, key)) {
|
||||
g_free(key);
|
||||
contact_skipped++;
|
||||
continue;
|
||||
}
|
||||
g_hash_table_add(seen_keys, key);
|
||||
|
||||
// Build a ProfMessage and insert via add_incoming
|
||||
// (add_incoming uses _add_to_db which preserves the original timestamp)
|
||||
gboolean is_outgoing = (g_strcmp0(pl->from_jid, myjid->barejid) == 0);
|
||||
|
||||
ProfMessage* msg = message_init();
|
||||
msg->id = pl->stanza_id ? g_strdup(pl->stanza_id) : NULL;
|
||||
msg->stanzaid = pl->archive_id ? g_strdup(pl->archive_id) : NULL;
|
||||
msg->plain = g_strdup(pl->message ? pl->message : "");
|
||||
msg->timestamp = g_date_time_ref(pl->timestamp);
|
||||
msg->type = ff_get_message_type_type(pl->type);
|
||||
msg->enc = ff_get_message_enc_type(pl->enc);
|
||||
msg->replace_id = pl->replace_id ? g_strdup(pl->replace_id) : NULL;
|
||||
msg->is_mam = TRUE; // Suppress dedup check in the backend
|
||||
|
||||
if (is_outgoing) {
|
||||
msg->from_jid = jid_create(myjid->barejid);
|
||||
if (pl->to_jid) {
|
||||
msg->to_jid = jid_create_from_bare_and_resource(pl->to_jid, pl->to_resource);
|
||||
} else {
|
||||
msg->to_jid = jid_create(contact);
|
||||
}
|
||||
} else {
|
||||
msg->from_jid = jid_create_from_bare_and_resource(pl->from_jid, pl->from_resource);
|
||||
if (pl->to_jid) {
|
||||
msg->to_jid = jid_create_from_bare_and_resource(pl->to_jid, pl->to_resource);
|
||||
} else {
|
||||
msg->to_jid = jid_create(myjid->barejid);
|
||||
}
|
||||
}
|
||||
|
||||
sqlite_be->add_incoming(msg);
|
||||
message_free(msg);
|
||||
|
||||
// Verify the row was actually inserted
|
||||
if (db_sqlite_last_changes() < 1) {
|
||||
log_error("import: insert failed for %s at %s", contact, ts);
|
||||
import_ok = FALSE;
|
||||
break;
|
||||
}
|
||||
contact_imported++;
|
||||
|
||||
if (contact_imported % EXPORT_PROGRESS_INTERVAL == 0) {
|
||||
cons_show(" ... %s: %d/%d imported so far", contact, contact_imported, total_lines);
|
||||
}
|
||||
}
|
||||
|
||||
if (import_ok) {
|
||||
db_sqlite_end_transaction();
|
||||
} else {
|
||||
db_sqlite_rollback_transaction();
|
||||
cons_show_error("Import of %s failed — transaction rolled back.", contact);
|
||||
}
|
||||
|
||||
cons_show("Imported %s: %d new, %d skipped (already in SQLite)",
|
||||
contact, contact_imported, contact_skipped);
|
||||
total_imported += contact_imported;
|
||||
|
||||
g_hash_table_destroy(seen_keys);
|
||||
g_slist_free_full(ff_lines, (GDestroyNotify)ff_parsed_line_free);
|
||||
|
||||
// If the per-contact import bailed mid-stream (likely a system-level
|
||||
// problem: disk full, sqlite locked, etc.) — don't blindly try the
|
||||
// remaining contacts; they'll almost certainly hit the same error
|
||||
// and produce a confusing wall of "Import of X failed" messages.
|
||||
// Stop here so the user can investigate the root cause.
|
||||
if (!import_ok) {
|
||||
cons_show_error("Aborting import — system-level failure suspected. Fix the underlying issue and retry.");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
g_slist_free_full(contacts, g_free);
|
||||
_close_temp_sqlite(sqlite_opened);
|
||||
return total_imported;
|
||||
}
|
||||
|
||||
#endif /* HAVE_SQLITE */
|
||||
1019
src/database_flatfile.c
Normal file
1019
src/database_flatfile.c
Normal file
File diff suppressed because it is too large
Load Diff
172
src/database_flatfile.h
Normal file
172
src/database_flatfile.h
Normal file
@@ -0,0 +1,172 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
//
|
||||
// This file is part of CProof.
|
||||
// See LICENSE for the full GPLv3 text and the special OpenSSL linking exception.
|
||||
|
||||
/*
|
||||
* database_flatfile.h
|
||||
* vim: expandtab:ts=4:sts=4:sw=4
|
||||
*
|
||||
* Internal header shared between database_flatfile*.c modules.
|
||||
* Not part of the public API — do not include from outside the flatfile backend.
|
||||
*/
|
||||
|
||||
#ifndef DATABASE_FLATFILE_H
|
||||
#define DATABASE_FLATFILE_H
|
||||
|
||||
#include <glib.h>
|
||||
#include <stdio.h>
|
||||
#include <sys/types.h>
|
||||
|
||||
#include "database.h"
|
||||
#include "xmpp/xmpp.h"
|
||||
#include "xmpp/message.h"
|
||||
|
||||
// --- Constants ---
|
||||
|
||||
#define DIR_FLATLOG "flatlog"
|
||||
#define FLATFILE_FORMAT_VERSION 1
|
||||
#define FF_VERSION_MARKER "format-version: "
|
||||
#define FF_STRINGIFY_(x) #x
|
||||
#define FF_STRINGIFY(x) FF_STRINGIFY_(x)
|
||||
#define FLATFILE_HEADER "# cproof chat log — UTF-8, LF line endings, " FF_VERSION_MARKER FF_STRINGIFY(FLATFILE_FORMAT_VERSION) "\n"
|
||||
#define FF_MAX_LINE_LEN (10 * 1024 * 1024) /* 10 MB — reject lines longer than this */
|
||||
#define FF_MAX_LMC_DEPTH 100 /* max correction chain depth */
|
||||
|
||||
// --- Shared global ---
|
||||
|
||||
// Account JID stored during init for path construction.
|
||||
// Defined in database_flatfile.c, used by all flatfile modules.
|
||||
extern char* g_flatfile_account_jid;
|
||||
|
||||
// --- Parsed line structure ---
|
||||
|
||||
typedef struct
|
||||
{
|
||||
char* timestamp_str;
|
||||
GDateTime* timestamp;
|
||||
char* type;
|
||||
char* enc;
|
||||
char* stanza_id;
|
||||
char* archive_id;
|
||||
char* replace_id;
|
||||
char* from_jid;
|
||||
char* from_resource;
|
||||
char* to_jid;
|
||||
char* to_resource;
|
||||
int marked_read; // -1 = unset (NULL in DB), 0 = unread, 1 = read
|
||||
char* message;
|
||||
off_t file_offset;
|
||||
} ff_parsed_line_t;
|
||||
|
||||
// --- Sparse index for single-file lookup ---
|
||||
|
||||
// Sample every N-th log line for the sparse index.
|
||||
// 500 lines ≈ one index entry per ~50 KB of log data, so a 100K-message
|
||||
// contact requires only ~200 entries (~3 KB) for O(log n) time-range lookup.
|
||||
#define FF_INDEX_STEP 500
|
||||
|
||||
// --- Metadata field prefixes (used in _ff_cache_line_ids and ff_parse_line) ---
|
||||
|
||||
#define FF_META_PREFIX_ID "id:"
|
||||
#define FF_META_PREFIX_AID "aid:"
|
||||
|
||||
typedef struct
|
||||
{
|
||||
off_t byte_offset;
|
||||
gint64 timestamp_epoch;
|
||||
} ff_index_entry_t;
|
||||
|
||||
typedef struct
|
||||
{
|
||||
time_t mtime;
|
||||
off_t size;
|
||||
ino_t inode;
|
||||
} ff_file_stamp_t;
|
||||
|
||||
typedef struct
|
||||
{
|
||||
char* filepath;
|
||||
ff_index_entry_t* entries;
|
||||
size_t n_entries;
|
||||
size_t cap_entries;
|
||||
size_t total_lines;
|
||||
ff_file_stamp_t stamp;
|
||||
int bom_len;
|
||||
GHashTable* archive_ids; // set: archive_id -> NULL (MAM dedup, O(1))
|
||||
GHashTable* stanza_senders; // map: stanza_id -> from_jid (LMC validation, O(1))
|
||||
} ff_contact_state_t;
|
||||
|
||||
// State management
|
||||
ff_contact_state_t* ff_state_new(const char* filepath);
|
||||
void ff_state_free(ff_contact_state_t* state);
|
||||
gboolean ff_state_ensure_fresh(ff_contact_state_t* state);
|
||||
off_t ff_state_offset_for_time(ff_contact_state_t* state, const char* iso_time);
|
||||
|
||||
// --- Type conversion helpers ---
|
||||
|
||||
const char* ff_get_message_type_str(prof_msg_type_t type);
|
||||
prof_msg_type_t ff_get_message_type_type(const char* const type);
|
||||
const char* ff_get_message_enc_str(prof_enc_t enc);
|
||||
prof_enc_t ff_get_message_enc_type(const char* const encstr);
|
||||
|
||||
// --- Path helpers ---
|
||||
|
||||
char* ff_jid_to_dir(const char* jid);
|
||||
char* ff_get_contact_dir(const char* contact_barejid);
|
||||
char* ff_get_log_path(const char* contact_barejid);
|
||||
gboolean ff_ensure_dir(const char* path);
|
||||
|
||||
// --- Escape / unescape ---
|
||||
|
||||
char* ff_escape_message(const char* text);
|
||||
char* ff_unescape_message(const char* text);
|
||||
char* ff_escape_meta_value(const char* val);
|
||||
char* ff_unescape_meta_value(const char* val);
|
||||
|
||||
// --- I/O ---
|
||||
|
||||
// Skip UTF-8 BOM if present; returns 3 if skipped, 0 otherwise.
|
||||
int ff_skip_bom(FILE* fp);
|
||||
|
||||
// Scan leading '#' comment lines for the format-version marker. Returns
|
||||
// the version found, 0 if no marker is present, or -1 on read error.
|
||||
// File position is rewound to the start of the comment block on entry.
|
||||
int ff_read_format_version(FILE* fp);
|
||||
|
||||
char* ff_readline(FILE* fp, gboolean* truncated);
|
||||
void ff_write_line(FILE* fp, const char* timestamp, const char* type, const char* enc,
|
||||
const char* stanza_id, const char* archive_id, const char* replace_id,
|
||||
const char* from_jid, const char* from_resource,
|
||||
const char* to_jid, const char* to_resource, int marked_read,
|
||||
const char* message_text);
|
||||
|
||||
// --- Parser helpers ---
|
||||
|
||||
const char* ff_find_unescaped_char(const char* str, char ch);
|
||||
char** ff_split_meta(const char* meta);
|
||||
const char* ff_find_unescaped_colonspace(const char* str);
|
||||
char* ff_unescape_sender_resource(const char* res);
|
||||
|
||||
// --- Parser ---
|
||||
|
||||
void ff_parsed_line_free(ff_parsed_line_t* pl);
|
||||
ff_parsed_line_t* ff_parse_line(const char* line);
|
||||
ProfMessage* ff_parsed_to_profmessage(ff_parsed_line_t* pl);
|
||||
|
||||
// --- Integrity verification (database_flatfile_verify.c) ---
|
||||
|
||||
// Run integrity checks against one contact's history.log (or every contact
|
||||
// when contact_barejid == NULL).
|
||||
//
|
||||
// Returns a freshly allocated GSList<integrity_issue_t*> reporting:
|
||||
// - File-level: missing log, BOM, CRLF, empty, wrong permissions
|
||||
// - Line-level: invalid UTF-8, control characters, unparsable lines,
|
||||
// timestamps out of order, duplicate stanza-id /
|
||||
// archive-id (tracked separately)
|
||||
// - Cross-line: broken `corrects:` LMC references
|
||||
//
|
||||
// Callers must free with g_slist_free_full(issues, integrity_issue_free).
|
||||
GSList* ff_verify_integrity(const gchar* const contact_barejid);
|
||||
|
||||
#endif
|
||||
825
src/database_flatfile_parser.c
Normal file
825
src/database_flatfile_parser.c
Normal file
@@ -0,0 +1,825 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
//
|
||||
// This file is part of CProof.
|
||||
// See LICENSE for the full GPLv3 text and the special OpenSSL linking exception.
|
||||
|
||||
/*
|
||||
* database_flatfile_parser.c
|
||||
* vim: expandtab:ts=4:sts=4:sw=4
|
||||
*
|
||||
* Flat-file backend: type helpers, path helpers, escape/unescape,
|
||||
* line I/O, and the tolerant log-line parser.
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <sys/stat.h>
|
||||
#include <glib.h>
|
||||
#include <glib/gstdio.h>
|
||||
#include <limits.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <errno.h>
|
||||
|
||||
#include "log.h"
|
||||
#include "common.h"
|
||||
#include "config/files.h"
|
||||
#include "database_flatfile.h"
|
||||
|
||||
// =========================================================================
|
||||
// Type conversion helpers
|
||||
// =========================================================================
|
||||
|
||||
const char*
|
||||
ff_get_message_type_str(prof_msg_type_t type)
|
||||
{
|
||||
switch (type) {
|
||||
case PROF_MSG_TYPE_CHAT:
|
||||
return "chat";
|
||||
case PROF_MSG_TYPE_MUC:
|
||||
return "muc";
|
||||
case PROF_MSG_TYPE_MUCPM:
|
||||
return "mucpm";
|
||||
case PROF_MSG_TYPE_UNINITIALIZED:
|
||||
return "chat";
|
||||
}
|
||||
return "chat";
|
||||
}
|
||||
|
||||
prof_msg_type_t
|
||||
ff_get_message_type_type(const char* const type)
|
||||
{
|
||||
if (g_strcmp0(type, "chat") == 0) {
|
||||
return PROF_MSG_TYPE_CHAT;
|
||||
} else if (g_strcmp0(type, "muc") == 0) {
|
||||
return PROF_MSG_TYPE_MUC;
|
||||
} else if (g_strcmp0(type, "mucpm") == 0) {
|
||||
return PROF_MSG_TYPE_MUCPM;
|
||||
}
|
||||
return PROF_MSG_TYPE_CHAT;
|
||||
}
|
||||
|
||||
const char*
|
||||
ff_get_message_enc_str(prof_enc_t enc)
|
||||
{
|
||||
switch (enc) {
|
||||
case PROF_MSG_ENC_OX:
|
||||
return "ox";
|
||||
case PROF_MSG_ENC_PGP:
|
||||
return "pgp";
|
||||
case PROF_MSG_ENC_OTR:
|
||||
return "otr";
|
||||
case PROF_MSG_ENC_OMEMO:
|
||||
return "omemo";
|
||||
case PROF_MSG_ENC_NONE:
|
||||
return "none";
|
||||
}
|
||||
return "none";
|
||||
}
|
||||
|
||||
prof_enc_t
|
||||
ff_get_message_enc_type(const char* const encstr)
|
||||
{
|
||||
if (g_strcmp0(encstr, "ox") == 0) {
|
||||
return PROF_MSG_ENC_OX;
|
||||
} else if (g_strcmp0(encstr, "pgp") == 0) {
|
||||
return PROF_MSG_ENC_PGP;
|
||||
} else if (g_strcmp0(encstr, "otr") == 0) {
|
||||
return PROF_MSG_ENC_OTR;
|
||||
} else if (g_strcmp0(encstr, "omemo") == 0) {
|
||||
return PROF_MSG_ENC_OMEMO;
|
||||
}
|
||||
return PROF_MSG_ENC_NONE;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Path helpers
|
||||
// =========================================================================
|
||||
|
||||
// Sanitise a JID for use as a directory name.
|
||||
// 1. Replace '@' with '_at_'
|
||||
// 2. Reject / strip path-separator and traversal characters: '/', '\\', '..'
|
||||
// This prevents a malicious federated JID like "../../../tmp/pwned" from
|
||||
// escaping the log directory tree.
|
||||
char*
|
||||
ff_jid_to_dir(const char* jid)
|
||||
{
|
||||
if (!jid || jid[0] == '\0')
|
||||
return NULL;
|
||||
|
||||
// Replace '@' with '_at_' (str_replace returns a freshly allocated string)
|
||||
char* result = str_replace(jid, "@", "_at_");
|
||||
if (!result)
|
||||
return NULL;
|
||||
|
||||
// Replace path-separator characters in place
|
||||
for (char* p = result; *p; p++) {
|
||||
if (*p == '/' || *p == '\\')
|
||||
*p = '_';
|
||||
}
|
||||
|
||||
// Collapse any remaining ".." sequences to "__" (belt-and-suspenders)
|
||||
char* dotdot;
|
||||
while ((dotdot = strstr(result, "..")) != NULL) {
|
||||
dotdot[0] = '_';
|
||||
dotdot[1] = '_';
|
||||
}
|
||||
|
||||
if (result[0] == '\0') {
|
||||
free(result);
|
||||
return NULL;
|
||||
}
|
||||
// Callers free via g_free, str_replace allocates via malloc — handover.
|
||||
char* gresult = g_strdup(result);
|
||||
free(result);
|
||||
return gresult;
|
||||
}
|
||||
|
||||
// Get the base directory for a contact's logs:
|
||||
// ~/.local/share/profanity/flatlog/{my_jid_dir}/{contact_jid_dir}/
|
||||
char*
|
||||
ff_get_contact_dir(const char* contact_barejid)
|
||||
{
|
||||
if (!g_flatfile_account_jid || !contact_barejid)
|
||||
return NULL;
|
||||
|
||||
auto_gchar gchar* data_path = files_get_data_path(DIR_FLATLOG);
|
||||
auto_gchar gchar* my_dir = ff_jid_to_dir(g_flatfile_account_jid);
|
||||
auto_gchar gchar* contact_dir = ff_jid_to_dir(contact_barejid);
|
||||
|
||||
char* result = g_strdup_printf("%s/%s/%s", data_path, my_dir, contact_dir);
|
||||
return result;
|
||||
}
|
||||
|
||||
// Get the single log file path for a contact: {contact_dir}/history.log
|
||||
char*
|
||||
ff_get_log_path(const char* contact_barejid)
|
||||
{
|
||||
auto_gchar gchar* contact_dir = ff_get_contact_dir(contact_barejid);
|
||||
if (!contact_dir)
|
||||
return NULL;
|
||||
|
||||
char* result = g_strdup_printf("%s/history.log", contact_dir);
|
||||
return result;
|
||||
}
|
||||
|
||||
// Ensure the directory exists, create if needed.
|
||||
// Refuses to follow symlinks at the final component.
|
||||
gboolean
|
||||
ff_ensure_dir(const char* path)
|
||||
{
|
||||
if (g_file_test(path, G_FILE_TEST_IS_SYMLINK)) {
|
||||
log_error("flatfile: directory path is a symlink, refusing: %s", path);
|
||||
return FALSE;
|
||||
}
|
||||
if (g_file_test(path, G_FILE_TEST_IS_DIR))
|
||||
return TRUE;
|
||||
if (g_mkdir_with_parents(path, S_IRWXU) != 0) {
|
||||
log_error("flatfile: Could not create directory: %s (errno=%d)", path, errno);
|
||||
return FALSE;
|
||||
}
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Escape / unescape helpers
|
||||
// =========================================================================
|
||||
//
|
||||
// Message text and metadata values from remote peers can contain arbitrary
|
||||
// characters including newlines, pipes and brackets. Without escaping, a
|
||||
// crafted message could inject fake log lines (log injection / format
|
||||
// injection). We escape on write and unescape on read.
|
||||
|
||||
// Escape message body: \ -> \\, \n -> \n literal, \r -> \r literal
|
||||
char*
|
||||
ff_escape_message(const char* text)
|
||||
{
|
||||
if (!text)
|
||||
return g_strdup("");
|
||||
GString* out = g_string_sized_new(strlen(text));
|
||||
for (const char* p = text; *p; p++) {
|
||||
switch (*p) {
|
||||
case '\\':
|
||||
g_string_append(out, "\\\\");
|
||||
break;
|
||||
case '\n':
|
||||
g_string_append(out, "\\n");
|
||||
break;
|
||||
case '\r':
|
||||
g_string_append(out, "\\r");
|
||||
break;
|
||||
default:
|
||||
g_string_append_c(out, *p);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return g_string_free(out, FALSE);
|
||||
}
|
||||
|
||||
// Unescape message body: \\ -> \, \n -> newline, \r -> CR
|
||||
char*
|
||||
ff_unescape_message(const char* text)
|
||||
{
|
||||
if (!text)
|
||||
return g_strdup("");
|
||||
GString* out = g_string_sized_new(strlen(text));
|
||||
for (const char* p = text; *p; p++) {
|
||||
if (*p != '\\' || !*(p + 1)) {
|
||||
g_string_append_c(out, *p);
|
||||
continue;
|
||||
}
|
||||
p++;
|
||||
switch (*p) {
|
||||
case '\\':
|
||||
g_string_append_c(out, '\\');
|
||||
break;
|
||||
case 'n':
|
||||
g_string_append_c(out, '\n');
|
||||
break;
|
||||
case 'r':
|
||||
g_string_append_c(out, '\r');
|
||||
break;
|
||||
default:
|
||||
g_string_append_c(out, '\\');
|
||||
g_string_append_c(out, *p);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return g_string_free(out, FALSE);
|
||||
}
|
||||
|
||||
// Escape metadata value (stanza_id, archive_id, replace_id):
|
||||
// these come from remote servers and may contain |, ], \, newlines.
|
||||
char*
|
||||
ff_escape_meta_value(const char* val)
|
||||
{
|
||||
if (!val || strlen(val) == 0)
|
||||
return NULL;
|
||||
GString* out = g_string_sized_new(strlen(val));
|
||||
for (const char* p = val; *p; p++) {
|
||||
switch (*p) {
|
||||
case '|':
|
||||
g_string_append(out, "\\|");
|
||||
break;
|
||||
case ']':
|
||||
g_string_append(out, "\\]");
|
||||
break;
|
||||
case '\\':
|
||||
g_string_append(out, "\\\\");
|
||||
break;
|
||||
case '\n':
|
||||
g_string_append(out, "\\n");
|
||||
break;
|
||||
case '\r':
|
||||
g_string_append(out, "\\r");
|
||||
break;
|
||||
default:
|
||||
g_string_append_c(out, *p);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return g_string_free(out, FALSE);
|
||||
}
|
||||
|
||||
// Unescape metadata value
|
||||
char*
|
||||
ff_unescape_meta_value(const char* val)
|
||||
{
|
||||
if (!val)
|
||||
return NULL;
|
||||
GString* out = g_string_sized_new(strlen(val));
|
||||
for (const char* p = val; *p; p++) {
|
||||
if (*p != '\\' || !*(p + 1)) {
|
||||
g_string_append_c(out, *p);
|
||||
continue;
|
||||
}
|
||||
p++;
|
||||
switch (*p) {
|
||||
case '|':
|
||||
g_string_append_c(out, '|');
|
||||
break;
|
||||
case ']':
|
||||
g_string_append_c(out, ']');
|
||||
break;
|
||||
case '\\':
|
||||
g_string_append_c(out, '\\');
|
||||
break;
|
||||
case 'n':
|
||||
g_string_append_c(out, '\n');
|
||||
break;
|
||||
case 'r':
|
||||
g_string_append_c(out, '\r');
|
||||
break;
|
||||
default:
|
||||
g_string_append_c(out, '\\');
|
||||
g_string_append_c(out, *p);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return g_string_free(out, FALSE);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// BOM helper
|
||||
// =========================================================================
|
||||
//
|
||||
// Skip UTF-8 BOM (EF BB BF) at the beginning of a file.
|
||||
// Returns 3 if BOM was present, 0 otherwise. File position is set past
|
||||
// the BOM (or rewound to the start).
|
||||
int
|
||||
ff_skip_bom(FILE* fp)
|
||||
{
|
||||
int c1 = fgetc(fp);
|
||||
int c2 = fgetc(fp);
|
||||
int c3 = fgetc(fp);
|
||||
if (c1 == 0xEF && c2 == 0xBB && c3 == 0xBF)
|
||||
return 3;
|
||||
fseek(fp, 0, SEEK_SET);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Format-version helper
|
||||
// =========================================================================
|
||||
//
|
||||
// Scan leading '#' comment lines for the `format-version: <N>` marker
|
||||
// (anywhere within the comment line). Stops at the first non-comment
|
||||
// line or after FF_VERSION_SCAN_MAX comments. Restores fp to its
|
||||
// position at entry, so callers can re-parse the header normally
|
||||
// afterwards.
|
||||
#define FF_VERSION_SCAN_MAX 16
|
||||
|
||||
int
|
||||
ff_read_format_version(FILE* fp)
|
||||
{
|
||||
long start = ftell(fp);
|
||||
if (start < 0)
|
||||
return -1;
|
||||
|
||||
int version = 0;
|
||||
size_t marker_len = strlen(FF_VERSION_MARKER);
|
||||
|
||||
for (int i = 0; i < FF_VERSION_SCAN_MAX; i++) {
|
||||
gboolean truncated = FALSE;
|
||||
char* line = ff_readline(fp, &truncated);
|
||||
if (!line)
|
||||
break;
|
||||
if (line[0] != '#') {
|
||||
free(line);
|
||||
break;
|
||||
}
|
||||
char* found = strstr(line, FF_VERSION_MARKER);
|
||||
if (found) {
|
||||
char* num_start = found + marker_len;
|
||||
char* endptr = NULL;
|
||||
long v = strtol(num_start, &endptr, 10);
|
||||
if (endptr != num_start && v > 0 && v <= INT_MAX)
|
||||
version = (int)v;
|
||||
free(line);
|
||||
break;
|
||||
}
|
||||
free(line);
|
||||
}
|
||||
|
||||
fseek(fp, start, SEEK_SET);
|
||||
return version;
|
||||
}
|
||||
|
||||
// Readline helper
|
||||
// =========================================================================
|
||||
//
|
||||
// POSIX getline() for dynamic-length lines. Returns line without trailing
|
||||
// newline, or NULL on EOF. Sets *truncated=TRUE if the line had no trailing
|
||||
// newline at EOF (partial write detection).
|
||||
char*
|
||||
ff_readline(FILE* fp, gboolean* truncated)
|
||||
{
|
||||
char* line = NULL;
|
||||
size_t cap = 0;
|
||||
ssize_t nread = getline(&line, &cap, fp);
|
||||
if (nread == -1) {
|
||||
free(line);
|
||||
return NULL;
|
||||
}
|
||||
// Guard against pathological lines that could exhaust memory.
|
||||
// getline() already allocated, so we free and skip if too long.
|
||||
if (nread > FF_MAX_LINE_LEN) {
|
||||
log_error("flatfile: line too long (%zd bytes), skipping", nread);
|
||||
// Check newline termination before freeing
|
||||
gboolean had_newline = (nread > 0 && line[nread - 1] == '\n');
|
||||
free(line);
|
||||
// Skip to next newline if the overlength line wasn't newline-terminated
|
||||
if (!had_newline) {
|
||||
int ch;
|
||||
while ((ch = fgetc(fp)) != EOF && ch != '\n') {}
|
||||
}
|
||||
if (truncated)
|
||||
*truncated = FALSE;
|
||||
// Return empty string so caller's loop continues (parse will reject it).
|
||||
// strdup() so callers can always free() consistently.
|
||||
return strdup("");
|
||||
}
|
||||
if (truncated)
|
||||
*truncated = FALSE;
|
||||
if (nread > 0 && line[nread - 1] == '\n') {
|
||||
line[--nread] = '\0';
|
||||
} else if (feof(fp)) {
|
||||
// Line without trailing newline at EOF — likely a partial write
|
||||
if (truncated)
|
||||
*truncated = TRUE;
|
||||
}
|
||||
return line;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Line writer
|
||||
// =========================================================================
|
||||
//
|
||||
// Format: {ISO8601} [{type}|{enc}|id:{stanza_id}|aid:{archive_id}|corrects:{replace_id}] {from_jid}/{resource}: {message}
|
||||
//
|
||||
// Message text is escaped: \\ -> \\\\, newline -> \\n, CR -> \\r
|
||||
// Metadata values are escaped: |, ], \\, newline, CR
|
||||
// Lines starting with '#' are comments.
|
||||
// Empty lines are skipped.
|
||||
|
||||
void
|
||||
ff_write_line(FILE* fp, const char* timestamp, const char* type, const char* enc,
|
||||
const char* stanza_id, const char* archive_id, const char* replace_id,
|
||||
const char* from_jid, const char* from_resource,
|
||||
const char* to_jid, const char* to_resource, int marked_read,
|
||||
const char* message_text)
|
||||
{
|
||||
// Escape metadata values from remote peers
|
||||
auto_gchar gchar* safe_sid = ff_escape_meta_value(stanza_id);
|
||||
auto_gchar gchar* safe_aid = ff_escape_meta_value(archive_id);
|
||||
auto_gchar gchar* safe_rid = ff_escape_meta_value(replace_id);
|
||||
|
||||
// Build metadata section: [type|enc|id:...|aid:...|corrects:...|to:...|to_res:...|read:...]
|
||||
GString* meta = g_string_new("[");
|
||||
g_string_append(meta, type ? type : "chat");
|
||||
g_string_append_c(meta, '|');
|
||||
g_string_append(meta, enc ? enc : "none");
|
||||
if (safe_sid) {
|
||||
g_string_append_printf(meta, "|id:%s", safe_sid);
|
||||
}
|
||||
if (safe_aid) {
|
||||
g_string_append_printf(meta, "|aid:%s", safe_aid);
|
||||
}
|
||||
if (safe_rid) {
|
||||
g_string_append_printf(meta, "|corrects:%s", safe_rid);
|
||||
}
|
||||
if (to_jid && strlen(to_jid) > 0
|
||||
&& g_strcmp0(to_jid, "(null)") != 0) {
|
||||
auto_gchar gchar* safe_to = ff_escape_meta_value(to_jid);
|
||||
g_string_append_printf(meta, "|to:%s", safe_to);
|
||||
}
|
||||
if (to_resource && strlen(to_resource) > 0
|
||||
&& g_strcmp0(to_resource, "(null)") != 0) {
|
||||
auto_gchar gchar* safe_tores = ff_escape_meta_value(to_resource);
|
||||
g_string_append_printf(meta, "|to_res:%s", safe_tores);
|
||||
}
|
||||
if (marked_read >= 0) {
|
||||
g_string_append_printf(meta, "|read:%d", marked_read ? 1 : 0);
|
||||
}
|
||||
g_string_append_c(meta, ']');
|
||||
|
||||
// Build sender — escape ": " in the resource part to prevent
|
||||
// the parser from splitting at the wrong point.
|
||||
const gboolean from_jid_usable = from_jid && *from_jid
|
||||
&& g_strcmp0(from_jid, "(null)") != 0;
|
||||
GString* sender = g_string_new(from_jid_usable ? from_jid : "unknown");
|
||||
if (from_resource && strlen(from_resource) > 0
|
||||
&& g_strcmp0(from_resource, "(null)") != 0) {
|
||||
// Escape backslash and colon-space in resource
|
||||
GString* safe_res = g_string_sized_new(strlen(from_resource));
|
||||
for (const char* p = from_resource; *p; p++) {
|
||||
if (*p == '\\') {
|
||||
g_string_append(safe_res, "\\\\");
|
||||
} else if (*p == ':' && *(p + 1) == ' ') {
|
||||
g_string_append(safe_res, "\\: ");
|
||||
p++; // skip the space too
|
||||
} else {
|
||||
g_string_append_c(safe_res, *p);
|
||||
}
|
||||
}
|
||||
g_string_append_printf(sender, "/%s", safe_res->str);
|
||||
g_string_free(safe_res, TRUE);
|
||||
}
|
||||
|
||||
// Escape message body to prevent log injection
|
||||
auto_char char* safe_msg = ff_escape_message(message_text);
|
||||
|
||||
int ret = fprintf(fp, "%s %s %s: %s\n",
|
||||
timestamp, meta->str, sender->str, safe_msg);
|
||||
if (ret < 0) {
|
||||
log_error("flatfile: fprintf failed (errno=%d)", errno);
|
||||
}
|
||||
|
||||
g_string_free(meta, TRUE);
|
||||
g_string_free(sender, TRUE);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Parser helpers
|
||||
// =========================================================================
|
||||
|
||||
// Find the first occurrence of 'ch' that is not preceded by an unescaped backslash.
|
||||
const char*
|
||||
ff_find_unescaped_char(const char* str, char ch)
|
||||
{
|
||||
if (!str)
|
||||
return NULL;
|
||||
for (const char* p = str; *p; p++) {
|
||||
if (*p == '\\' && *(p + 1)) {
|
||||
p++; // skip escaped character
|
||||
continue;
|
||||
}
|
||||
if (*p == ch)
|
||||
return p;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Split metadata content on unescaped '|'. Returns a NULL-terminated array.
|
||||
// Caller must g_strfreev() the result.
|
||||
char**
|
||||
ff_split_meta(const char* meta)
|
||||
{
|
||||
GPtrArray* arr = g_ptr_array_new();
|
||||
const char* start = meta;
|
||||
for (const char* p = meta;; p++) {
|
||||
if (*p == '\\' && *(p + 1)) {
|
||||
p++; // skip escaped char
|
||||
continue;
|
||||
}
|
||||
if (*p == '|' || *p == '\0') {
|
||||
g_ptr_array_add(arr, g_strndup(start, g_diff_to_gsize(p, start)));
|
||||
if (*p == '\0')
|
||||
break;
|
||||
start = p + 1;
|
||||
}
|
||||
}
|
||||
g_ptr_array_add(arr, NULL);
|
||||
return (char**)g_ptr_array_free(arr, FALSE);
|
||||
}
|
||||
|
||||
// Find first unescaped ": " (colon-space) in a string.
|
||||
const char*
|
||||
ff_find_unescaped_colonspace(const char* str)
|
||||
{
|
||||
if (!str)
|
||||
return NULL;
|
||||
for (const char* p = str; *p; p++) {
|
||||
if (*p == '\\' && *(p + 1)) {
|
||||
p++; // skip escaped character
|
||||
continue;
|
||||
}
|
||||
if (*p == ':' && *(p + 1) == ' ') {
|
||||
return p;
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Unescape a sender resource: "\\" -> '\', "\: " -> ": "
|
||||
char*
|
||||
ff_unescape_sender_resource(const char* res)
|
||||
{
|
||||
if (!res)
|
||||
return NULL;
|
||||
GString* out = g_string_sized_new(strlen(res));
|
||||
for (const char* p = res; *p; p++) {
|
||||
if (*p != '\\' || !*(p + 1)) {
|
||||
g_string_append_c(out, *p);
|
||||
continue;
|
||||
}
|
||||
p++;
|
||||
if (*p == '\\') {
|
||||
g_string_append_c(out, '\\');
|
||||
} else if (*p == ':' && *(p + 1) == ' ') {
|
||||
g_string_append(out, ": ");
|
||||
p++; // skip the space
|
||||
} else {
|
||||
// Unknown escape — preserve literally
|
||||
g_string_append_c(out, '\\');
|
||||
g_string_append_c(out, *p);
|
||||
}
|
||||
}
|
||||
return g_string_free(out, FALSE);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Line parser
|
||||
// =========================================================================
|
||||
|
||||
void
|
||||
ff_parsed_line_free(ff_parsed_line_t* pl)
|
||||
{
|
||||
if (!pl)
|
||||
return;
|
||||
g_free(pl->timestamp_str);
|
||||
if (pl->timestamp)
|
||||
g_date_time_unref(pl->timestamp);
|
||||
g_free(pl->type);
|
||||
g_free(pl->enc);
|
||||
g_free(pl->stanza_id);
|
||||
g_free(pl->archive_id);
|
||||
g_free(pl->replace_id);
|
||||
g_free(pl->from_jid);
|
||||
g_free(pl->from_resource);
|
||||
g_free(pl->to_jid);
|
||||
g_free(pl->to_resource);
|
||||
g_free(pl->message);
|
||||
g_free(pl);
|
||||
}
|
||||
|
||||
// Populate the parsed-line metadata fields from the pipe-split parts[]
|
||||
// (the contents of a "[...]" section). Index 0 is type, index 1 is enc;
|
||||
// remaining parts are key:value pairs (id:, aid:, corrects:, to:, to_res:, read:).
|
||||
static void
|
||||
_ff_parse_meta_parts(char** parts, ff_parsed_line_t* out)
|
||||
{
|
||||
for (int i = 0; parts[i]; i++) {
|
||||
if (i == 0) {
|
||||
out->type = g_strdup(parts[i]);
|
||||
} else if (i == 1) {
|
||||
out->enc = g_strdup(parts[i]);
|
||||
} else if (g_str_has_prefix(parts[i], FF_META_PREFIX_ID)) {
|
||||
out->stanza_id = ff_unescape_meta_value(parts[i] + strlen(FF_META_PREFIX_ID));
|
||||
} else if (g_str_has_prefix(parts[i], FF_META_PREFIX_AID)) {
|
||||
out->archive_id = ff_unescape_meta_value(parts[i] + strlen(FF_META_PREFIX_AID));
|
||||
} else if (g_str_has_prefix(parts[i], "corrects:")) {
|
||||
out->replace_id = ff_unescape_meta_value(parts[i] + strlen("corrects:"));
|
||||
} else if (g_str_has_prefix(parts[i], "to:")) {
|
||||
out->to_jid = ff_unescape_meta_value(parts[i] + strlen("to:"));
|
||||
} else if (g_str_has_prefix(parts[i], "to_res:")) {
|
||||
out->to_resource = ff_unescape_meta_value(parts[i] + strlen("to_res:"));
|
||||
} else if (g_str_has_prefix(parts[i], "read:")) {
|
||||
out->marked_read = atoi(parts[i] + strlen("read:")) ? 1 : 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Parse a single line. Returns NULL on parse failure.
|
||||
// Line format: {timestamp} [{metadata}] {sender}: {message}
|
||||
ff_parsed_line_t*
|
||||
ff_parse_line(const char* line)
|
||||
{
|
||||
if (!line || line[0] == '\0' || line[0] == '#') {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Strip trailing \r if present (CRLF handling)
|
||||
char* work = g_strdup(line);
|
||||
gsize len = strlen(work);
|
||||
if (len > 0 && work[len - 1] == '\r') {
|
||||
work[len - 1] = '\0';
|
||||
len--;
|
||||
}
|
||||
if (len == 0) {
|
||||
g_free(work);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// UTF-8 validation
|
||||
const gchar* end;
|
||||
if (!g_utf8_validate(work, -1, &end)) {
|
||||
log_warning("flatfile: invalid UTF-8 at byte offset %ld", (long)(end - work));
|
||||
// Attempt Latin-1 fallback
|
||||
gsize br, bw;
|
||||
GError* err = NULL;
|
||||
char* converted = g_convert(work, -1, "UTF-8", "ISO-8859-1", &br, &bw, &err);
|
||||
if (converted) {
|
||||
g_free(work);
|
||||
work = converted;
|
||||
} else {
|
||||
if (err)
|
||||
g_error_free(err);
|
||||
g_free(work);
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
|
||||
ff_parsed_line_t* result = g_malloc0(sizeof(ff_parsed_line_t));
|
||||
result->file_offset = -1;
|
||||
result->marked_read = -1; // unset by default
|
||||
|
||||
// Parse timestamp — everything up to first space followed by '['
|
||||
char* bracket_start = strchr(work, '[');
|
||||
char* first_space = strchr(work, ' ');
|
||||
|
||||
if (bracket_start && first_space && first_space < bracket_start) {
|
||||
// Standard format with metadata: {timestamp} [{meta}] {sender}: {msg}
|
||||
result->timestamp_str = g_strndup(work, g_diff_to_gsize(first_space, work));
|
||||
|
||||
// Parse metadata section [...]
|
||||
const char* bracket_end = ff_find_unescaped_char(bracket_start + 1, ']');
|
||||
if (!bracket_end) {
|
||||
// No closing bracket — malformed metadata
|
||||
ff_parsed_line_free(result);
|
||||
g_free(work);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
auto_gchar gchar* meta_content = g_strndup(bracket_start + 1, g_diff_to_gsize(bracket_end, bracket_start + 1));
|
||||
char** parts = ff_split_meta(meta_content);
|
||||
if (parts) {
|
||||
_ff_parse_meta_parts(parts, result);
|
||||
g_strfreev(parts);
|
||||
}
|
||||
|
||||
// Parse sender: message after '] '
|
||||
const char* after_meta = bracket_end + 1;
|
||||
if (*after_meta == ' ')
|
||||
after_meta++;
|
||||
|
||||
// Find first *unescaped* ': ' which separates sender from message.
|
||||
const char* colon = ff_find_unescaped_colonspace(after_meta);
|
||||
if (colon) {
|
||||
auto_gchar gchar* raw_sender = g_strndup(after_meta, g_diff_to_gsize(colon, after_meta));
|
||||
|
||||
// Split sender into jid/resource, then unescape resource
|
||||
char* slash = strchr(raw_sender, '/');
|
||||
if (slash) {
|
||||
result->from_jid = g_strndup(raw_sender, g_diff_to_gsize(slash, raw_sender));
|
||||
result->from_resource = ff_unescape_sender_resource(slash + 1);
|
||||
} else {
|
||||
result->from_jid = g_strdup(raw_sender);
|
||||
}
|
||||
|
||||
result->message = ff_unescape_message(colon + 2);
|
||||
} else {
|
||||
// No ': ' found, treat entire rest as message with unknown sender
|
||||
result->from_jid = g_strdup("unknown");
|
||||
result->message = ff_unescape_message(after_meta);
|
||||
}
|
||||
} else if (first_space) {
|
||||
// Legacy/simple format without metadata: {timestamp} - {sender}: {msg}
|
||||
result->timestamp_str = g_strndup(work, g_diff_to_gsize(first_space, work));
|
||||
result->type = g_strdup("chat");
|
||||
result->enc = g_strdup("none");
|
||||
|
||||
char* rest = first_space + 1;
|
||||
// Skip " - " if present (chatlog.c format)
|
||||
if (g_str_has_prefix(rest, "- ")) {
|
||||
rest += 2;
|
||||
}
|
||||
|
||||
char* colon = strstr(rest, ": ");
|
||||
if (colon) {
|
||||
char* sender = g_strndup(rest, g_diff_to_gsize(colon, rest));
|
||||
char* slash = strchr(sender, '/');
|
||||
if (slash) {
|
||||
result->from_jid = g_strndup(sender, g_diff_to_gsize(slash, sender));
|
||||
result->from_resource = g_strdup(slash + 1);
|
||||
} else {
|
||||
result->from_jid = g_strdup(sender);
|
||||
}
|
||||
g_free(sender);
|
||||
result->message = ff_unescape_message(colon + 2);
|
||||
} else {
|
||||
result->from_jid = g_strdup("unknown");
|
||||
result->message = ff_unescape_message(rest);
|
||||
}
|
||||
} else {
|
||||
// No space at all — can't parse
|
||||
ff_parsed_line_free(result);
|
||||
g_free(work);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Parse timestamp
|
||||
result->timestamp = g_date_time_new_from_iso8601(result->timestamp_str, NULL);
|
||||
if (!result->timestamp) {
|
||||
log_warning("flatfile: unparsable timestamp: %s", result->timestamp_str);
|
||||
ff_parsed_line_free(result);
|
||||
g_free(work);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Default type/enc if missing
|
||||
if (!result->type)
|
||||
result->type = g_strdup("chat");
|
||||
if (!result->enc)
|
||||
result->enc = g_strdup("none");
|
||||
|
||||
g_free(work);
|
||||
return result;
|
||||
}
|
||||
|
||||
// Convert parsed line to ProfMessage
|
||||
ProfMessage*
|
||||
ff_parsed_to_profmessage(ff_parsed_line_t* pl)
|
||||
{
|
||||
ProfMessage* msg = message_init();
|
||||
msg->id = pl->stanza_id ? g_strdup(pl->stanza_id) : NULL;
|
||||
msg->from_jid = jid_create_from_bare_and_resource(pl->from_jid, pl->from_resource);
|
||||
if (pl->to_jid) {
|
||||
msg->to_jid = jid_create_from_bare_and_resource(pl->to_jid, pl->to_resource);
|
||||
}
|
||||
msg->plain = g_strdup(pl->message ? pl->message : "");
|
||||
msg->timestamp = g_date_time_ref(pl->timestamp);
|
||||
msg->type = ff_get_message_type_type(pl->type);
|
||||
msg->enc = ff_get_message_enc_type(pl->enc);
|
||||
return msg;
|
||||
}
|
||||
370
src/database_flatfile_verify.c
Normal file
370
src/database_flatfile_verify.c
Normal file
@@ -0,0 +1,370 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
//
|
||||
// This file is part of CProof.
|
||||
// See LICENSE for the full GPLv3 text and the special OpenSSL linking exception.
|
||||
|
||||
/*
|
||||
* database_flatfile_verify.c
|
||||
* vim: expandtab:ts=4:sts=4:sw=4
|
||||
*
|
||||
* Flat-file backend: integrity verification (/history verify).
|
||||
*
|
||||
* High-level flow (see ff_verify_integrity):
|
||||
* 1. Resolve target contact directories — either a single contact (when
|
||||
* contact_barejid != NULL) or every contact directory under
|
||||
* flatlog/<account>/.
|
||||
* 2. For each contact's history.log:
|
||||
* - Check file permissions (warn if not 0600).
|
||||
* - First pass: line-by-line UTF-8 / control-char / parser /
|
||||
* duplicate-id / timestamp-ordering checks. Records every
|
||||
* stanza-id and archive-id seen so the second pass can resolve
|
||||
* LMC references. stanza-id and archive-id are tracked in
|
||||
* *separate* hash tables — duplicate detection is reported
|
||||
* per id-kind to avoid spurious cross-kind warnings.
|
||||
* - Second pass: re-read the file and verify each `corrects:` link
|
||||
* points at a stanza-id that was seen in pass 1.
|
||||
* 3. Aggregate every issue into a GSList<integrity_issue_t> and return
|
||||
* it to the caller, ready for /history verify rendering.
|
||||
*
|
||||
* The file is split into small helpers so each pass can be understood in
|
||||
* isolation; ff_verify_integrity itself is just orchestration.
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <sys/stat.h>
|
||||
#include <glib.h>
|
||||
#include <glib/gstdio.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "log.h"
|
||||
#include "config/files.h"
|
||||
#include "database_flatfile.h"
|
||||
|
||||
// =========================================================================
|
||||
// Issue construction
|
||||
// =========================================================================
|
||||
|
||||
static integrity_issue_t*
|
||||
_issue_new(integrity_level_t level, const char* file, int line, char* message)
|
||||
{
|
||||
integrity_issue_t* issue = g_new0(integrity_issue_t, 1);
|
||||
issue->level = level;
|
||||
issue->file = g_strdup(file ? file : "");
|
||||
issue->line = line;
|
||||
issue->message = message; // takes ownership of the (g_strdup'd) message
|
||||
return issue;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Contact discovery
|
||||
// =========================================================================
|
||||
|
||||
// Build the list of contact directories to verify. If contact_barejid is
|
||||
// supplied, the list is either a singleton or empty (and an INFO issue is
|
||||
// appended explaining no logs exist for that contact). Otherwise, every
|
||||
// subdirectory of flatlog/<account>/ is enumerated.
|
||||
static GSList*
|
||||
_collect_contact_dirs(const gchar* const contact_barejid, GSList** issues)
|
||||
{
|
||||
GSList* contact_dirs = NULL;
|
||||
|
||||
if (contact_barejid) {
|
||||
auto_gchar gchar* cdir = ff_get_contact_dir(contact_barejid);
|
||||
if (cdir && g_file_test(cdir, G_FILE_TEST_IS_DIR)) {
|
||||
contact_dirs = g_slist_prepend(contact_dirs, g_strdup(cdir));
|
||||
} else {
|
||||
*issues = g_slist_prepend(*issues,
|
||||
_issue_new(INTEGRITY_INFO, contact_barejid, 0,
|
||||
g_strdup("No log files found for this contact")));
|
||||
}
|
||||
return contact_dirs;
|
||||
}
|
||||
|
||||
auto_gchar gchar* data_path = files_get_data_path(DIR_FLATLOG);
|
||||
auto_gchar gchar* my_dir = ff_jid_to_dir(g_flatfile_account_jid);
|
||||
auto_gchar gchar* base_dir = g_strdup_printf("%s/%s", data_path, my_dir);
|
||||
|
||||
GDir* dir = g_dir_open(base_dir, 0, NULL);
|
||||
if (!dir)
|
||||
return NULL;
|
||||
|
||||
const gchar* dname;
|
||||
while ((dname = g_dir_read_name(dir)) != NULL) {
|
||||
char* full = g_strdup_printf("%s/%s", base_dir, dname);
|
||||
if (g_file_test(full, G_FILE_TEST_IS_DIR)) {
|
||||
contact_dirs = g_slist_prepend(contact_dirs, full);
|
||||
} else {
|
||||
g_free(full);
|
||||
}
|
||||
}
|
||||
g_dir_close(dir);
|
||||
return contact_dirs;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Permission check
|
||||
// =========================================================================
|
||||
|
||||
static void
|
||||
_check_permissions(const char* filepath, const char* basename, GSList** issues)
|
||||
{
|
||||
struct stat st;
|
||||
if (g_stat(filepath, &st) != 0)
|
||||
return;
|
||||
if ((st.st_mode & 0777) == (S_IRUSR | S_IWUSR))
|
||||
return;
|
||||
*issues = g_slist_prepend(*issues,
|
||||
_issue_new(INTEGRITY_WARNING, basename, 0,
|
||||
g_strdup_printf("File permissions are %o, expected 600 (sensitive data)",
|
||||
st.st_mode & 0777)));
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// First pass: per-line content checks + id collection
|
||||
// =========================================================================
|
||||
//
|
||||
// For every non-empty, non-comment line:
|
||||
// - validate UTF-8
|
||||
// - flag control characters
|
||||
// - try to parse via ff_parse_line(); if that fails, record the lineno
|
||||
// - check timestamp monotonicity
|
||||
// - flag duplicate stanza-id and duplicate archive-id (separate tables)
|
||||
// - record every stanza-id encountered for LMC pass 2
|
||||
//
|
||||
// Also detects: BOM (informational), CRLF line endings (warning), empty
|
||||
// file (informational).
|
||||
|
||||
static void
|
||||
_first_pass(FILE* fp, const char* basename,
|
||||
GHashTable* seen_stanza_ids, GHashTable* seen_archive_ids,
|
||||
GHashTable* all_stanza_ids, GSList** issues)
|
||||
{
|
||||
if (ff_skip_bom(fp)) {
|
||||
*issues = g_slist_prepend(*issues,
|
||||
_issue_new(INTEGRITY_INFO, basename, 0,
|
||||
g_strdup("File has UTF-8 BOM — harmless but unnecessary")));
|
||||
}
|
||||
|
||||
char* buf = NULL;
|
||||
int lineno = 0;
|
||||
GDateTime* prev_ts = NULL;
|
||||
gboolean has_crlf = FALSE;
|
||||
gboolean is_empty = TRUE;
|
||||
|
||||
while ((buf = ff_readline(fp, NULL)) != NULL) {
|
||||
lineno++;
|
||||
gsize len = strlen(buf);
|
||||
|
||||
if (len > 0 && buf[len - 1] == '\r') {
|
||||
has_crlf = TRUE;
|
||||
buf[--len] = '\0';
|
||||
}
|
||||
|
||||
if (len == 0 || buf[0] == '#') {
|
||||
free(buf);
|
||||
continue;
|
||||
}
|
||||
is_empty = FALSE;
|
||||
|
||||
const gchar* end;
|
||||
if (!g_utf8_validate(buf, -1, &end)) {
|
||||
*issues = g_slist_prepend(*issues,
|
||||
_issue_new(INTEGRITY_ERROR, basename, lineno,
|
||||
g_strdup_printf("Invalid UTF-8 at byte offset %ld", (long)(end - buf))));
|
||||
free(buf);
|
||||
continue;
|
||||
}
|
||||
|
||||
for (gsize i = 0; i < len; i++) {
|
||||
unsigned char ch = (unsigned char)buf[i];
|
||||
if (ch < 0x20 && ch != '\t') {
|
||||
*issues = g_slist_prepend(*issues,
|
||||
_issue_new(INTEGRITY_WARNING, basename, lineno,
|
||||
g_strdup_printf("Contains control character 0x%02x", ch)));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
ff_parsed_line_t* pl = ff_parse_line(buf);
|
||||
if (!pl) {
|
||||
*issues = g_slist_prepend(*issues,
|
||||
_issue_new(INTEGRITY_ERROR, basename, lineno,
|
||||
g_strdup("Unparsable line")));
|
||||
free(buf);
|
||||
continue;
|
||||
}
|
||||
free(buf);
|
||||
|
||||
if (prev_ts && g_date_time_compare(pl->timestamp, prev_ts) < 0) {
|
||||
auto_gchar gchar* ts_cur = g_date_time_format_iso8601(pl->timestamp);
|
||||
auto_gchar gchar* ts_prev = g_date_time_format_iso8601(prev_ts);
|
||||
*issues = g_slist_prepend(*issues,
|
||||
_issue_new(INTEGRITY_WARNING, basename, lineno,
|
||||
g_strdup_printf("Timestamp out of order (%s after %s)", ts_cur, ts_prev)));
|
||||
}
|
||||
if (prev_ts)
|
||||
g_date_time_unref(prev_ts);
|
||||
prev_ts = g_date_time_ref(pl->timestamp);
|
||||
|
||||
if (pl->stanza_id && pl->stanza_id[0] != '\0') {
|
||||
if (g_hash_table_contains(seen_stanza_ids, pl->stanza_id)) {
|
||||
// Older clients (Pidgin, Adium, even early Profanity)
|
||||
// sometimes reuse client-generated stanza-ids across
|
||||
// distinct messages, so a collision is not a real
|
||||
// integrity defect — log it for diagnostics but don't
|
||||
// surface it through /history verify.
|
||||
log_debug("flatfile verify: duplicate stanza-id \"%s\" at %s:%d",
|
||||
pl->stanza_id, basename, lineno);
|
||||
} else {
|
||||
g_hash_table_insert(seen_stanza_ids, g_strdup(pl->stanza_id), GINT_TO_POINTER(lineno));
|
||||
}
|
||||
g_hash_table_insert(all_stanza_ids, g_strdup(pl->stanza_id), GINT_TO_POINTER(lineno));
|
||||
}
|
||||
if (pl->archive_id && pl->archive_id[0] != '\0') {
|
||||
if (g_hash_table_contains(seen_archive_ids, pl->archive_id)) {
|
||||
*issues = g_slist_prepend(*issues,
|
||||
_issue_new(INTEGRITY_WARNING, basename, lineno,
|
||||
g_strdup_printf("Duplicate archive-id \"%s\"", pl->archive_id)));
|
||||
} else {
|
||||
g_hash_table_insert(seen_archive_ids, g_strdup(pl->archive_id), GINT_TO_POINTER(lineno));
|
||||
}
|
||||
}
|
||||
|
||||
ff_parsed_line_free(pl);
|
||||
}
|
||||
|
||||
if (prev_ts)
|
||||
g_date_time_unref(prev_ts);
|
||||
|
||||
if (has_crlf) {
|
||||
*issues = g_slist_prepend(*issues,
|
||||
_issue_new(INTEGRITY_WARNING, basename, 0,
|
||||
g_strdup("File uses Windows line endings (CRLF) — consider converting to LF")));
|
||||
}
|
||||
if (is_empty) {
|
||||
*issues = g_slist_prepend(*issues,
|
||||
_issue_new(INTEGRITY_INFO, basename, 0,
|
||||
g_strdup("File is empty (no message lines)")));
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Second pass: LMC reference resolution
|
||||
// =========================================================================
|
||||
//
|
||||
// Walks the file again, and for every line that carries a `corrects:` link,
|
||||
// checks that the referenced stanza-id was seen in pass 1. Broken references
|
||||
// surface as INTEGRITY_ERROR.
|
||||
|
||||
static void
|
||||
_lmc_pass(FILE* fp, const char* basename, GHashTable* all_stanza_ids, GSList** issues)
|
||||
{
|
||||
ff_skip_bom(fp);
|
||||
|
||||
char* buf = NULL;
|
||||
int lineno = 0;
|
||||
|
||||
while ((buf = ff_readline(fp, NULL)) != NULL) {
|
||||
lineno++;
|
||||
gsize len = strlen(buf);
|
||||
if (len > 0 && buf[len - 1] == '\r')
|
||||
buf[--len] = '\0';
|
||||
if (len == 0 || buf[0] == '#') {
|
||||
free(buf);
|
||||
continue;
|
||||
}
|
||||
|
||||
ff_parsed_line_t* pl = ff_parse_line(buf);
|
||||
free(buf);
|
||||
if (!pl)
|
||||
continue;
|
||||
|
||||
if (pl->replace_id && pl->replace_id[0] != '\0'
|
||||
&& !g_hash_table_contains(all_stanza_ids, pl->replace_id)) {
|
||||
*issues = g_slist_prepend(*issues,
|
||||
_issue_new(INTEGRITY_ERROR, basename, lineno,
|
||||
g_strdup_printf("Broken correction reference: corrects:%s not found",
|
||||
pl->replace_id)));
|
||||
}
|
||||
ff_parsed_line_free(pl);
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Per-contact verification
|
||||
// =========================================================================
|
||||
|
||||
static void
|
||||
_verify_contact_dir(const char* cdir_path, GSList** issues)
|
||||
{
|
||||
auto_gchar gchar* filepath = g_strdup_printf("%s/history.log", cdir_path);
|
||||
if (!g_file_test(filepath, G_FILE_TEST_EXISTS)) {
|
||||
log_debug("flatfile verify: skipping %s (no history.log)", cdir_path);
|
||||
return;
|
||||
}
|
||||
|
||||
// Reconstruct the contact JID from the directory name so issue reports
|
||||
// identify which contact the problem belongs to (every contact has its
|
||||
// own history.log, so the bare filename is ambiguous).
|
||||
auto_gchar gchar* dir_name = g_path_get_basename(cdir_path);
|
||||
auto_gcharv gchar** parts = g_strsplit(dir_name, "_at_", -1);
|
||||
auto_gchar gchar* contact_jid = g_strjoinv("@", parts);
|
||||
auto_gchar gchar* basename = g_strdup_printf("%s/history.log", contact_jid);
|
||||
|
||||
_check_permissions(filepath, basename, issues);
|
||||
|
||||
FILE* fp = fopen(filepath, "r");
|
||||
if (!fp) {
|
||||
log_warning("flatfile verify: cannot open %s for reading", filepath);
|
||||
return;
|
||||
}
|
||||
|
||||
// Separate tables by id-kind: a stanza-id and an archive-id can legitimately
|
||||
// share the same string without that being a duplicate.
|
||||
GHashTable* seen_stanza_ids = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL);
|
||||
GHashTable* seen_archive_ids = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL);
|
||||
GHashTable* all_stanza_ids = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL);
|
||||
|
||||
_first_pass(fp, basename, seen_stanza_ids, seen_archive_ids, all_stanza_ids, issues);
|
||||
fclose(fp);
|
||||
|
||||
// Second pass: LMC references
|
||||
fp = fopen(filepath, "r");
|
||||
if (fp) {
|
||||
_lmc_pass(fp, basename, all_stanza_ids, issues);
|
||||
fclose(fp);
|
||||
}
|
||||
|
||||
g_hash_table_destroy(seen_stanza_ids);
|
||||
g_hash_table_destroy(seen_archive_ids);
|
||||
g_hash_table_destroy(all_stanza_ids);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Public entry point
|
||||
// =========================================================================
|
||||
|
||||
GSList*
|
||||
ff_verify_integrity(const gchar* const contact_barejid)
|
||||
{
|
||||
GSList* issues = NULL;
|
||||
|
||||
if (!g_flatfile_account_jid) {
|
||||
issues = g_slist_prepend(issues,
|
||||
_issue_new(INTEGRITY_ERROR, "N/A", 0,
|
||||
g_strdup("Flat-file backend not initialized")));
|
||||
return issues;
|
||||
}
|
||||
|
||||
GSList* contact_dirs = _collect_contact_dirs(contact_barejid, &issues);
|
||||
|
||||
for (GSList* cd = contact_dirs; cd; cd = cd->next) {
|
||||
_verify_contact_dir((const char*)cd->data, &issues);
|
||||
}
|
||||
|
||||
g_slist_free_full(contact_dirs, g_free);
|
||||
return g_slist_reverse(issues);
|
||||
}
|
||||
1104
src/database_sqlite.c
Normal file
1104
src/database_sqlite.c
Normal file
File diff suppressed because it is too large
Load Diff
@@ -3,49 +3,27 @@
|
||||
* vim: expandtab:ts=4:sts=4:sw=4
|
||||
*
|
||||
* Copyright (C) 2012 - 2019 James Booth <boothj5@gmail.com>
|
||||
* Copyright (C) 2019 - 2025 Michael Vetter <jubalh@iodoru.org>
|
||||
*
|
||||
* This file is part of Profanity.
|
||||
*
|
||||
* Profanity is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Profanity is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Profanity. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* In addition, as a special exception, the copyright holders give permission to
|
||||
* link the code of portions of this program with the OpenSSL library under
|
||||
* certain conditions as described in each individual source file, and
|
||||
* distribute linked combinations including the two.
|
||||
*
|
||||
* You must obey the GNU General Public License in all respects for all of the
|
||||
* code used other than OpenSSL. If you modify file(s) with this exception, you
|
||||
* may extend this exception to your version of the file(s), but you are not
|
||||
* obligated to do so. If you do not wish to do so, delete this exception
|
||||
* statement from your version. If you delete this exception statement from all
|
||||
* source files in the program, then also delete it here.
|
||||
* Copyright (C) 2019 - 2026 Michael Vetter <jubalh@iodoru.org>
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later WITH OpenSSL-exception
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <assert.h>
|
||||
#include <glib.h>
|
||||
|
||||
#include "ai/ai_client.h"
|
||||
#include "log.h"
|
||||
#include "chatlog.h"
|
||||
#include "database.h"
|
||||
#include "client_events.h"
|
||||
#include "config/preferences.h"
|
||||
#include "event/common.h"
|
||||
#include "plugins/plugins.h"
|
||||
#include "ui/window_list.h"
|
||||
#include "ui/inputwin.h"
|
||||
#include "ui/window.h"
|
||||
#include "xmpp/chat_session.h"
|
||||
#include "xmpp/session.h"
|
||||
#include "xmpp/xmpp.h"
|
||||
@@ -116,7 +94,7 @@ cl_ev_reconnect(void)
|
||||
void
|
||||
cl_ev_presence_send(const resource_presence_t presence_type, const int idle_secs)
|
||||
{
|
||||
auto_char char* signed_status = NULL;
|
||||
auto_gchar gchar* signed_status = NULL;
|
||||
|
||||
#ifdef HAVE_LIBGPGME
|
||||
ProfAccount* account = accounts_get_account(session_get_account_name());
|
||||
@@ -135,9 +113,19 @@ cl_ev_send_msg_correct(ProfChatWin* chatwin, const char* const msg, const char*
|
||||
chat_state_active(chatwin->state);
|
||||
|
||||
gboolean request_receipt = prefs_get_boolean(PREF_RECEIPTS_REQUEST);
|
||||
if (request_receipt) {
|
||||
auto_char char* jid = chat_session_get_jid(chatwin->barejid);
|
||||
EntityCapabilities* caps = caps_lookup(jid);
|
||||
if (caps != NULL) {
|
||||
GSList* found = g_slist_find_custom(caps->features, XMPP_FEATURE_RECEIPTS, (GCompareFunc)g_strcmp0);
|
||||
request_receipt = (found != NULL);
|
||||
caps_destroy(caps);
|
||||
}
|
||||
}
|
||||
|
||||
auto_char char* plugin_msg = plugins_pre_chat_message_send(chatwin->barejid, msg);
|
||||
const char* const message = plugin_msg ?: msg;
|
||||
auto_gchar gchar* sanitized_msg = str_xml_sanitize(plugin_msg ?: msg);
|
||||
const char* message = sanitized_msg;
|
||||
|
||||
char* replace_id = NULL;
|
||||
if (correct_last_msg) {
|
||||
@@ -177,7 +165,7 @@ cl_ev_send_msg_correct(ProfChatWin* chatwin, const char* const msg, const char*
|
||||
#ifdef HAVE_LIBOTR
|
||||
handled = otr_on_message_send(chatwin, message, request_receipt, replace_id);
|
||||
#endif
|
||||
if (!handled) {
|
||||
if (!handled && allow_unencrypted_message(chatwin, message)) {
|
||||
auto_char char* id = message_send_chat(chatwin->barejid, message, oob_url, request_receipt, replace_id);
|
||||
chat_log_msg_out(chatwin->barejid, message, NULL);
|
||||
log_database_add_outgoing_chat(id, chatwin->barejid, message, replace_id, PROF_MSG_ENC_NONE);
|
||||
@@ -199,7 +187,8 @@ void
|
||||
cl_ev_send_muc_msg_corrected(ProfMucWin* mucwin, const char* const msg, const char* const oob_url, gboolean correct_last_msg)
|
||||
{
|
||||
auto_char char* plugin_msg = plugins_pre_room_message_send(mucwin->roomjid, msg);
|
||||
const char* const message = plugin_msg ?: msg;
|
||||
auto_gchar gchar* sanitized_msg = str_xml_sanitize(plugin_msg ?: msg);
|
||||
const char* message = sanitized_msg;
|
||||
|
||||
char* replace_id = NULL;
|
||||
if (correct_last_msg) {
|
||||
@@ -240,7 +229,8 @@ cl_ev_send_priv_msg(ProfPrivateWin* privwin, const char* const msg, const char*
|
||||
privwin_message_left_room(privwin);
|
||||
} else {
|
||||
auto_char char* plugin_msg = plugins_pre_priv_message_send(privwin->fulljid, msg);
|
||||
const char* const message = plugin_msg ?: msg;
|
||||
auto_gchar gchar* sanitized_msg = str_xml_sanitize(plugin_msg ?: msg);
|
||||
const char* message = sanitized_msg;
|
||||
auto_jid Jid* jidp = jid_create(privwin->fulljid);
|
||||
|
||||
auto_char char* id = message_send_private(privwin->fulljid, message, oob_url);
|
||||
@@ -251,3 +241,55 @@ cl_ev_send_priv_msg(ProfPrivateWin* privwin, const char* const msg, const char*
|
||||
plugins_post_priv_message_send(privwin->fulljid, message);
|
||||
}
|
||||
}
|
||||
|
||||
gboolean
|
||||
allow_unencrypted_message(ProfChatWin* chatwin, const char* const msg)
|
||||
{
|
||||
if (!prefs_get_boolean(PREF_FORCE_ENCRYPTION)) {
|
||||
return TRUE; // Encryption not enforced
|
||||
}
|
||||
|
||||
auto_gchar gchar* force_enc_mode = prefs_get_string(PREF_FORCE_ENCRYPTION_MODE);
|
||||
|
||||
if (g_strcmp0(force_enc_mode, "block") == 0) {
|
||||
win_println((ProfWin*)chatwin, THEME_ERROR, "-", "Message not sent: encryption required. Enable (omemo/pgp/otr) encryption or /force-encryption off.");
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
if (g_strcmp0(force_enc_mode, "resend-to-confirm") == 0) {
|
||||
// We do not use chatwin->last_message because it should be set only if it's sent.
|
||||
guint msg_hash = g_str_hash(msg);
|
||||
if (msg_hash == chatwin->last_attempted_msg_hash) {
|
||||
chatwin->last_attempted_msg_hash = 0; // reset hash
|
||||
return TRUE;
|
||||
}
|
||||
win_println((ProfWin*)chatwin, THEME_ERROR, "-", "Message not sent: encryption required. Enable (omemo/pgp/otr) encryption or press Enter again to send without encryption. Use /force-encryption off to disable this protection.");
|
||||
chatwin->last_attempted_msg_hash = msg_hash;
|
||||
inp_set_line(msg); // reset message to prevent the need to retype it
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
win_println((ProfWin*)chatwin, THEME_ERROR, "-", "Message not sent: invalid encryption mode (%s). Use '/force-encryption policy resend-to-confirm'.", force_enc_mode);
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
void
|
||||
cl_ev_send_ai_msg(ProfAiWin* aiwin, const char* const message, const char* const id)
|
||||
{
|
||||
if (!aiwin || !message) {
|
||||
return;
|
||||
}
|
||||
|
||||
assert(aiwin->memcheck == PROFAIWIN_MEMCHECK);
|
||||
|
||||
if (!aiwin->session) {
|
||||
log_error("[AI] AI session not initialized");
|
||||
return;
|
||||
}
|
||||
|
||||
// Display user message in AI window.
|
||||
win_print_outgoing(&aiwin->window, ">>", id, NULL, message);
|
||||
|
||||
// Send to AI provider.
|
||||
ai_send_prompt(aiwin->session, message, aiwin);
|
||||
}
|
||||
|
||||
@@ -4,33 +4,7 @@
|
||||
*
|
||||
* Copyright (C) 2012 - 2019 James Booth <boothj5@gmail.com>
|
||||
*
|
||||
* This file is part of Profanity.
|
||||
*
|
||||
* Profanity is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Profanity is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Profanity. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* In addition, as a special exception, the copyright holders give permission to
|
||||
* link the code of portions of this program with the OpenSSL library under
|
||||
* certain conditions as described in each individual source file, and
|
||||
* distribute linked combinations including the two.
|
||||
*
|
||||
* You must obey the GNU General Public License in all respects for all of the
|
||||
* code used other than OpenSSL. If you modify file(s) with this exception, you
|
||||
* may extend this exception to your version of the file(s), but you are not
|
||||
* obligated to do so. If you do not wish to do so, delete this exception
|
||||
* statement from your version. If you delete this exception statement from all
|
||||
* source files in the program, then also delete it here.
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later WITH OpenSSL-exception
|
||||
*/
|
||||
|
||||
#ifndef EVENT_CLIENT_EVENTS_H
|
||||
@@ -51,5 +25,10 @@ void cl_ev_send_msg(ProfChatWin* chatwin, const char* const msg, const char* con
|
||||
void cl_ev_send_muc_msg_corrected(ProfMucWin* mucwin, const char* const msg, const char* const oob_url, gboolean correct_last_msg);
|
||||
void cl_ev_send_muc_msg(ProfMucWin* mucwin, const char* const msg, const char* const oob_url);
|
||||
void cl_ev_send_priv_msg(ProfPrivateWin* privwin, const char* const msg, const char* const oob_url);
|
||||
void cl_ev_send_ai_msg(ProfAiWin* aiwin, const char* const message, const char* const id);
|
||||
|
||||
// Checks if an unencrypted message can be sent based on encryption preferences.
|
||||
// Returns TRUE if allowed, FALSE if blocked.
|
||||
gboolean allow_unencrypted_message(ProfChatWin* chatwin, const char* const msg);
|
||||
|
||||
#endif
|
||||
|
||||
@@ -2,35 +2,9 @@
|
||||
* common.c
|
||||
* vim: expandtab:ts=4:sts=4:sw=4
|
||||
*
|
||||
* Copyright (C) 2019 - 2025 Michael Vetter <jubalh@iodoru.org>
|
||||
*
|
||||
* This file is part of Profanity.
|
||||
*
|
||||
* Profanity is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Profanity is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Profanity. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* In addition, as a special exception, the copyright holders give permission to
|
||||
* link the code of portions of this program with the OpenSSL library under
|
||||
* certain conditions as described in each individual source file, and
|
||||
* distribute linked combinations including the two.
|
||||
*
|
||||
* You must obey the GNU General Public License in all respects for all of the
|
||||
* code used other than OpenSSL. If you modify file(s) with this exception, you
|
||||
* may extend this exception to your version of the file(s), but you are not
|
||||
* obligated to do so. If you do not wish to do so, delete this exception
|
||||
* statement from your version. If you delete this exception statement from all
|
||||
* source files in the program, then also delete it here.
|
||||
* Copyright (C) 2019 - 2026 Michael Vetter <jubalh@iodoru.org>
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later WITH OpenSSL-exception
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
|
||||
@@ -2,40 +2,16 @@
|
||||
* common.h
|
||||
* vim: expandtab:ts=4:sts=4:sw=4
|
||||
*
|
||||
* Copyright (C) 2019 - 2025 Michael Vetter <jubalh@iodoru.org>
|
||||
*
|
||||
* This file is part of Profanity.
|
||||
*
|
||||
* Profanity is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Profanity is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Profanity. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* In addition, as a special exception, the copyright holders give permission to
|
||||
* link the code of portions of this program with the OpenSSL library under
|
||||
* certain conditions as described in each individual source file, and
|
||||
* distribute linked combinations including the two.
|
||||
*
|
||||
* You must obey the GNU General Public License in all respects for all of the
|
||||
* code used other than OpenSSL. If you modify file(s) with this exception, you
|
||||
* may extend this exception to your version of the file(s), but you are not
|
||||
* obligated to do so. If you do not wish to do so, delete this exception
|
||||
* statement from your version. If you delete this exception statement from all
|
||||
* source files in the program, then also delete it here.
|
||||
* Copyright (C) 2019 - 2026 Michael Vetter <jubalh@iodoru.org>
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later WITH OpenSSL-exception
|
||||
*/
|
||||
|
||||
#ifndef EVENT_COMMON_H
|
||||
#define EVENT_COMMON_H
|
||||
|
||||
#include "glib.h"
|
||||
|
||||
void ev_disconnect_cleanup(void);
|
||||
void ev_inc_connection_counter(void);
|
||||
void ev_reset_connection_counter(void);
|
||||
|
||||
@@ -3,35 +3,9 @@
|
||||
* vim: expandtab:ts=4:sts=4:sw=4
|
||||
*
|
||||
* Copyright (C) 2012 - 2019 James Booth <boothj5@gmail.com>
|
||||
* Copyright (C) 2019 - 2025 Michael Vetter <jubalh@iodoru.org>
|
||||
*
|
||||
* This file is part of Profanity.
|
||||
*
|
||||
* Profanity is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Profanity is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Profanity. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* In addition, as a special exception, the copyright holders give permission to
|
||||
* link the code of portions of this program with the OpenSSL library under
|
||||
* certain conditions as described in each individual source file, and
|
||||
* distribute linked combinations including the two.
|
||||
*
|
||||
* You must obey the GNU General Public License in all respects for all of the
|
||||
* code used other than OpenSSL. If you modify file(s) with this exception, you
|
||||
* may extend this exception to your version of the file(s), but you are not
|
||||
* obligated to do so. If you do not wish to do so, delete this exception
|
||||
* statement from your version. If you delete this exception statement from all
|
||||
* source files in the program, then also delete it here.
|
||||
* Copyright (C) 2019 - 2026 Michael Vetter <jubalh@iodoru.org>
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later WITH OpenSSL-exception
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
@@ -86,6 +60,10 @@ void
|
||||
sv_ev_login_account_success(char* account_name, gboolean secured)
|
||||
{
|
||||
ProfAccount* account = accounts_get_account(account_name);
|
||||
if (!account) {
|
||||
log_error("Could not find account: %s", account_name);
|
||||
return;
|
||||
}
|
||||
|
||||
bookmark_ignore_on_connect(account->jid);
|
||||
|
||||
@@ -103,7 +81,9 @@ sv_ev_login_account_success(char* account_name, gboolean secured)
|
||||
omemo_on_connect(account);
|
||||
#endif
|
||||
|
||||
log_database_init(account);
|
||||
if (!log_database_init(account)) {
|
||||
log_error("Failed to initialize database for account: %s", account->jid);
|
||||
}
|
||||
vcard_user_refresh();
|
||||
avatar_pep_subscribe();
|
||||
|
||||
@@ -176,7 +156,7 @@ sv_ev_roster_received(void)
|
||||
GDateTime* lastdt = g_date_time_new_from_timeval_utc(&lasttv);
|
||||
GTimeSpan diff_micros = g_date_time_difference(nowdt, lastdt);
|
||||
|
||||
diff_secs = (diff_micros / 1000) / 1000;
|
||||
diff_secs = (int)((diff_micros / 1000) / 1000);
|
||||
g_date_time_unref(lastdt);
|
||||
}
|
||||
g_date_time_unref(nowdt);
|
||||
@@ -361,7 +341,7 @@ sv_ev_room_message(ProfMessage* message)
|
||||
status_bar_active(num, WIN_MUC, mucwin->roomjid);
|
||||
|
||||
if ((g_strcmp0(mynick, message->from_jid->resourcepart) != 0) && (prefs_get_boolean(PREF_BEEP))) {
|
||||
beep();
|
||||
ui_beep();
|
||||
}
|
||||
|
||||
// not currently on groupchat window
|
||||
@@ -369,7 +349,7 @@ sv_ev_room_message(ProfMessage* message)
|
||||
status_bar_new(num, WIN_MUC, mucwin->roomjid);
|
||||
|
||||
if ((g_strcmp0(mynick, message->from_jid->resourcepart) != 0) && (prefs_get_boolean(PREF_FLASH))) {
|
||||
flash();
|
||||
ui_flash();
|
||||
}
|
||||
|
||||
cons_show_incoming_room_message(message->from_jid->resourcepart, mucwin->roomjid, num, mention, triggers, mucwin->unread, window);
|
||||
@@ -587,6 +567,8 @@ _sv_ev_incoming_omemo(ProfChatWin* chatwin, gboolean new_win, ProfMessage* messa
|
||||
chat_log_omemo_msg_in(message);
|
||||
}
|
||||
chatwin->pgp_recv = FALSE;
|
||||
|
||||
wins_omemo_trust_changed(chatwin->barejid);
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -596,6 +578,9 @@ _sv_ev_incoming_plain(ProfChatWin* chatwin, gboolean new_win, ProfMessage* messa
|
||||
if (message->body) {
|
||||
message->enc = PROF_MSG_ENC_NONE;
|
||||
message->plain = strdup(message->body);
|
||||
if (message->plain == NULL) {
|
||||
return;
|
||||
}
|
||||
_clean_incoming_message(message);
|
||||
chatwin_incoming_msg(chatwin, message, new_win);
|
||||
log_database_add_incoming(message);
|
||||
@@ -1150,14 +1135,13 @@ int
|
||||
sv_ev_certfail(const char* const errormsg, const TLSCertificate* cert)
|
||||
{
|
||||
// check profanity trusted certs
|
||||
if (tlscerts_exists(cert->fingerprint)) {
|
||||
if (tlscerts_exists(cert)) {
|
||||
cafile_add(cert);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// check current cert
|
||||
char* current_fp = tlscerts_get_current();
|
||||
if (current_fp && g_strcmp0(current_fp, cert->fingerprint) == 0) {
|
||||
if (tlscerts_current_fingerprint_equals(cert)) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -1188,11 +1172,11 @@ sv_ev_certfail(const char* const errormsg, const TLSCertificate* cert)
|
||||
|
||||
if (g_strcmp0(cmd, "/tls allow") == 0) {
|
||||
cons_show("Continuing with connection.");
|
||||
tlscerts_set_current(cert->fingerprint);
|
||||
tlscerts_set_current(cert);
|
||||
return 1;
|
||||
} else if (g_strcmp0(cmd, "/tls always") == 0) {
|
||||
cons_show("Adding %s to trusted certificates.", cert->fingerprint);
|
||||
if (!tlscerts_exists(cert->fingerprint)) {
|
||||
if (!tlscerts_exists(cert)) {
|
||||
tlscerts_add(cert);
|
||||
cafile_add(cert);
|
||||
}
|
||||
@@ -1303,6 +1287,10 @@ sv_ev_bookmark_autojoin(Bookmark* bookmark)
|
||||
static void
|
||||
_cut(ProfMessage* message, const char* cut)
|
||||
{
|
||||
if (message->plain == NULL) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (strstr(message->plain, cut)) {
|
||||
auto_gcharv gchar** split = g_strsplit(message->plain, cut, -1);
|
||||
free(message->plain);
|
||||
|
||||
@@ -4,33 +4,7 @@
|
||||
*
|
||||
* Copyright (C) 2012 - 2019 James Booth <boothj5@gmail.com>
|
||||
*
|
||||
* This file is part of Profanity.
|
||||
*
|
||||
* Profanity is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Profanity is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Profanity. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* In addition, as a special exception, the copyright holders give permission to
|
||||
* link the code of portions of this program with the OpenSSL library under
|
||||
* certain conditions as described in each individual source file, and
|
||||
* distribute linked combinations including the two.
|
||||
*
|
||||
* You must obey the GNU General Public License in all respects for all of the
|
||||
* code used other than OpenSSL. If you modify file(s) with this exception, you
|
||||
* may extend this exception to your version of the file(s), but you are not
|
||||
* obligated to do so. If you do not wish to do so, delete this exception
|
||||
* statement from your version. If you delete this exception statement from all
|
||||
* source files in the program, then also delete it here.
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later WITH OpenSSL-exception
|
||||
*/
|
||||
|
||||
#ifndef EVENT_SERVER_EVENTS_H
|
||||
|
||||
6
src/gitversion.h.in
Normal file
6
src/gitversion.h.in
Normal file
@@ -0,0 +1,6 @@
|
||||
#ifndef PROF_GIT_BRANCH
|
||||
#define PROF_GIT_BRANCH @PROF_GIT_BRANCH@
|
||||
#endif
|
||||
#ifndef PROF_GIT_REVISION
|
||||
#define PROF_GIT_REVISION @PROF_GIT_REVISION@
|
||||
#endif
|
||||
64
src/log.c
64
src/log.c
@@ -3,35 +3,9 @@
|
||||
* vim: expandtab:ts=4:sts=4:sw=4
|
||||
*
|
||||
* Copyright (C) 2012 - 2019 James Booth <boothj5@gmail.com>
|
||||
* Copyright (C) 2018 - 2025 Michael Vetter <jubalh@iodoru.org>
|
||||
*
|
||||
* This file is part of Profanity.
|
||||
*
|
||||
* Profanity is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Profanity is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Profanity. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* In addition, as a special exception, the copyright holders give permission to
|
||||
* link the code of portions of this program with the OpenSSL library under
|
||||
* certain conditions as described in each individual source file, and
|
||||
* distribute linked combinations including the two.
|
||||
*
|
||||
* You must obey the GNU General Public License in all respects for all of the
|
||||
* code used other than OpenSSL. If you modify file(s) with this exception, you
|
||||
* may extend this exception to your version of the file(s), but you are not
|
||||
* obligated to do so. If you do not wish to do so, delete this exception
|
||||
* statement from your version. If you delete this exception statement from all
|
||||
* source files in the program, then also delete it here.
|
||||
* Copyright (C) 2018 - 2026 Michael Vetter <jubalh@iodoru.org>
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later WITH OpenSSL-exception
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
@@ -60,6 +34,7 @@ static FILE* logp;
|
||||
static gchar* mainlogfile = NULL;
|
||||
static gboolean user_provided_log = FALSE;
|
||||
static log_level_t level_filter;
|
||||
static pid_t prof_pid;
|
||||
|
||||
static int stderr_inited;
|
||||
static log_level_t stderr_level;
|
||||
@@ -75,6 +50,8 @@ enum {
|
||||
static void
|
||||
_rotate_log_file(void)
|
||||
{
|
||||
if (!mainlogfile)
|
||||
return;
|
||||
auto_gchar gchar* log_file = g_strdup(mainlogfile);
|
||||
size_t len = strlen(log_file);
|
||||
auto_gchar gchar* log_file_new = malloc(len + 5);
|
||||
@@ -186,7 +163,7 @@ log_error(const char* const msg, ...)
|
||||
}
|
||||
|
||||
void
|
||||
log_init(log_level_t filter, char* log_file)
|
||||
log_init(log_level_t filter, const char* const log_file)
|
||||
{
|
||||
level_filter = filter;
|
||||
|
||||
@@ -198,6 +175,8 @@ log_init(log_level_t filter, char* log_file)
|
||||
|
||||
logp = fopen(mainlogfile, "a");
|
||||
g_chmod(mainlogfile, S_IRUSR | S_IWUSR);
|
||||
|
||||
prof_pid = getpid();
|
||||
}
|
||||
|
||||
const gchar*
|
||||
@@ -225,14 +204,11 @@ log_close(void)
|
||||
static void
|
||||
_log_msg(log_level_t level, const char* const area, const char* const msg)
|
||||
{
|
||||
GDateTime* dt = g_date_time_new_now_local();
|
||||
|
||||
char* level_str = _log_abbreviation_string_from_level(level);
|
||||
|
||||
auto_gchar gchar* date_fmt = g_date_time_format_iso8601(dt);
|
||||
auto_gchar gchar* date_fmt = prof_date_time_format_iso8601(NULL);
|
||||
|
||||
fprintf(logp, "%s: %s: %s: %s\n", date_fmt, area, level_str, msg);
|
||||
g_date_time_unref(dt);
|
||||
fprintf(logp, "%s: %08d: %s: %s: %s\n", date_fmt, prof_pid, area, level_str, msg);
|
||||
|
||||
fflush(logp);
|
||||
|
||||
@@ -359,11 +335,6 @@ log_stderr_init(log_level_t level)
|
||||
if (rc != 0)
|
||||
goto err;
|
||||
|
||||
close(STDERR_FILENO);
|
||||
rc = dup2(stderr_pipe[1], STDERR_FILENO);
|
||||
if (rc < 0)
|
||||
goto err_close;
|
||||
|
||||
rc = log_stderr_nonblock_set(stderr_pipe[0])
|
||||
?: log_stderr_nonblock_set(stderr_pipe[1]);
|
||||
if (rc != 0)
|
||||
@@ -371,15 +342,22 @@ log_stderr_init(log_level_t level)
|
||||
|
||||
stderr_buf = malloc(STDERR_BUFSIZE);
|
||||
stderr_msg = g_string_sized_new(STDERR_BUFSIZE);
|
||||
stderr_level = level;
|
||||
stderr_inited = 1;
|
||||
|
||||
prof_add_shutdown_routine(_log_stderr_close);
|
||||
|
||||
if (stderr_buf == NULL || stderr_msg == NULL) {
|
||||
errno = ENOMEM;
|
||||
goto err_free;
|
||||
}
|
||||
|
||||
if (dup2(stderr_pipe[1], STDERR_FILENO) < 0)
|
||||
goto err_free;
|
||||
// fd 2 now owns the pipe write end; release the original.
|
||||
close(stderr_pipe[1]);
|
||||
stderr_pipe[1] = STDERR_FILENO;
|
||||
|
||||
stderr_level = level;
|
||||
stderr_inited = 1;
|
||||
|
||||
prof_add_shutdown_routine(_log_stderr_close);
|
||||
return;
|
||||
|
||||
err_free:
|
||||
|
||||
34
src/log.h
34
src/log.h
@@ -4,33 +4,7 @@
|
||||
*
|
||||
* Copyright (C) 2012 - 2019 James Booth <boothj5@gmail.com>
|
||||
*
|
||||
* This file is part of Profanity.
|
||||
*
|
||||
* Profanity is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Profanity is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Profanity. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* In addition, as a special exception, the copyright holders give permission to
|
||||
* link the code of portions of this program with the OpenSSL library under
|
||||
* certain conditions as described in each individual source file, and
|
||||
* distribute linked combinations including the two.
|
||||
*
|
||||
* You must obey the GNU General Public License in all respects for all of the
|
||||
* code used other than OpenSSL. If you modify file(s) with this exception, you
|
||||
* may extend this exception to your version of the file(s), but you are not
|
||||
* obligated to do so. If you do not wish to do so, delete this exception
|
||||
* statement from your version. If you delete this exception statement from all
|
||||
* source files in the program, then also delete it here.
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later WITH OpenSSL-exception
|
||||
*/
|
||||
|
||||
#ifndef LOG_H
|
||||
@@ -50,13 +24,17 @@ typedef enum {
|
||||
PROF_LEVEL_ERROR
|
||||
} log_level_t;
|
||||
|
||||
void log_init(log_level_t filter, char* log_file);
|
||||
void log_init(log_level_t filter, const char* const log_file);
|
||||
log_level_t log_get_filter(void);
|
||||
void log_close(void);
|
||||
const gchar* get_log_file_location(void);
|
||||
G_GNUC_PRINTF(1, 2)
|
||||
void log_debug(const char* const msg, ...);
|
||||
G_GNUC_PRINTF(1, 2)
|
||||
void log_info(const char* const msg, ...);
|
||||
G_GNUC_PRINTF(1, 2)
|
||||
void log_warning(const char* const msg, ...);
|
||||
G_GNUC_PRINTF(1, 2)
|
||||
void log_error(const char* const msg, ...);
|
||||
void log_msg(log_level_t level, const char* const area, const char* const msg);
|
||||
int log_level_from_string(char* log_level, log_level_t* level);
|
||||
|
||||
38
src/main.c
38
src/main.c
@@ -3,35 +3,9 @@
|
||||
* vim: expandtab:ts=4:sts=4:sw=4
|
||||
*
|
||||
* Copyright (C) 2012 - 2019 James Booth <boothj5@gmail.com>
|
||||
* Copyright (C) 2019 - 2025 Michael Vetter <jubalh@iodoru.org>
|
||||
*
|
||||
* This file is part of Profanity.
|
||||
*
|
||||
* Profanity is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Profanity is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Profanity. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* In addition, as a special exception, the copyright holders give permission to
|
||||
* link the code of portions of this program with the OpenSSL library under
|
||||
* certain conditions as described in each individual source file, and
|
||||
* distribute linked combinations including the two.
|
||||
*
|
||||
* You must obey the GNU General Public License in all respects for all of the
|
||||
* code used other than OpenSSL. If you modify file(s) with this exception, you
|
||||
* may extend this exception to your version of the file(s), but you are not
|
||||
* obligated to do so. If you do not wish to do so, delete this exception
|
||||
* statement from your version. If you delete this exception statement from all
|
||||
* source files in the program, then also delete it here.
|
||||
* Copyright (C) 2019 - 2026 Michael Vetter <jubalh@iodoru.org>
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later WITH OpenSSL-exception
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
@@ -78,7 +52,7 @@ main(int argc, char** argv)
|
||||
|
||||
GOptionEntry entries[] = {
|
||||
{ "version", 'v', 0, G_OPTION_ARG_NONE, &version, "Show version information", NULL },
|
||||
{ "account", 'a', 0, G_OPTION_ARG_STRING, &account_name, "Auto connect to an account on startup" },
|
||||
{ "account", 'a', 0, G_OPTION_ARG_STRING, &account_name, "Auto connect to an account on startup", NULL },
|
||||
{ "log", 'l', 0, G_OPTION_ARG_STRING, &log, "Set logging levels, DEBUG, INFO, WARN (default), ERROR", "LEVEL" },
|
||||
{ "config", 'c', 0, G_OPTION_ARG_STRING, &config_file, "Use an alternative configuration file", NULL },
|
||||
{ "logfile", 'f', 0, G_OPTION_ARG_STRING, &log_file, "Specify log file", NULL },
|
||||
@@ -93,9 +67,9 @@ main(int argc, char** argv)
|
||||
context = g_option_context_new(NULL);
|
||||
g_option_context_add_main_entries(context, entries, NULL);
|
||||
if (!g_option_context_parse(context, &argc, &argv, &error)) {
|
||||
g_print("%s\n", error->message);
|
||||
g_print("%s\n", PROF_GERROR_MESSAGE(error));
|
||||
g_option_context_free(context);
|
||||
g_error_free(error);
|
||||
PROF_GERROR_FREE(error);
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -107,7 +81,7 @@ main(int argc, char** argv)
|
||||
|
||||
// lets use fixed email instead of PACKAGE_BUGREPORT
|
||||
g_print("Copyright (C) 2012 - 2019 James Booth <boothj5web@gmail.com>.\n");
|
||||
g_print("Copyright (C) 2019 - 2025 Michael Vetter <jubalh@iodoru.org>.\n");
|
||||
g_print("Copyright (C) 2019 - 2026 Michael Vetter <jubalh@iodoru.org>.\n");
|
||||
g_print("License GPLv3+: GNU GPL version 3 or later <https://www.gnu.org/licenses/gpl.html>\n");
|
||||
g_print("\n");
|
||||
g_print("This is free software; you are free to change and redistribute it.\n");
|
||||
|
||||
@@ -4,39 +4,18 @@
|
||||
*
|
||||
* Copyright (C) 2019 Paul Fariello <paul@fariello.eu>
|
||||
*
|
||||
* This file is part of Profanity.
|
||||
*
|
||||
* Profanity is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Profanity is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Profanity. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* In addition, as a special exception, the copyright holders give permission to
|
||||
* link the code of portions of this program with the OpenSSL library under
|
||||
* certain conditions as described in each individual source file, and
|
||||
* distribute linked combinations including the two.
|
||||
*
|
||||
* You must obey the GNU General Public License in all respects for all of the
|
||||
* code used other than OpenSSL. If you modify file(s) with this exception, you
|
||||
* may extend this exception to your version of the file(s), but you are not
|
||||
* obligated to do so. If you do not wish to do so, delete this exception
|
||||
* statement from your version. If you delete this exception statement from all
|
||||
* source files in the program, then also delete it here.
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later WITH OpenSSL-exception
|
||||
*/
|
||||
#include "config.h"
|
||||
|
||||
#include <assert.h>
|
||||
#ifdef HAVE_LIBOMEMO_C
|
||||
#include <omemo/signal_protocol.h>
|
||||
#include <omemo/signal_protocol_types.h>
|
||||
#else
|
||||
#include <signal/signal_protocol.h>
|
||||
#include <signal/signal_protocol_types.h>
|
||||
#endif
|
||||
|
||||
#include "log.h"
|
||||
#include "omemo/omemo.h"
|
||||
@@ -254,7 +233,7 @@ omemo_decrypt_func(signal_buffer** output, int cipher, const uint8_t* key, size_
|
||||
{
|
||||
int ret = SG_SUCCESS;
|
||||
gcry_cipher_hd_t hd;
|
||||
unsigned char* plaintext;
|
||||
unsigned char* plaintext = NULL;
|
||||
size_t plaintext_len;
|
||||
int mode;
|
||||
int algo;
|
||||
@@ -289,6 +268,13 @@ omemo_decrypt_func(signal_buffer** output, int cipher, const uint8_t* key, size_
|
||||
}
|
||||
|
||||
plaintext_len = ciphertext_len;
|
||||
// PKCS#5/PKCS#7 padding requires at least one byte of plaintext;
|
||||
// reject malformed empty ciphertext before any plaintext[len - 1]
|
||||
// access could underflow / read out-of-bounds.
|
||||
if (plaintext_len == 0) {
|
||||
ret = SG_ERR_INVAL;
|
||||
goto out;
|
||||
}
|
||||
plaintext = malloc(plaintext_len);
|
||||
gcry_cipher_decrypt(hd, plaintext, plaintext_len, ciphertext, ciphertext_len);
|
||||
|
||||
@@ -300,6 +286,12 @@ omemo_decrypt_func(signal_buffer** output, int cipher, const uint8_t* key, size_
|
||||
assert(FALSE);
|
||||
}
|
||||
|
||||
// padding byte must address bytes that exist in plaintext.
|
||||
if (padding == 0 || (size_t)padding > plaintext_len) {
|
||||
ret = SG_ERR_UNKNOWN;
|
||||
goto out;
|
||||
}
|
||||
|
||||
for (int i = 0; i < padding; i++) {
|
||||
if (plaintext[plaintext_len - 1 - i] != padding) {
|
||||
ret = SG_ERR_UNKNOWN;
|
||||
@@ -389,7 +381,7 @@ out:
|
||||
|
||||
gcry_error_t
|
||||
aes256gcm_crypt_file(FILE* in, FILE* out, off_t file_size,
|
||||
unsigned char key[], unsigned char nonce[], bool encrypt)
|
||||
unsigned char key[], unsigned char nonce[], gboolean encrypt)
|
||||
{
|
||||
|
||||
if (!gcry_control(GCRYCTL_INITIALIZATION_FINISHED_P)) {
|
||||
@@ -422,7 +414,7 @@ aes256gcm_crypt_file(FILE* in, FILE* out, off_t file_size,
|
||||
|
||||
unsigned char buffer[AES256_GCM_BUFFER_SIZE];
|
||||
|
||||
int bytes = 0;
|
||||
size_t bytes = 0;
|
||||
off_t bytes_read = 0, bytes_available = 0, read_size = 0;
|
||||
while (bytes_read < file_size) {
|
||||
bytes_available = file_size - bytes_read;
|
||||
@@ -437,8 +429,8 @@ aes256gcm_crypt_file(FILE* in, FILE* out, off_t file_size,
|
||||
read_size = AES256_GCM_BUFFER_SIZE;
|
||||
}
|
||||
|
||||
bytes = fread(buffer, 1, read_size, in);
|
||||
bytes_read += bytes;
|
||||
bytes = fread(buffer, 1, (size_t)read_size, in);
|
||||
bytes_read += (off_t)bytes;
|
||||
|
||||
if (encrypt) {
|
||||
res = gcry_cipher_encrypt(hd, buffer, bytes, NULL, 0);
|
||||
|
||||
@@ -4,37 +4,18 @@
|
||||
*
|
||||
* Copyright (C) 2019 Paul Fariello <paul@fariello.eu>
|
||||
*
|
||||
* This file is part of Profanity.
|
||||
*
|
||||
* Profanity is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Profanity is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Profanity. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* In addition, as a special exception, the copyright holders give permission to
|
||||
* link the code of portions of this program with the OpenSSL library under
|
||||
* certain conditions as described in each individual source file, and
|
||||
* distribute linked combinations including the two.
|
||||
*
|
||||
* You must obey the GNU General Public License in all respects for all of the
|
||||
* code used other than OpenSSL. If you modify file(s) with this exception, you
|
||||
* may extend this exception to your version of the file(s), but you are not
|
||||
* obligated to do so. If you do not wish to do so, delete this exception
|
||||
* statement from your version. If you delete this exception statement from all
|
||||
* source files in the program, then also delete it here.
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later WITH OpenSSL-exception
|
||||
*/
|
||||
#include <stdio.h>
|
||||
#include <stdbool.h>
|
||||
#include "config.h"
|
||||
|
||||
#ifdef HAVE_LIBOMEMO_C
|
||||
#include <omemo/signal_protocol_types.h>
|
||||
#else
|
||||
#include <signal/signal_protocol_types.h>
|
||||
#endif
|
||||
|
||||
#include <gcrypt.h>
|
||||
|
||||
#define AES128_GCM_KEY_LENGTH 16
|
||||
@@ -185,7 +166,7 @@ int aes128gcm_decrypt(unsigned char* plaintext,
|
||||
const unsigned char* const key, const unsigned char* const tag);
|
||||
|
||||
gcry_error_t aes256gcm_crypt_file(FILE* in, FILE* out, off_t file_size,
|
||||
unsigned char key[], unsigned char nonce[], bool encrypt);
|
||||
unsigned char key[], unsigned char nonce[], gboolean encrypt);
|
||||
|
||||
char* aes256gcm_create_secure_fragment(unsigned char* key,
|
||||
unsigned char* nonce);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -4,33 +4,7 @@
|
||||
*
|
||||
* Copyright (C) 2019 Paul Fariello <paul@fariello.eu>
|
||||
*
|
||||
* This file is part of Profanity.
|
||||
*
|
||||
* Profanity is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Profanity is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Profanity. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* In addition, as a special exception, the copyright holders give permission to
|
||||
* link the code of portions of this program with the OpenSSL library under
|
||||
* certain conditions as described in each individual source file, and
|
||||
* distribute linked combinations including the two.
|
||||
*
|
||||
* You must obey the GNU General Public License in all respects for all of the
|
||||
* code used other than OpenSSL. If you modify file(s) with this exception, you
|
||||
* may extend this exception to your version of the file(s), but you are not
|
||||
* obligated to do so. If you do not wish to do so, delete this exception
|
||||
* statement from your version. If you delete this exception statement from all
|
||||
* source files in the program, then also delete it here.
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later WITH OpenSSL-exception
|
||||
*/
|
||||
#include <glib.h>
|
||||
#include <gcrypt.h>
|
||||
@@ -85,6 +59,9 @@ void omemo_trust(const char* const jid, const char* const fingerprint);
|
||||
void omemo_untrust(const char* const jid, const char* const fingerprint);
|
||||
GList* omemo_known_device_identities(const char* const jid);
|
||||
gboolean omemo_is_trusted_identity(const char* const jid, const char* const fingerprint);
|
||||
gboolean omemo_is_jid_trusted(const char* const jid);
|
||||
GList* omemo_get_jid_untrusted_fingerprints(const char* const jid);
|
||||
gboolean omemo_is_device_active(const char* const jid, const char* const fingerprint);
|
||||
char* omemo_fingerprint_autocomplete(const char* const search_str, gboolean previous, void* context);
|
||||
void omemo_fingerprint_autocomplete_reset(void);
|
||||
gboolean omemo_automatic_start(const char* const recipient);
|
||||
@@ -95,8 +72,9 @@ void omemo_start_muc_sessions(const char* const roomjid);
|
||||
void omemo_start_device_session(const char* const jid, uint32_t device_id, GList* prekeys, uint32_t signed_prekey_id, const unsigned char* const signed_prekey, size_t signed_prekey_len, const unsigned char* const signature, size_t signature_len, const unsigned char* const identity_key, size_t identity_key_len);
|
||||
|
||||
gboolean omemo_loaded(void);
|
||||
|
||||
char* omemo_on_message_send(ProfWin* win, const char* const message, gboolean request_receipt, gboolean muc, const char* const replace_id);
|
||||
char* omemo_on_message_recv(const char* const from, uint32_t sid, const unsigned char* const iv, size_t iv_len, GList* keys, const unsigned char* const payload, size_t payload_len, gboolean muc, gboolean* trusted);
|
||||
char* omemo_on_message_recv(const char* const from, uint32_t sid, const unsigned char* const iv, size_t iv_len, GList* keys, const unsigned char* const payload, size_t payload_len, gboolean muc, gboolean* trusted, omemo_error_t* error) __attribute__((nonnull(9, 10)));
|
||||
|
||||
char* omemo_encrypt_file(FILE* in, FILE* out, off_t file_size, int* gcry_res);
|
||||
gcry_error_t omemo_decrypt_file(FILE* in, FILE* out, off_t file_size, const char* fragment);
|
||||
|
||||
@@ -4,38 +4,17 @@
|
||||
*
|
||||
* Copyright (C) 2019 Paul Fariello <paul@fariello.eu>
|
||||
*
|
||||
* This file is part of Profanity.
|
||||
*
|
||||
* Profanity is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Profanity is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Profanity. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* In addition, as a special exception, the copyright holders give permission to
|
||||
* link the code of portions of this program with the OpenSSL library under
|
||||
* certain conditions as described in each individual source file, and
|
||||
* distribute linked combinations including the two.
|
||||
*
|
||||
* You must obey the GNU General Public License in all respects for all of the
|
||||
* code used other than OpenSSL. If you modify file(s) with this exception, you
|
||||
* may extend this exception to your version of the file(s), but you are not
|
||||
* obligated to do so. If you do not wish to do so, delete this exception
|
||||
* statement from your version. If you delete this exception statement from all
|
||||
* source files in the program, then also delete it here.
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later WITH OpenSSL-exception
|
||||
*/
|
||||
#include <glib.h>
|
||||
#include <signal/signal_protocol.h>
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#ifdef HAVE_LIBOMEMO_C
|
||||
#include <omemo/signal_protocol.h>
|
||||
#else
|
||||
#include <signal/signal_protocol.h>
|
||||
#endif
|
||||
|
||||
#include "log.h"
|
||||
#include "omemo/omemo.h"
|
||||
#include "omemo/store.h"
|
||||
|
||||
@@ -4,38 +4,16 @@
|
||||
*
|
||||
* Copyright (C) 2019 Paul Fariello <paul@fariello.eu>
|
||||
*
|
||||
* This file is part of Profanity.
|
||||
*
|
||||
* Profanity is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Profanity is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Profanity. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* In addition, as a special exception, the copyright holders give permission to
|
||||
* link the code of portions of this program with the OpenSSL library under
|
||||
* certain conditions as described in each individual source file, and
|
||||
* distribute linked combinations including the two.
|
||||
*
|
||||
* You must obey the GNU General Public License in all respects for all of the
|
||||
* code used other than OpenSSL. If you modify file(s) with this exception, you
|
||||
* may extend this exception to your version of the file(s), but you are not
|
||||
* obligated to do so. If you do not wish to do so, delete this exception
|
||||
* statement from your version. If you delete this exception statement from all
|
||||
* source files in the program, then also delete it here.
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later WITH OpenSSL-exception
|
||||
*/
|
||||
#include <signal/signal_protocol.h>
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#ifdef HAVE_LIBOMEMO_C
|
||||
#include <omemo/signal_protocol.h>
|
||||
#else
|
||||
#include <signal/signal_protocol.h>
|
||||
#endif
|
||||
|
||||
#define OMEMO_STORE_GROUP_IDENTITY "identity"
|
||||
#define OMEMO_STORE_GROUP_PREKEYS "prekeys"
|
||||
#define OMEMO_STORE_GROUP_SIGNED_PREKEYS "signed_prekeys"
|
||||
@@ -50,7 +28,7 @@ typedef struct
|
||||
signal_buffer* private;
|
||||
uint32_t registration_id;
|
||||
GHashTable* trusted;
|
||||
bool recv;
|
||||
gboolean recv;
|
||||
} identity_key_store_t;
|
||||
|
||||
GHashTable* session_store_new(void);
|
||||
|
||||
@@ -4,33 +4,7 @@
|
||||
*
|
||||
* Copyright (C) 2012 - 2019 James Booth <boothj5@gmail.com>
|
||||
*
|
||||
* This file is part of Profanity.
|
||||
*
|
||||
* Profanity is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Profanity is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Profanity. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* In addition, as a special exception, the copyright holders give permission to
|
||||
* link the code of portions of this program with the OpenSSL library under
|
||||
* certain conditions as described in each individual source file, and
|
||||
* distribute linked combinations including the two.
|
||||
*
|
||||
* You must obey the GNU General Public License in all respects for all of the
|
||||
* code used other than OpenSSL. If you modify file(s) with this exception, you
|
||||
* may extend this exception to your version of the file(s), but you are not
|
||||
* obligated to do so. If you do not wish to do so, delete this exception
|
||||
* statement from your version. If you delete this exception statement from all
|
||||
* source files in the program, then also delete it here.
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later WITH OpenSSL-exception
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
@@ -296,14 +270,16 @@ otr_on_message_recv(const char* const barejid, const char* const resource, const
|
||||
// check for OTR whitespace (opportunistic or always)
|
||||
if (policy == PROF_OTRPOLICY_OPPORTUNISTIC || policy == PROF_OTRPOLICY_ALWAYS) {
|
||||
if (whitespace_base) {
|
||||
if (strstr(message, OTRL_MESSAGE_TAG_V2) || strstr(message, OTRL_MESSAGE_TAG_V1)) {
|
||||
char* tag_position = whitespace_base + strlen(OTRL_MESSAGE_TAG_BASE);
|
||||
if (strncmp(tag_position, OTRL_MESSAGE_TAG_V2, strlen(OTRL_MESSAGE_TAG_V2)) == 0 || strncmp(tag_position, OTRL_MESSAGE_TAG_V1, strlen(OTRL_MESSAGE_TAG_V1)) == 0) {
|
||||
// Remove whitespace pattern for proper display in UI
|
||||
// Handle both BASE+TAGV1/2(16+8) and BASE+TAGV1+TAGV2(16+8+8)
|
||||
int tag_length = 24;
|
||||
if (strstr(message, OTRL_MESSAGE_TAG_V2) && strstr(message, OTRL_MESSAGE_TAG_V1)) {
|
||||
tag_length = 32;
|
||||
}
|
||||
memmove(whitespace_base, whitespace_base + tag_length, tag_length);
|
||||
// Move the rest of the message (with NUL) over the tag.
|
||||
memmove(whitespace_base, whitespace_base + tag_length, strlen(whitespace_base + tag_length) + 1);
|
||||
char* otr_query_message = otr_start_query();
|
||||
cons_show("OTR Whitespace pattern detected. Attempting to start OTR session…");
|
||||
free(message_send_chat_otr(barejid, otr_query_message, FALSE, NULL));
|
||||
@@ -661,11 +637,9 @@ otr_get_policy(const char* const recipient)
|
||||
prof_otrpolicy_t result = PROF_OTRPOLICY_MANUAL;
|
||||
if (g_strcmp0(account->otr_policy, "manual") == 0) {
|
||||
result = PROF_OTRPOLICY_MANUAL;
|
||||
}
|
||||
if (g_strcmp0(account->otr_policy, "opportunistic") == 0) {
|
||||
} else if (g_strcmp0(account->otr_policy, "opportunistic") == 0) {
|
||||
result = PROF_OTRPOLICY_OPPORTUNISTIC;
|
||||
}
|
||||
if (g_strcmp0(account->otr_policy, "always") == 0) {
|
||||
} else if (g_strcmp0(account->otr_policy, "always") == 0) {
|
||||
result = PROF_OTRPOLICY_ALWAYS;
|
||||
}
|
||||
account_free(account);
|
||||
|
||||
@@ -4,33 +4,7 @@
|
||||
*
|
||||
* Copyright (C) 2012 - 2019 James Booth <boothj5@gmail.com>
|
||||
*
|
||||
* This file is part of Profanity.
|
||||
*
|
||||
* Profanity is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Profanity is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Profanity. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* In addition, as a special exception, the copyright holders give permission to
|
||||
* link the code of portions of this program with the OpenSSL library under
|
||||
* certain conditions as described in each individual source file, and
|
||||
* distribute linked combinations including the two.
|
||||
*
|
||||
* You must obey the GNU General Public License in all respects for all of the
|
||||
* code used other than OpenSSL. If you modify file(s) with this exception, you
|
||||
* may extend this exception to your version of the file(s), but you are not
|
||||
* obligated to do so. If you do not wish to do so, delete this exception
|
||||
* statement from your version. If you delete this exception statement from all
|
||||
* source files in the program, then also delete it here.
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later WITH OpenSSL-exception
|
||||
*/
|
||||
|
||||
#ifndef OTR_OTR_H
|
||||
|
||||
@@ -4,33 +4,7 @@
|
||||
*
|
||||
* Copyright (C) 2012 - 2019 James Booth <boothj5@gmail.com>
|
||||
*
|
||||
* This file is part of Profanity.
|
||||
*
|
||||
* Profanity is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Profanity is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Profanity. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* In addition, as a special exception, the copyright holders give permission to
|
||||
* link the code of portions of this program with the OpenSSL library under
|
||||
* certain conditions as described in each individual source file, and
|
||||
* distribute linked combinations including the two.
|
||||
*
|
||||
* You must obey the GNU General Public License in all respects for all of the
|
||||
* code used other than OpenSSL. If you modify file(s) with this exception, you
|
||||
* may extend this exception to your version of the file(s), but you are not
|
||||
* obligated to do so. If you do not wish to do so, delete this exception
|
||||
* statement from your version. If you delete this exception statement from all
|
||||
* source files in the program, then also delete it here.
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later WITH OpenSSL-exception
|
||||
*/
|
||||
|
||||
#ifndef OTR_OTRLIB_H
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user