commit 0ba4fee9e773959d4cc96fed721443973337cc05 Author: Aster Fialla Date: Thu Feb 12 20:05:31 2026 -0500 project setup with example bot diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..cd967fc --- /dev/null +++ b/.dockerignore @@ -0,0 +1,25 @@ +**/.dockerignore +**/.env +**/.git +**/.gitignore +**/.project +**/.settings +**/.toolstarget +**/.vs +**/.vscode +**/.idea +**/*.*proj.user +**/*.dbmdl +**/*.jfm +**/azds.yaml +**/bin +**/charts +**/docker-compose* +**/Dockerfile* +**/node_modules +**/npm-debug.log +**/obj +**/secrets.dev.yaml +**/values.dev.yaml +LICENSE +README.md \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..edfb386 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +bin/ +obj/ +/packages/ +riderModule.iml +/_ReSharper.Caches/ +.idea +config.yml +postgres_pwd.txt \ No newline at end of file diff --git a/BasicCommands.cs b/BasicCommands.cs new file mode 100644 index 0000000..10fe306 --- /dev/null +++ b/BasicCommands.cs @@ -0,0 +1,122 @@ + +using Fluxer.Net.Commands; +using Fluxer.Net.Commands.Attributes; +using Fluxer.Net.Data.Models; + +namespace PluralFlux; + + +/// +/// Example command module demonstrating basic commands. +/// +public class BasicCommands : ModuleBase +{ + /// + /// Simple ping command that responds with "pong". + /// + [Command("ping")] + [Summary("Check if the bot is responsive")] + public async Task PingCommand() + { + await ReplyAsync("pong ;P"); + } + + /// + /// Hello command that mentions the user. + /// + [Command("hello")] + [Alias("hi", "hey")] + [Summary("Get a friendly greeting")] + public async Task HelloCommand() + { + await ReplyAsync($"Hello, <@{Context.User.Id}>! 👋"); + } + + /// + /// Info command that shows bot information and available commands. + /// + [Command("info")] + [Summary("Show bot information and available commands")] + public async Task InfoCommand() + { + await ReplyAsync( + $"**Fluxer.Net Example Bot**\n" + + $"Version: 0.4.0\n" + + $"Framework: .NET 7.0\n" + + $"Library: Fluxer.Net\n\n" + + $"Available Commands:\n" + + $"• `/ping` - Check if bot is responsive\n" + + $"• `/hello` - Get a friendly greeting\n" + + $"• `/info` - Show this information\n" + + $"• `/embed` - Show an example rich embed\n" + + $"• `/echo ` - Echo back your message\n" + + $"• `/add ` - Add two numbers" + ); + } + + /// + /// Embed command that demonstrates rich embeds using EmbedBuilder. + /// + [Command("embed")] + [Summary("Show an example rich embed")] + public async Task EmbedCommand() + { + var embed = new Fluxer.Net.EmbedBuilder.EmbedBuilder() + .WithTitle("Example Rich Embed") + .WithDescription("This is a demonstration of Fluxer.Net's EmbedBuilder system, " + + "based on Discord.Net's implementation. Embeds support rich formatting " + + "with titles, descriptions, fields, images, and more!") + .WithColor(0x5865F2) // Blurple color + .WithAuthor( + name: Context.User.Username, + iconUrl: Context.User.Avatar != null + ? $"https://cdn.fluxer.dev/avatars/{Context.User.Id}/{Context.User.Avatar}.png" + : null + ) + .WithThumbnailUrl("https://avatars.githubusercontent.com/u/20194446") + .AddField("Field 1", "This is an inline field", inline: true) + .AddField("Field 2", "This is also inline", inline: true) + .AddField("Field 3", "This is another inline field", inline: true) + .AddField("Full Width Field", "This field takes up the full width because inline is false", inline: false) + .AddField("Bot Stats", $"Guilds: 1\nChannels: 5\nUptime: {DateTime.UtcNow:HH:mm:ss}", inline: true) + .WithFooter("Fluxer.Net v0.4.0", "https://avatars.githubusercontent.com/u/20194446") + .WithCurrentTimestamp() + .Build(); + + await Context.Client.SendMessage(Context.ChannelId, new() + { + Content = "Here's an example of a rich embed:", + Embeds = new List { embed } + }); + } + + /// + /// Echo command that repeats the user's message. + /// + [Command("echo")] + [Summary("Echo back your message")] + public async Task EchoCommand([Remainder] string message) + { + await ReplyAsync(message); + } + + /// + /// Add command that adds two numbers together. + /// + [Command("add")] + [Summary("Add two numbers together")] + public async Task AddCommand(int a, int b) + { + await ReplyAsync($"{a} + {b} = {a + b}"); + } + + /// + /// Example command with optional parameter. + /// + [Command("greet")] + [Summary("Greet someone (or yourself)")] + public async Task GreetCommand(string name = "stranger") + { + await ReplyAsync($"Hello, {name}!"); + } +} \ No newline at end of file diff --git a/ConfigExtension.cs b/ConfigExtension.cs new file mode 100644 index 0000000..aa86e5c --- /dev/null +++ b/ConfigExtension.cs @@ -0,0 +1,56 @@ +namespace PluralFlux; + +public static class ConfigExtension +{ + //Load YML configuration from file + public static Dictionary? LoadConfig() + { + //NOTE: This path may need to be modified depending on where you run the example from + if (File.Exists("./config.yml")) + { + string[] lines = File.ReadAllLines("./config.yml"); + Dictionary configValues = ParseYaml(lines); + + // Use the values from the config + if (configValues.TryGetValue("Token", out var token)) + { + Console.WriteLine($"Token: {token}"); + } + + return configValues; + } + + return null; + } + + //Custom YML parser + private static Dictionary ParseYaml(string[] lines) + { + Dictionary configValues = new Dictionary(); + string currentKey = ""; + + foreach (string line in lines) + { + if (line.Trim().StartsWith('#')) + { + // Skip comments + continue; + } + + if (line.Contains(":")) + { + string[] parts = line.Split(':'); + currentKey = parts[0].Trim(); + string value = parts.Length > 1 ? parts[1].Trim() : ""; + configValues[currentKey] = value; + } + else if (!string.IsNullOrEmpty(currentKey)) + { + // Multi-line values + configValues[currentKey] += Environment.NewLine + line.Trim(); + } + } + + return configValues; + } +} \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..2c9e1c2 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,21 @@ +FROM mcr.microsoft.com/dotnet/runtime:10.0 AS base +USER $APP_UID +WORKDIR /app + +FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build +ARG BUILD_CONFIGURATION=Release +WORKDIR /src +COPY ["PluralFlux.csproj", "./"] +RUN dotnet restore "PluralFlux.csproj" +COPY . . +WORKDIR "/src/" +RUN dotnet build "./PluralFlux.csproj" -c $BUILD_CONFIGURATION -o /app/build + +FROM build AS publish +ARG BUILD_CONFIGURATION=Release +RUN dotnet publish "./PluralFlux.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false + +FROM base AS final +WORKDIR /app +COPY --from=publish /app/publish . +ENTRYPOINT ["dotnet", "PluralFlux.dll"] diff --git a/PluralFlux.csproj b/PluralFlux.csproj new file mode 100644 index 0000000..6e1ca25 --- /dev/null +++ b/PluralFlux.csproj @@ -0,0 +1,15 @@ + + + + Exe + net10.0 + enable + enable + Linux + + + + + + + diff --git a/PluralFlux.sln b/PluralFlux.sln new file mode 100644 index 0000000..4137c08 --- /dev/null +++ b/PluralFlux.sln @@ -0,0 +1,21 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PluralFlux", "PluralFlux.csproj", "{A1182490-2575-4A5E-9A11-1499AAF87A7C}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{F8342F54-03DE-42A4-BF87-29D4D90EF052}" + ProjectSection(SolutionItems) = preProject + compose.yaml = compose.yaml + EndProjectSection +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {A1182490-2575-4A5E-9A11-1499AAF87A7C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A1182490-2575-4A5E-9A11-1499AAF87A7C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A1182490-2575-4A5E-9A11-1499AAF87A7C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A1182490-2575-4A5E-9A11-1499AAF87A7C}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/PluralFlux.sln.DotSettings.user b/PluralFlux.sln.DotSettings.user new file mode 100644 index 0000000..af51dea --- /dev/null +++ b/PluralFlux.sln.DotSettings.user @@ -0,0 +1,2 @@ + + ForceIncluded \ No newline at end of file diff --git a/Program.cs b/Program.cs new file mode 100644 index 0000000..042ce45 --- /dev/null +++ b/Program.cs @@ -0,0 +1,405 @@ +// ============================================================================ +// Fluxer.Net Example Project - Getting Started Tutorial +// ============================================================================ +// This example demonstrates the core concepts of building a Fluxer bot using +// the Fluxer.Net library. You'll learn how to: +// 1. Configure logging for debugging and monitoring +// 2. Set up both the Gateway (real-time events) and API (REST operations) +// 3. Handle gateway events (like messages) +// 4. Make API calls (like sending messages) +// 5. Implement basic command handling +// +// Prerequisites: +// - A Fluxer account and bot token (add it to config.yml) +// - .NET 7.0 or higher +// - Basic understanding of async/await in C# +// ============================================================================ + +using System.Reflection; +using Serilog; +using Serilog.Core; +using Serilog.Sinks.SystemConsole.Themes; +using Fluxer.Net; +using Fluxer.Net.Commands; +using Fluxer.Net.Data.Enums; +using Fluxer.Net.Gateway.Data; +using PluralFlux; + +// ============================================================================ +// STEP 1: Configure Logging +// ============================================================================ +// Serilog provides structured logging for the Fluxer.Net library. This helps +// you debug issues and monitor your bot's activity. Logs are written to both +// the console (for development) and a file (for production debugging). + +Log.Logger = new LoggerConfiguration() + .MinimumLevel.Debug() // Log everything (Verbose, Debug, Info, Warning, Error, Fatal) + .WriteTo.Console(theme: AnsiConsoleTheme.Code) // Pretty console output with colors + .CreateLogger(); + +// ============================================================================ +// STEP 2: Load Your Bot Token +// ============================================================================ +// The token authenticates your bot with the Fluxer API. NEVER commit your +// token to version control! Store it in config.yml (which is .gitignored). +// +// To get a token: +// 1. Create a bot in the Fluxer developer portal +// 2. Copy the bot token +// 3. Paste it into config.yml as "Token: flx_your_token_here" + +var config = ConfigExtension.LoadConfig(); +if (config == null) +{ + Log.Error("YAML file not found. Please create a config.yml file with your bot token."); + Log.Error("Example format:\n Token: flx_your_token_here"); + return; +} + +Log.Debug("Config file loaded successfully."); + +// ============================================================================ +// STEP 3: Initialize the Gateway Client (Real-Time Events) +// ============================================================================ +// The GatewayClient connects to Fluxer's WebSocket gateway to receive real-time +// events like messages, reactions, member joins, etc. This is the "listening" +// part of your bot that responds to what happens on Fluxer. +// +// Key Configuration Options: +// - ReconnectAttemptDelay: Seconds to wait between reconnection attempts +// - Serilog: Logger instance for gateway events +// - IgnoredGatewayEvents: Filter out events you don't need (reduces processing) +// - Presence: Your bot's initial status (Online, Idle, DND, Invisible) + +var gateway = new GatewayClient(config["Token"], new() +{ + ReconnectAttemptDelay = 2, // Reconnect quickly if connection drops + Serilog = Log.Logger as Logger, // Use our configured logger + + // Ignore high-volume events we don't need to reduce CPU/memory usage + // Common events to ignore: PRESENCE_UPDATE, TYPING_START, VOICE_STATE_UPDATE + IgnoredGatewayEvents = new() + { + "PRESENCE_UPDATE" // We don't need to track when users go online/offline + }, + + // Set your bot's status. Options: Online, Idle, DND, Invisible + Presence = new PresenceUpdateGatewayData(Status.Online) +}); + +// ============================================================================ +// STEP 4: Initialize the API Client (REST Operations) +// ============================================================================ +// The ApiClient handles REST API requests for creating, reading, updating, and +// deleting resources (messages, channels, guilds, users, etc.). This is the +// "action" part of your bot that makes changes on Fluxer. +// +// Key Features: +// - Automatic rate limiting (enabled by default via sliding window algorithm) +// - Token-based authentication +// - Full coverage of 150+ Fluxer API endpoints +// - Shared logging configuration with the gateway + +var api = new ApiClient(config[key: "Token"], new() +{ + Serilog = Log.Logger as Logger, // Use our configured logger + EnableRateLimiting = true // Prevent hitting rate limits (default: true) +}); + +// ============================================================================ +// STEP 4.5: Initialize the Command Service +// ============================================================================ +// The CommandService provides a Discord.Net-style command framework for handling +// text-based commands. It automatically discovers command modules, parses arguments, +// and executes commands with support for preconditions and dependency injection. +// +// Key Features: +// - Attribute-based command definition +// - Automatic type parsing (string, int, bool, DateTime, etc.) +// - Optional and remainder parameters +// - Precondition support (RequireOwner, RequireContext, etc.) +// - Sync/Async execution modes + +var commands = new CommandService( + prefixChar: '/', // Commands start with / + logger: Log.Logger as Logger, // Use our configured logger + services: null // No dependency injection for this example +); + +// Automatically register all command modules from this assembly +await commands.AddModulesAsync(Assembly.GetExecutingAssembly()); + +Log.Information("Registered {ModuleCount} command module(s) with {CommandCount} command(s)", + commands.Modules.Count, commands.Commands.Count()); + +// ============================================================================ +// STEP 5: Handle Command-Line Arguments +// ============================================================================ +// This example supports a --revoke flag to log out the bot and invalidate +// the current token. Useful for testing or emergency shutdowns. +// +// Usage: dotnet run --revoke + +if (args.Length > 0 && args[0] == "--revoke") +{ + Log.Information("Revoking token and logging out..."); + await api.Logout(); + Log.Information("Token revoked successfully. The bot is now logged out."); + return; +} + +// ============================================================================ +// STEP 6: Example API Call - Update Bot Nickname +// ============================================================================ +// This demonstrates a simple API call to update the bot's nickname in a guild. +// Replace the guild ID with your own guild/community ID. +// +// To find your guild ID: +// 1. Enable developer mode in Fluxer settings +// 2. Right-click your guild/community +// 3. Click "Copy ID" + +// NOTE: Replace this guild ID with your own! +// await api.UpdateCurrentMember(1431484523333775609, new() +// { +// Nickname = "Fluxer.Net Example Bot" +// }); + +// ============================================================================ +// STEP 7: Subscribe to Gateway Events +// ============================================================================ +// The gateway uses an event-driven architecture. You subscribe to events by +// attaching handlers to the GatewayClient. Here we demonstrate basic command +// handling by listening for MESSAGE_CREATE events. +// +// Available Events (just a few examples): +// - MessageCreate: New message posted +// - MessageUpdate: Message edited +// - MessageDelete: Message deleted +// - GuildCreate: Bot added to a guild +// - GuildMemberAdd: User joined a guild +// - MessageReactionAdd: Reaction added to a message +// ... and many more! See GatewayClient.cs for the full list. + +gateway.MessageCreate += async messageData => +{ + try + { + + // Log every message for debugging (optional - can be noisy!) + Log.Debug("Message received in channel {ChannelId} from {Username}: {Content}", + messageData.ChannelId, messageData.Author.Username, messageData.Content); + + // ======================================================================== + // Command Handling using CommandService + // ======================================================================== + // The CommandService automatically parses commands and executes them. + // Commands are defined in module classes (see Modules/BasicCommands.cs) + + // Check if the message starts with the command prefix + int argPos = 0; + if (messageData.Content?.StartsWith("pf;") == true) + { + argPos = 3; // Skip the prefix character + + // Create a command context with all the necessary information + var context = new CommandContext(api, gateway, messageData); + + // Execute the command + var result = await commands.ExecuteAsync(context, argPos); + + // Log command execution results + if (!result.IsSuccess) + { + // Log errors (you can also send error messages to the user here) + Log.Warning("Command execution failed: {Error} ({ErrorType})", + result.Error, result.ErrorType); + + // Optionally send error message to user + if (result.ErrorType == CommandError.UnknownCommand) + { + // Don't spam for unknown commands - just log it + } + else if (result.ErrorType == CommandError.BadArgCount) + { + await api.SendMessage(messageData.ChannelId, new() + { + Content = $"❌ Error: {result.Error}" + }); + } + else if (result.ErrorType == CommandError.ParseFailed) + { + await api.SendMessage(messageData.ChannelId, new() + { + Content = $"❌ Error: {result.Error}" + }); + } + else if (result.ErrorType == CommandError.UnmetPrecondition) + { + await api.SendMessage(messageData.ChannelId, new() + { + Content = $"⛔ {result.Error}" + }); + } + else + { + await api.SendMessage(messageData.ChannelId, new() + { + Content = $"❌ An error occurred: {result.Error}" + }); + } + } + else + { + Log.Information("Command executed successfully by {Username} ({UserId})", + messageData.Author.Username, messageData.Author.Id); + } + } + } + catch (Exception ex) + { + Log.Error(ex, "Error while processing message"); + } +}; + +// ============================================================================ +// Additional Gateway Event Examples (Uncomment to use) +// ============================================================================ + +// Example: Log when the bot is ready +gateway.Ready += readyData => +{ + try + { + Log.Information("Bot is ready! Logged in as {Username}", readyData.User?.Username); + } + catch (Exception ex) + { + Log.Error(ex, "Error on ready event"); + } +}; + +// Example: Track message deletions +// gateway.MessageDelete += deleteData => +// { +// Log.Information("Message {MessageId} was deleted from channel {ChannelId}", +// deleteData.Id, deleteData.ChannelId); +// }; + +// Example: Welcome new guild members +// gateway.GuildMemberAdd += async memberData => +// { +// Log.Information("New member joined guild {GuildId}: User {UserId}", +// memberData.GuildId, memberData.UserId); +// +// // Send a welcome message (replace with your welcome channel ID) +// // await api.SendMessage(yourWelcomeChannelId, new() +// // { +// // Content = $"Welcome to the server, <@{memberData.UserId}>! 🎉" +// // }); +// }; + +// Example: Track message reactions +// gateway.MessageReactionAdd += reactionData => +// { +// Log.Debug("Reaction {Emoji} added to message {MessageId} by user {UserId}", +// reactionData.Emoji?.Name, reactionData.MessageId, reactionData.UserId); +// }; + +// ============================================================================ +// STEP 8: Connect to the Gateway +// ============================================================================ +// This establishes the WebSocket connection and starts receiving events. +// IMPORTANT: Uncomment this line to actually connect! It's commented out by +// default so you can test API calls without connecting to the gateway. + +await gateway.ConnectAsync(); +Log.Information("Connected to Fluxer gateway. Bot is now online!"); + +// ============================================================================ +// STEP 9: Keep the Bot Running +// ============================================================================ +// The bot needs to stay running to continue receiving events. Task.Delay(-1) +// blocks the main thread indefinitely. The bot will run until you stop it +// with Ctrl+C or kill the process. +// +// In production, you might want to: +// - Add graceful shutdown handling (CancellationToken) +// - Implement a /shutdown command for authorized users +// - Run as a system service or Docker container + +// await api.UpdateCurrentMember(1431484523333775609, new() { Nickname = "Fluxer.Net" }); + +Log.Information("Bot is running. Press Ctrl+C to stop."); +await Task.Delay(-1); + +// ============================================================================ +// Next Steps & Resources +// ============================================================================ +// Now that you understand the basics, here are some ideas to expand your bot: +// +// 1. Add more commands: +// - Create new command modules in the Modules/ folder +// - Use preconditions: [RequireOwner], [RequireContext(ContextType.Guild)] +// - Add parameter types: int, bool, DateTime, TimeSpan, enums, etc. +// - Use [Remainder] for multi-word parameters +// - Use [Alias] to add alternative command names +// - Implement BeforeExecute/AfterExecute hooks in your modules +// +// Example command module: +// public class ModerationCommands : ModuleBase +// { +// [Command("kick")] +// [RequireUserPermission(Permissions.KickMembers)] +// [RequireContext(ContextType.Guild)] +// public async Task KickCommand(ulong userId, [Remainder] string reason = "No reason provided") +// { +// // Kick logic here +// await ReplyAsync($"Kicked user {userId} for: {reason}"); +// } +// } +// +// 2. Use more API endpoints: +// - Create/manage channels: api.CreateChannel() +// - Manage roles: api.CreateRole(), api.UpdateRole() +// - Send embeds: Use EmbedBuilder to create rich embeds (see /embed command) +// - Manage members: api.UpdateMember(), api.KickMember() +// +// Example: Create a complex embed with error handling +// try { +// var embed = new EmbedBuilder() +// .WithTitle("Server Stats") +// .WithDescription($"Statistics for {guildName}") +// .WithColor(0x00FF00) // Green +// .AddField("Total Members", memberCount.ToString(), inline: true) +// .AddField("Online Members", onlineCount.ToString(), inline: true) +// .AddField("Total Channels", channelCount.ToString(), inline: true) +// .WithThumbnailUrl(guildIconUrl) +// .WithFooter($"Requested by {username}", userAvatarUrl) +// .WithCurrentTimestamp() +// .Build(); +// +// await api.SendMessage(channelId, new() { Embeds = new() { embed } }); +// } catch (InvalidOperationException ex) { +// Log.Error(ex, "Embed validation failed - check field lengths and URL formats"); +// } +// +// 3. Implement advanced features: +// - Database integration for persistent data (Entity Framework, Dapper, etc.) +// - Scheduled tasks and background jobs +// - Custom preconditions for advanced permission checks +// - Dependency injection with service providers +// +// 4. Explore rate limiting: +// - Check remaining requests: api.RateLimitManager.GetBucketInfoAsync() +// - Monitor active buckets: api.RateLimitManager.ActiveBucketCount +// - See RateLimiting/README.md for more details +// +// 5. Documentation: +// - API endpoints: See ApiClient.cs for all 150+ methods +// - Gateway events: See GatewayClient.cs for all event types +// - Rate limiting: See RateLimiting/README.md +// - Configuration: See FluxerConfig.cs for all options +// +// Happy coding! 🚀 +// ============================================================================ \ No newline at end of file diff --git a/compose.yaml b/compose.yaml new file mode 100644 index 0000000..b507e39 --- /dev/null +++ b/compose.yaml @@ -0,0 +1,38 @@ +services: + pluralflux: + image: pluralflux + build: + context: . + dockerfile: Dockerfile + postgres: + image: postgres:latest + container_name: pluralflux-postgres + environment: + POSTGRES_PASSWORD: /run/secrets/postgres_pwd + secrets: + - postgres_pwd + volumes: + - pgdata:/var/lib/postgresql/data + ports: + - "5432:5432" + pgadmin: + image: dpage/pgadmin4:latest + ports: + - 5050:80 + environment: + # Required by pgAdmin + PGADMIN_DEFAULT_EMAIL: pieartsy@pm.me + PGADMIN_DEFAULT_PASSWORD: /run/secrets/postgres_pwd + # Don't require the user to login + PGADMIN_CONFIG_SERVER_MODE: 'False' + # Don't require a "master" password after logging in + PGADMIN_CONFIG_MASTER_PASSWORD_REQUIRED: 'False' + secrets: + - postgres_pwd + +volumes: + pgdata: + +secrets: + postgres_pwd: + file: ./postgres_pwd.txt \ No newline at end of file