diff --git a/ChaosBot/Discord/Modules/Admin/Role.cs b/ChaosBot/Discord/Modules/Admin/Role.cs new file mode 100644 index 0000000..1389d88 --- /dev/null +++ b/ChaosBot/Discord/Modules/Admin/Role.cs @@ -0,0 +1,205 @@ +using System; +using Discord; +using Discord.Commands; +using System.Threading.Tasks; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Text.RegularExpressions; +using ChaosBot.Discord.PreConditions; +using ChaosBot.Models; +using ChaosBot.Repositories; +using Microsoft.EntityFrameworkCore; +using NLog; + +namespace ChaosBot.Discord.Modules.Admin +{ + public class Role : ModuleBase + { + private static readonly ILogger Logger = Program.Logger; + + [Command("role")] + [Alias("role help", "role info")] + [CheckCommandPerm("Admin")] + public async Task RoleInfoCommand() + { + try + { + var sb = new StringBuilder(); + var embed = new EmbedBuilder(); + + embed.WithColor(new Color(255, 255, 0)); + embed.Title = $"Role Management Help"; + sb.AppendLine(); + sb.AppendLine("To add a role-reaction to a message:"); + sb.AppendLine($"{ConfigurationRepository.GetValue("Discord:Prefix", Context.Guild.Id, "!")}role add "); + sb.AppendLine("To add many role-reactions to a message add more sets of emote and role:"); + sb.AppendLine($"{ConfigurationRepository.GetValue("Discord:Prefix", Context.Guild.Id, "!")}role add "); + sb.AppendLine("To remove a role-reaction from a message:"); + sb.AppendLine($"{ConfigurationRepository.GetValue("Discord:Prefix", Context.Guild.Id, "!")}role remove "); + sb.AppendLine(); + sb.AppendLine("To view this help:"); + sb.AppendLine($"{ConfigurationRepository.GetValue("Discord:Prefix", Context.Guild.Id, "!")}role help"); + + /* + * Add the string to the Embed + */ + embed.Description = sb.ToString(); + + /* + * Reply with the Embed created above + */ + await ReplyAsync(null, false, embed.Build()); + } + catch (Exception ex) + { + Logger.Error($"{MethodBase.GetCurrentMethod().ReflectedType.FullName}: Exception [{ex}] thrown, <[{ex.Message}]>."); + } + } + + [Command("role add")] + [CheckCommandPerm("Admin")] + public async Task RoleAddCommand(params string[] parameters) + { + try + { + await using (ChaosbotContext dbContext = new ChaosbotContext()) + { + string parameterString = String.Join("", parameters); + + // Fetch last message + IMessage message = (await Context.Channel.GetMessagesAsync(2).FlattenAsync()).Last(); + + // Parse parameters + string pattern = @"((|[^<]+)<@&(\d+)>)"; + string input = parameterString; + + foreach (Match m in Regex.Matches(input, pattern)) + { + try + { + IEmote emote; + string emoteString = m.Groups[2].Value; + string roleString = m.Groups[3].Value; + IRole role = Context.Guild.Roles.First(r => r.Id.ToString() == roleString); + + if (Emote.TryParse(emoteString, out Emote tempEmote)) + { + if (tempEmote.Animated && Context.Client.CurrentUser.PremiumType != PremiumType.Nitro) + throw new NotSupportedException("No support for animated icons"); + + if (Context.Guild.Emotes.All(e => e.Id != tempEmote.Id) && + Context.Client.CurrentUser.PremiumType != PremiumType.Nitro) + throw new NotSupportedException($"No support for emotes from other servers"); + + emote = tempEmote; + } + else + { + emote = new Emoji(emoteString); + } + + // Register DB entry + RoleReaction roleReaction = new RoleReaction + { + DiscordGuildId = Context.Guild.Id, + DiscordMessageId = message.Id, + DiscordRoleId = role.Id, + DiscordEmoteName = emote.ToString() + }; + + await dbContext.RoleReactions.Upsert(roleReaction) + .On(r => new {r.DiscordGuildId, r.DiscordEmoteNameEncoded, r.DiscordMessageId, r.DiscordRoleId}) + .RunAsync(); + + // Add reaction to message + await message.AddReactionAsync(emote); + } + catch (Exception ex) + { + await ReplyAsync($"Something went wrong trying to process {m.Value}: {ex.Message}"); + Logger.Error( + $"{MethodBase.GetCurrentMethod().ReflectedType.FullName}: Exception [{ex}] thrown, <[{ex.Message}]>."); + } + } + + await dbContext.SaveChangesAsync(); + } + } + catch (Exception ex) + { + Logger.Error($"{MethodBase.GetCurrentMethod().ReflectedType.FullName}: Exception [{ex}] thrown, <[{ex.Message}]>."); + } + } + + [Command("role remove")] + [CheckCommandPerm("Admin")] + public async Task RoleRemoveCommand(params string[] parameters) + { + try + { + await using (ChaosbotContext dbContext = new ChaosbotContext()) + { + string parameterString = String.Join("", parameters); + + // Fetch last message + IMessage message = (await Context.Channel.GetMessagesAsync(2).FlattenAsync()).Last(); + + // Parse parameters + string pattern = @"(|..)"; + string input = parameterString; + + foreach (Match m in Regex.Matches(input, pattern)) + { + Logger.Info(m.Value); + try + { + IEmote emote; + string emoteString = m.Value; + + if (Emote.TryParse(emoteString, out Emote tempEmote)) + { + if (tempEmote.Animated && Context.Client.CurrentUser.PremiumType != PremiumType.Nitro) + throw new NotSupportedException("No support for animated icons"); + + if (Context.Guild.Emotes.All(e => e.Id != tempEmote.Id) && + Context.Client.CurrentUser.PremiumType != PremiumType.Nitro) + throw new NotSupportedException($"No support for emotes from other servers"); + + emote = tempEmote; + } + else + { + emote = new Emoji(emoteString); + } + + // Delete DB entries + IQueryable roleReactions = dbContext.RoleReactions; + dbContext.RemoveRange(roleReactions + .Where(r => r.DiscordGuildId == Context.Guild.Id) + .ToList() + .Where(r => r.DiscordEmoteName == emote.ToString()) + .Where(r => r.DiscordMessageId == message.Id) + .ToList()); + + // Remove reaction from message + await message.RemoveReactionAsync(emote, Context.Client.CurrentUser); + } + catch (Exception ex) + { + await ReplyAsync($"Something went wrong trying to process {m.Value}: {ex.Message}"); + Logger.Error( + $"{MethodBase.GetCurrentMethod().ReflectedType.FullName}: Exception [{ex}] thrown, <[{ex.Message}]>."); + } + } + + await dbContext.SaveChangesAsync(); + } + } + catch (Exception ex) + { + Logger.Error($"{MethodBase.GetCurrentMethod().ReflectedType.FullName}: Exception [{ex}] thrown, <[{ex.Message}]>."); + } + } + } +} diff --git a/ChaosBot/Discord/Services/CommandHandler.cs b/ChaosBot/Discord/Services/CommandHandler.cs index af9d4a3..f2d2290 100644 --- a/ChaosBot/Discord/Services/CommandHandler.cs +++ b/ChaosBot/Discord/Services/CommandHandler.cs @@ -33,6 +33,10 @@ namespace ChaosBot.Discord.Services _client.MessageReceived += MessageReceivedAsync; + _client.ReactionAdded += ReactionAddedAsync; + + _client.ReactionRemoved += ReactionRemovedAsync; + _client.UserJoined += AnnounceJoinedUser; _client.UserLeft += AnnounceLeftUser; @@ -81,6 +85,16 @@ namespace ChaosBot.Discord.Services } } + public async Task ReactionAddedAsync(Cacheable cacheableMessage, ISocketMessageChannel socketMessageChannel, SocketReaction reaction) + { + RoleReactionHandler.HandleReactionAdded(cacheableMessage, socketMessageChannel, reaction); + } + + public async Task ReactionRemovedAsync(Cacheable cacheableMessage, ISocketMessageChannel socketMessageChannel, SocketReaction reaction) + { + RoleReactionHandler.HandleReactionRemoved(cacheableMessage, socketMessageChannel, reaction); + } + public async Task AnnounceJoinedUser(SocketGuildUser user) { try diff --git a/ChaosBot/Discord/Services/RoleReactionHandler.cs b/ChaosBot/Discord/Services/RoleReactionHandler.cs new file mode 100644 index 0000000..39c4802 --- /dev/null +++ b/ChaosBot/Discord/Services/RoleReactionHandler.cs @@ -0,0 +1,76 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using ChaosBot.Models; +using Discord; +using Discord.WebSocket; +using NLog; + +namespace ChaosBot.Discord.Services +{ + public static class RoleReactionHandler + { + private static readonly ILogger Logger = Program.Logger; + + public static async void HandleReactionAdded(Cacheable cacheableMessage, ISocketMessageChannel socketMessageChannel, SocketReaction reaction) + { + Optional optionalUser = reaction.User; + if (!optionalUser.IsSpecified) return; + if (!(optionalUser.Value is IGuildUser user)) return; + if (!(socketMessageChannel is SocketGuildChannel channel)) return; + + await using ChaosbotContext dbContext = new ChaosbotContext(); + + IQueryable roleReactionsQueryable = dbContext.RoleReactions; + List roleReactions = roleReactionsQueryable + .Where(r => r.DiscordMessageId == cacheableMessage.Id) + .ToList() + .Where(r => r.DiscordEmoteName == reaction.Emote.ToString()) + .ToList(); + + foreach (RoleReaction roleReaction in roleReactions) + { + try + { + SocketRole role = channel.Guild.Roles.FirstOrDefault(r => r.Id == roleReaction.DiscordRoleId); + await user.AddRoleAsync(role); + } + catch (Exception ex) + { + Logger.Error($"{MethodBase.GetCurrentMethod().ReflectedType.FullName}: Exception [{ex}] thrown, <[{ex.Message}]>."); + } + } + } + + public static async void HandleReactionRemoved(Cacheable cacheableMessage, ISocketMessageChannel socketMessageChannel, SocketReaction reaction) + { + Optional optionalUser = reaction.User; + if (!optionalUser.IsSpecified) return; + if (!(optionalUser.Value is IGuildUser user)) return; + if (!(socketMessageChannel is SocketGuildChannel channel)) return; + + await using ChaosbotContext dbContext = new ChaosbotContext(); + + IQueryable roleReactionsQueryable = dbContext.RoleReactions; + List roleReactions = roleReactionsQueryable + .Where(r => r.DiscordMessageId == cacheableMessage.Id) + .ToList() + .Where(r => r.DiscordEmoteName == reaction.Emote.ToString()) + .ToList(); + + foreach (RoleReaction roleReaction in roleReactions) + { + try + { + SocketRole role = channel.Guild.Roles.FirstOrDefault(r => r.Id == roleReaction.DiscordRoleId); + await user.RemoveRoleAsync(role); + } + catch (Exception ex) + { + Logger.Error($"{MethodBase.GetCurrentMethod().ReflectedType.FullName}: Exception [{ex}] thrown, <[{ex.Message}]>."); + } + } + } + } +} \ No newline at end of file diff --git a/ChaosBot/Migrations/20200819203430_RoleReaction.Designer.cs b/ChaosBot/Migrations/20200819203430_RoleReaction.Designer.cs new file mode 100644 index 0000000..6826a1a --- /dev/null +++ b/ChaosBot/Migrations/20200819203430_RoleReaction.Designer.cs @@ -0,0 +1,163 @@ +// +using System; +using ChaosBot.Models; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +namespace ChaosBot.Migrations +{ + [DbContext(typeof(ChaosbotContext))] + [Migration("20200819203430_RoleReaction")] + partial class RoleReaction + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "3.1.6") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + modelBuilder.Entity("ChaosBot.Models.CommandPermission", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint unsigned"); + + b.Property("Command") + .IsRequired() + .HasColumnType("varchar(128) CHARACTER SET utf8mb4") + .HasMaxLength(128); + + b.Property("DiscordGuildId") + .HasColumnType("bigint unsigned"); + + b.Property("TargetId") + .HasColumnType("bigint unsigned"); + + b.Property("TargetType") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("CommandPermissions"); + }); + + modelBuilder.Entity("ChaosBot.Models.Configuration", b => + { + b.Property("DiscordGuildId") + .HasColumnType("bigint unsigned"); + + b.Property("Key") + .HasColumnType("varchar(128) CHARACTER SET utf8mb4") + .HasMaxLength(128); + + b.Property("SerializedValue") + .IsRequired() + .HasColumnType("longtext CHARACTER SET utf8mb4"); + + b.HasKey("DiscordGuildId", "Key"); + + b.ToTable("Configuration"); + }); + + modelBuilder.Entity("ChaosBot.Models.Experience", b => + { + b.Property("DiscordGuildId") + .HasColumnType("bigint unsigned"); + + b.Property("DiscordUserId") + .HasColumnType("bigint unsigned"); + + b.Property("Amount") + .HasColumnType("bigint unsigned"); + + b.Property("LastUpdated") + .HasColumnType("datetime"); + + b.Property("Level") + .HasColumnType("bigint unsigned"); + + b.HasKey("DiscordGuildId", "DiscordUserId"); + + b.ToTable("ExperiencePoints"); + }); + + modelBuilder.Entity("ChaosBot.Models.LodestoneCharacter", b => + { + b.Property("DiscordGuildId") + .HasColumnType("bigint unsigned"); + + b.Property("LodestoneId") + .HasColumnType("bigint unsigned"); + + b.Property("Avatar") + .IsRequired() + .HasColumnType("longtext CHARACTER SET utf8mb4"); + + b.Property("DiscordUserId") + .HasColumnType("bigint unsigned"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext CHARACTER SET utf8mb4"); + + b.HasKey("DiscordGuildId", "LodestoneId"); + + b.ToTable("LodestoneCharacter"); + }); + + modelBuilder.Entity("ChaosBot.Models.LodestoneFreeCompany", b => + { + b.Property("DiscordGuildId") + .HasColumnType("bigint unsigned"); + + b.Property("LodestoneId") + .HasColumnType("bigint unsigned"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext CHARACTER SET utf8mb4"); + + b.HasKey("DiscordGuildId", "LodestoneId"); + + b.ToTable("LodestoneFreeCompany"); + }); + + modelBuilder.Entity("ChaosBot.Models.Point", b => + { + b.Property("DiscordGuildId") + .HasColumnType("bigint unsigned"); + + b.Property("DiscordUserId") + .HasColumnType("bigint unsigned"); + + b.Property("Amount") + .HasColumnType("bigint unsigned"); + + b.HasKey("DiscordGuildId", "DiscordUserId"); + + b.ToTable("Points"); + }); + + modelBuilder.Entity("ChaosBot.Models.Raffle", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint unsigned"); + + b.Property("DiscordGuildId") + .HasColumnType("bigint unsigned"); + + b.Property("DiscordUserId") + .HasColumnType("bigint unsigned"); + + b.HasKey("Id"); + + b.ToTable("Raffles"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/ChaosBot/Migrations/20200819203430_RoleReaction.cs b/ChaosBot/Migrations/20200819203430_RoleReaction.cs new file mode 100644 index 0000000..45887d3 --- /dev/null +++ b/ChaosBot/Migrations/20200819203430_RoleReaction.cs @@ -0,0 +1,33 @@ +using System; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace ChaosBot.Migrations +{ + public partial class RoleReaction : Migration + { + protected readonly string Table = "RoleReactions"; + + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: Table, + columns: table => new + { + DiscordGuildId = table.Column(nullable: false), + DiscordMessageId = table.Column(nullable: false), + DiscordRoleId = table.Column(nullable: false), + DiscordEmoteNameEncoded = table.Column(nullable: false, maxLength: 128) + }, + constraints: table => + { + table.PrimaryKey("PK_RoleReactions", x => new { x.DiscordGuildId, x.DiscordRoleId, x.DiscordMessageId, x.DiscordEmoteNameEncoded }); + }); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable(name: Table); + } + } +} diff --git a/ChaosBot/Migrations/ChaosbotContextModelSnapshot.cs b/ChaosBot/Migrations/ChaosbotContextModelSnapshot.cs index 9903ed6..449c819 100644 --- a/ChaosBot/Migrations/ChaosbotContextModelSnapshot.cs +++ b/ChaosBot/Migrations/ChaosbotContextModelSnapshot.cs @@ -1,4 +1,5 @@ // +using System; using ChaosBot.Models; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; @@ -24,7 +25,8 @@ namespace ChaosBot.Migrations b.Property("Command") .IsRequired() - .HasColumnType("longtext CHARACTER SET utf8mb4"); + .HasColumnType("varchar(128) CHARACTER SET utf8mb4") + .HasMaxLength(128); b.Property("DiscordGuildId") .HasColumnType("bigint unsigned"); @@ -58,6 +60,28 @@ namespace ChaosBot.Migrations b.ToTable("Configuration"); }); + modelBuilder.Entity("ChaosBot.Models.Experience", b => + { + b.Property("DiscordGuildId") + .HasColumnType("bigint unsigned"); + + b.Property("DiscordUserId") + .HasColumnType("bigint unsigned"); + + b.Property("Amount") + .HasColumnType("bigint unsigned"); + + b.Property("LastUpdated") + .HasColumnType("datetime"); + + b.Property("Level") + .HasColumnType("bigint unsigned"); + + b.HasKey("DiscordGuildId", "DiscordUserId"); + + b.ToTable("ExperiencePoints"); + }); + modelBuilder.Entity("ChaosBot.Models.LodestoneCharacter", b => { b.Property("DiscordGuildId") diff --git a/ChaosBot/Models/ChaosbotContext.cs b/ChaosBot/Models/ChaosbotContext.cs index 7715eb6..bb6c866 100644 --- a/ChaosBot/Models/ChaosbotContext.cs +++ b/ChaosBot/Models/ChaosbotContext.cs @@ -12,6 +12,7 @@ namespace ChaosBot.Models public DbSet CommandPermissions { get; set; } public DbSet Configuration { get; set; } public DbSet ExperiencePoints { get; set; } + public DbSet RoleReactions { get; set; } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { @@ -46,6 +47,8 @@ namespace ChaosBot.Models .HasKey(x => new {x.DiscordGuildId, x.DiscordUserId}); modelBuilder.Entity() .HasKey(x => new {x.DiscordGuildId, x.Key}); + modelBuilder.Entity() + .HasKey(x => new {x.DiscordGuildId, x.DiscordMessageId, x.DiscordRoleId, x.DiscordEmoteNameEncoded}); } } } \ No newline at end of file diff --git a/ChaosBot/Models/RoleReaction.cs b/ChaosBot/Models/RoleReaction.cs new file mode 100644 index 0000000..deaf0c7 --- /dev/null +++ b/ChaosBot/Models/RoleReaction.cs @@ -0,0 +1,37 @@ +using System; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using System.Text; + +namespace ChaosBot.Models +{ + #region Required + public class RoleReaction + { + [Required] + public ulong DiscordGuildId { get; set; } + [Required] + public ulong DiscordMessageId { get; set; } + [Required] + public ulong DiscordRoleId { get; set; } + + [NotMapped] + public string DiscordEmoteName + { + get + { + if (DiscordEmoteNameEncoded == null) return null; + return Encoding.UTF8.GetString(Convert.FromBase64String(DiscordEmoteNameEncoded)); + } + set + { + if (value != null) + DiscordEmoteNameEncoded = Convert.ToBase64String(Encoding.UTF8.GetBytes(value)); + } + } + + [Required] + public string DiscordEmoteNameEncoded { get; set; } + } + #endregion +} \ No newline at end of file