Sep 2026 Solo build: spec, code, hand-off
TNIO Schedule Bot
A Discord bot that builds the weekly event schedule post from a shared Google Calendar and edits it whenever the calendar changes, handed off to a friend with a one-click installer and a local control panel.
Shared calendar
Oct 12 – 18- Mon 128:00 PMWeekly training @host
- Tue 139:00 PMStory night (O)
- Wed 1412:30 AMunder TueLate patrol (F)
- Thu 157:30 PMWeekly training
- Fri 168:00 PMOpen meetup (O) @host
- Sat 17All dayskippedCommunity day
- Sat 176:00 PMFaction drills (F)
- Sun 183:00 PMWeekly recap
Overflow test. Over 2,000 characters, a message drops the host mentions first, then cuts events from the end.
Schedule channel
3 of 3 messagesHow it runs
Next sync
The real bot runs every 5 minutes from Windows Task Scheduler. Here, every 5 seconds.
The channel matches the calendar.
Last run · Sun 9:00 PM
Posted the week
Three new messages at the weekly post time.
- Sun 9:00 PMWeek of Oct 12: posted
Try it
Each sync
- All 3 found
- Edit only the messages whose text changed. No change, no call.
- None found
- Post all 3. Each pings @everyone if the setting is on.
- 1 or 2 found
- Delete them and post all 3 again, with no ping.
Events before 5:00 AM show under the day before. All-day events are skipped. Edits and reposts never notify anyone.
The events for this Discord community were already planned in a shared Google Calendar, while the weekly schedule in Discord was typed out by hand in a fixed format. I built a bot that writes that post from the calendar instead. At the weekly post time (Sunday 9:00 PM Eastern by default) it posts the next week as three messages that match the hand-written format, and during the week it edits those same messages when an event is added, moved, or cancelled. It runs as a short job every five minutes on the Windows PC of the friend who looks after it, and that friend sets it up and runs it from a browser panel without opening a terminal.
One calendar, one schedule post
Before the bot, the same week lived in two places: the shared Google Calendar the organizers edited, and a schedule post in Discord written by hand. I made the calendar the source of truth. Every run reads the week from the Google Calendar API and builds all three messages again from scratch, so a deleted or cancelled event simply drops out with no special handling.
The schedule follows the rules the community already used, which I wrote into a short spec agreed with the project owner. All times are Eastern. An event that starts before 5:00 AM is late night and shows under the day before, so a schedule week runs from Monday 5:00 AM to the next Monday 5:00 AM. All-day events are skipped, recurring events show each time they happen, and the header and spacing copy the hand-written post.
Post once, then edit in place
Each run looks for the bot's own messages from the 7 days and 1 hour before the week starts, and recognizes each one by a marker: the week title for the first message and the first day heading for the other two. If all three are there, it edits only the ones whose text changed, and an unchanged week makes no Discord call at all. If none are there, it posts all three. If someone deleted one or two, it removes the rest and posts the full set again so the days stay in order.
Hosts are written the way the organizers already wrote them, as @name in the event title. The bot matches that name against the server's member list by username, display name, or nickname and turns it into a clickable mention. Discord caps a message at 2,000 characters, so when a busy stretch goes over, the bot first drops the host mentions from that message, then removes events from the end and adds a line pointing readers to the calendar.
A scheduled job, not an always-on bot
The bot does not stay connected to Discord. Each run logs in over the REST API, syncs the current week (plus the next one once the post time has passed), and closes, with no gateway connection. Windows Task Scheduler starts it every 5 minutes and 1 minute after each sign-in on the friend's PC, and the task stays on after a restart. It runs under that user's own account, so no Windows password is stored, and an OS file lock keeps a scheduled run and a manual sync from overlapping.
Scheduled runs never open a browser. If Google needs a new sign-in, the run writes a plain-language error to a status file and the log, and the panel shows a sign-in button.
A control panel instead of a terminal
The setup is built so the friend who runs the bot never needs a terminal. Install.bat checks for Python, installs Git with winget if it is missing, connects a folder downloaded as a ZIP to the GitHub repo, builds the Python environment, and copies the settings templates. Start Control Panel.bat then opens a small Flask site on localhost.
The panel has a status band with the last run in plain words, an automatic-run switch that adds or removes the scheduled task, a Sync now button that asks for confirmation before posting to the real server, and a preview of the three messages drawn the way Discord shows them. Everything else sits in four tabs:
- Posting
- Weekly post day and time, a switch between the test server and the real server, the channel, emoji, and @everyone setting for each, and the footer text with a character count
- Hosts
- Extra host names mapped to Discord user IDs, for names the member list cannot match
- Connections
- The Discord bot token, which is never shown back, the calendar ID, and Google sign-in
- Update
- Pulls the newest version with a fast-forward-only git pull and reinstalls the requirements
Secrets and permissions
Google access uses the read-only Calendar scope, so the bot cannot change the calendar. The OAuth client file, the saved Google token, the .env file with the Discord token, the host list, and the logs stay on the bot PC and are ignored by Git. On the Discord side the bot asks for four channel permissions (View Channel, Send Messages, Read Message History, Mention Everyone) and one privileged intent, Server Members, used only to look up host names. Without that intent it still posts, with the names as plain text.
The panel has no password, so I closed it off another way. It listens on 127.0.0.1 only, rejects any request whose Host header is not localhost to block DNS rebinding, and accepts writes only as JSON with no foreign Origin, so another website open in the same browser cannot drive it. The bot token is write-only in the panel: it is never sent back to the page, and a test checks that it does not appear in the response.
Spec, tests, and CI
The rules live in a spec in the repo, and later requests from the owner are recorded there with their dates, such as an @everyone line at the top of all three messages and a footer that can be edited from the panel. The code that builds the messages has no network access, so the formatting rules are tested directly.
The suite has 88 pytest cases covering week boundaries, the late-night cutoff, a week across a daylight saving change, message overflow, the edit, post, and repost rules against a fake channel, member name matching, and the panel's Host, Origin, and token checks. GitHub Actions runs Ruff lint, a Ruff format check, and pytest on every push to main and every pull request. It went from first commit to the editable footer between September 24 and 28, 2026.