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

Remora.Discord part 2 out of ∞

Signed-off-by: Octol1ttle <l1ttleofficial@outlook.com>
This commit is contained in:
Octol1ttle 2023-05-17 00:18:12 +05:00
parent 2e8392f5d7
commit d0ecfc7928
Signed by: Octol1ttle
GPG key ID: B77C34313AEE1FFF
8 changed files with 946 additions and 1158 deletions

View file

@ -1,13 +1,16 @@
using System.Reflection;
using Microsoft.Extensions.Configuration; 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 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.Caching.Extensions; using Remora.Discord.Caching.Extensions;
using Remora.Discord.Caching.Services; using Remora.Discord.Caching.Services;
using Remora.Discord.Gateway;
using Remora.Discord.Gateway.Extensions; using Remora.Discord.Gateway.Extensions;
using Remora.Discord.Hosting.Extensions; using Remora.Discord.Hosting.Extensions;
using Remora.Rest.Core;
namespace Boyfriend; namespace Boyfriend;
@ -15,7 +18,8 @@ public class Boyfriend {
public static ILogger<Boyfriend> Logger = null!; public static ILogger<Boyfriend> Logger = null!;
public static IConfiguration GuildConfiguration = null!; public static IConfiguration GuildConfiguration = null!;
private static readonly Dictionary<string, string> ReflectionMessageCache = new(); 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();
@ -48,9 +52,17 @@ public class Boyfriend {
services.AddDiscordCaching(); services.AddDiscordCaching();
services.Configure<CacheSettings>( services.Configure<CacheSettings>(
settings => { settings.SetAbsoluteExpiration<IMessage>(TimeSpan.FromDays(7)); }); settings => {
settings.SetDefaultAbsoluteExpiration(TimeSpan.FromHours(1));
settings.SetDefaultSlidingExpiration(TimeSpan.FromMinutes(30));
settings.SetAbsoluteExpiration<IMessage>(TimeSpan.FromDays(7));
settings.SetSlidingExpiration<IMessage>(TimeSpan.FromDays(7));
});
services.AddSingleton<IConfigurationBuilder, ConfigurationBuilder>(); services.AddTransient<IConfigurationBuilder, ConfigurationBuilder>();
services.Configure<DiscordGatewayClientOptions>(
options => options.Intents |= GatewayIntents.MessageContents);
} }
).ConfigureLogging( ).ConfigureLogging(
c => c.AddConsole() c => c.AddConsole()
@ -60,19 +72,6 @@ public class Boyfriend {
} }
public static string GetLocalized(string key) { public static string GetLocalized(string key) {
var propertyName = key; return Messages.ResourceManager.GetString(key, Messages.Culture) ?? key;
key = $"{Messages.Culture}/{key}";
if (ReflectionMessageCache.TryGetValue(key, out var cached)) return cached;
var toReturn =
typeof(Messages).GetProperty(propertyName, BindingFlags.NonPublic | BindingFlags.Static)?.GetValue(null)
?.ToString();
if (toReturn is null) {
Logger.LogError("Could not find localized property: {Name}", propertyName);
return key;
}
ReflectionMessageCache.Add(key, toReturn);
return toReturn;
} }
} }

View file

@ -26,10 +26,19 @@
<ItemGroup> <ItemGroup>
<Compile Remove="old\**"/> <Compile Remove="old\**"/>
<Compile Update="Messages.Designer.cs">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
<DependentUpon>Messages.resx</DependentUpon>
</Compile>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<EmbeddedResource Remove="old\**"/> <EmbeddedResource Remove="old\**"/>
<EmbeddedResource Update="Messages.resx.bak">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>Messages.Designer.cs</LastGenOutput>
</EmbeddedResource>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View file

@ -1,31 +1,121 @@
using System.Drawing;
using Microsoft.Extensions.Logging;
using Remora.Discord.API.Abstractions.Gateway.Events; using Remora.Discord.API.Abstractions.Gateway.Events;
using Remora.Discord.API.Abstractions.Objects;
using Remora.Discord.API.Abstractions.Rest; using Remora.Discord.API.Abstractions.Rest;
using Remora.Discord.Caching;
using Remora.Discord.Caching.Services;
using Remora.Discord.Extensions.Embeds;
using Remora.Discord.Extensions.Formatting;
using Remora.Discord.Gateway.Responders; using Remora.Discord.Gateway.Responders;
using Remora.Results; using Remora.Results;
// ReSharper disable UnusedType.Global
namespace Boyfriend; namespace Boyfriend;
public class ReadyResponder : IResponder<IGuildCreate> { public class GuildCreateResponder : IResponder<IGuildCreate> {
private readonly IDiscordRestChannelAPI _channelApi; private readonly IDiscordRestChannelAPI _channelApi;
private readonly IDiscordRestUserAPI _userApi;
public ReadyResponder(IDiscordRestChannelAPI channelApi) { public GuildCreateResponder(IDiscordRestChannelAPI channelApi, IDiscordRestUserAPI userApi) {
_channelApi = channelApi; _channelApi = channelApi;
_userApi = userApi;
} }
public async Task<Result> RespondAsync(IGuildCreate gatewayEvent, CancellationToken ct = default) { public async Task<Result> RespondAsync(IGuildCreate gatewayEvent, CancellationToken ct = default) {
if (!gatewayEvent.Guild.IsT0) return Result.FromSuccess(); // is IAvailableGuild if (!gatewayEvent.Guild.IsT0) return Result.FromSuccess(); // is IAvailableGuild
var guild = gatewayEvent.Guild.AsT0; var guild = gatewayEvent.Guild.AsT0;
if (guild.GetConfigBool("SendReadyMessages").IsDefined(out var enabled) Boyfriend.Logger.LogInformation("Joined guild \"{Name}\"", guild.Name);
&& enabled
&& guild.GetChannel("PrivateFeedbackChannel").IsDefined(out var channel)) { var channelResult = guild.ID.GetChannel("PrivateFeedbackChannel");
if (!channelResult.IsDefined(out var channel)) return Result.FromSuccess();
var currentUserResult = await _userApi.GetCurrentUserAsync(ct);
if (!currentUserResult.IsDefined(out var currentUser)) return Result.FromError(currentUserResult);
if (guild.GetConfigBool("ReceiveStartupMessages").IsDefined(out var shouldSendStartupMessage)
&& shouldSendStartupMessage) {
Messages.Culture = guild.GetCulture(); Messages.Culture = guild.GetCulture();
var i = Random.Shared.Next(1, 4); var i = Random.Shared.Next(1, 4);
var embed = new EmbedBuilder()
.WithTitle(Boyfriend.GetLocalized($"Beep{i}"))
.WithDescription(Messages.Ready)
.WithUserFooter(currentUser)
.WithCurrentTimestamp()
.WithColour(Color.Aqua)
.Build();
if (!embed.IsDefined(out var built)) return Result.FromError(embed);
return (Result)await _channelApi.CreateMessageAsync( return (Result)await _channelApi.CreateMessageAsync(
channel.ID, string.Format(Messages.Ready, Boyfriend.GetLocalized($"Beep{i}")), ct: ct); channel, embeds: new[] { built }!, ct: ct);
} }
return Result.FromSuccess(); return Result.FromSuccess();
} }
} }
public class MessageDeletedResponder : IResponder<IMessageDelete> {
private readonly IDiscordRestAuditLogAPI _auditLogApi;
private readonly CacheService _cacheService;
private readonly IDiscordRestChannelAPI _channelApi;
private readonly IDiscordRestUserAPI _userApi;
public MessageDeletedResponder(
IDiscordRestChannelAPI channelApi, IDiscordRestUserAPI userApi, CacheService cacheService,
IDiscordRestAuditLogAPI auditLogApi) {
_channelApi = channelApi;
_userApi = userApi;
_cacheService = cacheService;
_auditLogApi = auditLogApi;
}
public async Task<Result> RespondAsync(IMessageDelete gatewayEvent, CancellationToken ct = default) {
if (!gatewayEvent.GuildID.IsDefined(out var guildId)) return Result.FromSuccess();
var channelResult = guildId.GetChannel("PrivateFeedbackChannel");
if (!channelResult.IsDefined(out var channel)) return Result.FromSuccess();
var messageResult = await _cacheService.TryGetValueAsync<IMessage>(
new KeyHelpers.MessageCacheKey(gatewayEvent.ChannelID, gatewayEvent.ID), ct);
if (messageResult.IsDefined(out var message)) {
var auditLogResult = await _auditLogApi.GetGuildAuditLogAsync(
guildId, actionType: AuditLogEvent.MessageDelete, limit: 1, ct: ct);
if (!auditLogResult.IsDefined(out var auditLogPage)) return Result.FromError(auditLogResult);
var auditLog = auditLogPage.AuditLogEntries.Single();
if (!auditLog.Options.IsDefined(out var options))
return Result.FromError(new ArgumentNullError(nameof(auditLog.Options)));
var user = message.Author;
if (options.ChannelID == gatewayEvent.ChannelID
&& DateTimeOffset.UtcNow.Subtract(auditLog.ID.Timestamp).TotalSeconds <= 2) {
var userResult = await _userApi.GetUserAsync(auditLog.UserID!.Value, ct);
if (!userResult.IsDefined(out user)) return Result.FromError(userResult);
}
var embed = new EmbedBuilder()
.WithAuthor(string.Format(Messages.CachedMessageDeleted, message.Author))
.WithTitle(
message.Author,
string.Format(
Messages.CachedMessageDeleted,
$"{message.Author.Username}#{message.Author.Discriminator:0000}"))
.WithDescription(Markdown.BlockCode(message.Content.SanitizeForBlockCode()))
.WithActionFooter(user)
.WithTimestamp(message.Timestamp)
.WithColour(Color.Crimson)
.Build();
if (!embed.IsDefined(out var built)) return Result.FromError(embed);
return (Result)await _channelApi.CreateMessageAsync(
channel, embeds: new[] { built }, allowedMentions: Boyfriend.NoMentions, ct: ct);
}
return (Result)messageResult;
}
}

View file

@ -1,7 +1,10 @@
using System.Globalization; using System.Globalization;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Remora.Discord.API.Abstractions.Gateway.Events; using Remora.Discord.API;
using Remora.Discord.API.Abstractions.Objects; using Remora.Discord.API.Abstractions.Objects;
using Remora.Discord.API.Objects;
using Remora.Discord.Extensions.Embeds;
using Remora.Rest.Core;
using Remora.Results; using Remora.Results;
namespace Boyfriend; namespace Boyfriend;
@ -18,18 +21,49 @@ public static class Extensions {
return value is not null ? Result<bool>.FromSuccess(value.Value) : Result<bool>.FromError(new NotFoundError()); return value is not null ? Result<bool>.FromSuccess(value.Value) : Result<bool>.FromError(new NotFoundError());
} }
public static Result<IChannel> GetChannel(this IGuildCreate.IAvailableGuild guild, string key) { public static Result<Snowflake> GetChannel(this Snowflake guildId, string key) {
var value = Boyfriend.GuildConfiguration.GetValue<ulong?>($"GuildConfigs:{guild.ID}:{key}"); var value = Boyfriend.GuildConfiguration.GetValue<ulong?>($"GuildConfigs:{guildId}:{key}");
if (value is null) return Result<IChannel>.FromError(new NotFoundError()); return value is not null
? Result<Snowflake>.FromSuccess(DiscordSnowflake.New(value.Value))
var match = guild.Channels.SingleOrDefault(channel => channel!.ID.Equals(value.Value), null); : Result<Snowflake>.FromError(new NotFoundError());
return match is not null
? Result<IChannel>.FromSuccess(match)
: Result<IChannel>.FromError(new NotFoundError());
} }
public static CultureInfo GetCulture(this IGuild guild) { public static CultureInfo GetCulture(this IGuild guild) {
var value = Boyfriend.GuildConfiguration.GetValue<string?>($"GuildConfigs:{guild.ID}:Language"); var value = Boyfriend.GuildConfiguration.GetValue<string?>($"GuildConfigs:{guild.ID}:Language");
return value is not null ? CultureInfoCache[value] : CultureInfoCache["en"]; return value is not null ? CultureInfoCache[value] : CultureInfoCache["en"];
} }
public static EmbedBuilder WithUserFooter(this EmbedBuilder builder, IUser user) {
var avatarUrlResult = CDN.GetUserAvatarUrl(user, imageSize: 256);
var avatarUrl = avatarUrlResult.IsSuccess
? avatarUrlResult.Entity.AbsoluteUri
: CDN.GetDefaultUserAvatarUrl(user, imageSize: 256).Entity.AbsoluteUri;
return builder.WithFooter(new EmbedFooter($"{user.Username}#{user.Discriminator:0000}", avatarUrl));
}
public static EmbedBuilder WithActionFooter(this EmbedBuilder builder, IUser user) {
var avatarUrlResult = CDN.GetUserAvatarUrl(user, imageSize: 256);
var avatarUrl = avatarUrlResult.IsSuccess
? avatarUrlResult.Entity.AbsoluteUri
: CDN.GetDefaultUserAvatarUrl(user, imageSize: 256).Entity.AbsoluteUri;
return builder.WithFooter(
new EmbedFooter($"{Messages.IssuedBy}:\n{user.Username}#{user.Discriminator:0000}", avatarUrl));
}
public static EmbedBuilder WithTitle(this EmbedBuilder builder, IUser avatarSource, string text) {
var avatarUrlResult = CDN.GetUserAvatarUrl(avatarSource, imageSize: 256);
var avatarUrl = avatarUrlResult.IsSuccess
? avatarUrlResult.Entity
: CDN.GetDefaultUserAvatarUrl(avatarSource, imageSize: 256).Entity;
builder.Author = new EmbedAuthorBuilder(text, iconUrl: avatarUrl.AbsoluteUri);
return builder;
}
public static string SanitizeForBlockCode(this string s) {
return s.Replace("```", "```");
}
} }

1691
Messages.Designer.cs generated

File diff suppressed because it is too large Load diff

View file

@ -112,16 +112,20 @@
<value>2.0</value> <value>2.0</value>
</resheader> </resheader>
<resheader name="reader"> <resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral,
PublicKeyToken=b77a5c561934e089
</value>
</resheader> </resheader>
<resheader name="writer"> <resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral,
PublicKeyToken=b77a5c561934e089
</value>
</resheader> </resheader>
<data name="Ready" xml:space="preserve"> <data name="Ready" xml:space="preserve">
<value>{0}I'm ready!</value> <value>I'm ready!</value>
</data> </data>
<data name="CachedMessageDeleted" xml:space="preserve"> <data name="CachedMessageDeleted" xml:space="preserve">
<value>Deleted message from {0} in channel {1}: {2}</value> <value>Deleted message by {0}:</value>
</data> </data>
<data name="CachedMessageCleared" xml:space="preserve"> <data name="CachedMessageCleared" xml:space="preserve">
<value>Cleared message from {0} in channel {1}: {2}</value> <value>Cleared message from {0} in channel {1}: {2}</value>
@ -474,4 +478,7 @@
<data name="InvalidRemindIn" xml:space="preserve"> <data name="InvalidRemindIn" xml:space="preserve">
<value>You need to specify when I should send you the reminder!</value> <value>You need to specify when I should send you the reminder!</value>
</data> </data>
<data name="IssuedBy" xml:space="preserve">
<value>Issued by</value>
</data>
</root> </root>

View file

@ -112,16 +112,20 @@
<value>2.0</value> <value>2.0</value>
</resheader> </resheader>
<resheader name="reader"> <resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral,
PublicKeyToken=b77a5c561934e089
</value>
</resheader> </resheader>
<resheader name="writer"> <resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral,
PublicKeyToken=b77a5c561934e089
</value>
</resheader> </resheader>
<data name="Ready" xml:space="preserve"> <data name="Ready" xml:space="preserve">
<value>{0}Я запустился!</value> <value>Я запустился!</value>
</data> </data>
<data name="CachedMessageDeleted" xml:space="preserve"> <data name="CachedMessageDeleted" xml:space="preserve">
<value>Удалено сообщение от {0} в канале {1}: {2}</value> <value>Сообщение {0} удалено:</value>
</data> </data>
<data name="CachedMessageCleared" xml:space="preserve"> <data name="CachedMessageCleared" xml:space="preserve">
<value>Очищено сообщение от {0} в канале {1}: {2}</value> <value>Очищено сообщение от {0} в канале {1}: {2}</value>
@ -474,4 +478,7 @@
<data name="InvalidRemindIn" xml:space="preserve"> <data name="InvalidRemindIn" xml:space="preserve">
<value>Нужно указать время, через которое придёт напоминание!</value> <value>Нужно указать время, через которое придёт напоминание!</value>
</data> </data>
<data name="IssuedBy" xml:space="preserve">
<value>Ответственный</value>
</data>
</root> </root>

View file

@ -112,16 +112,20 @@
<value>2.0</value> <value>2.0</value>
</resheader> </resheader>
<resheader name="reader"> <resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral,
PublicKeyToken=b77a5c561934e089
</value>
</resheader> </resheader>
<resheader name="writer"> <resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral,
PublicKeyToken=b77a5c561934e089
</value>
</resheader> </resheader>
<data name="Ready" xml:space="preserve"> <data name="Ready" xml:space="preserve">
<value>{0}я родился!</value> <value>я родился!</value>
</data> </data>
<data name="CachedMessageDeleted" xml:space="preserve"> <data name="CachedMessageDeleted" xml:space="preserve">
<value>вырезано сообщение от {0} в канале {1}: {2}</value> <value>сообщение {0} вырезано:</value>
</data> </data>
<data name="CachedMessageCleared" xml:space="preserve"> <data name="CachedMessageCleared" xml:space="preserve">
<value>вырезано сообщение (используя `!clear`) от {0} в канале {1}: {2}</value> <value>вырезано сообщение (используя `!clear`) от {0} в канале {1}: {2}</value>
@ -474,4 +478,7 @@
<data name="InvalidRemindIn" xml:space="preserve"> <data name="InvalidRemindIn" xml:space="preserve">
<value>шизоид у меня на часах такого нету</value> <value>шизоид у меня на часах такого нету</value>
</data> </data>
<data name="IssuedBy" xml:space="preserve">
<value>ответственный</value>
</data>
</root> </root>