diff --git a/locale/Messages.resx b/locale/Messages.resx index 1ebd008..0bcd089 100644 --- a/locale/Messages.resx +++ b/locale/Messages.resx @@ -584,11 +584,14 @@ Report an issue - + See you soon, {0}! Leave message + + Kicked + diff --git a/locale/Messages.ru.resx b/locale/Messages.ru.resx index 333fae0..35ea613 100644 --- a/locale/Messages.ru.resx +++ b/locale/Messages.ru.resx @@ -591,4 +591,7 @@ Сообщение о выходе + + Выгнан + diff --git a/locale/Messages.tt-ru.resx b/locale/Messages.tt-ru.resx index b0036f5..48196c6 100644 --- a/locale/Messages.tt-ru.resx +++ b/locale/Messages.tt-ru.resx @@ -502,7 +502,7 @@ приколы полученные по заслугам - забанен + пермабан вышел из сервера @@ -591,4 +591,7 @@ до свидания (типо настройка) + + кикнут + diff --git a/src/Commands/KickCommandGroup.cs b/src/Commands/KickCommandGroup.cs index ee94b93..a278fb4 100644 --- a/src/Commands/KickCommandGroup.cs +++ b/src/Commands/KickCommandGroup.cs @@ -151,7 +151,9 @@ public class KickCommandGroup : CommandGroup return Result.FromError(kickResult.Error); } - data.GetOrCreateMemberData(target.ID).Roles.Clear(); + var memberData = data.GetOrCreateMemberData(target.ID); + memberData.Roles.Clear(); + memberData.Kicked = true; var title = string.Format(Messages.UserKicked, target.GetTag()); var description = MarkdownExtensions.BulletPoint(string.Format(Messages.DescriptionActionReason, reason)); diff --git a/src/Commands/MuteCommandGroup.cs b/src/Commands/MuteCommandGroup.cs index 522c7f7..c7b21f6 100644 --- a/src/Commands/MuteCommandGroup.cs +++ b/src/Commands/MuteCommandGroup.cs @@ -300,9 +300,9 @@ public class MuteCommandGroup : CommandGroup } var memberData = data.GetOrCreateMemberData(target.ID); - var isMuted = memberData.MutedUntil is not null || communicationDisabledUntil is not null; + var wasMuted = memberData.MutedUntil is not null || communicationDisabledUntil is not null; - if (!isMuted) + if (!wasMuted) { var failedEmbed = new EmbedBuilder().WithSmallTitle(Messages.UserNotMuted, bot) .WithColour(ColorsList.Red).Build(); diff --git a/src/Commands/ToolsCommandGroup.cs b/src/Commands/ToolsCommandGroup.cs index f04ddf6..1dbf72d 100644 --- a/src/Commands/ToolsCommandGroup.cs +++ b/src/Commands/ToolsCommandGroup.cs @@ -122,32 +122,21 @@ public class ToolsCommandGroup : CommandGroup embedColor = AppendGuildInformation(embedColor, guildMember, builder); } - var isMuted = (memberData.MutedUntil is not null && DateTimeOffset.UtcNow <= memberData.MutedUntil) || - communicationDisabledUntil is not null; + var wasMuted = (memberData.MutedUntil is not null && DateTimeOffset.UtcNow <= memberData.MutedUntil) || + communicationDisabledUntil is not null; + var wasBanned = memberData.BannedUntil is not null; + var wasKicked = memberData.Kicked; - var existingBanResult = await _guildApi.GetGuildBanAsync(guildId, target.ID, ct); - - if (isMuted || existingBanResult.IsDefined()) + if (wasMuted || wasBanned || wasKicked) { builder.Append("### ") .AppendLine(Markdown.Bold(Messages.UserInfoPunishments)); + + embedColor = AppendPunishmentsInformation(wasMuted, wasKicked, wasBanned, memberData, + builder, embedColor, communicationDisabledUntil); } - if (isMuted) - { - AppendMuteInformation(memberData, communicationDisabledUntil, builder); - - embedColor = ColorsList.Red; - } - - if (existingBanResult.IsDefined()) - { - AppendBanInformation(memberData, builder); - - embedColor = ColorsList.Black; - } - - if (!guildMemberResult.IsSuccess && !existingBanResult.IsDefined()) + if (!guildMemberResult.IsSuccess && !wasBanned) { builder.Append("### ") .AppendLine(Markdown.Bold(Messages.UserInfoNotOnGuild)); @@ -166,6 +155,29 @@ public class ToolsCommandGroup : CommandGroup return await _feedback.SendContextualEmbedResultAsync(embed, ct: ct); } + private static Color AppendPunishmentsInformation(bool wasMuted, bool wasKicked, bool wasBanned, + MemberData memberData, StringBuilder builder, Color embedColor, DateTimeOffset? communicationDisabledUntil) + { + if (wasMuted) + { + AppendMuteInformation(memberData, communicationDisabledUntil, builder); + embedColor = ColorsList.Red; + } + + if (wasKicked) + { + builder.AppendBulletPointLine(Messages.UserInfoKicked); + } + + if (wasBanned) + { + AppendBanInformation(memberData, builder); + embedColor = ColorsList.Black; + } + + return embedColor; + } + private static Color AppendGuildInformation(Color color, IGuildMember guildMember, StringBuilder builder) { if (guildMember.Nickname.IsDefined(out var nickname)) diff --git a/src/Data/MemberData.cs b/src/Data/MemberData.cs index 0b0cfb2..8e23e54 100644 --- a/src/Data/MemberData.cs +++ b/src/Data/MemberData.cs @@ -18,6 +18,7 @@ public sealed class MemberData public ulong Id { get; } public DateTimeOffset? BannedUntil { get; set; } public DateTimeOffset? MutedUntil { get; set; } + public bool Kicked { get; set; } public List Roles { get; set; } = []; public List Reminders { get; } = []; } diff --git a/src/Messages.Designer.cs b/src/Messages.Designer.cs index e9d4e74..d780d0a 100644 --- a/src/Messages.Designer.cs +++ b/src/Messages.Designer.cs @@ -1046,6 +1046,14 @@ namespace Octobot { internal static string SettingsLeaveMessage { get { return ResourceManager.GetString("SettingsLeaveMessage", resourceCulture); + } + } + + internal static string UserInfoKicked + { + get + { + return ResourceManager.GetString("UserInfoKicked", resourceCulture); } } } diff --git a/src/Octobot.cs b/src/Octobot.cs index 2648338..063bd14 100644 --- a/src/Octobot.cs +++ b/src/Octobot.cs @@ -2,11 +2,10 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; -using Octobot.Commands; using Octobot.Commands.Events; using Octobot.Services; +using Octobot.Services.Profiler; using Octobot.Services.Update; -using Remora.Commands.Extensions; using Remora.Discord.API.Abstractions.Gateway.Commands; using Remora.Discord.API.Abstractions.Objects; using Remora.Discord.API.Objects; @@ -14,8 +13,8 @@ using Remora.Discord.Caching.Extensions; using Remora.Discord.Caching.Services; using Remora.Discord.Commands.Extensions; using Remora.Discord.Commands.Services; +using Remora.Discord.Extensions.Extensions; using Remora.Discord.Gateway; -using Remora.Discord.Gateway.Extensions; using Remora.Discord.Hosting.Extensions; using Remora.Rest.Core; using Serilog.Extensions.Logging; @@ -82,34 +81,20 @@ public sealed class Octobot // Init .AddDiscordCaching() .AddDiscordCommands(true, false) + .AddRespondersFromAssembly(typeof(Octobot).Assembly) + .AddCommandGroupsFromAssembly(typeof(Octobot).Assembly) // Slash command event handlers .AddPreparationErrorEvent() .AddPostExecutionEvent() // Services + .AddTransient() + .AddSingleton() .AddSingleton() .AddSingleton() .AddHostedService(provider => provider.GetRequiredService()) .AddHostedService() .AddHostedService() - .AddHostedService() - // Slash commands - .AddCommandTree() - .WithCommandGroup() - .WithCommandGroup() - .WithCommandGroup() - .WithCommandGroup() - .WithCommandGroup() - .WithCommandGroup() - .WithCommandGroup() - .WithCommandGroup() - .WithCommandGroup(); - var responderTypes = typeof(Octobot).Assembly - .GetExportedTypes() - .Where(t => t.IsResponder()); - foreach (var responderType in responderTypes) - { - services.AddResponder(responderType); - } + .AddHostedService(); } ).ConfigureLogging( c => c.AddConsole() diff --git a/src/Responders/GuildMemberJoinedResponder.cs b/src/Responders/GuildMemberJoinedResponder.cs index 66faa28..eee93b6 100644 --- a/src/Responders/GuildMemberJoinedResponder.cs +++ b/src/Responders/GuildMemberJoinedResponder.cs @@ -43,6 +43,8 @@ public class GuildMemberJoinedResponder : IResponder var cfg = data.Settings; var memberData = data.GetOrCreateMemberData(user.ID); + memberData.Kicked = false; + var returnRolesResult = await TryReturnRolesAsync(cfg, memberData, gatewayEvent.GuildID, user.ID, ct); if (!returnRolesResult.IsSuccess) { diff --git a/src/Services/Profiler/Profiler.cs b/src/Services/Profiler/Profiler.cs new file mode 100644 index 0000000..8d4ca98 --- /dev/null +++ b/src/Services/Profiler/Profiler.cs @@ -0,0 +1,114 @@ +using System.Diagnostics; +using System.Text; +using Microsoft.Extensions.Logging; +using Remora.Results; + +// TODO: remove in future profiler PRs +// ReSharper disable All + +namespace Octobot.Services.Profiler; + +/// +/// Provides the ability to profile how long certain parts of code take to complete using es. +/// +/// Resolve instead in singletons. +public sealed class Profiler +{ + private const int MaxProfilerTime = 1000; // milliseconds + private readonly List _events = []; + private readonly ILogger _logger; + + public Profiler(ILogger logger) + { + _logger = logger; + } + + /// + /// Pushes an event to the profiler. + /// + /// The ID of the event. + public void Push(string id) + { + _events.Add(new ProfilerEvent + { + Id = id, + Stopwatch = Stopwatch.StartNew() + }); + } + + /// + /// Pops the last pushed event from the profiler. + /// + /// Thrown if the profiler contains no events. + public void Pop() + { + if (_events.Count is 0) + { + throw new InvalidOperationException("Nothing to pop"); + } + + _events.Last().Stopwatch.Stop(); + } + + /// + /// If the profiler took too long to execute, this will log a warning with per-event time usage + /// + /// + private void Report() + { + var main = _events[0]; + if (main.Stopwatch.ElapsedMilliseconds < MaxProfilerTime) + { + return; + } + + var unprofiled = main.Stopwatch.ElapsedMilliseconds; + var builder = new StringBuilder().AppendLine(); + for (var i = 1; i < _events.Count; i++) + { + var profilerEvent = _events[i]; + if (profilerEvent.Stopwatch.IsRunning) + { + throw new InvalidOperationException( + $"Tried to report on a profiler with running stopwatches: {profilerEvent.Id}"); + } + + builder.AppendLine($"{profilerEvent.Id}: {profilerEvent.Stopwatch.ElapsedMilliseconds}ms"); + unprofiled -= profilerEvent.Stopwatch.ElapsedMilliseconds; + } + + builder.AppendLine($": {unprofiled}ms"); + + _logger.LogWarning("Profiler {ID} took {Elapsed} milliseconds to execute (max: {Max}ms):{Events}", main.Id, + main.Stopwatch.ElapsedMilliseconds, MaxProfilerTime, builder.ToString()); + } + + /// + /// the profiler and on it afterwards. + /// + public void PopAndReport() + { + Pop(); + Report(); + } + + /// + /// on the profiler and return a . + /// + /// + /// + public Result ReportWithResult(Result result) + { + PopAndReport(); + return result; + } + + /// + /// Calls with + /// + /// A successful result. + public Result ReportWithSuccess() + { + return ReportWithResult(Result.FromSuccess()); + } +} diff --git a/src/Services/Profiler/ProfilerEvent.cs b/src/Services/Profiler/ProfilerEvent.cs new file mode 100644 index 0000000..f655fc4 --- /dev/null +++ b/src/Services/Profiler/ProfilerEvent.cs @@ -0,0 +1,9 @@ +using System.Diagnostics; + +namespace Octobot.Services.Profiler; + +public struct ProfilerEvent +{ + public string Id { get; init; } + public Stopwatch Stopwatch { get; init; } +} diff --git a/src/Services/Profiler/ProfilerFactory.cs b/src/Services/Profiler/ProfilerFactory.cs new file mode 100644 index 0000000..0135771 --- /dev/null +++ b/src/Services/Profiler/ProfilerFactory.cs @@ -0,0 +1,27 @@ +using Microsoft.Extensions.DependencyInjection; + +namespace Octobot.Services.Profiler; + +/// +/// Provides a method to create a . Useful in singletons. +/// +public sealed class ProfilerFactory +{ + private readonly IServiceScopeFactory _scopeFactory; + + public ProfilerFactory(IServiceScopeFactory scopeFactory) + { + _scopeFactory = scopeFactory; + } + + /// + /// Creates a new . + /// + /// A new . + // TODO: remove in future profiler PRs + // ReSharper disable once UnusedMember.Global + public Profiler Create() + { + return _scopeFactory.CreateScope().ServiceProvider.GetRequiredService(); + } +}