diff --git a/.idea/.idea.ARC V2/.idea/.gitignore b/.idea/.idea.ARC V2/.idea/.gitignore new file mode 100644 index 0000000..2be93dd --- /dev/null +++ b/.idea/.idea.ARC V2/.idea/.gitignore @@ -0,0 +1,13 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Rider ignored files +/contentModel.xml +/.idea.ARC V2.iml +/modules.xml +/projectSettingsUpdater.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/.idea.ARC V2/.idea/.name b/.idea/.idea.ARC V2/.idea/.name new file mode 100644 index 0000000..4c9dd3e --- /dev/null +++ b/.idea/.idea.ARC V2/.idea/.name @@ -0,0 +1 @@ +ARC V2 \ No newline at end of file diff --git a/.idea/.idea.ARC V2/.idea/encodings.xml b/.idea/.idea.ARC V2/.idea/encodings.xml new file mode 100644 index 0000000..df87cf9 --- /dev/null +++ b/.idea/.idea.ARC V2/.idea/encodings.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/.idea/.idea.ARC V2/.idea/indexLayout.xml b/.idea/.idea.ARC V2/.idea/indexLayout.xml new file mode 100644 index 0000000..7b08163 --- /dev/null +++ b/.idea/.idea.ARC V2/.idea/indexLayout.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/.idea.ARC V2/.idea/vcs.xml b/.idea/.idea.ARC V2/.idea/vcs.xml new file mode 100644 index 0000000..a329890 --- /dev/null +++ b/.idea/.idea.ARC V2/.idea/vcs.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/ARC/ARC.csproj b/ARC/ARC.csproj index 653a127..a158861 100644 --- a/ARC/ARC.csproj +++ b/ARC/ARC.csproj @@ -27,6 +27,7 @@ + @@ -34,7 +35,6 @@ - diff --git a/ARC/Arc.cs b/ARC/Arc.cs index 7b5ceac..f489435 100644 --- a/ARC/Arc.cs +++ b/ARC/Arc.cs @@ -2,7 +2,9 @@ using Arc.Exceptions; using Arc.Schema; using Arc.Services; +using ARC.Services; using DSharpPlus; +using DSharpPlus.Entities; using DSharpPlus.EventArgs; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; @@ -93,26 +95,48 @@ private static async Task StartDiscordBot(IConfigurationRoot settings) LoggerFactory = logFactory }; - _clientInstance = new DiscordClient(discordConfig); + _clientInstance = new DiscordClient(discordConfig) + { + ClientVersion = "2.0" + }; _serviceProvider = ConfigureServices(settings); // Run any necessary steps before starting the bot here. ServiceProvider.GetRequiredService(); ServiceProvider.GetRequiredService(); + ServiceProvider.GetRequiredService(); + ServiceProvider.GetRequiredService(); + ServiceProvider.GetRequiredService(); + // Connect to discord! await _clientInstance.ConnectAsync(); _clientInstance.Ready += ClientInstanceOnReady; + _clientInstance.ClientErrored += ClientInstanceOnClientErrored; await Task.Delay(-1); } + private static async Task ClientInstanceOnClientErrored(DiscordClient sender, ClientErrorEventArgs args) + { + var debug_log = await ClientInstance.GetChannelAsync(ulong.Parse(GlobalConfig.GetSection("discord:debug_log").Value)); + var errorEmbed = new DiscordEmbedBuilder() + .WithTitle("Error!") + .WithColor(DiscordColor.Red) + .WithDescription($"***An error occured !***\n```{args.Exception}```"); + + await debug_log.SendMessageAsync(errorEmbed); + + } + private static async Task ClientInstanceOnReady(DiscordClient sender, ReadyEventArgs e) { await Task.Run(() => { Log.Logger.Information($"Logged in as {sender.CurrentUser}"); Log.Logger.Information($"Ready!"); + //ClientInstance.BulkOverwriteGlobalApplicationCommandsAsync(new List() { }); + //ClientInstance.BulkOverwriteGuildApplicationCommandsAsync(975717691564376084, new List() { }); }); } @@ -139,6 +163,9 @@ private static IServiceProvider ConfigureServices(IConfigurationRoot settings) .AddSingleton(settings) .AddSingleton() .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() .BuildServiceProvider(); return services; diff --git a/ARC/Exceptions/ArcExceptions.cs b/ARC/Exceptions/ArcExceptions.cs index 54f2c89..638f747 100644 --- a/ARC/Exceptions/ArcExceptions.cs +++ b/ARC/Exceptions/ArcExceptions.cs @@ -13,4 +13,9 @@ public ArcNotInitializedException(string? message = null) : base($"ARC was not p public class ArcInitFailedException : ArcException { public ArcInitFailedException(string? message = null) : base($"ARC Initialization failed: {message ?? Empty}") { } +} + +public class ArcModmailFailedException : ArcException +{ + public ArcModmailFailedException(string? message = null) : base($"ARC Modmail failed: {message ?? Empty}") { } } \ No newline at end of file diff --git a/ARC/Extensions/ClientExtensions.cs b/ARC/Extensions/ClientExtensions.cs index f678a8c..9738209 100644 --- a/ARC/Extensions/ClientExtensions.cs +++ b/ARC/Extensions/ClientExtensions.cs @@ -1,9 +1,4 @@ using DSharpPlus; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace ARC.Extensions { diff --git a/ARC/Extensions/EmbedBuilderExtensions.cs b/ARC/Extensions/EmbedBuilderExtensions.cs index b17e237..30818df 100644 --- a/ARC/Extensions/EmbedBuilderExtensions.cs +++ b/ARC/Extensions/EmbedBuilderExtensions.cs @@ -1,10 +1,6 @@ -using Arc.Schema; + using DSharpPlus.Entities; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; + namespace ARC.Extensions { diff --git a/ARC/Extensions/GuildExtensions.cs b/ARC/Extensions/GuildExtensions.cs new file mode 100644 index 0000000..8ac1a9f --- /dev/null +++ b/ARC/Extensions/GuildExtensions.cs @@ -0,0 +1,25 @@ +using Arc.Schema; +using DSharpPlus.Entities; +using DSharpPlus; +namespace ARC.Extensions; + +public static class GuildExtensions +{ + + private static ArcDbContext DbContext => Arc.Arc.ArcDbContext; + private static DiscordClient ClientInstance => Arc.Arc.ClientInstance; + + public static async Task Log(this DiscordGuild guild, DiscordMessageBuilder message) + { + + if (!DbContext.Config[guild.Id].ContainsKey("logchannel")) + return; + + ulong logChannelSnowflake = ulong.Parse(DbContext.Config[guild.Id]["logchannel"]); + var channel = await ClientInstance.GetChannelAsync(logChannelSnowflake); + + await channel.SendMessageAsync(message); + + } + +} \ No newline at end of file diff --git a/ARC/Migrations/20230408134552_ArcV2-1.1.Designer.cs b/ARC/Migrations/20230408134552_ArcV2-1.1.Designer.cs new file mode 100644 index 0000000..9306f90 --- /dev/null +++ b/ARC/Migrations/20230408134552_ArcV2-1.1.Designer.cs @@ -0,0 +1,124 @@ +// +using System; +using Arc.Schema; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace ARC.Migrations +{ + [DbContext(typeof(ArcDbContext))] + [Migration("20230408134552_ArcV2-1.1")] + partial class ArcV211 + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.0-preview.2.23128.3") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Arc.Schema.Appeal", b => + { + b.Property("AppealId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("AppealId")); + + b.Property("NextAppeal") + .HasColumnType("timestamp with time zone"); + + b.Property("UserSnowflake") + .HasColumnType("bigint"); + + b.HasKey("AppealId"); + + b.ToTable("Appeals"); + }); + + modelBuilder.Entity("Arc.Schema.GuildConfig", b => + { + b.Property("ConfigId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ConfigId")); + + b.Property("ConfigGuildSnowflake") + .HasColumnType("bigint"); + + b.Property("ConfigKey") + .IsRequired() + .HasColumnType("text"); + + b.Property("ConfigValue") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("ConfigId"); + + b.ToTable("GuildConfigs"); + }); + + modelBuilder.Entity("Arc.Schema.Modmail", b => + { + b.Property("ModmailId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ModmailId")); + + b.Property("ChannelSnowflake") + .HasColumnType("bigint"); + + b.Property("UserSnowflake") + .HasColumnType("bigint"); + + b.Property("WebhookSnowflake") + .HasColumnType("bigint"); + + b.HasKey("ModmailId"); + + b.ToTable("Modmails"); + }); + + modelBuilder.Entity("Arc.Schema.UserNote", b => + { + b.Property("NoteId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("NoteId")); + + b.Property("AuthorSnowflake") + .HasColumnType("bigint"); + + b.Property("DateAdded") + .HasColumnType("timestamp with time zone"); + + b.Property("GuildSnowflake") + .HasColumnType("bigint"); + + b.Property("Note") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserSnowflake") + .HasColumnType("bigint"); + + b.HasKey("NoteId"); + + b.ToTable("UserNotes"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/ARC/Migrations/20230408134552_ArcV2-1.1.cs b/ARC/Migrations/20230408134552_ArcV2-1.1.cs new file mode 100644 index 0000000..a8156eb --- /dev/null +++ b/ARC/Migrations/20230408134552_ArcV2-1.1.cs @@ -0,0 +1,29 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace ARC.Migrations +{ + /// + public partial class ArcV211 : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "GuildSnowflake", + table: "UserNotes", + type: "bigint", + nullable: false, + defaultValue: 0L); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "GuildSnowflake", + table: "UserNotes"); + } + } +} diff --git a/ARC/Migrations/ArcDbContextModelSnapshot.cs b/ARC/Migrations/ArcDbContextModelSnapshot.cs index 96c516c..7987251 100644 --- a/ARC/Migrations/ArcDbContextModelSnapshot.cs +++ b/ARC/Migrations/ArcDbContextModelSnapshot.cs @@ -101,6 +101,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("DateAdded") .HasColumnType("timestamp with time zone"); + b.Property("GuildSnowflake") + .HasColumnType("bigint"); + b.Property("Note") .IsRequired() .HasColumnType("text"); diff --git a/ARC/Modules/ArcModule.cs b/ARC/Modules/ArcModule.cs new file mode 100644 index 0000000..c46aece --- /dev/null +++ b/ARC/Modules/ArcModule.cs @@ -0,0 +1,38 @@ + +using Arc.Schema; +using DSharpPlus; +using DSharpPlus.SlashCommands; +using Microsoft.Extensions.Configuration; +using Serilog; + + +namespace ARC.Modules +{ + internal abstract class ArcModule : ApplicationCommandModule + { + + protected static bool _loaded = false; + protected readonly ArcDbContext DbContext; + protected readonly IServiceProvider ServiceProvider; + protected readonly DiscordClient ClientInstance; + protected readonly IConfigurationRoot GlobalConfig; + + protected ArcModule(string moduleName) + { + + DbContext = Arc.Arc.ArcDbContext; + ServiceProvider = Arc.Arc.ServiceProvider; + ClientInstance = Arc.Arc.ClientInstance; + GlobalConfig = Arc.Arc.GlobalConfig; + + if (_loaded) + return; + RegisterEvents(); + Log.Logger.Information($"MODULE LOADED: {moduleName}"); + _loaded = true; + } + + protected abstract void RegisterEvents(); + + } +} diff --git a/ARC/Modules/ModerationModule.cs b/ARC/Modules/ModerationModule.cs new file mode 100644 index 0000000..310b4e2 --- /dev/null +++ b/ARC/Modules/ModerationModule.cs @@ -0,0 +1,188 @@ +using Arc.Schema; +using ARC.Services; +using DSharpPlus; +using DSharpPlus.Entities; +using DSharpPlus.EventArgs; +using DSharpPlus.Interactivity; +using DSharpPlus.SlashCommands; +using ARC.Extensions; + +namespace ARC.Modules +{ + internal class ModerationModule : ArcModule + { + public InteractionService InteractivityService { get; set; } + + public ModerationModule() : base("Moderation") { + + } + + protected override void RegisterEvents() + { + + ClientInstance.ComponentInteractionCreated += ClientInstance_ComponentInteractionCreated; + ClientInstance.ModalSubmitted += HandleUserNotesModal; + + } + + [ContextMenu(DSharpPlus.ApplicationCommandType.UserContextMenu, "User Notes", false), + SlashCommandPermissions(DSharpPlus.Permissions.ManageMessages)] + public async Task UserNotes(ContextMenuContext ctx) + { + + var embed = new DiscordEmbedBuilder() + .WithAuthor($"{ctx.TargetUser} Notes", null, ctx.TargetMember.GetAvatarUrl(ImageFormat.Auto)) + .WithColor(DiscordColor.Blurple); + + var response = new DiscordInteractionResponseBuilder() + .AddComponents(new List() { + new DiscordButtonComponent(ButtonStyle.Primary, $"addnote.{ctx.TargetMember.Id}", "Add Note", false, new DiscordComponentEmoji("📝")), + new DiscordButtonComponent(ButtonStyle.Primary, $"viewnotes.{ctx.TargetMember.Id}", $"View {DbContext.GetUserNotes(ctx.TargetMember.Id, ctx.Guild.Id).Count} Notes", false, new DiscordComponentEmoji("📜")) + }) + .AddEmbed(embed) + .AsEphemeral(true); + + await ctx.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, response); + + } + + private async Task ClientInstance_ComponentInteractionCreated(DiscordClient sender, DSharpPlus.EventArgs.ComponentInteractionCreateEventArgs args) + { + + if (args.Id.StartsWith("viewnotes.")) + await ViewUserNotes(args); + + if (args.Id.StartsWith("addnote.")) + await AddUserNote(args); + + if (args.Id.StartsWith("usernote.delete.")) + await DeleteUserNote(args); + + } + + private async Task DeleteUserNote(ComponentInteractionCreateEventArgs args) + { + + var noteId = long.Parse(args.Interaction.Data.CustomId.Split('.')[2]); + var notes = DbContext.UserNotes.Where(x => x.NoteId == noteId); + + if (!notes.Any()) + return; + + var note = notes.ToList()[0]; + + DbContext.Remove(note); + + // TODO: RESTORE BUTTON + Arc.Arc.ArcDbContext.SaveChanges(); + + var embed = new DiscordEmbedBuilder() + .WithAuthor($"A Note was deleted from {note.User.Username}#{note.User.Discriminator}", null, note.User.GetAvatarUrl(ImageFormat.Auto)) + .WithDescription($"```{note.Note}```") + .AddField("Added By:", $"{note.Author.Mention}", true) + .AddField("Deleted By:", $"{args.User.Mention}", true) + .AddField("Time added:", $"", true) + .AddField("Time deleted:", $"", true) + .WithFooter($"BillieBot v{ClientInstance.ClientVersion} UserNotes", ClientInstance.CurrentUser.GetAvatarUrl(ImageFormat.Auto)) + .WithTimestamp(DateTime.UtcNow) + .WithColor(DiscordColor.Red) + .Build(); + + await note.Guild.Log(new DiscordMessageBuilder().WithEmbed(embed)); + } + + private async Task AddUserNote(ComponentInteractionCreateEventArgs eventArgs) + { + ulong userId = ulong.Parse(eventArgs.Id.Split('.')[1]); + var user = await ClientInstance.GetUserAsync(userId); + + var modal = new DiscordInteractionResponseBuilder() + .WithTitle($"Add Note To {user.Username}") + .WithCustomId($"addnote.{userId}") + .AddComponents(new TextInputComponent(label: "Note", + customId: $"usernote.content", + placeholder: "Enter user note...", + required: true, + style: TextInputStyle.Paragraph)); + + await eventArgs.Interaction.CreateResponseAsync(InteractionResponseType.Modal, modal); + + } + + private async Task ViewUserNotes(ComponentInteractionCreateEventArgs eventArgs) + { + + await eventArgs.Interaction.CreateResponseAsync(InteractionResponseType.DeferredChannelMessageWithSource, new DiscordInteractionResponseBuilder() { IsEphemeral = true }); + + ulong userSnowflake = ulong.Parse(eventArgs.Id.Split('.')[1]); + List notes = DbContext.GetUserNotes(userSnowflake, eventArgs.Guild.Id); + + List pages = new List(); + foreach (var note in notes) + { + var embed = note.CreateEmbedPage(); + var page = new Page(null, embed); + List buttons = new() { + new DiscordButtonComponent(ButtonStyle.Danger, $"usernote.delete.{note.NoteId}", "Delete", false, new DiscordComponentEmoji("🗑️")) + }; + + page.Components = buttons; + pages.Add(page); + } + + await InteractivityService.CreatePaginationResponse(pages, eventArgs.Interaction); + + } + + private async Task HandleUserNotesModal(DiscordClient sender, ModalSubmitEventArgs args) + { + if (args.Interaction.Data.CustomId.StartsWith("addnote.")) + { + + ulong userSnowflake = ulong.Parse(args.Interaction.Data.CustomId.Split('.')[1]); + var author = args.Interaction.User; + var user = await ClientInstance.GetUserAsync(userSnowflake); + String content = args.Values["usernote.content"]; + DateTime dateadded = DateTime.UtcNow; + var guild = args.Interaction.Guild; + + var note = new UserNote((long)guild.Id, (long)user.Id, content, dateadded, (long)author.Id); + + DbContext.UserNotes.Add(note); + await DbContext.SaveChangesAsync(); + + var embed = new DiscordEmbedBuilder() + .WithAuthor($"A New Note was Added to {user.Username}#{user.Discriminator}", null, user.GetAvatarUrl(ImageFormat.Auto)) + .WithDescription($"```{content}```") + .AddField("Added By:", $"{author.Mention}", true) + .AddField("Time added:", $"", true) + .WithFooter($"BillieBot v{ClientInstance.ClientVersion} UserNotes", ClientInstance.CurrentUser.GetAvatarUrl(ImageFormat.Auto)) + .WithTimestamp(dateadded) + .WithColor(DiscordColor.Green) + .Build(); + + await guild.Log( + new DiscordMessageBuilder() + .WithEmbed(embed)); + + var embed2 = new DiscordEmbedBuilder() + .WithAuthor($"{user} Notes", null, user.GetAvatarUrl(ImageFormat.Auto)) + .WithColor(DiscordColor.Blurple); + + var resp = new DiscordInteractionResponseBuilder() + .AddEmbed(embed2) + .AddComponents(new List() + { + new DiscordButtonComponent(ButtonStyle.Primary, $"addnote.{user.Id}", "Add Note", false, + new DiscordComponentEmoji("📝")), + new DiscordButtonComponent(ButtonStyle.Primary, $"viewnotes.{user.Id}", + $"View {DbContext.GetUserNotes(user.Id, guild.Id).Count} Notes", false, + new DiscordComponentEmoji("📜")) + }); + + await args.Interaction.CreateResponseAsync(InteractionResponseType.UpdateMessage, resp); + } + } + + } +} diff --git a/ARC/Modules/UtilitiesModule.cs b/ARC/Modules/UtilitiesModule.cs new file mode 100644 index 0000000..bdafb91 --- /dev/null +++ b/ARC/Modules/UtilitiesModule.cs @@ -0,0 +1,230 @@ +using Arc.Schema; +using Arc.Services; + +using DSharpPlus; +using DSharpPlus.Entities; +using DSharpPlus.SlashCommands; + +using System.Diagnostics; + + +namespace ARC.Modules +{ + internal class UtilitiesModule : ArcModule + { + + public UptimeService UptimeService { get; set; } + + public UtilitiesModule() : base("Utilities") { + + } + + protected override void RegisterEvents() + { + ClientInstance.ComponentInteractionCreated += ClientInstance_ComponentInteractionCreated; + } + + + #region Utilities commands + + [SlashCommand("Ping", "Gets the latency numbers related to the bot.")] + public async Task PingCommand(InteractionContext ctx) + { + + Stopwatch timer = new Stopwatch(); + timer.Start(); + await ctx.Channel.TriggerTypingAsync(); + var msg = await ctx.Channel.SendMessageAsync("."); + timer.Stop(); + await msg.DeleteAsync(); + + + string wsText = "Websocket latency"; + string wsPing = ctx.Client.Ping.ToString(); + + string rtText = "Roundtrip latency"; + string rtPing = timer.ElapsedMilliseconds.ToString(); + + var embed = new DiscordEmbedBuilder() + .WithColor(DiscordColor.PhthaloBlue) + .WithDescription($"🌐 **{wsText}:** ``{wsPing}ms``\n💬 **{rtText}:** ``{rtPing}ms``"); + + var response = new DiscordInteractionResponseBuilder { } + .AddEmbed(embed); + + await ctx.CreateResponseAsync(response); + + } + + [SlashCommand("Uptime", "Get the bot's uptime")] + public async Task UptimeCommand(InteractionContext ctx) + { + + string uptimeMsg = "Uptime"; + string uptimeDays = "Days"; + string uptimeHours = "Hrs"; + string uptimeMinutes = "Mins"; + string uptimeSeconds = "Sec"; + + var uptime = UptimeService.Uptime.Elapsed; + + var embed = new DiscordEmbedBuilder() + .WithAuthor(ClientInstance.CurrentUser.Username, null, ClientInstance.CurrentUser.AvatarUrl) + .WithColor(DiscordColor.PhthaloBlue) + .WithDescription($"**{uptimeMsg}:** ``{uptime.Days}{uptimeDays} {uptime.Hours}{uptimeHours} {uptime.Minutes}{uptimeMinutes} {uptime.Seconds}{uptimeSeconds}``"); + + var response = new DiscordInteractionResponseBuilder() { } + .AddEmbed(embed); + + await ctx.CreateResponseAsync(response); + + } + + [SlashCommand("Avatar", "Get your own or a user's avatar")] + public async Task AvatarCommand(InteractionContext ctx, [Option("User", "Select which user to get the avatar from"),] DiscordUser user = null) + { + + if (user is null) + { + user = ctx.User; + } + + var selectOptions = new List() { + new DiscordSelectComponentOption("Global Avatar", $"global.{user.Id}.{ctx.User.Id}", "Get the user's global avatar", false, new DiscordComponentEmoji("🌐")), + new DiscordSelectComponentOption("Server Avatar", $"server.{user.Id}.{ctx.User.Id}", "Get the user's server avatar", false, new DiscordComponentEmoji("🖥️")) + }; + + var selectmenu = new DiscordSelectComponent("avatar_component", "Select...", selectOptions, false, 1, 1); + + var res = new DiscordInteractionResponseBuilder() + .WithContent(user.GetAvatarUrl(ImageFormat.Auto)) + .AddComponents(selectmenu); + + await ctx.CreateResponseAsync(res); + + } + + private async Task ClientInstance_ComponentInteractionCreated(DiscordClient sender, DSharpPlus.EventArgs.ComponentInteractionCreateEventArgs eventArgs) + { + if (!eventArgs.Id.Equals("avatar_component")) + return; + + string response = ""; + + if (eventArgs.Interaction.Data.Values[0].Split(".")[0] == "server") + { + response = eventArgs.Guild.Members[ulong.Parse(eventArgs.Interaction.Data.Values[0].Split(".")[1])].GetGuildAvatarUrl(ImageFormat.Auto); + } + + if (eventArgs.Interaction.Data.Values[0].Split(".")[0] == "global") + { + var user = await eventArgs.Guild.GetMemberAsync(ulong.Parse(eventArgs.Interaction.Data.Values[0].Split(".")[1])); + response = user.GetAvatarUrl(ImageFormat.Auto); + } + + if (ulong.Parse(eventArgs.Interaction.Data.Values[0].Split(".")[2]) == eventArgs.Interaction.User.Id) + { + await eventArgs.Interaction.CreateResponseAsync(InteractionResponseType.UpdateMessage, new DiscordInteractionResponseBuilder() + .WithContent(response) + .AddComponents(eventArgs.Message.Components)); + } + else + { + await eventArgs.Interaction.CreateResponseAsync(InteractionResponseType.UpdateMessage, new DiscordInteractionResponseBuilder() + .WithContent(eventArgs.Message.Content) + .AddComponents(eventArgs.Message.Components)); + } + + } + + #endregion + + #region server management + + [SlashCommand("SetConfig", "Set a config string"), + SlashCommandPermissions(Permissions.ManageGuild)] + public async Task SetConfigCommand(InteractionContext ctx, [Option("key", "The key name of the config string")] string configKey, [Option("value", "The value of the config string")] string configValue) + { + var config = DbContext.GuildConfigs.Where(c => c.ConfigGuildSnowflake == (long)ctx.Guild.Id && c.ConfigKey.Equals(configKey)); + + if (config.Any()) + config.First().ConfigValue = configValue; + else + { + GuildConfig configin = new((long)ctx.Guild.Id, configKey, configValue); + DbContext.GuildConfigs.Add(configin); + + } + + await DbContext.SaveChangesAsync(); + + var embed = new DiscordEmbedBuilder() + .WithTitle("Config update was successful!") + .WithDescription($"``{configKey} --> {configValue}``"); + + var response = new DiscordInteractionResponseBuilder() + { + IsEphemeral = true + } + .AddEmbed(embed); + + await ctx.CreateResponseAsync(response); + + } + + [SlashCommand("GetConfig", "Set a config string"), + SlashCommandPermissions(Permissions.ManageGuild)] + public async Task GetConfigCommand(InteractionContext ctx, [Option("key", "The key name of the config string")] string configKey) + { + var config = DbContext.GuildConfigs.Where(c => c.ConfigGuildSnowflake == (long)ctx.Guild.Id && c.ConfigKey.Equals(configKey)); + string? configvalue = null; + string descriptionString; + if (config.Any()) + configvalue = config.First().ConfigValue; + + descriptionString = $"``{configKey}`` is currently set to ``{configvalue}``"; + + if (configvalue == null || string.IsNullOrWhiteSpace(configvalue) || configvalue.ToLower().Equals("null")) + descriptionString = $"``{configKey}`` is not currently set to anything"; + + var embed = new DiscordEmbedBuilder() + .WithTitle($"Config for {ctx.Guild}") + .WithDescription(descriptionString); + + var response = new DiscordInteractionResponseBuilder() + { + IsEphemeral = true + } + .AddEmbed(embed); + + await ctx.CreateResponseAsync(response); + + } + + [SlashCommand("BanAppealMsg", "Send the ban appeal message"), + SlashCommandPermissions(Permissions.Administrator)] + public async Task BanAppealMessage(InteractionContext ctx) + { + + DiscordEmbed embedBuild = new DiscordEmbedBuilder() + .WithColor(DiscordColor.DarkRed) + .WithTitle("Ban Appeal") + .WithDescription($"Welcome to {ctx.Guild.Name}.\n\nTo open a ban appeal, please click the button below.") + .WithThumbnail("https://www.pngkey.com/png/full/382-3821512_tak-icon-hammer-01-hammer.png") + .Build(); + + var buttons = new List() { + new DiscordButtonComponent(ButtonStyle.Primary, $"banappeal.send", "Open A Ban Appeal") + }; + + var message = new DiscordMessageBuilder().WithEmbed(embedBuild).AddComponents(buttons); + + await ctx.Channel.SendMessageAsync(message); + await ctx.Interaction.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, new DiscordInteractionResponseBuilder().WithContent("Success!").AsEphemeral(true)); + + } + + #endregion + + } +} diff --git a/ARC/Schema/ArcDbContext.cs b/ARC/Schema/ArcDbContext.cs index 942a566..9ceaed3 100644 --- a/ARC/Schema/ArcDbContext.cs +++ b/ARC/Schema/ArcDbContext.cs @@ -1,14 +1,13 @@ -using System.ComponentModel.DataAnnotations; -using Arc.Exceptions; + using ARC.Extensions; -using DocumentFormat.OpenXml.ExtendedProperties; + using DSharpPlus; using DSharpPlus.Entities; using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Metadata; + using Microsoft.Extensions.Configuration; using Npgsql; -using Serilog; + namespace Arc.Schema; @@ -70,7 +69,17 @@ public async Task CloseModmail(Modmail modmail) await SaveChangesAsync(); } + public List GetUserNotes(ulong userSnowflake, ulong guildSnowflake) + { + var notes = UserNotes.Where(x => x.UserSnowflake == (long)userSnowflake && x.GuildSnowflake == (long)guildSnowflake).ToList(); + return notes; + } + public List GetNextAppeal(ulong userSnowflake) + { + return Appeals.ToList().Where(x => x.UserSnowflake == (long)userSnowflake).ToList(); + } + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) => optionsBuilder.UseNpgsql(new NpgsqlConnection(DbPath)); @@ -124,21 +133,15 @@ public async Task CreateSession() $"Modmail with user: {User}" ); - ChannelSnowflake = (long)mailChannel.Id; - Uri avatarUri = new Uri(User.GetAvatarUrl(DSharpPlus.ImageFormat.Auto)); + DiscordWebhook discordWebhook; - using (var httpClient = new HttpClient()) - { - var uriWoQuery = avatarUri.GetLeftPart(UriPartial.Path); - var fileExt = Path.GetExtension(uriWoQuery); - - var path = Path.Combine(Path.GetTempPath(), $"avatar{fileExt}"); - var imageBytes = await httpClient.GetByteArrayAsync(avatarUri); - Stream filestream = new MemoryStream(imageBytes); - discordWebhook = await mailChannel.CreateWebhookAsync(User.Username, avatar: filestream); - } + discordWebhook = await mailChannel.CreateWebhookAsync(User.Username); + + ChannelSnowflake = (long)mailChannel.Id; WebhookSnowflake = (long)discordWebhook.Id; - + + await Arc.ArcDbContext.Modmails.AddAsync(this); + await Arc.ArcDbContext.SaveChangesAsync(); return true; } @@ -233,8 +236,8 @@ public async Task SendModmailMenu() public async Task SaveTranscript() { - File.Delete("./temp/transcript.html"); - File.Copy("./template.html", "./temp/transcript.html"); + File.Delete($"./temp/transcript-{ModmailId}.html"); + File.Copy("./template.html", $"./temp/transcript-{ModmailId}.html"); IReadOnlyList msgs = await Channel.GetMessagesAsync(2000); @@ -242,7 +245,7 @@ public async Task SaveTranscript() { var message = msgs[i]; - await File.AppendAllTextAsync("./temp/transcript.html", + await File.AppendAllTextAsync($"./temp/transcript-{ModmailId}.html", $@" @@ -272,13 +275,13 @@ await File.AppendAllTextAsync("./temp/transcript.html", } - await File.AppendAllTextAsync("./temp/transcript.html", @" + await File.AppendAllTextAsync($"./temp/transcript-{ModmailId}.html", @" "); } public DiscordUser User => Arc.ClientInstance.GetUserAsync((ulong)UserSnowflake).GetAwaiter().GetResult(); - public DiscordWebhook Webhook => Arc.ClientInstance.GetWebhookAsync((ulong) WebhookSnowflake).GetAwaiter().GetResult(); + public DiscordWebhook Webhook => Channel.GetWebhooksAsync().GetAwaiter().GetResult()[0]; public DiscordChannel Channel => Arc.ClientInstance.GetChannelAsync((ulong)ChannelSnowflake).GetAwaiter().GetResult(); public DiscordGuild Guild => Arc.ClientInstance.GetGuildAsync(Channel.Guild.Id).GetAwaiter().GetResult(); public DiscordMember Member => Guild.GetMemberAsync(User.Id).GetAwaiter().GetResult(); @@ -328,7 +331,7 @@ private Appeal() { } - + public DiscordUser User => Arc.ClientInstance.GetUserAsync((ulong)UserSnowflake).GetAwaiter().GetResult(); } [PrimaryKey("NoteId")] @@ -337,13 +340,15 @@ public class UserNote public long NoteId { get; } public long UserSnowflake { get; set; } + public long GuildSnowflake { get; set; } public string Note { get; set; } public DateTime DateAdded { get; set; } public long AuthorSnowflake { get; set; } - public UserNote(long userSnowflake, string note, DateTime dateAdded, long authorSnowflake) + public UserNote(long guildSnowflake, long userSnowflake, string note, DateTime dateAdded, long authorSnowflake) { + GuildSnowflake = guildSnowflake; UserSnowflake = userSnowflake; Note = note; DateAdded = dateAdded; @@ -356,6 +361,20 @@ private UserNote() } + public DiscordUser User => Arc.ClientInstance.GetUserAsync((ulong)UserSnowflake).GetAwaiter().GetResult(); + public DiscordGuild Guild => Arc.ClientInstance.GetGuildAsync((ulong)GuildSnowflake).GetAwaiter().GetResult(); + public DiscordMember Member => Guild.GetMemberAsync(User.Id).GetAwaiter().GetResult(); + public DiscordUser Author => Arc.ClientInstance.GetUserAsync((ulong)AuthorSnowflake).GetAwaiter().GetResult(); + public DiscordMember AuthorMember => Guild.GetMemberAsync(Author.Id).GetAwaiter().GetResult(); + + internal DiscordEmbedBuilder CreateEmbedPage() + { + return new DiscordEmbedBuilder() + .WithAuthor($"{User} Note #{NoteId}", null, User.GetAvatarUrl(ImageFormat.Auto)) + .WithDescription($"```{Note}```") + .WithTimestamp(DateAdded) + .WithFooter($"Note added by {Author.Username}#{Author.Discriminator}", Author.GetAvatarUrl(ImageFormat.Auto)); + } } diff --git a/ARC/Services/ArcService.cs b/ARC/Services/ArcService.cs index d852b3f..49c6a16 100644 --- a/ARC/Services/ArcService.cs +++ b/ARC/Services/ArcService.cs @@ -1,6 +1,6 @@ using Arc.Schema; using DSharpPlus; -using DSharpPlus.Entities; + using Microsoft.Extensions.Configuration; using Serilog; diff --git a/ARC/Services/BanAppealService.cs b/ARC/Services/BanAppealService.cs new file mode 100644 index 0000000..81421c7 --- /dev/null +++ b/ARC/Services/BanAppealService.cs @@ -0,0 +1,244 @@ + +using Arc.Schema; +using Arc.Services; +using DSharpPlus; +using DSharpPlus.Entities; +using DSharpPlus.EventArgs; + + +namespace ARC.Services; + +public class BanAppealService : ArcService +{ + public BanAppealService() : base("Appeal") + { + + ClientInstance.ComponentInteractionCreated += ClientInstanceOnComponentInteractionCreated; + ClientInstance.ModalSubmitted += ClientInstanceOnModalSubmitted; + + } + + private async Task ClientInstanceOnModalSubmitted(DiscordClient sender, ModalSubmitEventArgs args) + { + + if (args.Interaction.Data.CustomId != "banappeal.recieved") + return; + + var appeals = DbContext.GetNextAppeal(args.Interaction.User.Id); + var appeal = appeals.Any()? appeals[0] : null; + + if (appeal == null || appeal.NextAppeal.ToUniversalTime().CompareTo(DateTime.UtcNow) < 0) + { + + if (appeal is not null) + DbContext.Appeals.Remove(appeal); + + appeal = new Appeal((long)args.Interaction.User.Id, DateTime.UtcNow.AddDays(30)); + + DbContext.Appeals.Add(appeal); + await DbContext.SaveChangesAsync(); + + appeal = DbContext.GetNextAppeal((ulong)appeal.UserSnowflake)[0]; + + DiscordEmbed embed = new DiscordEmbedBuilder() + .WithAuthor($"New Ban Appeal From {args.Interaction.User.Username}#{args.Interaction.User.Discriminator}", iconUrl:args.Interaction.User.GetAvatarUrl(ImageFormat.Auto)) + .AddField("Which moderator banned you?", args.Values["banappeal.response.mod"]) + .AddField("What was the reason given for your ban?", args.Values["banappeal.response.reason"]) + .AddField("Why do you think you should be unbanned?", args.Values["banappeal.response.why"]) + .WithTimestamp(DateTime.UtcNow); + + var buttons = new List() { + new DiscordButtonComponent(ButtonStyle.Success, $"banappeal.unban.{appeal.AppealId}", "Unban", false, new DiscordComponentEmoji("🔓")), + new DiscordButtonComponent(ButtonStyle.Danger, $"banappeal.deny.{appeal.AppealId}", "Deny", false, new DiscordComponentEmoji("🔨")) + }; + + var response = new DiscordMessageBuilder() + .AddEmbed(embed) + .AddComponents(buttons); + + var appealChannelSnowflake = ulong.Parse(DbContext.Config[args.Interaction.Guild.Id]["appealschannel"]); + var appealChannel = await ClientInstance.GetChannelAsync(appealChannelSnowflake); + + + await args.Interaction.CreateResponseAsync(InteractionResponseType.DeferredMessageUpdate); + await appealChannel.SendMessageAsync(response); + return; + + } + + DiscordEmbed embed2 = new DiscordEmbedBuilder() + .WithAuthor($"{args.Interaction.User.Username}#{args.Interaction.User.Discriminator}, You've already appealed", iconUrl:args.Interaction.User.GetAvatarUrl(ImageFormat.Auto)) + .WithColor(DiscordColor.Red) + .WithFooter("Thank you for your patience!", ClientInstance.CurrentUser.GetAvatarUrl(ImageFormat.Auto)) + .WithDescription($"Please wait for the result of your appeal. If you already received it, you can wait out your ban duration. If you were permanently banned, you can appeal again on ") + .WithTimestamp(DateTime.UtcNow); + + var user = await args.Interaction.Guild.GetMemberAsync(args.Interaction.User.Id); + await user.SendMessageAsync(embed2); + await args.Interaction.CreateResponseAsync(InteractionResponseType.DeferredMessageUpdate); + + } + + private async Task ClientInstanceOnComponentInteractionCreated(DiscordClient sender, ComponentInteractionCreateEventArgs args) + { + + switch (args.Id) + { + case "banappeal.send": + await HandleBanAppeal(args); + break; + } + + if (args.Id.Contains("banappeal.deny")) + await HandleAppealDeny(args); + + if (args.Id.Contains("banappeal.unban")) + await HandleAppealAccept(args); + + } + + private async Task HandleAppealAccept(ComponentInteractionCreateEventArgs args) + { + + var appealId = long.Parse(args.Id.Split('.')[2]); + var appeals = DbContext.Appeals.ToList().Where(x => x.AppealId == appealId).ToList(); + + if (!appeals.Any()) + await args.Interaction.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, + new DiscordInteractionResponseBuilder() + .WithContent($"ERROR: appeal not found, you can delete it.") + .AsEphemeral()); + + var appeal = appeals[0]; + + var appealGuildSnowflake = ulong.Parse(DbContext.Config[args.Interaction.Guild.Id]["mainserver"]); + var guild = await ClientInstance.GetGuildAsync(appealGuildSnowflake); + + await guild.UnbanMemberAsync(appeal.User); + + DbContext.Appeals.Remove(appeal); + await DbContext.SaveChangesAsync(); + + var msg = args.Message.Embeds[0]; + + var mbuilder= new DiscordInteractionResponseBuilder() + .AddEmbed(new DiscordEmbedBuilder(msg) + .WithColor(DiscordColor.Green) + .WithTitle($"Appeal Accepted by {args.User.Username}#{args.User.Discriminator}!")); + + await args.Interaction.CreateResponseAsync(InteractionResponseType.UpdateMessage, mbuilder); + + try + { + var member = await args.Guild.GetMemberAsync(appeal.User.Id); + await member.SendMessageAsync(new DiscordEmbedBuilder() + .WithAuthor( + $"{member.Username}#{member.Discriminator}, your ban appeal was accepted by {args.User.Username}#{args.User.Discriminator}!", + iconUrl: member.GetAvatarUrl(ImageFormat.Auto)) + .WithColor(DiscordColor.Green) + .WithFooter("Thank you for your patience!", ClientInstance.CurrentUser.GetAvatarUrl(ImageFormat.Auto)) + .WithDescription( + $"Congrats!, your ban appeal was accepted in {guild.Name}!") + .WithTimestamp(DateTime.UtcNow)); + } + catch (Exception ex) + { + + } + } + + private async Task HandleAppealDeny(ComponentInteractionCreateEventArgs args) + { + + var appealId = long.Parse(args.Id.Split('.')[2]); + var appeals = DbContext.Appeals.ToList().Where(x => x.AppealId == appealId).ToList(); + + if (!appeals.Any()) + await args.Interaction.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, + new DiscordInteractionResponseBuilder() + .WithContent($"ERROR: appeal not found! You can delete it.") + .AsEphemeral()); + + var appeal = appeals[0]; + + var appealGuildSnowflake = ulong.Parse(DbContext.Config[args.Interaction.Guild.Id]["mainserver"]); + var guild = await ClientInstance.GetGuildAsync(appealGuildSnowflake); + + var msg = args.Message.Embeds[0]; + + var mbuilder= new DiscordInteractionResponseBuilder() + .AddEmbed(new DiscordEmbedBuilder(msg) + .WithColor(DiscordColor.Red) + .WithTitle($"Appeal Denied by {args.User.Username}#{args.User.Discriminator}!")); + + await args.Interaction.CreateResponseAsync(InteractionResponseType.UpdateMessage, mbuilder); + + try + { + var member = await args.Guild.GetMemberAsync(appeal.User.Id); + await member.SendMessageAsync(new DiscordEmbedBuilder() + .WithAuthor( + $"{member.Username}#{member.Discriminator}, your ban appeal was denied by {args.User.Username}#{args.User.Discriminator}!", + iconUrl: member.GetAvatarUrl(ImageFormat.Auto)) + .WithColor(DiscordColor.Red) + .WithFooter("Thank you for your patience!", ClientInstance.CurrentUser.GetAvatarUrl(ImageFormat.Auto)) + .WithDescription( + "Unfortunately, your ban appeal was rejected. If you've been temporarily banned, please wait the duration of your ban. If you were permanently banned, you can appeal again in a month.") + .WithTimestamp(DateTime.UtcNow)); + } + catch (Exception ex) + { + + } + } + + private async Task HandleBanAppeal(ComponentInteractionCreateEventArgs args) + { + + var resp = new DiscordInteractionResponseBuilder() + .WithTitle("Ban appeal") + .WithCustomId("banappeal.recieved") + .AddComponents( new TextInputComponent(label:"Which moderator banned you?", + customId: $"banappeal.response.mod", + placeholder:"Izzy#4810", + required: true, max_length: 30)) + .AddComponents( new TextInputComponent(label:"What was the reason given for your ban?", + customId:"banappeal.response.reason", + placeholder:"Reason...", required: true, max_length: 30)) + .AddComponents( new TextInputComponent(label:"Why do you think you should be unbanned?", + customId:"banappeal.response.why", placeholder:"Explain why...", + required: true, style: TextInputStyle.Paragraph)); + + ulong appealGuildSnowflake; + try + { + appealGuildSnowflake = ulong.Parse(DbContext.Config[args.Interaction.Guild.Id]["mainserver"]); + } + catch (KeyNotFoundException ex) + { + await args.Interaction.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, + new DiscordInteractionResponseBuilder() + .WithContent("This guild is not setup for ban appeals! Please ask an admin to set the 'mainserver' key to the ID of your main server using /setconfig") + .AsEphemeral()); + return; + } + + var guild = await ClientInstance.GetGuildAsync(appealGuildSnowflake); + + var bans = await guild.GetBansAsync(); + + if (bans.All(x => x.User.Id != args.User.Id)) + { + await args.Interaction.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, + new DiscordInteractionResponseBuilder() + .WithContent($"You are not banned in {guild.Name}") + .AsEphemeral()); + return; + } + + + await args.Interaction.CreateResponseAsync(InteractionResponseType.Modal, resp); + + } + +} \ No newline at end of file diff --git a/ARC/Services/InteractionService.cs b/ARC/Services/InteractionService.cs new file mode 100644 index 0000000..0220cba --- /dev/null +++ b/ARC/Services/InteractionService.cs @@ -0,0 +1,157 @@ +using Arc.Services; +using DSharpPlus.Entities; +using DSharpPlus.Interactivity; +using DSharpPlus; + +using DSharpPlus.Interactivity.Extensions; + + +namespace ARC.Services +{ + + internal class PaginationSession + { + private DiscordInteraction _interaction; + + private DiscordMessage _message; + + private List _pages = new List(); + + private int _pageIndex; + + private List PaginationButtons { + get + { + return new List() { + new DiscordButtonComponent(ButtonStyle.Primary, "pagination.previous", "", !(_pages.Count > 1), new DiscordComponentEmoji("◀️")), + new DiscordButtonComponent(ButtonStyle.Primary, "pagination.stop", "", false, new DiscordComponentEmoji("⏹️")), + new DiscordButtonComponent(ButtonStyle.Primary, "pagination.next", "", !(_pages.Count > 1), new DiscordComponentEmoji("▶️")) + }; + } + } + + public PaginationSession(DiscordInteraction interaction, List pages) + { + _interaction = interaction; + _pages = pages; + + if (_pages.Count < 1 ) + _pages.Add(new Page(null, new DiscordEmbedBuilder() + .WithAuthor("No more pages avalible", null) + .WithDescription("```No pages```"))); + + Task.Run(async () => + { + await Task.Delay(60000); + await Stop(); + }); + + } + + public async Task Start() + { + Arc.Arc.ClientInstance.ComponentInteractionCreated += this.PaginationInteractionHandler; + _message = await _interaction.GetOriginalResponseAsync(); + await Update(); + } + + private async Task PaginationInteractionHandler(DiscordClient sender, DSharpPlus.EventArgs.ComponentInteractionCreateEventArgs args) + { + if (!args.Message.Equals(_message)) + return; + + await args.Interaction.CreateResponseAsync(InteractionResponseType.DeferredMessageUpdate); + + switch (args.Interaction.Data.CustomId) { + + case "pagination.previous": + DecIndex(); + await Update(); + break; + + case "pagination.stop": + await Stop(); + break; + + case "pagination.next": + IncIndex(); + await Update(); + break; + } + + + if (args.Interaction.Data.CustomId.Contains("delete")) + { + + var currentPage = _pages[_pageIndex]; + + var component = currentPage.Components.First(x => x.CustomId.Contains("delete")); + currentPage.Components.Remove(component); + currentPage.Embed = new DiscordEmbedBuilder(currentPage.Embed).WithDescription("```Deleted```"); + + await Update(); + + } + + } + + public async Task Stop() + { + + await Update(); + Arc.Arc.ClientInstance.ComponentInteractionCreated -= this.PaginationInteractionHandler; + + + var hook = new DiscordWebhookBuilder().AddEmbed(_pages[_pageIndex].Embed); + hook.ClearComponents(); + await _interaction.EditOriginalResponseAsync(hook); + + } + + public async Task Update() + { + var webhook = new DiscordWebhookBuilder() + .AddComponents(PaginationButtons) + .AddEmbed(_pages[_pageIndex].Embed); + + if (_pages[_pageIndex].Components.Count > 0) + { + webhook.AddComponents(_pages[_pageIndex].Components); + } + + await _interaction.EditOriginalResponseAsync(webhook); + } + + public void IncIndex() + { + if (_pageIndex >= _pages.Count - 1) + _pageIndex = _pageIndex - _pages.Count; + + _pageIndex++; + } + + public void DecIndex() + { + if (_pageIndex <= 0) + _pageIndex = _pages.Count; + _pageIndex--; + } + + } + + internal class InteractionService : ArcService + { + + public InteractionService() : base("Interactions") + { + ClientInstance.UseInteractivity(); + } + + public async Task CreatePaginationResponse(List pages, DiscordInteraction interaction) + { + PaginationSession session = new PaginationSession(interaction, pages); + await session.Start(); + } + + } +} diff --git a/ARC/Services/ModMailService.cs b/ARC/Services/ModMailService.cs index b9ac533..223cbf7 100644 --- a/ARC/Services/ModMailService.cs +++ b/ARC/Services/ModMailService.cs @@ -3,11 +3,10 @@ using DSharpPlus; using DSharpPlus.Entities; using DSharpPlus.EventArgs; -using Fluent.Architecture; + using Serilog; -using System; -using System.Diagnostics.Tracing; -using System.Linq.Dynamic.Core; + +using Arc.Exceptions; namespace Arc.Services; @@ -22,7 +21,12 @@ public ModMailService() : base("Modmail") private async Task ClientInstance_ComponentInteractionCreated(DiscordClient sender, ComponentInteractionCreateEventArgs args) { + var eventId = args.Id; + + if (!eventId.StartsWith("modmail")) + return; + var eventAction = ClientInstance.GetEventAction(eventId); if (eventAction == null) @@ -87,21 +91,19 @@ private async Task ClientInstanceOnMessageCreated(DiscordClient sender, MessageC // TODO: INSERT SERVER PICKING MECHANISM HERE // For now we will simply choose the billie server. - + Modmail? modmail = null; try { var guild = await sender.GetGuildAsync(ulong.Parse(GlobalConfig.GetSection("discord:guild").Value ?? "0")); - var modmail = new Modmail(e.Author.Id, guild); + modmail = new Modmail(e.Author.Id, guild); var session = await modmail.CreateSession(); if (!session) - return; - - DbContext.Modmails.Add(modmail); - DbContext.SaveChanges(); - + { + throw new ArcInitFailedException("Modmail failed becase the session could not be created!"); + } await modmail.SendUserSystem("Your mod mail request was recieved! Please wait and a staff member will contact you shortly!"); await modmail.SendModmailMenu(); @@ -110,6 +112,11 @@ private async Task ClientInstanceOnMessageCreated(DiscordClient sender, MessageC Log.Logger.Error($"MODMAIL CREATION FAILED: {ex}"); + if (modmail != null) + { + DbContext.Remove(modmail); + await DbContext.SaveChangesAsync(); + } } @@ -141,7 +148,7 @@ private async Task ClientInstanceOnMessageCreated(DiscordClient sender, MessageC private async Task HandleMailChannelMessage(DiscordClient sender, MessageCreateEventArgs e) { - var mail = DbContext.Modmails.Where(x => x.ChannelSnowflake == (long)e.Channel.Id).ToList(); + var mail = DbContext.Modmails.ToList().Where(x => x.ChannelSnowflake == (long)e.Channel.Id).ToList(); if (!mail.Any()) return; @@ -169,19 +176,18 @@ private async Task SaveModMailSession(Modmail modmail, DiscordUser saver) // TODO: SAVE TO DATABASE AND DISPLAY FOR USER DASHBOARD INSTEAD var transcrpt = await ClientInstance.GetChannelAsync(ulong.Parse(DbContext.Config[modmail.Guild.Id]["transcriptchannel"])); - var filestream = new MemoryStream(await File.ReadAllBytesAsync("./temp/transcript.html")); - + var transcrptfiles = await ClientInstance.GetChannelAsync(ulong.Parse(GlobalConfig.GetSection("discord:debug_log").Value)); + var msg = new DiscordMessageBuilder(); - msg.AddFile(new FileStream("./temp/transcript.html", FileMode.OpenOrCreate)); + msg.AddFile(new FileStream($"./temp/transcript-{modmail.ModmailId}.html", FileMode.OpenOrCreate)); + var debug_file = await transcrptfiles.SendMessageAsync(msg); var embed = new DiscordEmbedBuilder() .WithModmailStyle() .WithTitle("Modmail Transcirpt") - .WithDescription($"**Modmail with:** {modmail.User.Mention}\n**Saved** **by** {saver.Mention}"); - - msg.AddEmbed(embed); + .WithDescription($"**Modmail with:** {modmail.User.Mention}\n**Saved** **by** {saver.Mention}\n\n[Transcript]({debug_file.Attachments[0].Url})"); - await transcrpt.SendMessageAsync(msg); + await transcrpt.SendMessageAsync(embed); } diff --git a/ARC/Services/SlashCommandsService.cs b/ARC/Services/SlashCommandsService.cs new file mode 100644 index 0000000..1a6ae46 --- /dev/null +++ b/ARC/Services/SlashCommandsService.cs @@ -0,0 +1,54 @@ +using Arc.Services; +using ARC.Modules; +using DSharpPlus; +using DSharpPlus.Entities; +using DSharpPlus.SlashCommands; +using Serilog; + +using DSharpPlus.EventArgs; + +namespace ARC.Services +{ + internal class SlashCommandsService : ArcService + { + + public SlashCommandsService() : base("Slashcmds") + { + var slashCommandsConfig = new SlashCommandsConfiguration() + { + Services = ServiceProvider, + + }; + + var slashCommands = ClientInstance.UseSlashCommands(slashCommandsConfig); + + // Register slash command modules here + slashCommands.RegisterCommands(); + slashCommands.RegisterCommands(); + + slashCommands.SlashCommandErrored += SlashCommands_SlashCommandErrored; + ClientInstance.ClientErrored += ClientInstanceOnClientErrored; + } + + private async Task ClientInstanceOnClientErrored(DiscordClient sender, ClientErrorEventArgs args) + { + Log.Logger.Error(args.Exception.ToString()); + } + + private async Task SlashCommands_SlashCommandErrored(SlashCommandsExtension sender, DSharpPlus.SlashCommands.EventArgs.SlashCommandErrorEventArgs args) + { + + Log.Logger.Error(args.Exception.ToString()); + + var errorEmbed = new DiscordEmbedBuilder() + .WithTitle("Error!") + .WithColor(DiscordColor.Red) + .WithDescription($"***An error occured! Please report this to your server admin or Izzy***\n```{args.Exception}```"); + + await args.Context.Interaction.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, new DiscordInteractionResponseBuilder() + .AddEmbed(errorEmbed) + .AsEphemeral()); + + } + } +} diff --git a/DSharpPlus b/DSharpPlus index 0d72a44..26873fb 160000 --- a/DSharpPlus +++ b/DSharpPlus @@ -1 +1 @@ -Subproject commit 0d72a44dced62b618382550a8ae31d03fb222127 +Subproject commit 26873fb3e4e993340f68ab0314bd994be98e9df0 diff --git a/global.json b/global.json new file mode 100644 index 0000000..388e72e --- /dev/null +++ b/global.json @@ -0,0 +1,7 @@ +{ + "sdk": { + "version": "7.0.0", + "rollForward": "major", + "allowPrerelease": true + } +} \ No newline at end of file