1
0
Fork 1
mirror of https://github.com/TeamOctolings/Octobot.git synced 2025-04-20 00:43:36 +03:00

Merge branch 'master' into wipeout-exclamation-mark

Signed-off-by: Macintxsh <95250141+mctaylors@users.noreply.github.com>
This commit is contained in:
Macintxsh 2023-12-28 10:01:00 +03:00 committed by GitHub
commit 1bcc62ee42
Signed by: GitHub
GPG key ID: 4AEE18F83AFDEB23
26 changed files with 276 additions and 137 deletions

View file

@ -18,3 +18,5 @@ P:System.DateTime.Now;Use System.DateTime.UtcNow instead.
P:System.DateTimeOffset.Now;Use System.DateTimeOffset.UtcNow instead. P:System.DateTimeOffset.Now;Use System.DateTimeOffset.UtcNow instead.
P:System.DateTimeOffset.DateTime;Use System.DateTimeOffset.UtcDateTime instead. P:System.DateTimeOffset.DateTime;Use System.DateTimeOffset.UtcDateTime instead.
M:System.IO.File.OpenWrite(System.String);File.OpenWrite(string) does not clear the file before writing to it. Use File.Create(string) instead. M:System.IO.File.OpenWrite(System.String);File.OpenWrite(string) does not clear the file before writing to it. Use File.Create(string) instead.
M:System.Threading.Thread.Sleep(System.Int32);Use Task.Delay(int, CancellationToken) instead.
M:System.Threading.Thread.Sleep(System.TimeSpan);Use Task.Delay(TimeSpan, CancellationToken) instead.

View file

@ -20,7 +20,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="DiffPlex" Version="1.7.1" /> <PackageReference Include="DiffPlex" Version="1.7.2" />
<PackageReference Include="Humanizer.Core.ru" Version="2.14.1" /> <PackageReference Include="Humanizer.Core.ru" Version="2.14.1" />
<PackageReference Include="JetBrains.Annotations" Version="2023.3.0" /> <PackageReference Include="JetBrains.Annotations" Version="2023.3.0" />
<PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4" /> <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4" />

View file

@ -400,7 +400,7 @@
<value>Octobot's source code</value> <value>Octobot's source code</value>
</data> </data>
<data name="AboutBot" xml:space="preserve"> <data name="AboutBot" xml:space="preserve">
<value>About Octobot</value> <value>About {0}</value>
</data> </data>
<data name="AboutDeveloper@mctaylors" xml:space="preserve"> <data name="AboutDeveloper@mctaylors" xml:space="preserve">
<value>developer &amp; designer, Octobot's Wiki creator</value> <value>developer &amp; designer, Octobot's Wiki creator</value>
@ -588,4 +588,7 @@
<data name="InvalidTimeSpan" xml:space="preserve"> <data name="InvalidTimeSpan" xml:space="preserve">
<value>Incorrect time specified.</value> <value>Incorrect time specified.</value>
</data> </data>
<data name="UserInfoKicked" xml:space="preserve">
<value>Kicked</value>
</data>
</root> </root>

View file

@ -400,7 +400,7 @@
<value>Исходный код Octobot</value> <value>Исходный код Octobot</value>
</data> </data>
<data name="AboutBot" xml:space="preserve"> <data name="AboutBot" xml:space="preserve">
<value>Об Octobot</value> <value>О боте {0}</value>
</data> </data>
<data name="AboutDeveloper@neroduckale" xml:space="preserve"> <data name="AboutDeveloper@neroduckale" xml:space="preserve">
<value>разработчик</value> <value>разработчик</value>
@ -588,4 +588,7 @@
<data name="InvalidTimeSpan" xml:space="preserve"> <data name="InvalidTimeSpan" xml:space="preserve">
<value>Указано некорректное время.</value> <value>Указано некорректное время.</value>
</data> </data>
<data name="UserInfoKicked" xml:space="preserve">
<value>Выгнан</value>
</data>
</root> </root>

View file

@ -400,7 +400,7 @@
<value>репа Octobot (тык)</value> <value>репа Octobot (тык)</value>
</data> </data>
<data name="AboutBot" xml:space="preserve"> <data name="AboutBot" xml:space="preserve">
<value>немного об Octobot</value> <value>немного об {0}</value>
</data> </data>
<data name="AboutDeveloper@mctaylors" xml:space="preserve"> <data name="AboutDeveloper@mctaylors" xml:space="preserve">
<value>скучный девелопер + дизайнер создавший Octobot's Wiki</value> <value>скучный девелопер + дизайнер создавший Octobot's Wiki</value>
@ -502,7 +502,7 @@
<value>приколы полученные по заслугам</value> <value>приколы полученные по заслугам</value>
</data> </data>
<data name="UserInfoBannedPermanently" xml:space="preserve"> <data name="UserInfoBannedPermanently" xml:space="preserve">
<value>забанен</value> <value>пермабан</value>
</data> </data>
<data name="UserInfoNotOnGuild" xml:space="preserve"> <data name="UserInfoNotOnGuild" xml:space="preserve">
<value>вышел из сервера</value> <value>вышел из сервера</value>
@ -588,4 +588,7 @@
<data name="InvalidTimeSpan" xml:space="preserve"> <data name="InvalidTimeSpan" xml:space="preserve">
<value>ты там правильно напиши таймспан</value> <value>ты там правильно напиши таймспан</value>
</data> </data>
<data name="UserInfoKicked" xml:space="preserve">
<value>кикнут</value>
</data>
</root> </root>

View file

@ -15,6 +15,7 @@ using Remora.Discord.Commands.Contexts;
using Remora.Discord.Commands.Feedback.Messages; using Remora.Discord.Commands.Feedback.Messages;
using Remora.Discord.Commands.Feedback.Services; using Remora.Discord.Commands.Feedback.Services;
using Remora.Discord.Extensions.Embeds; using Remora.Discord.Extensions.Embeds;
using Remora.Discord.Extensions.Formatting;
using Remora.Rest.Core; using Remora.Rest.Core;
using Remora.Results; using Remora.Results;
@ -27,11 +28,11 @@ namespace Octobot.Commands;
public class AboutCommandGroup : CommandGroup public class AboutCommandGroup : CommandGroup
{ {
private static readonly (string Username, Snowflake Id)[] Developers = private static readonly (string Username, Snowflake Id)[] Developers =
{ [
("Octol1ttle", new Snowflake(504343489664909322)), ("Octol1ttle", new Snowflake(504343489664909322)),
("mctaylors", new Snowflake(326642240229474304)), ("mctaylors", new Snowflake(326642240229474304)),
("neroduckale", new Snowflake(474943797063843851)) ("neroduckale", new Snowflake(474943797063843851))
}; ];
private readonly ICommandContext _context; private readonly ICommandContext _context;
private readonly IFeedbackService _feedback; private readonly IFeedbackService _feedback;
@ -88,12 +89,15 @@ public class AboutCommandGroup : CommandGroup
{ {
var guildMemberResult = await _guildApi.GetGuildMemberAsync( var guildMemberResult = await _guildApi.GetGuildMemberAsync(
guildId, dev.Id, ct); guildId, dev.Id, ct);
var tag = guildMemberResult.IsSuccess ? $"<@{dev.Id}>" : $"@{dev.Username}"; var tag = guildMemberResult.IsSuccess
? $"<@{dev.Id}>"
: Markdown.Hyperlink($"@{dev.Username}", $"https://github.com/{dev.Username}");
builder.AppendBulletPointLine($"{tag} — {$"AboutDeveloper@{dev.Username}".Localized()}"); builder.AppendBulletPointLine($"{tag} — {$"AboutDeveloper@{dev.Username}".Localized()}");
} }
var embed = new EmbedBuilder().WithSmallTitle(Messages.AboutBot, bot) var embed = new EmbedBuilder()
.WithSmallTitle(string.Format(Messages.AboutBot, bot.Username), bot)
.WithDescription(builder.ToString()) .WithDescription(builder.ToString())
.WithColour(ColorsList.Cyan) .WithColour(ColorsList.Cyan)
.WithImageUrl("https://cdn.mctaylors.ru/octobot-banner.png") .WithImageUrl("https://cdn.mctaylors.ru/octobot-banner.png")

View file

@ -33,12 +33,12 @@ public class BanCommandGroup : CommandGroup
private readonly IDiscordRestGuildAPI _guildApi; private readonly IDiscordRestGuildAPI _guildApi;
private readonly GuildDataService _guildData; private readonly GuildDataService _guildData;
private readonly IDiscordRestUserAPI _userApi; private readonly IDiscordRestUserAPI _userApi;
private readonly UtilityService _utility; private readonly Utility _utility;
public BanCommandGroup( public BanCommandGroup(
ICommandContext context, IDiscordRestChannelAPI channelApi, GuildDataService guildData, ICommandContext context, IDiscordRestChannelAPI channelApi, GuildDataService guildData,
IFeedbackService feedback, IDiscordRestGuildAPI guildApi, IDiscordRestUserAPI userApi, IFeedbackService feedback, IDiscordRestGuildAPI guildApi, IDiscordRestUserAPI userApi,
UtilityService utility) Utility utility)
{ {
_context = context; _context = context;
_channelApi = channelApi; _channelApi = channelApi;

View file

@ -30,11 +30,11 @@ public class ClearCommandGroup : CommandGroup
private readonly IFeedbackService _feedback; private readonly IFeedbackService _feedback;
private readonly GuildDataService _guildData; private readonly GuildDataService _guildData;
private readonly IDiscordRestUserAPI _userApi; private readonly IDiscordRestUserAPI _userApi;
private readonly UtilityService _utility; private readonly Utility _utility;
public ClearCommandGroup( public ClearCommandGroup(
IDiscordRestChannelAPI channelApi, ICommandContext context, GuildDataService guildData, IDiscordRestChannelAPI channelApi, ICommandContext context, GuildDataService guildData,
IFeedbackService feedback, IDiscordRestUserAPI userApi, UtilityService utility) IFeedbackService feedback, IDiscordRestUserAPI userApi, Utility utility)
{ {
_channelApi = channelApi; _channelApi = channelApi;
_context = context; _context = context;

View file

@ -30,12 +30,12 @@ public class KickCommandGroup : CommandGroup
private readonly IDiscordRestGuildAPI _guildApi; private readonly IDiscordRestGuildAPI _guildApi;
private readonly GuildDataService _guildData; private readonly GuildDataService _guildData;
private readonly IDiscordRestUserAPI _userApi; private readonly IDiscordRestUserAPI _userApi;
private readonly UtilityService _utility; private readonly Utility _utility;
public KickCommandGroup( public KickCommandGroup(
ICommandContext context, IDiscordRestChannelAPI channelApi, GuildDataService guildData, ICommandContext context, IDiscordRestChannelAPI channelApi, GuildDataService guildData,
IFeedbackService feedback, IDiscordRestGuildAPI guildApi, IDiscordRestUserAPI userApi, IFeedbackService feedback, IDiscordRestGuildAPI guildApi, IDiscordRestUserAPI userApi,
UtilityService utility) Utility utility)
{ {
_context = context; _context = context;
_channelApi = channelApi; _channelApi = channelApi;
@ -151,7 +151,9 @@ public class KickCommandGroup : CommandGroup
return Result.FromError(kickResult.Error); 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 title = string.Format(Messages.UserKicked, target.GetTag());
var description = MarkdownExtensions.BulletPoint(string.Format(Messages.DescriptionActionReason, reason)); var description = MarkdownExtensions.BulletPoint(string.Format(Messages.DescriptionActionReason, reason));

View file

@ -32,11 +32,11 @@ public class MuteCommandGroup : CommandGroup
private readonly IDiscordRestGuildAPI _guildApi; private readonly IDiscordRestGuildAPI _guildApi;
private readonly GuildDataService _guildData; private readonly GuildDataService _guildData;
private readonly IDiscordRestUserAPI _userApi; private readonly IDiscordRestUserAPI _userApi;
private readonly UtilityService _utility; private readonly Utility _utility;
public MuteCommandGroup( public MuteCommandGroup(
ICommandContext context, GuildDataService guildData, IFeedbackService feedback, ICommandContext context, GuildDataService guildData, IFeedbackService feedback,
IDiscordRestGuildAPI guildApi, IDiscordRestUserAPI userApi, UtilityService utility) IDiscordRestGuildAPI guildApi, IDiscordRestUserAPI userApi, Utility utility)
{ {
_context = context; _context = context;
_guildData = guildData; _guildData = guildData;
@ -300,9 +300,9 @@ public class MuteCommandGroup : CommandGroup
} }
var memberData = data.GetOrCreateMemberData(target.ID); 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) var failedEmbed = new EmbedBuilder().WithSmallTitle(Messages.UserNotMuted, bot)
.WithColour(ColorsList.Red).Build(); .WithColour(ColorsList.Red).Build();

View file

@ -36,7 +36,7 @@ public class SettingsCommandGroup : CommandGroup
/// that the orders match. /// that the orders match.
/// </remarks> /// </remarks>
private static readonly IOption[] AllOptions = private static readonly IOption[] AllOptions =
{ [
GuildSettings.Language, GuildSettings.Language,
GuildSettings.WelcomeMessage, GuildSettings.WelcomeMessage,
GuildSettings.ReceiveStartupMessages, GuildSettings.ReceiveStartupMessages,
@ -51,17 +51,17 @@ public class SettingsCommandGroup : CommandGroup
GuildSettings.MuteRole, GuildSettings.MuteRole,
GuildSettings.EventNotificationRole, GuildSettings.EventNotificationRole,
GuildSettings.EventEarlyNotificationOffset GuildSettings.EventEarlyNotificationOffset
}; ];
private readonly ICommandContext _context; private readonly ICommandContext _context;
private readonly IFeedbackService _feedback; private readonly IFeedbackService _feedback;
private readonly GuildDataService _guildData; private readonly GuildDataService _guildData;
private readonly IDiscordRestUserAPI _userApi; private readonly IDiscordRestUserAPI _userApi;
private readonly UtilityService _utility; private readonly Utility _utility;
public SettingsCommandGroup( public SettingsCommandGroup(
ICommandContext context, GuildDataService guildData, ICommandContext context, GuildDataService guildData,
IFeedbackService feedback, IDiscordRestUserAPI userApi, UtilityService utility) IFeedbackService feedback, IDiscordRestUserAPI userApi, Utility utility)
{ {
_context = context; _context = context;
_guildData = guildData; _guildData = guildData;

View file

@ -122,32 +122,21 @@ public class ToolsCommandGroup : CommandGroup
embedColor = AppendGuildInformation(embedColor, guildMember, builder); embedColor = AppendGuildInformation(embedColor, guildMember, builder);
} }
var isMuted = (memberData.MutedUntil is not null && DateTimeOffset.UtcNow <= memberData.MutedUntil) || var wasMuted = (memberData.MutedUntil is not null && DateTimeOffset.UtcNow <= memberData.MutedUntil) ||
communicationDisabledUntil is not null; 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 (wasMuted || wasBanned || wasKicked)
if (isMuted || existingBanResult.IsDefined())
{ {
builder.Append("### ") builder.Append("### ")
.AppendLine(Markdown.Bold(Messages.UserInfoPunishments)); .AppendLine(Markdown.Bold(Messages.UserInfoPunishments));
embedColor = AppendPunishmentsInformation(wasMuted, wasKicked, wasBanned, memberData,
builder, embedColor, communicationDisabledUntil);
} }
if (isMuted) if (!guildMemberResult.IsSuccess && !wasBanned)
{
AppendMuteInformation(memberData, communicationDisabledUntil, builder);
embedColor = ColorsList.Red;
}
if (existingBanResult.IsDefined())
{
AppendBanInformation(memberData, builder);
embedColor = ColorsList.Black;
}
if (!guildMemberResult.IsSuccess && !existingBanResult.IsDefined())
{ {
builder.Append("### ") builder.Append("### ")
.AppendLine(Markdown.Bold(Messages.UserInfoNotOnGuild)); .AppendLine(Markdown.Bold(Messages.UserInfoNotOnGuild));
@ -166,6 +155,29 @@ public class ToolsCommandGroup : CommandGroup
return await _feedback.SendContextualEmbedResultAsync(embed, ct: ct); 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) private static Color AppendGuildInformation(Color color, IGuildMember guildMember, StringBuilder builder)
{ {
if (guildMember.Nickname.IsDefined(out var nickname)) if (guildMember.Nickname.IsDefined(out var nickname))
@ -393,7 +405,7 @@ public class ToolsCommandGroup : CommandGroup
} }
private static readonly TimestampStyle[] AllStyles = private static readonly TimestampStyle[] AllStyles =
{ [
TimestampStyle.ShortDate, TimestampStyle.ShortDate,
TimestampStyle.LongDate, TimestampStyle.LongDate,
TimestampStyle.ShortTime, TimestampStyle.ShortTime,
@ -401,7 +413,7 @@ public class ToolsCommandGroup : CommandGroup
TimestampStyle.ShortDateTime, TimestampStyle.ShortDateTime,
TimestampStyle.LongDateTime, TimestampStyle.LongDateTime,
TimestampStyle.RelativeTime TimestampStyle.RelativeTime
}; ];
/// <summary> /// <summary>
/// A slash command that shows the current timestamp with an optional offset in all styles supported by Discord. /// A slash command that shows the current timestamp with an optional offset in all styles supported by Discord.

View file

@ -18,6 +18,7 @@ public sealed class MemberData
public ulong Id { get; } public ulong Id { get; }
public DateTimeOffset? BannedUntil { get; set; } public DateTimeOffset? BannedUntil { get; set; }
public DateTimeOffset? MutedUntil { get; set; } public DateTimeOffset? MutedUntil { get; set; }
public bool Kicked { get; set; }
public List<ulong> Roles { get; set; } = []; public List<ulong> Roles { get; set; } = [];
public List<Reminder> Reminders { get; } = []; public List<Reminder> Reminders { get; } = [];
} }

View file

@ -1044,5 +1044,13 @@ namespace Octobot {
return ResourceManager.GetString("InvalidTimeSpan", resourceCulture); return ResourceManager.GetString("InvalidTimeSpan", resourceCulture);
} }
} }
internal static string UserInfoKicked
{
get
{
return ResourceManager.GetString("UserInfoKicked", resourceCulture);
}
}
} }
} }

View file

@ -2,11 +2,10 @@ using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Octobot.Commands;
using Octobot.Commands.Events; using Octobot.Commands.Events;
using Octobot.Services; using Octobot.Services;
using Octobot.Services.Profiler;
using Octobot.Services.Update; using Octobot.Services.Update;
using Remora.Commands.Extensions;
using Remora.Discord.API.Abstractions.Gateway.Commands; using Remora.Discord.API.Abstractions.Gateway.Commands;
using Remora.Discord.API.Abstractions.Objects; using Remora.Discord.API.Abstractions.Objects;
using Remora.Discord.API.Objects; using Remora.Discord.API.Objects;
@ -14,8 +13,8 @@ using Remora.Discord.Caching.Extensions;
using Remora.Discord.Caching.Services; using Remora.Discord.Caching.Services;
using Remora.Discord.Commands.Extensions; using Remora.Discord.Commands.Extensions;
using Remora.Discord.Commands.Services; using Remora.Discord.Commands.Services;
using Remora.Discord.Extensions.Extensions;
using Remora.Discord.Gateway; using Remora.Discord.Gateway;
using Remora.Discord.Gateway.Extensions;
using Remora.Discord.Hosting.Extensions; using Remora.Discord.Hosting.Extensions;
using Remora.Rest.Core; using Remora.Rest.Core;
using Serilog.Extensions.Logging; using Serilog.Extensions.Logging;
@ -24,12 +23,12 @@ namespace Octobot;
public sealed class Octobot public sealed class Octobot
{ {
public static readonly AllowedMentions NoMentions = new(
Array.Empty<MentionType>(), Array.Empty<Snowflake>(), Array.Empty<Snowflake>());
public const string RepositoryUrl = "https://github.com/LabsDevelopment/Octobot"; public const string RepositoryUrl = "https://github.com/LabsDevelopment/Octobot";
public const string IssuesUrl = $"{RepositoryUrl}/issues"; public const string IssuesUrl = $"{RepositoryUrl}/issues";
public static readonly AllowedMentions NoMentions = new(
Array.Empty<MentionType>(), Array.Empty<Snowflake>(), Array.Empty<Snowflake>());
public static async Task Main(string[] args) public static async Task Main(string[] args)
{ {
var host = CreateHostBuilder(args).UseConsoleLifetime().Build(); var host = CreateHostBuilder(args).UseConsoleLifetime().Build();
@ -82,34 +81,20 @@ public sealed class Octobot
// Init // Init
.AddDiscordCaching() .AddDiscordCaching()
.AddDiscordCommands(true, false) .AddDiscordCommands(true, false)
.AddRespondersFromAssembly(typeof(Octobot).Assembly)
.AddCommandGroupsFromAssembly(typeof(Octobot).Assembly)
// Slash command event handlers // Slash command event handlers
.AddPreparationErrorEvent<LoggingPreparationErrorEvent>() .AddPreparationErrorEvent<LoggingPreparationErrorEvent>()
.AddPostExecutionEvent<ErrorLoggingPostExecutionEvent>() .AddPostExecutionEvent<ErrorLoggingPostExecutionEvent>()
// Services // Services
.AddTransient<Profiler>()
.AddSingleton<ProfilerFactory>()
.AddSingleton<Utility>()
.AddSingleton<GuildDataService>() .AddSingleton<GuildDataService>()
.AddSingleton<UtilityService>() .AddHostedService<GuildDataService>(provider => provider.GetRequiredService<GuildDataService>())
.AddHostedService<MemberUpdateService>() .AddHostedService<MemberUpdateService>()
.AddHostedService<ScheduledEventUpdateService>() .AddHostedService<ScheduledEventUpdateService>()
.AddHostedService<SongUpdateService>() .AddHostedService<SongUpdateService>();
.AddHostedService<BackgroundGuildDataSaverService>()
// Slash commands
.AddCommandTree()
.WithCommandGroup<AboutCommandGroup>()
.WithCommandGroup<BanCommandGroup>()
.WithCommandGroup<ClearCommandGroup>()
.WithCommandGroup<KickCommandGroup>()
.WithCommandGroup<MuteCommandGroup>()
.WithCommandGroup<PingCommandGroup>()
.WithCommandGroup<RemindCommandGroup>()
.WithCommandGroup<SettingsCommandGroup>()
.WithCommandGroup<ToolsCommandGroup>();
var responderTypes = typeof(Octobot).Assembly
.GetExportedTypes()
.Where(t => t.IsResponder());
foreach (var responderType in responderTypes)
{
services.AddResponder(responderType);
}
} }
).ConfigureLogging( ).ConfigureLogging(
c => c.AddConsole() c => c.AddConsole()

View file

@ -25,11 +25,11 @@ public class GuildLoadedResponder : IResponder<IGuildCreate>
private readonly GuildDataService _guildData; private readonly GuildDataService _guildData;
private readonly ILogger<GuildLoadedResponder> _logger; private readonly ILogger<GuildLoadedResponder> _logger;
private readonly IDiscordRestUserAPI _userApi; private readonly IDiscordRestUserAPI _userApi;
private readonly UtilityService _utility; private readonly Utility _utility;
public GuildLoadedResponder( public GuildLoadedResponder(
IDiscordRestChannelAPI channelApi, GuildDataService guildData, ILogger<GuildLoadedResponder> logger, IDiscordRestChannelAPI channelApi, GuildDataService guildData, ILogger<GuildLoadedResponder> logger,
IDiscordRestUserAPI userApi, UtilityService utility) IDiscordRestUserAPI userApi, Utility utility)
{ {
_channelApi = channelApi; _channelApi = channelApi;
_guildData = guildData; _guildData = guildData;

View file

@ -43,6 +43,8 @@ public class GuildMemberJoinedResponder : IResponder<IGuildMemberAdd>
var cfg = data.Settings; var cfg = data.Settings;
var memberData = data.GetOrCreateMemberData(user.ID); var memberData = data.GetOrCreateMemberData(user.ID);
memberData.Kicked = false;
var returnRolesResult = await TryReturnRolesAsync(cfg, memberData, gatewayEvent.GuildID, user.ID, ct); var returnRolesResult = await TryReturnRolesAsync(cfg, memberData, gatewayEvent.GuildID, user.ID, ct);
if (!returnRolesResult.IsSuccess) if (!returnRolesResult.IsSuccess)
{ {

View file

@ -1,23 +0,0 @@
using Microsoft.Extensions.Hosting;
namespace Octobot.Services;
public sealed class BackgroundGuildDataSaverService : BackgroundService
{
private readonly GuildDataService _guildData;
public BackgroundGuildDataSaverService(GuildDataService guildData)
{
_guildData = guildData;
}
protected override async Task ExecuteAsync(CancellationToken ct)
{
using var timer = new PeriodicTimer(TimeSpan.FromMinutes(5));
while (await timer.WaitForNextTickAsync(ct))
{
await _guildData.SaveAsync(ct);
}
}
}

View file

@ -11,35 +11,23 @@ namespace Octobot.Services;
/// <summary> /// <summary>
/// Handles saving, loading, initializing and providing <see cref="GuildData" />. /// Handles saving, loading, initializing and providing <see cref="GuildData" />.
/// </summary> /// </summary>
public sealed class GuildDataService : IHostedService public sealed class GuildDataService : BackgroundService
{ {
private readonly ConcurrentDictionary<Snowflake, GuildData> _datas = new(); private readonly ConcurrentDictionary<Snowflake, GuildData> _datas = new();
private readonly ILogger<GuildDataService> _logger; private readonly ILogger<GuildDataService> _logger;
// https://github.com/dotnet/aspnetcore/issues/39139 public GuildDataService(ILogger<GuildDataService> logger)
public GuildDataService(
IHostApplicationLifetime lifetime, ILogger<GuildDataService> logger)
{ {
_logger = logger; _logger = logger;
lifetime.ApplicationStopping.Register(ApplicationStopping);
} }
public Task StartAsync(CancellationToken ct) public override Task StopAsync(CancellationToken ct)
{ {
return Task.CompletedTask; base.StopAsync(ct);
return SaveAsync(ct);
} }
public Task StopAsync(CancellationToken ct) private Task SaveAsync(CancellationToken ct)
{
return Task.CompletedTask;
}
private void ApplicationStopping()
{
SaveAsync(CancellationToken.None).GetAwaiter().GetResult();
}
public Task SaveAsync(CancellationToken ct)
{ {
var tasks = new List<Task>(); var tasks = new List<Task>();
var datas = _datas.Values.ToArray(); var datas = _datas.Values.ToArray();
@ -68,6 +56,16 @@ public sealed class GuildDataService : IHostedService
File.Delete(tempFilePath); File.Delete(tempFilePath);
} }
protected override async Task ExecuteAsync(CancellationToken ct)
{
using var timer = new PeriodicTimer(TimeSpan.FromMinutes(5));
while (await timer.WaitForNextTickAsync(ct))
{
await SaveAsync(ct);
}
}
public async Task<GuildData> GetData(Snowflake guildId, CancellationToken ct = default) public async Task<GuildData> GetData(Snowflake guildId, CancellationToken ct = default)
{ {
return _datas.TryGetValue(guildId, out var data) ? data : await InitializeData(guildId, ct); return _datas.TryGetValue(guildId, out var data) ? data : await InitializeData(guildId, ct);

View file

@ -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;
/// <summary>
/// Provides the ability to profile how long certain parts of code take to complete using <see cref="Stopwatch"/>es.
/// </summary>
/// <remarks>Resolve <see cref="ProfilerFactory"/> instead in singletons.</remarks>
public sealed class Profiler
{
private const int MaxProfilerTime = 1000; // milliseconds
private readonly List<ProfilerEvent> _events = [];
private readonly ILogger<Profiler> _logger;
public Profiler(ILogger<Profiler> logger)
{
_logger = logger;
}
/// <summary>
/// Pushes an event to the profiler.
/// </summary>
/// <param name="id">The ID of the event.</param>
public void Push(string id)
{
_events.Add(new ProfilerEvent
{
Id = id,
Stopwatch = Stopwatch.StartNew()
});
}
/// <summary>
/// Pops the last pushed event from the profiler.
/// </summary>
/// <exception cref="InvalidOperationException">Thrown if the profiler contains no events.</exception>
public void Pop()
{
if (_events.Count is 0)
{
throw new InvalidOperationException("Nothing to pop");
}
_events.Last().Stopwatch.Stop();
}
/// <summary>
/// If the profiler took too long to execute, this will log a warning with per-event time usage
/// </summary>
/// <exception cref="InvalidOperationException"></exception>
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>: {unprofiled}ms");
_logger.LogWarning("Profiler {ID} took {Elapsed} milliseconds to execute (max: {Max}ms):{Events}", main.Id,
main.Stopwatch.ElapsedMilliseconds, MaxProfilerTime, builder.ToString());
}
/// <summary>
/// <see cref="Pop"/> the profiler and <see cref="Report"/> on it afterwards.
/// </summary>
public void PopAndReport()
{
Pop();
Report();
}
/// <summary>
/// <see cref="PopAndReport"/> on the profiler and return a <see cref="Result{TEntity}"/>.
/// </summary>
/// <param name="result"></param>
/// <returns></returns>
public Result ReportWithResult(Result result)
{
PopAndReport();
return result;
}
/// <summary>
/// Calls <see cref="ReportWithResult"/> with <see cref="Result.FromSuccess"/>
/// </summary>
/// <returns>A successful result.</returns>
public Result ReportWithSuccess()
{
return ReportWithResult(Result.FromSuccess());
}
}

View file

@ -0,0 +1,9 @@
using System.Diagnostics;
namespace Octobot.Services.Profiler;
public struct ProfilerEvent
{
public string Id { get; init; }
public Stopwatch Stopwatch { get; init; }
}

View file

@ -0,0 +1,27 @@
using Microsoft.Extensions.DependencyInjection;
namespace Octobot.Services.Profiler;
/// <summary>
/// Provides a method to create a <see cref="Profiler"/>. Useful in singletons.
/// </summary>
public sealed class ProfilerFactory
{
private readonly IServiceScopeFactory _scopeFactory;
public ProfilerFactory(IServiceScopeFactory scopeFactory)
{
_scopeFactory = scopeFactory;
}
/// <summary>
/// Creates a new <see cref="Profiler"/>.
/// </summary>
/// <returns>A new <see cref="Profiler"/>.</returns>
// TODO: remove in future profiler PRs
// ReSharper disable once UnusedMember.Global
public Profiler Create()
{
return _scopeFactory.CreateScope().ServiceProvider.GetRequiredService<Profiler>();
}
}

View file

@ -16,7 +16,7 @@ namespace Octobot.Services.Update;
public sealed partial class MemberUpdateService : BackgroundService public sealed partial class MemberUpdateService : BackgroundService
{ {
private static readonly string[] GenericNicknames = private static readonly string[] GenericNicknames =
{ [
"Albatross", "Alpha", "Anchor", "Banjo", "Bell", "Beta", "Blackbird", "Bulldog", "Canary", "Albatross", "Alpha", "Anchor", "Banjo", "Bell", "Beta", "Blackbird", "Bulldog", "Canary",
"Cat", "Calf", "Cyclone", "Daisy", "Dalmatian", "Dart", "Delta", "Diamond", "Donkey", "Duck", "Cat", "Calf", "Cyclone", "Daisy", "Dalmatian", "Dart", "Delta", "Diamond", "Donkey", "Duck",
"Emu", "Eclipse", "Flamingo", "Flute", "Frog", "Goose", "Hatchet", "Heron", "Husky", "Hurricane", "Emu", "Eclipse", "Flamingo", "Flute", "Frog", "Goose", "Hatchet", "Heron", "Husky", "Hurricane",
@ -24,16 +24,16 @@ public sealed partial class MemberUpdateService : BackgroundService
"Nautilus", "Ostrich", "Octopus", "Pelican", "Puffin", "Pyramid", "Rattle", "Robin", "Rose", "Nautilus", "Ostrich", "Octopus", "Pelican", "Puffin", "Pyramid", "Rattle", "Robin", "Rose",
"Salmon", "Seal", "Shark", "Sheep", "Snake", "Sonar", "Stump", "Sparrow", "Toaster", "Toucan", "Salmon", "Seal", "Shark", "Sheep", "Snake", "Sonar", "Stump", "Sparrow", "Toaster", "Toucan",
"Torus", "Violet", "Vortex", "Vulture", "Wagon", "Whale", "Woodpecker", "Zebra", "Zigzag" "Torus", "Violet", "Vortex", "Vulture", "Wagon", "Whale", "Woodpecker", "Zebra", "Zigzag"
}; ];
private readonly IDiscordRestChannelAPI _channelApi; private readonly IDiscordRestChannelAPI _channelApi;
private readonly IDiscordRestGuildAPI _guildApi; private readonly IDiscordRestGuildAPI _guildApi;
private readonly GuildDataService _guildData; private readonly GuildDataService _guildData;
private readonly ILogger<MemberUpdateService> _logger; private readonly ILogger<MemberUpdateService> _logger;
private readonly UtilityService _utility; private readonly Utility _utility;
public MemberUpdateService(IDiscordRestChannelAPI channelApi, IDiscordRestGuildAPI guildApi, public MemberUpdateService(IDiscordRestChannelAPI channelApi, IDiscordRestGuildAPI guildApi,
GuildDataService guildData, ILogger<MemberUpdateService> logger, UtilityService utility) GuildDataService guildData, ILogger<MemberUpdateService> logger, Utility utility)
{ {
_channelApi = channelApi; _channelApi = channelApi;
_guildApi = guildApi; _guildApi = guildApi;

View file

@ -19,10 +19,10 @@ public sealed class ScheduledEventUpdateService : BackgroundService
private readonly IDiscordRestGuildScheduledEventAPI _eventApi; private readonly IDiscordRestGuildScheduledEventAPI _eventApi;
private readonly GuildDataService _guildData; private readonly GuildDataService _guildData;
private readonly ILogger<ScheduledEventUpdateService> _logger; private readonly ILogger<ScheduledEventUpdateService> _logger;
private readonly UtilityService _utility; private readonly Utility _utility;
public ScheduledEventUpdateService(IDiscordRestChannelAPI channelApi, IDiscordRestGuildScheduledEventAPI eventApi, public ScheduledEventUpdateService(IDiscordRestChannelAPI channelApi, IDiscordRestGuildScheduledEventAPI eventApi,
GuildDataService guildData, ILogger<ScheduledEventUpdateService> logger, UtilityService utility) GuildDataService guildData, ILogger<ScheduledEventUpdateService> logger, Utility utility)
{ {
_channelApi = channelApi; _channelApi = channelApi;
_eventApi = eventApi; _eventApi = eventApi;

View file

@ -9,7 +9,7 @@ namespace Octobot.Services.Update;
public sealed class SongUpdateService : BackgroundService public sealed class SongUpdateService : BackgroundService
{ {
private static readonly (string Author, string Name, TimeSpan Duration)[] SongList = private static readonly (string Author, string Name, TimeSpan Duration)[] SongList =
{ [
("Yoko & the Gold Bazookas", "Rockagilly Blues", new TimeSpan(0, 2, 52)), ("Yoko & the Gold Bazookas", "Rockagilly Blues", new TimeSpan(0, 2, 52)),
("Deep Cut", "Big Betrayal", new TimeSpan(0, 5, 55)), ("Deep Cut", "Big Betrayal", new TimeSpan(0, 5, 55)),
("Squid Sisters", "Tomorrow's Nostalgia Today", new TimeSpan(0, 3, 7)), ("Squid Sisters", "Tomorrow's Nostalgia Today", new TimeSpan(0, 3, 7)),
@ -30,7 +30,7 @@ public sealed class SongUpdateService : BackgroundService
("Turquoise October", "Octoling Rendezvous", new TimeSpan(0, 1, 57)), ("Turquoise October", "Octoling Rendezvous", new TimeSpan(0, 1, 57)),
("Damp Socks feat. Off the Hook", "Tentacle to the Metal", new TimeSpan(0, 2, 51)), ("Damp Socks feat. Off the Hook", "Tentacle to the Metal", new TimeSpan(0, 2, 51)),
("Off the Hook", "Fly Octo Fly ~ Ebb & Flow (Octo)", new TimeSpan(0, 3, 5)) ("Off the Hook", "Fly Octo Fly ~ Ebb & Flow (Octo)", new TimeSpan(0, 3, 5))
}; ];
private readonly List<Activity> _activityList = [new Activity("with Remora.Discord", ActivityType.Game)]; private readonly List<Activity> _activityList = [new Activity("with Remora.Discord", ActivityType.Game)];

View file

@ -1,7 +1,6 @@
using System.Drawing; using System.Drawing;
using System.Text; using System.Text;
using System.Text.Json.Nodes; using System.Text.Json.Nodes;
using Microsoft.Extensions.Hosting;
using Octobot.Data; using Octobot.Data;
using Octobot.Extensions; using Octobot.Extensions;
using Remora.Discord.API.Abstractions.Objects; using Remora.Discord.API.Abstractions.Objects;
@ -17,14 +16,14 @@ namespace Octobot.Services;
/// Provides utility methods that cannot be transformed to extension methods because they require usage /// Provides utility methods that cannot be transformed to extension methods because they require usage
/// of some Discord APIs. /// of some Discord APIs.
/// </summary> /// </summary>
public sealed class UtilityService : IHostedService public sealed class Utility
{ {
private readonly IDiscordRestChannelAPI _channelApi; private readonly IDiscordRestChannelAPI _channelApi;
private readonly IDiscordRestGuildScheduledEventAPI _eventApi; private readonly IDiscordRestGuildScheduledEventAPI _eventApi;
private readonly IDiscordRestGuildAPI _guildApi; private readonly IDiscordRestGuildAPI _guildApi;
private readonly IDiscordRestUserAPI _userApi; private readonly IDiscordRestUserAPI _userApi;
public UtilityService( public Utility(
IDiscordRestChannelAPI channelApi, IDiscordRestGuildScheduledEventAPI eventApi, IDiscordRestGuildAPI guildApi, IDiscordRestChannelAPI channelApi, IDiscordRestGuildScheduledEventAPI eventApi, IDiscordRestGuildAPI guildApi,
IDiscordRestUserAPI userApi) IDiscordRestUserAPI userApi)
{ {
@ -34,16 +33,6 @@ public sealed class UtilityService : IHostedService
_userApi = userApi; _userApi = userApi;
} }
public Task StartAsync(CancellationToken ct)
{
return Task.CompletedTask;
}
public Task StopAsync(CancellationToken ct)
{
return Task.CompletedTask;
}
/// <summary> /// <summary>
/// Checks whether or not a member can interact with another member /// Checks whether or not a member can interact with another member
/// </summary> /// </summary>