From fbee6d71f036d1d302031ff55e002d69544ac0b6 Mon Sep 17 00:00:00 2001 From: pieartsy Date: Fri, 13 Feb 2026 18:20:29 -0500 Subject: [PATCH] discord js (#1) * clear out fluxer.net * basic discord.js bot with ping, and echo * added creation of webhook * simplifying webhook logic * sending messages as webhook * deleting orignal message * adding sequelize file * commented out pgadmin part while it's not working * adding member sort of working * changing names of values * updating names of values and adding new method in memberHelper * renamed messagehelper function * deleted proxyhelper * passed only channel id into webhook helper methods * added new functions and got update working * changed message match to better reducer * adjusted webhook helper to use actual data * refactored bot.js and removed unused methods --- .gitignore | 11 +- BasicCommands.cs | 122 ---------- ConfigExtension.cs | 56 ----- PluralFlux.csproj | 15 -- PluralFlux.sln | 21 -- PluralFlux.sln.DotSettings.user | 2 - Program.cs | 405 -------------------------------- bot.js | 66 ++++++ compose.yaml | 28 +-- eslint.config.js | 53 +++++ helpers/memberHelper.js | 137 +++++++++++ helpers/messageHelper.js | 37 +++ helpers/webhookHelper.js | 37 +++ package.json | 24 ++ sequelize.js | 59 +++++ 15 files changed, 431 insertions(+), 642 deletions(-) delete mode 100644 BasicCommands.cs delete mode 100644 ConfigExtension.cs delete mode 100644 PluralFlux.csproj delete mode 100644 PluralFlux.sln delete mode 100644 PluralFlux.sln.DotSettings.user delete mode 100644 Program.cs create mode 100644 bot.js create mode 100644 eslint.config.js create mode 100644 helpers/memberHelper.js create mode 100644 helpers/messageHelper.js create mode 100644 helpers/webhookHelper.js create mode 100644 package.json create mode 100644 sequelize.js diff --git a/.gitignore b/.gitignore index 3181764..6420400 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,5 @@ -bin/ -obj/ -/packages/ -riderModule.iml -/_ReSharper.Caches/ +node_modules .idea -config.yml -secrets/ \ No newline at end of file +secrets/ +package-lock.json +config.json \ No newline at end of file diff --git a/BasicCommands.cs b/BasicCommands.cs deleted file mode 100644 index 10fe306..0000000 --- a/BasicCommands.cs +++ /dev/null @@ -1,122 +0,0 @@ - -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 deleted file mode 100644 index aa86e5c..0000000 --- a/ConfigExtension.cs +++ /dev/null @@ -1,56 +0,0 @@ -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/PluralFlux.csproj b/PluralFlux.csproj deleted file mode 100644 index 6e1ca25..0000000 --- a/PluralFlux.csproj +++ /dev/null @@ -1,15 +0,0 @@ - - - - Exe - net10.0 - enable - enable - Linux - - - - - - - diff --git a/PluralFlux.sln b/PluralFlux.sln deleted file mode 100644 index 4137c08..0000000 --- a/PluralFlux.sln +++ /dev/null @@ -1,21 +0,0 @@ - -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 deleted file mode 100644 index af51dea..0000000 --- a/PluralFlux.sln.DotSettings.user +++ /dev/null @@ -1,2 +0,0 @@ - - ForceIncluded \ No newline at end of file diff --git a/Program.cs b/Program.cs deleted file mode 100644 index 042ce45..0000000 --- a/Program.cs +++ /dev/null @@ -1,405 +0,0 @@ -// ============================================================================ -// 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/bot.js b/bot.js new file mode 100644 index 0000000..ea29d87 --- /dev/null +++ b/bot.js @@ -0,0 +1,66 @@ +import { Client, GatewayDispatchEvents } from "@discordjs/core"; +import { REST } from "@discordjs/rest"; +import { WebSocketManager } from "@discordjs/ws"; +import { db } from './sequelize.js'; +import { webhookHelper } from "./helpers/webhookHelper.js"; +import { messageHelper } from "./helpers/messageHelper.js"; +import { memberHelper } from "./helpers/memberHelper.js"; + +const token = process.env.FLUXER_BOT_TOKEN; + +if (!token) { + console.error("Missing FLUXER_BOT_TOKEN environment variable."); + process.exit(1); +} + +const rest = new REST({ + api: "https://api.fluxer.app", + version: "1", +}).setToken(token); + +const gateway = new WebSocketManager({ + token, + intents: 0, // Fluxer has no intents yet + rest, + version: "1", +}); + +export const client = new Client({ rest, gateway }); + +let plural_flux_name = ""; +let plural_flux_discriminator = ""; + +client.on(GatewayDispatchEvents.MessageCreate, async ({ api, data }) => { + if (data.webhook_id) { + return; + } + else if (data.author.username === plural_flux_name && data.author.discriminator === plural_flux_discriminator) { + return; + } + else if (!data.content.startsWith(messageHelper.prefix)) { + const proxyMatch = await messageHelper.parse_proxy_tags(data.author.id, data.content); + if (!proxyMatch.proxy) { + return; + } + const member = await memberHelper.get_member_by_proxy(data.author.id, proxyMatch.proxy); + await webhookHelper.replace_message(api, data, proxyMatch.message, member); + } + else { + const command_name = data.content.slice(messageHelper.prefix.length).split(" ")[0]; + const args = messageHelper.parse_command_args(data.content, command_name); + + if (command_name === "member" || command_name === "m") { + const reply = await memberHelper.parse_member_command(data.author.id, args); + await api.channels.createMessage(data.channel_id, {content: reply}); + } + } +}); + +client.on(GatewayDispatchEvents.Ready, async ({data}) => { + console.log(`Logged in as ${data.user.username}#${data.user.discriminator}`); + plural_flux_name = data.user.username; + plural_flux_discriminator = data.user.discriminator; + await db.check_connection(); +}); + +gateway.connect(); \ No newline at end of file diff --git a/compose.yaml b/compose.yaml index 58ba970..7f04505 100644 --- a/compose.yaml +++ b/compose.yaml @@ -10,20 +10,20 @@ services: - pgdata:/var/lib/postgresql ports: - "5432:5432" - pgadmin: - image: dpage/pgadmin4:latest - ports: - - 5050:80 - environment: - # Required by pgAdmin - PGADMIN_DEFAULT_EMAIL: pieartsy@pm.me - PGADMIN_DEFAULT_PASSWORD_FILE: /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 +# pgadmin: +# image: dpage/pgadmin4:latest +# ports: +# - 5050:80 +# environment: +# # Required by pgAdmin +# PGADMIN_DEFAULT_EMAIL: pieartsy@pm.me +# PGADMIN_DEFAULT_PASSWORD_FILE: /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: diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..e64b18e --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,53 @@ +const js = require('@eslint/js'); + +module.exports = [ + js.configs.recommended, + { + languageOptions: { + ecmaVersion: 'latest', + }, + rules: { + 'arrow-spacing': ['warn', { before: true, after: true }], + 'brace-style': ['error', 'stroustrup', { allowSingleLine: true }], + 'comma-dangle': ['error', 'always-multiline'], + 'comma-spacing': 'error', + 'comma-style': 'error', + curly: ['error', 'multi-line', 'consistent'], + 'dot-location': ['error', 'property'], + 'handle-callback-err': 'off', + indent: ['error', 'tab'], + 'keyword-spacing': 'error', + 'max-nested-callbacks': ['error', { max: 4 }], + 'max-statements-per-line': ['error', { max: 2 }], + 'no-console': 'off', + 'no-empty-function': 'error', + 'no-floating-decimal': 'error', + 'no-inline-comments': 'error', + 'no-lonely-if': 'error', + 'no-multi-spaces': 'error', + 'no-multiple-empty-lines': ['error', { max: 2, maxEOF: 1, maxBOF: 0 }], + 'no-shadow': ['error', { allow: ['err', 'resolve', 'reject'] }], + 'no-trailing-spaces': ['error'], + 'no-var': 'error', + 'no-undef': 'off', + 'object-curly-spacing': ['error', 'always'], + 'prefer-const': 'error', + quotes: ['error', 'single'], + semi: ['error', 'always'], + 'space-before-blocks': 'error', + 'space-before-function-paren': [ + 'error', + { + anonymous: 'never', + named: 'never', + asyncArrow: 'always', + }, + ], + 'space-in-parens': 'error', + 'space-infix-ops': 'error', + 'space-unary-ops': 'error', + 'spaced-comment': 'error', + yoda: 'error', + }, + }, +]; \ No newline at end of file diff --git a/helpers/memberHelper.js b/helpers/memberHelper.js new file mode 100644 index 0000000..e0bfb84 --- /dev/null +++ b/helpers/memberHelper.js @@ -0,0 +1,137 @@ +import { db } from '../sequelize.js'; + +const mh = {}; + +const errorEnums = { + NO_MEMBER: "No member was found.", + NO_NAME_PROVIDED: "No member name was provided for", + NO_VALUE: "has not been set for this member.", + ADD_ERROR: "Error adding member.", + MEMBER_EXISTS: "A member with that name already exists.", + USER_NO_MEMBERS: "You have no members created." +} + +mh.parse_member_command = async function(author_id, args){ + console.log(author_id, args); + if (!args) { + return `${errorEnums.NO_NAME_PROVIDED} querying.` + } + switch(args[0]) { + case 'new': + return await add_new_member(author_id, args); + case 'delete': + return await delete_member(author_id, args); + } + switch(args[1]) { + case 'displayname': + return await set_display_name(author_id, args); + // case 'proxy': + // return await set_proxy(author_id, args); + // case 'avatar': + // return await set_avatar(author_id, args) + default: + return await get_member_info(author_id, args); + } +} + +async function add_new_member(author_id, args) { + const member_name = args[1]; + const display_name = args[2]; + const proxy = args[3]; + const propic = args[4]; + if (!member_name) { + return `${errorEnums.NO_NAME_PROVIDED} adding.`; + } + const member = await get_member_info(author_id, member_name); + if (member !== errorEnums.NO_MEMBER) { + return errorEnums.MEMBER_EXISTS; + } + const trimmed_name = display_name ? display_name.replaceAll(' ', '') : null; + const trimmed_proxy = proxy ? proxy.trim() : null; + return await db.members.create({ + name: member_name, + userid: author_id, + displayname: trimmed_name !== null ? display_name : null, + proxy: trimmed_proxy, + propic: propic + }).then((m) => { + let success = `Member was successfully added.\nName: ${m.dataValues.name}` + success += display_name ? `\nDisplay name: ${m.dataValues.displayname}` : ""; + success += proxy ? `\nProxy tag: ${m.dataValues.proxy} `: ""; + success += propic ? `\nProfile picture: ${m.dataValues.proxy} `: ""; + return success; + }).catch(e => { + return `${errorEnums.ADD_ERROR}: ${e.message}`; + }) +} + +async function get_member_info(author_id, member_name) { + let member = await db.members.findOne({ where: { name: member_name, userid: author_id } }); + if (member) { + let member_info = `Member name: ${member.name}`; + member_info += member.displayname ? `\nDisplay name: ${member.displayname}` : '\nDisplay name: unset'; + member_info += member.proxy ? `\nProxy Tag: ${member.proxy}` : '\nProxy tag: unset'; + member_info += member.propic ? `\nProfile pic: ${member.propic}` : '\nProfile pic: unset'; + return member_info; + } + return errorEnums.NO_MEMBER; +} + +async function set_display_name(author_id, args) { + const member_name = args[0]; + const display_name = args[2]; + const trimmed_name = display_name ? display_name.replaceAll(' ', '') : null; + console.log(trimmed_name, display_name); + if (!member_name) { + return `${errorEnums.NO_NAME_PROVIDED} display name.`; + } + else if (!display_name || trimmed_name === null ) { + let member = await get_member_info(author_id, args); + console.log(member.displayname); + if (member.displayname) { + return `Display name for ${member_name} is: ${member.displayname}.`; + } + return `Display name ${errorEnums.NO_VALUE}` + } + console.log(display_name); + return await update_member(author_id, args); +} + +async function update_member(author_id, args) { + const member_name = args[0]; + const column_Name = args[1]; + const value = args[2]; + return await db.members.update({[column_Name]: value}, { where: { name: member_name, userid: author_id } }).then(() => { + return `Updated ${column_Name} for ${member_name} to ${value}`; + }).catch(e => { + return `${errorEnums.NO_MEMBER}: ${e.message}`; + }); +} + +async function delete_member(author_id, args) { + const member_name = args[1]; + if (!member_name) { + return `${errorEnums.NO_NAME_PROVIDED} deletion.`; + } + return await db.members.destroy({ where: { name: member_name, userid: author_id } }).then(() => { + return `${member_name} has been deleted.`; + }).catch(e => { + return `${errorEnums.NO_MEMBER}: ${e.message}`; + }); +} + +mh.get_member_by_proxy = async function(author_id, proxy) { + return await db.members.findOne({ where: { userid: author_id, proxy: proxy } }).catch(e => { + return `${errorEnums.NO_MEMBER}: ${e.message}`; + }); +} + +mh.get_members_by_author = async function(author_id) { + return await db.members.findAll({ where: { userid: author_id } }).catch(e => { + // I have no idea how this could possibly happen but better safe than sorry + return `${errorEnums.USER_NO_MEMBERS}: ${e.message}`; + }); +} + + +export const memberHelper = mh; \ No newline at end of file diff --git a/helpers/messageHelper.js b/helpers/messageHelper.js new file mode 100644 index 0000000..7391c64 --- /dev/null +++ b/helpers/messageHelper.js @@ -0,0 +1,37 @@ +import {memberHelper} from "./memberHelper.js"; + +const msgh = {}; + +msgh.prefix = "pf;" + +msgh.parse_command_args = function(text, command_name) { + const message = text.slice(msgh.prefix.length + command_name.length).trim(); + // slices up message arguments including retaining quoted strings + return message.match(/\\?.|^$/g).reduce((accumulator, chara) => { + if (chara === '"') { + // checks whether string is within quotes or not + accumulator.quote ^= 1; + } else if (!accumulator.quote && chara === ' '){ + // if not currently in quoted string, push empty string to start word + accumulator.array.push(''); + } else { + // accumulates characters to the last string in the array and removes escape characters + accumulator.array[accumulator.array.length-1] += chara.replace(/\\(.)/,"$1"); + } + return accumulator; + }, {array: ['']}).array // initial array with empty string for the reducer +} + +msgh.parse_proxy_tags = async function (author_id, text){ + const members = await memberHelper.get_members_by_author(author_id); + const proxyMessage = {} + members.forEach(member => { + if (text.startsWith(member.proxy) && text.length > member.proxy.length) { + proxyMessage.proxy = member.proxy; + proxyMessage.message = text.slice(member.proxy.length).trim(); + } + }) + return proxyMessage; +} + +export const messageHelper = msgh; diff --git a/helpers/webhookHelper.js b/helpers/webhookHelper.js new file mode 100644 index 0000000..2baefe9 --- /dev/null +++ b/helpers/webhookHelper.js @@ -0,0 +1,37 @@ +const wh = {}; + +wh.get_or_create_webhook = async function (api, channel_id) { + const name = 'PluralFlux Proxy Webhook'; + let webhook = await get_webhook(api, channel_id, name); + if (webhook === undefined) { + webhook = await api.channels.createWebhook(channel_id, {name: name}); + } + return webhook; +} + +async function get_webhook(api, channel_id, name) { + const all_webhooks = await api.channels.getWebhooks(channel_id); + if (all_webhooks.length === 0) { + return; + } + let pf_webhook; + all_webhooks.forEach((webhook) => { + if (webhook.name === name) { + pf_webhook = webhook; + } + }) + return pf_webhook; +} + +wh.replace_message = async function (api, data, text, member) { + if (text.length > 0) { + const webhook = await wh.get_or_create_webhook(api, data.channel_id); + await api.webhooks.execute(webhook.id, webhook.token, {content: text, username: member.displayname ?? member.name, propic: member.propic}); + await api.channels.deleteMessage(data.channel_id, data.id); + } + else { + await api.channels.createMessage(data.channel_id, {content: '(Please input a message!)'}); + } +} + +export const webhookHelper = wh; \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..2112f7a --- /dev/null +++ b/package.json @@ -0,0 +1,24 @@ +{ + "name": "pluralflux", + "version": "1.0.0", + "description": "", + "main": "bot.js", + "type": "module", + "repository": { + "type": "git", + "url": "https://github.com/pieartsy/PluralFlux.git" + }, + "private": true, + "dependencies": { + "@discordjs/core": "^2.4.0", + "@discordjs/rest": "^2.6.0", + "@discordjs/ws": "^2.0.4", + "pg": "^8.18.0", + "pg-hstore": "^2.3.4", + "sequelize": "^6.37.7" + }, + "devDependencies": { + "@eslint/js": "^10.0.1", + "eslint": "^10.0.0" + } +} diff --git a/sequelize.js b/sequelize.js new file mode 100644 index 0000000..8838cca --- /dev/null +++ b/sequelize.js @@ -0,0 +1,59 @@ +import {DataTypes, Sequelize} from 'sequelize'; + +const password = process.env.POSTGRES_PASSWORD; + +if (!password) { + console.error("Missing POSTGRES_PWD environment variable."); + process.exit(1); +} + +const database = {}; + +const sequelize = new Sequelize('postgres', 'postgres', password, { + host: 'localhost', + dialect: 'postgres' +}); + +database.sequelize = sequelize; +database.Sequelize = Sequelize; + +database.members = sequelize.define('Member', { + userid: { + type: DataTypes.STRING, + allowNull: false, + }, + name: { + type: DataTypes.STRING, + allowNull: false, + }, + displayname: { + type: DataTypes.STRING, + }, + propic: { + type: DataTypes.STRING, + }, + proxy: { + type: DataTypes.STRING, + } +}); + +database.check_connection = async function() { + await sequelize.authenticate().then(async (result) => { + console.log('Connection has been established successfully.'); + await syncModels(); + }).catch(err => { + console.error('Unable to connect to the database:', err); + process.exit(1); + }); +} + +async function syncModels() { + await sequelize.sync({force:true}).then((result) => { + console.log('Models synced successfully.'); + }).catch((err) => { + console.error('Syncing models did not work', err); + process.exit(1); + }); +} + +export const db = database; \ No newline at end of file