2019-03-16 15:20:29 +00:00
|
|
|
|
using System;
|
|
|
|
|
using System.Collections.Generic;
|
2019-03-15 15:09:31 +00:00
|
|
|
|
using System.Globalization;
|
2021-03-09 21:31:38 +00:00
|
|
|
|
using System.Linq;
|
2019-03-15 15:09:31 +00:00
|
|
|
|
using System.Text;
|
2019-03-13 18:14:00 +00:00
|
|
|
|
using System.Threading.Tasks;
|
|
|
|
|
using CompatApiClient.Utils;
|
2021-03-09 21:31:38 +00:00
|
|
|
|
using CompatBot.Database;
|
2019-03-13 18:14:00 +00:00
|
|
|
|
using CompatBot.Utils;
|
2020-03-07 16:21:35 +00:00
|
|
|
|
using DSharpPlus;
|
|
|
|
|
using DSharpPlus.Entities;
|
2019-03-13 18:14:00 +00:00
|
|
|
|
using DSharpPlus.EventArgs;
|
|
|
|
|
|
2022-06-29 19:59:46 +00:00
|
|
|
|
namespace CompatBot.EventHandlers;
|
|
|
|
|
|
|
|
|
|
public static class UsernameZalgoMonitor
|
2019-03-13 18:14:00 +00:00
|
|
|
|
{
|
2024-05-18 13:26:34 +00:00
|
|
|
|
private static readonly HashSet<char> OversizedChars =
|
|
|
|
|
[
|
2022-06-29 19:59:46 +00:00
|
|
|
|
'꧁', '꧂', '⎝', '⎠', '⧹', '⧸', '⎛', '⎞', '﷽', '⸻', 'ဪ', '꧅', '꧄', '˞',
|
2024-05-18 13:26:34 +00:00
|
|
|
|
];
|
2021-03-09 18:04:57 +00:00
|
|
|
|
|
2022-06-29 19:59:46 +00:00
|
|
|
|
public static async Task OnUserUpdated(DiscordClient c, UserUpdateEventArgs args)
|
|
|
|
|
{
|
|
|
|
|
try
|
2019-03-13 18:14:00 +00:00
|
|
|
|
{
|
2023-04-21 15:59:24 +00:00
|
|
|
|
if (await c.GetMemberAsync(args.UserAfter).ConfigureAwait(false) is DiscordMember m
|
2022-06-29 19:59:46 +00:00
|
|
|
|
&& NeedsRename(m.DisplayName))
|
2019-03-16 15:20:29 +00:00
|
|
|
|
{
|
2022-06-29 19:59:46 +00:00
|
|
|
|
var suggestedName = StripZalgo(m.DisplayName, m.Username, m.Id).Sanitize();
|
|
|
|
|
await c.ReportAsync("🔣 Potential display name issue",
|
2023-04-20 16:22:50 +00:00
|
|
|
|
$"""
|
|
|
|
|
User {m.GetMentionWithNickname()} has changed their __username__ and is now shown as **{m.DisplayName.Sanitize()}**
|
|
|
|
|
Automatically renamed to: **{suggestedName}**
|
|
|
|
|
""",
|
2022-06-29 19:59:46 +00:00
|
|
|
|
null,
|
|
|
|
|
ReportSeverity.Low);
|
|
|
|
|
await DmAndRenameUserAsync(c, m, suggestedName).ConfigureAwait(false);
|
2019-03-16 15:20:29 +00:00
|
|
|
|
}
|
2019-03-13 18:14:00 +00:00
|
|
|
|
}
|
2022-06-29 19:59:46 +00:00
|
|
|
|
catch (Exception e)
|
|
|
|
|
{
|
|
|
|
|
Config.Log.Error(e);
|
|
|
|
|
}
|
|
|
|
|
}
|
2019-03-13 18:14:00 +00:00
|
|
|
|
|
2022-06-29 19:59:46 +00:00
|
|
|
|
public static async Task OnMemberUpdated(DiscordClient c, GuildMemberUpdateEventArgs args)
|
|
|
|
|
{
|
|
|
|
|
try
|
2019-03-13 18:14:00 +00:00
|
|
|
|
{
|
2022-06-29 19:59:46 +00:00
|
|
|
|
//member object most likely will not be updated in client cache at this moment
|
|
|
|
|
string? fallback;
|
|
|
|
|
if (args.NicknameAfter is string name)
|
|
|
|
|
fallback = args.Member.Username;
|
|
|
|
|
else
|
2019-03-17 20:34:06 +00:00
|
|
|
|
{
|
2022-06-29 19:59:46 +00:00
|
|
|
|
name = args.Member.Username;
|
|
|
|
|
fallback = null;
|
2019-03-16 15:20:29 +00:00
|
|
|
|
}
|
2022-06-29 19:59:46 +00:00
|
|
|
|
|
|
|
|
|
var member = await args.Guild.GetMemberAsync(args.Member.Id).ConfigureAwait(false) ?? args.Member;
|
|
|
|
|
if (NeedsRename(name))
|
2019-03-16 15:20:29 +00:00
|
|
|
|
{
|
2022-06-29 19:59:46 +00:00
|
|
|
|
var suggestedName = StripZalgo(name, fallback, args.Member.Id).Sanitize();
|
|
|
|
|
await c.ReportAsync("🔣 Potential display name issue",
|
2023-04-20 16:22:50 +00:00
|
|
|
|
$"""
|
|
|
|
|
Member {member.GetMentionWithNickname()} has changed their __display name__ and is now shown as **{name.Sanitize()}**
|
|
|
|
|
Automatically renamed to: **{suggestedName}**
|
|
|
|
|
""",
|
2022-06-29 19:59:46 +00:00
|
|
|
|
null,
|
|
|
|
|
ReportSeverity.Low);
|
|
|
|
|
await DmAndRenameUserAsync(c, member, suggestedName).ConfigureAwait(false);
|
2019-03-16 15:20:29 +00:00
|
|
|
|
}
|
2019-03-13 18:14:00 +00:00
|
|
|
|
}
|
2022-06-29 19:59:46 +00:00
|
|
|
|
catch (Exception e)
|
|
|
|
|
{
|
|
|
|
|
Config.Log.Error(e);
|
|
|
|
|
}
|
|
|
|
|
}
|
2019-03-13 18:14:00 +00:00
|
|
|
|
|
2022-06-29 19:59:46 +00:00
|
|
|
|
public static async Task OnMemberAdded(DiscordClient c, GuildMemberAddEventArgs args)
|
|
|
|
|
{
|
|
|
|
|
try
|
2019-03-13 18:14:00 +00:00
|
|
|
|
{
|
2022-06-29 19:59:46 +00:00
|
|
|
|
var name = args.Member.DisplayName;
|
|
|
|
|
if (NeedsRename(name))
|
2019-03-17 20:34:06 +00:00
|
|
|
|
{
|
2022-06-29 19:59:46 +00:00
|
|
|
|
var suggestedName = StripZalgo(name, args.Member.Username, args.Member.Id).Sanitize();
|
|
|
|
|
await c.ReportAsync("🔣 Potential display name issue",
|
2023-04-20 16:22:50 +00:00
|
|
|
|
$"""
|
|
|
|
|
New member joined the server: {args.Member.GetMentionWithNickname()} and is shown as **{name.Sanitize()}**
|
|
|
|
|
Automatically renamed to: **{suggestedName}**
|
|
|
|
|
""",
|
2022-06-29 19:59:46 +00:00
|
|
|
|
null,
|
|
|
|
|
ReportSeverity.Low);
|
2021-03-09 09:03:41 +00:00
|
|
|
|
await DmAndRenameUserAsync(c, args.Member, suggestedName).ConfigureAwait(false);
|
2019-03-16 15:20:29 +00:00
|
|
|
|
}
|
2019-03-13 18:14:00 +00:00
|
|
|
|
}
|
2022-06-29 19:59:46 +00:00
|
|
|
|
catch (Exception e)
|
2019-03-15 15:09:31 +00:00
|
|
|
|
{
|
2022-06-29 19:59:46 +00:00
|
|
|
|
Config.Log.Error(e);
|
2019-03-15 15:09:31 +00:00
|
|
|
|
}
|
2022-06-29 19:59:46 +00:00
|
|
|
|
}
|
2019-03-15 15:09:31 +00:00
|
|
|
|
|
2022-06-29 19:59:46 +00:00
|
|
|
|
public static bool NeedsRename(string displayName)
|
|
|
|
|
{
|
|
|
|
|
displayName = displayName.Normalize().TrimEager();
|
|
|
|
|
return displayName != StripZalgo(displayName, null, 0ul, NormalizationForm.FormC, 3);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static async Task DmAndRenameUserAsync(DiscordClient client, DiscordMember member, string suggestedName)
|
|
|
|
|
{
|
|
|
|
|
try
|
2020-03-07 16:21:35 +00:00
|
|
|
|
{
|
2022-06-29 19:59:46 +00:00
|
|
|
|
var renameTask = member.ModifyAsync(m => m.Nickname = suggestedName);
|
|
|
|
|
Config.Log.Info($"Renamed {member.Username}#{member.Discriminator} ({member.Id}) to {suggestedName}");
|
|
|
|
|
var rulesChannel = await client.GetChannelAsync(Config.BotRulesChannelId).ConfigureAwait(false);
|
2023-04-20 16:22:50 +00:00
|
|
|
|
var msg = $"""
|
|
|
|
|
Hello, your current _display name_ is breaking {rulesChannel.Mention} #7, so you have been renamed to `{suggestedName}`.
|
|
|
|
|
I'm not perfect and can't clean all the junk in names in some cases, so change your nickname at your discretion.
|
|
|
|
|
You can change your _display name_ by clicking on the server name at the top left and selecting **Change Nickname**.
|
|
|
|
|
""";
|
2022-06-29 19:59:46 +00:00
|
|
|
|
var dm = await member.CreateDmChannelAsync().ConfigureAwait(false);
|
|
|
|
|
await dm.SendMessageAsync(msg).ConfigureAwait(false);
|
|
|
|
|
await renameTask.ConfigureAwait(false);
|
2020-03-07 16:21:35 +00:00
|
|
|
|
}
|
2022-06-29 19:59:46 +00:00
|
|
|
|
catch (Exception e)
|
2019-03-13 18:14:00 +00:00
|
|
|
|
{
|
2022-06-29 19:59:46 +00:00
|
|
|
|
Config.Log.Warn(e);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static string StripZalgo(string displayName, string? userName, ulong userId, NormalizationForm normalizationForm = NormalizationForm.FormD, int level = 0)
|
|
|
|
|
{
|
|
|
|
|
const int minNicknameLength = 2;
|
|
|
|
|
displayName = displayName.Normalize(normalizationForm).TrimEager();
|
|
|
|
|
if (displayName is null or {Length: <minNicknameLength} && userName is not null)
|
|
|
|
|
displayName = userName.Normalize(normalizationForm).TrimEager();
|
|
|
|
|
if (displayName is null or {Length: <minNicknameLength})
|
|
|
|
|
return GenerateRandomName(userId);
|
2019-03-13 18:14:00 +00:00
|
|
|
|
|
2022-06-29 19:59:46 +00:00
|
|
|
|
var builder = new StringBuilder();
|
|
|
|
|
bool skipLowSurrogate = false;
|
|
|
|
|
int consecutive = 0;
|
|
|
|
|
int codePoint = 0;
|
|
|
|
|
char highSurrogate = '\0';
|
|
|
|
|
bool hasNormalCharacterBefore = false;
|
|
|
|
|
foreach (var c in displayName)
|
|
|
|
|
{
|
|
|
|
|
switch (char.GetUnicodeCategory(c))
|
2019-03-13 18:14:00 +00:00
|
|
|
|
{
|
2022-12-05 07:14:56 +00:00
|
|
|
|
case UnicodeCategory.EnclosingMark:
|
2022-06-29 19:59:46 +00:00
|
|
|
|
case UnicodeCategory.ModifierSymbol:
|
|
|
|
|
case UnicodeCategory.NonSpacingMark:
|
|
|
|
|
if (++consecutive < level && hasNormalCharacterBefore)
|
|
|
|
|
builder.Append(c);
|
|
|
|
|
break;
|
2019-03-13 18:14:00 +00:00
|
|
|
|
|
2022-06-29 19:59:46 +00:00
|
|
|
|
case UnicodeCategory.Control:
|
|
|
|
|
case UnicodeCategory.Format:
|
2023-07-05 19:12:34 +00:00
|
|
|
|
case UnicodeCategory.PrivateUse:
|
2022-06-29 19:59:46 +00:00
|
|
|
|
break;
|
2019-03-15 15:09:31 +00:00
|
|
|
|
|
2022-06-29 19:59:46 +00:00
|
|
|
|
case UnicodeCategory.Surrogate:
|
|
|
|
|
if (char.IsHighSurrogate(c))
|
|
|
|
|
{
|
|
|
|
|
codePoint = 0x10000 | ((c & 0x3ff) << 10);
|
|
|
|
|
highSurrogate = c;
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
codePoint |= c & 0x3ff;
|
|
|
|
|
if (codePoint is >= 0x016a0 and < 0x01700 // Runic
|
|
|
|
|
or >= 0x101d0 and < 0x10200 // Phaistos Disc
|
|
|
|
|
or >= 0x10380 and < 0x10400 // Ugaritic and Old Persian
|
|
|
|
|
or >= 0x12000 and < 0x13000) // Cuneiform
|
|
|
|
|
continue;
|
2021-05-01 17:54:07 +00:00
|
|
|
|
|
2022-06-29 19:59:46 +00:00
|
|
|
|
builder.Append(highSurrogate).Append(c);
|
|
|
|
|
hasNormalCharacterBefore = true;
|
|
|
|
|
consecutive = 0;
|
|
|
|
|
}
|
|
|
|
|
break;
|
2021-05-01 17:54:07 +00:00
|
|
|
|
|
2022-06-29 19:59:46 +00:00
|
|
|
|
case UnicodeCategory.OtherNotAssigned when c >= 0xdb40:
|
|
|
|
|
skipLowSurrogate = true;
|
|
|
|
|
break;
|
2019-03-13 18:14:00 +00:00
|
|
|
|
|
2022-06-29 19:59:46 +00:00
|
|
|
|
default:
|
|
|
|
|
if (char.IsLowSurrogate(c) && skipLowSurrogate)
|
|
|
|
|
skipLowSurrogate = false;
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
if (!OversizedChars.Contains(c))
|
2019-03-15 15:09:31 +00:00
|
|
|
|
{
|
2022-06-29 19:59:46 +00:00
|
|
|
|
builder.Append(c);
|
|
|
|
|
hasNormalCharacterBefore = true;
|
|
|
|
|
consecutive = 0;
|
2019-03-15 15:09:31 +00:00
|
|
|
|
}
|
2022-06-29 19:59:46 +00:00
|
|
|
|
}
|
|
|
|
|
break;
|
2021-05-01 18:58:39 +00:00
|
|
|
|
}
|
2019-03-13 18:14:00 +00:00
|
|
|
|
}
|
2022-06-29 19:59:46 +00:00
|
|
|
|
var result = builder.ToString().TrimEager();
|
|
|
|
|
if (result is null or {Length: <minNicknameLength})
|
2021-03-09 11:46:31 +00:00
|
|
|
|
{
|
2022-06-29 19:59:46 +00:00
|
|
|
|
if (userName is null)
|
|
|
|
|
return GenerateRandomName(userId);
|
|
|
|
|
return StripZalgo(userName, null, userId, normalizationForm, level);
|
2021-03-09 11:46:31 +00:00
|
|
|
|
}
|
2022-06-29 19:59:46 +00:00
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static string GenerateRandomName(ulong userId)
|
|
|
|
|
{
|
|
|
|
|
var hash = userId.GetHashCode();
|
|
|
|
|
var rng = new Random(hash);
|
|
|
|
|
using var db = new ThumbnailDb();
|
|
|
|
|
var count = db.NamePool.Count();
|
|
|
|
|
var name = db.NamePool.Skip(rng.Next(count)).First().Name;
|
|
|
|
|
return name + Config.RenameNameSuffix;
|
2019-03-13 18:14:00 +00:00
|
|
|
|
}
|
2022-06-29 19:59:46 +00:00
|
|
|
|
}
|