From 3064e9301115bb74993d170ad8a075b8d8af4a82 Mon Sep 17 00:00:00 2001 From: Garret Patti <42485635+garretpatti@users.noreply.github.com> Date: Sat, 4 Jul 2026 20:43:38 -0400 Subject: [PATCH] init commit --- .env.example | 2 + .github/copilot-instructions.md | 45 +++++++++ .gitignore | 50 ++++++++++ .vscode/tasks.json | 44 +++++++++ AGENTS.md | 32 +++++++ Commands/GeneralCommands.cs | 71 +++++++++++++++ Dockerfile | 18 ++++ Program.cs | 50 ++++++++++ README.md | 157 ++++++++++++++++++++++++++++++++ Services/BotService.cs | 102 +++++++++++++++++++++ TentacleBot.csproj | 27 ++++++ docker-compose.yml | 9 ++ 12 files changed, 607 insertions(+) create mode 100644 .env.example create mode 100644 .github/copilot-instructions.md create mode 100644 .gitignore create mode 100644 .vscode/tasks.json create mode 100644 AGENTS.md create mode 100644 Commands/GeneralCommands.cs create mode 100644 Dockerfile create mode 100644 Program.cs create mode 100644 README.md create mode 100644 Services/BotService.cs create mode 100644 TentacleBot.csproj create mode 100644 docker-compose.yml diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..562eb11 --- /dev/null +++ b/.env.example @@ -0,0 +1,2 @@ +# Copy this file to .env and set your real values. +DISCORD_BOT_TOKEN=YOUR_DISCORD_BOT_TOKEN_HERE diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000..d81c33f --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,45 @@ +# TentacleBot - Discord .NET Bot + +A Discord bot project built with Discord.Net and .NET 8+, featuring dependency injection and slash commands. + +## Setup Instructions + +1. Verify .NET SDK is installed (version 8.0 or higher) +2. Install project dependencies: `dotnet restore` +3. Configure bot token in `appsettings.json` +4. Build the project: `dotnet build` +5. Run the bot: `dotnet run` + +## Project Structure + +- **Program.cs**: Bot initialization and dependency injection setup +- **Services/BotService.cs**: Core bot service managing connection and event handlers +- **Commands/GeneralCommands.cs**: Example slash command module +- **appsettings.json**: Configuration file for bot token and prefix + +## Key Features + +- Dependency injection for services +- Slash command support +- Event handling for bot lifecycle +- Discord.Net integration + +## Configuration + +Before running, update `appsettings.json` with your Discord bot token: + +```json +{ + "Discord": { + "BotToken": "YOUR_BOT_TOKEN_HERE" + } +} +``` + +## Running the Bot + +```bash +dotnet run +``` + +The bot will connect to Discord and respond to slash commands. diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1a7a575 --- /dev/null +++ b/.gitignore @@ -0,0 +1,50 @@ +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Ww][Ii][Nn]32/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ +[Ll]ogs/ + +# Visual Studio cache/options +.vs/ + +# Visual Studio Code +.vscode/settings.json +.vscode/launch.json + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# Configuration files with sensitive data +appsettings.json +!appsettings.*.json + +# NuGet +.nuget/ +*.nupkg +*.snupkg + +# dotnet +project.assets.json +project.lock.json + +# secrets +.env + +# OS generated files +*.swp +*.swo +*.DS_Store \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..985ae15 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,44 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "build", + "command": "dotnet", + "type": "shell", + "args": [ + "build", + "--configuration", + "Debug" + ], + "problemMatcher": "$msCompile", + "group": { + "kind": "build", + "isDefault": true + } + }, + { + "label": "run", + "command": "dotnet", + "type": "shell", + "args": [ + "run", + "--no-build" + ], + "problemMatcher": "$msCompile", + "group": { + "kind": "test", + "isDefault": true + }, + "isBackground": false + }, + { + "label": "clean", + "command": "dotnet", + "type": "shell", + "args": [ + "clean" + ], + "problemMatcher": "$msCompile" + } + ] +} diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..dbd2c56 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,32 @@ +# TentacleBot - Agent Instructions + +## Quick Commands + +```bash +dotnet restore && dotnet run # Run the bot locally +docker compose up --build -d # Run with Docker +docker compose logs -f # View Docker logs +``` + +## Setup + +1. Set `DISCORD_BOT_TOKEN` env var (preferred) or edit `appsettings.json` +2. Copy `.env.example` to `.env` for Docker: `cp .env.example .env` +3. `appsettings.json` is gitignored — never commit it + +## Architecture + +- **`Program.cs`** — Entry point, DI setup, registers command modules from the assembly +- **`Services/BotService.cs`** — Core bot lifecycle (login, ready handler, interaction routing) +- **`Commands/GeneralCommands.cs`** — Slash commands (`/general ping`, `/general hello`, `/botinfo`) + +Add new commands by creating a class in `Commands/` that extends `InteractionModuleBase`. Commands are auto-discovered via `AddModulesAsync(typeof(Program).Assembly)`. + +## Gotchas + +- **Two `DiscordSocketClient` instances** are created in `Program.cs` — one for the bot client and one passed into `InteractionService`. They are separate instances sharing the same config. +- **Token priority**: `DISCORD_BOT_TOKEN` env var is checked first, then `appsettings.json` → `Discord:BotToken`. + +## No tests, no lint config + +This project has no test framework, no linting, and no CI. `dotnet build` is the only verification step. diff --git a/Commands/GeneralCommands.cs b/Commands/GeneralCommands.cs new file mode 100644 index 0000000..a6ea46c --- /dev/null +++ b/Commands/GeneralCommands.cs @@ -0,0 +1,71 @@ +using Discord; +using Discord.Interactions; + +namespace TentacleBot.Commands; + +[Group("general", "General commands")] +public class GeneralCommands : InteractionModuleBase +{ + /// + /// Example ping command that responds with pong + /// + [SlashCommand("ping", "Responds with pong and latency")] + public async Task HandlePingAsync() + { + await DeferAsync(); + + var latency = Context.Client.Latency; + var embed = new EmbedBuilder() + .WithTitle("Pong!") + .WithDescription($"Latency: {latency}ms") + .WithColor(Color.Green) + .WithTimestamp(DateTimeOffset.UtcNow) + .Build(); + + await FollowupAsync(embed: embed); + } + + /// + /// Example hello command with a user parameter + /// + [SlashCommand("hello", "Greet a user")] + public async Task HandleHelloAsync( + [Summary("user", "The user to greet")] IUser? user = null) + { + await DeferAsync(); + + var targetUser = user ?? Context.User; + var embed = new EmbedBuilder() + .WithTitle($"Hello, {targetUser.Username}!") + .WithDescription($"Nice to meet you! ?") + .WithThumbnailUrl(targetUser.GetAvatarUrl() ?? targetUser.GetDefaultAvatarUrl()) + .WithColor(Color.Blue) + .WithTimestamp(DateTimeOffset.UtcNow) + .Build(); + + await FollowupAsync(embed: embed); + } + + /// + /// Example command that shows bot information + /// + [SlashCommand("botinfo", "Display bot information")] + public async Task HandleBotInfoAsync() + { + await DeferAsync(); + + var bot = Context.Client.CurrentUser; + var embed = new EmbedBuilder() + .WithTitle("Bot Information") + .AddField("Name", bot?.Username, true) + .AddField("ID", bot?.Id, true) + .AddField("Created", bot?.CreatedAt.ToString("g"), true) + .AddField("Guilds", Context.Client.Guilds.Count, true) + .WithColor(Color.Purple) + .WithThumbnailUrl(bot?.GetAvatarUrl()) + .WithTimestamp(DateTimeOffset.UtcNow) + .Build(); + + await FollowupAsync(embed: embed); + } +} diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..44f278d --- /dev/null +++ b/Dockerfile @@ -0,0 +1,18 @@ +FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build +WORKDIR /src + +COPY TentacleBot.csproj ./ +RUN dotnet restore + +COPY . ./ +RUN dotnet publish -c Release -o /app/publish /p:UseAppHost=false + +FROM mcr.microsoft.com/dotnet/runtime:8.0 AS final +WORKDIR /app + +COPY --from=build /app/publish ./ + +ENV DOTNET_RUNNING_IN_CONTAINER=true +ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=false + +ENTRYPOINT ["dotnet", "TentacleBot.dll"] diff --git a/Program.cs b/Program.cs new file mode 100644 index 0000000..b8d1fad --- /dev/null +++ b/Program.cs @@ -0,0 +1,50 @@ +using Discord; +using Discord.WebSocket; +using Discord.Interactions; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using TentacleBot.Services; +using TentacleBot.Commands; + +var configuration = new ConfigurationBuilder() + .SetBasePath(Directory.GetCurrentDirectory()) + .AddJsonFile("appsettings.json") + .Build(); + +var discordConfig = new DiscordSocketConfig +{ + GatewayIntents = GatewayIntents.All, + LogLevel = LogSeverity.Info, + UseInteractionSnowflakeDate = false, +}; + +var client = new DiscordSocketClient(discordConfig); + +var services = new ServiceCollection() + .AddLogging(builder => builder.AddConsole()) + .AddSingleton(configuration) + .AddSingleton(client) + .AddSingleton(new InteractionService(client, new InteractionServiceConfig + { + LogLevel = LogSeverity.Info, + UseCompiledLambda = true, + })) + .AddSingleton(); + +var serviceProvider = services.BuildServiceProvider(); + +var botService = serviceProvider.GetRequiredService(); +var interactionService = serviceProvider.GetRequiredService(); + +// Register command modules +await interactionService.AddModulesAsync( + typeof(Program).Assembly, + serviceProvider); + +// Initialize the bot +await botService.InitializeAsync(); + +// Keep the bot running +await Task.Delay(Timeout.Infinite); diff --git a/README.md b/README.md new file mode 100644 index 0000000..a380196 --- /dev/null +++ b/README.md @@ -0,0 +1,157 @@ +# TentacleBot - Discord .NET Bot + +A Discord bot project built with Discord.Net and .NET 8+, featuring dependency injection and slash commands. + +## Features + +- ? Built with Discord.Net 3.14+ +- ? Dependency Injection for services +- ? Slash commands support +- ? Rich embed messages +- ? Example commands included +- ? .NET 8 or higher + +## Prerequisites + +- .NET 8.0 or higher ([download here](https://dotnet.microsoft.com/download)) +- A Discord Bot Token (create one at [Discord Developer Portal](https://discord.com/developers/applications)) + +## Quick Start + +### 1. Clone and Setup + +```bash +dotnet restore +``` + +### 2. Configure Your Bot Token + +Edit `appsettings.json` and replace `YOUR_BOT_TOKEN_HERE` with your actual Discord bot token: + +```json +{ + "Discord": { + "BotToken": "YOUR_ACTUAL_TOKEN_HERE" + } +} +``` + +### 3. Run the Bot + +```bash +dotnet run +``` + +The bot will connect to Discord and register its slash commands globally. + +## Run With Docker Compose + +1. Copy the example env file: + +```bash +cp .env.example .env +``` + +2. Edit `.env` and set your Discord token: + +```dotenv +DISCORD_BOT_TOKEN=YOUR_ACTUAL_TOKEN_HERE +``` + +3. Build and run with Docker Compose: + +```bash +docker compose up --build -d +``` + +4. View logs: + +```bash +docker compose logs -f +``` + +5. Stop the bot: + +```bash +docker compose down +``` + +## Project Structure + +``` +TentacleBot/ +„¥„Ÿ„Ÿ Program.cs # Entry point and DI setup +„¥„Ÿ„Ÿ Services/ +„  „¤„Ÿ„Ÿ BotService.cs # Core bot service +„¥„Ÿ„Ÿ Commands/ +„  „¤„Ÿ„Ÿ GeneralCommands.cs # Example slash commands +„¥„Ÿ„Ÿ appsettings.json # Configuration +„¥„Ÿ„Ÿ Dockerfile # Container image definition +„¥„Ÿ„Ÿ docker-compose.yml # Local compose setup +„¥„Ÿ„Ÿ .env.example # Environment variable template +„¥„Ÿ„Ÿ TentacleBot.csproj # Project file +„¤„Ÿ„Ÿ README.md # This file +``` + +## Example Commands + +The bot includes example slash commands: + +- `/general ping` - Shows bot latency +- `/general hello [user]` - Greet a user +- `/general botinfo` - Display bot information + +## Adding New Commands + +Create a new class in the `Commands` folder that inherits from `InteractionModuleBase`: + +```csharp +using Discord.Interactions; + +namespace TentacleBot.Commands; + +public class MyCommands : InteractionModuleBase +{ + [SlashCommand("mycommand", "My custom command")] + public async Task HandleMyCommandAsync() + { + await RespondAsync("Hello from my command!"); + } +} +``` + +Commands are automatically discovered and registered when the bot starts. + +## Configuration + +### appsettings.json + +```json +{ + "Discord": { + "BotToken": "YOUR_BOT_TOKEN_HERE" + }, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning" + } + } +} +``` + +## Troubleshooting + +- **"Bot token not found"**: Set `DISCORD_BOT_TOKEN` in `.env` (Docker) or in your shell. The app falls back to `appsettings.json` if the env var is not set +- **"Insufficient permissions"**: Ensure your bot has the necessary permissions in Discord Developer Portal +- **Commands not appearing**: The bot needs to connect first. Wait a few seconds after starting, then try `/` in Discord + +## Resources + +- [Discord.Net Documentation](https://discordnet.dev/) +- [Discord Developer Portal](https://discord.com/developers/applications) +- [.NET Documentation](https://docs.microsoft.com/dotnet) + +## License + +MIT diff --git a/Services/BotService.cs b/Services/BotService.cs new file mode 100644 index 0000000..42cf245 --- /dev/null +++ b/Services/BotService.cs @@ -0,0 +1,102 @@ +using Discord; +using Discord.WebSocket; +using Discord.Interactions; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Configuration; + +namespace TentacleBot.Services; + +public class BotService +{ + private readonly DiscordSocketClient _client; + private readonly InteractionService _interactionService; + private readonly ILogger _logger; + private readonly IConfiguration _configuration; + private readonly IServiceProvider _serviceProvider; + + public BotService( + DiscordSocketClient client, + InteractionService interactionService, + ILogger logger, + IConfiguration configuration, + IServiceProvider serviceProvider) + { + _client = client; + _interactionService = interactionService; + _logger = logger; + _configuration = configuration; + _serviceProvider = serviceProvider; + } + + public async Task InitializeAsync() + { + _client.Log += LogAsync; + _interactionService.Log += LogAsync; + + _client.Ready += ClientReadyAsync; + _client.InteractionCreated += InteractionCreatedAsync; + + var token = Environment.GetEnvironmentVariable("DISCORD_BOT_TOKEN"); + if (string.IsNullOrWhiteSpace(token)) + { + token = _configuration["Discord:BotToken"]; + } + + if (string.IsNullOrEmpty(token)) + { + throw new InvalidOperationException("Discord bot token not found. Set DISCORD_BOT_TOKEN or Discord:BotToken in appsettings.json"); + } + + await _client.LoginAsync(TokenType.Bot, token); + await _client.StartAsync(); + + _logger.LogInformation("Bot service initialized"); + } + + private async Task ClientReadyAsync() + { + _logger.LogInformation("Bot connected as {BotName}#{Discriminator}", _client.CurrentUser?.Username, _client.CurrentUser?.Discriminator); + + // Register slash commands + await _interactionService.RegisterCommandsGloballyAsync(); + + _logger.LogInformation("Slash commands registered"); + } + + private async Task InteractionCreatedAsync(SocketInteraction interaction) + { + try + { + var ctx = new SocketInteractionContext(_client, interaction); + await _interactionService.ExecuteCommandAsync(ctx, _serviceProvider); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error handling interaction"); + + if (interaction.Type == InteractionType.ApplicationCommand) + { + await interaction.GetOriginalResponseAsync().ContinueWith(async msg => + { + if (msg.IsCompletedSuccessfully) + { + await interaction.FollowupAsync("An error occurred while executing the command."); + } + }); + } + } + } + + private static Task LogAsync(LogMessage message) + { + Console.WriteLine($"[{message.Severity}] {message.Source}: {message.Message}"); + return Task.CompletedTask; + } + + public async Task StopAsync() + { + await _client.StopAsync(); + _client.Dispose(); + _logger.LogInformation("Bot service stopped"); + } +} diff --git a/TentacleBot.csproj b/TentacleBot.csproj new file mode 100644 index 0000000..a0ef490 --- /dev/null +++ b/TentacleBot.csproj @@ -0,0 +1,27 @@ + + + + Exe + net8.0 + enable + enable + TentacleBot + TentacleBot + + + + + + + + + + + + + + PreserveNewest + + + + diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..a36bc47 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,9 @@ +services: + tentaclebot: + build: + context: . + dockerfile: Dockerfile + container_name: tentaclebot + restart: unless-stopped + env_file: + - .env