2024-10-14 14:56:40 +02:00
|
|
|
// Copyright (C) 2021-present sam (starshines.gay)
|
|
|
|
|
//
|
|
|
|
|
// This program is free software: you can redistribute it and/or modify
|
|
|
|
|
// it under the terms of the GNU Affero General Public License as published
|
|
|
|
|
// by the Free Software Foundation, either version 3 of the License, or
|
|
|
|
|
// (at your option) any later version.
|
|
|
|
|
//
|
|
|
|
|
// This program is distributed in the hope that it will be useful,
|
|
|
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
|
// GNU Affero General Public License for more details.
|
|
|
|
|
//
|
|
|
|
|
// You should have received a copy of the GNU Affero General Public License
|
|
|
|
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
|
|
2024-08-20 14:11:08 +02:00
|
|
|
using Catalogger.Backend.Cache.InMemoryCache;
|
|
|
|
|
using Catalogger.Backend.Database;
|
|
|
|
|
using Catalogger.Backend.Database.Queries;
|
|
|
|
|
using Catalogger.Backend.Extensions;
|
|
|
|
|
using Catalogger.Backend.Services;
|
2024-08-20 18:18:17 +02:00
|
|
|
using Humanizer;
|
2024-08-20 14:11:08 +02:00
|
|
|
using Remora.Discord.API.Abstractions.Gateway.Events;
|
|
|
|
|
using Remora.Discord.API.Abstractions.Objects;
|
|
|
|
|
using Remora.Discord.Extensions.Embeds;
|
|
|
|
|
using Remora.Discord.Gateway.Responders;
|
|
|
|
|
using Remora.Rest.Core;
|
|
|
|
|
using Remora.Results;
|
|
|
|
|
|
|
|
|
|
namespace Catalogger.Backend.Bot.Responders.Channels;
|
|
|
|
|
|
|
|
|
|
public class ChannelUpdateResponder(
|
|
|
|
|
ILogger logger,
|
|
|
|
|
DatabaseContext db,
|
|
|
|
|
ChannelCache channelCache,
|
|
|
|
|
RoleCache roleCache,
|
|
|
|
|
UserCache userCache,
|
2024-10-09 17:35:11 +02:00
|
|
|
WebhookExecutorService webhookExecutor
|
|
|
|
|
) : IResponder<IChannelUpdate>
|
2024-08-20 14:11:08 +02:00
|
|
|
{
|
|
|
|
|
private readonly ILogger _logger = logger.ForContext<ChannelUpdateResponder>();
|
|
|
|
|
|
|
|
|
|
public async Task<Result> RespondAsync(IChannelUpdate evt, CancellationToken ct = default)
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
if (!channelCache.TryGet(evt.ID, out var oldChannel))
|
|
|
|
|
{
|
|
|
|
|
_logger.Debug("Updated channel {ChannelId} wasn't in the cache", evt.ID);
|
|
|
|
|
return Result.Success;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var guildConfig = await db.GetGuildAsync(evt.GuildID.Value, ct);
|
|
|
|
|
|
|
|
|
|
var builder = new EmbedBuilder()
|
2024-10-09 17:35:11 +02:00
|
|
|
.WithTitle(
|
|
|
|
|
evt.Type switch
|
|
|
|
|
{
|
|
|
|
|
ChannelType.GuildVoice => "Voice channel edited",
|
|
|
|
|
ChannelType.GuildCategory => "Category channel edited",
|
|
|
|
|
ChannelType.GuildAnnouncement or ChannelType.GuildText =>
|
|
|
|
|
"Text channel edited",
|
|
|
|
|
_ => "Channel edited",
|
|
|
|
|
}
|
|
|
|
|
)
|
2024-08-20 14:11:08 +02:00
|
|
|
.WithColour(DiscordUtils.Blue)
|
|
|
|
|
.WithFooter($"ID: {evt.ID} | Name: {evt.Name}")
|
|
|
|
|
.WithCurrentTimestamp();
|
|
|
|
|
|
|
|
|
|
if (oldChannel.ParentID != evt.ParentID)
|
2024-08-20 18:18:17 +02:00
|
|
|
{
|
2024-10-09 17:35:11 +02:00
|
|
|
var categoryUpdate = CategoryUpdate(
|
|
|
|
|
oldChannel.ParentID.OrDefault(),
|
|
|
|
|
evt.ParentID.OrDefault()
|
|
|
|
|
);
|
2024-08-20 18:18:17 +02:00
|
|
|
if (!string.IsNullOrWhiteSpace(categoryUpdate))
|
|
|
|
|
builder.AddField("Category", categoryUpdate);
|
|
|
|
|
}
|
2024-08-20 14:11:08 +02:00
|
|
|
|
|
|
|
|
if (oldChannel.Name != evt.Name)
|
|
|
|
|
builder.AddField("Name", $"**Before:** {oldChannel.Name}\n**After:** {evt.Name}");
|
|
|
|
|
|
|
|
|
|
if (oldChannel.Topic != evt.Topic)
|
|
|
|
|
{
|
|
|
|
|
var oldTopic = oldChannel.Topic.OrDefault() ?? "(none)";
|
|
|
|
|
var newTopic = evt.Topic.OrDefault() ?? "(none)";
|
|
|
|
|
|
|
|
|
|
var topicField = $"**Before:** {oldTopic}\n\n**After:** {newTopic}";
|
2024-10-09 17:35:11 +02:00
|
|
|
if (topicField.Length > 1000)
|
|
|
|
|
topicField = topicField[..1000] + "…";
|
2024-08-20 14:11:08 +02:00
|
|
|
|
|
|
|
|
builder.AddField("Description", topicField);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var oldOverrides = oldChannel.PermissionOverwrites.OrDefault() ?? [];
|
|
|
|
|
var newOverrides = evt.PermissionOverwrites.OrDefault() ?? [];
|
|
|
|
|
|
2024-10-09 17:35:11 +02:00
|
|
|
var addedOverrides = newOverrides
|
|
|
|
|
.Where(o => oldOverrides.All(o2 => o.ID != o2.ID))
|
|
|
|
|
.ToList();
|
|
|
|
|
var removedOverrides = oldOverrides
|
|
|
|
|
.Where(o => newOverrides.All(o2 => o.ID != o2.ID))
|
|
|
|
|
.ToList();
|
2024-08-20 14:11:08 +02:00
|
|
|
// Overrides filtered to ones that exist in both lists, but have different allow or deny values
|
2024-10-09 17:35:11 +02:00
|
|
|
var editedOverrides = newOverrides.Where(o =>
|
|
|
|
|
oldOverrides.Any(o2 =>
|
|
|
|
|
o.ID == o2.ID
|
|
|
|
|
&& (o.Allow.Value != o2.Allow.Value || o.Deny.Value != o2.Deny.Value)
|
|
|
|
|
)
|
|
|
|
|
);
|
2024-08-20 14:11:08 +02:00
|
|
|
|
2024-08-20 18:18:17 +02:00
|
|
|
if (addedOverrides.Count != 0)
|
|
|
|
|
{
|
|
|
|
|
var addedOverrideNames = new List<string>();
|
|
|
|
|
foreach (var o in addedOverrides)
|
|
|
|
|
{
|
|
|
|
|
if (o.Type is PermissionOverwriteType.Member)
|
|
|
|
|
{
|
|
|
|
|
var user = await userCache.GetUserAsync(o.ID);
|
|
|
|
|
addedOverrideNames.Add(user != null ? $"<@{user.ID}>" : $"user {o.ID}");
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
2024-10-09 17:35:11 +02:00
|
|
|
addedOverrideNames.Add(
|
|
|
|
|
roleCache.TryGet(o.ID, out var role) ? role.Name : $"role {o.ID}"
|
|
|
|
|
);
|
2024-08-20 18:18:17 +02:00
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
builder.AddField("Added overrides", string.Join(", ", addedOverrideNames));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-08-20 14:11:08 +02:00
|
|
|
if (removedOverrides.Count != 0)
|
|
|
|
|
{
|
|
|
|
|
var removedOverrideNames = new List<string>();
|
|
|
|
|
foreach (var o in removedOverrides)
|
|
|
|
|
{
|
|
|
|
|
if (o.Type is PermissionOverwriteType.Member)
|
|
|
|
|
{
|
|
|
|
|
var user = await userCache.GetUserAsync(o.ID);
|
|
|
|
|
removedOverrideNames.Add(user != null ? $"<@{user.ID}>" : $"user {o.ID}");
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
2024-10-09 17:35:11 +02:00
|
|
|
removedOverrideNames.Add(
|
|
|
|
|
roleCache.TryGet(o.ID, out var role) ? role.Name : $"role {o.ID}"
|
|
|
|
|
);
|
2024-08-20 14:11:08 +02:00
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
builder.AddField("Removed overrides", string.Join(", ", removedOverrideNames));
|
|
|
|
|
}
|
|
|
|
|
|
2024-08-20 18:18:17 +02:00
|
|
|
foreach (var overwrite in editedOverrides)
|
|
|
|
|
{
|
2024-10-09 17:35:11 +02:00
|
|
|
var perms = string.Join(
|
|
|
|
|
"\n",
|
|
|
|
|
PermissionUpdate(oldOverrides.First(o => o.ID == overwrite.ID), overwrite)
|
|
|
|
|
);
|
|
|
|
|
if (string.IsNullOrWhiteSpace(perms))
|
|
|
|
|
continue;
|
2024-08-20 18:18:17 +02:00
|
|
|
|
|
|
|
|
builder.AddField(await OverwriteName(overwrite), perms.Trim());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
foreach (var overwrite in addedOverrides)
|
|
|
|
|
{
|
|
|
|
|
var embedFieldValue = "";
|
|
|
|
|
if (overwrite.Allow.GetPermissions().Count != 0)
|
|
|
|
|
embedFieldValue += $"\u2705 {overwrite.Allow.ToPrettyString()}";
|
|
|
|
|
if (overwrite.Deny.GetPermissions().Count != 0)
|
|
|
|
|
embedFieldValue += $"\n\n\u274c {overwrite.Deny.ToPrettyString()}";
|
2024-10-09 17:35:11 +02:00
|
|
|
if (string.IsNullOrWhiteSpace(embedFieldValue))
|
|
|
|
|
continue;
|
2024-08-20 18:18:17 +02:00
|
|
|
builder.AddField(await OverwriteName(overwrite), embedFieldValue.Trim());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Sometimes we get channel update events for channels that didn't actually have anything loggable change.
|
|
|
|
|
// If that happens, there will be no embed fields, so just check for that
|
2024-10-09 17:35:11 +02:00
|
|
|
if (builder.Fields.Count == 0)
|
|
|
|
|
return Result.Success;
|
|
|
|
|
webhookExecutor.QueueLog(
|
|
|
|
|
guildConfig,
|
|
|
|
|
LogChannelType.ChannelUpdate,
|
|
|
|
|
builder.Build().GetOrThrow()
|
|
|
|
|
);
|
2024-08-20 18:18:17 +02:00
|
|
|
|
|
|
|
|
return Result.Success;
|
2024-08-20 14:11:08 +02:00
|
|
|
}
|
|
|
|
|
finally
|
|
|
|
|
{
|
|
|
|
|
channelCache.Set(evt);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private string CategoryUpdate(Snowflake? oldCategory, Snowflake? newCategory)
|
|
|
|
|
{
|
|
|
|
|
var value = "";
|
|
|
|
|
if (oldCategory != null && channelCache.TryGet(oldCategory.Value, out var oldChannel))
|
|
|
|
|
value += $"**Before:** {oldChannel.Name}";
|
|
|
|
|
if (newCategory != null && channelCache.TryGet(newCategory.Value, out var newChannel))
|
|
|
|
|
value += $"\n**After:** {newChannel.Name}";
|
|
|
|
|
return value.Trim();
|
|
|
|
|
}
|
2024-08-20 18:18:17 +02:00
|
|
|
|
|
|
|
|
private async Task<string> OverwriteName(IPermissionOverwrite overwrite)
|
|
|
|
|
{
|
|
|
|
|
switch (overwrite.Type)
|
|
|
|
|
{
|
|
|
|
|
case PermissionOverwriteType.Role:
|
|
|
|
|
return roleCache.TryGet(overwrite.ID, out var role)
|
|
|
|
|
? $"Override for {role.Name}"
|
|
|
|
|
: $"Override for role {overwrite.ID}";
|
|
|
|
|
case PermissionOverwriteType.Member:
|
|
|
|
|
var user = await userCache.GetUserAsync(overwrite.ID);
|
2024-10-09 17:35:11 +02:00
|
|
|
return user != null
|
|
|
|
|
? $"Override for {user.Tag()}"
|
|
|
|
|
: $"Override for user {overwrite.ID}";
|
2024-08-20 18:18:17 +02:00
|
|
|
default:
|
2024-10-09 17:35:11 +02:00
|
|
|
throw new ArgumentOutOfRangeException(
|
|
|
|
|
nameof(overwrite),
|
|
|
|
|
overwrite.Type,
|
|
|
|
|
"Invalid PermissionOverwriteType"
|
|
|
|
|
);
|
2024-08-20 18:18:17 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-10-09 17:35:11 +02:00
|
|
|
private static IEnumerable<string> PermissionUpdate(
|
|
|
|
|
IPermissionOverwrite oldOverwrite,
|
|
|
|
|
IPermissionOverwrite newOverwrite
|
|
|
|
|
)
|
2024-08-20 18:18:17 +02:00
|
|
|
{
|
|
|
|
|
foreach (var perm in Enum.GetValues<DiscordPermission>())
|
|
|
|
|
{
|
2024-10-09 17:35:11 +02:00
|
|
|
if (
|
|
|
|
|
newOverwrite.Allow.HasPermission(perm)
|
|
|
|
|
&& !oldOverwrite.Allow.HasPermission(perm)
|
|
|
|
|
&& !oldOverwrite.Deny.HasPermission(perm)
|
|
|
|
|
)
|
2024-08-20 18:18:17 +02:00
|
|
|
{
|
|
|
|
|
yield return $"\u2b1c \u279c \u2705 {perm.Humanize(LetterCasing.Title)}";
|
|
|
|
|
}
|
2024-10-09 17:35:11 +02:00
|
|
|
else if (
|
|
|
|
|
newOverwrite.Deny.HasPermission(perm)
|
|
|
|
|
&& !oldOverwrite.Allow.HasPermission(perm)
|
|
|
|
|
&& !oldOverwrite.Deny.HasPermission(perm)
|
|
|
|
|
)
|
2024-08-20 18:18:17 +02:00
|
|
|
{
|
|
|
|
|
yield return $"\u2b1c \u279c \u274c {perm.Humanize(LetterCasing.Title)}";
|
|
|
|
|
}
|
2024-10-09 17:35:11 +02:00
|
|
|
else if (
|
|
|
|
|
newOverwrite.Allow.HasPermission(perm) && oldOverwrite.Deny.HasPermission(perm)
|
|
|
|
|
)
|
2024-08-20 18:18:17 +02:00
|
|
|
{
|
|
|
|
|
yield return $"\u274c \u279c \u2705 {perm.Humanize(LetterCasing.Title)}";
|
|
|
|
|
}
|
2024-10-09 17:35:11 +02:00
|
|
|
else if (
|
|
|
|
|
newOverwrite.Deny.HasPermission(perm) && oldOverwrite.Allow.HasPermission(perm)
|
|
|
|
|
)
|
2024-08-20 18:18:17 +02:00
|
|
|
{
|
|
|
|
|
yield return $"\u2705 \u279c \u274c {perm.Humanize(LetterCasing.Title)}";
|
|
|
|
|
}
|
2024-10-09 17:35:11 +02:00
|
|
|
else if (
|
|
|
|
|
!newOverwrite.Allow.HasPermission(perm)
|
|
|
|
|
&& !newOverwrite.Deny.HasPermission(perm)
|
|
|
|
|
&& oldOverwrite.Allow.HasPermission(perm)
|
|
|
|
|
)
|
2024-08-20 18:18:17 +02:00
|
|
|
{
|
|
|
|
|
yield return $"\u2705 \u279c \u2b1c {perm.Humanize(LetterCasing.Title)}";
|
|
|
|
|
}
|
2024-10-09 17:35:11 +02:00
|
|
|
else if (
|
|
|
|
|
!newOverwrite.Allow.HasPermission(perm)
|
|
|
|
|
&& !newOverwrite.Deny.HasPermission(perm)
|
|
|
|
|
&& oldOverwrite.Allow.HasPermission(perm)
|
|
|
|
|
)
|
2024-08-20 18:18:17 +02:00
|
|
|
{
|
|
|
|
|
yield return $"\u274c \u279c \u2b1c {perm.Humanize(LetterCasing.Title)}";
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-10-09 17:35:11 +02:00
|
|
|
}
|