Commit Graph

1701 Commits

Author SHA1 Message Date
f9e0ba9630 fix(ai): fix memory leaks in ai session handling
Add missing `ai_session_unref` calls to prevent memory leaks.

- In `_ai_request_thread`, release session on error paths and
  after successful processing.
- In `cmd_ai_start`, release reference after passing ownership
  to the AI window.
2026-05-15 11:58:25 +00:00
5e329c77e1 fix(ai): fix memory leak in AI message stanza ID
Store stanza ID in an auto_gchar variable to ensure automatic memory cleanup.
2026-05-15 08:55:56 +00:00
9e5dfb14f8 feat(ai): add AI client with multi-provider chat support
Add an AI client module that integrates with OpenAI-compatible API
providers (OpenAI, Perplexity, and custom endpoints) to provide
AI-assisted chat within CProof. Users can start sessions with /ai start,
send prompts, receive responses in a dedicated AI window, switch between
providers and models, and manage API keys — all with tab-completion.

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

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

Architecture:

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

Tests include 470+ lines of unit tests covering provider management,
session lifecycle, JSON parsing (multiple formats), autocomplete cycling,
and error handling, plus functional tests for /ai command dispatch.
A stub_ai.c module isolates unit tests from UI dependencies.
2026-05-15 02:21:54 +00:00
3f36c303c2 feat(history): flat-file backend with bidirectional SQLite migration
A flat-file alternative to the SQLite chatlog backend with runtime
switching, full migration tooling, integrity verification, and a
synthetic load harness. SQLite remains the default; both backends share
one dispatch layer (db_backend_t vtable) so callers don't change.

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

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

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

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

Author: jabber.developer2 <jabber.developer2@jabber.space>
Reviewed-by: jabber.developer <jabber.developer@jabber.space>
2026-05-05 19:26:07 +00:00
9ec01fa8cc fix: CWE-134 format string audit and compiler hardening
Security:
Fix CWE-134 in iq.c: user-controlled string passed as format argument
Add G_GNUC_PRINTF annotations to all variadic printf-like wrappers
in ui.h, log.h and http_common.h
Compiler flags (configure.ac):

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

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

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

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

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

Author: jabber.developer2 <jabber.developer2@jabber.space>
2026-03-07 11:55:50 +01:00
467222d0ca fix(ui,db): harden NULL handling, fix CWE-134, optimize iterations
security(CWE-134): fix format string injections + add CI check
fix(ui): subwindow lifecycle, newwin/newpad guards, fallback timestamps
fix(db): sqlite cleanup on failures, sqlite3_close_v2
fix(xmpp): queued_messages loop, barejid leak
perf(core): g_hash_table_iter_init instead of g_hash_table_get_keys
refactor(ui): CLAMP macro in _check_subwin_width
test: XEP-0012 and XEP-0045 functional tests

Author: jabber.developer2
Closes #58, #85
2026-02-06 19:27:40 +01:00
88b48000f8 fix(cmd): increase /connect max args from 7 to 9
The /connect command can take: account + server <s> + port <p> + tls <t> + auth <a>
which totals 9 arguments, not 7. This fixes argument parsing for full command usage.
2026-01-06 17:22:53 +03:00
20af44196b fix(cmd): cmd_time: Avoid extra loop iterations for single option 2025-11-27 11:38:49 +01:00
ad95edb2b9 feat: make /log level work with non-default files 2025-10-13 20:47:34 +02:00
Steffen Jaeckel
d3fa1c2f78 Tidy up some code
* less allocations
* less duplicate code

Signed-off-by: Steffen Jaeckel <s@jaeckel.eu>

(cherry picked from commit 9d335729a0)
2025-09-10 14:07:41 +02:00
Steffen Jaeckel
39aab45162 Fix memory leak.
Signed-off-by: Steffen Jaeckel <s@jaeckel.eu>

(cherry picked from commit e0f107f75e)
2025-09-10 14:01:22 +02:00
Martin Dosch
6b6576b31c Fix typo in examples.
(cherry picked from commit 5696bf77c7)

Author:    Martin Dosch <martin@mdosch.de>
2025-09-10 13:58:57 +02:00
65cf9ddb9c fix(cmd_sub): Prevent crash with malformed JID in /sub command
Added null check for jidp in cmd_sub to handle jid_create returning NULL.

Crash occurred when processing malformed JID inputs like @example.com.

Ensures robust handling of invalid JIDs.

Fixes #22
2025-09-01 23:29:26 +02:00
51d2355d97 fix(cmd_caps): Prevent crash in /caps with malformed JID input
- Added null check for `jid` in `cmd_caps` to handle `jid_create` returning NULL.
- Crash occurred when processing malformed inputs like `@example.com` in `/caps`.
- Ensures robust handling of invalid JID strings in `WIN_CHAT` and `WIN_CONSOLE` cases.

Fixes #20
2025-09-01 21:28:19 +02:00
f4405d114a Add unit tests for forced encryption check function 2025-08-27 16:48:27 +02:00
442bfb6ce1 feat: Add /force-encryption command and update message handling
Implement /force-encryption command to configure forced encryption
settings with on|off and policy resend-to-confirm|block options.

The changes enhance user experience by clarifying encryption requirements
and providing actionable commands. Users can now press Enter again to
confirm unencrypted messages in resend-to-confirm mode.
2025-08-25 21:11:27 +02:00
ad294cdd6d feat(editor): add asynchronous external editor support
The synchronous `get_message_from_editor` blocked the main loop (`prof_run`) while launching an external editor like `vim`, halting network I/O in `session_process_events` and preventing incoming message reception.

Introduced `get_message_from_editor_async` for `cmd_editor` to run the editor asynchronously. It forks and execs the editor in a thread (`editor_thread`), suspending NCurses to free the terminal. The main loop skips `inp_readline` and `ui_update` via a new `background_mode` flag while the editor runs, allowing `session_process_events` to keep the connection alive.

On editor completion, `editor_process` (called per loop iteration) resumes NCurses with `ui_resize`, inserts the result into the readline buffer and clears `background_mode`.

Retained synchronous `get_message_from_editor` for ~20 existing code paths (e.g., `vcard_nickname`) to avoid breaking them.

Tested with `vim` and `nano`: confirms no rendering conflicts, messages received during editing, and seamless resume. Edge cases like editor crashes handled via error logging and seamless resume.
2025-08-07 03:03:43 +02:00
5ed8ec30f0 Update name and license 2025-07-19 14:32:26 +02:00
Michael Vetter
091ff41c06 Merge pull request #2041 from profanity-im/some-improvements
Summary:
- Print the final storage location when successfully decrypting a file with `aesgcm://` source.
- Less GString
- Re-factor cmd_presence()
- Improve const correctness
- Use correct free function. Fixes: 75d7663 ("Free wins summary list")

Pull Request Source: https://github.com/profanity-im/profanity/pull/2041

Author: Steffen Jaeckel <s@jaeckel.eu>
2025-07-19 00:13:49 +02:00
Michael Vetter
5385cc28be Add note about xep0454 to /sendfile
Fix https://github.com/profanity-im/profanity/issues/2040
2025-04-22 15:07:33 +02:00
Steffen Jaeckel
3bf72210e1 Add iso8601 when setting a time format
Instead of manually typing the format-string, one can now simply use
`iso8601`.

Signed-off-by: Steffen Jaeckel <s@jaeckel.eu>
2025-04-02 13:07:47 +02:00
Steffen Jaeckel
9c2b3f6579 Re-factor cmd_time()
Instead of c&p'ing the same code again and again, put everything in a
struct and loop over it.

This also fixes `vcard` not being in included in `all`.

Fixes: f934c5b59f ("Add vCard support")

Signed-off-by: Steffen Jaeckel <s@jaeckel.eu>
2025-04-02 13:07:47 +02:00
Michael Vetter
07dfeec816 Release 0.15.0 2025-03-27 20:06:38 +01:00
Steffen Jaeckel
422f5fa6f9 Slightly improve http_download related allocations.
* use `calloc()` to allocate zero-initialized structs.
* order the allocation of members by their declaration.
* order the freeing of members in reverse order.
* move init of `silent` flag in plugin case.
* fix double free of `tmpname` in `aesgcm_file_get()`.
* make some functions static.

Signed-off-by: Steffen Jaeckel <s@jaeckel.eu>
2025-03-18 17:00:58 +01:00
Michael Vetter
5b9516b483 Merge pull request #2022 from profanity-im/fixes
Memory fixes
2025-03-13 09:58:09 +01:00
Steffen Jaeckel
c0da36c48d Rage-cleanup.
While trying to get the unit tests working again I stumbled over all those
things that I thought could be better^TM.

Now we also know "TODO: why does this make the test fail?" - because
the unit tests are brittle AF ... and we have to init the subsystems
we use in the test, otherwise the cleanup will fail...

BTW. you can now also only run a single test ... or a pattern or so ...
you'd have to read how `cmocka_set_test_filter()` works exactly.

Signed-off-by: Steffen Jaeckel <s@jaeckel.eu>
2025-03-10 11:45:15 +01:00
Steffen Jaeckel
b55bf6e4bd Fix some more memory leaks + an invalid read.
`ProfMessage` was not completely initialized, fix this by using `calloc()`
for the allocation.

Signed-off-by: Steffen Jaeckel <s@jaeckel.eu>
2025-03-10 11:45:15 +01:00
Steffen Jaeckel
c5a131ee46 Introduce our own shutdown callback mechanism.
Instead of adding stuff to `_shutdown()`, we can now register a shutdown
routine from the respective `init()` function of our modules.

This also has the advantage, that we're sure they're called in reverse
order from how the initialization happened.

I didn't simply use `atexit()` because POSIX says that the number of
possible callbacks is limited (min 32) and I was not sure whether
we will maybe extend this number at one point.

Signed-off-by: Steffen Jaeckel <s@jaeckel.eu>
2025-03-07 21:09:16 +01:00
Michael Vetter
55a8a26a24 Attempt to open file later to prevent potential resource leak 2025-03-06 11:19:00 +01:00
Steffen Jaeckel
8151dfa3b4 /quit now exits from the event loop
Before this change issuing `/quit` directly called `exit(0)` and did not
invoke all the graceful shutdown routines. Now we first try to exit from
the event loop, which includes cleaning up everything.
In case the event loop is stuck for some reason, you could try to issue a
second `/quit`, which will then directly call `exit(0)`.

Signed-off-by: Steffen Jaeckel <s@jaeckel.eu>
2025-01-28 16:43:13 +01:00
Steffen Jaeckel
2696989ce5 Fix a bunch of obvious memory leaks at termination
Signed-off-by: Steffen Jaeckel <s@jaeckel.eu>
2025-01-28 16:43:13 +01:00
Steffen Jaeckel
80193ca41f cmd_ac: Refactor alloc, reset & free
Add all elements to an array, in order to easily be able to run operations
in bulk.

Signed-off-by: Steffen Jaeckel <s@jaeckel.eu>
2025-01-28 16:43:13 +01:00
Hanspeter Portner
cb395b7c09 Make muc_nick return a const char* const
In the documentation of the `muc_nick` function we can read:

> The nickname is owned by the chat room and should not be modified or freed

We should reflect that in the return type and `strdup` the value for the
plugin API.

Fixes: https://github.com/profanity-im/profanity/issues/2013
2025-01-26 18:16:14 +01:00
Michael Vetter
69301503ce Add missing space to description 2024-09-19 08:34:21 +02:00
Michael Vetter
996a1fdf35 Fix build on some compilers regardind switch statement
Fix the famous `error: a label can only be part of a statement and a
declaration is not a statement`.
2024-06-20 18:04:12 +02:00
Michael Vetter
f147a5ca27 Change default download location
`/url save $someurl` will now download to
`~/.local/share/profanity/downloads/from_jid/date/filename` instead of
`~/.local/share/profanity/downloads`.

Like this the downloaded files should be better ordered.

This will only happen for MUC and 1:1 chat windows.
Private windows might have only a nick or jid. Lets not distinguish
between those two ways. And since the nick option is unreliable to be
the same person lets just put them in the general downloads folder.
As is done with downloads from all other windows as well.

I also had the idea to make this configurable but this suits my needs
and time limits right now.
2024-06-20 13:35:14 +02:00
Michael Vetter
5765edbe1b Reset autoconnect account when the account gets removed
When a user added an account, set it as autoconnect and then removed
that account. It still was set as the autoconnect account.

```
    /account add test
    /autoconnect set test
    /account remove test
    /save
    /quit
    Start profanity
```

Fix https://github.com/profanity-im/profanity/issues/1976
2024-06-20 11:04:20 +02:00
Michael Vetter
87935b744d Merge pull request #1979 from profanity-im/fix/leaks
Fix memleaks
2024-06-20 10:51:12 +02:00
Steffen Jaeckel
848acdd1fa Improve const correctness
Signed-off-by: Steffen Jaeckel <jaeckel-floss@eyet-services.de>
2024-06-19 16:01:46 +02:00
Michael Vetter
b04650fb1c Fix memleak in cmd_omemo_gen() 2024-06-19 11:40:23 +02:00
Michael Vetter
8a62d8e88b Fix typo in OMEMO message 2024-06-19 11:37:34 +02:00
Michael Vetter
cfd4207d93 Merge pull request #1973 from profanity-im/fix/omemo-muc-twice
Check whether we are already in an omemo session for MUCs as well
2024-06-14 13:51:56 +02:00
Michael Vetter
1dd65bca6b Check whether we are already in an omemo session for MUCs as well
We do this for regular chatwins already.
2024-06-02 19:07:58 +02:00
Michael Vetter
694680d69e Remove subscription when reporting spam/abuse
When users receive a message and a subscription request from a new
contact, and they use `/blocked report-abse JID` or `/blocked
report-spam JID` they most likely want the subscription request to
vanish as well.

According to mdosch Conversations is behaving like this too.

Fix: https://github.com/profanity-im/profanity/issues/1970
2024-06-02 18:48:39 +02:00
ike08
23fa4750ff Fix unable to download item twice
## Summary

Partial fix for https://github.com/profanity-im/profanity/issues/1939  

> When doing the same in an unencrypted (no e2ee) chat there is no Downloading… message at all but the file is downloaded.  

Download a file twice with `/url save`, the second download will not print download progress to the window.  

The cause is `HTTPDownload`'s `silent` variable is not initialized; so, `silent` points to a second-hand stack memory address with old data. `silent` references data, so the `if` statement will fail in **src/tools/http_download.c:206** and download progress will not print to the window.  

The fix is to initialize `silent` in both encrypted and unencrypted file download scenarios.  

## Testing

Valgrind: Yes  

- `/url save` without OMEMO  
  **SUCCESS**: Try three times with the same URL and download status will display every time.  

- `/url save` with OMEMO  
  **SUCCESS**: Try three times with the same URL and download status will display every time.  

- `/plugins install https://raw.githubusercontent.com/profanity-im/profanity-plugins/master/stable/sounds.py`  
  **SUCCESS**: Try once and download progress is hidden.
2024-02-28 07:23:59 -07:00
Michael Vetter
569e37f018 Update copyright to 2024 2024-01-22 16:03:48 +01:00
Steffen Jaeckel
3c939264b3 Less GString usage
Signed-off-by: Steffen Jaeckel <jaeckel-floss@eyet-services.de>
2023-12-12 18:27:11 +01:00
Steffen Jaeckel
b4c088232e Improve const correctness
Signed-off-by: Steffen Jaeckel <jaeckel-floss@eyet-services.de>
2023-12-12 18:27:09 +01:00
Steffen Jaeckel
bac24601da Introduce equals_our_barejid()
Instead of always repeating the same pattern, introduce a helper function.

Signed-off-by: Steffen Jaeckel <jaeckel-floss@eyet-services.de>
2023-12-12 18:27:02 +01:00
Steffen Jaeckel
847a86de50 add connection_get_jid()
Use a singleton `Jid` inside the connection instead of always re-creating
a `Jid` from the same string.

Signed-off-by: Steffen Jaeckel <jaeckel-floss@eyet-services.de>
2023-12-12 18:26:21 +01:00