The house bot for the BlaskAttacks community Discord. One dependency
(discord.js), no build step, no database.
| Feature | How |
|---|---|
| Twitch live panel that updates itself | /twitch panel posts a live/offline card. The bot edits it in place every couple of minutes, forever. Pin it. |
| Going-live announcements | /twitch announce channel:#live role:@Viewers posts once per stream, with a role ping. |
| Stream status on demand | /twitch status, or @BlaskBot is he live? |
| News digest | Twice a day: AI, data centers, water & natural resources, war, economy, energy & oil, renewables & nuclear, preparedness (blackouts, shortages, the strategic reserve), and a bright-side section (breakthroughs, regenerative farming, fixes that work). /news channel #news to switch it on. |
| Unusual stock activity | Every 10 minutes through the trading day (06:00 to 14:15 Mountain, weekdays) the bot reads Yahoo's gainers, losers and most-active screens and posts a card for anything up or down 15%, or on 3x volume with a 7% move, with the two headlines that explain it (or an honest "no news explains this"), pinging the @stocks role once per batch. Several names moving on one story lead with "Theme: Greenland". /stockalerts watch add symbol: (mods, up to 20) checks named tickers every 5 minutes at lower thresholds. One alert per ticker per session; a flat day posts nothing. |
| Stock digest | Twice a day: big movers, IPOs, government contracts, earnings. /stocks channel #stocks. |
| News on demand | /news search query: posts the five newest headlines on anything from the last week. Or ask: @BlaskBot any news on small modular reactors?, @BlaskBot what's the latest on oil prices. |
| Stock cards | /stock symbol:NVDA (company names autocomplete), or @BlaskBot how's $TSLA doing. Price, day change, returns, 52-week range, 50/200-day averages, volume, a plain-English readout. |
| Stock basics | /learn menu of 25 lessons, or ask: @BlaskBot what is a P/E ratio. |
| Game deals | Daily at STEAM_DEALS_TIMES: what is on sale and what is free to keep, on Steam and Epic. /deals channel #deals switches it on; /deals free asks any time. Free-to-keep games are also checked every STEAM_FREE_POLL_HOURS and announced once each, so nobody misses a giveaway. |
| Gaming news, only when it matters | About two hours after a free-game post, and at 16:00 as a fallback, the bot reads the gaming press (IGN, PC Gamer, Eurogamer, Kotaku, GamesRadar, Rock Paper Shotgun, Google News) and posts up to five stories to the deals channel with the same role ping, but only if at least two outlets ran the same story or a headline says something changed (announced, delayed, cancelled, layoffs, acquired). A quiet day posts nothing. /deals news forces a check. |
| Find something to play | /findgame genre:horror mode:coop picks free multiplayer games the server could start tonight, ranked by how many people own them. Needs no Steam key. |
| What the group already owns | /steam link connects your own Steam account; /steam together lists the multiplayer games enough of you own to play, and /steam recent what the server has actually been playing this fortnight. Opt-in, self-service, /steam unlink erases it. Needs a Steam key from the operator, and your profile's Game details set to public. |
| Scoreboard | /score win game: and /score loss game: file a result, /score board is the standings, /score me your own record, /score undo takes the last one back. Or just say it: @BlaskBot i won at rivals. Self-reported — the bot cannot check a result and does not pretend to. |
| Films | /movie show title: posts a card with the blurb, poster, runtime and the TMDB and IMDb scores. /movie rate files your own score out of 10 (halves allowed) with an optional note; /movie top and /movie mine show what the server made of things, and /movie unrate takes yours back off the board. Titles tolerate typos: "the godfarther" is found, and a title the bot cannot match confidently gets a "did you mean" card rather than a guess. A name shared by a film and a show matches confidently either way, so it comes back as one or the other rather than as a choice. |
| Who's in it | /cast title: lists the cast of a film or a show, and season: with episode: narrows it to one episode's cast and crew. Or ask: @BlaskBot tell me about tales from the crypt season 4 episode 5, which answers about the episode rather than the 1972 film of the same name. |
| Movie night | /movie recommend genre:horror seen: (or @BlaskBot recommend a horror movie we haven't watched / ...the chat liked) suggests three films, from TMDB minus anything the server has seen, or from the server's own scores, each with an "Add" button that files a request. /movie request puts a film on the list, /movie list shows it with vote buttons, /movie poll runs a timed vote between up to three films (/movie endpoll, or @BlaskBot end the poll, lets a mod finish it early), and /movie watched logs one and closes its request. /movie recent is what the server has seen lately. /movie unrequest, /movie remove (mods) and /movie unwatch take things back. |
| Now watching | @BlaskBot we are watching baskin (or /movie watching title:) posts a green "Now watching" card with stills, the blurb and the scores. Add s4e5 or season 4 episode 5 for an episode of a show. A film's card has a "We finished it" button that logs the viewing and turns the same card into the rate prompt. @BlaskBot what are we watching links latecomers to the card; it stops being the answer after six hours, or when someone says @BlaskBot clear now watching. Everyone with the Movie Night role gets a DM with the film and a link to the card, at most once every 15 minutes per server; it needs the Server Members intent switched on in the Developer Portal. Mods can point it at another role with @BlaskBot movie night role @Role, or stop it with movie night role off. |
| The bits | Say "gabagool" anywhere. Say "solid snake" to the bot four times. /joke, /quote. Heckle a joke and see what happens. |
| Self-assignable roles | /roles panel title:"Pick your pings" roles:@News @Stocks posts a card of buttons. Tap one to take the role, tap again to drop it. style:menu makes it a select menu instead. Up to 25 roles to a panel. |
| Rules gate | /rules (or !gate) posts a panel; new members answer 3 questions off the rules, type YES, get the Viewer role. |
| Moderation | /mod timeout|warn|note|kick|ban|unban|history|forget, /whois, /mod log #channel. Every action written down. |
| Wellbeing net | The bot reads for two kinds of language: crisis, and a hard time. Crisis gets the person a DM with the crisis lines and puts an alert in the staff channel. A hard time gets the alert only — someone having a bad week needs a human, not a phone number from a bot. /mod alerts channel:#staff sets where. Nothing is recorded, nothing is deleted, nobody is in trouble. |
| The look | Cards are Discord layout components in the Nightfloor palette: phosphor green for live and up, oxblood for down and danger, brass for money and warnings, bone for everything else. |
The Sopranos and Metal Gear material, the beef escalation and the gabagool
barrage are ported from bxbot. The Twitch poller is descended from civicbot's
and grows the self-updating panel and the announcements. The stock lessons draw
on the Definitions and Learn pages of the gambler project.
Application id and token are in .env (never commit it). Two things to set
in the portal at https://discord.com/developers/applications:
- Bot → Privileged Gateway Intents → Message Content Intent: ON. Without it the bot cannot see "gabagool" in ordinary messages and mention text arrives blank. It needs no approval below 100 servers.
- General Information → Privacy Policy URL / Terms of Service URL:
https://blaskattacks.com/blaskbot-privacy-policy/andhttps://blaskattacks.com/blaskbot-terms-of-service/.
Invite link (Installation → Guild Install, scopes bot + applications.commands),
with these permissions: View Channels, Send Messages, Embed Links, Attach
Files, Read Message History, Manage Roles (rules gate), Moderate Members,
Kick Members, Ban Members (moderation), Manage Messages (to delete its own
!gate prompt). Permission integer 1099780189190:
https://discord.com/oauth2/authorize?client_id=1551014907301072927&scope=bot%20applications.commands&permissions=1099780189190
After inviting: drag the bot's role above Viewer in Server Settings →
Roles, or the rules gate cannot hand the role out. The bot says so in its log
at boot if this is wrong.
cp .env.example .envDISCORD_TOKEN and DISCORD_CLIENT_ID are required. Everything else has a
default. TWITCH_CLIENT_ID / TWITCH_CLIENT_SECRET come from
https://dev.twitch.tv/console/apps and switch the live status on; without
them every Twitch card just shows the link.
Digest times are local to NEWS_TZ (default America/Denver):
NEWS_TIMES=08:00,18:00, STOCKS_TIMES=07:00,14:30.
Deals, free-to-keep games and /findgame need no key. STEAM_API_KEY (free,
from https://steamcommunity.com/dev/apikey) switches on the two features
that read a profile — /steam link and /steam together — and nothing else
depends on it. STEAM_DEALS_TIMES=10:00 is the deals digest, on the same
clock as the others; STEAM_FREE_POLL_HOURS=6 is how often the free-games
check runs.
npm installnpm run deploynpm startChecks that need no token:
npm testnpm run preflightnpm run digestThe last one fetches the real feeds and prints what a digest would contain right now, without posting anything.
To post a digest from a shell instead of a slash command (needs the token):
npm run post -- newsOn the VPS, prefix it with BLASKBOT_DATA_DIR=/var/lib/blaskbot and run it as
the blaskbot user, otherwise it reads an empty ./state and reports
"no channel configured". Discord caps a message at 6000 characters across its
embeds, so a full nine-section digest arrives as three or four messages.
| Command | Does |
|---|---|
/twitch panel |
Posts the self-updating card here (or channel:). Pin it. Post as many as you like; each one is kept current. Delete the message and the bot forgets it. |
/twitch announce channel:#live role:@Viewers |
Going-live posts. Run it with no channel to turn off. |
/news channel channel:#news role:@news |
News digest destination, with an optional role pinged once per digest. /news now posts one immediately, /news preview shows you one privately. |
/stocks channel channel:#stocks role:@stocks |
Same for the stock digest. |
/deals channel channel:#deals role:@gaymer |
Deals destination; the role is pinged only when a game is free and with gaming news, never for a sale. |
/rules |
Posts the entry quiz panel. Needs a role called Viewer (or set VERIFY_ROLE). |
/mod log channel:#mod-log |
Echo every /mod action there. |
/mod alerts channel:#staff |
Where wellbeing alerts go. Falls back to the /mod log channel. With neither set the bot says so once in the log and then stays quiet. |
/roles panel title: roles: |
Posts a self-assignable role panel here. /roles add role:@X run in the same channel adds a role to that panel (no message id needed); /roles remove and /roles list likewise. Needs the bot's role above every role it hands out. |
/deals channel channel:#deals |
Where the deals digest posts. Run it with no channel to turn it off. /deals now posts one immediately. |
/twitch settings |
What is configured. |
@BlaskBot deals here |
The same setup without a slash command, for anyone with Manage Messages, Manage Server, Administrator, or the server owner. Works for news, stocks, deals, alerts, mod log and announce here @Role; deals channel #channel names another channel; deals off turns one off; deals now / news now / stocks now posts one immediately; settings shows the lot. Anyone else typing these gets the normal reply, never the setup. |
Erasing someone's moderation record: /mod forget removes their entries
and posts one line to the /mod log channel naming who erased whose record.
If a person asks for a full erasure, delete that log line by hand too; the
privacy policy says so.
Everything is stored per server in settings.json in the data directory
(./state locally, /var/lib/blaskbot on the VPS). Removing the bot from a
server deletes its entry.
The live panel. src/twitch.js polls Helix every TWITCH_POLL_SECONDS
(120). It computes a signature from status, stream id, title, game and
viewers rounded to tens; when that changes, or every 10 minutes regardless,
it edits each remembered panel message. A panel whose message was deleted is
dropped from the settings on the first failed edit. Panels posted before
this version were plain embeds; they convert to the layout card on the next
poll, in place, with no need to repost them. Going live is announced
once per Twitch stream id, and the id is saved so a restart mid-stream does
not announce it twice. The bot's own presence flips to "Streaming <title>"
while live.
Digests. src/news.js holds the categories and feeds. A digest fetches
everything, keeps stories under NEWS_MAX_AGE_HOURS, drops anything already
posted (matched by normalised headline, so the same story from two outlets
counts once), skips press-release reprint sites (SOURCE_BLOCKLIST), caps any
one outlet at two per category, and takes the newest N. src/schedule.js
fires the slots on the wall clock in NEWS_TZ. Adding a category is adding an
object to NEWS_CATEGORIES or STOCK_CATEGORIES; a Google News search feed
is googleNews('your query').
Stock cards. src/stocks.js reads Yahoo's public chart endpoint (no key)
and computes the returns and averages from the closes. The readout in
describe() is descriptive on purpose: it says "above its 200-day average",
never "buy". Every card carries the disclaimer.
Mentions. In src/index.js, in order: Solid Snake → Sopranos bait → live
question → stock ticker → lesson → help → two-part bits → heckles → praise /
jokes / off-topic → "didn't catch that". A live beef (they heckled twice
inside five minutes) jumps the queue and gets the mob voice.
Role panels. src/rolepanels.js holds the record and the rendering; a
panel is a card with one button per role, or a select menu if you asked for
style:menu. A button toggles that role; the menu sets exactly what is
ticked. The role id rides in the button's custom id, so a panel keeps working
even if the settings file is lost — only editing a panel needs the record.
The bot refuses a role it cannot actually grant and says which of the four
reasons it is: the role is gone, it is missing Manage Roles, the role sits
above its own, or an integration owns it. Answers are private, so the channel
never fills with "role added".
The wellbeing net. src/wellbeing.js classifies a message and nothing
else: crisis, struggle, or nothing. src/wellbeinggate.js does the
sending. It is loose on purpose — a false positive costs a mod ten seconds
and a miss costs something that cannot be got back — with one concession to a
gaming chat, which is that clear hyperbole ("this boss makes me want to die
lol") is downgraded so a human still glances at it but nobody gets a hotline
DM for a joke about Elden Ring. One alert per person per level per window, 12
hours for crisis and 6 for struggle, so a bad night is one thread in the staff
room rather than twenty. It runs in DMs as well as channels, because a DM to
the bot is somewhere people say things they will not say in front of everyone.
An alert from a DM goes only to the servers that person shares with the bot,
each one asked directly rather than taken on trust from the member cache.
Staff are skipped in channels so a mod quoting a member's words to discuss
them does not trip it, and not skipped in DMs. It never deletes, never
punishes, and writes nothing down about anyone.
The cards. src/nightfloor.js is the house style, carried over from the
gambler project's dark trading floor: an accent stripe that means something,
a heading, a data block in monospace, a hairline, then the controls. Discord
owns the fonts and the background, so what survives here is the palette and
the layout discipline. card() takes an ordered list of parts and skips the
falsy ones, so a caller can write image && gallery(image) without a branch.
Anything with no accessory to hang on the right — no avatar, no button —
comes back as a plain block of text instead, because Discord rejects a section
that has one missing.
Game deals. src/steam/store.js reads the Steam store's featured and
search endpoints and Epic's free-games endpoint, and normalises all three into
one shape; src/steam/deals.js builds the digest and posts it. Nothing here
needs a key, and no request carries anything about a Discord user. The
free-to-keep poll is separate from the daily digest and remembers what it has
already announced, so a giveaway running all week is posted once rather than
every six hours. A deal is only right for the moment it was read, which is why
the card links straight to the store page.
The game finder. src/steam/finder.js takes a genre, asks SteamSpy for
that tag, and ranks by how many people own it and then by how well it reviews.
SteamSpy does not carry categories, so multiplayer and co-op are confirmed
through the store itself. Owner counts are estimates and the card says so;
nothing in here decides a game is good, it reports what a lot of people own
and how they rated it. The tag responses are large — Indie is about 17 MB —
so they are cached rather than fetched per question.
Steam libraries. src/steam/accounts.js holds one row per person who ran
/steam link: their Discord id, their SteamID64, the Steam name at the time,
and when. That is the whole file. src/steam/library.js reads libraries live
through the Steam Web API, compares them in memory and discards them —
no game list, playtime or friends list is ever written down. Linking is
self-service and only for yourself; there is no way to link somebody else.
/steam unlink still works when STEAM_API_KEY is unset, on purpose, so what
is stored can always be removed even with the feature otherwise switched off.
A link is keyed by Discord account and not by server, so it works everywhere
the bot and the person both are — and it is not dropped when the bot leaves a
server, unlike that server's settings. Both together and recent fetch only
the ids that are already linked; the full member list is never read.
The scoreboard. src/score.js appends one line of JSON per result to
scores.jsonl, the same shape as the moderation record, and builds the cards.
Results are self-reported: Steam does not expose wins and losses for arbitrary
games, so members file their own and the board is a record of what people said
happened. A person is capped at 30 results an hour. The mention parser in
src/commands/score.js reads "i won at rivals" and "lost in apex to @x", and
refuses far more than it accepts — no result without both a result word and a
game, nothing from a question, nothing that mentions somebody who is not the
named opponent. Filing a made-up result under a real person's name is worse
than not understanding them, so when it is unsure it says nothing.
Voice. All the bot's wording is in src/voice.js. The lessons are in
src/learn.js, the quiz questions in src/verify.js.
Same box as bxbot and civicbot (82.29.197.244, Hostinger), same shape:
systemd unit, its own unprivileged user, state under /var/lib/blaskbot,
code under /opt/blaskbot, no Docker.
From the project root in Git Bash (not PowerShell; the pipe carries a tarball):
bash deploy/ship.sh --first --commandsThat creates the user, ships the code, runs npm ci, installs the unit,
copies .env.example to /opt/blaskbot/.env if there is no .env yet,
registers the slash commands and starts the service. The real .env is never
shipped; on a first install copy it up yourself:
scp -F /dev/null -i ~/.ssh/vps_key .env root@82.29.197.244:/opt/blaskbot/.envEvery later deploy:
bash deploy/ship.shAdd --commands when a command's name, description or options changed.
The VPS .env has DISCORD_GUILD_ID set to Blask's server, so commands
register to that server and appear instantly. The global set was cleared on
20 September 2026 so the two cannot both be live. If the bot ever joins a
second server, blank the guild id, run npm run deploy once for the global
set, and expect up to an hour before the commands show.
Watch it:
ssh -F /dev/null -i ~/.ssh/vps_key root@82.29.197.244 journalctl -u blaskbot -fA healthy boot logs Logged in as, the command list, Rules gate ready,
[twitch] watching twitch.tv/blaskattack every 120s and the digest schedule.
Rolling back. ship.sh tars the previous code to /root/blaskbot-<stamp>.tgz
before every deploy. Extract it over /opt/blaskbot and restart.
This product uses the TMDB API but is not endorsed or certified by TMDB. TMDB's terms require that line wherever their data is shown, so it belongs here, in the terms of service, and on every card built from TMDB data.
Film blurbs, posters and ratings come from TMDB; IMDb and Rotten Tomatoes
scores come from OMDb when OMDB_API_KEY is set; a keyless Wikipedia summary
is the fallback when neither key is configured.
docs/privacy-policy.md and docs/terms-of-service.md are the source.
node scripts/build-site.js renders them to site/blaskbot-privacy-policy/
and site/blaskbot-terms-of-service/. Upload those two folders into the
WordPress public_html on the shared host and they are served at
https://blaskattacks.com/blaskbot-privacy-policy/ and /blaskbot-terms-of-service/,
ahead of WordPress. If what the bot stores ever changes, change the policy in
the same commit.