Repository navigation
Conversation
release-stats also writes desktop-platform-totals.csv: Linux (GitHub .deb, .AppImage, .rpm, .freebsd plus Flathub installs since the release day), macOS (.dmg, .zip) and Windows (.exe) for the latest stable release.
The mau job merges each Play Console MAU export dropped into its inbox into a daily history and posts last month once its last day is in, with the change on the month before; from the 10th it posts a reminder until then. A job may now watch a glob under its state directory: a matching file starts it through a session-ops@<job>.path unit.
Desktop has no active-user count, so the post lists the latest stable release's downloads per platform and adds them to Android's MAU for the total. The timer moves to 11:00, an hour clear of the queue's posts.
The monthly post is now the one place Desktop downloads are counted, so release-stats and its per-release CSVs go.
A Discord app on webhooks.session.codes, answering two guild slash commands: - /run job:<job> starts session-ops@<job>.service for a job jobs.toml marks `discord = true` (crowdin-sync and crowdin-duplicates), refused while that job or any queued job is running or waiting to. - /mau-upload file:<csv> downloads the attachment, checks it with mau's own parser, and moves it into mau's inbox, whose path unit runs the job. The relay runs as opsbot behind nginx on 127.0.0.1:8081, with no gateway connection. It refuses a request that Discord did not sign, that comes from another server, or whose author is in neither allowlist. The polkit rule `session-ops polkit` writes from jobs.toml lets opsbot start the discord jobs and nothing else, and the mau inbox is the one path it can write.
/run refused while the queue was busy, but nothing stopped the queue's timer starting while a /run job was still going: /run crowdin-duplicates at 09:55 then ran beside the 10:00 crowdin-sync, two Crowdin clients against one rate limit. /run now also refuses when the queue timer's next run is sooner than now plus the job's timeout, since systemd kills the job by then. The next run comes from `systemctl list-timers --output=json`: on systemd 252, the host's version, `show --timestamp=unix -P NextElapseUSecRealtime` still prints local time. The check and `systemctl start --no-block` now run under one lock, so a second /run a moment later sees the first's job queued. A job's timeout is parsed as a systemd time span when jobs.toml loads, so a bad one fails there.
…the URL Discord checks the Interactions Endpoint URL with a request that, without the route, hits the catch-all 404, and it then refuses to save the URL.
default_member_permissions "0" hides a command from every member without Administrator; the owner and administrators still see it. The relay's allowlist refuses them like anyone else.
install.sh made every watched job's inbox writable by opsbot, though the relay writes only mau's. Other watched inboxes stay the job account's alone, at 0700. A test ties the relay unit's ReadWritePaths= and install.sh to mau's watch.
The doc page named only the busy check. The start lock holds only within one process, which is how session-ops-discord.service runs uvicorn.
/run refuses a job whose timeout would carry it past the queue's next run, read from list-timers. With RandomizedDelaySec, re-arming the timer draws a new delay, so the queue could start up to two minutes before the time /run read. On one host the delay spread nothing.
The App Store Connect API's App Sessions report counts a device once per app version, OS and territory it used in the month, so its sums run 5% to 20% above App Analytics. iOS therefore comes from the same kind of manual export as Android: the inbox tells the two apart by their columns, and a month posts once both have its last day.
iOS's opted-in devices are divided by the month's opt-in rate from the App Store Connect API's App Opt In report, named with its counts in the post. Android outside Play is one line: the latest release's GitHub APK downloads, and an F-Droid estimate of 10% of every other figure, since F-Droid publishes no counts.
/mau-upload is now the way exports reach the inbox: it takes either store's export and names the platform, the reminder points at it alone, and MAU_INBOX_HOST, which only fed the reminder's rsync line, goes.
- A failure before the inbox is filed moves its exports to rejected/, and an unreadable export is a rejection, so the path unit never restarts the job into its start limit. - Apple's fractional value for a day it is still counting is kept, and a fractional month end counts as missing; the reminder says so. - Revisions stay in history.json until the month posts, which lists those of the two month ends it compares and then forgets them. - An empty asc-key.p8 fails naming the file.
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.
A monthly Discord post of active users per platform, and two Discord slash commands:
/mau-uploadto hand that job its store exports, and/runto start a job now. Includes #72.mau: monthly active usersSources
/mau-upload. The job takes each month-end value..deb,.AppImage,.rpm,.freebsdand Flathub installs since the release day; macOS is.dmgand.zip; Windows is.exe. Desktop has no active-user count.release-statsis removed: the post is now the one place Desktop downloads are counted.Flow
/var/lib/session-ops/mau/inbox/, from/mau-uploadorrsync. Asession-ops@mau.pathunit starts the job on arrival.history.json. Changes to figures already in the history are kept until the month posts, which lists those for the two month ends it compares.rejected/. Any failure before the inbox is filed moves its exports torejected/as well, so the path unit never loops into its start limit.watchinjobs.tomlsession-ops unitswrites the.pathdrop-in, andinstall.shcreates the inbox and enables the unit for ready jobs.Discord commands
Commands
/run job:<job>startssession-ops@<job>.service. The choices come from the jobsjobs.tomlmarksdiscord = true:crowdin-syncandcrowdin-duplicatesfor now. It is refused while that job or any queued job is running or waiting to, and when the queue timer's next run (fromsystemctl list-timers --output=json) is sooner than now plus the job'stimeout, since systemd kills the job by then. That keeps the queue's no-overlap rule both ways. The check and the start run under one lock, so two/runa moment apart cannot both pass. The queue timer no longer hasRandomizedDelaySec, so its next run is the scheduled time. The confirmation naming who started it is posted in the channel; refusals are ephemeral./mau-upload file:<csv>downloads the attachment from Discord's CDN and checks it withmau.parse_export, which takes either store's export and names its platform in the reply. Only then is it renamed intomau/inbox/(written as a dotfile first, which the path unit's glob skips). A filemauwould reject is refused in Discord and never lands. The monthly reminder points at it alone.What the app sees
HTTP interactions only,
applications.commandsscope, no gateway, so Discord sends nothing but invocations of its own commands. Commands are registered withdefault_member_permissions: "0", hidden from everyone but server administrators until Server Settings → Integrations grants them to a role.Gates
DISCORD_GUILD_IDmatch (DMs refused).ALLOWED_USER_IDS/ALLOWED_ROLE_IDS; both empty refuses everybody.opsbot.session-ops polkitgenerates a rule fromjobs.tomlallowing that accountverb == "start"on exactly thediscord = trueunits. The mau inbox (sessionops:opsbot 0770) is its only writable path.Deploy
deploy/session-ops-discord.serviceon127.0.0.1:8081, hardened likezendesk-relay, plusAF_UNIXfor systemctl's D-Bus calls.install.sh: theopsbotaccount, the group on mau's inbox (other watched inboxes stay 0700), the polkit rule, and starting the service oncediscord.envhas content. The two relays now share one enable/restart helper.nginx-webhooks.conf: alocation = /discord/interactionsblock. certbot owns the live file, so it needs adding by hand.deploy/README.mdunder "Discord commands". The bot token is only forsession-ops discord-registerand is typed in at registration, never stored indiscord.env.Not yet done
/etc/polkit-1). Untilapt install polkitd(Debian 12 ships 122, which reads the JSrules.drule), every/rungets "Access denied".runuser -u opsbot -- systemctl --no-ask-password start session-ops@token-expiry.serviceis denied./run crowdin-duplicatesstarts the job and its post appears./mau-uploadwith a real export leads to amaurun.Deploy
/etc/session-ops/mau.env:MAU_DISCORD_WEBHOOK_URL,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.envand the nginx block for the commands (see above), thensession-ops discord-registeronce.Tests
uv run --locked python -m unittest discover -s tests -t .(860 OK, 1 skipped),ruff check .clean.