Release: open source, and a command for every setting #31

Merged
FernandoJVideira merged 8 commits from dev into master 2026-10-23 01:13:43 +00:00
11 changed files with 289 additions and 175 deletions
Showing only changes of commit 30cbd9a798 - Show all commits

No files matched your search

+1
View File
@@ -44,6 +44,7 @@ These are the things a reviewer will check first. The reasons are in the [Develo
- **Check permissions in the handler.** `DefaultMemberPermissions` only hides a command from the menu.
- **No SQL outside repos.** Always use query parameters (`$1`), never string formatting.
- **A module that acts on a server starts off.** An admin turns it on.
- **Every setting has a command.** The bot works fully without a dashboard, so a setting must be visible and changeable with slash commands.
- **Escape user text and set `AllowedMentions`** so a message can't ping people.
- **Validate everything that comes from users or from other programs writing to the database**: options, custom IDs, command payloads.
- **No secrets** in code, logs, tests or commit messages.
+1 -1
View File
@@ -2,7 +2,7 @@
Velox is a Discord bot written in Go, backed by PostgreSQL. It covers server security (anti-raid and anti-nuke), support tickets, moderation, leveling, music, meeting rooms, scheduling, Twitch alerts and a few games. Every module is off until a server turns it on.
**This repository is the bot, and it is the only open source part of Velox.** The web dashboard and its API, which the hosted Velox uses, are separate proprietary software and are not part of this repository or its license. The bot does not need them: everything in the [command reference](docs/commands.md) works on its own, with PostgreSQL as its only dependency.
**This repository is the bot, and it is the only open source part of Velox.** The web dashboard and its API, which the hosted Velox uses, are separate proprietary software and are not part of this repository or its license. The bot does not need them. Every setting and every action is a slash command (see the [command reference](docs/commands.md)), with PostgreSQL as its only dependency.
The dashboard and the bot meet in the database. The dashboard writes settings and requests into tables, and the bot reads them (and is woken through Postgres `NOTIFY` where it has to react at once). Those tables are documented, so you can change the same settings with SQL, or build your own tool on top. See [Architecture](docs/architecture.md) and [Database](docs/database.md).
+8 -7
View File
@@ -9,7 +9,7 @@ The rule is the same on the bot and on the hosted Velox dashboard. The dashboard
## Who the testers are
- The **beta server** and the **Beta Tester role** live in the `beta_settings` table, one row. On the hosted Velox the owner sets them on the dashboard. If you host the bot yourself, set them with SQL (see [Settings without a dashboard](self-hosting.md#settings-without-a-dashboard)).
- The **beta server** and the **Beta Tester role** are chosen by the bot's owner (the owner of the Discord application) with `/beta set`, in a direct message with the bot. They are stored in the `beta_settings` table, one row. `/beta status` shows them and `/beta off` clears the role, which turns beta features off for everyone.
- Giving someone the role in Discord makes them a tester. Taking it away removes their access, within about 30 seconds on the bot (a few seconds if the bot has them cached).
- Until a role is chosen, **nobody** has beta access.
- Being a tester is about the person, so it is always checked in the **beta server**, also when they use a feature somewhere else. Having a role with the same ID in another server means nothing.
@@ -17,15 +17,16 @@ The rule is the same on the bot and on the hosted Velox dashboard. The dashboard
## Which servers have beta features
- The beta server always has them.
- Any other server gets them only when it is in the `beta_guilds` table. Beta features can act on a whole server (anti-raid behaviour, for example), so a server should only be listed with its owner's agreement. On the hosted Velox, only the server's own owner can switch it on, from the dashboard. If you host the bot yourself, you add rows yourself.
- The bot polls the table every 30 seconds, together with the beta settings.
- Any other server gets them only when its **owner** runs `/config beta-features enabled:true` there. Beta features can act on a whole server (anti-raid behaviour, for example), so the people who answer for the server decide. A server's admins who are not its owner cannot switch it on.
- The bot's owner can take a server out with `/beta remove-server` (and see them with `/beta servers`), but cannot put one in.
- The servers are the `beta_guilds` table. The bot polls it every 30 seconds, together with the beta settings.
Beta never replaces a feature's own permission checks. A beta command that needs Manage Server still needs it, a music command still needs the DJ role, and anything for the owner or trusted users still needs that.
## What a tester sees
- **Bot:** beta commands appear only in the beta server and in opted-in servers, and run only for testers. When a server opts in, the commands show up within about 30 seconds. When it opts out, they are removed.
- **On the hosted Velox dashboard:** beta pages and links show a **Beta** badge, only to testers and only for servers where beta features are on.
- **On the hosted Velox dashboard** (proprietary, not in this repository): beta pages and links show a **Beta** badge, only to testers and only for servers where beta features are on.
## On the bot
@@ -34,8 +35,8 @@ Code: `internal/beta` (the rules), `internal/commands/commands.go` (`RegisterBet
- Beta commands are registered in each server where beta features are on, and only there. `/beta` is only in the beta server.
- Each beta handler is wrapped in `beta.Gate`. It checks that beta is on in the server of the interaction and that the person is a tester, and otherwise answers "This feature is in beta and isn't available to you or this server yet." (Autocomplete requests get an empty list.) Hiding the command is never the only protection.
- For a person using a command in another server, the bot looks up their roles in the beta server: first in its gateway cache, otherwise it asks Discord (remembered for 30 seconds). If anything fails, the answer is no.
- If the beta server changes in `beta_settings`, the beta commands move within 30 seconds. In the beta server, commands this bot left there that no longer exist are removed too. If the beta server is also `GUILD_ID`, the regular commands are kept.
- `/beta` shows the bot's owner how access is set up. Only the owner of the Discord application can run it, and it is hidden from everyone but administrators.
- If the beta server changes, the beta commands move within 30 seconds. In the beta server, commands this bot left there that no longer exist are removed too. If the beta server is also `GUILD_ID`, the regular commands are kept.
- `/beta` is registered globally but works only in a direct message with the bot, so it is in no server's menu, and only the owner of the Discord application can use it.
- The bot remembers the servers where it registered beta commands (`beta_registered` table). A server that opted out while the bot was **offline** is cleaned up on the next start, and a cleanup that fails is retried every 30 seconds. A server the bot has left is forgotten.
### Add a beta command
@@ -76,4 +77,4 @@ The hosted Velox has a proprietary dashboard and API that apply the same rule to
- Servers choose to take part, because a beta feature can affect everyone in them.
- Access is decided on the server, never in the browser.
- Beta commands do not exist outside the beta server, and the gate checks again when they run.
- The settings are in the database, not in the environment, so they can change without a deploy.
- The settings are in the database and changed with commands, not in the environment, so they change without a deploy.
+35 -4
View File
@@ -46,6 +46,8 @@ Anyone can use these. `/rps`, `/rpsstats` and `/rpsleaderboard` work in servers
| `/slvl set-channel` | `channel`* | Manage Server, leveling on | Where level-up messages go |
| `/slvl set-message` | `message`* | Manage Server, leveling on | The level-up message. Variables: `{user}`, `{level}` |
| `/slvl set-reward` | `level`*, `role`* | Manage Server, leveling on | Role granted at a level |
| `/slvl remove-reward` | `level`* | Manage Server, leveling on | Removes a level's role reward |
| `/slvl status` | | Manage Server | Shows the leveling settings |
| `/rank` | `user` | Leveling on | A rank card for you or another member |
| `/leaderboard` | | Leveling on | Top 10 in the server |
| `/rewards` | | Leveling on | The role rewards |
@@ -58,6 +60,12 @@ Anyone can use these. `/rps`, `/rpsstats` and `/rpsleaderboard` work in servers
| `/queue` | | **DJ** role | Shows the queue |
| `/volume` | `value`* (0 to 150) | **DJ** role | Sets the volume |
| `/music toggle` | | Manage Server | Turns music on or off in this server |
| `/music skip` | | **DJ** role | Skips the current song |
| `/music pause`, `/music resume` | | **DJ** role | Pauses or resumes |
| `/music stop` | | **DJ** role | Stops and clears the queue |
| `/music repeat` | `mode`* (off, song, queue) | **DJ** role | Repeats the song, the queue, or nothing |
| `/music remove` | `position`* (its number in `/queue`) | **DJ** role | Removes a queued song |
| `/music move` | `from`*, `to`* | **DJ** role | Moves a queued song |
Requires Lavalink. See [Getting started](getting-started.md#optional-music-with-lavalink).
@@ -89,6 +97,8 @@ Each subcommand checks the Discord permission in its "Needs" column.
| `/moderation ban` | `user`*, `delete_days` (0 to 7), `reason` | Ban Members | Bans a member |
| `/moderation unban` | `user_id`*, `reason` | Ban Members | Unbans by ID |
| `/moderation slowmode` | `channel`*, `seconds`* (0 to 21600) | Manage Channels | Sets slowmode, 0 turns it off |
| `/moderation bans` | | Ban Members | Lists banned users, with their IDs |
| `/moderation timeouts` | | Moderate Members | Lists members who are timed out now |
## Timezone and scheduling
@@ -125,6 +135,11 @@ Every subcommand needs **Manage Server**.
| `/config setlogchannel` | `channel`* | Where logs go |
| `/config enablelogging` | | Turns logging on |
| `/config disablelogging` | | Turns it off |
| `/config view` | | Shows the welcome, default role, logging and Twitch settings |
| `/config clear` | `setting`* (welcome-channel, welcome-message, welcome-dm, welcome-gif, default-role, log-channel) | Clears one |
| `/config toggletwitch` | `enabled`* | Turns Twitch live notifications on or off |
| `/config streamers` | | Lists the watched streamers |
| `/config beta-features` | `enabled` | Lets testers use beta features here. Only the **server owner** can change it. Leave out `enabled` to see the state |
| `/config addstreamer` | `username`* | Adds a Twitch streamer to watch |
| `/config removestreamer` | `username`* | Removes one |
| `/config settwitchnotificationchannel` | `channel`* | Where live alerts go |
@@ -137,7 +152,10 @@ Every subcommand needs **Manage Server**.
| `/tickets category add` | `name`*, `staff-role`, `emoji`, `description` | Manage Server | Adds a category to the panel's dropdown |
| `/tickets category remove` | `name`* | Manage Server | Removes a category |
| `/tickets category list` | | Manage Server | Lists categories |
| `/tickets category edit` | `name`*, `new-name`, `staff-role`, `clear-staff-role`, `emoji`, `description` | Manage Server | Changes a category (`-` removes the emoji or description) |
| `/tickets panel` | | Manage Server | Posts the panel, or updates it after changing categories |
| `/tickets settings` | `enabled`, `panel-title`, `panel-description`, `max-open` | Manage Server | Shows the settings, or changes the ones you give |
| `/tickets list` | `status` (open or closed) | Manage Server | Lists recent tickets |
| `/ticket add` | `user`* | The opener or staff | Adds someone to the ticket |
| `/ticket remove` | `user`* | Staff | Removes someone |
| `/ticket claim` | | Staff | Takes the ticket |
@@ -160,6 +178,17 @@ In this table "Manage Server" also lets Administrators and trusted members throu
| `/security quarantined` | | Manage Server | Lists quarantined members |
| `/security release` | `user`* | Owner or trusted | Releases a member and gives their roles back |
| `/security end-raid` | | Owner or trusted | Ends raid mode now |
| `/security settings` | | Manage Server | Shows every setting |
| `/security history` | | Manage Server | Shows the latest security events |
| `/security joingate check` | `check`* (no-avatar, new-account, invite-in-name, blocked-names, unverified-bot, unauthorized-bot), `action`* | Owner or trusted | What happens when a check catches a new member |
| `/security joingate account-age` | `days`* (1 to 365) | Owner or trusted | How old an account must be |
| `/security words add`, `remove` | `list`* (username or chat), `word`* | Owner or trusted | Blocked words for new members' names (Join Gate) or messages (heat) |
| `/security words list` | `list`* | Manage Server | Shows a list |
| `/security raid` | `join-count`, `join-seconds`, `action`, `pause-invites`, `quiet-minutes` | Owner or trusted | Tunes raid mode |
| `/security antinuke set` | `window-seconds`, `lock-server`, `auto-restore` | Owner or trusted | Tunes anti-nuke |
| `/security antinuke limit` | `action`*, `count`* (0 to 100) | Owner or trusted | How many times someone may do an action in the window (0: never) |
| `/security heat` | `strictness`, `action`, `timeout-minutes`, `escalate`, `panic-members`, `mention-lockdown` | Owner or trusted | Tunes the spam heat system |
| `/security alert-dms` | `on`* | Owner or trusted | DM critical alerts to the owner and trusted users |
| `/security snapshot` | | Manage Server | Saves roles and channels |
| `/security restore` | | Owner or trusted | Recreates deleted roles and channels from the latest snapshot |
| `/lockdown channel` | `channel` (default: this one) | Manage Channels | Locks a channel |
@@ -173,8 +202,10 @@ In this table "Manage Server" also lets Administrators and trusted members throu
| Command | Needs | What it does |
| --- | --- | --- |
| `/beta` | The bot's owner | Shows how beta access is set up. Registered in the beta server only |
| `/beta status` | The bot's owner, **in a DM with the bot** | Shows how beta access is set up |
| `/beta set` | The bot's owner, in a DM | `server-id`* and `role-id`*: chooses the beta server and the Beta Tester role |
| `/beta off` | The bot's owner, in a DM | Turns beta features off for everyone |
| `/beta servers` | The bot's owner, in a DM | Lists the servers that opted in |
| `/beta remove-server` | The bot's owner, in a DM | `server-id`*: takes a server out of the beta |
Other beta commands appear only in the beta server and in servers whose owner opted in, and only testers can run them.
Beta features come and go. See [Beta features](beta-features.md).
`/beta` is registered globally but only works in a direct message with the bot, so it is in no server's menu. A server opts itself in with `/config beta-features` (its owner). Other beta commands appear only in the beta server and in servers that opted in, and only testers can run them. See [Beta features](beta-features.md).
+3 -1
View File
@@ -190,7 +190,9 @@ added, err := services.Global.Meeting.AddLobby(ctx, guildID, channelID)
**6. Enforce limits in the database when you can.** The 10-lobby limit is a trigger, so the bot and anything else writing to the table cannot disagree and two simultaneous requests cannot both slip through.
**7. Default to off.** A module that does something to a server without being asked (posts messages, moderates, grants roles) must start disabled and be turned on by an admin.
**7. Give it commands.** Every setting must be visible and changeable from Discord (a slash command to set it, and a way to see it). The bot has to work completely without a dashboard, so never add a setting that only a dashboard or SQL can change. Put the options in the command definition, and `internal/commands/limits_test.go` will tell you if it goes over Discord's limits.
**8. Default to off.** A module that does something to a server without being asked (posts messages, moderates, grants roles) must start disabled and be turned on by an admin.
## React to a gateway event
+4 -4
View File
@@ -29,13 +29,13 @@ Rules that hold everywhere in this module:
- Roles that are configured (such as the quarantine role) are checked when they are used, not only when saved. A quarantine role that gained dangerous permissions is not assigned.
- Messages ping only the configured alert role, and names are escaped.
Thresholds, Join Gate filters and heat settings are rows in the `security_settings` table (the hosted Velox edits them on its dashboard; see [Settings without a dashboard](self-hosting.md#settings-without-a-dashboard)). The bot's role must be above the roles it protects against: it can only quarantine members whose highest role is below its own.
Every threshold, filter and action is a command: `/security settings` shows them all, and `/security joingate`, `words`, `raid`, `antinuke`, `heat` and `alert-dms` change them (owner or trusted users only). `/security history` shows what the system has done. The bot's role must be above the roles it protects against: it can only quarantine members whose highest role is below its own.
## Tickets
Support tickets in private threads. Code: `internal/tickets`, commands in `internal/commands/tickets`.
1. An admin runs `/tickets setup`, adds categories with `/tickets category add` (each with its own staff role), and posts the panel with `/tickets panel`.
1. An admin runs `/tickets setup`, adds categories with `/tickets category add` (each with its own staff role), and posts the panel with `/tickets panel`. `/tickets settings` changes the panel text and the open ticket limit and turns tickets on or off, `/tickets category edit` changes a category, and `/tickets list` shows tickets.
2. The panel has a category dropdown and a **Create ticket** button. A short form asks what the ticket is about.
3. A **private thread** is created in the ticket channel. The opener and the category's staff role can see it.
4. Staff can `/ticket claim`. Anyone involved can `/ticket add` someone, staff can `/ticket remove`, and the opener or staff can `/ticket close [reason]`.
@@ -68,8 +68,8 @@ Lavalink-powered playback. Code: `internal/music`, commands in `internal/command
- `/play` takes a name or a URL, `/queue` shows the queue, `/volume` sets 0 to 150 percent. These need a role called **DJ**.
- The now-playing message has buttons for playback and repeat. Buttons also check for the DJ role.
- `/music toggle` (Manage Server) turns music on or off in a server.
- The queue and now-playing state are kept in the database, and playback can be controlled by writing to the `music_commands` table. The hosted Velox dashboard uses this (see [Database](database.md)).
- `/music toggle` (Manage Server) turns music on or off in a server. `/music skip`, `pause`, `resume`, `stop`, `repeat`, `remove` and `move` control playback and the queue (DJ role).
- The queue and now-playing state are kept in the database, and playback can also be controlled by writing to the `music_commands` table. The hosted Velox dashboard uses this (see [Database](database.md)); you never need it, the commands above do the same.
- Music needs a Lavalink server. Without `LAVALINK_HOST` the rest of the bot works and music does not.
## Meeting rooms
+3
View File
@@ -106,6 +106,9 @@ Nothing is on by default. In your server:
- `/slvl toggle` for leveling.
- `/config setlogchannel` and `/config enablelogging` for logs.
- `/music toggle` for music (needs Lavalink).
- `/config toggletwitch` and `/config addstreamer` for Twitch alerts.
`/security settings`, `/tickets settings`, `/slvl status` and `/config view` show what is set.
The [Features](features.md) page explains each module, and the [Command reference](commands.md) lists every option.
+14 -28
View File
@@ -8,7 +8,7 @@ You will run:
- **PostgreSQL**, started by the same Compose file.
- Optionally **Lavalink**, if you want music.
The hosted Velox also has a web dashboard. It is proprietary and not part of this repository, so it is not something you can host. You do not need it. See [Settings without a dashboard](#settings-without-a-dashboard).
The hosted Velox also has a web dashboard. It is proprietary and not part of this repository, so it is not something you can host, and you do not need it: everything it does is also a command. See [Everything is a command](#everything-is-a-command).
## Before you start
@@ -154,38 +154,24 @@ Run it on a schedule and keep copies off the server. To restore into an empty da
Discord requires you to **verify** your application once the bot is in 100 servers, and to be approved for the privileged intents (Server Members, Message Content). Plan for that if you intend to run a public bot. For a bot in a few servers you manage yourself, nothing is needed.
## Settings without a dashboard
## Everything is a command
The bot works fully on its own, and every command in the [Command reference](commands.md) does. Most things are set with slash commands. A few settings that the hosted Velox sets on its dashboard have no command, so you set them in the database. They are plain rows, described in `schema.sql`.
The bot is complete on its own: every setting and every action is a slash command, and nothing needs a web dashboard. The [Command reference](commands.md) lists them all. The ones you will want first:
**Security thresholds and filters.** The defaults are in `security_settings`. To change a server's settings, insert or update its row (the columns are documented in `schema.sql`, and invalid values are refused):
- **Modules:** `/security toggle`, `/tickets setup`, `/slvl toggle`, `/music toggle`, `/config enablelogging`, `/config toggletwitch`.
- **Seeing what is set:** `/security settings`, `/tickets settings`, `/slvl status`, `/config view`.
- **Tuning security:** `/security joingate`, `words`, `raid`, `antinuke` and `heat` change every threshold, filter and action (owner or trusted users only).
- **The beta program:** the bot's owner sets it up in a direct message with the bot, see below.
```sql
INSERT INTO security_settings (guild_id, raid_join_count, raid_join_seconds)
VALUES (123456789012345678, 15, 10)
ON CONFLICT (guild_id) DO UPDATE
SET raid_join_count = EXCLUDED.raid_join_count, raid_join_seconds = EXCLUDED.raid_join_seconds;
### Setting up beta features
Beta features are off until you choose a beta server and a Beta Tester role. As the owner of the Discord application, open a **direct message** with your bot and run:
```
/beta set server-id:<the server's ID> role-id:<the role's ID>
```
Protections are switched on with `/security toggle`, not with SQL.
**Beta features.** Beta features are off until you choose a beta server and a Beta Tester role. Run this once, with your own IDs (the bot reads it every 30 seconds):
```sql
INSERT INTO beta_settings (id, guild_id, role_id) VALUES (TRUE, 123456789012345678, 234567890123456789)
ON CONFLICT (id) DO UPDATE SET guild_id = EXCLUDED.guild_id, role_id = EXCLUDED.role_id;
```
To let another server's beta features on, add it (its owner's agreement is yours to arrange):
```sql
INSERT INTO beta_guilds (guild_id, enabled_by) VALUES (345678901234567890, 123456789012345678)
ON CONFLICT DO NOTHING;
```
See [Beta features](beta-features.md) for what this does.
**Queue and panel requests.** The `*_commands` tables let another program ask the bot to skip a song, post the ticket panel or end a raid. See [Database](database.md#dashboard-to-bot-settings-and-requests) if you want to build something on them.
(In Discord, turn on Developer Mode, then right-click the server or role and choose Copy ID.) `/beta status` shows the result, `/beta off` turns the beta off, `/beta servers` lists the servers that opted in. A server's owner opts the server in with `/config beta-features`. The bot reads the change within 30 seconds. See [Beta features](beta-features.md).
## Making it your own
+1 -1
View File
@@ -50,7 +50,7 @@ A dashboard (the hosted Velox has a proprietary one) or any tool of yours talks
## Beta features are not available to a tester
Go through [Beta features](beta-features.md): is the Beta Tester role set in `beta_settings`? Is the person in the **beta server**, and do they have the role there? Is the server where they use it the beta server, or is it listed in `beta_guilds`? Has 30 seconds passed? `/beta` (owner only) shows how access is set up.
Go through [Beta features](beta-features.md): has the bot's owner set the Beta Tester role (`/beta status` in a DM with the bot shows it)? Is the person in the **beta server**, and do they have the role there? Is the server where they use it the beta server, or has its owner run `/config beta-features enabled:true`? Has 30 seconds passed?
## Getting more detail
+179 -129
View File
@@ -2,6 +2,7 @@ package help
import (
"log"
"unicode/utf8"
"github.com/bwmarrin/discordgo"
)
@@ -11,6 +12,179 @@ var Help = &discordgo.ApplicationCommand{
Description: "Get help with the bot",
}
// A message's embeds share a limit of 6000 characters, and one embed holds at
// most 25 fields of 1024 characters. The reference is longer than that, so it
// is sent as a few messages.
const (
maxEmbedChars = 5000
maxFieldChars = 1024
)
func fields() []*discordgo.MessageEmbedField {
return []*discordgo.MessageEmbedField{
{
Name: "Fun",
Value: "" +
"`/ping` — Pong!\n" +
"`/joke [type]` — Random joke (optional category)\n" +
"`/coinflip` — Flip a coin\n" +
"`/dice <expr>` — Roll dice (e.g. `d20`, `2d20+1`, `2#d20+1`)\n" +
"`/rps <hand>` — Rock Paper Scissors (server only)\n" +
"`/rpsstats` — Your RPS score (server only)\n" +
"`/rpsleaderboard` — Top RPS scores (server only)",
},
{
Name: "Music (requires **DJ** role)",
Value: "" +
"`/play <query>` — Play a song (search or URL)\n" +
"`/queue` — Show the current queue\n" +
"`/volume <0-150>` — Set playback volume\n" +
"`/music skip|pause|resume|stop` — Control playback\n" +
"`/music repeat <off|song|queue>`, `/music remove <n>`, `/music move <from> <to>` — Repeat and reorder\n" +
"`/music toggle` — Turn music on or off (**Manage Server**)",
},
{
Name: "Scheduling (1:1 sessions)",
Value: "" +
"`/schedule create user:@someone datetime:\"YYYY-MM-DD HH:MM\" [description]` — Create a session\n" +
"`/schedule list` — List your upcoming sessions\n" +
"`/schedule reschedule id:<id> datetime:\"YYYY-MM-DD HH:MM\"` — Request a new time\n\n" +
"Invites are sent via DM; react with ✅ / ❌ / 🔁 to respond.",
},
{
Name: "Timezone",
Value: "" +
"`/timezone set <IANA>` — Set your timezone (e.g. `Europe/Lisbon`)\n" +
"`/timezone show` — Show your timezone",
},
{
Name: "Projects",
Value: "" +
"`/projects list` — List active projects\n" +
"`/projects create name:\"...\" [description]` — Create a project (**Manage Server**)\n" +
"`/projects add-helper project-id:<id> user:@someone` — Add helper (**Manage Server**)",
},
{
Name: "Tickets",
Value: "" +
"Open a ticket from the panel: you get a private thread with the staff.\n" +
"`/ticket add user:@someone` — Add someone to your ticket\n" +
"`/ticket close [reason]` — Close the ticket (opener or staff)\n" +
"`/ticket claim`, `/ticket remove user:@someone` — Staff only\n" +
"`/tickets setup`, `/tickets settings`, `/tickets category add|edit|remove|list`, `/tickets panel`, `/tickets list` — Set up (**Manage Server**)",
},
{
Name: "Security (anti-raid & anti-nuke)",
Value: "" +
"`/security status`, `/security settings`, `/security history` — What's on, every setting, what happened\n" +
"`/security toggle` — Join Gate, raid mode, anti-nuke, heat on/off (off: owner or trusted)\n" +
"`/security joingate`, `words`, `raid`, `antinuke`, `heat`, `alert-dms` — Tune thresholds and filters (owner or trusted)\n" +
"`/security alerts`, `/security quarantine-role`, `/security release`, `/security end-raid` — Owner or trusted\n" +
"`/security trust add|remove` — Owner only\n" +
"`/security snapshot` — Save roles and channels; `/security restore` — Rebuild what a nuke deleted (owner or trusted)\n" +
"`/lockdown channel|server|joins`, `/unlock …` (unlock: owner or trusted)",
},
{
Name: "Meetings (**Manage Server**)",
Value: "" +
"`/meeting add-lobby channel:#voice` — Add a voice lobby that creates meetings (up to 10)\n" +
"`/meeting remove-lobby channel:#voice` — Stop using a voice lobby\n" +
"`/meeting lobbies` — List the voice lobbies\n" +
"`/meeting lock` — Lock your meeting room\n" +
"`/meeting lock-private` — Lock + hide your meeting room\n" +
"`/meeting unlock` — Unlock your meeting room\n" +
"`/meeting invite user:@someone` — Allow a user to join\n" +
"`/meeting uninvite user:@someone` — Remove a user's permission",
},
{
Name: "Moderation (focused tools)",
Value: "" +
"`/moderation purge amount:<1-100>` — Delete recent messages (**Manage Messages**)\n" +
"`/moderation timeout user:@someone duration:<10m|1h|...> [reason]` — Timeout member (**Moderate Members**)\n" +
"`/moderation kick user:@someone [reason]` — Kick member (**Kick Members**)\n" +
"`/moderation ban user:@someone [delete_days] [reason]` — Ban member (**Ban Members**)\n" +
"`/moderation unban user_id:<id> [reason]` — Unban by user ID (**Ban Members**)\n" +
"`/moderation untimeout user:@someone [reason]` — Remove timeout (**Moderate Members**)\n" +
"`/moderation slowmode channel:#channel seconds:<0-21600>` — Set slowmode (**Manage Channels**)\n" +
"`/moderation bans`, `/moderation timeouts` — Who is banned, who is timed out",
},
{
Name: "Leveling",
Value: "" +
"`/slvl toggle` — Enable/disable leveling (**Manage Server**)\n" +
"`/slvl set user:@someone level:<n>` — Set user level (**Manage Server**)\n" +
"`/slvl set-channel channel:#channel` — Level-up message channel (**Manage Server**)\n" +
"`/slvl set-message message:\"...\"` — Custom level-up message (**Manage Server**)\n" +
"`/slvl set-reward level:<n> role:@role`, `/slvl remove-reward level:<n>` — Role rewards (**Manage Server**)\n" +
"`/slvl status` — Show the leveling settings (**Manage Server**)\n" +
"`/rank [user]` — Your rank card (server only)\n" +
"`/leaderboard` — Top levels (server only)\n" +
"`/rewards` — List configured rewards (server only)",
},
{
Name: "Twitch live notifications (**Manage Server**)",
Value: "" +
"`/config addstreamer username:<twitch-name>` — Add a streamer\n" +
"`/config removestreamer username:<twitch-name>` — Remove a streamer\n" +
"`/config settwitchnotificationchannel channel:#channel` — Set notification channel\n" +
"`/config toggletwitch enabled:<bool>`, `/config streamers` — Turn alerts on or off, list streamers",
},
{
Name: "Welcome / onboarding (**Manage Server**)",
Value: "" +
"`/config setwelcomechannel channel:#channel` — Channel for welcome messages\n" +
"`/config setwelcomemessage message:\"...\"` — Welcome text (supports `{user}`, `{server}`)\n" +
"`/config setwelcomedm message:\"...\"` — DM template (supports `{user}`, `{server}`)\n" +
"`/config setwelcomegif url:<image-url>` — GIF/image used in welcome embed\n" +
"`/config setdefaultrole role:@role` — Role granted to new members\n" +
"`/config view`, `/config clear setting:<...>` — See or clear these settings",
},
{
Name: "Logging (**Manage Server**)",
Value: "" +
"`/config setlogchannel channel:#channel` — Set log output channel\n" +
"`/config enablelogging` — Enable event/moderation logging\n" +
"`/config disablelogging` — Disable event/moderation logging",
},
{
Name: "Beta features",
Value: "" +
"`/config beta-features [enabled]` — Let testers use beta features here (server owner only)\n" +
"Bot owner: `/beta` in a DM with the bot — set the beta server and role, list or remove servers",
},
{
Name: "Other",
Value: "`/help` — Show this help in DMs",
},
}
}
func fieldSize(f *discordgo.MessageEmbedField) int {
return utf8.RuneCountInString(f.Name) + utf8.RuneCountInString(f.Value)
}
// embeds splits the reference into embeds that fit Discord's limits.
func embeds(author *discordgo.MessageEmbedAuthor) []*discordgo.MessageEmbed {
var out []*discordgo.MessageEmbed
cur := &discordgo.MessageEmbed{
Title: "Velox Bot Help",
Description: "Command reference. Some commands require server permissions or roles.",
Color: 0xFFA500,
Author: author,
}
size := utf8.RuneCountInString(cur.Title) + utf8.RuneCountInString(cur.Description) + utf8.RuneCountInString(author.Name)
for _, f := range fields() {
if size+fieldSize(f) > maxEmbedChars || len(cur.Fields) >= 25 {
out = append(out, cur)
cur = &discordgo.MessageEmbed{Title: "Velox Bot Help (continued)", Color: 0xFFA500}
size = utf8.RuneCountInString(cur.Title)
}
cur.Fields = append(cur.Fields, f)
size += fieldSize(f)
}
return append(out, cur)
}
func HelpHandler(s *discordgo.Session, i *discordgo.InteractionCreate) {
s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
@@ -40,134 +214,10 @@ func HelpHandler(s *discordgo.Session, i *discordgo.InteractionCreate) {
URL: "https://gitea.fernandovideira.com/FernandoJVideira/velox-bot",
}
embed := &discordgo.MessageEmbed{
Title: "Velox Bot Help",
Description: "Command reference. Some commands require server permissions or roles.",
Color: 0xFFA500,
Fields: []*discordgo.MessageEmbedField{
{
Name: "Fun",
Value: "" +
"`/ping` — Pong!\n" +
"`/joke [type]` — Random joke (optional category)\n" +
"`/coinflip` — Flip a coin\n" +
"`/dice <expr>` — Roll dice (e.g. `d20`, `2d20+1`, `2#d20+1`)\n" +
"`/rps <hand>` — Rock Paper Scissors (server only)\n" +
"`/rpsstats` — Your RPS score (server only)\n" +
"`/rpsleaderboard` — Top RPS scores (server only)",
},
{
Name: "Music (requires **DJ** role)",
Value: "" +
"`/play <query>` — Play a song (search or URL)\n" +
"`/queue` — Show the current queue\n" +
"`/volume <0-150>` — Set playback volume",
},
{
Name: "Scheduling (1:1 sessions)",
Value: "" +
"`/schedule create user:@someone datetime:\"YYYY-MM-DD HH:MM\" [description]` — Create a session\n" +
"`/schedule list` — List your upcoming sessions\n" +
"`/schedule reschedule id:<id> datetime:\"YYYY-MM-DD HH:MM\"` — Request a new time\n\n" +
"Invites are sent via DM; react with ✅ / ❌ / 🔁 to respond.",
},
{
Name: "Timezone",
Value: "" +
"`/timezone set <IANA>` — Set your timezone (e.g. `Europe/Lisbon`)\n" +
"`/timezone show` — Show your timezone",
},
{
Name: "Projects",
Value: "" +
"`/projects list` — List active projects\n" +
"`/projects create name:\"...\" [description]` — Create a project (**Manage Server**)\n" +
"`/projects add-helper project-id:<id> user:@someone` — Add helper (**Manage Server**)",
},
{
Name: "Tickets",
Value: "" +
"Open a ticket from the panel: you get a private thread with the staff.\n" +
"`/ticket add user:@someone` — Add someone to your ticket\n" +
"`/ticket close [reason]` — Close the ticket (opener or staff)\n" +
"`/ticket claim`, `/ticket remove user:@someone` — Staff only\n" +
"`/tickets setup`, `/tickets category add|remove|list`, `/tickets panel` — Set up (**Manage Server**)",
},
{
Name: "Security (anti-raid & anti-nuke)",
Value: "" +
"`/security status` — What's on and who is trusted\n" +
"`/security toggle` — Join Gate, raid mode, anti-nuke on/off (off: owner or trusted)\n" +
"`/security alerts`, `/security quarantine-role`, `/security release`, `/security end-raid` — Owner or trusted\n" +
"`/security trust add|remove` — Owner only\n" +
"`/security snapshot` — Save roles and channels; `/security restore` — Rebuild what a nuke deleted (owner or trusted)\n" +
"`/lockdown channel|server|joins`, `/unlock …` (unlock: owner or trusted)",
},
{
Name: "Meetings (**Manage Server**)",
Value: "" +
"`/meeting add-lobby channel:#voice` — Add a voice lobby that creates meetings (up to 10)\n" +
"`/meeting remove-lobby channel:#voice` — Stop using a voice lobby\n" +
"`/meeting lobbies` — List the voice lobbies\n" +
"`/meeting lock` — Lock your meeting room\n" +
"`/meeting lock-private` — Lock + hide your meeting room\n" +
"`/meeting unlock` — Unlock your meeting room\n" +
"`/meeting invite user:@someone` — Allow a user to join\n" +
"`/meeting uninvite user:@someone` — Remove a user's permission",
},
{
Name: "Moderation (focused tools)",
Value: "" +
"`/moderation purge amount:<1-100>` — Delete recent messages (**Manage Messages**)\n" +
"`/moderation timeout user:@someone duration:<10m|1h|...> [reason]` — Timeout member (**Moderate Members**)\n" +
"`/moderation kick user:@someone [reason]` — Kick member (**Kick Members**)\n" +
"`/moderation ban user:@someone [delete_days] [reason]` — Ban member (**Ban Members**)\n" +
"`/moderation unban user_id:<id> [reason]` — Unban by user ID (**Ban Members**)\n" +
"`/moderation untimeout user:@someone [reason]` — Remove timeout (**Moderate Members**)\n" +
"`/moderation slowmode channel:#channel seconds:<0-21600>` — Set slowmode (**Manage Channels**)",
},
{
Name: "Leveling",
Value: "" +
"`/slvl toggle` — Enable/disable leveling (**Manage Server**)\n" +
"`/slvl set user:@someone level:<n>` — Set user level (**Manage Server**)\n" +
"`/slvl set-channel channel:#channel` — Level-up message channel (**Manage Server**)\n" +
"`/slvl set-message message:\"...\"` — Custom level-up message (**Manage Server**)\n" +
"`/slvl set-reward level:<n> role:@role` — Role rewards (**Manage Server**)\n" +
"`/rank [user]` — Your rank card (server only)\n" +
"`/leaderboard` — Top levels (server only)\n" +
"`/rewards` — List configured rewards (server only)",
},
{
Name: "Twitch live notifications (**Manage Server**)",
Value: "" +
"`/config addstreamer username:<twitch-name>` — Add a streamer\n" +
"`/config removestreamer username:<twitch-name>` — Remove a streamer\n" +
"`/config settwitchnotificationchannel channel:#channel` — Set notification channel",
},
{
Name: "Welcome / onboarding (**Manage Server**)",
Value: "" +
"`/config setwelcomechannel channel:#channel` — Channel for welcome messages\n" +
"`/config setwelcomemessage message:\"...\"` — Welcome text (supports `{user}`, `{server}`)\n" +
"`/config setwelcomedm message:\"...\"` — DM template (supports `{user}`, `{server}`)\n" +
"`/config setwelcomegif url:<image-url>` — GIF/image used in welcome embed\n" +
"`/config setdefaultrole role:@role` — Role granted to new members",
},
{
Name: "Logging (**Manage Server**)",
Value: "" +
"`/config setlogchannel channel:#channel` — Set log output channel\n" +
"`/config enablelogging` — Enable event/moderation logging\n" +
"`/config disablelogging` — Disable event/moderation logging",
},
{
Name: "Other",
Value: "`/help` — Show this help in DMs",
},
},
Author: &author,
for _, e := range embeds(&author) {
if _, err := s.ChannelMessageSendEmbed(dmChannel.ID, e); err != nil {
log.Printf("Error sending help: %v", err)
return
}
}
s.ChannelMessageSendEmbed(dmChannel.ID, embed)
}
+40
View File
@@ -0,0 +1,40 @@
package help
import (
"testing"
"unicode/utf8"
"github.com/bwmarrin/discordgo"
)
// Discord refuses an embed that is over its limits, and then /help would send
// nothing. This catches it when a command is added to the text.
func TestHelpFitsDiscordsLimits(t *testing.T) {
list := embeds(&discordgo.MessageEmbedAuthor{Name: "Velox Bot"})
if len(list) == 0 {
t.Fatal("no help")
}
seen := 0
for n, e := range list {
total := utf8.RuneCountInString(e.Title) + utf8.RuneCountInString(e.Description)
if len(e.Fields) > 25 {
t.Errorf("embed %d has %d fields, at most 25", n, len(e.Fields))
}
for _, f := range e.Fields {
seen++
if l := utf8.RuneCountInString(f.Value); l == 0 || l > maxFieldChars {
t.Errorf("field %q has %d characters, must be 1 to %d", f.Name, l, maxFieldChars)
}
if l := utf8.RuneCountInString(f.Name); l == 0 || l > 256 {
t.Errorf("field name %q has %d characters, at most 256", f.Name, l)
}
total += fieldSize(f)
}
if total > 6000 {
t.Errorf("embed %d has %d characters, Discord allows 6000", n, total)
}
}
if seen != len(fields()) {
t.Errorf("%d fields shown of %d", seen, len(fields()))
}
}