All coding agents working in this repository must read and follow this entire file before making any changes.
After any context compaction (conversation summary, truncated history, or handoff to a new agent turn with only a recap): re-read this entire AGENTS.md file before writing code or continuing the task. Compaction drops detail from the thread — the repo rules here are authoritative, not the summary.
For OMEMO troubleshooting tasks, agents may use tools/correlate_omemo_xml.sh --account <account> as a convenience helper to correlate WeeChat log events with raw XML.
This is a WeeChat plugin for XMPP/Jabber written in C++. It uses libstrophe for XMPP protocol handling and LMDB for local persistence (MAM cache, capabilities cache). This is a fork with bug fixes and feature enhancements.
Canonical XEP specs for all implemented XEPs are stored in docs/specs/xep-NNNN.txt (fetched directly from https://xmpp.org/extensions/xep-NNNN.html).
The XEP specifications must be religiously followed at all times to ensure compatibility. This is non-negotiable. It guarantees correct interoperability with other clients (e.g. rich file previews via SFS/ESFS metadata in Conversations and similar, proper element nesting, size/hash reporting for plain vs. encrypted uploads, namespaces, and full protocol flows). Even small deviations "for convenience", legacy fallbacks, or perceived simplicity have caused real-world failures such as missing previews, corrupt metadata, service-unavailable errors in MUC, or incorrect links.
When implementing or modifying support for any XEP:
- Fetch the canonical spec:
curl -s https://xmpp.org/extensions/xep-NNNN.html -o docs/specs/xep-NNNN.txt - Verify stanza structure, namespaces, and protocol behavior against that spec before finalizing code. Cross-check all examples, MUST/RECOMMENDED/SHOULD requirements, and related XEPs (e.g. XEP-0446 file metadata, XEP-0300 hashes).
- Commit the spec file alongside the implementation.
Do not modify the canonical spec files to match the code — the spec is authoritative.
- Primary OMEMO specification:
docs/specs/xep-0384.txt(canonical, fetched from https://xmpp.org/extensions/xep-0384.html) - For OMEMO changes, always verify stanza structure, namespaces, and pubsub node behavior against XEP-0384 before finalizing code.
- Protocol:
eu.siacs.conversations.axolotl(legacy/Gajim-compatible) is the sole supported namespace. OMEMO:2 (urn:xmpp:omemo:2) must not be published, encoded, or treated as a primary path. Any incoming OMEMO:2 stanza may be silently ignored or answered with a key-not-found error. - Trust model: Blind Trust Before Verification (BTBV) TOFU — mirrors Gajim's
get_default_trust(). Trust levels:UNTRUSTED=0,VERIFIED=1,UNDECIDED=2,BLIND=3(stored in LMDB astrust:{jid}:{device_id}). OnlyVERIFIEDandBLINDdevices receive encrypted key material at encode time. - No ATM: XEP-0450 Automatic Trust Management is removed. Do not add it back.
- Reference implementation: Gajim + python-omemo-dr at
/usr/lib/python3/dist-packages/gajim/common/modules/omemo.py,/usr/lib/python3/dist-packages/gajim/common/storage/omemo.py,/usr/lib/python3/dist-packages/omemo_dr/ - Refactor status: Axolotl-only + BTBV refactor is complete on
master. SeeTODO.mdfor remaining open work (not the archived phase plans — those live in git history).
Canonical backend: CMake 3.22+ with Ninja (build/). The root makefile is a thin
GNU make wrapper that maps DEBUG / ASAN / PACKAGE_BUILD to CMake cache vars and
runs cmake --build. On BSD use gmake instead of make. Legacy pre-CMake rules live
under legacy/ for reference only.
- Build command:
make— parallel by default (-j$(nproc)); usemake -j1only when debugging ordering issues. Default toolchain is Clang (CC=clang,CXX=clang++; Homebrew LLVM on macOS). The makefile assigns these explicitly (GNU make predefinesCXX=g++, so?=does not work). - Don't redirect make stdout, this just obfuscates issues.
- Clean command:
make clean(avoid unless necessary; ccache makes rebuilds quick);make distcleanremovesbuild/and forces reconfigure. - Output:
xmpp.soin repo root (WeeChat plugin MODULE target) - Dependencies: Managed via git submodules in
deps/; system libs via pkg-config (seecmake/Dependencies.cmake) - Always build after code changes; doctests run automatically only with
DEBUG=1(vendored indeps/doctest/). Plainmakeskips them — usemake testto run tests without a full debug rebuild. - Includes: use
-Isrcpaths (plugin.hh,xmpp/stanza_view.hh) — never../relative includes insrc/ - ccache: auto-enabled when
ccacheis onPATH(CMAKE_*_COMPILER_LAUNCHER).CXX="ccache clang++"still works; setCCACHE=0to disable. - lld: used automatically when
-fuse-ld=lldworks (XEPHER_USE_LLD=ONby default). Faster links of large plugin objects. - Doctests: link the normal
xmpp.so(no second coverage-instrumented full compile).make coveragebuildsxmpp.cov.so+tests/run.covonly when needed. GIT_COMMIT: compiled only intosrc/version.cppso commits do not bust ccache for every TU. Useweechat::plugin_version()for display strings.- Developer tools:
make toolsbuildstools/dump_mam_dbandtools/dump_omemo_db(skipped whenPACKAGE_BUILD=1).
Build profiles (agents: use DEBUG=1 for iterative development):
| Profile | Command | CMake | When |
|---|---|---|---|
| Dev (default for agents) | make DEBUG=1 |
Debug |
Day-to-day coding, assertions on; runs 147 doctests |
| Optimized | make |
Release |
Pre-release smoke test; skips doctests |
| ASan | make DEBUG=1 ASAN=1 |
Debug + sanitizer |
Memory debugging (Linux: links libasan) |
Combine with ccache: CXX="ccache clang++" make DEBUG=1. Switching between DEBUG=1 and a plain make rebuilds objects — that is expected.
cmake --preset dev # or: release, asan, package
cmake --build --preset dev # xepher_dev: plugin, then doctests
ctest --preset dev # doctests only (after build)
cmake --build build --target tools # LMDB inspector binariesCMakePresets.json mirrors the wrapper profiles. compile_commands.json is symlinked to the repo root after configure (clangd).
PACKAGE_BUILD=1: all packaging scripts and specs pass this tomake. Skips the.sourceELF section and developer tools (XEPHER_BUILD_TOOLS=OFF). Without it, tarball builds lacking.gitcan archive the entire build tree and produce gigabyte RPM/APK packages..sourceembed (Linux only, opt-in): setEMBED_SOURCE=1or-DXEPHER_EMBED_SOURCE=ON(also used bymake release). Default off so iterative links skip the full-tree tar/objcopy. Skipped whenPACKAGE_BUILD=1.make cleanremovesxmpp.so,build/artifacts, and test/tool outputs so container builds never reuse a host-built plugin.
Primary path — GitHub Actions (.github/workflows/packages.yml):
- On
v*tag push: builds all five distros sequentially in fresh Docker containers, uploads artifacts, and attaches them to the GitHub Release. workflow_dispatch: pick version, optional distros (allor e.g.alpine/debian alpine), and attach_to_release (default on) so a failed distro can be rebuilt without redoing the other four.- CI entry point:
packaging/github-build.sh <version> [--debian|--fedora|--arch|--alpine|--void](same script for local Docker verification).
Local package verification (requires Docker):
bash packaging/github-build.sh X.Y.Z # all distros
bash packaging/github-build.sh X.Y.Z --fedora # single distroCI single-distro retry (after a partial Packages failure):
gh workflow run packages.yml -f version=X.Y.Z -f distros=alpine -f attach_to_release=true
# or UI: Actions → Packages → Run workflowOptional — persistent distrobox containers (packaging/distrobox-build.sh):
- Reuses containers; stamps installed deps under
/opt/xepher-build/. Uses the samebuild-*-inside.shscripts as CI. - Alpine and Void invoke
docker rundirectly inside the script.
Shared helpers (packaging/scripts/):
prepare-source-tree.sh— copy/projectto a writable dir,make clean, setsafe.directoryfor any submodule steps.build-{deb,rpm,arch,alpine,void}-inside.sh— per-distro logic; all passPACKAGE_BUILD=1.docker-arch-wrapper.sh— Archmakepkgruns as non-rootbuilder; chowns/outputfor artifact copy.
Output lands in packaging/build/ (.deb, .rpm, .pkg.tar.zst, .apk, .xbps).
- Minimize changes - make surgical, targeted fixes
- Use existing code style and patterns consistently
- Write modern C++23 by default (project standard via
-std=c++23) in every new or touched.cpp/.hh/.inl— apply the Modernization Patterns below as you implement, not in a later sweep. Do not land raw loops,const std::string&read-only params, manualfind != npos, or string+concatenation in new code whenranges/string_view/fmt/expected/spanfit. - Use
nullptrnotNULL - RAII for resource management
- Keep functions focused and concise
- Functions, variables, and most types:
snake_case(e.g.mam_cache_put_message,send_bookmarks,chat_type). - Private / implementation member variables: trailing underscore (e.g.
filter_,buf_,on_select_). - Namespaces:
weechat::,weechat::xmpp::,weechat::ui::,stanza::xep0384::, etc. - Primary rule: When adding or editing code, open a nearby file in the same directory/subsystem (e.g.
src/connection/*.cpporsrc/command/*.inl) and match the dominant local style and indentation exactly. Avoid global style changes or renames.
std::unique_ptr/std::shared_ptr: Already used extensively - prefer over raw pointersstd::optional: Already used - better than null pointers for optional valuesstd::string_view: Already used - safer thanconst char*for read-only stringsstd::span: Consider using for array views instead of pointer+size pairsstd::expected: Use for error handling instead of exceptions (C++23)- Range-based algorithms:
std::ranges::for safer iteration std::make_unique/std::make_shared: Always prefer overnew- Move semantics: Use
std::moveto avoid unnecessary copies - Structured bindings:
auto [key, value] = map.find(...)for cleaner code
Modernization Patterns (non-negotiable for all new and modified code — not optional follow-up work. Established via surgical updates; agents must use these in new code, refactors, and list/string/error handling in .cpp/.inl files. Follow "surgical/minimal" rule — only apply where it simplifies without touching C ABI boundaries like LMDB cursors or WeeChat hook signatures. Use StanzaView for inbound stanza traversal (not raw libstrophe child iteration). Match local style exactly by opening a nearby file first. Always #include <ranges> / <expected> / <span> / <algorithm> (for std::ranges::) in the thin .cpp wrapper before #includeing the .inl. Use fmt::format for string assembly; -Isrc includes (plugin.hh, xmpp/foo.hh) — never ../ relative paths.)
- std::string_view for read-only params: Replace
const std::string& s(andconst char*) withstd::string_view s. Onlystd::string(...)cast when storing or returning ownership. - std::span for byte buffers: Use for owned data passed to C APIs, e.g.:
Create locals from
[[nodiscard]] auto base64_encode_raw(std::span<const std::uint8_t> data) -> std::string; // ... std::span<unsigned char> out_view{out}; int n = BIO_read(..., out_view.data(), ...);
std::vector/std::array:std::span<T> view{vec};. - std::ranges + views pipelines (replace manual loops / classical algos / eager temporaries):
- Tokenization:
for (auto r : input | std::views::split(separator)) { std::string s(r.begin(), r.end()); ... }(seesplit()helper). - Collect/filter/transform:
std::ranges::copy( split(*devlist, ';') | std::views::transform(parse_uint32) | std::views::filter([](auto p){ return p && is_valid(*p); }) | std::views::transform([](auto p){ return *p; }), std::back_inserter(devices)); // or ` | std::ranges::to<std::vector<uint32_t>>() `
- Side-effect iteration:
std::ranges::for_each(range, lambda)(avoids indexfor (size_t i=0; ...)for joins). - Joins: simple
vec | std::views::join_with(", ") | std::ranges::to<std::string>(); complex (e.g. colored separators) fall back tofor_each+ flag. - Other:
views::take(N),views::filter+transform,enumeratewhere index+value needed. - Upgrade any remaining
std::transform/std::for_each/ manualfor+push_back+ifto the above.
- Tokenization:
- std::expected<T, std::string> for errors (carry diagnostics instead of losing info in
optional/bool/exceptions):Usage:[[nodiscard]] auto parse_uint32(std::string_view value) -> std::expected<std::uint32_t, std::string>; // ... if (error != std::errc{} || ptr != end) return std::unexpected("invalid uint32"); return parsed;
if (auto e = foo(); e) { use(*e); } else { log(e.error()); },.value_or(default),e ? *e : fallback. Call sites often need zero changes due to bool-conversion +value_or. Established inesfs_b64_decode,pkcs7_unpad,parse_uint32/parse_int64,load_tofu_trust. - Modern container/string APIs:
m.contains(k)(notm.find(k) != end()orm.count(k)>0),s.contains(substr)(notfind != npos). - Structured bindings (everywhere for maps, pairs, from_chars, if-init):
for (auto& [_, acc] : accounts) { use(acc); } else if (auto it = m.find(k); it != m.end()) { auto& [key, val] = *it; ... } const auto [ptr, ec] = std::from_chars(...);
- General: Prefer
std::ranges::sort/unique/for_each/copy_ifetc. over raw loops or<algorithm>. Zero classical<algorithm>calls remain in src (except commented). Update this section when adding new patterns (e.g. moreviews).
Build note: Use parallel + ccache during iterative work: CXX="ccache clang++" make DEBUG=1 (parallel -j is on by default; see Build System). Run make DEBUG=1 (not clean) after every logical group of changes; verify doctests pass.
(Concrete examples of these patterns are visible throughout src/ — e.g. OMEMO helpers, account/channel map handling, connection data-form/OG parsing, and avatar/ base64 paths. Extend them surgically.)
- Output and buffer operations: use
UiPort,BufferPort,LineStorePort, andRenderEventin handler/command logic — not rawweechat_printf/weechat_buffer_*(see Port abstraction). Directweechat_*calls belong only in port adapters and hook registration glue. - Hook/callback function signatures must match WeeChat exactly (
weechat_*types at the C boundary). - Use typed
UiPortmethods (printf_error,printf_info,printf_network,printf_date_tags_network,printf_date_tags_error) for prefixed notifications — not embeddedRuntimePort::default_runtime().prefix()in message bodies. UseRuntimePort::default_runtime().color()/.xmpp_color()for inline styling; reserveprefix("action")only for dated chat message columns (/me, MAM mentions). Rawweechat_prefix()belongs in port adapters (ui_port.cpp) only. - Buffer display values:
"1"(don't auto-switch),"auto"(auto-switch) - Plugin reload is supported (
/plugin reload xmpp): unload drains workers, disconnects, clears accounts, then shuts down libstrophe.make installuses temp+rename so a running WeeChat keeps the old mapped inode until reload (in-place overwrite ofxmpp.sostill causes SIGBUS — avoid rawcponto the live plugin path).
Raw xmpp_stanza_new() calls are forbidden in all new code and all .inl / .cpp files.
Use the fluent stanza builder system for outbound stanzas.
Raw xmpp_stanza_get_* / manual child-pointer walks are forbidden in handler and domain logic.
Use xmpp::StanzaView for inbound reads — wrap at the handler edge (StanzaView view{stanza};), then use attr_string(), child(), text(), and range-for over children().
- Base class:
stanza::spec— subclassed insrc/xmpp/xep-NNNN.inlfiles. - Attributes:
attr("name", value), namespace:xmlns<NsType>(), children:child(spec&), text:text(sv). - Build to
shared_ptr<xmpp_stanza_t>:auto sp = my_spec.build(ctx); - All namespace types live in
src/xmpp/ns.hh(e.g.urn::xmpp::omemo::_2,urn::xmpp::hints,eu::siacs::conversations::axolotl). - OMEMO:2 and legacy axolotl types are in
src/xmpp/xep-0384.inl(stanza::xep0384::encrypted,stanza::xep0384::header,stanza::xep0384::keys,stanza::xep0384::key,stanza::xep0384::payload,stanza::xep0384::store_hint, and axolotl_ variants).
Functions that return a raw xmpp_stanza_t* (transferring ownership to the caller) must ref-bump before the shared_ptr destructs. Use xmpp_stanza_clone (libstrophe 0.14) which increments the refcount and returns the same pointer:
auto sp = my_spec.build(ctx);
xmpp_stanza_clone(sp.get()); // bump refcount; shared_ptr dtor will release its ref
return sp.get(); // caller owns one reference; must call xmpp_stanza_release()connection.send() does not take ownership. Pass .get() from the shared_ptr directly:
auto msg_sp = stanza::message().type("chat").to(jid).id(uuid).build(ctx);
account.connection.send(msg_sp.get()); // shared_ptr releases on scope exitspec::child() copies the spec by value, so specs can be built up in loops:
stanza::xep0384::keys keys_spec(jid);
for (auto &dev : device_list) {
keys_spec.add_key(stanza::xep0384::key(rid, b64, is_kex));
}
header_spec.add_keys(keys_spec);Raw malloc/free/new/delete are forbidden. Use RAII exclusively.
Use heap_buf / make_heap_buf from src/omemo.hh:
// heap_buf = std::unique_ptr<uint8_t[], decltype(&free)>
uint8_t *raw = nullptr;
size_t len = base64_decode(text, strlen(text), &raw);
heap_buf buf = make_heap_buf(raw); // auto-freed on scope exitUse std::string + fmt::format — never malloc/snprintf:
std::string k_foo = fmt::format("prefix_{}_{}", name, id);
MDB_val mdb_key = { .mv_size = k_foo.size(), .mv_data = k_foo.data() };gcry_random_bytes()— allocate once, free withgcry_free()(notfree()).gcry_md_read()— returns an internal pointer into gcrypt's handle; never callfree()on it.
weechat_string_dyn_free(ptr, 0)— frees only thechar**container; reallocs*ptrto exact size and returns it.*ptris a caller-owned heap string. Assign*ptrto astd::unique_ptr<char, decltype(&free)>or callfree()when done.weechat_string_dyn_free(ptr, 1)— frees both thechar**container and*ptr;*ptris dangling afterwards. Returnsnullptr.
Use std::vector<T> for storage and std::vector<T*> for the pointer array (push nullptr as terminator):
std::vector<t_pre_key> storage;
std::vector<t_pre_key *> ptrs;
// ... populate storage, push &storage.back() into ptrs ...
ptrs.push_back(nullptr); // null terminator
func(ptrs.data()); // pass raw arrayUse std::make_unique<T[]>(N) instead of malloc:
auto buf = std::make_unique<xmpp_stanza_t*[]>(101);
xmpp_stanza_t **children = buf.get(); // auto-freed on scope exitAlways commit or abort every transaction — never let one go out of scope uncommitted.
The codebase has been refactored so that large .inl implementation fragments are
each compiled as their own translation unit via a thin wrapper .cpp in a subdirectory.
Do not open .inl files to read logic — open the corresponding .cpp wrapper instead,
which sets up all necessary includes and then #includes the .inl.
- src/connection/helpers.cpp — anonymous-namespace helpers shared by connection TUs
- src/connection/presence_handler.cpp — XMPP presence stanza handler
- src/connection/message_handler.cpp — thin adapter; delegates to
src/xmpp/message_*.cppslices - src/connection/iq_handler.cpp — thin adapter; delegates to
src/xmpp/iq_*.cppslices andiq_handlers.cpp - src/xmpp/stanza_view.cpp — inbound stanza reads (
StanzaView) - src/xmpp/iq_handlers.cpp — pure IQ reply builders (version, time, ping)
- src/xmpp/message_*.cpp, src/xmpp/iq_*.cpp — protocol parse/render slices (prefer extending these over raw strophe in handlers)
- src/weechat/ui_port.cpp, buffer_port.cpp, line_store.cpp, render_event.cpp — WeeChat port implementations
- src/connection/session_lifecycle.cpp — stream management + connect/disconnect lifecycle
- src/connection/internal.hh — declarations shared across connection TUs
- src/account/callbacks.cpp — WeeChat hook callbacks (fd, timer, input, etc.)
- src/account/lmdb_cache.cpp — LMDB MAM/caps/OMEMO cache read/write
- src/account.cpp — Account object: connect, disconnect, reset, channel/roster management
- src/command/account.cpp, channel.cpp, messaging.cpp, ephemeral.cpp,
notify.cpp, archive.cpp, encryption.cpp, history.cpp,
presence.cpp, roster.cpp, rooms.cpp, muc_admin.cpp — one
.cppper/xmppsub-command - src/channel.cpp — Chat buffer management (PM and MUC),
send_message - src/config.cpp — Plugin configuration
- src/omemo/api.cpp — Full OMEMO implementation (replaces the old
src/omemo.cpp) - src/pgp.cpp — PGP encryption support
Thin ports isolate inbound libstrophe reads and WeeChat output from domain logic.
All new and touched code must use ports and stanza builders — not raw weechat_* or xmpp_stanza_* in handler/command/domain logic. Migrate legacy calls surgically when editing that code path (same PR, no drive-by rewrites).
| Prefer | Instead of |
|---|---|
xmpp::StanzaView (src/xmpp/stanza_view.hh) |
xmpp_stanza_get_name, xmpp_stanza_get_attribute, xmpp_stanza_get_children, manual child-pointer walks |
stanza::spec builders (src/xmpp/node.hh, XEP .inl files) |
xmpp_stanza_new, xmpp_stanza_add_child, xmpp_stanza_set_* |
xmpp::handle_*_iq (src/xmpp/iq_handlers.hh) |
Inline IQ reply construction in connection handlers |
Handler slices follow parse → domain struct → render (src/xmpp/message_*.cpp, src/xmpp/iq_*.cpp, src/connection/*_handler.cpp). Add new protocol features by extending or mirroring these slices — not by growing monolithic .inl files with raw API calls.
Connection TUs register thin C adapters: receive xmpp_stanza_t*, wrap into StanzaView, delegate to pure functions.
| Port | Header | Role |
|---|---|---|
weechat::UiPort |
src/weechat/ui_port.hh |
Buffer output (printf, printf_error, printf_info, printf_network, printf_date_tags, printf_date_tags_network, printf_date_tags_error); UiPort::for_buffer() |
weechat::RuntimePort |
src/weechat/runtime_port.hh |
Host queries (version_string, color, prefix, xmpp_color); RuntimePort::default_runtime() |
weechat::BufferPort |
src/weechat/buffer_port.hh |
Buffer search + nicklist mutations |
weechat::LineStorePort |
src/weechat/line_store.hh |
Line updates by message tag (receipts, retractions, reactions, tombstones) |
RenderEvent / UiAction |
src/weechat/render_event.hh |
Sum type for the handler render step (print, nicklist, line glyph updates) |
Commands and handlers take or create UiPort (or return RenderEvents) — not weechat_printf / weechat_buffer_* in .inl logic.
- WeeChat hook/callback registrations (
weechat_*incallbacks.cppand similar). - libstrophe
xmpp_handler_addregistrations — handlers receivexmpp_stanza_t*, wrap intoStanzaViewon entry. - LMDB cursors, gcrypt, libsignal handles.
tests/weechat_stub.hh provides CapturingUiPort, NullUiPort, and StubRuntimePort. Handler-slice doctests exercise StanzaView + NullUiPort without a live WeeChat instance.
Migration history: port abstraction Waves 0–4 and Phases 1–5 (StanzaView handlers,
RenderEvent, BufferPort, parse utilities) are complete — see git history and archived
TODO.md entries.
Two channel types affect behavior throughout:
chat_type::PM- Private messages (1-on-1)chat_type::MUC- Multi-user chat (group rooms)
Check channel type before operations that differ (typing indicators, encryption, etc.)
Critical for proper PM buffer lifecycle:
0= Brand new channel - fetch 7 days of history-1= User deliberately closed - skip MAM, don't auto-create from presence>0= Existing channel - fetch only new messages since timestamp
- Location: account-specific
mam_db_path - Tables:
messages,timestamps,capabilities - Load caches on account connect
- Save to disk after updates
- Implement the feature in appropriate .cpp/.hh files
- Update README.md:
- Add to "This Fork" feature list if user-visible
- Document new commands in Commands section (Org format with examples)
- Mark relevant TODO items as
[X]
- Update DOAP.xml if implementing XEP support:
- Add
<implements>block with XEP number - Set
<xmpp:status>(complete/partial/planned) - Add descriptive
<xmpp:note> - Update version number if needed
- Add
- Test thoroughly - build, run, verify functionality
- Commit with descriptive message (see Git Conventions below)
-
README.md: Markdown format (GitHub-flavored)
- Use
#,##,###for heading levels (or keep Org-style*headings if present in legacy sections) - Code blocks: triple-backtick fences with language (
cpp,sh, etc.) - Commands: Show usage, examples, notes
- Keep consistent with existing style in the file
- Use
-
DOAP.xml: RDF/XML format
- Follow existing structure
- XEP versions should match spec version implemented
- Use descriptive notes for clarity
Xepher follows semantic versioning (MAJOR.MINOR.PATCH):
PATCHbump — bug fixes, performance improvements, no new user-visible featuresMINORbump — new user-visible features or significant behaviour changesMAJORbump — breaking changes or major architectural rewrites
- All commits pushed, 147 doctests passing (
make DEBUG=1), and optimized build OK (make). - Bump version in the three packaging files (all must match):
packaging/arch/PKGBUILD—pkgver=X.Y.Zpackaging/rpm/weechat-xmpp.spec—Version: X.Y.Z+ new%changelogentrypackaging/debian/changelog— new stanza at the top
- Commit the version bump:
chore: bump packaging to vX.Y.Z - Tag the release:
git tag -a vX.Y.Z -m "vX.Y.Z — <one-line summary>" - Push commits and tag:
git push && git push origin vX.Y.Z - Push the tag — GitHub Actions (
.github/workflows/packages.yml) builds all five distro packages sequentially and attaches them to the release automatically. - Edit the release on GitHub if needed (title, notes) —
gh release edit vX.Y.Z.
CI (default for releases): push vX.Y.Z tag → Actions runs packaging/github-build.sh X.Y.Z → artifacts attached to the release. Monitor with gh run watch.
CI retry one distro (does not re-run the other distros):
gh workflow run packages.yml -f version=X.Y.Z -f distros=alpine -f attach_to_release=true
gh run watch --repo ekollof/xepherLocal verification before tagging (requires Docker):
bash packaging/github-build.sh X.Y.Z # all distros
bash packaging/github-build.sh X.Y.Z --debian # single distroOptional — persistent distrobox containers (faster iterative local builds):
bash packaging/distrobox-build.sh X.Y.Z
bash packaging/distrobox-build.sh X.Y.Z --fedoraOutput lands in packaging/build/. All packaging paths pass PACKAGE_BUILD=1 to make.
If CI is unavailable, build locally then:
gh release create vX.Y.Z \
--title "vX.Y.Z — <summary>" \
--notes "..." \
--target master \
packaging/build/xepher_X.Y.Z-1_amd64.deb \
packaging/build/xepher-dbgsym_X.Y.Z-1_amd64.deb \
packaging/build/xepher-X.Y.Z-1.fcNN.x86_64.rpm \
packaging/build/xepher-X.Y.Z-1-x86_64.pkg.tar.zst \
packaging/build/xepher-debug-X.Y.Z-1-x86_64.pkg.tar.zst \
packaging/build/xepher-X.Y.Z_1.x86_64.xbps \
packaging/build/xepher-X.Y.Z-r0.apkmaster is protected on GitHub:
- Force pushes and branch deletion are blocked.
- Direct pushes from the sole maintainer are still allowed.
- To change protection rules:
gh api repos/ekollof/xepher/branches/master/protection
<type>: <short description>
[optional detailed explanation]
Types:
feat:- New featurefix:- Bug fixdocs:- Documentation onlyrefactor:- Code restructuring without behavior changetest:- Adding/updating testschore:- Maintenance tasks
Examples:
feat: implement /bookmark command for MUC bookmark management
fix: typing indicator shows nickname in MUC instead of room JID
docs: add comprehensive command documentation to README.md
- Make focused, atomic commits (one logical change per commit)
- Test builds before committing
- Update documentation in same commit as feature (keeps history clean)
- Push regularly to backup work
-
PM Buffer Recreation: Three causes must be prevented
- MAM cache containing old messages
- MAM fetch logic ignoring closed channels
- Presence handler auto-creating PM channels
- Solution: Use timestamp sentinel
-1and check everywhere
-
Typing Indicators: Different display for PM vs MUC
- MUC: Show nickname (resource part of JID)
- PM: Show bare JID (user@domain)
-
Auto-encryption Detection: Only for PM channels
- Check channel type before auto-enabling
- Show notification when auto-enabled
- Buffer display
"auto"causes unwanted buffer switching on plugin load - Use
"1"to keep user in current buffer /plugin reload xmppis supported (cold re-init; reconnect accounts after)
Skip autojoin for IRC gateway rooms (causes connection issues):
- JID contains
%character - JID contains "biboumi"
- JID contains "@irc."
- 147 doctests cover handler slices,
StanzaView, IQ builders, parse utilities, and port stubs (make DEBUG=1runs them automatically;make testanytime) — extend these when adding protocol logic - Full WeeChat integration still requires manual testing in a live instance
- Use
/debug dumpfor troubleshooting - Check logs:
/set xmpp.look.debug_level 2
- Main logs directory:
~/.local/share/weechat/logs/ - XMPP plugin logs:
~/.local/share/weechat/logs/xmpp.account.<account>.weechatlog - Raw XML log:
~/.local/share/weechat/xmpp/raw_xml_<account>.log(only written whenxmpp.look.raw_xml_log on) - OMEMO correlation helper:
tools/correlate_omemo_xml.sh - MAM LMDB cache inspector:
tools/dump_mam_db— dumps all 6 tables in the MAM cache with pretty-printed values. Build viamake tools. Run withXMPP_ACCOUNT=<account> tools/dump_mam_db(or--db <path>). Tables:messages(key:<channel>:<ts>:<msg_id>, val:<from>|<ts>|<body>),timestamps(key:<channel>, val: time_t),retractions,cursors(RSM cursors),omemo_plaintext,capabilities. Use--filter <prefix>to narrow to a specific channel,--limit Nto cap output,--table <name>to dump one table. - For OMEMO log debugging, always run
tools/correlate_omemo_xml.sh --account <account>first to correlate event logs with raw XML before proposing protocol-level fixes. - Example:
tail -n 300 ~/.local/share/weechat/logs/xmpp.account.andrath.weechatlog - Filter logs:
grep "OMEMO\|bundle\|devicelist" ~/.local/share/weechat/logs/xmpp.account.*.weechatlog - Note: After rebuilding, install
xmpp.sothen/plugin reload xmpp(or restart WeeChat). Reload disconnects and closes XMPP buffers; reconnect accounts afterward.
The plugin has two independent opt-in debug modes, both off by default. Agents investigating protocol issues should enable both before inspecting logs.
Routes internal protocol messages (PEP, avatar, vCard, OMEMO, stream
management, CSI, upload service, devicelists) to the xmpp.debug WeeChat
buffer instead of the account buffer. Each line has a [file:line] prefix.
Enable via the debug socket:
bash ~/Code/weechat-export/weechat-cmd.sh '/set xmpp.look.debug on'Or directly inside WeeChat:
/set xmpp.look.debug on
View the buffer:
/buffer xmpp.debug
XDEBUG(...) log file: WeeChat writes the xmpp.debug buffer to:
~/.local/share/weechat/logs/xmpp.debug.weechatlog
This is the on-disk record of all XDEBUG output. Tail it directly to
monitor debug messages without opening WeeChat:
tail -f ~/.local/share/weechat/logs/xmpp.debug.weechatlogWhen off, no FEED buffers are created or restored on connect, pubsub push
handlers ignore feed nodes, and /feed is blocked (except /feed close to
dismiss existing buffers). Default: on.
/set xmpp.look.feeds off
Appends every SEND and RECV XML stanza to a per-account log file:
~/.local/share/weechat/xmpp/raw_xml_<account>.log
Enable:
bash ~/Code/weechat-export/weechat-cmd.sh '/set xmpp.look.raw_xml_log on'Or inside WeeChat:
/set xmpp.look.raw_xml_log on
Read recent entries:
tail -n 100 ~/.local/share/weechat/xmpp/raw_xml_<account>.logSearch for a specific stanza:
grep -A 20 "RECV iq" ~/.local/share/weechat/xmpp/raw_xml_<account>.log | head -60- Enable both options (via debug socket or FIFO so WeeChat stays running).
- Reproduce the issue.
- Read
xmpp.debugbuffer for high-level protocol event sequence. - Cross-reference with
raw_xml_<account>.logfor exact wire content. - For OMEMO issues: run
tools/correlate_omemo_xml.sh --account <account>. - Disable both options when done to restore normal behaviour.
Two mechanisms are available for sending commands to or evaluating expressions in a running WeeChat process without touching the terminal.
weechat_debug_socket.py opens a Unix socket at:
$XDG_RUNTIME_DIR/weechat/weechat_debug.sock
# typically: /run/user/1000/weechat/weechat_debug.sock
Use the weechat-cmd.sh wrapper from ~/Code/weechat-export/:
# Eval a WeeChat expression (must use ${...} syntax) — returns result
bash ~/Code/weechat-export/weechat-cmd.sh '${info:version}'
bash ~/Code/weechat-export/weechat-cmd.sh '${weechat.color.chat_bg}'
# Execute a WeeChat command — prints "ok", no output returned
bash ~/Code/weechat-export/weechat-cmd.sh '/xmpp feed andrath@deimos.hackerheaven.org urn:xmpp:microblog:0'Or directly with socat:
echo '${info:version}' | socat - UNIX-CONNECT:/run/user/1000/weechat/weechat_debug.sockImportant: The script must be loaded in the running WeeChat first. If the socket is absent, load it via the FIFO (see below):
echo "*/python load weechat_debug_socket.py" > /run/user/1000/weechat/weechat_fifo_<pid>After make install (atomic replace into ~/.local/share/weechat/plugins/),
run /plugin reload xmpp (or unload then load). Reload tears down connections
and buffers; reconnect accounts afterward. Do not cp over the installed
xmpp.so while WeeChat is running — that truncates the mapped file and SIGBUS.
WeeChat creates a FIFO at:
$XDG_RUNTIME_DIR/weechat/weechat_fifo_<pid>
# typically: /run/user/1000/weechat/weechat_fifo_<pid>
Find the current FIFO:
ls /run/user/1000/weechat/weechat_fifo_*Send a command (note the *\t prefix — * means "core buffer", tab-separated):
echo "*/python load weechat_debug_socket.py" > /run/user/1000/weechat/weechat_fifo_<pid>The FIFO is write-only and returns no output. Use it only when the debug socket
is not yet available (e.g. to bootstrap loading weechat_debug_socket.py).
Prefer the debug socket for all other interactions.
Testing Approach:
- Pragmatic manual testing - The codebase uses minimal automated tests due to:
- Complex WeeChat plugin API dependencies
- XMPP protocol interactions requiring real servers
- Encryption libraries (OMEMO, PGP) hard to mock
- Test incrementally - Build and manually verify each feature as implemented
- Document test procedures - Keep notes on how to verify critical features
- Focus on regression prevention - Manually retest previously fixed bugs
- Read
AGENTS.md— full file at session start and again after any context compaction before continuing - Understand the request - ask clarifying questions if needed
- Explore existing code - find similar handler slices and port usage (match
StanzaView/UiPortpatterns in nearby files) - Make minimal changes - surgical fixes, don't refactor unnecessarily
- Build and test:
make DEBUG=1 && <test in WeeChat>(run plainmakebefore release to verify the optimized build) - Update documentation - README.md, DOAP.xml if applicable
- Commit with clear message - follow conventions above
- Push to repository - backup work regularly
After implementing features, manually verify in WeeChat:
Critical Features to Retest:
- PM buffers don't reappear after
/close - Typing indicators show nicknames (not room JID) in MUC
- Plain text is default for new PMs (not OMEMO)
- Auto-encryption enables when receiving encrypted messages
- Buffer doesn't auto-switch on plugin load
-
/bookmarkcommand works (add, list, delete, autojoin) - Biboumi/IRC gateway rooms don't autojoin
-
/listdiscovers public rooms -
/pingprovides feedback - Capability cache persists across restarts
Basic Smoke Test:
- Load plugin:
/plugin load xmpp.so - Connect account:
/xmpp connect <account> - Join MUC:
/join room@conference.server - Send message: Type and press enter
- Verify typing indicators work
- Close and verify no recreation:
/closethen reconnect
When implementing a new feature:
- Code implementation in appropriate files
- Build succeeds (
make DEBUG=1; plainmakebefore release) - Manual testing in WeeChat (see Manual Testing Checklist)
- README.md updated (feature list, commands, TODOs)
- DOAP.xml updated (if XEP-related)
- Test critical regressions (PM recreation, typing indicators, etc.)
- Commit with descriptive message
- Push to repository
- Origin: Fork of bqv/weechat-xmpp
- Remote: https://github.com/ekollof/xepher.git
- Branch: master (protected — no force-push, no deletion)
- Language: C++23 (
-std=c++23) - Dependencies: libstrophe, LMDB, WeeChat API
| Resource | URL |
|---|---|
| GitHub repository | https://github.com/ekollof/xepher |
| GitHub wiki | https://github.com/ekollof/xepher/wiki — clone with git clone https://github.com/ekollof/xepher.wiki.git |
| GitHub Pages (website) | https://ekollof.github.io/xepher — source on the gh-pages branch |
| Releases | https://github.com/ekollof/xepher/releases |
When updating documentation:
- Code-level guidance for agents →
AGENTS.md(this file) - User-facing docs →
README.md(Markdown) andDOAP.xmlin the repo root - Wiki → clone
https://github.com/ekollof/xepher.wiki.git, edit, commit, push (no PRs — direct push tomaster) - Website →
gh-pagesbranch; editindex.htmldirectly
# Dev build (agents: use this while coding)
make DEBUG=1
CXX="ccache clang++" make DEBUG=1
# Optimized build (pre-release / matches installed plugin performance)
make
# AddressSanitizer (combine with DEBUG=1; Linux links libasan)
make DEBUG=1 ASAN=1
# Serial build (debugging makefile ordering only)
make -j1 DEBUG=1
# Clean build (avoid; prefer ccache incremental)
make clean && make DEBUG=1
# Distribution build (no .source embed — matches packaging; optimized)
make PACKAGE_BUILD=1 weechat-xmpp
# Direct CMake (IDE presets — equivalent to wrapper profiles)
cmake --preset dev && cmake --build --preset dev
ctest --preset dev
# Developer tools (LMDB inspectors)
make tools
# Package all distros via Docker (same script as CI)
bash packaging/github-build.sh X.Y.Z
bash packaging/github-build.sh X.Y.Z --fedora
# Optional: persistent distrobox package builds
bash packaging/distrobox-build.sh X.Y.Z
# Watch CI package build after tagging
gh run watch --repo ekollof/xepher
# Check git status
git status
# See what changed
git diff
# Correlate OMEMO events with raw XML before OMEMO fixes
tools/correlate_omemo_xml.sh --account <account>
# Commit everything
git add -A && git commit -m "message"
# Push changes
git push- Re-read
AGENTS.mdif the thread was compacted or you are continuing from a summary - Follow existing patterns in the codebase (ports + stanza builders + modernized
ranges/string_view/fmtin touched areas) - Make minimal changes - don't refactor working code; migrate raw
weechat_*/xmpp_stanza_*only in code you are already editing - Test incrementally -
make DEBUG=1after each logical change - Document as you go - don't leave docs for later
- Ask before major changes - discuss architecture decisions
- Never create loose summary/planning documents - only update README.md, DOAP.xml, or code comments