Run the bot¶
At this phase phpbotscout serve connects to Discord, logs the messages in the
channels you name, and does nothing else. It cannot post, moderate, or answer —
not because those are switched off, but because the code that would do them is
not built.
Before you start¶
You need the Discord application registered and installed, with the Message Content and Server Members intents enabled. See Register the Discord application.
Configure¶
Create .phpbotscout.yaml in the working directory, or use any source the
config store reads:
discord:
guilds:
- space: "1531227937678954747" # the guild (server) ID
channels: # the allowlist — empty permits nothing
- "1531227938622800055"
auth:
env: DISCORD_PHPBOTSCOUT_TOKEN # the NAME of the variable, not the token
health:
host: "127.0.0.1" # explicit interface, never 0.0.0.0
port: 8081
Then export the token itself:
The credential¶
auth takes exactly one of three forms, following the toolkit's convention:
| Form | Meaning |
|---|---|
env: NAME |
the name of an environment variable holding the token — recommended, and the only form permitted under CI |
keychain: service/account |
an OS keychain entry |
value: … |
the token itself; refused when CI=true |
Setting two is an error rather than a preference, because guessing which one you meant is the wrong kind of helpful where a secret is concerned.
The allowlist fails closed¶
An empty channels list permits nothing and is rejected at startup. A
watchlist that silently meant everywhere would be the wrong default for
reading people's messages.
One guild, for now¶
guilds is a list, but exactly one entry is accepted. Multi-tenancy is a v1
non-goal; the list shape exists so supporting several later is a configuration
change rather than a migration of everyone's config file.
Run¶
You should see the allowlist confirmed at startup:
INFO starting http server tls=false addr=127.0.0.1:8081
INFO watching channels space=1531227937678954747 channels=[1531227938622800055]
That line is worth reading. It is how you confirm the bot is watching what you meant before it has seen a single message.
Stop it with SIGTERM or Ctrl-C. It closes the gateway and exits cleanly; a
second signal forces the exit.
Check it is healthy¶
Three endpoints, mounted on the health listener:
{
"overall_healthy": true,
"services": [
{"name": "discord-gateway/1531227937678954747", "status": "OK"},
{"name": "health", "status": "OK"}
]
}
| Endpoint | Answers |
|---|---|
/livez |
should this process be restarted? |
/readyz |
is the bot currently seeing messages? |
/healthz |
both, per service |
The distinction matters. A bot that is up but disconnected looks exactly like a quiet channel, so readiness tracks the gateway connection: reconnecting is live but not ready.
The listener binds loopback by default and refuses to start on 0.0.0.0.
Binding every interface and relying on a firewall to be correct is how private
services become public ones.
What it reads, and what it keeps¶
Discord's intents are guild-wide, not per-channel. There is no way to ask the gateway for only your allowlisted channels, so the bot receives every message in the guild and discards the ones it must not read. The filtering is necessarily done by the bot.
That makes "what it reads" and "what it keeps" different answers:
| Allowlisted channel | Any other channel | |
|---|---|---|
| Received from Discord | yes | yes |
| Written to the log | yes, content redacted | nothing at all |
| Counted | yes | one unlabelled counter |
Nothing about a message from an unwatched channel is retained — not its content, not its channel, not its author. A per-channel counter would build an activity profile of exactly the channels the allowlist exists to exclude.
Message content that is logged passes through redact first, because it is
untrusted input from a public server.
Changing configuration while it runs¶
| Setting | Behaviour |
|---|---|
discord.guilds[].channels |
applied immediately |
discord.guilds[].space |
needs a restart |
discord.guilds[].auth |
needs a restart |
The allowlist is pure filtering, so it hot-reloads. Identity is not: applying a changed credential live would mean tearing down a working session on a config write, and a typo would take the bot off Discord until somebody noticed — at whatever time the file happened to be edited.
An ignored change is never silent. Editing either logs a warning naming the field:
Testing¶
just test-e2e # BDD scenarios; no credentials, no network
just test-discord # forced-disconnect regression test; needs live credentials
test-discord cuts a real TCP connection to prove the session resumes rather
than starting fresh. A reconnect that re-identified would drop every message
buffered during the gap — for a support bot, questions nobody will ever answer,
with no error and nothing to notice. The bot reports that case as a warning and
a counter, and deliberately stays ready: the session is usable, and restarting
it would not bring the lost messages back.