Repository navigation
Conversation
Bilb
added a commit
that referenced
this pull request
Oct 15, 2026
session-ops-discord.service answers four slash commands over Discord's HTTP interactions, behind nginx at /discord/interactions: - /run job:<job> starts a job jobs.toml marks `discord = true` by writing /run/session-ops/requests/<job>, which the job's path unit watches. It is refused while that job or a queued one is running or waiting, or when the queue would start before the job's timeout. - /mau [month:YYYY-MM] starts mau the same way, leaving it the interaction to answer in its inbox. The figures go to the channel as a follow-up; a missing export, a bad month or a failure is told to the person asking alone. - /mau-upload file:<csv> checks a Play Console or App Store Connect export and puts it in mau's inbox, telling the uploader alone whether it has last month's final figure. - /versions shows store-versions' last read of each platform, to everyone. Each interaction is signature-checked, then refused outside DISCORD_GUILD_ID, outside ALLOWED_CHANNEL_IDS when set, and, /versions aside, for anyone not in ALLOWED_USER_IDS or ALLOWED_ROLE_IDS. The relay runs as opsbot, which starts no unit and can write only the requests directory and mau's inbox. mau merges every export into a daily history.json and, once both stores have a final figure for the month's last day, answers with Android (Play, plus GitHub APKs and an F-Droid estimate), iOS scaled by Apple's opt-in rate from the App Store Connect API, Desktop downloads from GitHub and Flathub, and the total. It has no timer, so the silence checker leaves it alone. release-stats is folded into it. jobs.toml gains `discord` and `enabled` (default true); `session-ops list --ready` covers jobs a path unit starts, and `--timers` the ones with a timer of their own. The queue timer loses its random delay, so /run knows when the queue next starts.
Bilb
force-pushed
the
feat/platform-stats
branch
from
October 16, 2026 08:29
f24e162 to
53eee94
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Four Discord slash commands:
/maufor monthly active users per platform,/mau-uploadto hand it the store exports,/runto start a job now, and/versionsfor each store's Session version against GitHub's./mau: monthly active usersNo timer: mau runs only when someone asks with
/mau(optionallymonth:YYYY-MM). The silence checker leaves it alone.Answers
alerts.env's channel, a systemd stop at the 5-minute timeout included.Sources
/mau-upload.release-statsis removed: this is now the one place Desktop downloads are counted.Flow
inbox/request.json(the interaction's application id and token, and the month) and asks for the unit as/rundoes. It acknowledges Discord with an ephemeral deferral, and removesrequest.jsonif the unit did not start.request.json, checks its shape (digits for the id, no/in the token), merges every export ininbox/into a dailyhistory.jsonand files them indone/, then answers by editing the acknowledgement and, for the figures, posting a follow-up. It retries a 404, since its first edit can reach Discord before the relay's acknowledgement. The token never reaches an alert or the journal.Discord commands
/run job:<job>startssession-ops@<job>.serviceby writing/run/session-ops/requests/<job>, which the job's path unit watches withPathChanged=, then waits up to 1.5 s for the unit's start time to change. Offered for thediscord = truejobs other than mau:crowdin-syncandcrowdin-duplicatesfor now. Refused while that job or a queued one is running or waiting, or when the queue's next run is sooner than the job's timeout. The check and the start share one lock. The queue timer loses its random delay, so its next run is the scheduled time./mau-upload file:<csv>downloads the attachment from Discord's CDN, checks it withmau.parse_export, and only then renames it into mau's inbox. It replies to the uploader alone, saying whether the export has last month's final figure./versionsreplies in the channel with one line per store-versions platform from itsstate.json, which store-versions now writes 0644 for the relay. Read-only, so it is visible to everyone and skips the user and role allowlist.What the app sees: HTTP interactions only,
applications.commandsscope, no gateway./run,/mauand/mau-uploadare registered hidden from everyone but server administrators until Server Settings → Integrations grants them to a role.Gates
DISCORD_GUILD_ID(DMs refused), and the channel inALLOWED_CHANNEL_IDSwhen that is set.ALLOWED_USER_IDS/ALLOWED_ROLE_IDS; both empty refuses everybody./versionsskips this check.opsbot, which cannot start a unit. It can write only/run/session-ops/requests(root:opsbot 0770), where eachdiscord = truejob has aPathChanged=watch and nothing else, and mau's inbox (sessionops:opsbot 0770). No polkit.Registry and install
jobs.toml:discord, andenabled(defaulttrue; a job setfalsegets no units and is offered nowhere).session-ops list --readycovers jobs a timer, the queue or a path unit starts;--timerslists those with a timer of their own, which is allinstall.shenables timers for.install.sh: theopsbotaccount, mau's inbox, the requests directory, the path units of the readydiscord = truejobs, andsession-ops-discord.serviceoncediscord.envhas content.Deploy
/etc/session-ops/mau.env:ASC_ISSUER_ID,ASC_KEY_ID;/etc/session-ops/asc-key.p8, mode 600 (install.shcreates it empty, and an empty one fails the run naming it)./etc/session-ops/discord.env, then thelocation = /discord/interactionsblock fromnginx-webhooks.confcopied into the live file by hand, since certbot owns it, thensession-ops discord-registeronce. Steps indeploy/README.mdunder "Discord commands".Not yet done: the Discord side ran end to end against a test app (verification, registration, the gates,
/runreplies,/mau-uploadrefusals), before/mauanswered through the interaction. Nothing has run on the host. After deploying:systemctl list-units 'session-ops@*.path'lists crowdin-duplicates, crowdin-sync and mau, active./run crowdin-duplicatesstarts the job and its post appears./versionslists the seven platforms./mauwith no exports tells only you what is missing; with both, the figures appear in the channel.Tests
uv run --locked python -m unittest discover -s tests -t .(1008 OK, 1 skipped),ruff check .clean.