A helpful Discord bot for Pumping Station One members. Guides users through troubleshooting flows via interactive DM conversations using a "choose your own adventure" style interface.
"It looks like you're locked out! Would you like help with that?" 📎
- Overview
- Features
- Architecture
- Quick Start
- Configuration
- Commands
- Creating and Editing Flows
- The Lockout Flow
- Session Behavior
- Escalation System
- Deployment
- Development
- Design Decisions
- Related PS1 Bots
- License
PS1 Clippy is a triage bot that helps Pumping Station One members get assistance through interactive decision trees. The primary use case is helping members who are locked out of the building, but the system is designed to be extensible for any help topic.
- User runs
/lockedoutor/clippyin any Discord channel - Bot sends a public acknowledgment and initiates a DM conversation
- User navigates through a decision tree using buttons
- Bot provides solutions, links to resources, or escalates to human help
- Session expires after 30 minutes of inactivity
PS1 Clippy works alongside the existing PS1 bot ecosystem:
| Bot | Purpose |
|---|---|
| PS1 Clippy (this bot) | Triage layer - guides users to the right solution |
| Door Bot | Handles emergency door access via /door command with TOTP |
| Role Bot | Links Discord accounts to PS1 membership via /authorize |
| OTP Display | ESP32 device that displays the door TOTP code |
Clippy doesn't replace Door Bot - it helps users understand when and how to use Door Bot, among other solutions.
- Interactive Help Flows - Choose-your-own-adventure style troubleshooting via DMs
- Multiple Entry Points -
/clippyfor general help,/lockedoutfor lockout-specific help, or DM the bot directly - YAML-Defined Flows - Decision trees defined in simple YAML files, no code changes needed
- Session Management - 30-minute timeout, back navigation, restart option
- Helper Mode - Staff with the Helper role can trigger flows for other users
- Escalation - Automatic forum post creation in #help-me when human help is needed
- Hot Reload - Update flows without restarting the bot via
/clippy-admin reload - Path Logging - Tracks which paths users take through flows for debugging
- All interactive conversations happen in DMs (private, less intimidating)
- Public acknowledgment in channel so others know help is coming
- Back button to revisit previous steps
- Restart button to start over
- Clear escape hatches to human help at every step
- Rich embeds with links and formatted instructions
- Mobile-friendly button interface
┌─────────────────────────────────────────────────────────────────┐
│ Discord Server │
├─────────────────────────────────────────────────────────────────┤
│ │
│ User runs /lockedout Bot sends DM with buttons │
│ │ │ │
│ ▼ ▼ │
│ ┌─────────────┐ ┌─────────────────┐ │
│ │ Channel │─────────────▶│ DM Channel │ │
│ │ (public) │ "Check DMs" │ (private) │ │
│ └─────────────┘ └────────┬────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ Flow Engine │ │
│ │ (YAML-driven) │ │
│ └────────┬────────┘ │
│ │ │
│ ┌───────────────────┼───────────────────┐ │
│ ▼ ▼ ▼ │
│ ┌──────────┐ ┌──────────────┐ ┌──────────┐│
│ │ Solution │ │ Escalate │ │ End ││
│ │ (embed) │ │ to #help-me │ │ Session ││
│ └──────────┘ └──────────────┘ └──────────┘│
│ │
└─────────────────────────────────────────────────────────────────┘
| Component | Technology |
|---|---|
| Language | Python 3.12+ |
| Discord Library | discord.py 2.4+ |
| Configuration | python-dotenv |
| Flow Definitions | YAML (PyYAML) |
| Deployment | Docker |
| Package Management | pip / uv |
- Python 3.12 or higher
- Discord Bot Token from Discord Developer Portal
- A Discord server to test in
-
Clone the repository:
git clone https://github.com/pumpingstationone/ps1_clippy.git cd ps1_clippy -
Create and activate a virtual environment:
python -m venv .venv source .venv/bin/activate # On Windows: .venv\Scripts\activate
-
Install dependencies:
pip install -e . -
Configure environment:
cp .env.example .env # Edit .env with your Discord bot token and server IDs -
Run the bot:
python -m ps1_clippy
# Configure environment
cp .env.example .env
# Edit .env with your settings
# Build and run
docker compose up -d --build
# View logs
docker compose logs -f
# Stop
docker compose downCreate a .env file with the following variables:
| Variable | Required | Default | Description |
|---|---|---|---|
DISCORD_BOT_TOKEN |
Yes | - | Your Discord bot token |
DISCORD_GUILD_ID |
Yes | - | Your Discord server ID |
DISCORD_HELP_CHANNEL_ID |
No | - | Forum channel ID for escalation posts |
DISCORD_HELPER_ROLE |
No | Helper |
Role name or ID that can use admin commands |
SESSION_TIMEOUT_MINUTES |
No | 30 |
How long before inactive sessions expire |
LOG_LEVEL |
No | INFO |
Logging verbosity (DEBUG, INFO, WARNING, ERROR) |
FLOWS_GITHUB_REPO |
No | - | GitHub repo for flows (format: owner/repo) |
FLOWS_GITHUB_BRANCH |
No | main |
Branch to pull flows from |
FLOWS_GITHUB_PATH |
No | flows |
Path within the repo where flows are stored |
GITHUB_TOKEN |
No | - | GitHub token (only needed for private repos) |
- Enable Developer Mode in Discord: User Settings → App Settings → Advanced → Developer Mode
- Right-click on your server → Copy Server ID (this is
DISCORD_GUILD_ID) - Right-click on your help forum channel → Copy Channel ID (this is
DISCORD_HELP_CHANNEL_ID)
| Command | Description |
|---|---|
/clippy |
Start the main help menu |
/lockedout |
Start the lockout troubleshooting flow |
| DM the bot | Starts help menu or matches keywords to specific flows |
These require the configured Helper role:
| Command | Description |
|---|---|
/lockedout @user |
Start lockout flow for another user |
/clippy-admin preview |
Generate Mermaid diagram of flows |
/clippy-admin reload [source] [branch] |
Reload flows (see below) |
/clippy-admin status |
Show current flows source and loaded flows |
/clippy-admin check |
Check if updates are available in the remote repo |
/clippy-admin sessions |
List all active user sessions |
| Source | Description |
|---|---|
| (empty) | Reload from configured GitHub repo (or local if not configured) |
github |
Same as empty - reload from configured repo |
cache |
Reload from local cache without fetching (for testing local edits) |
local |
Force reload from local flows/ directory |
clear |
Clear any temporary override, reload from main source |
owner/repo |
Load from a fork as a temporary override (resets on restart) |
Content creators can test their changes without affecting production:
- Fork the flows repo to your GitHub account
- Make changes on a branch
- Run
/clippy-admin reload yourname/flows-repo feature-branch - Test your changes
- Run
/clippy-admin reload clearto revert (or just restart the bot) - Once satisfied, open a PR to the main flows repo
When a user runs /lockedout:
- Bot sends a public message in the channel: "📎 I'm here to help you @user! Check your DMs."
- Bot opens a DM with the user and sends the first flow step
- User navigates using buttons in the DM
- Session tracked for 30 minutes
When a Helper runs /lockedout @someone:
- Bot sends: "📎 I'm here to help @someone! Check your DMs."
- Bot DMs the target user (not the helper)
- Useful for helping users who don't know the command
Flows are defined in YAML files in the flows/ directory. The bot loads all .yaml files at startup and can reload them on demand.
# Metadata about the flow
metadata:
name: myflow # Unique identifier (used in flow:myflow references)
description: My helpful flow # Human-readable description
version: "1.0" # Version for tracking changes
trigger_command: /mycommand # Optional: dedicated slash command
trigger_dm_keywords: # Optional: keywords that trigger this flow via DM
- help me
- need assistance
# The decision tree
nodes:
start: # Every flow MUST have a 'start' node
message: |
Welcome! How can I help you today?
buttons:
- label: "Option A"
next: node_a
- label: "Option B"
next: node_b
style: secondary
node_a:
message: |
Here's some helpful information about Option A.
embed:
title: "Learn More"
description: |
Detailed information goes here.
You can use **markdown** formatting!
url: "https://example.com"
color: 0x00ff00 # Green
buttons:
- label: "This helped!"
next: success_end
- label: "I need more help"
next: escalate
action: escalate
node_b:
message: |
Here's information about Option B.
buttons:
- label: "Got it!"
next: success_end
escalate:
message: |
I'll get you some human help.
action: create_forum_post
buttons:
- label: "Thanks!"
next: end
success_end:
message: |
Glad I could help! Come back anytime.
action: end
end:
message: |
Thanks for using the help system!
action: end| Property | Required | Description |
|---|---|---|
message |
Yes | Text shown to user (supports Discord markdown) |
buttons |
No | Array of button choices |
embed |
No | Rich embed with title, description, URL, color |
action |
No | Special action to perform (see below) |
| Property | Required | Description |
|---|---|---|
label |
Yes | Button text (max ~80 characters) |
next |
Yes | Node ID to navigate to, or flow:flowname to switch flows |
style |
No | primary (blue), secondary (gray), success (green), danger (red) |
action |
No | Action to perform when clicked |
| Action | Description |
|---|---|
end |
End the session cleanly |
escalate |
Mark the session as needing escalation (for logging) |
create_forum_post |
Create a post in the #help-me forum channel |
Use next: flow:flowname to jump to a different flow:
buttons:
- label: "I'm actually locked out"
next: flow:lockedout # Jumps to lockedout.yaml, starting at 'start' nodeThe _menu.yaml file (note the underscore prefix) is special - it's shown when users:
- Run
/clippy - DM the bot without matching any flow keywords
- Edit the YAML file in
flows/ - Run
/clippy-admin reload localin Discord (or restart the bot) - Changes take effect immediately for new sessions
When FLOWS_GITHUB_REPO is configured, the bot pulls flows from GitHub:
- Push changes to the configured flows repo
- Run
/clippy-admin reloadin Discord - Bot pulls the latest from GitHub and reloads
To check for updates without applying them:
/clippy-admin check
To see what source is currently loaded:
/clippy-admin status
- Keep messages concise - Users are often on mobile
- Limit buttons - Discord allows max 5 buttons per row, 5 rows total (25 buttons max, but aim for 4-5 per node)
- Always provide an escape hatch - Every path should have a "none of these fit" or "need human help" option
- Use embeds for links - Embed URLs are more visible than inline links
- Test on mobile - The experience should work well on phone screens
- Log meaningful paths - Use descriptive node names for easier log analysis
The primary flow helps members who are locked out of the PS1 building.
START: "What's your situation?"
│
├── "I've paid my dues" / "I'm not sure if I've paid"
│ │
│ └── "Do you have your keyfob?"
│ │
│ ├── "Yes, but where do I tap?"
│ │ └── Instructions on tap location
│ │ └── "Still won't open" → Check membership
│ │
│ ├── "Yes, but door won't open"
│ │ └── Check membership portal
│ │ ├── "Emergency - need in NOW" → Door Bot instructions
│ │ ├── "Everything looks correct" → Escalate to #help-me
│ │ └── "I'm not in PS1 Discord" → Discord guide
│ │
│ ├── "No, I forgot/lost it"
│ │ └── Emergency Door Bot access instructions
│ │
│ └── "I'm new - how do I get a key?"
│ └── New member keyfob instructions
│
└── "None of these fit"
└── Escalate to #help-me
| Resource | URL |
|---|---|
| Membership Portal | https://membership.pumpingstationone.org |
| Emergency Door Bot Access | https://wiki.pumpingstationone.org/wiki/Emergency_Door_Bot_Access |
| New Member Keyfob Guide | https://wiki.pumpingstationone.org/wiki/Member_Manual#How_do_I_get_a_key-fob.3F |
| Discord Server Guide | https://wiki.pumpingstationone.org/wiki/Discord |
| General Contact | info@pumpingstationone.org |
- Created - When user triggers a flow via command or DM
- Active - User navigating through nodes, clicking buttons
- Expired - No activity for 30 minutes (configurable)
- Ended - User reached an
endaction or session was manually ended
If a user runs /lockedout while they have an active session:
- Bot shows a prompt: "You already have an active session. What would you like to do?"
- Options: Restart (abandon old session) or Continue where I left off
- Choosing "Continue" resends the current node
- Back Button - Appears after the first step, returns to previous node
- Restart Button - Always available, starts the flow over from the beginning
- History - Full path is tracked for logging and back navigation
If the bot cannot DM the user:
- Public message in channel: "I can't send DMs to @user. Please enable DMs from server members and try again, or DM me directly!"
- User can then DM the bot to start the flow
When a user needs human help, the bot can create a forum post in the #help-me channel.
- User clicks a button with
action: create_forum_post - Bot creates a new thread in the configured forum channel
- Thread includes:
- User mention
- Flow name
- Last 5 steps of their path (for context)
- Request for human assistance
- User receives a link to the thread
Set DISCORD_HELP_CHANNEL_ID to your forum channel's ID. The channel must be a Forum Channel (not a regular text channel).
Help request from @username
User: @username
Flow: lockedout
Path taken: [started lockedout] → has_keyfob → check_membership → escalate_help
This user needs human assistance. Please help them out!
The Docker setup mounts the flows/ directory as a volume, so you can edit flows without rebuilding:
# Initial deployment
docker compose up -d --build
# View logs
docker compose logs -f
# After editing flows/
# Just run /clippy-admin reload in Discord - no restart needed!
# After editing Python code
docker compose up -d --build
# Stop
docker compose downservices:
clippy:
build: .
container_name: ps1-clippy
restart: unless-stopped
env_file:
- .env
volumes:
- ./flows:/app/flows:ro # Flows mounted read-only
environment:
- PYTHONUNBUFFERED=1# On your server
cd /path/to/ps1_clippy
source .venv/bin/activate
python -m ps1_clippy
# Or with a process manager like systemd
# Create /etc/systemd/system/ps1-clippy.service[Unit]
Description=PS1 Clippy Discord Bot
After=network.target
[Service]
Type=simple
User=ps1bot
WorkingDirectory=/opt/ps1_clippy
Environment=PATH=/opt/ps1_clippy/.venv/bin
ExecStart=/opt/ps1_clippy/.venv/bin/python -m ps1_clippy
Restart=always
RestartSec=10
[Install]
WantedBy=multi-user.target# Clone and setup
git clone https://github.com/pumpingstationone/ps1_clippy.git
cd ps1_clippy
python -m venv .venv
source .venv/bin/activate
pip install -e ".[dev]"
# Run locally
python -m ps1_clippypytestThe project follows standard Python conventions. Key points:
- Type hints throughout
- Dataclasses for models
- Async/await for Discord operations
- Create
flows/myflow.yamlfollowing the structure above - Restart the bot or run
/clippy-admin reload - The flow is now available via
flow:myflowreferences
- Add
trigger_command: /mycommandto your flow's metadata - Register the command in
ps1_clippy/bot/client.py:
@self.tree.command(
name="mycommand",
description="Description shown in Discord",
)
async def mycommand(interaction: discord.Interaction):
await self._start_flow(interaction, "myflow")- Restart the bot to register the new command
These decisions were made during the design and development of PS1 Clippy:
PS1 Clippy is separate from Door Bot because:
- Separation of concerns - Door Bot handles security-critical door access; Clippy handles triage
- Independent deployment - Can update Clippy without affecting door security
- Simpler codebase - Each bot does one thing well
- Matches existing pattern - PS1 already runs multiple specialized bots
- Privacy - Users might not want to publicly announce they're locked out
- Less noise - Doesn't clutter public channels
- Better UX - DMs feel more like a conversation with a helper
- Persistent - Users can scroll back through the conversation
- Non-programmers can edit - Board members and admins can update flows
- Version controlled - Changes tracked in git, easy to review
- Hot reloadable - No code deployment needed for content changes
- Readable - Easy to understand the decision tree structure
- Long enough that users don't feel rushed
- Short enough that abandoned sessions don't accumulate
- Matches typical "locked out at the door" scenario duration
When a user runs /lockedout with an active session, they're asked whether to restart or continue. This was chosen over:
- Auto-restart (loses progress)
- Auto-continue (confusing if they meant to restart)
- Ignore (frustrating user experience)
Handles emergency door access using TOTP codes.
- Repository: https://github.com/pumpingstationone/ps1_doorbot
- Command:
/door- Opens the door using the code displayed on the door - Stack: Python, discord.py, uhppoted
Links Discord accounts to PS1 membership.
- Repository: https://github.com/pumpingstationone/ps1_rolebot
- Command:
/authorize- Verifies email and Stripe payment, assigns role - Stack: Python, discord.py, FastAPI, Stripe, Mailgun
ESP32 device that displays the door TOTP code.
- Repository: https://github.com/pumpingstationone/ps1_otp
- Hardware: ESP32 with 7-segment displays
- Stack: Arduino/C++
If you're setting up a new Discord application:
- Go to Discord Developer Portal
- Click New Application and name it "PS1 Clippy"
- Go to Bot section:
- Click Reset Token and save it for your
.env - Enable Message Content Intent under Privileged Gateway Intents
- Click Reset Token and save it for your
- Go to OAuth2 → URL Generator:
- Scopes:
bot,applications.commands - Bot Permissions:
- Send Messages
- Send Messages in Threads
- Create Public Threads
- Embed Links
- Use Slash Commands
- Scopes:
- Use the generated URL to invite the bot to your server
MIT License
Copyright (c) 2024 Pumping Station One
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.