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

Remora.Discord part 3 out of ∞

Signed-off-by: Octol1ttle <l1ttleofficial@outlook.com>
This commit is contained in:
Octol1ttle 2023-05-18 10:28:25 +05:00
parent c4835a4e78
commit 67a15f3822
Signed by: Octol1ttle
GPG key ID: B77C34313AEE1FFF
8 changed files with 322 additions and 198 deletions

View file

@ -62,7 +62,7 @@ public class Boyfriend {
services.AddTransient<IConfigurationBuilder, ConfigurationBuilder>(); services.AddTransient<IConfigurationBuilder, ConfigurationBuilder>();
services.Configure<DiscordGatewayClientOptions>( services.Configure<DiscordGatewayClientOptions>(
options => options.Intents |= GatewayIntents.MessageContents); options => options.Intents |= GatewayIntents.MessageContents | GatewayIntents.GuildMembers);
} }
).ConfigureLogging( ).ConfigureLogging(
c => c.AddConsole() c => c.AddConsole()

View file

@ -19,11 +19,13 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="DiffPlex" Version="1.7.1"/>
<PackageReference Include="Humanizer.Core.ru" Version="2.14.1"/> <PackageReference Include="Humanizer.Core.ru" Version="2.14.1"/>
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0-preview.3.23174.8"/> <PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0-preview.3.23174.8"/>
<PackageReference Include="Remora.Discord" Version="2023.3.0"/> <PackageReference Include="Remora.Discord" Version="2023.3.0"/>
</ItemGroup> </ItemGroup>
<!-- TODO: remove this when done -->
<ItemGroup> <ItemGroup>
<Compile Remove="old\**"/> <Compile Remove="old\**"/>
<Compile Update="Messages.Designer.cs"> <Compile Update="Messages.Designer.cs">
@ -35,7 +37,7 @@
<ItemGroup> <ItemGroup>
<EmbeddedResource Remove="old\**"/> <EmbeddedResource Remove="old\**"/>
<EmbeddedResource Update="Messages.resx.bak"> <EmbeddedResource Update="Messages.resx">
<Generator>ResXFileCodeGenerator</Generator> <Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>Messages.Designer.cs</LastGenOutput> <LastGenOutput>Messages.Designer.cs</LastGenOutput>
</EmbeddedResource> </EmbeddedResource>

View file

@ -1,4 +1,6 @@
using System.Drawing; using System.Drawing;
using DiffPlex;
using DiffPlex.DiffBuilder;
using Microsoft.Extensions.Logging; 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.Objects;
@ -29,15 +31,16 @@ public class GuildCreateResponder : IResponder<IGuildCreate> {
var guild = gatewayEvent.Guild.AsT0; var guild = gatewayEvent.Guild.AsT0;
Boyfriend.Logger.LogInformation("Joined guild \"{Name}\"", guild.Name); Boyfriend.Logger.LogInformation("Joined guild \"{Name}\"", guild.Name);
var channelResult = guild.ID.GetChannel("PrivateFeedbackChannel"); var channelResult = guild.ID.GetConfigChannel("PrivateFeedbackChannel");
if (!channelResult.IsDefined(out var channel)) return Result.FromSuccess(); if (!channelResult.IsDefined(out var channel)) return Result.FromSuccess();
var currentUserResult = await _userApi.GetCurrentUserAsync(ct); var currentUserResult = await _userApi.GetCurrentUserAsync(ct);
if (!currentUserResult.IsDefined(out var currentUser)) return Result.FromError(currentUserResult); if (!currentUserResult.IsDefined(out var currentUser)) return Result.FromError(currentUserResult);
if (guild.GetConfigBool("ReceiveStartupMessages").IsDefined(out var shouldSendStartupMessage) if (!guild.GetConfigBool("ReceiveStartupMessages").IsDefined(out var shouldSendStartupMessage)
&& shouldSendStartupMessage) { || !shouldSendStartupMessage) return Result.FromSuccess();
Messages.Culture = guild.GetCulture();
Messages.Culture = guild.ID.GetGuildCulture();
var i = Random.Shared.Next(1, 4); var i = Random.Shared.Next(1, 4);
var embed = new EmbedBuilder() var embed = new EmbedBuilder()
@ -53,35 +56,31 @@ public class GuildCreateResponder : IResponder<IGuildCreate> {
return (Result)await _channelApi.CreateMessageAsync( return (Result)await _channelApi.CreateMessageAsync(
channel, embeds: new[] { built }!, ct: ct); channel, embeds: new[] { built }!, ct: ct);
} }
return Result.FromSuccess();
}
} }
public class MessageDeletedResponder : IResponder<IMessageDelete> { public class MessageDeletedResponder : IResponder<IMessageDelete> {
private readonly IDiscordRestAuditLogAPI _auditLogApi; private readonly IDiscordRestAuditLogAPI _auditLogApi;
private readonly CacheService _cacheService; private readonly CacheService _cacheService;
private readonly IDiscordRestChannelAPI _channelApi; private readonly IDiscordRestChannelAPI _channelApi;
private readonly IDiscordRestUserAPI _userApi;
public MessageDeletedResponder( public MessageDeletedResponder(
IDiscordRestChannelAPI channelApi, IDiscordRestUserAPI userApi, CacheService cacheService, IDiscordRestAuditLogAPI auditLogApi, CacheService cacheService, IDiscordRestChannelAPI channelApi) {
IDiscordRestAuditLogAPI auditLogApi) {
_channelApi = channelApi;
_userApi = userApi;
_cacheService = cacheService;
_auditLogApi = auditLogApi; _auditLogApi = auditLogApi;
_cacheService = cacheService;
_channelApi = channelApi;
} }
public async Task<Result> RespondAsync(IMessageDelete gatewayEvent, CancellationToken ct = default) { public async Task<Result> RespondAsync(IMessageDelete gatewayEvent, CancellationToken ct = default) {
if (!gatewayEvent.GuildID.IsDefined(out var guildId)) return Result.FromSuccess(); if (!gatewayEvent.GuildID.IsDefined(out var guildId)) return Result.FromSuccess();
var channelResult = guildId.GetChannel("PrivateFeedbackChannel"); var channelResult = guildId.GetConfigChannel("PrivateFeedbackChannel");
if (!channelResult.IsDefined(out var channel)) return Result.FromSuccess(); if (!channelResult.IsDefined(out var logChannel)) return Result.FromSuccess();
var messageResult = await _cacheService.TryGetValueAsync<IMessage>( var messageResult = await _cacheService.TryGetValueAsync<IMessage>(
new KeyHelpers.MessageCacheKey(gatewayEvent.ChannelID, gatewayEvent.ID), ct); new KeyHelpers.MessageCacheKey(gatewayEvent.ChannelID, gatewayEvent.ID), ct);
if (messageResult.IsDefined(out var message)) { if (!messageResult.IsDefined(out var message)) return Result.FromError(messageResult);
if (string.IsNullOrWhiteSpace(message.Content)) return Result.FromSuccess();
var auditLogResult = await _auditLogApi.GetGuildAuditLogAsync( var auditLogResult = await _auditLogApi.GetGuildAuditLogAsync(
guildId, actionType: AuditLogEvent.MessageDelete, limit: 1, ct: ct); guildId, actionType: AuditLogEvent.MessageDelete, limit: 1, ct: ct);
if (!auditLogResult.IsDefined(out var auditLogPage)) return Result.FromError(auditLogResult); if (!auditLogResult.IsDefined(out var auditLogPage)) return Result.FromError(auditLogResult);
@ -93,29 +92,121 @@ public class MessageDeletedResponder : IResponder<IMessageDelete> {
var user = message.Author; var user = message.Author;
if (options.ChannelID == gatewayEvent.ChannelID if (options.ChannelID == gatewayEvent.ChannelID
&& DateTimeOffset.UtcNow.Subtract(auditLog.ID.Timestamp).TotalSeconds <= 2) { && DateTimeOffset.UtcNow.Subtract(auditLog.ID.Timestamp).TotalSeconds <= 2) {
var userResult = await _userApi.GetUserAsync(auditLog.UserID!.Value, ct); var userResult = await _cacheService.TryGetValueAsync<IUser>(
new KeyHelpers.UserCacheKey(auditLog.UserID!.Value), ct);
if (!userResult.IsDefined(out user)) return Result.FromError(userResult); if (!userResult.IsDefined(out user)) return Result.FromError(userResult);
} }
Messages.Culture = guildId.GetGuildCulture();
var embed = new EmbedBuilder() var embed = new EmbedBuilder()
.WithAuthor(string.Format(Messages.CachedMessageDeleted, message.Author)) .WithSmallTitle(
.WithTitle(
message.Author, message.Author,
string.Format( string.Format(
Messages.CachedMessageDeleted, Messages.CachedMessageDeleted,
$"{message.Author.Username}#{message.Author.Discriminator:0000}")) message.Author.GetTag()))
.WithDescription(Markdown.BlockCode(message.Content.SanitizeForBlockCode())) .WithDescription(
$"{Mention.Channel(gatewayEvent.ChannelID)}\n{Markdown.BlockCode(message.Content.SanitizeForBlockCode())}")
.WithActionFooter(user) .WithActionFooter(user)
.WithTimestamp(message.Timestamp) .WithTimestamp(message.Timestamp)
.WithColour(Color.Crimson) .WithColour(Color.Crimson)
.Build(); .Build();
if (!embed.IsDefined(out var built)) return Result.FromError(embed);
return (Result)await _channelApi.CreateMessageAsync(
logChannel, embeds: new[] { built }, allowedMentions: Boyfriend.NoMentions, ct: ct);
}
}
public class MessageEditedResponder : IResponder<IMessageUpdate> {
private readonly CacheService _cacheService;
private readonly IDiscordRestChannelAPI _channelApi;
public MessageEditedResponder(CacheService cacheService, IDiscordRestChannelAPI channelApi) {
_cacheService = cacheService;
_channelApi = channelApi;
}
public async Task<Result> RespondAsync(IMessageUpdate gatewayEvent, CancellationToken ct = default) {
if (!gatewayEvent.GuildID.IsDefined(out var guildId)) return Result.FromSuccess();
if (!gatewayEvent.ChannelID.IsDefined(out var channelId))
return Result.FromError(new ArgumentNullError(nameof(gatewayEvent.ChannelID)));
if (!gatewayEvent.ID.IsDefined(out var messageId))
return Result.FromError(new ArgumentNullError(nameof(gatewayEvent.ID)));
if (!gatewayEvent.Content.IsDefined(out var newContent))
return Result.FromError(new ArgumentNullError(nameof(gatewayEvent.Content)));
if (!gatewayEvent.EditedTimestamp.IsDefined(out var timestamp))
return Result.FromError(new ArgumentNullError(nameof(gatewayEvent.EditedTimestamp)));
var messageResult = await _cacheService.TryGetValueAsync<IMessage>(
new KeyHelpers.MessageCacheKey(channelId, messageId), ct);
if (!messageResult.IsDefined(out var message)) return Result.FromError(messageResult);
if (string.IsNullOrWhiteSpace(message.Content)
|| string.IsNullOrWhiteSpace(newContent)
|| message.Content == newContent) return Result.FromSuccess();
var logChannelResult = guildId.GetConfigChannel("PrivateFeedbackChannel");
if (!logChannelResult.IsDefined(out var logChannel)) return Result.FromSuccess();
var currentUserResult = await _cacheService.TryGetValueAsync<IUser>(
new KeyHelpers.CurrentUserCacheKey(), ct);
if (!currentUserResult.IsDefined(out var currentUser)) return Result.FromError(currentUserResult);
var diff = new SideBySideDiffBuilder(Differ.Instance).BuildDiffModel(message.Content, newContent, true, true);
Messages.Culture = guildId.GetGuildCulture();
var embed = new EmbedBuilder()
.WithSmallTitle(
message.Author,
string.Format(Messages.CachedMessageEdited, message.Author.GetTag()),
$"https://discord.com/channels/{guildId}/{channelId}/{messageId}")
.WithDescription($"{Mention.Channel(message.ChannelID)}\n{diff.AsMarkdown()}")
.WithUserFooter(currentUser)
.WithTimestamp(timestamp.Value)
.WithColour(Color.Gold)
.Build();
if (!embed.IsDefined(out var built)) return Result.FromError(embed);
return (Result)await _channelApi.CreateMessageAsync(
logChannel, embeds: new[] { built }, allowedMentions: Boyfriend.NoMentions, ct: ct);
}
}
public class GuildMemberAddResponder : IResponder<IGuildMemberAdd> {
private readonly CacheService _cacheService;
private readonly IDiscordRestChannelAPI _channelApi;
public GuildMemberAddResponder(CacheService cacheService, IDiscordRestChannelAPI channelApi) {
_cacheService = cacheService;
_channelApi = channelApi;
}
public async Task<Result> RespondAsync(IGuildMemberAdd gatewayEvent, CancellationToken ct = default) {
if (!gatewayEvent.GuildID.GetConfigString("WelcomeMessage").IsDefined(out var welcomeMessage)
|| welcomeMessage is "off" or "disable" or "disabled")
return Result.FromSuccess();
if (welcomeMessage is "default" or "reset") {
Messages.Culture = gatewayEvent.GuildID.GetGuildCulture();
welcomeMessage = Messages.DefaultWelcomeMessage;
}
if (!gatewayEvent.GuildID.GetConfigChannel("PublicFeedbackChannel").IsDefined(out var channel))
return Result.FromSuccess();
if (!gatewayEvent.User.IsDefined(out var user))
return Result.FromError(new ArgumentNullError(nameof(gatewayEvent.User)));
var guildResult = await _cacheService.TryGetValueAsync<IGuild>(
new KeyHelpers.GuildCacheKey(gatewayEvent.GuildID), ct);
if (!guildResult.IsDefined(out var guild)) return Result.FromError(guildResult);
var embed = new EmbedBuilder()
.WithSmallTitle(user, string.Format(welcomeMessage, user.GetTag(), guild.Name))
.WithGuildFooter(guild)
.WithTimestamp(gatewayEvent.JoinedAt)
.WithColour(Color.LawnGreen)
.Build();
if (!embed.IsDefined(out var built)) return Result.FromError(embed); if (!embed.IsDefined(out var built)) return Result.FromError(embed);
return (Result)await _channelApi.CreateMessageAsync( return (Result)await _channelApi.CreateMessageAsync(
channel, embeds: new[] { built }, allowedMentions: Boyfriend.NoMentions, ct: ct); channel, embeds: new[] { built }, allowedMentions: Boyfriend.NoMentions, ct: ct);
} }
return (Result)messageResult;
}
} }

View file

@ -1,9 +1,12 @@
using System.Globalization; using System.Globalization;
using System.Text;
using DiffPlex.DiffBuilder.Model;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Remora.Discord.API; using Remora.Discord.API;
using Remora.Discord.API.Abstractions.Objects; using Remora.Discord.API.Abstractions.Objects;
using Remora.Discord.API.Objects; using Remora.Discord.API.Objects;
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;
@ -21,15 +24,20 @@ 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<Snowflake> GetChannel(this Snowflake guildId, string key) { public static Result<Snowflake> GetConfigChannel(this Snowflake guildId, string key) {
var value = Boyfriend.GuildConfiguration.GetValue<ulong?>($"GuildConfigs:{guildId}:{key}"); var value = Boyfriend.GuildConfiguration.GetValue<ulong?>($"GuildConfigs:{guildId}:{key}");
return value is not null return value is not null
? Result<Snowflake>.FromSuccess(DiscordSnowflake.New(value.Value)) ? Result<Snowflake>.FromSuccess(DiscordSnowflake.New(value.Value))
: Result<Snowflake>.FromError(new NotFoundError()); : Result<Snowflake>.FromError(new NotFoundError());
} }
public static CultureInfo GetCulture(this IGuild guild) { public static Result<string> GetConfigString(this Snowflake guildId, string key) {
var value = Boyfriend.GuildConfiguration.GetValue<string?>($"GuildConfigs:{guild.ID}:Language"); var value = Boyfriend.GuildConfiguration.GetValue<string?>($"GuildConfigs:{guildId}:{key}");
return value is not null ? Result<string>.FromSuccess(value) : Result<string>.FromError(new NotFoundError());
}
public static CultureInfo GetGuildCulture(this Snowflake guildId) {
var value = Boyfriend.GuildConfiguration.GetValue<string?>($"GuildConfigs:{guildId}:Language");
return value is not null ? CultureInfoCache[value] : CultureInfoCache["en"]; return value is not null ? CultureInfoCache[value] : CultureInfoCache["en"];
} }
@ -39,7 +47,7 @@ public static class Extensions {
? avatarUrlResult.Entity.AbsoluteUri ? avatarUrlResult.Entity.AbsoluteUri
: CDN.GetDefaultUserAvatarUrl(user, imageSize: 256).Entity.AbsoluteUri; : CDN.GetDefaultUserAvatarUrl(user, imageSize: 256).Entity.AbsoluteUri;
return builder.WithFooter(new EmbedFooter($"{user.Username}#{user.Discriminator:0000}", avatarUrl)); return builder.WithFooter(new EmbedFooter(user.GetTag(), avatarUrl));
} }
public static EmbedBuilder WithActionFooter(this EmbedBuilder builder, IUser user) { public static EmbedBuilder WithActionFooter(this EmbedBuilder builder, IUser user) {
@ -49,21 +57,44 @@ public static class Extensions {
: CDN.GetDefaultUserAvatarUrl(user, imageSize: 256).Entity.AbsoluteUri; : CDN.GetDefaultUserAvatarUrl(user, imageSize: 256).Entity.AbsoluteUri;
return builder.WithFooter( return builder.WithFooter(
new EmbedFooter($"{Messages.IssuedBy}:\n{user.Username}#{user.Discriminator:0000}", avatarUrl)); new EmbedFooter($"{Messages.IssuedBy}:\n{user.GetTag()}", avatarUrl));
} }
public static EmbedBuilder WithTitle(this EmbedBuilder builder, IUser avatarSource, string text) { public static EmbedBuilder WithSmallTitle(
this EmbedBuilder builder, IUser avatarSource, string text, string? url = default) {
var avatarUrlResult = CDN.GetUserAvatarUrl(avatarSource, imageSize: 256); var avatarUrlResult = CDN.GetUserAvatarUrl(avatarSource, imageSize: 256);
var avatarUrl = avatarUrlResult.IsSuccess var avatarUrl = avatarUrlResult.IsSuccess
? avatarUrlResult.Entity ? avatarUrlResult.Entity
: CDN.GetDefaultUserAvatarUrl(avatarSource, imageSize: 256).Entity; : CDN.GetDefaultUserAvatarUrl(avatarSource, imageSize: 256).Entity;
builder.Author = new EmbedAuthorBuilder(text, iconUrl: avatarUrl.AbsoluteUri); builder.Author = new EmbedAuthorBuilder(text, url, avatarUrl.AbsoluteUri);
return builder; return builder;
} }
public static EmbedBuilder WithGuildFooter(this EmbedBuilder builder, IGuild guild) {
var iconUrlResult = CDN.GetGuildIconUrl(guild, imageSize: 256);
var iconUrl = iconUrlResult.IsSuccess
? iconUrlResult.Entity.AbsoluteUri
: default(Optional<string>);
return builder.WithFooter(new EmbedFooter(guild.Name, iconUrl));
}
public static string SanitizeForBlockCode(this string s) { public static string SanitizeForBlockCode(this string s) {
return s.Replace("```", "```"); return s.Replace("```", "```");
} }
public static string AsMarkdown(this SideBySideDiffModel model) {
var builder = new StringBuilder();
foreach (var line in model.OldText.Lines.Where(piece => !string.IsNullOrWhiteSpace(piece.Text)))
builder.Append("-- ").AppendLine(line.Text);
foreach (var line in model.NewText.Lines) builder.Append("++ ").AppendLine(line.Text);
return Markdown.BlockCode(builder.ToString().SanitizeForBlockCode(), "diff");
}
public static string GetTag(this IUser user) {
return $"{user.Username}#{user.Discriminator:0000}";
}
} }

View file

@ -131,7 +131,7 @@
<value>Cleared message from {0} in channel {1}: {2}</value> <value>Cleared message from {0} in channel {1}: {2}</value>
</data> </data>
<data name="CachedMessageEdited" xml:space="preserve"> <data name="CachedMessageEdited" xml:space="preserve">
<value>Edited message in channel {0}: {1} -&gt; {2}</value> <value>Edited message by {0}:</value>
</data> </data>
<data name="DefaultWelcomeMessage" xml:space="preserve"> <data name="DefaultWelcomeMessage" xml:space="preserve">
<value>{0}, welcome to {1}</value> <value>{0}, welcome to {1}</value>

View file

@ -131,7 +131,7 @@
<value>Очищено сообщение от {0} в канале {1}: {2}</value> <value>Очищено сообщение от {0} в канале {1}: {2}</value>
</data> </data>
<data name="CachedMessageEdited" xml:space="preserve"> <data name="CachedMessageEdited" xml:space="preserve">
<value>Отредактировано сообщение в канале {0}: {1} -&gt; {2}</value> <value>Сообщение {0} отредактировано:</value>
</data> </data>
<data name="DefaultWelcomeMessage" xml:space="preserve"> <data name="DefaultWelcomeMessage" xml:space="preserve">
<value>{0}, добро пожаловать на сервер {1}</value> <value>{0}, добро пожаловать на сервер {1}</value>

View file

@ -131,7 +131,7 @@
<value>вырезано сообщение (используя `!clear`) от {0} в канале {1}: {2}</value> <value>вырезано сообщение (используя `!clear`) от {0} в канале {1}: {2}</value>
</data> </data>
<data name="CachedMessageEdited" xml:space="preserve"> <data name="CachedMessageEdited" xml:space="preserve">
<value>переделано сообщение от {0}: {1} -&gt; {2}</value> <value>сообщение {0} переделано:</value>
</data> </data>
<data name="DefaultWelcomeMessage" xml:space="preserve"> <data name="DefaultWelcomeMessage" xml:space="preserve">
<value>{0}, добро пожаловать на сервер {1}</value> <value>{0}, добро пожаловать на сервер {1}</value>