mirror of
https://github.com/TeamOctolings/Octobot.git
synced 2025-04-20 00:43:36 +03:00
Finish adapting code to new guild data storage
This commit is contained in:
parent
587e5d11f9
commit
1f45a605d7
17 changed files with 201 additions and 96 deletions
|
@ -1,6 +1,10 @@
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
using System.Timers;
|
||||||
|
using Boyfriend.Data;
|
||||||
using Discord;
|
using Discord;
|
||||||
|
using Discord.Rest;
|
||||||
using Discord.WebSocket;
|
using Discord.WebSocket;
|
||||||
|
using Timer = System.Timers.Timer;
|
||||||
|
|
||||||
namespace Boyfriend;
|
namespace Boyfriend;
|
||||||
|
|
||||||
|
@ -18,7 +22,10 @@ public static class Boyfriend {
|
||||||
LargeThreshold = 500
|
LargeThreshold = 500
|
||||||
};
|
};
|
||||||
|
|
||||||
private static readonly List<Tuple<Game, TimeSpan>> ActivityList = new() {
|
private static DateTimeOffset _nextSongAt = DateTimeOffset.MinValue;
|
||||||
|
private static uint _nextSongIndex;
|
||||||
|
|
||||||
|
private static readonly Tuple<Game, TimeSpan>[] ActivityList = {
|
||||||
Tuple.Create(new Game("Masayoshi Minoshima (ft. nomico) - Bad Apple!!", ActivityType.Listening),
|
Tuple.Create(new Game("Masayoshi Minoshima (ft. nomico) - Bad Apple!!", ActivityType.Listening),
|
||||||
new TimeSpan(0, 3, 40)),
|
new TimeSpan(0, 3, 40)),
|
||||||
Tuple.Create(new Game("Xi - Blue Zenith", ActivityType.Listening), new TimeSpan(0, 4, 16)),
|
Tuple.Create(new Game("Xi - Blue Zenith", ActivityType.Listening), new TimeSpan(0, 4, 16)),
|
||||||
|
@ -30,11 +37,13 @@ public static class Boyfriend {
|
||||||
|
|
||||||
public static readonly DiscordSocketClient Client = new(Config);
|
public static readonly DiscordSocketClient Client = new(Config);
|
||||||
|
|
||||||
|
private static readonly List<Task> GuildTickTasks = new();
|
||||||
|
|
||||||
public static void Main() {
|
public static void Main() {
|
||||||
Init().GetAwaiter().GetResult();
|
InitAsync().GetAwaiter().GetResult();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task Init() {
|
private static async Task InitAsync() {
|
||||||
var token = (await File.ReadAllTextAsync("token.txt")).Trim();
|
var token = (await File.ReadAllTextAsync("token.txt")).Trim();
|
||||||
|
|
||||||
Client.Log += Log;
|
Client.Log += Log;
|
||||||
|
@ -44,13 +53,34 @@ public static class Boyfriend {
|
||||||
|
|
||||||
EventHandler.InitEvents();
|
EventHandler.InitEvents();
|
||||||
|
|
||||||
while (ActivityList.Count > 0)
|
var timer = new Timer();
|
||||||
foreach (var activity in ActivityList) {
|
timer.Interval = 1000;
|
||||||
await Client.SetActivityAsync(activity.Item1);
|
timer.AutoReset = true;
|
||||||
await Task.Delay(activity.Item2);
|
timer.Elapsed += TickAllGuildsAsync;
|
||||||
|
timer.Start();
|
||||||
|
|
||||||
|
while (ActivityList.Length > 0)
|
||||||
|
if (DateTimeOffset.Now >= _nextSongAt) {
|
||||||
|
var nextSong = ActivityList[_nextSongIndex];
|
||||||
|
await Client.SetActivityAsync(nextSong.Item1);
|
||||||
|
_nextSongAt = DateTimeOffset.Now.Add(nextSong.Item2);
|
||||||
|
_nextSongIndex++;
|
||||||
|
if (_nextSongIndex >= ActivityList.Length) _nextSongIndex = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static async void TickAllGuildsAsync(object? sender, ElapsedEventArgs e) {
|
||||||
|
foreach (var guild in Client.Guilds) GuildTickTasks.Add(TickGuildAsync(guild));
|
||||||
|
|
||||||
|
try { Task.WaitAll(GuildTickTasks.ToArray()); } catch (AggregateException ex) {
|
||||||
|
foreach (var exc in ex.InnerExceptions)
|
||||||
|
await Log(new LogMessage(LogSeverity.Error, nameof(CommandProcessor),
|
||||||
|
"Exception while executing commands", exc));
|
||||||
|
}
|
||||||
|
|
||||||
|
GuildTickTasks.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
public static Task Log(LogMessage msg) {
|
public static Task Log(LogMessage msg) {
|
||||||
switch (msg.Severity) {
|
switch (msg.Severity) {
|
||||||
case LogSeverity.Critical:
|
case LogSeverity.Critical:
|
||||||
|
@ -78,18 +108,51 @@ public static class Boyfriend {
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*public static Dictionary<ulong, ReadOnlyCollection<ulong>> GetRemovedRoles(ulong id) {
|
private static async Task TickGuildAsync(SocketGuild guild) {
|
||||||
if (RemovedRolesDictionary.TryGetValue(id, out var dict)) return dict;
|
var data = GuildData.Get(guild);
|
||||||
var path = $"removedroles_{id}.json";
|
var config = data.Preferences;
|
||||||
|
_ = int.TryParse(config["EventEarlyNotificationOffset"], out var offset);
|
||||||
|
foreach (var schEvent in guild.Events)
|
||||||
|
if (config["AutoStartEvents"] is "true" && DateTimeOffset.Now >= schEvent.StartTime) {
|
||||||
|
await schEvent.StartAsync();
|
||||||
|
} else if (!data.EarlyNotifications.Contains(schEvent.Id) &&
|
||||||
|
DateTimeOffset.Now >= schEvent.StartTime.Subtract(new TimeSpan(0, offset, 0))) {
|
||||||
|
var receivers = config["EventStartedReceivers"];
|
||||||
|
var role = guild.GetRole(ulong.Parse(config["EventNotificationRole"]));
|
||||||
|
var mentions = StringBuilder;
|
||||||
|
|
||||||
if (!File.Exists(path)) File.Create(path).Dispose();
|
if (receivers.Contains("role") && role is not null) mentions.Append($"{role.Mention} ");
|
||||||
|
if (receivers.Contains("users") || receivers.Contains("interested"))
|
||||||
|
mentions = (await schEvent.GetUsersAsync(15))
|
||||||
|
.Where(user => role is null || !((RestGuildUser)user).RoleIds.Contains(role.Id))
|
||||||
|
.Aggregate(mentions, (current, user) => current.Append($"{user.Mention} "));
|
||||||
|
|
||||||
var json = File.ReadAllText(path);
|
await Utils.GetEventNotificationChannel(guild)?.SendMessageAsync(string.Format(Messages.EventStarted,
|
||||||
var removedRoles = JsonConvert.DeserializeObject<Dictionary<ulong, ReadOnlyCollection<ulong>>>(json)
|
mentions,
|
||||||
?? new Dictionary<ulong, ReadOnlyCollection<ulong>>();
|
Utils.Wrap(schEvent.Name),
|
||||||
|
Utils.Wrap(schEvent.Location) ?? Utils.MentionChannel(schEvent.Channel.Id)))!;
|
||||||
RemovedRolesDictionary.Add(id, removedRoles);
|
mentions.Clear();
|
||||||
|
data.EarlyNotifications.Add(schEvent.Id);
|
||||||
return removedRoles;
|
}
|
||||||
}*/
|
|
||||||
|
foreach (var mData in data.MemberData.Values) {
|
||||||
|
if (DateTimeOffset.Now >= mData.BannedUntil) _ = guild.RemoveBanAsync(mData.Id);
|
||||||
|
|
||||||
|
if (mData.IsInGuild) {
|
||||||
|
if (DateTimeOffset.Now >= mData.MutedUntil)
|
||||||
|
await Utils.UnmuteMemberAsync(data, Client.CurrentUser.ToString(), guild.GetUser(mData.Id),
|
||||||
|
Messages.PunishmentExpired);
|
||||||
|
|
||||||
|
foreach (var reminder in mData.Reminders) {
|
||||||
|
var channel = guild.GetTextChannel(reminder.ReminderChannel);
|
||||||
|
if (channel is null) {
|
||||||
|
await Utils.SendDirectMessage(Client.GetUser(mData.Id), reminder.ReminderText);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
await channel.SendMessageAsync($"<@{mData.Id}> {Utils.Wrap(reminder.ReminderText)}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,7 +32,7 @@ public sealed class CommandProcessor {
|
||||||
|
|
||||||
public async Task HandleCommandAsync() {
|
public async Task HandleCommandAsync() {
|
||||||
var guild = Context.Guild;
|
var guild = Context.Guild;
|
||||||
var data = GuildData.FromSocketGuild(guild);
|
var data = GuildData.Get(guild);
|
||||||
Utils.SetCurrentLanguage(guild);
|
Utils.SetCurrentLanguage(guild);
|
||||||
|
|
||||||
if (GetMember().Roles.Contains(data.MuteRole)) {
|
if (GetMember().Roles.Contains(data.MuteRole)) {
|
||||||
|
@ -80,8 +80,9 @@ public sealed class CommandProcessor {
|
||||||
|
|
||||||
public void Audit(string action, bool isPublic = true) {
|
public void Audit(string action, bool isPublic = true) {
|
||||||
var format = $"*[{Context.User.Mention}: {action}]*";
|
var format = $"*[{Context.User.Mention}: {action}]*";
|
||||||
if (isPublic) Utils.SafeAppendToBuilder(_stackedPublicFeedback, format, Context.Guild.SystemChannel);
|
var data = GuildData.Get(Context.Guild);
|
||||||
Utils.SafeAppendToBuilder(_stackedPrivateFeedback, format, Utils.GetBotLogChannel(Context.Guild));
|
if (isPublic) Utils.SafeAppendToBuilder(_stackedPublicFeedback, format, data.PublicFeedbackChannel);
|
||||||
|
Utils.SafeAppendToBuilder(_stackedPrivateFeedback, format, data.PrivateFeedbackChannel);
|
||||||
if (_tasks.Count is 0) SendFeedbacks(false);
|
if (_tasks.Count is 0) SendFeedbacks(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -89,8 +90,9 @@ public sealed class CommandProcessor {
|
||||||
if (reply && _stackedReplyMessage.Length > 0)
|
if (reply && _stackedReplyMessage.Length > 0)
|
||||||
_ = Context.Message.ReplyAsync(_stackedReplyMessage.ToString(), false, null, AllowedMentions.None);
|
_ = Context.Message.ReplyAsync(_stackedReplyMessage.ToString(), false, null, AllowedMentions.None);
|
||||||
|
|
||||||
var adminChannel = Utils.GetBotLogChannel(Context.Guild);
|
var data = GuildData.Get(Context.Guild);
|
||||||
var systemChannel = Context.Guild.SystemChannel;
|
var adminChannel = data.PublicFeedbackChannel;
|
||||||
|
var systemChannel = data.PrivateFeedbackChannel;
|
||||||
if (_stackedPrivateFeedback.Length > 0 && adminChannel is not null &&
|
if (_stackedPrivateFeedback.Length > 0 && adminChannel is not null &&
|
||||||
adminChannel.Id != Context.Message.Channel.Id) {
|
adminChannel.Id != Context.Message.Channel.Id) {
|
||||||
_ = Utils.SilentSendAsync(adminChannel, _stackedPrivateFeedback.ToString());
|
_ = Utils.SilentSendAsync(adminChannel, _stackedPrivateFeedback.ToString());
|
||||||
|
|
|
@ -30,7 +30,7 @@ public sealed class BanCommand : ICommand {
|
||||||
var guildBanMessage = $"({author}) {reason}";
|
var guildBanMessage = $"({author}) {reason}";
|
||||||
await guild.AddBanAsync(toBan.Item1, 0, guildBanMessage);
|
await guild.AddBanAsync(toBan.Item1, 0, guildBanMessage);
|
||||||
|
|
||||||
var memberData = GuildData.FromSocketGuild(guild).MemberData[toBan.Item1];
|
var memberData = GuildData.Get(guild).MemberData[toBan.Item1];
|
||||||
memberData.BannedUntil
|
memberData.BannedUntil
|
||||||
= duration.TotalSeconds < 1 ? DateTimeOffset.MaxValue : DateTimeOffset.Now.Add(duration);
|
= duration.TotalSeconds < 1 ? DateTimeOffset.MaxValue : DateTimeOffset.Now.Add(duration);
|
||||||
memberData.Roles.Clear();
|
memberData.Roles.Clear();
|
||||||
|
|
|
@ -7,7 +7,7 @@ public sealed class HelpCommand : ICommand {
|
||||||
public string[] Aliases { get; } = { "help", "помощь", "справка" };
|
public string[] Aliases { get; } = { "help", "помощь", "справка" };
|
||||||
|
|
||||||
public Task RunAsync(CommandProcessor cmd, string[] args, string[] cleanArgs) {
|
public Task RunAsync(CommandProcessor cmd, string[] args, string[] cleanArgs) {
|
||||||
var prefix = GuildData.FromSocketGuild(cmd.Context.Guild).Preferences["Prefix"];
|
var prefix = GuildData.Get(cmd.Context.Guild).Preferences["Prefix"];
|
||||||
var toSend = Boyfriend.StringBuilder.Append(Messages.CommandHelp);
|
var toSend = Boyfriend.StringBuilder.Append(Messages.CommandHelp);
|
||||||
|
|
||||||
foreach (var command in CommandProcessor.Commands)
|
foreach (var command in CommandProcessor.Commands)
|
||||||
|
|
|
@ -23,7 +23,7 @@ public sealed class KickCommand : ICommand {
|
||||||
string.Format(Messages.YouWereKicked, cmd.Context.User.Mention, cmd.Context.Guild.Name,
|
string.Format(Messages.YouWereKicked, cmd.Context.User.Mention, cmd.Context.Guild.Name,
|
||||||
Utils.Wrap(reason)));
|
Utils.Wrap(reason)));
|
||||||
|
|
||||||
GuildData.FromSocketGuild(cmd.Context.Guild).MemberData[toKick.Id].Roles.Clear();
|
GuildData.Get(cmd.Context.Guild).MemberData[toKick.Id].Roles.Clear();
|
||||||
|
|
||||||
await toKick.KickAsync(guildKickMessage);
|
await toKick.KickAsync(guildKickMessage);
|
||||||
var format = string.Format(Messages.FeedbackMemberKicked, toKick.Mention, Utils.Wrap(reason));
|
var format = string.Format(Messages.FeedbackMemberKicked, toKick.Mention, Utils.Wrap(reason));
|
||||||
|
|
|
@ -14,7 +14,7 @@ public sealed class MuteCommand : ICommand {
|
||||||
var duration = CommandProcessor.GetTimeSpan(args, 1);
|
var duration = CommandProcessor.GetTimeSpan(args, 1);
|
||||||
var reason = cmd.GetRemaining(args, duration.TotalSeconds < 1 ? 1 : 2, "MuteReason");
|
var reason = cmd.GetRemaining(args, duration.TotalSeconds < 1 ? 1 : 2, "MuteReason");
|
||||||
if (reason is null) return;
|
if (reason is null) return;
|
||||||
var guildData = GuildData.FromSocketGuild(cmd.Context.Guild);
|
var guildData = GuildData.Get(cmd.Context.Guild);
|
||||||
var role = guildData.MuteRole;
|
var role = guildData.MuteRole;
|
||||||
|
|
||||||
if ((role is not null && toMute.Roles.Contains(role))
|
if ((role is not null && toMute.Roles.Contains(role))
|
||||||
|
|
|
@ -7,11 +7,12 @@ public sealed class RemindCommand : ICommand {
|
||||||
|
|
||||||
public Task RunAsync(CommandProcessor cmd, string[] args, string[] cleanArgs) {
|
public Task RunAsync(CommandProcessor cmd, string[] args, string[] cleanArgs) {
|
||||||
var remindIn = CommandProcessor.GetTimeSpan(args, 0);
|
var remindIn = CommandProcessor.GetTimeSpan(args, 0);
|
||||||
var reminderText = cmd.GetRemaining(args, 1, "ReminderText");
|
var reminderText = cmd.GetRemaining(cleanArgs, 1, "ReminderText");
|
||||||
if (reminderText is not null)
|
if (reminderText is not null)
|
||||||
GuildData.FromSocketGuild(cmd.Context.Guild).MemberData[cmd.Context.User.Id].Reminders.Add(new Reminder {
|
GuildData.Get(cmd.Context.Guild).MemberData[cmd.Context.User.Id].Reminders.Add(new Reminder {
|
||||||
RemindAt = DateTimeOffset.Now.Add(remindIn),
|
RemindAt = DateTimeOffset.Now.Add(remindIn),
|
||||||
ReminderText = reminderText
|
ReminderText = reminderText,
|
||||||
|
ReminderChannel = cmd.Context.Channel.Id
|
||||||
});
|
});
|
||||||
|
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
|
|
|
@ -10,7 +10,7 @@ public sealed class SettingsCommand : ICommand {
|
||||||
if (!cmd.HasPermission(GuildPermission.ManageGuild)) return Task.CompletedTask;
|
if (!cmd.HasPermission(GuildPermission.ManageGuild)) return Task.CompletedTask;
|
||||||
|
|
||||||
var guild = cmd.Context.Guild;
|
var guild = cmd.Context.Guild;
|
||||||
var data = GuildData.FromSocketGuild(guild);
|
var data = GuildData.Get(guild);
|
||||||
var config = data.Preferences;
|
var config = data.Preferences;
|
||||||
|
|
||||||
if (args.Length is 0) {
|
if (args.Length is 0) {
|
||||||
|
@ -45,10 +45,7 @@ public sealed class SettingsCommand : ICommand {
|
||||||
var selectedSetting = args[0].ToLower();
|
var selectedSetting = args[0].ToLower();
|
||||||
|
|
||||||
var exists = false;
|
var exists = false;
|
||||||
// ReSharper disable once ForeachCanBePartlyConvertedToQueryUsingAnotherGetEnumerator
|
foreach (var setting in GuildData.DefaultPreferences.Keys.Where(x => x.ToLower() == selectedSetting)) {
|
||||||
// Too many allocations
|
|
||||||
foreach (var setting in GuildData.DefaultPreferences.Keys) {
|
|
||||||
if (selectedSetting != setting.ToLower()) continue;
|
|
||||||
selectedSetting = setting;
|
selectedSetting = setting;
|
||||||
exists = true;
|
exists = true;
|
||||||
break;
|
break;
|
||||||
|
@ -133,6 +130,11 @@ public sealed class SettingsCommand : ICommand {
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (selectedSetting.EndsWith("Offset") && !int.TryParse(value, out _)) {
|
||||||
|
cmd.Reply(Messages.InvalidSettingValue, ReplyEmojis.Error);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
if (selectedSetting is "MuteRole") data.MuteRole = guild.GetRole(mention);
|
if (selectedSetting is "MuteRole") data.MuteRole = guild.GetRole(mention);
|
||||||
|
|
||||||
config[selectedSetting] = value;
|
config[selectedSetting] = value;
|
||||||
|
|
|
@ -17,24 +17,16 @@ public sealed class UnmuteCommand : ICommand {
|
||||||
await UnmuteMemberAsync(cmd, toUnmute, reason);
|
await UnmuteMemberAsync(cmd, toUnmute, reason);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async Task UnmuteMemberAsync(CommandProcessor cmd, SocketGuildUser toUnmute,
|
private static async Task UnmuteMemberAsync(CommandProcessor cmd, SocketGuildUser toUnmute,
|
||||||
string reason) {
|
string reason) {
|
||||||
var requestOptions = Utils.GetRequestOptions($"({cmd.Context.User}) {reason}");
|
var isMuted = await Utils.UnmuteMemberAsync(GuildData.Get(cmd.Context.Guild), cmd.Context.User.ToString(),
|
||||||
var data = GuildData.FromSocketGuild(cmd.Context.Guild);
|
toUnmute, reason);
|
||||||
var role = data.MuteRole;
|
|
||||||
|
|
||||||
if (role is not null && toUnmute.Roles.Contains(role)) {
|
if (!isMuted) {
|
||||||
await toUnmute.AddRolesAsync(data.MemberData[toUnmute.Id].Roles, requestOptions);
|
|
||||||
await toUnmute.RemoveRoleAsync(role, requestOptions);
|
|
||||||
} else {
|
|
||||||
if (toUnmute.TimedOutUntil is null || toUnmute.TimedOutUntil.Value < DateTimeOffset.Now) {
|
|
||||||
cmd.Reply(Messages.MemberNotMuted, ReplyEmojis.Error);
|
cmd.Reply(Messages.MemberNotMuted, ReplyEmojis.Error);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await toUnmute.RemoveTimeOutAsync(requestOptions);
|
|
||||||
}
|
|
||||||
|
|
||||||
var feedback = string.Format(Messages.FeedbackMemberUnmuted, toUnmute.Mention, Utils.Wrap(reason));
|
var feedback = string.Format(Messages.FeedbackMemberUnmuted, toUnmute.Mention, Utils.Wrap(reason));
|
||||||
cmd.Reply(feedback, ReplyEmojis.Unmuted);
|
cmd.Reply(feedback, ReplyEmojis.Unmuted);
|
||||||
cmd.Audit(feedback);
|
cmd.Audit(feedback);
|
||||||
|
|
|
@ -20,28 +20,37 @@ public record GuildData {
|
||||||
{ "EventStartedReceivers", "interested,role" },
|
{ "EventStartedReceivers", "interested,role" },
|
||||||
{ "EventNotificationRole", "0" },
|
{ "EventNotificationRole", "0" },
|
||||||
{ "EventNotificationChannel", "0" },
|
{ "EventNotificationChannel", "0" },
|
||||||
{ "EventEarlyNotificationOffset", "0" }
|
{ "EventEarlyNotificationOffset", "0" },
|
||||||
// TODO: { "AutoStartEvents", "false" }
|
{ "AutoStartEvents", "false" }
|
||||||
};
|
};
|
||||||
|
|
||||||
public static readonly Dictionary<ulong, GuildData> GuildDataDictionary = new();
|
public static readonly Dictionary<ulong, GuildData> GuildDataDictionary = new();
|
||||||
|
|
||||||
|
private readonly string _configurationFile;
|
||||||
|
|
||||||
|
public readonly List<ulong> EarlyNotifications = new();
|
||||||
|
|
||||||
public readonly Dictionary<ulong, MemberData> MemberData;
|
public readonly Dictionary<ulong, MemberData> MemberData;
|
||||||
|
|
||||||
public readonly Dictionary<string, string> Preferences;
|
public readonly Dictionary<string, string> Preferences;
|
||||||
|
|
||||||
private SocketRole? _cachedMuteRole;
|
private SocketRole? _cachedMuteRole;
|
||||||
|
private SocketTextChannel? _cachedPrivateFeedbackChannel;
|
||||||
|
private SocketTextChannel? _cachedPublicFeedbackChannel;
|
||||||
|
|
||||||
private ulong _id;
|
private ulong _id;
|
||||||
|
|
||||||
[SuppressMessage("Performance", "CA1853:Unnecessary call to \'Dictionary.ContainsKey(key)\'")]
|
[SuppressMessage("Performance", "CA1853:Unnecessary call to \'Dictionary.ContainsKey(key)\'")]
|
||||||
// https://github.com/dotnet/roslyn-analyzers/issues/6377
|
// https://github.com/dotnet/roslyn-analyzers/issues/6377
|
||||||
private GuildData(SocketGuild guild) {
|
private GuildData(SocketGuild guild) {
|
||||||
if (!Directory.Exists($"{_id}")) Directory.CreateDirectory($"{_id}");
|
var idString = $"{_id}";
|
||||||
if (!Directory.Exists($"{_id}/MemberData")) Directory.CreateDirectory($"{_id}/MemberData");
|
var memberDataDir = $"{_id}/MemberData";
|
||||||
if (!File.Exists($"{_id}/Configuration.json")) File.Create($"{_id}/Configuration.json").Dispose();
|
_configurationFile = $"{_id}/Configuration.json";
|
||||||
|
if (!Directory.Exists(idString)) Directory.CreateDirectory(idString);
|
||||||
|
if (!Directory.Exists(memberDataDir)) Directory.CreateDirectory(memberDataDir);
|
||||||
|
if (!File.Exists(_configurationFile)) File.Create(_configurationFile).Dispose();
|
||||||
Preferences
|
Preferences
|
||||||
= JsonSerializer.Deserialize<Dictionary<string, string>>(File.ReadAllText($"{_id}/Configuration.json")) ??
|
= JsonSerializer.Deserialize<Dictionary<string, string>>(File.ReadAllText(_configurationFile)) ??
|
||||||
new Dictionary<string, string>();
|
new Dictionary<string, string>();
|
||||||
|
|
||||||
if (Preferences.Keys.Count < DefaultPreferences.Keys.Count)
|
if (Preferences.Keys.Count < DefaultPreferences.Keys.Count)
|
||||||
|
@ -53,7 +62,7 @@ public record GuildData {
|
||||||
Preferences.TrimExcess();
|
Preferences.TrimExcess();
|
||||||
|
|
||||||
MemberData = new Dictionary<ulong, MemberData>();
|
MemberData = new Dictionary<ulong, MemberData>();
|
||||||
foreach (var data in Directory.GetFiles($"{_id}/MemberData")) {
|
foreach (var data in Directory.GetFiles(memberDataDir)) {
|
||||||
var deserialised
|
var deserialised
|
||||||
= JsonSerializer.Deserialize<MemberData>(File.ReadAllText($"{_id}/MemberData/{data}.json"));
|
= JsonSerializer.Deserialize<MemberData>(File.ReadAllText($"{_id}/MemberData/{data}.json"));
|
||||||
MemberData.Add(deserialised!.Id, deserialised);
|
MemberData.Add(deserialised!.Id, deserialised);
|
||||||
|
@ -88,7 +97,25 @@ public record GuildData {
|
||||||
set => _cachedMuteRole = value;
|
set => _cachedMuteRole = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static GuildData FromSocketGuild(SocketGuild guild) {
|
public SocketTextChannel? PublicFeedbackChannel {
|
||||||
|
get {
|
||||||
|
if (Preferences["PublicFeedbackChannel"] is "0") return null;
|
||||||
|
return _cachedPublicFeedbackChannel ??= Boyfriend.Client.GetGuild(_id).TextChannels
|
||||||
|
.Single(x => x.Id == ulong.Parse(Preferences["PublicFeedbackChannel"]));
|
||||||
|
}
|
||||||
|
set => _cachedPublicFeedbackChannel = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public SocketTextChannel? PrivateFeedbackChannel {
|
||||||
|
get {
|
||||||
|
if (Preferences["PublicFeedbackChannel"] is "0") return null;
|
||||||
|
return _cachedPrivateFeedbackChannel ??= Boyfriend.Client.GetGuild(_id).TextChannels
|
||||||
|
.Single(x => x.Id == ulong.Parse(Preferences["PrivateFeedbackChannel"]));
|
||||||
|
}
|
||||||
|
set => _cachedPrivateFeedbackChannel = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static GuildData Get(SocketGuild guild) {
|
||||||
if (GuildDataDictionary.TryGetValue(guild.Id, out var stored)) return stored;
|
if (GuildDataDictionary.TryGetValue(guild.Id, out var stored)) return stored;
|
||||||
var newData = new GuildData(guild) {
|
var newData = new GuildData(guild) {
|
||||||
_id = guild.Id
|
_id = guild.Id
|
||||||
|
@ -99,7 +126,7 @@ public record GuildData {
|
||||||
|
|
||||||
public async Task Save(bool saveMemberData) {
|
public async Task Save(bool saveMemberData) {
|
||||||
Preferences.TrimExcess();
|
Preferences.TrimExcess();
|
||||||
await File.WriteAllTextAsync($"{_id}/Configuration.json",
|
await File.WriteAllTextAsync(_configurationFile,
|
||||||
JsonSerializer.Serialize(Preferences));
|
JsonSerializer.Serialize(Preferences));
|
||||||
if (saveMemberData)
|
if (saveMemberData)
|
||||||
foreach (var data in MemberData.Values)
|
foreach (var data in MemberData.Values)
|
||||||
|
|
|
@ -3,4 +3,5 @@
|
||||||
public struct Reminder {
|
public struct Reminder {
|
||||||
public DateTimeOffset RemindAt;
|
public DateTimeOffset RemindAt;
|
||||||
public string ReminderText;
|
public string ReminderText;
|
||||||
|
public ulong ReminderChannel;
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,7 +28,7 @@ public static class EventHandler {
|
||||||
var i = Random.Shared.Next(3);
|
var i = Random.Shared.Next(3);
|
||||||
|
|
||||||
foreach (var guild in Client.Guilds) {
|
foreach (var guild in Client.Guilds) {
|
||||||
var config = GuildData.FromSocketGuild(guild).Preferences;
|
var config = GuildData.Get(guild).Preferences;
|
||||||
var channel = guild.GetTextChannel(Utils.ParseMention(config["BotLogChannel"]));
|
var channel = guild.GetTextChannel(Utils.ParseMention(config["BotLogChannel"]));
|
||||||
Utils.SetCurrentLanguage(guild);
|
Utils.SetCurrentLanguage(guild);
|
||||||
|
|
||||||
|
@ -55,7 +55,8 @@ public static class EventHandler {
|
||||||
await Task.Delay(500);
|
await Task.Delay(500);
|
||||||
|
|
||||||
var auditLogEntry = (await guild.GetAuditLogsAsync(1).FlattenAsync()).First();
|
var auditLogEntry = (await guild.GetAuditLogsAsync(1).FlattenAsync()).First();
|
||||||
if (auditLogEntry.Data is MessageDeleteAuditLogData data && msg.Author.Id == data.Target.Id)
|
if (auditLogEntry.CreatedAt >= DateTimeOffset.Now.Subtract(TimeSpan.FromSeconds(1)) &&
|
||||||
|
auditLogEntry.Data is MessageDeleteAuditLogData data && msg.Author.Id == data.Target.Id)
|
||||||
mention = auditLogEntry.User.Mention;
|
mention = auditLogEntry.User.Mention;
|
||||||
|
|
||||||
await Utils.SendFeedbackAsync(string.Format(Messages.CachedMessageDeleted, msg.Author.Mention,
|
await Utils.SendFeedbackAsync(string.Format(Messages.CachedMessageDeleted, msg.Author.Mention,
|
||||||
|
@ -95,12 +96,12 @@ public static class EventHandler {
|
||||||
|
|
||||||
private static async Task UserJoinedEvent(SocketGuildUser user) {
|
private static async Task UserJoinedEvent(SocketGuildUser user) {
|
||||||
var guild = user.Guild;
|
var guild = user.Guild;
|
||||||
var data = GuildData.FromSocketGuild(guild);
|
var data = GuildData.Get(guild);
|
||||||
var config = data.Preferences;
|
var config = data.Preferences;
|
||||||
Utils.SetCurrentLanguage(guild);
|
Utils.SetCurrentLanguage(guild);
|
||||||
|
|
||||||
if (config["SendWelcomeMessages"] is "true")
|
if (config["SendWelcomeMessages"] is "true")
|
||||||
await Utils.SilentSendAsync(guild.SystemChannel,
|
await Utils.SilentSendAsync(data.PublicFeedbackChannel,
|
||||||
string.Format(config["WelcomeMessage"] is "default"
|
string.Format(config["WelcomeMessage"] is "default"
|
||||||
? Messages.DefaultWelcomeMessage
|
? Messages.DefaultWelcomeMessage
|
||||||
: config["WelcomeMessage"], user.Mention, guild.Name));
|
: config["WelcomeMessage"], user.Mention, guild.Name));
|
||||||
|
@ -129,7 +130,7 @@ public static class EventHandler {
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Task UserLeftEvent(SocketGuild guild, SocketUser user) {
|
private static Task UserLeftEvent(SocketGuild guild, SocketUser user) {
|
||||||
var data = GuildData.FromSocketGuild(guild).MemberData[user.Id];
|
var data = GuildData.Get(guild).MemberData[user.Id];
|
||||||
data.IsInGuild = false;
|
data.IsInGuild = false;
|
||||||
data.LeftAt.Add(DateTimeOffset.Now);
|
data.LeftAt.Add(DateTimeOffset.Now);
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
|
@ -137,7 +138,7 @@ public static class EventHandler {
|
||||||
|
|
||||||
private static async Task ScheduledEventCreatedEvent(SocketGuildEvent scheduledEvent) {
|
private static async Task ScheduledEventCreatedEvent(SocketGuildEvent scheduledEvent) {
|
||||||
var guild = scheduledEvent.Guild;
|
var guild = scheduledEvent.Guild;
|
||||||
var eventConfig = GuildData.FromSocketGuild(guild).Preferences;
|
var eventConfig = GuildData.Get(guild).Preferences;
|
||||||
var channel = Utils.GetEventNotificationChannel(guild);
|
var channel = Utils.GetEventNotificationChannel(guild);
|
||||||
Utils.SetCurrentLanguage(guild);
|
Utils.SetCurrentLanguage(guild);
|
||||||
|
|
||||||
|
@ -161,7 +162,7 @@ public static class EventHandler {
|
||||||
|
|
||||||
private static async Task ScheduledEventCancelledEvent(SocketGuildEvent scheduledEvent) {
|
private static async Task ScheduledEventCancelledEvent(SocketGuildEvent scheduledEvent) {
|
||||||
var guild = scheduledEvent.Guild;
|
var guild = scheduledEvent.Guild;
|
||||||
var eventConfig = GuildData.FromSocketGuild(guild).Preferences;
|
var eventConfig = GuildData.Get(guild).Preferences;
|
||||||
var channel = Utils.GetEventNotificationChannel(guild);
|
var channel = Utils.GetEventNotificationChannel(guild);
|
||||||
Utils.SetCurrentLanguage(guild);
|
Utils.SetCurrentLanguage(guild);
|
||||||
if (channel is not null)
|
if (channel is not null)
|
||||||
|
@ -171,7 +172,7 @@ public static class EventHandler {
|
||||||
|
|
||||||
private static async Task ScheduledEventStartedEvent(SocketGuildEvent scheduledEvent) {
|
private static async Task ScheduledEventStartedEvent(SocketGuildEvent scheduledEvent) {
|
||||||
var guild = scheduledEvent.Guild;
|
var guild = scheduledEvent.Guild;
|
||||||
var eventConfig = GuildData.FromSocketGuild(guild).Preferences;
|
var eventConfig = GuildData.Get(guild).Preferences;
|
||||||
var channel = Utils.GetEventNotificationChannel(guild);
|
var channel = Utils.GetEventNotificationChannel(guild);
|
||||||
Utils.SetCurrentLanguage(guild);
|
Utils.SetCurrentLanguage(guild);
|
||||||
|
|
||||||
|
@ -182,8 +183,9 @@ public static class EventHandler {
|
||||||
|
|
||||||
if (receivers.Contains("role") && role is not null) mentions.Append($"{role.Mention} ");
|
if (receivers.Contains("role") && role is not null) mentions.Append($"{role.Mention} ");
|
||||||
if (receivers.Contains("users") || receivers.Contains("interested"))
|
if (receivers.Contains("users") || receivers.Contains("interested"))
|
||||||
mentions = (await scheduledEvent.GetUsersAsync(15)).Aggregate(mentions,
|
mentions = (await scheduledEvent.GetUsersAsync(15))
|
||||||
(current, user) => current.Append($"{user.Mention} "));
|
.Where(user => role is null || !((RestGuildUser)user).RoleIds.Contains(role.Id))
|
||||||
|
.Aggregate(mentions, (current, user) => current.Append($"{user.Mention} "));
|
||||||
|
|
||||||
await channel.SendMessageAsync(string.Format(Messages.EventStarted, mentions,
|
await channel.SendMessageAsync(string.Format(Messages.EventStarted, mentions,
|
||||||
Utils.Wrap(scheduledEvent.Name),
|
Utils.Wrap(scheduledEvent.Name),
|
||||||
|
|
18
Boyfriend/Messages.Designer.cs
generated
18
Boyfriend/Messages.Designer.cs
generated
|
@ -833,15 +833,6 @@ namespace Boyfriend {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Looks up a localized string similar to Starter role.
|
|
||||||
/// </summary>
|
|
||||||
internal static string SettingsStarterRole {
|
|
||||||
get {
|
|
||||||
return ResourceManager.GetString("SettingsStarterRole", resourceCulture);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Looks up a localized string similar to Welcome message.
|
/// Looks up a localized string similar to Welcome message.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -851,6 +842,15 @@ namespace Boyfriend {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to The starter role was deleted! Please unset roles or channels from settings before deleting them.
|
||||||
|
/// </summary>
|
||||||
|
internal static string StarterRoleDeleted {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("StarterRoleDeleted", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Looks up a localized string similar to You cannot ban me!.
|
/// Looks up a localized string similar to You cannot ban me!.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
|
@ -189,9 +189,6 @@
|
||||||
<data name="SettingsSendWelcomeMessages" xml:space="preserve">
|
<data name="SettingsSendWelcomeMessages" xml:space="preserve">
|
||||||
<value>Send welcome messages</value>
|
<value>Send welcome messages</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="SettingsStarterRole" xml:space="preserve">
|
|
||||||
<value>Starter role</value>
|
|
||||||
</data>
|
|
||||||
<data name="SettingsMuteRole" xml:space="preserve">
|
<data name="SettingsMuteRole" xml:space="preserve">
|
||||||
<value>Mute role</value>
|
<value>Mute role</value>
|
||||||
</data>
|
</data>
|
||||||
|
@ -459,4 +456,7 @@
|
||||||
<data name="UserNotFound" xml:space="preserve">
|
<data name="UserNotFound" xml:space="preserve">
|
||||||
<value>I could not find this user in any guild I'm a member of! Check if the ID is correct and that the user was on this server no longer than 30 days ago (TODO)</value>
|
<value>I could not find this user in any guild I'm a member of! Check if the ID is correct and that the user was on this server no longer than 30 days ago (TODO)</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="StarterRoleDeleted" xml:space="preserve">
|
||||||
|
<value>The starter role was deleted! Please unset roles or channels from settings before deleting them</value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
||||||
|
|
|
@ -246,9 +246,6 @@
|
||||||
<data name="CannotTimeOutBot" xml:space="preserve">
|
<data name="CannotTimeOutBot" xml:space="preserve">
|
||||||
<value>Я не могу использовать тайм-ауты на других ботах! Попробуй указать роль мута в настройках</value>
|
<value>Я не могу использовать тайм-ауты на других ботах! Попробуй указать роль мута в настройках</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="SettingsStarterRole" xml:space="preserve">
|
|
||||||
<value>Начальная роль</value>
|
|
||||||
</data>
|
|
||||||
<data name="EventCreated" xml:space="preserve">
|
<data name="EventCreated" xml:space="preserve">
|
||||||
<value>{0} создал событие {1}! Оно пройдёт в {2} и начнётся <t:{3}:R>!{4}</value>
|
<value>{0} создал событие {1}! Оно пройдёт в {2} и начнётся <t:{3}:R>!{4}</value>
|
||||||
</data>
|
</data>
|
||||||
|
@ -459,4 +456,7 @@
|
||||||
<data name="UserNotFound" xml:space="preserve">
|
<data name="UserNotFound" xml:space="preserve">
|
||||||
<value>Я не смог найти этого пользователя ни в одном из серверов, в которых я есть. Проверь правильность ID и нахождение пользователя на этом сервере максимум 30 дней назад (TODO)</value>
|
<value>Я не смог найти этого пользователя ни в одном из серверов, в которых я есть. Проверь правильность ID и нахождение пользователя на этом сервере максимум 30 дней назад (TODO)</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="StarterRoleDeleted" xml:space="preserve">
|
||||||
|
<value>Начальная роль была удалена! Пожалуйста сбрасывай роли или каналы перед тем как их удалить</value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
||||||
|
|
|
@ -246,9 +246,6 @@
|
||||||
<data name="CannotTimeOutBot" xml:space="preserve">
|
<data name="CannotTimeOutBot" xml:space="preserve">
|
||||||
<value>я не могу замутить ботов, сделай что нибудь</value>
|
<value>я не могу замутить ботов, сделай что нибудь</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="SettingsStarterRole" xml:space="preserve">
|
|
||||||
<value>базовое звание</value>
|
|
||||||
</data>
|
|
||||||
<data name="EventCreated" xml:space="preserve">
|
<data name="EventCreated" xml:space="preserve">
|
||||||
<value>{0} приготовил новый квест {1}! он пройдёт в {2} и начнётся <t:{3}:R>!{4}</value>
|
<value>{0} приготовил новый квест {1}! он пройдёт в {2} и начнётся <t:{3}:R>!{4}</value>
|
||||||
</data>
|
</data>
|
||||||
|
@ -459,4 +456,7 @@
|
||||||
<data name="UserNotFound" xml:space="preserve">
|
<data name="UserNotFound" xml:space="preserve">
|
||||||
<value>TODO</value>
|
<value>TODO</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="StarterRoleDeleted" xml:space="preserve">
|
||||||
|
<value>TODO</value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
||||||
|
|
|
@ -29,10 +29,6 @@ public static partial class Utils {
|
||||||
return GetMessage($"Beep{(i < 0 ? Random.Shared.Next(3) + 1 : ++i)}");
|
return GetMessage($"Beep{(i < 0 ? Random.Shared.Next(3) + 1 : ++i)}");
|
||||||
}
|
}
|
||||||
|
|
||||||
public static SocketTextChannel? GetBotLogChannel(SocketGuild guild) {
|
|
||||||
return guild.GetTextChannel(ParseMention(GuildData.FromSocketGuild(guild).Preferences["BotLogChannel"]));
|
|
||||||
}
|
|
||||||
|
|
||||||
public static string? Wrap(string? original, bool limitedSpace = false) {
|
public static string? Wrap(string? original, bool limitedSpace = false) {
|
||||||
if (original is null) return null;
|
if (original is null) return null;
|
||||||
var maxChars = limitedSpace ? 970 : 1940;
|
var maxChars = limitedSpace ? 970 : 1940;
|
||||||
|
@ -92,8 +88,9 @@ public static partial class Utils {
|
||||||
|
|
||||||
public static async Task
|
public static async Task
|
||||||
SendFeedbackAsync(string feedback, SocketGuild guild, string mention, bool sendPublic = false) {
|
SendFeedbackAsync(string feedback, SocketGuild guild, string mention, bool sendPublic = false) {
|
||||||
var adminChannel = GetBotLogChannel(guild);
|
var data = GuildData.Get(guild);
|
||||||
var systemChannel = guild.SystemChannel;
|
var adminChannel = data.PrivateFeedbackChannel;
|
||||||
|
var systemChannel = data.PublicFeedbackChannel;
|
||||||
var toSend = $"*[{mention}: {feedback}]*";
|
var toSend = $"*[{mention}: {feedback}]*";
|
||||||
if (adminChannel is not null) await SilentSendAsync(adminChannel, toSend);
|
if (adminChannel is not null) await SilentSendAsync(adminChannel, toSend);
|
||||||
if (sendPublic && systemChannel is not null) await SilentSendAsync(systemChannel, toSend);
|
if (sendPublic && systemChannel is not null) await SilentSendAsync(systemChannel, toSend);
|
||||||
|
@ -106,7 +103,7 @@ public static partial class Utils {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void SetCurrentLanguage(SocketGuild guild) {
|
public static void SetCurrentLanguage(SocketGuild guild) {
|
||||||
Messages.Culture = CultureInfoCache[GuildData.FromSocketGuild(guild).Preferences["Lang"]];
|
Messages.Culture = CultureInfoCache[GuildData.Get(guild).Preferences["Lang"]];
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void SafeAppendToBuilder(StringBuilder appendTo, string appendWhat, SocketTextChannel? channel) {
|
public static void SafeAppendToBuilder(StringBuilder appendTo, string appendWhat, SocketTextChannel? channel) {
|
||||||
|
@ -129,7 +126,7 @@ public static partial class Utils {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static SocketTextChannel? GetEventNotificationChannel(SocketGuild guild) {
|
public static SocketTextChannel? GetEventNotificationChannel(SocketGuild guild) {
|
||||||
return guild.GetTextChannel(ParseMention(GuildData.FromSocketGuild(guild)
|
return guild.GetTextChannel(ParseMention(GuildData.Get(guild)
|
||||||
.Preferences["EventNotificationChannel"]));
|
.Preferences["EventNotificationChannel"]));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -141,6 +138,24 @@ public static partial class Utils {
|
||||||
return GuildData.GuildDataDictionary.Values.Any(gData => gData.MemberData.Values.Any(mData => mData.Id == id));
|
return GuildData.GuildDataDictionary.Values.Any(gData => gData.MemberData.Values.Any(mData => mData.Id == id));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static async Task<bool> UnmuteMemberAsync(GuildData data, string modDiscrim, SocketGuildUser toUnmute,
|
||||||
|
string reason) {
|
||||||
|
var requestOptions = GetRequestOptions($"({modDiscrim}) {reason}");
|
||||||
|
var role = data.MuteRole;
|
||||||
|
|
||||||
|
if (role is not null) {
|
||||||
|
if (!toUnmute.Roles.Contains(role)) return false;
|
||||||
|
await toUnmute.AddRolesAsync(data.MemberData[toUnmute.Id].Roles, requestOptions);
|
||||||
|
await toUnmute.RemoveRoleAsync(role, requestOptions);
|
||||||
|
} else {
|
||||||
|
if (toUnmute.TimedOutUntil is null || toUnmute.TimedOutUntil.Value < DateTimeOffset.Now) return false;
|
||||||
|
|
||||||
|
await toUnmute.RemoveTimeOutAsync(requestOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
[GeneratedRegex("[^0-9]")]
|
[GeneratedRegex("[^0-9]")]
|
||||||
private static partial Regex NumbersOnlyRegex();
|
private static partial Regex NumbersOnlyRegex();
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue