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