diff --git a/ChaosBot/Discord/Modules/Admin/Config.cs b/ChaosBot/Discord/Modules/Admin/Config.cs index 339ddc8..d95fd4a 100644 --- a/ChaosBot/Discord/Modules/Admin/Config.cs +++ b/ChaosBot/Discord/Modules/Admin/Config.cs @@ -6,6 +6,7 @@ using System.Text; using ChaosBot.Discord.PreConditions; using ChaosBot.Models; using ChaosBot.Repositories; +using ChaosBot.Services; using Discord; using Microsoft.EntityFrameworkCore; using Newtonsoft.Json; @@ -78,20 +79,23 @@ namespace ChaosBot.Discord.Modules.Admin { try { - if ((key != null) && (value != null)) + if ((key != null) && (value != null) ) { - using (ChaosbotContext dbContext = new ChaosbotContext()) + if(await RestrictedConfig.IsAllowed(key)) { - Configuration cnfSet = new Configuration(); - - cnfSet.Key = key; - cnfSet.DiscordGuildId = Context.Guild.Id; - cnfSet.SerializedValue = JsonConvert.SerializeObject(value); + using (ChaosbotContext dbContext = new ChaosbotContext()) + { + Configuration cnfSet = new Configuration(); - await dbContext.Configuration.Upsert(cnfSet) - .On(x => new {x.Key, x.DiscordGuildId}).RunAsync(); - - await ConfigGet(key, true); + cnfSet.Key = key; + cnfSet.DiscordGuildId = Context.Guild.Id; + cnfSet.SerializedValue = JsonConvert.SerializeObject(value); + + await dbContext.Configuration.Upsert(cnfSet) + .On(x => new {x.Key, x.DiscordGuildId}).RunAsync(); + + await ConfigGet(key, true); + } } } else @@ -111,7 +115,7 @@ namespace ChaosBot.Discord.Modules.Admin { try { - if (key != null) + if ((key != null) && (await RestrictedConfig.IsAllowed(key))) { StringBuilder sb = new StringBuilder(); EmbedBuilder embed = new EmbedBuilder(); diff --git a/ChaosBot/Discord/Modules/Admin/RankCheck.cs b/ChaosBot/Discord/Modules/Admin/RankCheck.cs new file mode 100644 index 0000000..a2643ed --- /dev/null +++ b/ChaosBot/Discord/Modules/Admin/RankCheck.cs @@ -0,0 +1,110 @@ +using System; +using Discord; +using Discord.Commands; +using System.Threading.Tasks; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Reflection; +using System.Text; +using Antlr4.Runtime.Misc; +using ChaosBot.Discord.PreConditions; +using ChaosBot.Lodestone; +using Microsoft.AspNetCore.Http; +using Newtonsoft.Json; +using NLog; + +namespace ChaosBot.Discord.Modules.Admin +{ + public class RankCheck : ModuleBase + { + private static readonly ILogger _logger = Program.Logger; + + [Command("rankCheck")] + [Alias("rc")] + [CheckCommandPerm("Admin")] + public async Task rankCheck() + { + try + { + List ranks = await GetRank(); + + var sb = new StringBuilder(); + var embed = new EmbedBuilder(); + + embed.WithColor(new Color(255, 255, 0)); + embed.Title = $"Pending Promotions"; + sb.AppendLine(); + sb.AppendLine(); + + sb.AppendLine($"**Recruits Pending Promotion to Initiate**"); + if(ranks.FindAll(x => x.IngameRole == ERole.Recruit).Any()) + { + foreach (var lsID in ranks.FindAll(x => x.IngameRole == ERole.Recruit)) + { + if ((lsID.ShouldBeRole != lsID.IngameRole) && (lsID.ShouldBeRole != null)) + sb.AppendLine(string.Format("{0} {1}", lsID.DisplayName, $"linked to <@{lsID.DiscordId}>")); + } + } + else + sb.AppendLine($"None at this time."); + + sb.AppendLine(); + sb.AppendLine($"**Initiates Pending Promotion to Member**"); + if (ranks.FindAll(x => x.IngameRole == ERole.Initiate).Any()) + { + foreach (var lsID in ranks.FindAll(x => x.IngameRole == ERole.Initiate)) + { + if ((lsID.ShouldBeRole != lsID.IngameRole) && (lsID.ShouldBeRole != null)) + sb.AppendLine(string.Format("{0} {1}", lsID.DisplayName, $"linked to <@{lsID.DiscordId}>")); + } + } + else + sb.AppendLine($"None at this time."); + + sb.AppendLine(); + sb.AppendLine($"Report Generated by {Context.User.Mention} at {DateTime.Now.ToString("dddd, dd MMMM yyyy h:mm tt")}."); + + /* + * 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}]>."); + } + } + + public async Task> GetRank() + { + string response = null; + + try + { + using (var client = new HttpClient()) + { + ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls13; + + var result = await client.GetAsync("https://www.ffxivhelix.com/rapi/clogiclodestone/v1/users"); + result.EnsureSuccessStatusCode(); + + response = await result.Content.ReadAsStringAsync(); + } + } + catch (Exception ex) + { + _logger.Error( + $"{MethodBase.GetCurrentMethod().ReflectedType.FullName}: Exception [{ex}] thrown, <[{ex.Message}]>."); + } + + return JsonConvert.DeserializeObject>(JsonConvert.SerializeObject(JsonConvert.DeserializeObject(response).Data)); + } + } +} \ No newline at end of file diff --git a/ChaosBot/Discord/Modules/User/Info.cs b/ChaosBot/Discord/Modules/User/Info.cs new file mode 100644 index 0000000..e2c6e41 --- /dev/null +++ b/ChaosBot/Discord/Modules/User/Info.cs @@ -0,0 +1,49 @@ +using System; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; +using ChaosBot.Discord.PreConditions; +using ChaosBot.Repositories; +using Discord; +using Discord.Commands; +using Microsoft.Extensions.Configuration; +using NLog; + +namespace ChaosBot.Discord.Modules.User +{ + public class Info : ModuleBase + { + private static readonly ILogger Logger = Program.Logger; + + [Command("info")] + [Alias("version")] + [CheckCommandPerm("User")] + public async Task ShowInfo() + { + try + { + var sb = new StringBuilder(); + var embed = new EmbedBuilder(); + + embed.WithColor(new Color(255, 255, 0)); + embed.Title = $"Information {Program.AppSettingsHandler.GetValue("Bot:Name")} v{Program.AppSettingsHandler.GetValue("Bot:Version")}"; + sb.AppendLine($"Prefix: {ConfigurationRepository.GetValue("Discord:Prefix", Context.Guild.Id)}"); + + /* + * 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}]>."); + } + } + } +} \ No newline at end of file diff --git a/ChaosBot/Discord/Services/CommandHandler.cs b/ChaosBot/Discord/Services/CommandHandler.cs index 106b3fd..d00c5a3 100644 --- a/ChaosBot/Discord/Services/CommandHandler.cs +++ b/ChaosBot/Discord/Services/CommandHandler.cs @@ -56,9 +56,16 @@ namespace ChaosBot.Discord.Services int argPos = 0; string prefix = ConfigurationRepository.GetValue("Discord:Prefix", context.Guild.Id, "!"); - if (!(message.HasMentionPrefix(_client.CurrentUser, ref argPos) || message.HasStringPrefix(prefix, ref argPos))) + if (!(message.HasMentionPrefix(_client.CurrentUser, ref argPos) || + message.HasStringPrefix(prefix, ref argPos))) + { + ExperienceHandler.addXP(context.Guild.Id, context.User.Id); return; + } + if(Convert.ToBoolean(ConfigurationRepository.GetValue("Experience:Commands", context.Guild.Id, "false"))) + ExperienceHandler.addXP(context.Guild.Id, context.User.Id); + await _commands.ExecuteAsync(context, argPos, _services); } catch (Exception ex) diff --git a/ChaosBot/Discord/Services/ExperienceHandler.cs b/ChaosBot/Discord/Services/ExperienceHandler.cs new file mode 100644 index 0000000..9b74856 --- /dev/null +++ b/ChaosBot/Discord/Services/ExperienceHandler.cs @@ -0,0 +1,51 @@ +using System; +using System.Linq; +using System.Reflection; +using ChaosBot.Models; +using ChaosBot.Repositories; +using Microsoft.EntityFrameworkCore; +using NLog; + +namespace ChaosBot.Discord.Services +{ + public class ExperienceHandler + { + private static readonly ILogger _logger = Program.Logger; + + public static async void addXP(ulong DiscordGuildId, ulong DiscordUserId) + { + try + { + using (ChaosbotContext dbContext = new ChaosbotContext()) + { + IQueryable ctxUser = dbContext.ExperiencePoints; + IQueryable usrXp = ctxUser + .Where(p => p.DiscordGuildId.Equals(DiscordGuildId)) + .Where(p => p.DiscordUserId.Equals(DiscordUserId)); + + Experience usrNewXp; + if (usrXp.Any()) + { + usrNewXp = usrXp.First(); + usrNewXp.Amount = usrNewXp.Amount + Convert.ToUInt64(ConfigurationRepository.GetValue("Experience:PerMsg", DiscordGuildId, "0")); + } + else + { + usrNewXp = new Experience(); + usrNewXp.Amount = Convert.ToUInt64(ConfigurationRepository.GetValue("Experience:PerMsg", DiscordGuildId, "0")); + } + usrNewXp.DiscordGuildId = DiscordGuildId; + usrNewXp.DiscordUserId = DiscordUserId; + + await dbContext.ExperiencePoints.Upsert(usrNewXp) + .On(x => new { x.DiscordGuildId, x.DiscordUserId}).RunAsync(); + } + } + catch (Exception ex) + { + _logger.Error( + $"{MethodBase.GetCurrentMethod().ReflectedType.FullName}: Exception [{ex}] thrown, <[{ex.Message}]>."); + } + } + } +} \ No newline at end of file diff --git a/ChaosBot/Lodestone/LodestoneRank.cs b/ChaosBot/Lodestone/LodestoneRank.cs new file mode 100644 index 0000000..e70983b --- /dev/null +++ b/ChaosBot/Lodestone/LodestoneRank.cs @@ -0,0 +1,155 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; + +namespace ChaosBot.Lodestone +{ + public partial class LodestoneRankApi + { + [JsonProperty("success")] + public bool Success { get; set; } + + [JsonProperty("error")] + public object Error { get; set; } + + [JsonProperty("data")] + public List Data { get; set; } + } + + public partial class LodestoneRank + { + [JsonProperty("lodestoneId")] + [JsonConverter(typeof(ParseStringConverter))] + public long LodestoneId { get; set; } + + [JsonProperty("ingameRole")] + public ERole IngameRole { get; set; } + + [JsonProperty("firstSeen")] + public DateTimeOffset FirstSeen { get; set; } + + [JsonProperty("displayName")] + public string DisplayName { get; set; } + + [JsonProperty("shouldBeRole")] + public ERole? ShouldBeRole { get; set; } + + [JsonProperty("discordId")] + public string DiscordId { get; set; } + } + + public enum ERole { Council, Initiate, Member, Mentor, Recruit }; + + public partial class LodestoneRankApi + { + public static LodestoneRankApi FromJson(string json) => JsonConvert.DeserializeObject(json, ChaosBot.Lodestone.Converter.Settings); + } + + public static class Serialize + { + public static string ToJson(this LodestoneRankApi self) => JsonConvert.SerializeObject(self, ChaosBot.Lodestone.Converter.Settings); + } + + internal static class Converter + { + public static readonly JsonSerializerSettings Settings = new JsonSerializerSettings + { + MetadataPropertyHandling = MetadataPropertyHandling.Ignore, + DateParseHandling = DateParseHandling.None, + Converters = + { + ERoleConverter.Singleton, + new IsoDateTimeConverter { DateTimeStyles = DateTimeStyles.AssumeUniversal } + }, + }; + } + + internal class ERoleConverter : JsonConverter + { + public override bool CanConvert(Type t) => t == typeof(ERole) || t == typeof(ERole?); + + public override object ReadJson(JsonReader reader, Type t, object existingValue, JsonSerializer serializer) + { + if (reader.TokenType == JsonToken.Null) return null; + var value = serializer.Deserialize(reader); + switch (value) + { + case "Council": + return ERole.Council; + case "Initiate": + return ERole.Initiate; + case "Member": + return ERole.Member; + case "Mentor": + return ERole.Mentor; + case "Recruit": + return ERole.Recruit; + } + throw new Exception("Cannot unmarshal type ERole"); + } + + public override void WriteJson(JsonWriter writer, object untypedValue, JsonSerializer serializer) + { + if (untypedValue == null) + { + serializer.Serialize(writer, null); + return; + } + var value = (ERole)untypedValue; + switch (value) + { + case ERole.Council: + serializer.Serialize(writer, "Council"); + return; + case ERole.Initiate: + serializer.Serialize(writer, "Initiate"); + return; + case ERole.Member: + serializer.Serialize(writer, "Member"); + return; + case ERole.Mentor: + serializer.Serialize(writer, "Mentor"); + return; + case ERole.Recruit: + serializer.Serialize(writer, "Recruit"); + return; + } + throw new Exception("Cannot marshal type ERole"); + } + + public static readonly ERoleConverter Singleton = new ERoleConverter(); + } + + internal class ParseStringConverter : JsonConverter + { + public override bool CanConvert(Type t) => t == typeof(long) || t == typeof(long?); + + public override object ReadJson(JsonReader reader, Type t, object existingValue, JsonSerializer serializer) + { + if (reader.TokenType == JsonToken.Null) return null; + var value = serializer.Deserialize(reader); + long l; + if (Int64.TryParse(value, out l)) + { + return l; + } + throw new Exception("Cannot unmarshal type long"); + } + + public override void WriteJson(JsonWriter writer, object untypedValue, JsonSerializer serializer) + { + if (untypedValue == null) + { + serializer.Serialize(writer, null); + return; + } + var value = (long)untypedValue; + serializer.Serialize(writer, value.ToString()); + return; + } + + public static readonly ParseStringConverter Singleton = new ParseStringConverter(); + } +} \ No newline at end of file diff --git a/ChaosBot/Models/ChaosbotContext.cs b/ChaosBot/Models/ChaosbotContext.cs index 4f74541..7715eb6 100644 --- a/ChaosBot/Models/ChaosbotContext.cs +++ b/ChaosBot/Models/ChaosbotContext.cs @@ -11,6 +11,7 @@ namespace ChaosBot.Models public DbSet Raffles { get; set; } public DbSet CommandPermissions { get; set; } public DbSet Configuration { get; set; } + public DbSet ExperiencePoints { get; set; } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { @@ -41,6 +42,8 @@ namespace ChaosBot.Models .HasKey(x => new {x.DiscordGuildId, x.LodestoneId}); modelBuilder.Entity() .HasKey(x => new {x.DiscordGuildId, x.DiscordUserId}); + modelBuilder.Entity() + .HasKey(x => new {x.DiscordGuildId, x.DiscordUserId}); modelBuilder.Entity() .HasKey(x => new {x.DiscordGuildId, x.Key}); } diff --git a/ChaosBot/Models/Experience.cs b/ChaosBot/Models/Experience.cs new file mode 100644 index 0000000..f46148e --- /dev/null +++ b/ChaosBot/Models/Experience.cs @@ -0,0 +1,15 @@ +using System.ComponentModel.DataAnnotations; + +namespace ChaosBot.Models +{ + #region Required + public class Experience + { + [Required] + public ulong DiscordUserId { get; set; } + [Required] + public ulong DiscordGuildId { get; set; } + public ulong Amount { get; set; } + } + #endregion +} \ No newline at end of file diff --git a/ChaosBot/Services/RestrictedConfig.cs b/ChaosBot/Services/RestrictedConfig.cs new file mode 100644 index 0000000..8c19c8b --- /dev/null +++ b/ChaosBot/Services/RestrictedConfig.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using ChaosBot.Models; +using ChaosBot.Repositories; +using Discord.Commands; +using Discord.WebSocket; +using NLog; + +namespace ChaosBot.Services +{ + public class RestrictedConfig + { + private static ILogger _logger = Program.Logger; + + public static async Task IsAllowed(string key) + { + List restrictedCfg = new List {"Database:Host", "Database:Port", "Database:Name", "Database:User", "Database:Pass", "Bot:Version", "NLog", "WebServer", "Discord:Token"}; + + if (restrictedCfg.Contains(key)) + return false; + + return true; + } + } +} \ No newline at end of file