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();
+ }
+}