Skip to content
modcommunityPublic

About

A full-fledged tool for interacting with the TMC API through the CLI. Useful for automation and more.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Latest commit

 

History

24 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

A command-line tool for TMC's public API. It creates, edits and deletes your assets, mods, servers, articles, communities, collections and groups — and, because that is what the API is actually used for, it uploads files and cuts releases in one command.

Supports both authentication modes the API accepts: bearer tokens and Ed25519 signed assertions (JWT mode) — and reading public items with no key at all (--anon).

No dependencies. Python 3.11+ and the standard library. cryptography is used for signing if it happens to be installed, and the bundled RFC 8032 implementation is used if it isn't — same signatures either way.

Install

pip install .                 # gives you `tmc` on PATH
pip install '.[fast]'         # …plus `cryptography` for faster JWT signing

Or just run it from a checkout — there is nothing to build:

./tmc --help
python src/main.py --help

Log in

Keys are managed under Account → API Keys on the site.

# Bearer key (the default kind). Omit --token to be prompted without echo.
tmc auth login --token tmc_xxxxxxxxxxxx

# JWT key: you hold the Ed25519 private key, the site holds only the public half.
tmc auth login --jwt --key-id tmcak_xxxxxxxx --private-key ~/keys/tmc.pem --copy-key

# Several sites or several keys:
tmc auth login --profile staging --base-url https://staging.example.com --token tmc_…
tmc auth use staging

Credentials live in ~/.config/tmc/config.json (mode 0600). TMC_TOKEN, TMC_KEY_ID, TMC_PRIVATE_KEY_FILE, TMC_BASE_URL and TMC_PROFILE work too, which is usually what you want in CI.

Every environment override (a flag beats the environment, the environment beats the profile):

Variable Meaning Default
TMC_BASE_URL API origin https://api.moddingcommunity.com
TMC_SITE_URL website origin (tmc open, tRPC for defcon) base URL minus a leading api. (https://moddingcommunity.com)
TMC_STATUS_URL status site origin for defcon (https://status.moddingcommunity.com) none: the site's own forwarding answers
TMC_PROFILE profile to use the config's default
TMC_TOKEN bearer key
TMC_KEY_ID, TMC_PRIVATE_KEY, TMC_PRIVATE_KEY_FILE JWT key id and PEM (inline or path)
TMC_JWT_LIFETIME assertion lifetime in seconds 60
TMC_APP_TOKEN app-user token for friends, party, … (used as-is, no refresh)
TMC_CONFIG_DIR where config.json lives ~/.config/tmc
TMC_COLOR always forces colour when piped auto (TTY)

Check what you have:

tmc auth whoami       # which key, and what the server lets it do
tmc auth doctor       # crypto backend, config permissions, connectivity

whoami establishes permissions with three deliberately harmless probe requests (a read, a create that cannot validate, a delete of an empty id list). Nothing is created or deleted. Pass --read-only to send only the read.

Reading without a key

GET on a content item answers with no Authorization header at all, for items that are completely public. --anon is how you ask for that:

tmc mod get 123 --anon
tmc mod list --anon --app 4 -o json
tmc article list --anon --official -o json    # the blog

What comes back is a summary — id, name, slug, path, description, app, categories, engagement counts, plus a live server block for servers — not the record. It is a genuinely different endpoint, so a few things follow:

  • Seven types only: asset, mod, server, community, article, collection, group. Relations, files, comments and reviews always need a key.
  • Two list filters, --app and --official, each only on the four types whose model has the column. --search, --tag, --category, --mine are dropped with a warning rather than silently ignored — the server would ignore them, and a filter that did nothing looks exactly like one that matched everything. Both are anonymous-only, and the keyed list warns the same way in the other direction.
  • Read-only. Any write is refused before the request leaves, since sending it would come back as a 401 that blames a key you did not send.
  • Its own quota, counted per source address and deliberately small. A key is free and has a much larger budget.
  • Three gates, any of which can turn it off: the site's own switch, the item being completely public (not hidden, not NSFW, no hidden parent), and the item's team not having set apiPublic: false. Only the third is told apart from "no such item" — you get a 403 api_disabled rather than a 404.

tmc mod update 5 --set apiPublic=false is the other side of that switch. (Servers are the one type that does not have the field.)

Everyday use

tmc mod list --mine --all                       # every page of your own mods
tmc mod list --search rust --tag pvp -o json
tmc mod get 42
tmc mod update 42 --set description="Now with fewer bugs"
tmc mod delete 42
tmc open mod 42 --browser                       # its page on the site

open prints the page's full address and, with --browser, opens it. The address is the site's own where it gives one; otherwise it is built from the record (a keyed record's url field is its slug, not a page). Comments, reviews, media and releases go through the site's /i/<type>/<id> permalink. Relative addresses are resolved against the website origin: --site-url, TMC_SITE_URL, a profile's site_url option, or the base URL minus a leading api. (api.moddingcommunity.com → moddingcommunity.com).

ls is an alias for list, and get with no id lists as well, so whichever of the three you reach for first is the one that works. help after a command is the same as --help on it: tmc mod help, tmc rel add help.

Creating something, with the body read from a file and the icon uploaded on the way through:

tmc mod create \
  --set name="My Mod" --set appId=1 \
  --content-file README.md \
  --icon assets/icon.png \
  --tag tooling --tag gamedev

--set coerces to the field's real type (the API's schemas are strict and do not coerce, so appId=1 has to become a number, not "1"). --set-json takes a JSON literal, --set-file reads a value from disk, and --json supplies a whole body that --set flags are then applied on top of.

Unknown field names are caught locally, with the near-miss:

$ tmc mod create --set tgs=pvp …
error: 'mod' has no field 'tgs'. Did you mean 'tags'?

Run tmc schema mod for the field list, or tmc schema for everything. Run tmc contract sync first and both answer from the live site rather than from this build's mirror — see Staying in step with the site.

Releases and files

This is the part worth having a tool for. release publish uploads the files, collects their ids, reads the existing release set and writes the release with everything attached — in the right order, without disturbing your other releases:

tmc release publish --mod 5 \
  --version 1.2.0 \
  --title "Bug fixes" \
  --content-file CHANGELOG.md \
  --file 'dist/*.zip' --file dist/checksums.txt

Run it again with the same --version and it updates that release, merging the new files into the existing set (--replace-files to swap them out instead). Other releases on the item are never touched, and a hidden release stays hidden — the API resets hidden when an update omits it, so publish always sends it back explicitly.

tmc release list  --mod 5
tmc release files --mod 5 --version 1.2.0
tmc release rm    --mod 5 --version 1.0.0

Files on their own:

tmc file upload 'dist/*.zip'          # batched past the API's 20-per-request cap
tmc file upload big.zip --raw         # body-is-the-file form
tmc file get <id>
tmc file update <id> --title "Renamed"
tmc file download <id> -O ./here/
tmc file rm <id>

Per-file size limits are the key owner's own — 20 MB standard, 1 GB for supporters. Anything over 20 MB gets a local warning; the server has the final say and answers 413.

Relations

Relations (tags, media, releases, links, sources, items) can be managed without re-sending the whole item. The verbs say what they do:

Command Method Meaning
tmc rel add POST merge these in, leave everything else
tmc rel set PUT this is now the complete set
tmc rel rm DELETE drop the ones you named
tmc rel clear DELETE drop all of them
tmc tags  add mod 5 pvp vanilla
tmc tags  rm  mod 5 pvp
tmc media add mod 5 --file shot.png --title Screenshot   # uploads, then attaches
tmc media add mod 5 --url https://example.com/a.png
tmc links add mod 5 https://github.com/me/repo --type GITHUB

tmc rel get mod 5 releases
tmc rel add mod 5 sources --member '{"sourceId": 3, "path": "/mods/mine"}'
tmc rel set mod 5 media --from-file gallery.json

tmc template media prints a starter array for --from-file.

The same warning the API's docs give applies to the item body: setting --tag, or a media/releases array via --set-json, replaces that relation wholesale. The CLI says so before it sends.

Bulk

tmc mod create --from-file mods.json      # any length; batched into 25s
tmc mod update --from-file edits.json     # each element needs an "id"
tmc mod delete 1 2 3 …                    # batched into 100s
tmc mod list --mine --all -o ids | xargs tmc mod delete

Bulk writes are not transactional server-side. If a batch fails partway, the CLI reports the failing element's index and lists what did get written, so you can resume rather than start over.

Output

-o table (default), json, jsonl, csv, tsv, yaml, ids. --field id,name keeps only the columns you want.

Data goes to stdout, progress and errors to stderr, so pipes stay clean:

tmc mod list --mine --all -o ids | xargs -n1 tmc mod get -o json

Exit codes: 0 ok, 2 usage, 3 auth (401/403), 4 not found, 5 validation (400/413), 6 rate limited, 7 server error, 8 network.

What it handles for you

  • A fresh JWT assertion per request attempt. A jti is consumed on first use, so a retry that resent the same header would be a 400. Signing happens inside the retry loop.
  • Rate limits. A 429 carries retry-after where the API sets one and names its own wait in prose where it does not; the CLI waits exactly that long, up to --retry-wait-max (120s), then tells you how long is left instead of hanging a build. 5xx and connection failures get exponential backoff, and only on idempotent methods — a POST is never silently repeated.
  • Every server cap. 25 items per bulk write, 100 per bulk delete, 200 relation members on a PUT/POST, 500 keys on a relation DELETE, 20 upload parts. Pass whatever you have; it gets batched.
  • Pagination. --all walks it, --max stops early.
  • Streaming uploads. A 1 GB file is read from disk in chunks, not into RAM.

Staying in step with the site

This CLI mirrors the API's field list locally so it can catch typos before spending a request — which means it can be one deploy behind. The site publishes what it actually accepts, so the mirror does not have to guess:

tmc contract sync          # fetch it (no key needed) and cache it
tmc contract show          # what is cached, and whether it is in use
tmc contract drift         # what the site has that this CLI does not
tmc contract clear         # forget it

After a sync, --set coercion, the unknown-field check, tmc schema and the completion scripts all answer from the site's field list instead of this build's. The commands do not change: --help has to work with no network, and a tool whose command list depends on the last successful fetch is a tool whose --help differs between two machines. So sync keeps this accurate about fields, and drift tells you when the tool itself needs a release.

A cached contract is used only against the site it came from, and only for 30 days. Two profiles pointed at production and a dev checkout have different registries, and judging one by the other's field list is worse than having none.

drift exits 3 when the two disagree, so CI can run it:

tmc contract drift --fetch --base-url https://moddingcommunity.com

It reports three things separately, because they mean different things:

It says What to do
fields the site has that this CLI does not nothing — a synced contract already covers them
commands the site has that this CLI does not release this CLI; tmc raw reaches them today
fields this CLI is stricter about nothing — this CLI refuses a float for appId and the server would take one

The document is GET <base>/api/content/spec and needs no credential — it is the one thing a fresh install runs before tmc auth login. Its shape is in cli-contract.md.

Escape hatch

Nothing is ever blocked by a stale mirror, contract or no contract:

tmc mod create --set someNewField=1 --allow-unknown-fields
tmc raw PUT /api/content/mod/5/releases --json @releases.json

There is a second implementation of this same command grammar: the web console at /console on the site, and in the corner of every page. It runs as your signed-in session rather than as a key, so there is nothing to log into — and it is the thing the contract keeps this tool in step with.

The public catalogue

tmc catalog reads what a visitor sees, from the app API's public half (/api/app/v1). It sends no credential — so it works before tmc auth login — and it covers what the content API does not hand out: categories, tags, owners, download/rating counts, dependencies, reviews, the games list.

tmc catalog browse mod --app 3 --category 7 --tag 1 --tag 2 --sort downloads
tmc catalog browse server --filter onlineOnly=true --filter os=LINUX --all --max 200
tmc catalog show mod 42                          # summary + counts
tmc catalog show mod 42 --part dependencies      # or releases, media, links
tmc catalog facets mod                           # game/category ids to filter by
tmc catalog reviews mod 42 --sort helpful
tmc catalog games --search seed
tmc catalog build 1 --platform LINUX_X64         # the installable native build of a game (null = none)
tmc catalog lookup play.example.com --port 27015 # listed servers at an address
tmc catalog listing --app hungario --all          # the address-only server listing a game's browser reads

Kinds are the app API's: asset mod server serverMap article community collection user. Ids on this surface are strings.

As yourself: friends, parties, installs, …

The per-user half of the app API needs an app-user session, not a content key. tmc auth app-login gets one through the same device flow the desktop app uses: it prints a URL and a code, you approve it in a browser where you are signed in, and the session is saved in the profile beside any content key.

tmc auth app-login               # prints the URL + code, waits for approval
tmc auth app-login --browser     # …and opens the URL
tmc auth app-whoami              # GET /me
tmc auth app-logout              # revokes the session server-side, then forgets it

tmc friends list                 # with presence
tmc friends requests | add USER | accept ID | decline ID | cancel ID | remove USER
tmc presence get USER...         # friends only
tmc presence set in_game --server 12 --joinable --detail "de_dust2"
tmc party mine [--members]
tmc party create --app 3 --name "Tonight" --max-users 8
tmc party invite PARTY USER... | invites | accept INVITE | decline INVITE
tmc party join PARTY [--password …] | leave PARTY | kick PARTY USER [--ban]
tmc party ready PARTY [--not] | start PARTY [--server 12]
tmc party round PARTY             # the ready round
tmc party force-start PARTY | cancel PARTY | end PARTY      # host / co-host
tmc party role PARTY USER co-host|member | transfer PARTY USER
tmc party offer PARTY --server 12 | reserve PARTY --server 12 --minutes 60 [--private] [--note …]
tmc party reserve-cancel RESERVATION
tmc installs list | create --app 3 --name Main | update ID … | delete ID
tmc installs items add|set|remove INSTALL mod 42 [--enable|--disable] [--order N]
tmc subscriptions list | add mod 42 | remove mod 42
tmc subscriptions prefs mod 42 --no-auto-update --paused
tmc reviews write mod 42 --rating 5 --content "Works great"
tmc reviews vote REVIEW up|down|clear | delete REVIEW
tmc stats top 3 kills --limit 10 # a leaderboard, with your own rank
tmc stats me 3 | defs 3
tmc account settings | set notifyParties=false timezone=Europe/Berlin
tmc account downloads [--items]  # what your app devices are downloading
tmc account avatar -o json > me.json && tmc account avatar-set me.json
tmc report mod 42 --type spam --title "Scam links" --content "The description links to …"

The access token lasts an hour and is renewed automatically with the refresh token (once per command: when it is past its expiry, or when the site answers 401); if that fails too, the command says so and the session is dropped — run app-login again. A session is tied to the base URL it was issued by and is never sent to another one. TMC_APP_TOKEN supplies an access token directly (nothing is stored or refreshed). tmc review (singular) is still the content API's; tmc reviews writes as you. Routes only a game client calls (heartbeat, play launch, stats submit, …) are deliberately not commands; see docs/surfaces.md.

Network status (Defcon)

tmc defcon reads the public half of Defcon, the network monitor — exactly what the site's /status page shows. No credential. It asks the API origin's REST mirrors (/api/status/defcon, /series, /mtr, /incidents), so the base URL is all it needs; against a site from before those mirrors (a 404), or when --site-url is given, it asks the website origin's tRPC procedures (defcon.public.status|series|mtr) instead.

The status site (website-status) serves the same mirrors on its own host: --status-url, TMC_STATUS_URL or a profile's status_url option points the REST reads there (default: the API origin, which forwards). tmc defcon summary is the status site's one-call answer and needs that host.

tmc defcon status                # overall verdict, counts, open incidents
tmc defcon status --check        # exit 0 OK · 1 DEGRADED · 2 DOWN · 3 UNKNOWN
tmc defcon monitors --status DOWN --status DEGRADED
tmc defcon monitor homepage      # by id or (part of) name: per-node latency now
tmc defcon nodes                 # monitoring nodes, when the site publishes them
tmc defcon incidents             # open incidents
tmc defcon incidents --all       # the last 90 days, open and resolved
tmc defcon latency 3 --range week --summary   # per node: latest, mean, peak ms
tmc defcon latency 3 --range day --node frankfurt -o csv
tmc defcon mtr 5                 # latest traceroute of an MTR monitor

The site's own switches are honoured: when the status page hides nodes or incidents, so does this. Incident history (--all) goes back 90 days and needs a site with /api/status/defcon/incidents.

Shell completion

tmc completion bash > /etc/bash_completion.d/tmc
tmc completion zsh  > "${fpath[1]}/_tmc"
tmc completion fish > ~/.config/fish/completions/tmc.fish

Tests

python -m unittest discover -s tests

The tests run the real CLI against an in-process mock of the API over a real socket, covering both auth modes (including Ed25519 against RFC 8032 vectors and an openssl-generated key), the anonymous surface, batching, relation merge/replace semantics, the release workflow, the retry path, open, the public catalogue and Defcon (tests/test_public.py), and the app-user session: device login, refresh, and every per-user group (tests/test_app_user.py).

Which address it talks to

The default is https://api.moddingcommunity.com — the public APIs now live on an origin of their own. The old address still works: the apex proxies /api/content to the same service rather than redirecting to it, deliberately, because a cross-origin redirect strips the Authorization header and would have turned every keyed call into a 401. So a profile saved before the move keeps working and there is nothing to migrate; --base-url still points the tool at a staging site, a container or a dev checkout.

The docs print the short form api.moddingcommunity.com/content/..., but this tool sends /api/content/.... That is the route's real name — the short spelling exists because nginx rewrites the prefix back on, so it is the one that stops working the moment you point --base-url straight at a container or a dev checkout with no proxy in front of it. The prefixed form answers on every one of them.

Reference

The API this speaks to is documented in the website repo at docs/api/public-content-api.md; the implementation is under src/lib/api/public/.

tmc catalog speaks to the app API's public reads (src/app/api/app/v1/, query and response shapes in src/types/app-api/contract.ts); tmc defcon to src/app/api/status/defcon/ (tRPC fallback: src/server/api/routers/defcon/public.ts), described in docs/defcon.md. The command grammar the web console shares with this tool is published at /api/content/spec (docs/api/cli-contract.md).

About

A full-fledged tool for interacting with the TMC API through the CLI. Useful for automation and more.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages