diff --git a/.env-sample b/.env-sample index 25230f3..55760c1 100644 --- a/.env-sample +++ b/.env-sample @@ -13,3 +13,8 @@ AF_PublicContemptChannel= AF_HallOfShameChannel= AF_RecruitInfoChannel= AF_RecruitAskChannel= +AF_ServerManagerUrl= +AF_ServerManagerApiKey= +AF_LogWebhookId= +AF_LogWebhookToken= +AF_LogWebhookLevel= diff --git a/.github/workflows/build_bot.yml b/.github/workflows/build_bot.yml index fd4be39..ab86eff 100644 --- a/.github/workflows/build_bot.yml +++ b/.github/workflows/build_bot.yml @@ -17,7 +17,7 @@ jobs: - name: Build solution run: msbuild /p:Configuration=Release - name: Upload ArmaforcesMissionBot build artifact - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v4 with: name: ArmaforcesMissionBot path: ArmaforcesMissionBot/bin/Release \ No newline at end of file diff --git a/ArmaforcesMissionBot/ArmaforcesMissionBot.csproj b/ArmaforcesMissionBot/ArmaforcesMissionBot.csproj index 798dd30..db5347e 100644 --- a/ArmaforcesMissionBot/ArmaforcesMissionBot.csproj +++ b/ArmaforcesMissionBot/ArmaforcesMissionBot.csproj @@ -23,6 +23,10 @@ + + + + diff --git a/ArmaforcesMissionBot/Controllers/ApiController.cs b/ArmaforcesMissionBot/Controllers/ApiController.cs index 7aff02e..8db5560 100644 --- a/ArmaforcesMissionBot/Controllers/ApiController.cs +++ b/ArmaforcesMissionBot/Controllers/ApiController.cs @@ -8,6 +8,7 @@ using Discord.WebSocket; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; using Newtonsoft.Json; using Newtonsoft.Json.Linq; @@ -24,6 +25,7 @@ public class ApiController : ControllerBase private readonly BanHelper _banHelper; private readonly SignupHelper _signupHelper; private readonly MiscHelper _miscHelper; + private readonly ILogger _logger; public ApiController( MissionsArchiveData missionsArchiveData, @@ -31,7 +33,8 @@ public ApiController( DiscordSocketClient client, BanHelper banHelper, SignupHelper signupHelper, - MiscHelper miscHelper) + MiscHelper miscHelper, + ILogger logger) { _missionsArchiveData = missionsArchiveData; _signupsData = signupsData; @@ -39,6 +42,7 @@ public ApiController( _banHelper = banHelper; _signupHelper = signupHelper; _miscHelper = miscHelper; + _logger = logger; } [HttpGet("currentMission")] @@ -345,7 +349,7 @@ public void Users() [HttpPost("createMission")] public async Task CreateMissionAsync(Mission mission) { - Console.WriteLine(JsonConvert.SerializeObject(mission)); + _logger.LogTrace($"Create mission request: {JsonConvert.SerializeObject(mission)}"); mission.Editing = ArmaforcesMissionBotSharedClasses.Mission.EditEnum.New; _signupsData.Missions.Add(mission); diff --git a/ArmaforcesMissionBot/DataClasses/Config.cs b/ArmaforcesMissionBot/DataClasses/Config.cs index 1f0ac7a..c31275b 100644 --- a/ArmaforcesMissionBot/DataClasses/Config.cs +++ b/ArmaforcesMissionBot/DataClasses/Config.cs @@ -1,10 +1,9 @@ -using dotenv.net; -using Newtonsoft.Json; +#nullable enable +using dotenv.net; using System; -using System.Collections.Generic; -using System.IO; using System.Reflection; -using System.Text; +using Serilog; +using Serilog.Events; namespace ArmaforcesMissionBot.DataClasses { @@ -19,6 +18,22 @@ public class Config public ulong BotRole { get; set; } public ulong RecruiterRole { get; set; } public ulong RecruitRole { get; set; } + + /// + /// Discord webhook ID for log messages. + /// + public ulong? LogWebhookId { get; set; } + + /// + /// Discord webhook token for log messages. + /// + public string? LogWebhookToken { get; set; } + + /// + /// Minimum level that will be logged to Discord webhook. + /// + public LogEventLevel LogWebhookLevel { get; set; } = LogEventLevel.Error; + public string KickImageUrl { get; set; } public string BanImageUrl { get; set; } public string ServerManagerUrl { get; set; } @@ -28,19 +43,61 @@ public class Config public ulong PublicContemptChannel { get; set; } public ulong HallOfShameChannel { get; set; } public ulong RecruitInfoChannel { get; set; } - public ulong RecruitAskChannel { get; set; } + public ulong RecruitAskChannel { get; set; } public void Load() { DotEnv.Config(false); PropertyInfo[] properties = typeof(Config).GetProperties(BindingFlags.Public | BindingFlags.Instance); - foreach (var prop in properties) + foreach (var propertyInfo in properties) + { + if(propertyInfo.PropertyType == typeof(string)) + TrySetStringValue(propertyInfo); + else if (propertyInfo.PropertyType == typeof(ulong) || propertyInfo.PropertyType == typeof(ulong?)) + TrySetUlongValue(propertyInfo); + else if (propertyInfo.PropertyType == typeof(LogEventLevel)) + TrySetEnumValue(propertyInfo); + } + } + + private void TrySetEnumValue(PropertyInfo propertyInfo) where T : struct + { + var stringValue = GetVariable(propertyInfo.Name); + if (stringValue != null && Enum.TryParse(stringValue, out T valueToSet)) + { + propertyInfo.SetValue(this, valueToSet); + } + } + + private void TrySetStringValue(PropertyInfo propertyInfo) + { + var stringValue = GetVariable(propertyInfo.Name); + if (stringValue != null) + { + propertyInfo.SetValue(this, stringValue); + } + } + + private void TrySetUlongValue(PropertyInfo propertyInfo) + { + var stringValue = GetVariable(propertyInfo.Name); + if (stringValue != null && ulong.TryParse(stringValue, out var valueToSet)) + { + propertyInfo.SetValue(this, valueToSet); + } + } + + private static string? GetVariable(string variableName) + { + try + { + return Environment.GetEnvironmentVariable("AF_" + variableName); + } + catch (Exception exception) { - if(prop.PropertyType == typeof(string)) - prop.SetValue(this, Environment.GetEnvironmentVariable("AF_" + prop.Name)); - if (prop.PropertyType == typeof(ulong)) - prop.SetValue(this, ulong.Parse(Environment.GetEnvironmentVariable("AF_" + prop.Name))); + Log.Warning(exception, "Failed to read environment variable {Name}", variableName); + return null; } } } diff --git a/ArmaforcesMissionBot/Handlers/CommandHandler.cs b/ArmaforcesMissionBot/Handlers/CommandHandler.cs index 6750158..30bdcda 100644 --- a/ArmaforcesMissionBot/Handlers/CommandHandler.cs +++ b/ArmaforcesMissionBot/Handlers/CommandHandler.cs @@ -6,6 +6,7 @@ using System.Reflection; using System.Text; using System.Threading.Tasks; +using Serilog; namespace ArmaforcesMissionBot.Handlers { @@ -50,17 +51,24 @@ private async Task HandleCommandAsync(SocketMessage messageParam) // Keep in mind that result does not indicate a return value // rather an object stating if the command executed successfully. - var result = await _commands.ExecuteAsync( - context: context, - argPos: argPos, - services: _services); + try + { + var result = await _commands.ExecuteAsync( + context: context, + argPos: argPos, + services: _services); - // Optionally, we may inform the user if the command fails - // to be executed; however, this may not always be desired, - // as it may clog up the request queue should a user spam a - // command. - if (!result.IsSuccess) - await context.Channel.SendMessageAsync(result.ErrorReason); + // Optionally, we may inform the user if the command fails + // to be executed; however, this may not always be desired, + // as it may clog up the request queue should a user spam a + // command. + if (!result.IsSuccess) + await context.Channel.SendMessageAsync(result.ErrorReason); + } + catch (Exception exception) + { + Log.Error(exception, "An error occured while handling a command {@Message}", message); + } } } } diff --git a/ArmaforcesMissionBot/Handlers/LoadupHandler.cs b/ArmaforcesMissionBot/Handlers/LoadupHandler.cs index 7bacfc0..4505d36 100644 --- a/ArmaforcesMissionBot/Handlers/LoadupHandler.cs +++ b/ArmaforcesMissionBot/Handlers/LoadupHandler.cs @@ -6,11 +6,11 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; -using System.Text; using System.Text.RegularExpressions; using System.Threading.Tasks; using ArmaforcesMissionBot.Features.Modsets; using ArmaforcesMissionBot.Features.Modsets.Legacy; +using Microsoft.Extensions.Logging; using static ArmaforcesMissionBot.DataClasses.SignupsData; namespace ArmaforcesMissionBot.Handlers @@ -22,9 +22,12 @@ public class LoadupHandler : IInstallable private Config _config; private ModsetProvider _newModsetProvider; private LegacyModsetProvider _legacyModsetProvider; - + private ILogger _logger; + public async Task Install(IServiceProvider map) { + _logger = map.GetRequiredService>(); + _client = map.GetService(); _config = map.GetService(); _newModsetProvider = new ModsetProvider(map.GetService()); @@ -36,7 +39,7 @@ public async Task Install(IServiceProvider map) private async Task Load(SocketGuild guild) { - Console.WriteLine($"[{DateTime.Now.ToString()}] Loading up from: {guild.Name}"); + _logger.LogInformation("Loading up from server: {ServerName}", guild.Name); await LoadMissions(guild); await LoadBans(guild); @@ -50,7 +53,7 @@ private async Task LoadMissions(SocketGuild guild) var channels = guild.CategoryChannels.Single(x => x.Id == _config.SignupsCategory); - Console.WriteLine($"[{DateTime.Now.ToString()}] Loading missions"); + _logger.LogInformation("Loading missions"); foreach (var channel in channels.Channels.Where(x => x.Id != _config.SignupsArchive && x.Id != _config.CreateMissionChannel && x.Id != _config.HallOfShameChannel).Reverse()) { @@ -118,7 +121,7 @@ await messages.ForEachAsync(async x => pattern += $"{match.Groups[0]} "; team.Slots.Add(slot); - Console.WriteLine($"New slot {slot.Emoji} [{slot.Count}] {slot.Name}"); + _logger.LogDebug("New slot {Emoji} [{Count}] {Name}", slot.Emoji, slot.Count, slot.Name); } team.Name = team.Name.Replace("|", ""); @@ -133,13 +136,13 @@ await messages.ForEachAsync(async x => { var signedID = ulong.Parse(match.Groups[2].Value); mission.SignedUsers.Add(signedID); - Console.WriteLine($"{match.Groups[1].Value} : {match.Groups[2].Value} ({signedID})"); + _logger.LogTrace("{Match1} : {Match2} ({UserId})", match.Groups[1].Value, match.Groups[2].Value, signedID); team.Slots.Single(x => x.Emoji == match.Groups[1].Value).Signed.Add(signedID); } } - catch(Exception e) + catch (Exception exception) { - Console.WriteLine($"Failed loading team {team.Name} : {e.Message}"); + _logger.LogWarning(exception, "Failed loading team {Name}", team.Name); } } @@ -197,7 +200,7 @@ private async Task LoadBans(SocketGuild guild) { var signups = _services.GetService(); - Console.WriteLine($"[{DateTime.Now.ToString()}] Loading bans"); + _logger.LogInformation("Loading bans"); var banChannel = guild.Channels.Single(x => x.Id == _config.HallOfShameChannel) as SocketTextChannel; var messages = banChannel.GetMessagesAsync(); @@ -269,7 +272,7 @@ private async Task LoadBanHistory(SocketGuild guild) var channels = guild.CategoryChannels.Single(x => x.Id == _config.SignupsCategory); - Console.WriteLine($"[{DateTime.Now.ToString()}] Loading ban history"); + _logger.LogInformation("Loading ban history"); // History of bans var shameChannel = guild.Channels.Single(x => x.Id == _config.HallOfShameChannel) as SocketTextChannel; var messages = shameChannel.GetMessagesAsync(); @@ -305,7 +308,7 @@ await messages.ForEachAsync(async x => uint.Parse(match.Groups[2].Value), uint.Parse(match.Groups[3].Value))); } - Console.WriteLine($"[{DateTime.Now.ToString()}] Loaded signup ban history"); + _logger.LogInformation("Loaded signup ban history"); } } finally @@ -335,7 +338,7 @@ await messages.ForEachAsync(async x => DateTime.Parse(match.Groups[3].Value), (BanType)Enum.Parse(typeof(BanType), match.Groups[4].Value))); } - Console.WriteLine($"[{DateTime.Now.ToString()}] Loaded reaction spam ban history"); + _logger.LogInformation("Loaded reaction spam ban history"); } } finally @@ -352,7 +355,7 @@ private async Task LoadMissionsArchive(SocketGuild guild) var channels = guild.CategoryChannels.Single(x => x.Id == _config.SignupsCategory); - Console.WriteLine($"[{DateTime.Now.ToString()}] Loading mission history"); + _logger.LogInformation("Loading mission history"); archive.ArchiveMissions.Clear(); // History of missions @@ -389,7 +392,7 @@ await messages.ForEachAsync(async x => DateTimeStyles.RoundtripKind, out date)) { - Console.WriteLine($"Loading failed on mission date: {embed.Footer.Value.Text}"); + _logger.LogWarning("Failed to parse archive mission date {Date}", embed.Footer.Value.Text); continue; } @@ -436,7 +439,7 @@ await messages.ForEachAsync(async x => return x.Date.CompareTo(y.Date); }); - Console.WriteLine($"[{DateTime.Now.ToString()}] Loaded {archive.ArchiveMissions.Count} archive missions"); + _logger.LogInformation("Loaded {Count} archive missions", archive.ArchiveMissions.Count); } private string GetModsetNameFromUnknownUrl(string unknownUrl) diff --git a/ArmaforcesMissionBot/Handlers/SignupHandler.cs b/ArmaforcesMissionBot/Handlers/SignupHandler.cs index e5d7859..d355059 100644 --- a/ArmaforcesMissionBot/Handlers/SignupHandler.cs +++ b/ArmaforcesMissionBot/Handlers/SignupHandler.cs @@ -5,11 +5,10 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Text; -using System.Text.RegularExpressions; using System.Threading.Tasks; using System.Timers; using ArmaforcesMissionBot.Helpers; +using Microsoft.Extensions.Logging; namespace ArmaforcesMissionBot.Handlers { @@ -32,9 +31,11 @@ public class SignupHandler : IInstallable private IServiceProvider _services; private Config _config; private Timer _timer; + private ILogger _logger; public async Task Install(IServiceProvider map) { + _logger = map.GetRequiredService>(); _client = map.GetService(); _config = map.GetService(); _miscHelper = map.GetService(); @@ -63,7 +64,7 @@ private async Task HandleReactionAdded(Cacheable message, I var mission = signups.Missions.Single(x => x.SignupChannel == channel.Id); await HandleReactionChange(message, channel, reaction, signups); - Console.WriteLine($"[{DateTime.Now.ToString()}] {reaction.User.Value.Username} added reaction {reaction.Emote.Name}"); + _logger.LogInformation("{User} added reaction {Emoji}", reaction.User.Value.Username, reaction.Emote.Name); if (signups.SignupBans.ContainsKey(reaction.User.Value.Id) && signups.SignupBans[reaction.User.Value.Id] > mission.Date) { @@ -133,7 +134,7 @@ private async Task HandleReactionAdded(Cacheable message, I else if(signups.Missions.Any(x => x.SignupChannel == channel.Id) && reaction.UserId != _client.CurrentUser.Id) { var user = _client.GetUser(reaction.UserId); - Console.WriteLine($"Naprawiam reakcje po spamie {user.Username}"); + _logger.LogInformation("Fixing reactions after {User} spam", user.Username); var teamMsg = await channel.GetMessageAsync(message.Id) as IUserMessage; await teamMsg.RemoveReactionAsync(reaction.Emote, user); } @@ -150,7 +151,7 @@ private async Task HandleReactionRemoved(Cacheable message, var mission = signups.Missions.Single(x => x.SignupChannel == channel.Id); var user = await (channel as IGuildChannel).Guild.GetUserAsync(reaction.UserId); - Console.WriteLine($"[{DateTime.Now.ToString()}] {user.Username} removed reaction {reaction.Emote.Name}"); + _logger.LogInformation("{User} removed reaction {Emoji}", user.Username, reaction.Emote.Name); await mission.Access.WaitAsync(-1); try @@ -218,7 +219,7 @@ private async Task HandleReactionChange(Cacheable message, signups.ReactionTimes[reaction.User.Value.Id].Enqueue(DateTime.Now); - Console.WriteLine($"[{ DateTime.Now.ToString()}] { reaction.User.Value.Username} spam counter: { signups.ReactionTimes[reaction.User.Value.Id].Count}"); + _logger.LogDebug("{User} spam counter increased: {Count}", reaction.User.Value.Username, signups.ReactionTimes[reaction.User.Value.Id].Count); if (signups.ReactionTimes[reaction.User.Value.Id].Count >= 10 && !signups.SpamBans.ContainsKey(reaction.User.Value.Id)) { diff --git a/ArmaforcesMissionBot/Helpers/BanHelper.cs b/ArmaforcesMissionBot/Helpers/BanHelper.cs index 3152c44..84ddcd5 100644 --- a/ArmaforcesMissionBot/Helpers/BanHelper.cs +++ b/ArmaforcesMissionBot/Helpers/BanHelper.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using Serilog; namespace ArmaforcesMissionBot.Helpers { @@ -54,9 +55,9 @@ public static async Task MakeBanMessage(IServiceProvider map, SocketGuild return sentMessage.Id; } } - catch(Exception e) + catch (Exception e) { - Console.WriteLine($"[{DateTime.Now.ToString()}] MakeBanMessageFailed: {e.Message}"); + Log.Error(e, "Ban message creation failed"); } return banMessageId; @@ -97,9 +98,9 @@ public static async Task MakeBanHistoryMessage(IServiceProvider map, SocketGuild signups.SignupBansHistoryMessage = sentMessage.Id; } } - catch(Exception e) + catch (Exception e) { - Console.WriteLine($"[{DateTime.Now.ToString()}] MakeBanHistoryMessageFailed: {e.Message}"); + Log.Error(e, "Creating ban history message failed"); } } @@ -278,9 +279,9 @@ public static async Task BanUserSpam(IServiceProvider map, IUser user) PermValue.Deny, PermValue.Deny)); } - catch(Exception e) + catch (Exception e) { - Console.WriteLine($"Woops, banning user from channel failed : {e.Message}"); + Log.Error(e, "Banning {User} from {Channel} failed", user.Username, missionChannel.Name); } } } diff --git a/ArmaforcesMissionBot/Helpers/LoggingHelper.cs b/ArmaforcesMissionBot/Helpers/LoggingHelper.cs new file mode 100644 index 0000000..c52ec14 --- /dev/null +++ b/ArmaforcesMissionBot/Helpers/LoggingHelper.cs @@ -0,0 +1,40 @@ +using ArmaforcesMissionBot.DataClasses; +using Serilog; +using Serilog.Core; +using Serilog.Sinks.Discord; + +namespace ArmaforcesMissionBot.Helpers +{ + internal class LoggingHelper + { + private const string OutputTemplate = "{Timestamp:yyyy-MM-ddTHH:mm:ss.fffzzz} [{Level:u3}] {SourceContext}: {Message:lj}{NewLine}{Exception}"; + + public static readonly LoggingLevelSwitch LoggingLevelSwitch = new LoggingLevelSwitch(); + + public static ILogger CreateSerilogLogger() + { + return CreateLoggerConfiguration() + .CreateLogger(); + } + + private static LoggerConfiguration CreateLoggerConfiguration() + { + var config = new Config(); + config.Load(); + + var loggerConfiguration = new LoggerConfiguration() + .Enrich.FromLogContext() + .WriteTo.Console(outputTemplate: OutputTemplate); + + if (config.LogWebhookId.HasValue && config.LogWebhookToken != null) + { + loggerConfiguration.WriteTo.Discord( + webhookId: config.LogWebhookId.Value, + webhookToken: config.LogWebhookToken, + restrictedToMinimumLevel: config.LogWebhookLevel); + } + + return loggerConfiguration; + } + } +} \ No newline at end of file diff --git a/ArmaforcesMissionBot/Helpers/MiscHelper.cs b/ArmaforcesMissionBot/Helpers/MiscHelper.cs index bbb2dfd..df7435f 100644 --- a/ArmaforcesMissionBot/Helpers/MiscHelper.cs +++ b/ArmaforcesMissionBot/Helpers/MiscHelper.cs @@ -1,166 +1,167 @@ -using ArmaforcesMissionBot.DataClasses; -using Discord; -using Discord.Commands; -using Discord.WebSocket; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text.RegularExpressions; -using System.Web; -using static ArmaforcesMissionBot.DataClasses.OpenedDialogs; - -namespace ArmaforcesMissionBot.Helpers -{ - public class MiscHelper - { - private readonly DiscordSocketClient _client; - private readonly Config _config; - - public MiscHelper(DiscordSocketClient client, Config config) - { - _client = client; - _config = config; - } - - public List BuildTeamSlots(ArmaforcesMissionBotSharedClasses.Mission.Team team) - { - List results = new List(); - results.Add(""); - foreach (var slot in team.Slots) - { - for (var i = 0; i < slot.Count; i++) - { - string description = $"{HttpUtility.HtmlDecode(slot.Emoji)}"; - if (slot.Name != "" && i == 0) - description += $"({slot.Name})"; - description += "-"; - if (i < slot.Signed.Count) - { - var user = _client.GetGuild(_config.AFGuild).GetUser(slot.Signed.ElementAt(i)); - if(user != null) - description += user.Mention; - } - description += "\n"; - - if(results.Last().Length + description.Length > 1024) - results.Add(""); - - results[results.Count-1] += description; - } - } - - /*foreach (var prebeton in team.Signed) - { - Console.WriteLine(prebeton.Value + " " + prebeton.Key); - Console.WriteLine(HttpUtility.HtmlDecode(prebeton.Value) + " " + HttpUtility.HtmlDecode(prebeton.Key)); - var regex = new Regex(Regex.Escape(HttpUtility.HtmlDecode(prebeton.Value)) + @"-(?:$|\n)"); - description = regex.Replace(description, HttpUtility.HtmlDecode(prebeton.Value) + "-" + HttpUtility.HtmlDecode(prebeton.Key) + "\n", 1); - }*/ - - return results; - } - - public void BuildTeamsEmbed(List teams, EmbedBuilder builder, bool removeSlotNamesFromName = false) - { - foreach (var team in teams) - { - var slots = BuildTeamSlots(team); - - var teamName = team.Name; - if (removeSlotNamesFromName) - { - foreach (var slot in team.Slots) - { - if (teamName.Contains(slot.Emoji)) - teamName = teamName.Remove(teamName.IndexOf(slot.Emoji)); - } - } - - if(slots.Count == 1) - builder.AddField(teamName, slots[0], true); - else if(slots.Count > 1) - { - foreach(var part in slots) - { - builder.AddField(teamName, part, true); - } - } - } - } - - public static string BuildEditTeamsPanel(List teams, int highlightIndex) - { - string result = ""; - - int index = 0; - foreach (var team in teams) - { - if (highlightIndex == index) - result += "**"; - result += $"{team.Name}"; - if (highlightIndex == index) - result += "**"; - result += "\n"; - index++; - } - - return result; - } - - public static int CountFreeSlots(ArmaforcesMissionBotSharedClasses.Mission mission) - { - return CountAllSlots(mission) - mission.SignedUsers.Count; - } - - public static int CountAllSlots(ArmaforcesMissionBotSharedClasses.Mission mission) - { - int slots = 0; - foreach (var team in mission.Teams) - { - foreach (var slot in team.Slots) - { - slots += slot.Count; - } - } - - return slots; - } - - public async void CreateConfirmationDialog( - OpenedDialogs openedDialogs, - SocketCommandContext context, - Embed description, - Action confirmAction, - Action cancelAction) - { - var dialog = new Dialog(); - - var message = await context.Channel.SendMessageAsync("Zgadza sie?", embed: description); - - dialog.DialogID = message.Id; - dialog.DialogOwner = context.User.Id; - dialog.Buttons["✔️"] = confirmAction; - dialog.Buttons["❌"] = cancelAction; - - var reactions = new List(); - foreach(var key in dialog.Buttons.Keys) - { - reactions.Add(new Emoji(key)); - } +using ArmaforcesMissionBot.DataClasses; +using Discord; +using Discord.Commands; +using Discord.WebSocket; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using System.Web; +using static ArmaforcesMissionBot.DataClasses.OpenedDialogs; + +namespace ArmaforcesMissionBot.Helpers +{ + public class MiscHelper + { + private readonly DiscordSocketClient _client; + private readonly Config _config; + + public MiscHelper(DiscordSocketClient client, Config config) + { + _client = client; + _config = config; + } + + public List BuildTeamSlots(ArmaforcesMissionBotSharedClasses.Mission.Team team) + { + List results = new List(); + results.Add(""); + foreach (var slot in team.Slots) + { + for (var i = 0; i < slot.Count; i++) + { + string description = $"{HttpUtility.HtmlDecode(slot.Emoji)}"; + if (slot.Name != "" && i == 0) + description += $"({slot.Name})"; + description += "-"; + if (i < slot.Signed.Count) + { + var user = _client.GetGuild(_config.AFGuild).GetUser(slot.Signed.ElementAt(i)); + if(user != null) + description += user.Mention; + } + description += "\n"; + + if(results.Last().Length + description.Length > 1024) + results.Add(""); + + results[results.Count-1] += description; + } + } + + /*foreach (var prebeton in team.Signed) + { + Log.Verbose(prebeton.Value + " " + prebeton.Key); + Log.Verbose(HttpUtility.HtmlDecode(prebeton.Value) + " " + HttpUtility.HtmlDecode(prebeton.Key)); + var regex = new Regex(Regex.Escape(HttpUtility.HtmlDecode(prebeton.Value)) + @"-(?:$|\n)"); + description = regex.Replace(description, HttpUtility.HtmlDecode(prebeton.Value) + "-" + HttpUtility.HtmlDecode(prebeton.Key) + "\n", 1); + }*/ + + return results; + } + + public void BuildTeamsEmbed(List teams, EmbedBuilder builder, bool removeSlotNamesFromName = false) + { + foreach (var team in teams) + { + var slots = BuildTeamSlots(team); + + var teamName = team.Name; + if (removeSlotNamesFromName) + { + foreach (var slot in team.Slots) + { + if (teamName.Contains(slot.Emoji)) + teamName = teamName.Remove(teamName.IndexOf(slot.Emoji)); + } + } + + if(slots.Count == 1) + builder.AddField(teamName, slots[0], true); + else if(slots.Count > 1) + { + foreach(var part in slots) + { + builder.AddField(teamName, part, true); + } + } + } + } + + public static string BuildEditTeamsPanel(List teams, int highlightIndex) + { + string result = ""; + + int index = 0; + foreach (var team in teams) + { + if (highlightIndex == index) + result += "**"; + result += $"{team.Name}"; + if (highlightIndex == index) + result += "**"; + result += "\n"; + index++; + } + + return result; + } + + public static int CountFreeSlots(ArmaforcesMissionBotSharedClasses.Mission mission) + { + return CountAllSlots(mission) - mission.SignedUsers.Count; + } + + public static int CountAllSlots(ArmaforcesMissionBotSharedClasses.Mission mission) + { + int slots = 0; + foreach (var team in mission.Teams) + { + foreach (var slot in team.Slots) + { + slots += slot.Count; + } + } + + return slots; + } + + public async Task CreateConfirmationDialog( + OpenedDialogs openedDialogs, + SocketCommandContext context, + Embed description, + Action confirmAction, + Action cancelAction) + { + var dialog = new Dialog(); + + var message = await context.Channel.SendMessageAsync("Zgadza sie?", embed: description); + + dialog.DialogID = message.Id; + dialog.DialogOwner = context.User.Id; + dialog.Buttons["✔️"] = confirmAction; + dialog.Buttons["❌"] = cancelAction; + + var reactions = new List(); + foreach(var key in dialog.Buttons.Keys) + { + reactions.Add(new Emoji(key)); + } await message.AddReactionsAsync(reactions.ToArray()); - - openedDialogs.Dialogs.Add(dialog); - } - - public static MatchCollection GetSlotMatchesFromText(string text) - { - string unicodeEmoji = @"(?:\u00a9|\u00ae|[\u2000-\u3300]|\ud83c[\ud000-\udfff]|\ud83d[\ud000-\udfff]|\ud83e[\ud000-\udfff])(?:\ufe0f)?"; - string emote = $@"((?:)?)|{unicodeEmoji})"; - string slotCount = @"(\[[0-9]+\])"; - string slotName = @"([^\|]*?)?"; - string rolePattern = $@"[ ]*{emote}[ ]*{slotCount}[ ]*{slotName}[ ]*(?:\|)?"; - - return Regex.Matches(text, rolePattern, RegexOptions.IgnoreCase | RegexOptions.RightToLeft); - } - } -} + + openedDialogs.Dialogs.Add(dialog); + } + + public static MatchCollection GetSlotMatchesFromText(string text) + { + string unicodeEmoji = @"(?:\u00a9|\u00ae|[\u2000-\u3300]|\ud83c[\ud000-\udfff]|\ud83d[\ud000-\udfff]|\ud83e[\ud000-\udfff])(?:\ufe0f)?"; + string emote = $@"((?:)?)|{unicodeEmoji})"; + string slotCount = @"(\[[0-9]+\])"; + string slotName = @"([^\|]*?)?"; + string rolePattern = $@"[ ]*{emote}[ ]*{slotCount}[ ]*{slotName}[ ]*(?:\|)?"; + + return Regex.Matches(text, rolePattern, RegexOptions.IgnoreCase | RegexOptions.RightToLeft); + } + } +} diff --git a/ArmaforcesMissionBot/Helpers/SignupHelper.cs b/ArmaforcesMissionBot/Helpers/SignupHelper.cs index feb7341..ef197e3 100644 --- a/ArmaforcesMissionBot/Helpers/SignupHelper.cs +++ b/ArmaforcesMissionBot/Helpers/SignupHelper.cs @@ -8,6 +8,7 @@ using System.Linq; using System.Threading.Tasks; using System.Web; +using Serilog; namespace ArmaforcesMissionBot.Helpers { @@ -144,7 +145,7 @@ public async Task CreateChannelForMission(SocketGuild guild, Mi } catch (Exception e) { - Console.WriteLine(e.Message); + Log.Error(e, "Failed to what the hell is happening here? It's not working anyway"); } finally { @@ -153,9 +154,9 @@ public async Task CreateChannelForMission(SocketGuild guild, Mi await signupChannel.AddPermissionOverwriteAsync(everyone, everyoneStartPermissions); } - catch(Exception e) + catch (Exception e) { - Console.WriteLine(e.Message); + Log.Error(e, "Failed to add bot permissions"); } diff --git a/ArmaforcesMissionBot/Helpers/StatsHelper.cs b/ArmaforcesMissionBot/Helpers/StatsHelper.cs index 388f135..bcc0318 100644 --- a/ArmaforcesMissionBot/Helpers/StatsHelper.cs +++ b/ArmaforcesMissionBot/Helpers/StatsHelper.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using Serilog; using static ArmaforcesMissionBot.Modules.Stats; namespace ArmaforcesMissionBot.Helpers @@ -133,7 +134,7 @@ public static async Task GetCache(SocketGuild guild, IUserMessage st else continue; tasks.Add(textChannel, task); - Console.WriteLine($"[{DateTime.Now.ToString()}] Added task {textChannel.Name} ({tasks.Count})"); + Log.Debug("New task for stats of {Channel}. Total tasks {Count}", textChannel.Name, tasks.Count); System.Threading.Thread.Sleep(200); } } @@ -208,7 +209,7 @@ public static async Task GetCache(SocketGuild guild, IUserMessage st { task.Value._lastMessagesLoaded = task.Value._messagesLoaded; - Console.WriteLine($"[{DateTime.Now.ToString()}] Updated task {task.Key.Name} ({task.Value._messagesLoaded}) - [{task.Value._oldestLoadedMessage.Timestamp.ToString()}], tasks: {tasks.Count}"); + Log.Debug($"[{DateTime.Now.ToString()}] Updated task {task.Key.Name} ({task.Value._messagesLoaded}) - [{task.Value._oldestLoadedMessage.Timestamp.ToString()}], tasks: {tasks.Count}"); if (searchDir == Direction.Before) task.Value._task = task.Key.GetMessagesAsync(task.Value._oldestLoadedMessage, searchDir, limit: _messagesInBatch).FlattenAsync(); else @@ -225,7 +226,7 @@ public static async Task GetCache(SocketGuild guild, IUserMessage st _cache._emotesUsage[emote.Key] += emote.Value; } tasks.Remove(task.Key); - Console.WriteLine($"[{DateTime.Now.ToString()}] Updated task {task.Key.Name} ({task.Value._messagesLoaded}), tasks: {tasks.Count}"); + Log.Debug($"[{DateTime.Now.ToString()}] Updated task {task.Key.Name} ({task.Value._messagesLoaded}), tasks: {tasks.Count}"); } } diff --git a/ArmaforcesMissionBot/Modules/Bans.cs b/ArmaforcesMissionBot/Modules/Bans.cs index f5b71fe..4e68b05 100644 --- a/ArmaforcesMissionBot/Modules/Bans.cs +++ b/ArmaforcesMissionBot/Modules/Bans.cs @@ -1,15 +1,13 @@ -using ArmaforcesMissionBot.Attributes; -using ArmaforcesMissionBot.DataClasses; +using ArmaforcesMissionBot.DataClasses; using Discord; using Discord.Commands; using Discord.WebSocket; using Microsoft.Extensions.DependencyInjection; using System; -using System.Collections.Generic; using System.Linq; -using System.Text; using System.Threading.Tasks; using ArmaforcesMissionBot.Helpers; +using Microsoft.Extensions.Logging; namespace ArmaforcesMissionBot.Modules { @@ -22,6 +20,7 @@ public class Bans : ModuleBase public CommandService _commands { get; set; } public MiscHelper MiscHelper { get; set; } + public ILogger _logger { get; set; } public Bans() { @@ -179,7 +178,7 @@ public async Task Unsign(ulong userID, IMessageChannel channel) { var mission = signups.Missions.Single(x => x.SignupChannel == channel.Id); - Console.WriteLine($"[{DateTime.Now.ToString()}] {userID} removed from mission {channel.Name} by {Context.User.Username}"); + _logger.LogInformation("{UserId} removed from mission {MissionName} by {User}", userID, channel.Name, Context.User.Username); await mission.Access.WaitAsync(-1); try diff --git a/ArmaforcesMissionBot/Modules/LoggingModule.cs b/ArmaforcesMissionBot/Modules/LoggingModule.cs new file mode 100644 index 0000000..da7d5f7 --- /dev/null +++ b/ArmaforcesMissionBot/Modules/LoggingModule.cs @@ -0,0 +1,38 @@ +using System.Threading.Tasks; +using ArmaforcesMissionBot.Helpers; +using Discord; +using Discord.Commands; +using Microsoft.Extensions.Logging; +using Serilog.Events; + +namespace ArmaforcesMissionBot.Modules +{ + public class LoggingModule : ModuleBase + { + private readonly ILogger _logger; + + public LoggingModule(ILogger logger) + { + _logger = logger; + } + + [Command("change-log-level")] + [Summary("Zmienia poziom logowania do czasu restartu bota")] + [RequireUserPermission(GuildPermission.ManageRoles)] + public async Task ChangeLogLevel(LogLevel newLogLevel) + { + var oldLogLevel = LoggingHelper.LoggingLevelSwitch.MinimumLevel; + if (oldLogLevel == (LogEventLevel) newLogLevel) + { + await ReplyAsync("New log level is the same as the old one"); + } + else + { + _logger.LogInformation("Changed log level from {OldLogLevel} to {NewLogLevel}", oldLogLevel, newLogLevel.ToString()); + await ReplyAsync($"Changed log level from {oldLogLevel} to {newLogLevel}"); + } + + LoggingHelper.LoggingLevelSwitch.MinimumLevel = (LogEventLevel)newLogLevel; + } + } +} \ No newline at end of file diff --git a/ArmaforcesMissionBot/Modules/Misc.cs b/ArmaforcesMissionBot/Modules/Misc.cs index acffddf..b7a48ec 100644 --- a/ArmaforcesMissionBot/Modules/Misc.cs +++ b/ArmaforcesMissionBot/Modules/Misc.cs @@ -1,9 +1,7 @@ using ArmaforcesMissionBot.Handlers; using Discord; using Discord.Commands; -using Discord.WebSocket; using System; -using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading.Tasks; @@ -22,6 +20,17 @@ public Misc() //_map = map; } + [Command("restart")] + [Summary("Throws terminating exception")] + [RequireUserPermission(GuildPermission.ManageRoles)] + public Task Restart() + { + ReplyAsync("Boderator is restarting"); + CrashActual(); + + return Task.CompletedTask; + } + [Command("snipe")] [Summary("Wyświetla ostatnio usunięte wiadomości z tego kanału.")] [RequireRank(RanksEnum.Recruiter)] @@ -97,5 +106,11 @@ public async Task Help() await ReplyAsync(embed: embed.Build()); } + + private static async void CrashActual() + { + await Task.Delay(50); + throw new ApplicationException("Boderator wypierdala się według życzenia"); + } } } diff --git a/ArmaforcesMissionBot/Modules/Ranks.cs b/ArmaforcesMissionBot/Modules/Ranks.cs index d1d3fbb..7416dca 100644 --- a/ArmaforcesMissionBot/Modules/Ranks.cs +++ b/ArmaforcesMissionBot/Modules/Ranks.cs @@ -4,10 +4,10 @@ using Discord.Commands; using Discord.WebSocket; using System; -using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; using System.Threading.Tasks; +using Microsoft.Extensions.Logging; namespace ArmaforcesMissionBot.Modules { @@ -17,13 +17,14 @@ public class Ranks : ModuleBase public IServiceProvider _map { get; set; } public DiscordSocketClient _client { get; set; } public Config _config { get; set; } + public ILogger _logger { get; set; } [Command("rekrutuj")] [Summary("Przydziela rangę rekrut.")] [RequireRank(RanksEnum.Recruiter)] public async Task Recruit(IGuildUser user) { - Console.WriteLine($"[{DateTime.Now.ToString()}] {Context.User.Username} called recruit command"); + _logger.LogInformation("{Recruiter} recruiting user {UserName}", Context.User.Username, user.Username); var signupRole = Context.Guild.GetRole(_config.SignupRole); if (user.RoleIds.Contains(_config.RecruitRole)) await ReplyAsync($"Przecież {user.Mention} już został zrekrutowany."); @@ -52,7 +53,7 @@ public async Task Recruit(IGuildUser user) [RequireRank(RanksEnum.Recruiter)] public async Task Kick(IGuildUser user) { - Console.WriteLine($"[{DateTime.Now.ToString()}] {Context.User.Username} called kick command"); + _logger.LogInformation("{Kicker} kicking user {UserName}", Context.User.Username, user.Username); var signupRole = Context.Guild.GetRole(_config.SignupRole); var userRoleIds = user.RoleIds; if (userRoleIds.All(x => x == _config.RecruitRole || x == _config.AFGuild)) diff --git a/ArmaforcesMissionBot/Modules/Signups.cs b/ArmaforcesMissionBot/Modules/Signups.cs index a90a216..7f544e6 100644 --- a/ArmaforcesMissionBot/Modules/Signups.cs +++ b/ArmaforcesMissionBot/Modules/Signups.cs @@ -7,25 +7,17 @@ using Newtonsoft.Json; using System; using System.Linq; -using System.Net; using System.Net.Http; using System.Text.RegularExpressions; using System.Threading.Tasks; -using ArmaforcesMissionBot.Attributes; -using ArmaforcesMissionBot.DataClasses; using ArmaforcesMissionBot.Exceptions; using ArmaforcesMissionBot.Extensions; using ArmaforcesMissionBot.Features; using ArmaforcesMissionBot.Features.Modsets; -using ArmaforcesMissionBot.Features.Modsets.Constants; using ArmaforcesMissionBot.Features.Signups.Importer; using ArmaforcesMissionBot.Helpers; using ArmaforcesMissionBotSharedClasses; using CSharpFunctionalExtensions; -using Discord; -using Discord.Commands; -using Discord.WebSocket; -using Newtonsoft.Json; namespace ArmaforcesMissionBot.Modules { @@ -273,7 +265,7 @@ public async Task AddTeam([Remainder]string teamText) .WithDescription(_miscHelper.BuildTeamSlots(team)[0]) .WithFooter(team.Pattern); - _miscHelper.CreateConfirmationDialog( + await _miscHelper.CreateConfirmationDialog( _dialogs, Context, embed.Build(), @@ -495,7 +487,7 @@ public async Task EndSignups() _miscHelper.BuildTeamsEmbed(mission.Teams, embed); - _miscHelper.CreateConfirmationDialog( + await _miscHelper.CreateConfirmationDialog( _dialogs, Context, embed.Build(), diff --git a/ArmaforcesMissionBot/Modules/Stats.cs b/ArmaforcesMissionBot/Modules/Stats.cs index f3049a9..a23578f 100644 --- a/ArmaforcesMissionBot/Modules/Stats.cs +++ b/ArmaforcesMissionBot/Modules/Stats.cs @@ -5,7 +5,6 @@ using Discord.Commands; using Discord.WebSocket; using System; -using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; diff --git a/ArmaforcesMissionBot/Program.cs b/ArmaforcesMissionBot/Program.cs index 9d9d1fe..f62dff7 100644 --- a/ArmaforcesMissionBot/Program.cs +++ b/ArmaforcesMissionBot/Program.cs @@ -1,14 +1,31 @@ -using Microsoft.AspNetCore.Hosting; +using System; +using Microsoft.AspNetCore.Hosting; using System.Threading.Tasks; +using ArmaforcesMissionBot.Helpers; using Microsoft.Extensions.Hosting; +using Serilog; namespace ArmaforcesMissionBot { public class Program { + public static async Task Main(string[] args) { - await CreateHostBuilder(args).Build().RunAsync(); + Log.Logger = LoggingHelper.CreateSerilogLogger(); + + try + { + await CreateHostBuilder(args).Build().RunAsync(); + } + catch (Exception exception) + { + Log.Fatal(exception, "Boderator terminated unexpectedly"); + } + finally + { + Log.CloseAndFlush(); + } } private static IHostBuilder CreateHostBuilder(string[] args) diff --git a/ArmaforcesMissionBot/Startup.cs b/ArmaforcesMissionBot/Startup.cs index 121cb18..1392967 100644 --- a/ArmaforcesMissionBot/Startup.cs +++ b/ArmaforcesMissionBot/Startup.cs @@ -1,19 +1,8 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Net; -using System.Net.Http; -using System.Text; -using System.Threading.Tasks; using System.Globalization; using ArmaforcesMissionBot.DependencyInjection; -using ArmaforcesMissionBot.Services; -using Discord; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.HttpsPolicy; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.Configuration; @@ -21,8 +10,7 @@ using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Logging; using Microsoft.AspNetCore.Localization; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; +using Serilog; namespace ArmaforcesMissionBot { @@ -49,6 +37,7 @@ public void ConfigureServices(IServiceCollection services) services.AddRouting(); services.AddMvc(option => option.EnableEndpointRouting = false); + services.AddSerilog(); services.AddLogging( builder => {