1
0
Fork 1
mirror of https://github.com/TeamOctolings/Octobot.git synced 2025-04-19 16:33: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.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Remora.Discord.API.Abstractions.Gateway.Commands;
using Remora.Discord.API.Abstractions.Objects;
using Remora.Discord.API.Objects;
using Remora.Discord.Caching.Extensions;
using Remora.Discord.Caching.Services;
using Remora.Discord.Gateway;
using Remora.Discord.Gateway.Extensions;
using Remora.Discord.Hosting.Extensions;
using Remora.Rest.Core;
namespace Boyfriend;
@ -15,7 +18,8 @@ public class Boyfriend {
public static ILogger<Boyfriend> Logger = 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) {
var host = CreateHostBuilder(args).UseConsoleLifetime().Build();
@ -48,9 +52,17 @@ public class Boyfriend {
services.AddDiscordCaching();
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(
c => c.AddConsole()
@ -60,19 +72,6 @@ public class Boyfriend {
}
public static string GetLocalized(string key) {
var propertyName = 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;
return Messages.ResourceManager.GetString(key, Messages.Culture) ?? key;
}
}

View file

@ -26,10 +26,19 @@
<ItemGroup>
<Compile Remove="old\**"/>
<Compile Update="Messages.Designer.cs">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
<DependentUpon>Messages.resx</DependentUpon>
</Compile>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Remove="old\**"/>
<EmbeddedResource Update="Messages.resx.bak">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>Messages.Designer.cs</LastGenOutput>
</EmbeddedResource>
</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.Objects;
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.Results;
// ReSharper disable UnusedType.Global
namespace Boyfriend;
public class ReadyResponder : IResponder<IGuildCreate> {
public class GuildCreateResponder : IResponder<IGuildCreate> {
private readonly IDiscordRestChannelAPI _channelApi;
private readonly IDiscordRestUserAPI _userApi;
public ReadyResponder(IDiscordRestChannelAPI channelApi) {
public GuildCreateResponder(IDiscordRestChannelAPI channelApi, IDiscordRestUserAPI userApi) {
_channelApi = channelApi;
_userApi = userApi;
}
public async Task<Result> RespondAsync(IGuildCreate gatewayEvent, CancellationToken ct = default) {
if (!gatewayEvent.Guild.IsT0) return Result.FromSuccess(); // is IAvailableGuild
var guild = gatewayEvent.Guild.AsT0;
if (guild.GetConfigBool("SendReadyMessages").IsDefined(out var enabled)
&& enabled
&& guild.GetChannel("PrivateFeedbackChannel").IsDefined(out var channel)) {
Boyfriend.Logger.LogInformation("Joined guild \"{Name}\"", guild.Name);
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();
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(
channel.ID, string.Format(Messages.Ready, Boyfriend.GetLocalized($"Beep{i}")), ct: ct);
channel, embeds: new[] { built }!, ct: ct);
}
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 Microsoft.Extensions.Configuration;
using Remora.Discord.API.Abstractions.Gateway.Events;
using Remora.Discord.API;
using Remora.Discord.API.Abstractions.Objects;
using Remora.Discord.API.Objects;
using Remora.Discord.Extensions.Embeds;
using Remora.Rest.Core;
using Remora.Results;
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());
}
public static Result<IChannel> GetChannel(this IGuildCreate.IAvailableGuild guild, string key) {
var value = Boyfriend.GuildConfiguration.GetValue<ulong?>($"GuildConfigs:{guild.ID}:{key}");
if (value is null) return Result<IChannel>.FromError(new NotFoundError());
var match = guild.Channels.SingleOrDefault(channel => channel!.ID.Equals(value.Value), null);
return match is not null
? Result<IChannel>.FromSuccess(match)
: Result<IChannel>.FromError(new NotFoundError());
public static Result<Snowflake> GetChannel(this Snowflake guildId, string key) {
var value = Boyfriend.GuildConfiguration.GetValue<ulong?>($"GuildConfigs:{guildId}:{key}");
return value is not null
? Result<Snowflake>.FromSuccess(DiscordSnowflake.New(value.Value))
: Result<Snowflake>.FromError(new NotFoundError());
}
public static CultureInfo GetCulture(this IGuild guild) {
var value = Boyfriend.GuildConfiguration.GetValue<string?>($"GuildConfigs:{guild.ID}:Language");
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

@ -98,49 +98,53 @@
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
<xsd:attribute name="name" type="xsd:string" use="required"/>
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="Ready" xml:space="preserve">
<value>{0}I'm ready!</value>
</data>
<data name="CachedMessageDeleted" xml:space="preserve">
<value>Deleted message from {0} in channel {1}: {2}</value>
</data>
<data name="CachedMessageCleared" xml:space="preserve">
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral,
PublicKeyToken=b77a5c561934e089
</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral,
PublicKeyToken=b77a5c561934e089
</value>
</resheader>
<data name="Ready" xml:space="preserve">
<value>I'm ready!</value>
</data>
<data name="CachedMessageDeleted" xml:space="preserve">
<value>Deleted message by {0}:</value>
</data>
<data name="CachedMessageCleared" xml:space="preserve">
<value>Cleared message from {0} in channel {1}: {2}</value>
</data>
<data name="CachedMessageEdited" xml:space="preserve">
<data name="CachedMessageEdited" xml:space="preserve">
<value>Edited message in channel {0}: {1} -&gt; {2}</value>
</data>
<data name="DefaultWelcomeMessage" xml:space="preserve">
<data name="DefaultWelcomeMessage" xml:space="preserve">
<value>{0}, welcome to {1}</value>
</data>
<data name="Beep1" xml:space="preserve">
<value>Bah! </value>
</data>
<data name="Beep2" xml:space="preserve">
<value>Bop! </value>
</data>
<data name="Beep1" xml:space="preserve">
<value>Bah!</value>
</data>
<data name="Beep2" xml:space="preserve">
<value>Bop!</value>
</data>
<data name="Beep3" xml:space="preserve">
<value>Beep! </value>
</data>
<value>Beep!</value>
</data>
<data name="CommandNoPermissionBot" xml:space="preserve">
<value>I do not have permission to execute this command!</value>
</data>
@ -474,4 +478,7 @@
<data name="InvalidRemindIn" xml:space="preserve">
<value>You need to specify when I should send you the reminder!</value>
</data>
<data name="IssuedBy" xml:space="preserve">
<value>Issued by</value>
</data>
</root>

View file

@ -98,49 +98,53 @@
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
<xsd:attribute name="name" type="xsd:string" use="required"/>
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="Ready" xml:space="preserve">
<value>{0}Я запустился!</value>
</data>
<data name="CachedMessageDeleted" xml:space="preserve">
<value>Удалено сообщение от {0} в канале {1}: {2}</value>
</data>
<data name="CachedMessageCleared" xml:space="preserve">
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral,
PublicKeyToken=b77a5c561934e089
</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral,
PublicKeyToken=b77a5c561934e089
</value>
</resheader>
<data name="Ready" xml:space="preserve">
<value>Я запустился!</value>
</data>
<data name="CachedMessageDeleted" xml:space="preserve">
<value>Сообщение {0} удалено:</value>
</data>
<data name="CachedMessageCleared" xml:space="preserve">
<value>Очищено сообщение от {0} в канале {1}: {2}</value>
</data>
<data name="CachedMessageEdited" xml:space="preserve">
<data name="CachedMessageEdited" xml:space="preserve">
<value>Отредактировано сообщение в канале {0}: {1} -&gt; {2}</value>
</data>
<data name="DefaultWelcomeMessage" xml:space="preserve">
<data name="DefaultWelcomeMessage" xml:space="preserve">
<value>{0}, добро пожаловать на сервер {1}</value>
</data>
<data name="Beep1" xml:space="preserve">
<value>Бап! </value>
</data>
<data name="Beep2" xml:space="preserve">
<value>Боп! </value>
</data>
<data name="Beep1" xml:space="preserve">
<value>Бап!</value>
</data>
<data name="Beep2" xml:space="preserve">
<value>Боп!</value>
</data>
<data name="Beep3" xml:space="preserve">
<value>Бип! </value>
</data>
<value>Бип!</value>
</data>
<data name="CommandNoPermissionBot" xml:space="preserve">
<value>У меня недостаточно прав для выполнения этой команды!</value>
</data>
@ -465,13 +469,16 @@
<data name="SettingsAutoStartEvents" xml:space="preserve">
<value>Автоматически начинать события</value>
</data>
<data name="MissingReminderText" xml:space="preserve">
<data name="MissingReminderText" xml:space="preserve">
<value>Тебе нужно указать текст напоминания!</value>
</data>
<data name="FeedbackReminderAdded" xml:space="preserve">
<data name="FeedbackReminderAdded" xml:space="preserve">
<value>Хорошо, я упомяну тебя &lt;t:{0}:f&gt;</value>
</data>
<data name="InvalidRemindIn" xml:space="preserve">
<data name="InvalidRemindIn" xml:space="preserve">
<value>Нужно указать время, через которое придёт напоминание!</value>
</data>
<data name="IssuedBy" xml:space="preserve">
<value>Ответственный</value>
</data>
</root>

View file

@ -98,49 +98,53 @@
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
<xsd:attribute name="name" type="xsd:string" use="required"/>
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="Ready" xml:space="preserve">
<value>{0}я родился!</value>
</data>
<data name="CachedMessageDeleted" xml:space="preserve">
<value>вырезано сообщение от {0} в канале {1}: {2}</value>
</data>
<data name="CachedMessageCleared" xml:space="preserve">
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral,
PublicKeyToken=b77a5c561934e089
</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral,
PublicKeyToken=b77a5c561934e089
</value>
</resheader>
<data name="Ready" xml:space="preserve">
<value>я родился!</value>
</data>
<data name="CachedMessageDeleted" xml:space="preserve">
<value>сообщение {0} вырезано:</value>
</data>
<data name="CachedMessageCleared" xml:space="preserve">
<value>вырезано сообщение (используя `!clear`) от {0} в канале {1}: {2}</value>
</data>
<data name="CachedMessageEdited" xml:space="preserve">
<data name="CachedMessageEdited" xml:space="preserve">
<value>переделано сообщение от {0}: {1} -&gt; {2}</value>
</data>
<data name="DefaultWelcomeMessage" xml:space="preserve">
<data name="DefaultWelcomeMessage" xml:space="preserve">
<value>{0}, добро пожаловать на сервер {1}</value>
</data>
<data name="Beep1" xml:space="preserve">
<value>брах! </value>
</data>
<data name="Beep2" xml:space="preserve">
<value>брох! </value>
</data>
<data name="Beep1" xml:space="preserve">
<value>брах!</value>
</data>
<data name="Beep2" xml:space="preserve">
<value>брох!</value>
</data>
<data name="Beep3" xml:space="preserve">
<value>брух! </value>
</data>
<value>брух!</value>
</data>
<data name="CommandNoPermissionBot" xml:space="preserve">
<value>у меня прав нету, сделай что нибудь.</value>
</data>
@ -256,7 +260,7 @@
<value>{0}квест {1} начинается в {2}!</value>
</data>
<data name="SettingsFrowningFace" xml:space="preserve">
<value>оъмъомоъемъъео(((( </value>
<value>оъмъомоъемъъео((((</value>
</data>
<data name="EventCancelled" xml:space="preserve">
<value>квест {0} отменен!{1}</value>
@ -465,13 +469,16 @@
<data name="SettingsAutoStartEvents" xml:space="preserve">
<value>автоматом стартить квесты</value>
</data>
<data name="MissingReminderText" xml:space="preserve">
<data name="MissingReminderText" xml:space="preserve">
<value>для крафта напоминалки нужен текст</value>
</data>
<data name="FeedbackReminderAdded" xml:space="preserve">
<data name="FeedbackReminderAdded" xml:space="preserve">
<value>вас понял, упоминание будет &lt;t:{0}:f&gt;</value>
</data>
<data name="InvalidRemindIn" xml:space="preserve">
<data name="InvalidRemindIn" xml:space="preserve">
<value>шизоид у меня на часах такого нету</value>
</data>
<data name="IssuedBy" xml:space="preserve">
<value>ответственный</value>
</data>
</root>