- C# 99.7%
- Shell 0.2%
- PowerShell 0.1%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
| src/Pyria | ||
| .gitignore | ||
| LICENSE | ||
| Pyria.sln | ||
| README.md | ||
Pyria
A general-purpose Discord bot written in C#.
Caution
This is an alpha system so please use at your own risk the EllieBotDevs team accepts no responsibility for any losses caused by using this project in its current state. Please see the LICENSE section for more details.
Modules
| Module | Commands |
|---|---|
| ⚙️ Administration | /admin settings, /admin levelup-channel, /admin ticket-category, /admin ticket-log, /admin ticket-role |
| 🛡️ Moderation | /mod kick, /mod ban, /mod unban, /mod mute, /mod unmute, /mod warn, /mod warnings, /mod delwarn, /mod purge, /mod slowmode |
| 🎵 Music | /music play, /music skip, /music stop, /music queue, /music volume, /music loop, /music nowplaying, /music remove |
| 🎮 Games | /game roll, /game coinflip, /game rps, /game 8ball, /game trivia, /game answer, /game hangman, /game guess |
| 📖 Help | /help commands, /help ping, /help botinfo, /help serverinfo, /help userinfo |
| 🎫 Tickets | /ticket open, /ticket close, /ticket adduser, /ticket removeuser, /ticket list |
| 📊 Xp | /xp rank, /xp leaderboard |
| 💬 Custom Responses | /cr add, /cr addglobal, /cr remove, /cr removeid, /cr list, /cr info |
Quick Start
1. Prerequisites
- .NET 9 SDK
- A Discord bot token from the Discord Developer Portal
Optional
- EF Core CLI tools (for managing migrations):
dotnet tool install --global dotnet-ef
2. Configure
Edit config.yml and set your bot token:
bot:
token: "YOUR_ACTUAL_BOT_TOKEN"
3. Discord Developer Portal
In your application's Bot tab, enable these privileged gateway intents:
- ✅ Server Members Intent
- ✅ Message Content Intent
4. Run
dotnet run
Or build a release binary:
dotnet build -c Release
dotnet ./bin/Release/net9.0/DiscordBot.dll
On first run the bot will:
- Check and migrate
config.ymlto the latest version (writing a timestamped backup first) - Apply any pending EF Core database migrations
- Register slash commands globally with Discord
Slash command propagation: Global registration can take up to an hour to appear in all servers. For instant updates during development, replace
RegisterCommandsGloballyAsync()withRegisterCommandsToGuildAsync(yourGuildId)inBotService.cs.
Project Structure
Pyria/
├── config.yml
├── Program.cs # Entry point + DI container
├── Configuration/
│ ├── BotConfig.cs # Typed config models + YAML loader
│ └── ConfigMigrator.cs # Automatic config version migrations
├── Models/
│ ├── UserLevel.cs
│ ├── Warning.cs
│ ├── ModLog.cs
│ ├── Ticket.cs
│ ├── CustomResponse.cs
│ ├── GuildSettings.cs
│ └── MusicTrack.cs # In-memory only — not persisted
├── Database/
│ ├── BotDbContext.cs # EF Core DbContext + entity configuration
│ ├── BotDbContextFactory.cs # Design-time factory for dotnet ef tooling
│ ├── DatabaseService.cs # LINQ query methods over BotDbContext
│ └── Migrations/ # Generated by: dotnet ef migrations add
│ ├── XXXXXXXXXXXXXX_init.cs
│ ├── BotDbContextModelSnapshot.cs
│ └── README.md
├── Services/
│ ├── BotService.cs # Gateway events + interaction dispatch
│ ├── LevelingService.cs # XP grant + level-up logic
│ ├── MusicService.cs # Per-guild in-memory queue
│ ├── CustomResponseService.cs # Trigger matching + CRUD
│ └── GuildSettingsService.cs # Per-guild settings with YAML fallback
└── Commands/
├── Administration/AdministrationModule.cs
├── Moderation/ModerationModule.cs
├── Music/MusicModule.cs
├── Games/GamesModule.cs
├── Help/HelpModule.cs
├── Tickets/TicketsModule.cs
├── Xp/XpModule.cs
└── CustomResponses/CustomResponseModule.cs
Configuration (config.yml)
The config file is versioned. On startup the bot compares config_version against the current version and automatically applies any pending migrations, writing a timestamped backup first (e.g. config.v1.20260509_120000.bak.yml). Do not edit config_version manually.
config_version: 2
bot:
token: "YOUR_BOT_TOKEN_HERE"
# {guild_count} is replaced at runtime
status: "/help commands | Serving {guild_count} servers"
database:
path: "bot.db"
leveling:
xp_per_message_min: 15
xp_per_message_max: 25
xp_cooldown_seconds: 60
# Global fallback channel name used when no per-guild override exists in the DB.
# Prefer using /admin levelup-channel in each server instead.
default_level_up_channel_name: ""
moderation:
# Name of the role applied by /mod mute
mute_role: "Muted"
# Global fallback mod log channel name
default_log_channel_name: ""
music:
max_queue_size: 100
volume_default: 50
Ticket settings (category, log channel, support role) and per-guild level-up channels are configured at runtime via
/adminand stored in the database — they no longer live inconfig.yml.
Adding a config version migration
- Bump
CurrentVersioninConfigMigrator.cs - Add a new entry to the
_migrationsdictionary - Write the migration method — it receives the raw YAML as a
Dictionary<object, object?>so you can rename, remove, or add keys freely
public const int CurrentVersion = 3;
private static readonly Dictionary<int, Action<...>> _migrations = new()
{
[1] = MigrateV2,
[2] = MigrateV3, // ← new
};
private static void MigrateV3(Dictionary<object, object?> raw, string path)
{
if (raw.TryGetValue("music", out var m) && m is Dictionary<object, object?> music)
music.TryAdd("announce_channel_name", "");
}
Database & Migrations
The bot uses Entity Framework Core 9 with the SQLite provider. All schema changes live in Database/Migrations/ as individual migration files. Migrations are applied automatically on startup.
Workflow for schema changes
# 1. Edit your model(s) in Models/
# 2. Generate the migration
dotnet ef migrations add DescriptiveMigrationName
# 3. Optional — apply manually (the bot does this on startup anyway)
dotnet ef database update
Rolling back
dotnet ef database update PreviousMigrationName
dotnet ef migrations remove
SQLite and ulong
SQLite has no native unsigned integer type. All Discord snowflake IDs (ulong) are stored as signed long and converted transparently via EF Core value converters defined in BotDbContext.OnModelCreating. No casting is needed in query code — EF Core handles it automatically.
EF Core 9 pending model warning
EF Core 9 raises PendingModelChangesWarning if the model snapshot doesn't exactly match the live PyriaDbContext. This is suppressed in BotDbContext.OnConfiguring to prevent a startup crash when the snapshot is slightly out of sync after a migration. If you see it as a console warning, run dotnet ef migrations add to bring the snapshot up to date.
Per-Server Administration
All server-specific settings are managed via /admin and stored in the database, so they persist across restarts and are fully independent per server. All /admin commands require the Administrator permission.
| Command | Description |
|---|---|
/admin settings |
Shows the current configuration for this server |
/admin levelup-channel [channel] |
Sets or clears the level-up notification channel |
/admin ticket-category [category] |
Sets the category new ticket channels are created under |
/admin ticket-log [channel] |
Sets or clears the ticket event log channel |
/admin ticket-role [role] |
Sets the support role that can see all ticket channels |
Custom Responses
Custom responses let moderators make the bot reply to trigger phrases without any code changes. Server-specific responses take priority over globals.
| Command | Permission | Description |
|---|---|---|
/cr add <trigger> <response> [match_type] |
Manage Messages | Adds a server-scoped response |
/cr addglobal <trigger> <response> [match_type] |
Administrator | Adds a response across all servers |
/cr remove <trigger> |
Manage Messages | Removes a server-scoped response by trigger |
/cr removeid <id> |
Manage Messages | Removes any response by ID |
/cr list [page] |
Manage Messages | Lists all responses for this server |
/cr info <id> |
Manage Messages | Shows full details of a response |
match_type is either contains (trigger appears anywhere in the message) or exact (full message must equal the trigger).
Xp
XP is granted once per message after the configured cooldown. The amount is randomised between xp_per_message_min and xp_per_message_max.
XP required to advance from level n to n+1:
XP(n) = 5n² + 50n + 100
| Transition | XP needed |
|---|---|
| 0 → 1 | 100 |
| 1 → 2 | 155 |
| 5 → 6 | 475 |
| 10 → 11 | 1,100 |
Adding Real Audio Playback
MusicModule and MusicService fully manage the per-guild queue, volume, loop state, and skip logic. Actual voice audio requires additional setup:
- Install FFmpeg and add it to your
PATH - Install yt-dlp for YouTube/URL resolution
- In
BotService, connect to the user's voice channel viaawait voiceChannel.ConnectAsync() - Stream PCM audio through the returned
IAudioClient
Required Bot Permissions
| Permission | Used by |
|---|---|
| View Channels | All modules |
| Send Messages | All modules |
| Embed Links | All modules |
| Read Message History | /mod purge, Tickets |
| Manage Messages | /mod purge, /mod slowmode |
| Manage Channels | Tickets |
| Manage Roles | /mod mute, /mod unmute |
| Kick Members | /mod kick |
| Ban Members | /mod ban, /mod unban |
| Connect / Speak | Music (when audio playback is wired up) |
Invite URL scopes: bot + applications.commands
applications.commandsis required for slash commands to register in servers.
Gateway Intents
The bot requests only the intents it actually uses:
| Intent | Purpose |
|---|---|
Guilds |
Guild/channel/role resolution |
GuildMembers |
Welcome messages, member lookups |
GuildMessages |
XP grants, custom response matching |
GuildMessageReactions |
Available for future use |
GuildVoiceStates |
Detecting the user's voice channel for music |
MessageContent |
Reading message text for XP and custom responses |
LICENSE
Copyright 2026 EllieBotDevs
Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
To read the entire license look in the LICENSE file.