Tacky is high-level library for building XMPP clients
  • Tcl 72.1%
  • C 11.4%
  • Rust 10.8%
  • Makefile 1.9%
  • Python 1.6%
  • Other 2.1%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Paavel A. 3d7e9f3a6f
1.1.18: the README's version for the tree to be tagged v1.1.18 (the user, 2026-10-31: "then build and release"; the app pins libtacky and the avatar-after-name report is ours), on 1.1.17 + fix/avatar-after-name: set_name, clear_name, set_nick, set_fields and set_photo read our vcard-temp again before they write it, so a name change on a server that converts avatars (XEP-0398, ejabberd mod_avatar as production runs it) no longer writes back a stale card without the PHOTO and has the server delete the XEP-0084 avatar for everyone; avatar rereads the card after a publish or disable the server converts, so the presence photo hash follows a picture change; the ejabberd rig loads mod_avatar and mod_vcard_xupdate and four integration cases keep the avatar across each way of changing the name. tools/stats.py --readme --cov _build/cov regenerated with the main .git mounted, coverage from make coverage-all on b297863b (Tcl 98.7 % procs, Rust 99.5 % lines, C 99.0 % lines; its tests/run.tcl 6988 total, 6978 passed, 0 failed, 10 skipped). Gate: a fresh build, tests/run.tcl -j 32 6988 total, 6978 passed, 0 failed, 10 skipped; test-rust 787 passed, 0 failed, 5 ignored; check-api, check-headers, check-native-neutral and wasm clean; integration on ejabberd 26.09 (mod_avatar, mod_vcard_xupdate) with omemo-bot: test_catchup, test_message, test_omemo, test_omemo2_interop, test_mucsub, test_album, test_sm_window, test_muc_captcha, test_muc_invite, test_muc_edit, test_register, test_gateway_register, test_avatar, test_jinglefile_s5b 86 passed, 0 failed, 3 skipped, test_tacky_avatar 5 and test_vcard4 4 passed; test_muc_captcha also on anotherim-server's room-pow image with and without captcha_cmd, 3 passed and 1 skipped each; test_avatar on Prosody 11 passed.
2026-10-31 10:15:48 +02:00
docker make audit, a RustSec dependency audit in the gate (the user, 2026-10-27): tools/cargo-audit.sh checks every Cargo.lock of the crates, tacky-core, tacky-crypto, tacky-keyring and tacky-omemo and their fuzz/ workspaces (found under tacky-*/, so a new one is covered), with cargo-audit 0.22.2, the project's static musl release pinned by version and sha256 in docker/linux.Dockerfile. The advisory database is a shallow clone fetched afresh at each run into _build/advisory-db; a fetch that fails fails the audit, saying nothing was audited and how old the cached copy is, and AUDIT_OFFLINE=1 audits against that copy. A vulnerability advisory fails; unmaintained, unsound and yanked ones are printed as warnings. audit.toml is the allow-list, empty, its entries passed as --ignore, and one without an id, a reason or a YYYY-MM-DD date fails the audit. docker/gate.sh (make gate) runs it before the lanes, a line per finding in its summary and the log in _build/gate/audit.log, GATE_AUDIT=0 leaving it out. README's target table and ROBOTS.md's Build entry say so. On develop it fails today, and the bumps are left for a decision: tacky-core on rustls 0.23.45 (RUSTSEC-2026-0346, -0347, -0348, -0349; fixed in 0.23.46), rustls-webpki 0.103.15 (RUSTSEC-2026-0382; 0.103.16), webpki-roots 1.0.9 (RUSTSEC-2026-0381; 1.0.10), time 0.3.45 (RUSTSEC-2026-0009; 0.3.47) and hickory-proto 0.24.4 (RUSTSEC-2026-0119; 0.26.1), its fuzz lock on the same but time and webpki-roots; tacky-crypto and tacky-omemo and their fuzz locks on crypto-bigint 0.6.1 (RUSTSEC-2026-0387; 0.7.6); tokio 1.53.1's RUSTSEC-2026-0386 (unsound) warns; tacky-keyring is clean. 2026-10-27 12:05:28 +02:00
docs OMEMO: the dead-domain rule no longer catches live domains (audit 2026-10-30 A1 M2): a list fetch failing with remote-server-timeout or remote-server-not-found is a vote (DownVote), the failures within DOMAIN_VOTE_BURST_MS (3 s) of a vote's first are that vote (a connect asks for every list at a domain at once and one s2s hiccup failed them together), and the domain is marked for DOMAIN_DOWN_MS only by two live votes naming two of its JIDs or more; not-found no longer marks alone (a transient DNS failure at our server), votes older than DOMAIN_DOWN_MS lapse, any other answer from the domain (a list read, a refusal) clears its votes and its mark (DomainAnswered), and our own account's domain is never voted on (OwnDomain), where a busy server's IQ timeouts would have stopped the lists of most contacts. omemo_harness answers per JID too (unreachable, unresolved, reachable). Tests: test_omemo_devices 53 of 53 three runs in a row, five new (one burst is one vote, not-found votes and marks with a second, never our own domain, votes expire, an answer clears) and the gone-domain one reworded for the burst rule, all six failing before. 2026-10-30 17:33:19 +02:00
embed Merge branch 'feat/stickers-packs' into develop: the stickers rework's library side beside the people directory. tackyd's usage and header name both bundled programs, tests/tackyd_program.tcl keeps the cases of each, and test_wire_surface counts 396 requests (391 at v1.0.7, two for the directory's listing and unlist, three for stickers fetch_pack, catalogue and create_pack). 2026-10-21 05:22:56 +03:00
examples Comments, build files and the crates' docs no longer cite documents this tree does not have: TRANSFER.md, HTTP-TCL.md, LOCALSTORE.md, ARCHITECTURE.md (the workspace's §9.1 and §9.2), MEDIA.md, JINGLE-FT.md, LIVENESS.md, NOTIFY.md, CHATLIST.md, THREADS.md, STORAGE.md, CHAOS-FINDINGS.md, ONE-CLIENT.md, ROADMAP-another-tacky.md, docs/DEPLOY.md, docs/STABILIZE.md, docs/discovery.md, docs/storage-lock.md, reports/OMEMO_issues.md and reviews/2026-09-*, nor ROBOTS.md by section numbers it lost long ago (§2.2, §7.1, §8, §10.80 and the rest). Each pointer now names the place that exists (tacky/http/README.md, tacky-keyring/README.md, ROBOTS.md's HTTP, "The store", "The shape" and rule 2 or 6, tacky_media.h's data channel section, session/liveness.tcl, modules/file.tcl, ext/common.h's rule 1, deps.mk's and wasm.mk's own sqlite3 paragraphs) or is dropped where the sentence already says what the document did; generated outputs (dist/HANDBOOK.md, REPORT.md under COV_DIR) and the guides' pattern are left as they are. Not touched, being other branches' files: tacky/xmpp/modules/ (about 110 such citations), ext/core/keyring.c and keyring.h, ext/core/tests/test_tls.tcl, tacky/core/keyring.tcl and storage.tcl, and the schema's doc strings. Only comments changed; check-native-neutral, check-headers, check-api and test-tacky are green. 2026-10-07 20:42:28 +03:00
ext An automatic sticker download gets a gate of its own (the user, 2026-10-27: stickers need their own auto-download option, Everyone, Contacts or Never, Everyone by default, never to private or loopback addresses, image types only, a small cap, the share's hash checked): http download -auto 1 -sticker 1 is judged by the installation setting attachment_autofetch_stickers (everyone when unset; contacts takes a sender who is a contact of the account or a URL on the account's own server; never refuses) in place of attachment_autofetch and attachment_autofetch_foreign, takes https only, caps the body at 512 KiB whatever max_bytes says (STICKER_MAX), and answers hash mismatch, keeping nothing, when the body does not hash to what the sender's share advertised (file.tcl HashVerdictFile under the url and from; no record judges nothing, as for any download). The fetch is made to the public internet only, every redirect's dial too: tacky-core gains is_public (none of this device's addresses, 10/8, 172.16/12, 192.168/16, 100.64/10, 169.254/16, fe80::/10, fc00::/7, multicast, broadcast, 240/4, those IPv4 ones IPv4-mapped or behind 64:ff9b::/96), dial_public and net::socket::connect_public, which drop such addresses after the resolve as dial_remote drops this device's, the FFI tacky_net_socket_new_public (tacky_ffi.h regenerated), tacky::core::net::socket::connect -public BOOL (no -proxy, no -remote), tacky/http's spec key public (https only, no proxy: "a public-only fetch is not made through a proxy", so an account with a SOCKS5 or Tor route leaves its stickers to a tap) and the transfer module's -public, which only the builtin backend takes. The image type is the host's to check once an encrypted share is decrypted (the https form of an aesgcm:// share is all this side sees). The schema documents sticker (since 1.1.5) and the new error; docs/features.md's XEP-0449 row says so, tacky/http/README.md has the key. Tests: Rust public_is_none_of_the_local_or_private_ranges, connect_public_dials_no_private_address, new_public_connects_to_no_local_or_private_address; test_net.tcl's -public cases (a boolean, no -proxy, no -remote, ten local and private spellings all resolve with no connect, -public 0 ordinary); http-public-only-https-direct-and-no-local-address; http-spec-defaults has the key; test_http's http-download-sticker-follows-its-own-policy, -dials-no-local-address and -hash-mismatch. Tested in the container: check-api clean, check-headers clean, make core and test-core all ok, tacky-core's cargo tests 251 and 8 passed with cov_stream's a_walk_with_one_refusal_among_timeouts_is_not_closed_refused failing alone too (no-such-host.invalid takes longer than its 0.3 s to fail on this host's resolver, untouched by this change); the http suites 48 passed, test_http 27, test_file 245, test_wire_surface 4, test_wire, test_stickers, test_avatar_fetch, test_file_upload_flow and test_jinglefile, 0 failed. 2026-10-27 06:00:26 +02:00
platforms The Windows archive carries the media binding, so a host on -media-backend webrtc can place calls there (anotherim-js #33, "Audio and video calls don't work": "start: calls are unavailable, no media backend is open (webrtc: its native binding is not in this build)"): libtackymedia_webrtc.dll was built and shipped and the shell named it with -webrtc-lib, but libtacky-win.a lacked tacky::media::rtc, since win-lib never got the rule Android has and compiled the shim with -DTACKY_WITHOUT_AV, which drops Tackymedia from embed/extensions.h, so package require tacky::media::rtc failed and ProbeWebrtc answered that text. The Makefile now builds $(W_EXT_DIR)/libtackymedia_ext.a from ext/media/media.c with -DTACKYMEDIA_NO_LINKED, lists it in W_EXT_LIBS and passes EMBED_CFLAGS="" in WINDOWS_TCL_ARGS, as android-lib does; media.c is ported to MinGW: under _WIN32 a small dlfcn shim opens the library with LoadLibraryW (the UTF-8 path converted to UTF-16, so an install path outside the ANSI code page loads, and its slashes made backslashes as LoadLibrary asks), resolves with GetProcAddress, reports FormatMessage's text for dlerror and never calls FreeLibrary (what RTLD_NODELETE gives the other platforms), and Tcl_CreateFileHandler/Tcl_DeleteFileHandler, which Tcl has only on Unix, are compiled out there, so a backend is always polled from the timer (tacky-media's poll_fd is -1 on Windows anyway). The comments in the Makefile, platforms/windows.mk, docker/in-image-windows.sh and ROBOTS.md's ext table say so, and in-image-windows.sh now fails when the archive has no Tackymedia_Init. Verified in the Windows image only, with no Windows machine and nothing run under Windows or wine: docker/in-image-windows.sh lib builds media.c clean under -Wall -Wextra, x86_64-w64-mingw32-nm shows tackymedia.o's T Tackymedia_Init beside the shim's reference, and a link proof (a MinGW executable over libtacky-win.a and the system import libraries) resolves every symbol, Tackymedia_Init, OpenCmd and LoadLibraryW among them. On Linux make media builds the binding under -Werror and make test-media passes, tacky-media's ctest 20, 10 and 3 of 3 and ext/media/tests/test_media.tcl 52 of 52. 2026-10-24 00:20:50 +03:00
schema A room's proof of work is solved only while our join to it is in flight (audit 2026-10-30 A1 M1; the user: "fix all"): captcha SolvesFor answers 1 for our own server or account and for a room muc IsJoining names (its JoinTimers entry, which HoldJoin keeps), so a room we are in already or have only bookmarked, which could keep a core at 100 % for 90 s per challenge for as long as it liked, gets the host's <Challenge> as in 1.1.15. The join's end stops the solve: muc:<Joined>, <Left>, <Error> and an internal muc:<Leaving> that leave now publishes before the room's echo. Schema, features.md, ROBOTS.md and captcha.tcl say so. Tests: test_captcha 33 of 33, four new (a joined room's and a bookmarked room's proof of work are the host's, leaving and a refused join stop the solve, hashcash::Solves empty), all four failing before; test_muc 299 of 299; check-api clean. 2026-10-30 17:25:09 +02:00
tacky vcard: set_name, clear_name, set_nick, set_fields and set_photo read our vcard-temp again before they write it, and avatar rereads it after a publish or disable the server converts (XEP-0398). The tester on 0.7.17: "You set an avatar, all fine, then you change the name -- the avatar is gone." The cause was on the server and ours: the vcard-temp read at <Ready> was republished whole on a name change, but ejabberd's mod_avatar had since written the new PEP avatar into the stored PHOTO, and a vcard-temp set without a PHOTO makes mod_avatar publish an empty XEP-0084 metadata, so the picture went for every client and the stored vCard; with an older picture in the stale copy it would come back instead. Reading the card again also covers a PHOTO another device wrote. The reread after a converted publish puts the PHOTO's hash into our presence, which said an empty <photo/> (no picture) until the next session. vcard set keeps its cached path: on a converting server it writes no vcard-temp. Tests: test_vcard.tcl vcard-{setname,clearname,setnick,setfields}-rereads-the-photo-the-server-wrote and test_avatar.tcl's two converting-server tests fail on v1.1.17 and pass; test_vcard4.tcl, test_presence.tcl and the clear_name test answer the new read. Gate: tests/run.tcl -j 32 6988 total, 6966 passed, 0 failed, 22 skipped; check-api clean; integration on ejabberd 26.09 (mod_avatar) with omemo-bot: test_avatar 11, test_tacky_avatar 5, test_vcard4 4 passed; test_avatar on Prosody 11 passed. 2026-10-31 09:47:17 +02:00
tacky-core tacky-core tests: a name that does not resolve is a label of 64 octets under .invalid (UNRESOLVABLE), which getaddrinfo refuses before any query, in place of no-such-host.invalid: cov_stream's a_name_that_does_not_resolve_is_closed_resolve and a_walk_with_one_refusal_among_timeouts_is_not_closed_refused, net::socket's and the FFI socket's resolve cases. The container's search domain made no-such-host.invalid a second lookup, no-such-host.invalid.owl, 436 ms through the rig's nameservers against the walk's 300 ms step, so the walk ended in a timeout and not Io (RL ran 1.1.16's gate and coverage with --dns-search=. for it); the long label fails in 1 ms on any resolver and never leaves the machine. cov_stream 10 runs in a row: before 3 failed, after 0; make test-rust green. 2026-10-30 17:35:06 +02:00
tacky-crypto tacky-crypto tacky_mtproto_dh_keypair and tacky_mtproto_dh_agree make their output slices only after the inputs' last use, one at a time, so an output that is an input buffer itself, which tacky_crypto.h promises is allowed, no longer puts a &mut beside a live & of the same bytes (undefined behaviour in Rust; audit 2026-10-27 native L2); a NULL output is still E_NULL before anything is computed or written, as tacky_crypto_x25519 already does. tests/mtproto.rs dh_outputs_may_be_the_input_buffers calls agree with key_out over a, over peer and over p, and keypair with a_out over server_random and g_a_out over p, each checked against the vector's key or by agreeing with the vector's b, and that a_out stays untouched when g_a_out is NULL. The old code computed before it wrote, so the bytes were right there too and the test passes on it as well: only Miri would see the aliasing, and the image has no Miri (nor a nightly); cargo test for tacky-crypto all green (96 unit, mtproto 6 of 6). 2026-10-27 03:25:08 +02:00
tacky-keyring The delta audit of 2026-10-27, D4: a keyring claim dated ahead by less than CLAIM_STALE (a clock stepped back a little under a live creator) is live, busy, rather than stale, so a second creator can no longer take it away within the seconds of a create; one further ahead is stale as before, and a near one heals once the clock passes it. Test without_hard_links_a_claim_from_the_future_is_stale extended (the near claim was taken over before); tacky-keyring 50 passed. 2026-10-27 17:09:24 +02:00
tacky-media framestream: rename the socket where link() is denied 2026-10-17 11:43:12 +04:00
tacky-omemo tacky-omemo: a heartbeat is due once per ratchet key, XEP-0384 §6's rule ("the first message for a given ratchet key with a counter of 53 or higher"), not after every 53 messages received since we last sent: a peer that has not read our heartbeat yet keeps its ratchet key, so reading a run of its archived messages in one go (an account catch-up after a day offline, 2026-10-30) made one heartbeat per 53 messages, each an empty <store/> message archived and fetched again by every device. Now wants_heartbeat is a flag set by the first message of the peer's current ratchet key whose counter reaches 53 (counted as picomemo and Conversations count it, the 53rd message of a chain, so the interop suite's heartbeats both ways stand), cleared by our next send and by a new ratchet key of the peer's (it has read something of ours); not in the session record, since the host asks right after the unwrap; since_sent stays in the record unused for the decision. tacky_omemo.h's comment says so. Tests: session.rs a_heartbeat_is_due_once_per_ratchet_key (3x53 messages on one key read with our heartbeats unread: 1 heartbeat, before 3; then a new key of hers is due again at its own 53rd), failing on the old rule and passing on the new; cargo test --features oracle for tacky-omemo all pass (114 + 21 + 28 + 3 + 7); test_omemo.tcl omemo-heartbeat-once-per-account-walk now has romeo read our heartbeat before its later messages, which are then on a new key. 2026-10-30 11:17:05 +02:00
tests vcard: set_name, clear_name, set_nick, set_fields and set_photo read our vcard-temp again before they write it, and avatar rereads it after a publish or disable the server converts (XEP-0398). The tester on 0.7.17: "You set an avatar, all fine, then you change the name -- the avatar is gone." The cause was on the server and ours: the vcard-temp read at <Ready> was republished whole on a name change, but ejabberd's mod_avatar had since written the new PEP avatar into the stored PHOTO, and a vcard-temp set without a PHOTO makes mod_avatar publish an empty XEP-0084 metadata, so the picture went for every client and the stored vCard; with an older picture in the stale copy it would come back instead. Reading the card again also covers a PHOTO another device wrote. The reread after a converted publish puts the PHOTO's hash into our presence, which said an empty <photo/> (no picture) until the next session. vcard set keeps its cached path: on a converting server it writes no vcard-temp. Tests: test_vcard.tcl vcard-{setname,clearname,setnick,setfields}-rereads-the-photo-the-server-wrote and test_avatar.tcl's two converting-server tests fail on v1.1.17 and pass; test_vcard4.tcl, test_presence.tcl and the clear_name test answer the new read. Gate: tests/run.tcl -j 32 6988 total, 6966 passed, 0 failed, 22 skipped; check-api clean; integration on ejabberd 26.09 (mod_avatar) with omemo-bot: test_avatar 11, test_tacky_avatar 5, test_vcard4 4 passed; test_avatar on Prosody 11 passed. 2026-10-31 09:47:17 +02:00
tools XEP-0514 Emoji Markup is not supported, per the user's ruling of 2026-10-27 (not wanted; Unicode emoji only): the leftovers of its reservation go. The schema loses message send's custom_emoji argument, Message.custom_emoji, the CustomEmoji type and the stickers module's note and Sticker.name clause about custom emoji; tacky/core/methods.tcl is regenerated and tacky::core::wire's message type no longer names custom_emoji; message send no longer takes -custom_emoji. The skeleton module tacky/xmpp/modules/emojimarkup.tcl, its four skeleton tests and the xep0514-ex1/ex2 fixtures are deleted, and test_stubs_messaging.tcl drops the reserved -custom_emoji call (7 reserved calls now), its wire frame, the empty -custom_emoji send and the markup flood test, which without the module is a plain text message. docs/features.md gets the legend entry ⛔ NOT SUPPORTED (deliberately not implemented, not planned) and the XEP-0514 row says so; tools/doap.py maps that status to wontfix and schema/tacky.doap is regenerated. check-api clean (69 modules, 594 methods, 189 events, 174 types), gen-api current; test_stubs_messaging 5, test_skeletons_messaging 19, test_wire_surface 4 (all 476 answered), test_reply 3, test_stickers 55, test_message 517 passed; the full unit run's only failures (test_mtproto*, test_jinglefile_s5b, test_http*, test_linkmeta, one test_omemo_devices) fail the same with this change stashed, in this worktree's container. 2026-10-27 14:14:00 +02:00
.gitignore The XMPP half of the Tcl tree moves under lib/xmpp and the tacky::xmpp packages, the first step of the rename pass. lib/taco/session becomes lib/xmpp/session with package and namespace tacky::xmpp::session (::tacky::session:: renamed everywhere as one token); lib/taco (taco.tcl, redact.tcl, modules/) becomes lib/xmpp/client (client.tcl, redact.tcl, modules/) with the package tacky::xmpp::client; the package xmpp becomes tacky::xmpp; tests/taco and tests/taco_integration become tests/client and tests/integration, all by git mv. lib/taco/pkgIndex.tcl goes and lib/xmpp/pkgIndex.tcl registers tacky::xmpp and its session, client and component subpackages, since pkgUnknown looks only one level below lib/. The internal names stay: the taco_* snit types, :🌮:, procs, State keys and every package version. Everything that named a moved path or package follows in this commit: tests/run.tcl's suite directories and auto_path, test_session.tcl's auto_path and test_session_scram.tcl's source path, deps.mk's load check list and comments, .gitignore's tmp directory, tools/api/tcl.py's module paths, tools/stats.py's parts and test-to-part map, the schema's per-module tcl paths and doc strings (methods.tcl came out unchanged from make gen-api), README.md, ROBOTS.md (with a decision paragraph naming the old names), docs/, the READMEs of lib/, ext/, examples/ and tests/servers, and comments across lib/ and tests/; notes about what a file was called before 2026-10-07 and the paths of the legacy repositories keep their old names. Checked in the container: make test-tacky (tests/run.tcl 4551 total, 4550 passed, 0 failed, 1 skipped, as before; the load check sees tacky::xmpp, tacky::xmpp::client and tackywire; 22 integration suites load), test-tackykeyring and test-tackycore (the tests/tcl suites), test-tk-example, test-build (24 passed) and check-api. 2026-10-07 12:49:27 +03:00
.gitmodules native/ dissolves: tacky-core, tacky-crypto, tacky-keyring, tacky-media and tacky-ratchet move to the repository root with git mv, the libdatachannel submodule's path following in .gitmodules, so the parts a host builds sit beside ext/, lib/ and embed/ instead of one level down. Every path that named native/ follows: the root Cargo.toml's members, deps.mk, the Makefile, coverage.mk, platforms/*.mk, the Windows image's script and Dockerfile, tools/stats.py and check-core-header.py, test_build.sh, the bindings' and Tcl modules' comments, the crates' own READMEs and the docs, with HANDBOOK.md regenerated from them. check-native-neutral keeps its name and now scans the tacky-*/ directories by glob, pruning the submodule as before; its rule reads that nothing under a tacky-*/ directory knows Tcl. Checked in the container: test-rust, check-headers, check-native-neutral, check-api and test-build (24 of 24) pass. 2026-10-07 09:15:32 +03:00
audit.toml make audit passes (the gate's RustSec audit of 17a91cd5, failing on develop): tacky-core and its fuzz lock take rustls 0.23.46 (RUSTSEC-2026-0346..0349), rustls-webpki 0.103.16 (-0382) and tokio 1.53.3 (-0386, unsound, a warning), tacky-core webpki-roots 1.0.10 (-0381), each a cargo update --precise in the image. Three advisories go into audit.toml instead, each because its fix needs a Rust newer than the crates' 1.85 floor (Debian's rustc in docker/linux.Dockerfile), with the analysis in the entry: time 0.3.45's RUSTSEC-2026-0009 (0.3.47 needs 1.88), a tacky-core dev-dependency only, through rcgen and yasna for test certificates, never in libtacky.a, and the stack exhaustion is in time's RFC 2822 parsing, which neither calls; hickory-proto 0.24.4's RUSTSEC-2026-0119 (0.26 needs 1.88 and the async-only resolver), whose O(n^2) name compression is in BinEncoder, encoding, while srv.rs's resolver encodes only its own one-question query and decodes answers (DnsResponse::from_message is test-only in hickory-resolver 0.24.1, and Android parses with Message::from_vec); crypto-bigint 0.6.1's RUSTSEC-2026-0387 (0.7.9 needs 1.87, 0.7.6-0.7.8 are yanked, crypto-primes 0.6 pins ^0.6), which does matter in kind, since OTR's D-H, SMP and DSA and MTProto's D-H run MontyForm and Uint::rem on secrets, but whose fixed leaks are 0.7-line code: borrowing_sub's branch came with #1029 after 0.6.1 (whose sbb is the older WideWord form), UintRef and BoxedMontyForm are unused, 0.7's multiply with a bool carry is not 0.6.1's mul_wide plus HAC 14.32 reduction, division is by the public q or p only; nothing has BINSEC-checked 0.6.1, which the entry says. Each entry names its follow-up, and raising the floor to 1.88 retires all three. ROBOTS.md's Build entry says so. In the container: make audit 8 locks clean; the webrtc lane (all test wasm REQUIRE=1) 217 suites, 6812 tests, 6811 passed, 1 skipped, Rust 784 passed, the SRV resolver's tests against a name server of their own among them; the datachannel lane the same but for three timing cases under the load of a parallel app build (cov_stream's 0.3 s resolve, test-core's footer bound, test_mtproto_tl_fuzz, test_omemo_devices), all passing on a rerun (test-rust and test-core 784 passed, the two suites 48 of 48); check-api clean. 2026-10-27 12:25:20 +02:00
coverage.mk Merge branch 'test/cov-cext' into develop: coverage tests for tacky-core's C binding, ext/core (the 2026-10-21 coverage push). 2026-10-21 06:21:17 +03:00
deps.mk make audit, a RustSec dependency audit in the gate (the user, 2026-10-27): tools/cargo-audit.sh checks every Cargo.lock of the crates, tacky-core, tacky-crypto, tacky-keyring and tacky-omemo and their fuzz/ workspaces (found under tacky-*/, so a new one is covered), with cargo-audit 0.22.2, the project's static musl release pinned by version and sha256 in docker/linux.Dockerfile. The advisory database is a shallow clone fetched afresh at each run into _build/advisory-db; a fetch that fails fails the audit, saying nothing was audited and how old the cached copy is, and AUDIT_OFFLINE=1 audits against that copy. A vulnerability advisory fails; unmaintained, unsound and yanked ones are printed as warnings. audit.toml is the allow-list, empty, its entries passed as --ignore, and one without an id, a reason or a YYYY-MM-DD date fails the audit. docker/gate.sh (make gate) runs it before the lanes, a line per finding in its summary and the log in _build/gate/audit.log, GATE_AUDIT=0 leaving it out. README's target table and ROBOTS.md's Build entry say so. On develop it fails today, and the bumps are left for a decision: tacky-core on rustls 0.23.45 (RUSTSEC-2026-0346, -0347, -0348, -0349; fixed in 0.23.46), rustls-webpki 0.103.15 (RUSTSEC-2026-0382; 0.103.16), webpki-roots 1.0.9 (RUSTSEC-2026-0381; 1.0.10), time 0.3.45 (RUSTSEC-2026-0009; 0.3.47) and hickory-proto 0.24.4 (RUSTSEC-2026-0119; 0.26.1), its fuzz lock on the same but time and webpki-roots; tacky-crypto and tacky-omemo and their fuzz locks on crypto-bigint 0.6.1 (RUSTSEC-2026-0387; 0.7.6); tokio 1.53.1's RUSTSEC-2026-0386 (unsound) warns; tacky-keyring is clean. 2026-10-27 12:05:28 +02:00
fuzz.mk Telegram secret chats' cryptography is tacky-crypto's, for the user's "Let's implement telegram secret chats" (2026-10-25): src/mtproto.rs has MTProto 2.0's end-to-end primitives as the design's frozen §3.1 names them, bytes in and bytes out since the shared key outlives the process and the caller stores it anyway. dh_check makes security_guidelines' checks of the server's p and g and a peer's g_a or g_b (p a safe prime of exactly 2048 bits by crypto-primes' Baillie-PSW, g one of 2 to 7 with its condition on p, the peer in [2^1984, p - 2^1984]), skipping the primality tests for Telegram's known good prime, whose SHA-256 is built in, and for the last eight primes verified in the process; dh_keypair draws a as the OS's 256 bytes XOR the server's random and gives g^a, always in the range a peer accepts; dh_agree gives peer^a zero-padded to 256 bytes, both over crypto-bigint's constant-time Montgomery exponentiation; key_fingerprint is SHA-1's last 64 bits little-endian; seal and open are the message format (msg_key from SHA-256 of key[88+x..] and the plaintext, the KDF's AES key and IV, AES-256-IGE over length, TL and 16 to 79 bytes of random padding), open comparing msg_key in constant time and refusing a wrong key, a length past the end and padding outside 12..1024 alike; Ige is AES-256-IGE streamed in whole blocks for files, wiping its chain and schedule on drop, and file_fingerprint is md5(key||iv) folded to an int32, the one use of the new md-5 dependency (added to the four lockfiles that carry this crate). The C ABI is tacky_mtproto_* in src/ffi.rs and tacky_crypto.h (seal wants tl_len + TACKY_MTPROTO_SEAL_OVERHEAD up front since its padding is random), and the Tcl binding tacky::crypto::mtproto::{dh_check,dh_keypair,dh_agree,key_fingerprint,seal,open,ige_new,ige_update,ige_free,file_fingerprint} in ext/crypto/crypto.c with an igeN handle table, dh_check's errors naming the failure ("p is not a safe prime", "g 5 needs p mod 5 in {1,4}", "peer value out of range") and open's one text for all ("message key mismatch"); README documents them. Vectors come from a second implementation, tests/client/fixtures/mtproto/gen.py (hashlib, tgcrypto 1.2.5's IGE, Python integers, the KDF written out from the formulas; run in a throwaway python:3.11-slim container), whose vectors.txt is checked in beside it and copied into tacky-crypto/tests/mtproto/ so the crate stays whole: IGE, both fingerprints, the KDF on both sides, whole messages at padding 12, 28, 79 and 1024, forged framing a receiver must refuse or take, D-H over Telegram's prime with g = 3, RFC 3526's 2048-bit prime with g = 2, a prime whose (p-1)/2 is composite and a composite p; IGE also against OpenSSL's test/igetest.c vectors (AES-128). 23 unit tests in mtproto.rs (every padding length from 12 to 1024 round-trips on both sides; 100 % of its lines covered, unioned as tools/cov-union.py counts), 5 C ABI tests in tests/mtproto.rs, a wipe test for the plaintexts and the IGE state, 86 Tcl checks in test_crypto.tcl; a new fuzz target crypto_mtproto (open under the vectors' key from both sides, dh_check and dh_agree on arbitrary p and peers, IGE in arbitrary pieces) with seeds from tools/fuzz/seeds.py, three minutes without a finding. cargo test 133 passed, test-rust 777, make test-crypto, check-native-neutral and check-api pass, the unit run 6148 passed, 0 failed, 10 skipped. 2026-10-25 20:15:07 +02:00
LICENSE libtacky v0.9.9-alpha3: the whole tree as of this release, in one commit (the user chose to fold the history once, at this tag, so the forge's commit list is readable; from here on every branch lands as one squashed commit). The full history up to this point, 1,000-odd commits with the v0.9.9-alpha1 and v0.9.9-alpha2 tags, is kept unchanged on the branch archive/pre-alpha3 (43b06d4), whose tree is byte-identical to this one. 2026-10-02 02:46:29 +03:00
Makefile make gate (docker/gate.sh) runs the release gate from the host with its independent parts at once: both MEDIA_BACKEND lanes side by side, webrtc (all test wasm REQUIRE=1) in _build/ and dist/ as before and datachannel (all test REQUIRE=1 MEDIA_BACKEND=datachannel) in _build-dc/ and _build-dc-dist/, mounted over /src/_build and /src/dist in its own container so every path the build names is the same and nothing of the other lane's is in reach (seeded from _build/ once, incremental after), then make test-tacky-rigs; each lane is the same goals and variables as when run alone, and neither switches the media binding, the archive and dist/ back and forth any more. GATE_VARS adds variables to both, GATE_RIGS=0 and GATE_LANES=0 leave a part out; logs in _build/gate/ and _build/shards/, and a one-line summary per run.tcl run and the Rust totals per lane. Measured at JOBS=8 beside tonight's other gates: the two lanes one after the other in one tree 98 s + 129 s (and 114 s to switch it back), at once 127 s; both lanes 6317 total, 6316 passed, 0 failed, 1 skipped and Rust 777 passed. ROBOTS.md says so, and that run.tcl -integration takes several suites and the rigs can be sharded. 2026-10-26 03:34:59 +02:00
README.md 1.1.18: the README's version for the tree to be tagged v1.1.18 (the user, 2026-10-31: "then build and release"; the app pins libtacky and the avatar-after-name report is ours), on 1.1.17 + fix/avatar-after-name: set_name, clear_name, set_nick, set_fields and set_photo read our vcard-temp again before they write it, so a name change on a server that converts avatars (XEP-0398, ejabberd mod_avatar as production runs it) no longer writes back a stale card without the PHOTO and has the server delete the XEP-0084 avatar for everyone; avatar rereads the card after a publish or disable the server converts, so the presence photo hash follows a picture change; the ejabberd rig loads mod_avatar and mod_vcard_xupdate and four integration cases keep the avatar across each way of changing the name. tools/stats.py --readme --cov _build/cov regenerated with the main .git mounted, coverage from make coverage-all on b297863b (Tcl 98.7 % procs, Rust 99.5 % lines, C 99.0 % lines; its tests/run.tcl 6988 total, 6978 passed, 0 failed, 10 skipped). Gate: a fresh build, tests/run.tcl -j 32 6988 total, 6978 passed, 0 failed, 10 skipped; test-rust 787 passed, 0 failed, 5 ignored; check-api, check-headers, check-native-neutral and wasm clean; integration on ejabberd 26.09 (mod_avatar, mod_vcard_xupdate) with omemo-bot: test_catchup, test_message, test_omemo, test_omemo2_interop, test_mucsub, test_album, test_sm_window, test_muc_captcha, test_muc_invite, test_muc_edit, test_register, test_gateway_register, test_avatar, test_jinglefile_s5b 86 passed, 0 failed, 3 skipped, test_tacky_avatar 5 and test_vcard4 4 passed; test_muc_captcha also on anotherim-server's room-pow image with and without captcha_cmd, 3 passed and 1 skipped each; test_avatar on Prosody 11 passed. 2026-10-31 10:15:48 +02:00
release.mk Comments, build files and the crates' docs no longer cite documents this tree does not have: TRANSFER.md, HTTP-TCL.md, LOCALSTORE.md, ARCHITECTURE.md (the workspace's §9.1 and §9.2), MEDIA.md, JINGLE-FT.md, LIVENESS.md, NOTIFY.md, CHATLIST.md, THREADS.md, STORAGE.md, CHAOS-FINDINGS.md, ONE-CLIENT.md, ROADMAP-another-tacky.md, docs/DEPLOY.md, docs/STABILIZE.md, docs/discovery.md, docs/storage-lock.md, reports/OMEMO_issues.md and reviews/2026-09-*, nor ROBOTS.md by section numbers it lost long ago (§2.2, §7.1, §8, §10.80 and the rest). Each pointer now names the place that exists (tacky/http/README.md, tacky-keyring/README.md, ROBOTS.md's HTTP, "The store", "The shape" and rule 2 or 6, tacky_media.h's data channel section, session/liveness.tcl, modules/file.tcl, ext/common.h's rule 1, deps.mk's and wasm.mk's own sqlite3 paragraphs) or is dropped where the sentence already says what the document did; generated outputs (dist/HANDBOOK.md, REPORT.md under COV_DIR) and the guides' pattern are left as they are. Not touched, being other branches' files: tacky/xmpp/modules/ (about 110 such citations), ext/core/keyring.c and keyring.h, ext/core/tests/test_tls.tcl, tacky/core/keyring.tcl and storage.tcl, and the schema's doc strings. Only comments changed; check-native-neutral, check-headers, check-api and test-tacky are green. 2026-10-07 20:42:28 +03:00
ROBOTS.md A room's proof of work is solved only while our join to it is in flight (audit 2026-10-30 A1 M1; the user: "fix all"): captcha SolvesFor answers 1 for our own server or account and for a room muc IsJoining names (its JoinTimers entry, which HoldJoin keeps), so a room we are in already or have only bookmarked, which could keep a core at 100 % for 90 s per challenge for as long as it liked, gets the host's <Challenge> as in 1.1.15. The join's end stops the solve: muc:<Joined>, <Left>, <Error> and an internal muc:<Leaving> that leave now publishes before the room's echo. Schema, features.md, ROBOTS.md and captcha.tcl say so. Tests: test_captcha 33 of 33, four new (a joined room's and a bookmarked room's proof of work are the host's, leaving and a refused join stop the solve, hashcash::Solves empty), all four failing before; test_muc 299 of 299; check-api clean. 2026-10-30 17:25:09 +02:00
tcl.mk AUDIT-2026-10-25 #27 (Low): the shim is compiled again in the same build whose flags changed, and the Windows build catches a stale shim: tcl.mk puts the shim.flags comparison on the file target itself (a recipe run every time through a phony prerequisite, rewriting the file only when the flags differ), so GNU make judges shim.o against the file's new mtime in that run; with the recipe on the phony shim-flags and none on the file, a scratch Makefile of the same shape under GNU Make 4.4.1 compiled one build late (FLAGS=x, then y: no compile; the next y: compile), and with the fix compiles at the change. docker/in-image-windows.sh also requires the archive's shim.o member to reference Tackymedia_Init (U), since libtackymedia_ext.a defines it whether or not the shim registers it. No test suite covers build rules (no tool tests): checked by the scratch Makefile, by make _build/shim.o with EMBED_CFLAGS changed (compiles at each change, not on a repeat), and by the awk guard over the Linux archive's nm output (finds the U); the Windows lane itself was not run. 2026-10-26 00:38:41 +02:00

libtacky

An XMPP client library in Tcl 9, Rust and C. It holds the protocol, the local store, end-to-end encryption, calls and file transfer. Apps talk to it through one JSON API. Tcl programs and gateways load its packages directly.

Version 1.1.18. Licence GPL-3.0.

What you get

  • dist/libtacky.a: the whole library, Tcl included, behind embed/tacky.h.
  • dist/tackyd-json: the same library over stdin and stdout.
  • A wasm build for a browser worker (make wasm).
  • Builds for Linux, Windows, Android and iOS (platforms/).
  • Tcl packages under tacky::*: session, client, component, HTTP.
  • dist/tackyd: runs a Tcl program with no Tcl installed.
  • Native crates with C ABIs: tacky-core, tacky-crypto, tacky-keyring, tacky-omemo, tacky-media.

The API is described once, in schema/tacky.schema.json. The handbook (make handbook) and the host bindings are generated from it.

Features

  • One SQLCipher store per account, keys in a sealed keyring
  • A message is acknowledged only after it is stored
  • SASL2, Bind 2, FAST, SCRAM with channel binding
  • Stream management with resume, CSI, reconnects
  • TLS through rustls, certificate pins, no plaintext fallback
  • STARTTLS, direct TLS and WebSocket
  • SOCKS5 proxy, Tor and I2P per account
  • OMEMO 0.3 and OMEMO 2 under one identity
  • OTR v3 secret chats with SMP
  • Telegram secret chats through a gateway
  • Group chats (MUC), bookmarks, archive (MAM) with catch-up
  • Replies, reactions, corrections, retractions, mentions
  • Polls, stickers, silent and scheduled messages
  • HTTP upload and Jingle file transfer, encrypted
  • Audio and video calls, group calls (Muji)
  • XEP-0114 components for gateways and bots

docs/features.md has one row per specification with its status. Our own extensions (urn:tacky:*) are specified under docs/protocol/.

status meaning specifications
🔵 MOSTLY implemented and tested, not checked by hand against other software 148
🟡 WIP partly implemented or partly tested 17
🔴 TODO not implemented, at most reserved in the API 22
🟢 DONE checked by hand; set by a maintainer 0

How it fits together

 app (libtacky.a), tackyd-json, or a browser worker (wasm)
   │  JSON  ["module","method",{args},token] →   ["result"|"error",token,…] ←
   ▼
 embed/tacky.c      one backend thread, one Tcl interpreter
   ▼
 tacky/core/wire    dispatch through the schema's allow-list
   ▼
 tacky/xmpp/client  one module per area (account, roster, message, muc, …)
   ├── tacky/xmpp/session   the XMPP session, in Tcl
   │     └── tacky::core    tacky-core (Rust): DNS, connect, TLS, XML
   ├── tacky/core/storage   SQLCipher store per account
   ├── tacky::core::keyring tacky-keyring (Rust): the key vault
   ├── tacky::omemo         tacky-omemo (Rust): the OMEMO ratchet
   ├── tacky/http           HTTP/1.1 in Tcl
   └── tacky::media         calls: webrtc | datachannel | host | remote

 tacky/xmpp/component      an XEP-0114 component, no client loaded
  • Rust reads untrusted bytes and holds keys: the stream, TLS, parsers, crypto, OMEMO.
  • C binds each crate to Tcl, and runs media (libwebrtc or libdatachannel).
  • Tcl is everything that means something: the session, the client, the store, HTTP.

Calls use one native media backend per build, chosen by MEDIA_BACKEND. ROBOTS.md has the full layout and the reasons behind it.

component language what it is code tests coverage
core Tcl tacky/core: the wire API dispatch, the per-account store, the keyring layer 2,404 1,737 98.9 % procs
C ext/core: the Tcl binding of tacky-core and the keyring; the SQLCipher crypto provider 3,901 3,435 99.3 % lines
Rust tacky-core: DNS, TCP, TLS (rustls), WebSocket, the XML stream, the C ABI; tacky-keyring: the key vault 12,546 7,527 99.3 % lines
crypto C ext/crypto: the tacky::crypto binding 1,369 676 99.7 % lines
Rust tacky-crypto: hashes, HMAC, HKDF, PBKDF2, AES, X25519 with a C ABI 2,543 2,929 99.9 % lines
http Tcl tacky/http: HTTP/1.1 client for uploads and downloads 1,065 2,194 100.0 % procs
media Tcl tacky/media: the call media contract and its backends 1,004 1,895 96.4 % procs
C ext/media: the binding; tacky-media: the media C API over libdatachannel, with audio (Opus) and video (VP8) engines 9,723 10,340 --
C++ tacky-media/webrtc: the same C API over libwebrtc 3,723 271 --
omemo C ext/omemo: the tacky::omemo binding 921 398 98.6 % lines
Rust tacky-omemo: X3DH, Double Ratchet, XEdDSA; OMEMO 0.3 and OMEMO 2 1,983 3,943 100.0 % lines
xmpp Tcl tacky/xmpp: the session, the client modules, the XEP-0114 component, the in-process test driver 72,383 119,604 98.8 % procs
everything else C, Tcl, Python, sh, make, CMake embed/, examples/, tools/, test runner, build files; not in the total 10,354 5,125 --
total (without everything else) Tcl 76,856 · C/C++ 19,637 · Rust 17,072 113,565 154,949 Tcl 98.7 % procs · Rust 99.5 % lines · C 99.0 % lines

code: lines of code, no blanks, comments or generated files. tests: lines of test code for that part. The total leaves out everything else. coverage: Tcl counts procedures called, Rust and C count lines run, from the make coverage-all run on 2026-10-31 at commit b297863b.

The media C and C++ have no coverage figure: the coverage run uses the default webrtc build, and libwebrtc is not measured.

Status and tests

The API is stable. A change that breaks a host comes with a new major version.

Unit suites run against in-process peers: a fake server with real SCRAM, TLS and WebSocket, a stream relay, fault shims for the C bindings, and reference vectors for the ratchet. Integration suites (tests/integration) run against a real server in a container, with OMEMO echo bots beside it.

Integration results, measured on v1.1.10 (2026-10-27), three rigs per server at once:

server passed failed skipped
Prosody 13 207 1 37
ejabberd 26.09 229 1 15
MongooseIM 6.10.0 160 0 49

MongooseIM's row is from v1.1 (2026-10-23); it was not rerun since. Skips are features a server or its rig lacks. The one failure on each server was a disappearing-messages test still expecting the old 60 s floor for a peer's timer; fixed in this release, it passes on both.

Nothing has been checked by hand against public servers or other clients yet. That is why no feature is DONE.

113,565 lines of code, 154,949 lines of tests (11,577 of them integration suites), 9,153 test cases. Coverage: Tcl 98.7 % procs · Rust 99.5 % lines · C 99.0 % lines (make coverage-all on 2026-10-31 at commit b297863b).

suite family files cases
Tacky unit suites (tests/client, tests/http, tests/tcl) 227 6909
Tacky integration suites (tests/integration, need a server) 44 255
Rust: tacky-core 53 431
Rust: tacky-keyring 6 52
Rust: tacky-crypto 21 134
Rust: tacky-omemo 15 176
Tcl extensions (ext/*) 19 1152
C: tacky-media: libtackymedia, rtc-ma, rtc-mv (ctest programs) 41 41
C++: tacky-media: webrtc (ctest programs) 3 3
total 429 9153

make docs regenerates these numbers. Coverage comes from the last make coverage-all; its full report is _build/cov/REPORT.md.

Building

Everything builds in libtacky's container image. The machine needs git and docker or podman.

git clone --recurse-submodules ssh://git@dev.another.im:3022/another-im/libtacky.git
cd libtacky
make all test               # build, run every suite that needs no server
make all test REQUIRE=1     # the gate: a missing prerequisite fails

MEDIA_BACKEND is webrtc (default), datachannel or none.

target what it does
make, make dist build everything; dist/libtacky.a
make tackyd-json, make tackyd the two executables
make test every suite that needs no server
make test-tacky-prosody, -ejabberd, -mongooseim integration suites against a server in a container
make coverage-all the coverage run behind the table above
make audit every Cargo.lock against the RustSec advisory database (audit.toml is the allow-list); in make gate
make docs handbook, README numbers, DOAP file, Rust API docs
make help every target

More in docs/building.md.

Using it

An app links embed/tacky.h and speaks JSON:

tacky *tacky_create(const char *const *backend_args, tacky_emit_fn emit, void *ud);
void   tacky_send(tacky *t, const char *json, size_t len);   /* ["module","method",{args},token] */
void   tacky_destroy(tacky *t);                              /* replies and events arrive at emit */
void   tacky_destroy_wait(tacky *t, unsigned int max_ms);

A Tcl program loads the packages, from a built checkout or through dist/tackyd:

package require tacky::xmpp::component
set h [tacky::xmpp::component new -domain gateway.example.org -secret $secret \
           -host 127.0.0.1 -port 5347 -command onEvent]
tacky::xmpp::component connect $h

examples/component/echo.tcl is a complete component. The handbook (make handbook) documents every method, event and Tcl command.