2023-12-11 23:38:26 +03:00
|
|
|
|
using System.Text;
|
2023-08-05 21:02:40 +03:00
|
|
|
|
using System.Text.RegularExpressions;
|
|
|
|
|
using Microsoft.Extensions.Hosting;
|
|
|
|
|
using Microsoft.Extensions.Logging;
|
2023-09-30 16:58:32 +03:00
|
|
|
|
using Octobot.Data;
|
2023-10-12 18:37:25 +03:00
|
|
|
|
using Octobot.Extensions;
|
2023-08-05 21:02:40 +03:00
|
|
|
|
using Remora.Discord.API.Abstractions.Objects;
|
|
|
|
|
using Remora.Discord.API.Abstractions.Rest;
|
|
|
|
|
using Remora.Discord.Extensions.Embeds;
|
|
|
|
|
using Remora.Discord.Extensions.Formatting;
|
|
|
|
|
using Remora.Rest.Core;
|
|
|
|
|
using Remora.Results;
|
|
|
|
|
|
2023-09-30 16:58:32 +03:00
|
|
|
|
namespace Octobot.Services.Update;
|
2023-08-05 21:02:40 +03:00
|
|
|
|
|
|
|
|
|
public sealed partial class MemberUpdateService : BackgroundService
|
|
|
|
|
{
|
|
|
|
|
private static readonly string[] GenericNicknames =
|
2023-12-20 19:23:37 +03:00
|
|
|
|
[
|
2023-08-05 21:02:40 +03:00
|
|
|
|
"Albatross", "Alpha", "Anchor", "Banjo", "Bell", "Beta", "Blackbird", "Bulldog", "Canary",
|
|
|
|
|
"Cat", "Calf", "Cyclone", "Daisy", "Dalmatian", "Dart", "Delta", "Diamond", "Donkey", "Duck",
|
|
|
|
|
"Emu", "Eclipse", "Flamingo", "Flute", "Frog", "Goose", "Hatchet", "Heron", "Husky", "Hurricane",
|
|
|
|
|
"Iceberg", "Iguana", "Kiwi", "Kite", "Lamb", "Lily", "Macaw", "Manatee", "Maple", "Mask",
|
|
|
|
|
"Nautilus", "Ostrich", "Octopus", "Pelican", "Puffin", "Pyramid", "Rattle", "Robin", "Rose",
|
|
|
|
|
"Salmon", "Seal", "Shark", "Sheep", "Snake", "Sonar", "Stump", "Sparrow", "Toaster", "Toucan",
|
|
|
|
|
"Torus", "Violet", "Vortex", "Vulture", "Wagon", "Whale", "Woodpecker", "Zebra", "Zigzag"
|
2023-12-20 19:23:37 +03:00
|
|
|
|
];
|
2023-08-05 21:02:40 +03:00
|
|
|
|
|
|
|
|
|
private readonly IDiscordRestChannelAPI _channelApi;
|
|
|
|
|
private readonly IDiscordRestGuildAPI _guildApi;
|
|
|
|
|
private readonly GuildDataService _guildData;
|
|
|
|
|
private readonly ILogger<MemberUpdateService> _logger;
|
2023-12-20 19:33:52 +03:00
|
|
|
|
private readonly Utility _utility;
|
2023-08-05 21:02:40 +03:00
|
|
|
|
|
|
|
|
|
public MemberUpdateService(IDiscordRestChannelAPI channelApi, IDiscordRestGuildAPI guildApi,
|
2023-12-20 19:33:52 +03:00
|
|
|
|
GuildDataService guildData, ILogger<MemberUpdateService> logger, Utility utility)
|
2023-08-05 21:02:40 +03:00
|
|
|
|
{
|
|
|
|
|
_channelApi = channelApi;
|
|
|
|
|
_guildApi = guildApi;
|
|
|
|
|
_guildData = guildData;
|
|
|
|
|
_logger = logger;
|
2023-11-04 21:33:37 +03:00
|
|
|
|
_utility = utility;
|
2023-08-05 21:02:40 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected override async Task ExecuteAsync(CancellationToken ct)
|
|
|
|
|
{
|
|
|
|
|
using var timer = new PeriodicTimer(TimeSpan.FromSeconds(1));
|
|
|
|
|
var tasks = new List<Task>();
|
|
|
|
|
|
|
|
|
|
while (await timer.WaitForNextTickAsync(ct))
|
|
|
|
|
{
|
|
|
|
|
var guildIds = _guildData.GetGuildIds();
|
|
|
|
|
|
|
|
|
|
tasks.AddRange(guildIds.Select(async id =>
|
|
|
|
|
{
|
|
|
|
|
var tickResult = await TickMemberDatasAsync(id, ct);
|
|
|
|
|
_logger.LogResult(tickResult, $"Error in member data update for guild {id}.");
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
await Task.WhenAll(tasks);
|
|
|
|
|
tasks.Clear();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async Task<Result> TickMemberDatasAsync(Snowflake guildId, CancellationToken ct)
|
|
|
|
|
{
|
|
|
|
|
var guildData = await _guildData.GetData(guildId, ct);
|
|
|
|
|
var defaultRole = GuildSettings.DefaultRole.Get(guildData.Settings);
|
|
|
|
|
var failedResults = new List<Result>();
|
2023-09-12 16:28:46 +03:00
|
|
|
|
var memberDatas = guildData.MemberData.Values.ToArray();
|
|
|
|
|
foreach (var data in memberDatas)
|
2023-08-05 21:02:40 +03:00
|
|
|
|
{
|
|
|
|
|
var tickResult = await TickMemberDataAsync(guildId, guildData, defaultRole, data, ct);
|
|
|
|
|
failedResults.AddIfFailed(tickResult);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return failedResults.AggregateErrors();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async Task<Result> TickMemberDataAsync(Snowflake guildId, GuildData guildData, Snowflake defaultRole,
|
|
|
|
|
MemberData data,
|
|
|
|
|
CancellationToken ct)
|
|
|
|
|
{
|
|
|
|
|
var failedResults = new List<Result>();
|
|
|
|
|
var id = data.Id.ToSnowflake();
|
|
|
|
|
|
2023-09-27 21:25:49 +03:00
|
|
|
|
var autoUnbanResult = await TryAutoUnbanAsync(guildId, id, data, ct);
|
|
|
|
|
failedResults.AddIfFailed(autoUnbanResult);
|
|
|
|
|
|
|
|
|
|
var guildMemberResult = await _guildApi.GetGuildMemberAsync(guildId, id, ct);
|
|
|
|
|
if (!guildMemberResult.IsDefined(out var guildMember))
|
|
|
|
|
{
|
|
|
|
|
return failedResults.AggregateErrors();
|
|
|
|
|
}
|
|
|
|
|
|
2023-11-04 21:33:37 +03:00
|
|
|
|
var interactionResult
|
|
|
|
|
= await _utility.CheckInteractionsAsync(guildId, null, id, "Update", ct);
|
|
|
|
|
if (!interactionResult.IsSuccess)
|
2023-09-30 18:36:55 +03:00
|
|
|
|
{
|
2024-03-20 21:08:16 +03:00
|
|
|
|
return ResultExtensions.FromError(interactionResult);
|
2023-09-30 18:36:55 +03:00
|
|
|
|
}
|
|
|
|
|
|
2023-11-04 21:33:37 +03:00
|
|
|
|
var canInteract = interactionResult.Entity is null;
|
2023-08-05 21:02:40 +03:00
|
|
|
|
|
2023-11-04 21:33:37 +03:00
|
|
|
|
if (data.MutedUntil is null)
|
2023-08-05 21:02:40 +03:00
|
|
|
|
{
|
2023-11-04 21:33:37 +03:00
|
|
|
|
data.Roles = guildMember.Roles.ToList().ConvertAll(r => r.Value);
|
2023-08-05 21:02:40 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!guildMember.User.IsDefined(out var user))
|
|
|
|
|
{
|
|
|
|
|
failedResults.AddIfFailed(new ArgumentNullError(nameof(guildMember.User)));
|
|
|
|
|
return failedResults.AggregateErrors();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (var i = data.Reminders.Count - 1; i >= 0; i--)
|
|
|
|
|
{
|
2023-12-11 23:38:26 +03:00
|
|
|
|
var reminderTickResult = await TickReminderAsync(data.Reminders[i], user, data, guildId, ct);
|
2023-08-05 21:02:40 +03:00
|
|
|
|
failedResults.AddIfFailed(reminderTickResult);
|
|
|
|
|
}
|
|
|
|
|
|
2023-11-04 21:33:37 +03:00
|
|
|
|
if (!canInteract)
|
|
|
|
|
{
|
|
|
|
|
return Result.FromSuccess();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var autoUnmuteResult = await TryAutoUnmuteAsync(guildId, id, data, ct);
|
|
|
|
|
failedResults.AddIfFailed(autoUnmuteResult);
|
|
|
|
|
|
|
|
|
|
if (!defaultRole.Empty() && !data.Roles.Contains(defaultRole.Value))
|
|
|
|
|
{
|
|
|
|
|
var addResult = await _guildApi.AddGuildMemberRoleAsync(
|
|
|
|
|
guildId, id, defaultRole, ct: ct);
|
|
|
|
|
failedResults.AddIfFailed(addResult);
|
|
|
|
|
}
|
|
|
|
|
|
2023-08-05 21:02:40 +03:00
|
|
|
|
if (GuildSettings.RenameHoistedUsers.Get(guildData.Settings))
|
|
|
|
|
{
|
|
|
|
|
var filterResult = await FilterNicknameAsync(guildId, user, guildMember, ct);
|
|
|
|
|
failedResults.AddIfFailed(filterResult);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return failedResults.AggregateErrors();
|
|
|
|
|
}
|
|
|
|
|
|
2023-09-27 21:25:49 +03:00
|
|
|
|
private async Task<Result> TryAutoUnbanAsync(
|
2023-09-21 20:16:09 +03:00
|
|
|
|
Snowflake guildId, Snowflake id, MemberData data, CancellationToken ct)
|
|
|
|
|
{
|
2023-09-27 22:07:46 +03:00
|
|
|
|
if (data.BannedUntil is null || DateTimeOffset.UtcNow <= data.BannedUntil)
|
2023-09-21 20:16:09 +03:00
|
|
|
|
{
|
2023-09-27 21:25:49 +03:00
|
|
|
|
return Result.FromSuccess();
|
|
|
|
|
}
|
2023-09-21 20:16:09 +03:00
|
|
|
|
|
2024-03-18 21:26:04 +03:00
|
|
|
|
var existingBanResult = await _guildApi.GetGuildBanAsync(guildId, id, ct);
|
|
|
|
|
if (!existingBanResult.IsDefined())
|
|
|
|
|
{
|
|
|
|
|
data.BannedUntil = null;
|
|
|
|
|
return Result.FromSuccess();
|
|
|
|
|
}
|
|
|
|
|
|
2023-09-27 21:25:49 +03:00
|
|
|
|
var unbanResult = await _guildApi.RemoveGuildBanAsync(
|
|
|
|
|
guildId, id, Messages.PunishmentExpired.EncodeHeader(), ct);
|
|
|
|
|
if (unbanResult.IsSuccess)
|
|
|
|
|
{
|
|
|
|
|
data.BannedUntil = null;
|
2023-09-21 20:16:09 +03:00
|
|
|
|
}
|
|
|
|
|
|
2023-09-27 21:25:49 +03:00
|
|
|
|
return unbanResult;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async Task<Result> TryAutoUnmuteAsync(
|
|
|
|
|
Snowflake guildId, Snowflake id, MemberData data, CancellationToken ct)
|
|
|
|
|
{
|
2023-09-27 22:07:46 +03:00
|
|
|
|
if (data.MutedUntil is null || DateTimeOffset.UtcNow <= data.MutedUntil)
|
2023-09-21 20:16:09 +03:00
|
|
|
|
{
|
2023-09-27 21:25:49 +03:00
|
|
|
|
return Result.FromSuccess();
|
|
|
|
|
}
|
2023-09-21 20:16:09 +03:00
|
|
|
|
|
2023-09-27 21:25:49 +03:00
|
|
|
|
var unmuteResult = await _guildApi.ModifyGuildMemberAsync(
|
|
|
|
|
guildId, id, roles: data.Roles.ConvertAll(r => r.ToSnowflake()),
|
|
|
|
|
reason: Messages.PunishmentExpired.EncodeHeader(), ct: ct);
|
|
|
|
|
if (unmuteResult.IsSuccess)
|
|
|
|
|
{
|
|
|
|
|
data.MutedUntil = null;
|
2023-09-21 20:16:09 +03:00
|
|
|
|
}
|
|
|
|
|
|
2023-09-27 21:25:49 +03:00
|
|
|
|
return unmuteResult;
|
2023-09-21 20:16:09 +03:00
|
|
|
|
}
|
|
|
|
|
|
2023-08-05 21:02:40 +03:00
|
|
|
|
private async Task<Result> FilterNicknameAsync(Snowflake guildId, IUser user, IGuildMember member,
|
|
|
|
|
CancellationToken ct)
|
|
|
|
|
{
|
|
|
|
|
var currentNickname = member.Nickname.IsDefined(out var nickname)
|
|
|
|
|
? nickname
|
2024-02-06 15:09:26 +03:00
|
|
|
|
: user.GlobalName.OrDefault(user.Username);
|
2023-08-05 21:02:40 +03:00
|
|
|
|
var characterList = currentNickname.ToList();
|
|
|
|
|
var usernameChanged = false;
|
|
|
|
|
foreach (var character in currentNickname)
|
|
|
|
|
{
|
|
|
|
|
if (IllegalChars().IsMatch(character.ToString()))
|
|
|
|
|
{
|
|
|
|
|
characterList.Remove(character);
|
|
|
|
|
usernameChanged = true;
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!usernameChanged)
|
|
|
|
|
{
|
|
|
|
|
return Result.FromSuccess();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var newNickname = string.Concat(characterList.ToArray());
|
|
|
|
|
|
|
|
|
|
return await _guildApi.ModifyGuildMemberAsync(
|
|
|
|
|
guildId, user.ID,
|
|
|
|
|
!string.IsNullOrWhiteSpace(newNickname)
|
|
|
|
|
? newNickname
|
|
|
|
|
: GenericNicknames[Random.Shared.Next(GenericNicknames.Length)],
|
|
|
|
|
ct: ct);
|
|
|
|
|
}
|
|
|
|
|
|
2023-10-04 19:55:39 +03:00
|
|
|
|
[GeneratedRegex("[^0-9A-Za-zА-Яа-яЁё]")]
|
2023-08-05 21:02:40 +03:00
|
|
|
|
private static partial Regex IllegalChars();
|
|
|
|
|
|
2023-12-11 23:38:26 +03:00
|
|
|
|
private async Task<Result> TickReminderAsync(Reminder reminder, IUser user, MemberData data, Snowflake guildId,
|
|
|
|
|
CancellationToken ct)
|
2023-08-05 21:02:40 +03:00
|
|
|
|
{
|
|
|
|
|
if (DateTimeOffset.UtcNow < reminder.At)
|
|
|
|
|
{
|
|
|
|
|
return Result.FromSuccess();
|
|
|
|
|
}
|
|
|
|
|
|
2023-12-11 23:38:26 +03:00
|
|
|
|
var builder = new StringBuilder()
|
|
|
|
|
.AppendBulletPointLine(string.Format(Messages.DescriptionReminder, Markdown.InlineCode(reminder.Text)))
|
|
|
|
|
.AppendBulletPointLine(string.Format(Messages.DescriptionActionJumpToMessage, $"https://discord.com/channels/{guildId.Value}/{reminder.ChannelId}/{reminder.MessageId}"));
|
|
|
|
|
|
2023-08-05 21:02:40 +03:00
|
|
|
|
var embed = new EmbedBuilder().WithSmallTitle(
|
|
|
|
|
string.Format(Messages.Reminder, user.GetTag()), user)
|
2023-12-11 23:38:26 +03:00
|
|
|
|
.WithDescription(builder.ToString())
|
2023-08-05 21:02:40 +03:00
|
|
|
|
.WithColour(ColorsList.Magenta)
|
|
|
|
|
.Build();
|
|
|
|
|
|
2023-12-17 19:47:52 +03:00
|
|
|
|
var messageResult = await _channelApi.CreateMessageWithEmbedResultAsync(
|
|
|
|
|
reminder.ChannelId.ToSnowflake(), Mention.User(user), embedResult: embed, ct: ct);
|
2023-08-05 21:02:40 +03:00
|
|
|
|
if (!messageResult.IsSuccess)
|
|
|
|
|
{
|
2024-03-20 21:08:16 +03:00
|
|
|
|
return ResultExtensions.FromError(messageResult);
|
2023-08-05 21:02:40 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
data.Reminders.Remove(reminder);
|
|
|
|
|
return Result.FromSuccess();
|
|
|
|
|
}
|
|
|
|
|
}
|