Refactor guild data storage (#13)

Co-authored-by: mctaylors <volkovvladislav8@gmail.com>
This commit is contained in:
Octol1ttle 2023-01-18 19:39:24 +05:00 committed by GitHub
parent f0a6c8faff
commit 7b8888dae3
Signed by: GitHub
GPG key ID: 4AEE18F83AFDEB23
24 changed files with 941 additions and 661 deletions

View file

@ -13,7 +13,9 @@ updates:
# Allow both direct and indirect updates for all packages # Allow both direct and indirect updates for all packages
- dependency-type: "all" - dependency-type: "all"
assignees: assignees:
- "l1ttleO" - "Octol1ttle"
labels:
- "type: dependencies"
- package-ecosystem: "nuget" # See documentation for possible values - package-ecosystem: "nuget" # See documentation for possible values
directory: "/Boyfriend" # Location of package manifests directory: "/Boyfriend" # Location of package manifests
@ -24,4 +26,6 @@ updates:
- dependency-type: "all" - dependency-type: "all"
# Add assignees # Add assignees
assignees: assignees:
- "l1ttleO" - "Octol1ttle"
labels:
- "type: dependencies"

View file

@ -29,7 +29,7 @@ jobs:
run: dotnet restore run: dotnet restore
- name: ReSharper CLI InspectCode - name: ReSharper CLI InspectCode
uses: muno92/resharper_inspectcode@1.6.0 uses: muno92/resharper_inspectcode@1.6.6
with: with:
solutionPath: ./Boyfriend-CSharp.sln solutionPath: ./Boyfriend-CSharp.sln
ignoreIssueType: InvertIf ignoreIssueType: InvertIf

View file

@ -1,8 +1,10 @@
using System.Collections.ObjectModel;
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 Newtonsoft.Json; using Timer = System.Timers.Timer;
namespace Boyfriend; namespace Boyfriend;
@ -20,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)),
@ -32,33 +37,13 @@ public static class Boyfriend {
public static readonly DiscordSocketClient Client = new(Config); public static readonly DiscordSocketClient Client = new(Config);
private static readonly Dictionary<ulong, Dictionary<string, string>> GuildConfigDictionary = new(); private static readonly List<Task> GuildTickTasks = new();
private static readonly Dictionary<ulong, Dictionary<ulong, ReadOnlyCollection<ulong>>> RemovedRolesDictionary =
new();
public static readonly Dictionary<string, string> DefaultConfig = new() {
{ "Prefix", "!" },
{ "Lang", "en" },
{ "ReceiveStartupMessages", "false" },
{ "WelcomeMessage", "default" },
{ "SendWelcomeMessages", "true" },
{ "BotLogChannel", "0" },
{ "StarterRole", "0" },
{ "MuteRole", "0" },
{ "RemoveRolesOnMute", "false" },
{ "FrowningFace", "true" },
{ "EventStartedReceivers", "interested,role" },
{ "EventNotificationRole", "0" },
{ "EventNotificationChannel", "0" },
{ "EventEarlyNotificationOffset", "0" }
};
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;
@ -68,13 +53,35 @@ 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;
if (ActivityList.Length is 0) timer.Dispose(); // CodeQL moment
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(Boyfriend),
"Exception while ticking guilds", 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:
@ -102,53 +109,63 @@ public static class Boyfriend {
return Task.CompletedTask; return Task.CompletedTask;
} }
public static async Task WriteGuildConfigAsync(ulong id) { private static async Task TickGuildAsync(SocketGuild guild) {
await File.WriteAllTextAsync($"config_{id}.json", var data = GuildData.Get(guild);
JsonConvert.SerializeObject(GuildConfigDictionary[id], Formatting.Indented)); var config = data.Preferences;
var saveData = false;
_ = int.TryParse(config["EventEarlyNotificationOffset"], out var offset);
foreach (var schEvent in guild.Events)
if (schEvent.Status is GuildScheduledEventStatus.Scheduled && 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))) {
data.EarlyNotifications.Add(schEvent.Id);
var receivers = config["EventStartedReceivers"];
var role = guild.GetRole(ulong.Parse(config["EventNotificationRole"]));
var mentions = StringBuilder;
if (RemovedRolesDictionary.TryGetValue(id, out var removedRoles)) if (receivers.Contains("role") && role is not null) mentions.Append($"{role.Mention} ");
await File.WriteAllTextAsync($"removedroles_{id}.json", if (receivers.Contains("users") || receivers.Contains("interested"))
JsonConvert.SerializeObject(removedRoles, Formatting.Indented)); mentions = (await schEvent.GetUsersAsync(15))
} .Where(user => role is null || !((RestGuildUser)user).RoleIds.Contains(role.Id))
.Aggregate(mentions, (current, user) => current.Append($"{user.Mention} "));
public static Dictionary<string, string> GetGuildConfig(ulong id) { await Utils.GetEventNotificationChannel(guild)?.SendMessageAsync(string.Format(
if (GuildConfigDictionary.TryGetValue(id, out var cfg)) return cfg; Messages.EventEarlyNotification,
mentions,
Utils.Wrap(schEvent.Name),
schEvent.StartTime.ToUnixTimeSeconds().ToString()))!;
mentions.Clear();
}
var path = $"config_{id}.json"; foreach (var mData in data.MemberData.Values) {
if (DateTimeOffset.Now >= mData.BannedUntil) _ = guild.RemoveBanAsync(mData.Id);
if (!File.Exists(path)) File.Create(path).Dispose(); if (mData.IsInGuild) {
if (DateTimeOffset.Now >= mData.MutedUntil) {
await Utils.UnmuteMemberAsync(data, Client.CurrentUser.ToString(), guild.GetUser(mData.Id),
Messages.PunishmentExpired);
saveData = true;
}
var json = File.ReadAllText(path); for (var i = mData.Reminders.Count - 1; i >= 0; i--) {
var config = JsonConvert.DeserializeObject<Dictionary<string, string>>(json) var reminder = mData.Reminders[i];
?? new Dictionary<string, string>(); if (DateTimeOffset.Now >= reminder.RemindAt) {
var channel = guild.GetTextChannel(reminder.ReminderChannel);
if (channel is null) {
await Utils.SendDirectMessage(Client.GetUser(mData.Id), reminder.ReminderText);
continue;
}
if (config.Keys.Count < DefaultConfig.Keys.Count) { await channel.SendMessageAsync($"<@{mData.Id}> {Utils.Wrap(reminder.ReminderText)}");
// ReSharper disable once ForeachCanBePartlyConvertedToQueryUsingAnotherGetEnumerator mData.Reminders.RemoveAt(i);
// Conversion will result in a lot of memory allocations
foreach (var key in DefaultConfig.Keys) saveData = true;
if (!config.ContainsKey(key)) }
config.Add(key, DefaultConfig[key]); }
} else if (config.Keys.Count > DefaultConfig.Keys.Count) { }
foreach (var key in config.Keys.Where(key => !DefaultConfig.ContainsKey(key))) config.Remove(key);
} }
GuildConfigDictionary.Add(id, config); if (saveData) data.Save(true).Wait();
return config;
}
public static Dictionary<ulong, ReadOnlyCollection<ulong>> GetRemovedRoles(ulong id) {
if (RemovedRolesDictionary.TryGetValue(id, out var dict)) return dict;
var path = $"removedroles_{id}.json";
if (!File.Exists(path)) File.Create(path).Dispose();
var json = File.ReadAllText(path);
var removedRoles = JsonConvert.DeserializeObject<Dictionary<ulong, ReadOnlyCollection<ulong>>>(json)
?? new Dictionary<ulong, ReadOnlyCollection<ulong>>();
RemovedRolesDictionary.Add(id, removedRoles);
return removedRoles;
} }
} }

View file

@ -1,5 +1,6 @@
using System.Text; using System.Text;
using Boyfriend.Commands; using Boyfriend.Commands;
using Boyfriend.Data;
using Discord; using Discord;
using Discord.Commands; using Discord.Commands;
using Discord.WebSocket; using Discord.WebSocket;
@ -12,7 +13,8 @@ public sealed class CommandProcessor {
public static readonly ICommand[] Commands = { public static readonly ICommand[] Commands = {
new BanCommand(), new ClearCommand(), new HelpCommand(), new BanCommand(), new ClearCommand(), new HelpCommand(),
new KickCommand(), new MuteCommand(), new PingCommand(), new KickCommand(), new MuteCommand(), new PingCommand(),
new SettingsCommand(), new UnbanCommand(), new UnmuteCommand() new SettingsCommand(), new UnbanCommand(), new UnmuteCommand(),
new RemindCommand()
}; };
private readonly StringBuilder _stackedPrivateFeedback = new(); private readonly StringBuilder _stackedPrivateFeedback = new();
@ -30,19 +32,18 @@ public sealed class CommandProcessor {
public async Task HandleCommandAsync() { public async Task HandleCommandAsync() {
var guild = Context.Guild; var guild = Context.Guild;
var config = Boyfriend.GetGuildConfig(guild.Id); var data = GuildData.Get(guild);
var muteRole = Utils.GetMuteRole(guild); Utils.SetCurrentLanguage(guild);
Utils.SetCurrentLanguage(guild.Id);
if (GetMember().Roles.Contains(muteRole)) { if (GetMember().Roles.Contains(data.MuteRole)) {
_ = Context.Message.ReplyAsync(Messages.UserCannotUnmuteThemselves); _ = Context.Message.DeleteAsync();
return; return;
} }
var list = Context.Message.Content.Split("\n"); var list = Context.Message.Content.Split("\n");
var cleanList = Context.Message.CleanContent.Split("\n"); var cleanList = Context.Message.CleanContent.Split("\n");
for (var i = 0; i < list.Length; i++) for (var i = 0; i < list.Length; i++)
_tasks.Add(RunCommandOnLine(list[i], cleanList[i], config["Prefix"])); _tasks.Add(RunCommandOnLine(list[i], cleanList[i], data.Preferences["Prefix"]));
try { Task.WaitAll(_tasks.ToArray()); } catch (AggregateException e) { try { Task.WaitAll(_tasks.ToArray()); } catch (AggregateException e) {
foreach (var ex in e.InnerExceptions) foreach (var ex in e.InnerExceptions)
@ -52,7 +53,7 @@ public sealed class CommandProcessor {
_tasks.Clear(); _tasks.Clear();
if (ConfigWriteScheduled) await Boyfriend.WriteGuildConfigAsync(guild.Id); if (ConfigWriteScheduled) await data.Save(true);
SendFeedbacks(); SendFeedbacks();
} }
@ -79,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.Id)); 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);
} }
@ -88,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.Id); 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());
@ -111,19 +114,30 @@ public sealed class CommandProcessor {
return null; return null;
} }
public SocketUser? GetUser(string[] args, string[] cleanArgs, int index, string? argument) { public Tuple<ulong, SocketUser?>? GetUser(string[] args, string[] cleanArgs, int index) {
if (index >= args.Length) { if (index >= args.Length) {
Utils.SafeAppendToBuilder(_stackedReplyMessage, $"{ReplyEmojis.MissingArgument} {Messages.MissingUser}", Utils.SafeAppendToBuilder(_stackedReplyMessage, $"{ReplyEmojis.MissingArgument} {Messages.MissingUser}",
Context.Message); Context.Message);
return null; return null;
} }
var user = Boyfriend.Client.GetUser(Utils.ParseMention(args[index])); var mention = Utils.ParseMention(args[index]);
if (user is null && argument is not null) if (mention is 0) {
Utils.SafeAppendToBuilder(_stackedReplyMessage, Utils.SafeAppendToBuilder(_stackedReplyMessage,
$"{ReplyEmojis.InvalidArgument} {string.Format(Messages.InvalidUser, Utils.Wrap(cleanArgs[index]))}", $"{ReplyEmojis.InvalidArgument} {string.Format(Messages.InvalidUser, Utils.Wrap(cleanArgs[index]))}",
Context.Message); Context.Message);
return user; return null;
}
var exists = Utils.UserExists(mention);
if (!exists) {
Utils.SafeAppendToBuilder(_stackedReplyMessage,
$"{ReplyEmojis.Error} {string.Format(Messages.UserNotFound, Utils.Wrap(cleanArgs[index]))}",
Context.Message);
return null;
}
return Tuple.Create(mention, Boyfriend.Client.GetUser(mention))!;
} }
public bool HasPermission(GuildPermission permission) { public bool HasPermission(GuildPermission permission) {
@ -134,7 +148,7 @@ public sealed class CommandProcessor {
return false; return false;
} }
if (!Context.Guild.GetUser(Context.User.Id).GuildPermissions.Has(permission) if (!GetMember().GuildPermissions.Has(permission)
&& Context.Guild.OwnerId != Context.User.Id) { && Context.Guild.OwnerId != Context.User.Id) {
Utils.SafeAppendToBuilder(_stackedReplyMessage, Utils.SafeAppendToBuilder(_stackedReplyMessage,
$"{ReplyEmojis.NoPermission} {Utils.GetMessage($"UserCannot{permission}")}", $"{ReplyEmojis.NoPermission} {Utils.GetMessage($"UserCannot{permission}")}",
@ -145,11 +159,15 @@ public sealed class CommandProcessor {
return true; return true;
} }
public SocketGuildUser? GetMember(SocketUser user) { private SocketGuildUser GetMember() {
return Context.Guild.GetUser(user.Id); return GetMember(Context.User.Id)!;
} }
public SocketGuildUser? GetMember(string[] args, string[] cleanArgs, int index, string? argument) { public SocketGuildUser? GetMember(ulong id) {
return Context.Guild.GetUser(id);
}
public SocketGuildUser? GetMember(string[] args, int index) {
if (index >= args.Length) { if (index >= args.Length) {
Utils.SafeAppendToBuilder(_stackedReplyMessage, $"{ReplyEmojis.MissingArgument} {Messages.MissingMember}", Utils.SafeAppendToBuilder(_stackedReplyMessage, $"{ReplyEmojis.MissingArgument} {Messages.MissingMember}",
Context.Message); Context.Message);
@ -157,17 +175,13 @@ public sealed class CommandProcessor {
} }
var member = Context.Guild.GetUser(Utils.ParseMention(args[index])); var member = Context.Guild.GetUser(Utils.ParseMention(args[index]));
if (member is null && argument is not null) if (member is null)
Utils.SafeAppendToBuilder(_stackedReplyMessage, Utils.SafeAppendToBuilder(_stackedReplyMessage,
$"{ReplyEmojis.InvalidArgument} {string.Format(Messages.InvalidMember, Utils.Wrap(cleanArgs[index]))}", $"{ReplyEmojis.InvalidArgument} {Messages.InvalidMember}",
Context.Message); Context.Message);
return member; return member;
} }
private SocketGuildUser GetMember() {
return Context.Guild.GetUser(Context.User.Id);
}
public ulong? GetBan(string[] args, int index) { public ulong? GetBan(string[] args, int index) {
if (index >= args.Length) { if (index >= args.Length) {
Utils.SafeAppendToBuilder(_stackedReplyMessage, $"{ReplyEmojis.MissingArgument} {Messages.MissingUser}", Utils.SafeAppendToBuilder(_stackedReplyMessage, $"{ReplyEmojis.MissingArgument} {Messages.MissingUser}",

View file

@ -1,3 +1,4 @@
using Boyfriend.Data;
using Discord; using Discord;
using Discord.WebSocket; using Discord.WebSocket;
@ -7,10 +8,10 @@ public sealed class BanCommand : ICommand {
public string[] Aliases { get; } = { "ban", "бан" }; public string[] Aliases { get; } = { "ban", "бан" };
public async Task RunAsync(CommandProcessor cmd, string[] args, string[] cleanArgs) { public async Task RunAsync(CommandProcessor cmd, string[] args, string[] cleanArgs) {
var toBan = cmd.GetUser(args, cleanArgs, 0, "ToBan"); var toBan = cmd.GetUser(args, cleanArgs, 0);
if (toBan is null || !cmd.HasPermission(GuildPermission.BanMembers)) return; if (toBan is null || !cmd.HasPermission(GuildPermission.BanMembers)) return;
var memberToBan = cmd.GetMember(toBan); var memberToBan = cmd.GetMember(toBan.Item1);
if (memberToBan is not null && !cmd.CanInteractWith(memberToBan, "Ban")) return; if (memberToBan is not null && !cmd.CanInteractWith(memberToBan, "Ban")) return;
var duration = CommandProcessor.GetTimeSpan(args, 1); var duration = CommandProcessor.GetTimeSpan(args, 1);
@ -18,21 +19,27 @@ public sealed class BanCommand : ICommand {
if (reason is not null) await BanUserAsync(cmd, toBan, duration, reason); if (reason is not null) await BanUserAsync(cmd, toBan, duration, reason);
} }
private static async Task BanUserAsync(CommandProcessor cmd, SocketUser toBan, TimeSpan duration, string reason) { private static async Task BanUserAsync(CommandProcessor cmd, Tuple<ulong, SocketUser?> toBan, TimeSpan duration,
string reason) {
var author = cmd.Context.User; var author = cmd.Context.User;
var guild = cmd.Context.Guild; var guild = cmd.Context.Guild;
await Utils.SendDirectMessage(toBan, if (toBan.Item2 is not null)
string.Format(Messages.YouWereBanned, author.Mention, guild.Name, Utils.Wrap(reason))); await Utils.SendDirectMessage(toBan.Item2,
string.Format(Messages.YouWereBanned, author.Mention, guild.Name, Utils.Wrap(reason)));
var guildBanMessage = $"({author}) {reason}"; var guildBanMessage = $"({author}) {reason}";
await guild.AddBanAsync(toBan, 0, guildBanMessage); await guild.AddBanAsync(toBan.Item1, 0, guildBanMessage);
var feedback = string.Format(Messages.FeedbackUserBanned, toBan.Mention, var memberData = GuildData.Get(guild).MemberData[toBan.Item1];
Utils.GetHumanizedTimeOffset(duration), Utils.Wrap(reason)); memberData.BannedUntil
= duration.TotalSeconds < 1 ? DateTimeOffset.MaxValue : DateTimeOffset.Now.Add(duration);
memberData.Roles.Clear();
cmd.ConfigWriteScheduled = true;
var feedback = string.Format(Messages.FeedbackUserBanned, $"<@{toBan.Item1.ToString()}>",
Utils.GetHumanizedTimeSpan(duration), Utils.Wrap(reason));
cmd.Reply(feedback, ReplyEmojis.Banned); cmd.Reply(feedback, ReplyEmojis.Banned);
cmd.Audit(feedback); cmd.Audit(feedback);
if (duration.TotalSeconds > 0)
await Task.FromResult(Utils.DelayedUnbanAsync(cmd, toBan.Id, Messages.PunishmentExpired, duration));
} }
} }

View file

@ -1,4 +1,5 @@
using Discord; using System.Diagnostics;
using Discord;
using Discord.WebSocket; using Discord.WebSocket;
namespace Boyfriend.Commands; namespace Boyfriend.Commands;
@ -7,7 +8,7 @@ public sealed class ClearCommand : ICommand {
public string[] Aliases { get; } = { "clear", "purge", "очистить", "стереть" }; public string[] Aliases { get; } = { "clear", "purge", "очистить", "стереть" };
public async Task RunAsync(CommandProcessor cmd, string[] args, string[] cleanArgs) { public async Task RunAsync(CommandProcessor cmd, string[] args, string[] cleanArgs) {
if (cmd.Context.Channel is not SocketTextChannel channel) throw new Exception(); if (cmd.Context.Channel is not SocketTextChannel channel) throw new UnreachableException();
if (!cmd.HasPermission(GuildPermission.ManageMessages)) return; if (!cmd.HasPermission(GuildPermission.ManageMessages)) return;
@ -18,6 +19,7 @@ public sealed class ClearCommand : ICommand {
var user = (SocketGuildUser)cmd.Context.User; var user = (SocketGuildUser)cmd.Context.User;
await channel.DeleteMessagesAsync(messages, Utils.GetRequestOptions(user.ToString()!)); await channel.DeleteMessagesAsync(messages, Utils.GetRequestOptions(user.ToString()!));
cmd.Audit(string.Format(Messages.FeedbackMessagesCleared, (toDelete + 1).ToString())); cmd.Audit(string.Format(Messages.FeedbackMessagesCleared, (toDelete + 1).ToString(),
Utils.MentionChannel(channel.Id)));
} }
} }

View file

@ -1,3 +1,4 @@
using Boyfriend.Data;
using Humanizer; using Humanizer;
namespace Boyfriend.Commands; namespace Boyfriend.Commands;
@ -6,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 = Boyfriend.GetGuildConfig(cmd.Context.Guild.Id)["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)

View file

@ -1,3 +1,4 @@
using Boyfriend.Data;
using Discord; using Discord;
using Discord.WebSocket; using Discord.WebSocket;
@ -7,7 +8,7 @@ public sealed class KickCommand : ICommand {
public string[] Aliases { get; } = { "kick", "кик", "выгнать" }; public string[] Aliases { get; } = { "kick", "кик", "выгнать" };
public async Task RunAsync(CommandProcessor cmd, string[] args, string[] cleanArgs) { public async Task RunAsync(CommandProcessor cmd, string[] args, string[] cleanArgs) {
var toKick = cmd.GetMember(args, cleanArgs, 0, "ToKick"); var toKick = cmd.GetMember(args, 0);
if (toKick is null || !cmd.HasPermission(GuildPermission.KickMembers)) return; if (toKick is null || !cmd.HasPermission(GuildPermission.KickMembers)) return;
if (cmd.CanInteractWith(toKick, "Kick")) if (cmd.CanInteractWith(toKick, "Kick"))
@ -22,6 +23,9 @@ 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.Get(cmd.Context.Guild).MemberData[toKick.Id].Roles.Clear();
cmd.ConfigWriteScheduled = true;
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));
cmd.Reply(format, ReplyEmojis.Kicked); cmd.Reply(format, ReplyEmojis.Kicked);

View file

@ -1,5 +1,5 @@
using Boyfriend.Data;
using Discord; using Discord;
using Discord.Net;
using Discord.WebSocket; using Discord.WebSocket;
namespace Boyfriend.Commands; namespace Boyfriend.Commands;
@ -8,64 +8,38 @@ public sealed class MuteCommand : ICommand {
public string[] Aliases { get; } = { "mute", "timeout", "заглушить", "мут" }; public string[] Aliases { get; } = { "mute", "timeout", "заглушить", "мут" };
public async Task RunAsync(CommandProcessor cmd, string[] args, string[] cleanArgs) { public async Task RunAsync(CommandProcessor cmd, string[] args, string[] cleanArgs) {
var toMute = cmd.GetMember(args, cleanArgs, 0, "ToMute"); var toMute = cmd.GetMember(args, 0);
if (toMute is null) return; if (toMute is null) return;
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 role = Utils.GetMuteRole(cmd.Context.Guild); var guildData = GuildData.Get(cmd.Context.Guild);
var role = guildData.MuteRole;
if ((role is not null && toMute.Roles.Contains(role)) if ((role is not null && toMute.Roles.Contains(role))
|| (toMute.TimedOutUntil is not null || (toMute.TimedOutUntil is not null
&& toMute.TimedOutUntil.Value.ToUnixTimeSeconds() && toMute.TimedOutUntil.Value
> DateTimeOffset.Now.ToUnixTimeSeconds())) { > DateTimeOffset.Now)) {
cmd.Reply(Messages.MemberAlreadyMuted, ReplyEmojis.Error); cmd.Reply(Messages.MemberAlreadyMuted, ReplyEmojis.Error);
return; return;
} }
var rolesRemoved = Boyfriend.GetRemovedRoles(cmd.Context.Guild.Id);
if (rolesRemoved.TryGetValue(toMute.Id, out var mutedRemovedRoles)) {
foreach (var roleId in mutedRemovedRoles) await toMute.AddRoleAsync(roleId);
rolesRemoved.Remove(toMute.Id);
cmd.ConfigWriteScheduled = true;
cmd.Reply(Messages.RolesReturned, ReplyEmojis.Warning);
}
if (cmd.HasPermission(GuildPermission.ModerateMembers) && cmd.CanInteractWith(toMute, "Mute")) if (cmd.HasPermission(GuildPermission.ModerateMembers) && cmd.CanInteractWith(toMute, "Mute"))
await MuteMemberAsync(cmd, toMute, duration, reason); await MuteMemberAsync(cmd, toMute, duration, guildData, reason);
} }
private static async Task MuteMemberAsync(CommandProcessor cmd, SocketGuildUser toMute, private static async Task MuteMemberAsync(CommandProcessor cmd, SocketGuildUser toMute,
TimeSpan duration, string reason) { TimeSpan duration, GuildData data, string reason) {
var guild = cmd.Context.Guild;
var config = Boyfriend.GetGuildConfig(guild.Id);
var requestOptions = Utils.GetRequestOptions($"({cmd.Context.User}) {reason}"); var requestOptions = Utils.GetRequestOptions($"({cmd.Context.User}) {reason}");
var role = Utils.GetMuteRole(guild); var role = data.MuteRole;
var hasDuration = duration.TotalSeconds > 0; var hasDuration = duration.TotalSeconds > 0;
if (role is not null) { if (role is not null) {
if (config["RemoveRolesOnMute"] is "true") { if (data.Preferences["RemoveRolesOnMute"] is "true")
var rolesRemoved = new List<ulong>(); await toMute.RemoveRolesAsync(toMute.Roles, requestOptions);
foreach (var userRole in toMute.Roles)
try {
if (userRole == guild.EveryoneRole || userRole == role) continue;
await toMute.RemoveRoleAsync(role);
rolesRemoved.Add(userRole.Id);
} catch (HttpException e) {
cmd.Reply(string.Format(Messages.RoleRemovalFailed, $"<@&{userRole}>", Utils.Wrap(e.Reason)),
ReplyEmojis.Warning);
}
Boyfriend.GetRemovedRoles(guild.Id).Add(toMute.Id, rolesRemoved.AsReadOnly());
cmd.ConfigWriteScheduled = true;
}
await toMute.AddRoleAsync(role, requestOptions); await toMute.AddRoleAsync(role, requestOptions);
if (hasDuration)
await Task.FromResult(Utils.DelayedUnmuteAsync(cmd, toMute, Messages.PunishmentExpired, duration));
} else { } else {
if (!hasDuration || duration.TotalDays > 28) { if (!hasDuration || duration.TotalDays > 28) {
cmd.Reply(Messages.DurationRequiredForTimeOuts, ReplyEmojis.Error); cmd.Reply(Messages.DurationRequiredForTimeOuts, ReplyEmojis.Error);
@ -80,8 +54,11 @@ public sealed class MuteCommand : ICommand {
await toMute.SetTimeOutAsync(duration, requestOptions); await toMute.SetTimeOutAsync(duration, requestOptions);
} }
data.MemberData[toMute.Id].MutedUntil = DateTimeOffset.Now.Add(duration);
cmd.ConfigWriteScheduled = true;
var feedback = string.Format(Messages.FeedbackMemberMuted, toMute.Mention, var feedback = string.Format(Messages.FeedbackMemberMuted, toMute.Mention,
Utils.GetHumanizedTimeOffset(duration), Utils.GetHumanizedTimeSpan(duration),
Utils.Wrap(reason)); Utils.Wrap(reason));
cmd.Reply(feedback, ReplyEmojis.Muted); cmd.Reply(feedback, ReplyEmojis.Muted);
cmd.Audit(feedback); cmd.Audit(feedback);

View file

@ -7,7 +7,7 @@ public sealed class PingCommand : ICommand {
var builder = Boyfriend.StringBuilder; var builder = Boyfriend.StringBuilder;
builder.Append(Utils.GetBeep()) builder.Append(Utils.GetBeep())
.Append(Math.Abs(DateTimeOffset.Now.Subtract(cmd.Context.Message.Timestamp).TotalMilliseconds)) .Append(Math.Round(Math.Abs(DateTimeOffset.Now.Subtract(cmd.Context.Message.Timestamp).TotalMilliseconds)))
.Append(Messages.Milliseconds); .Append(Messages.Milliseconds);
cmd.Reply(builder.ToString(), ReplyEmojis.Ping); cmd.Reply(builder.ToString(), ReplyEmojis.Ping);

View file

@ -0,0 +1,23 @@
using Boyfriend.Data;
namespace Boyfriend.Commands;
public sealed class RemindCommand : ICommand {
public string[] Aliases { get; } = { "remind", "reminder", "remindme", "напомни", "напомнить", "напоминание" };
public Task RunAsync(CommandProcessor cmd, string[] args, string[] cleanArgs) {
// TODO: actually make this good
var remindIn = CommandProcessor.GetTimeSpan(args, 0);
var reminderText = cmd.GetRemaining(cleanArgs, 1, "ReminderText");
if (reminderText is not null)
GuildData.Get(cmd.Context.Guild).MemberData[cmd.Context.User.Id].Reminders.Add(new Reminder {
RemindAt = DateTimeOffset.Now.Add(remindIn),
ReminderText = reminderText,
ReminderChannel = cmd.Context.Channel.Id
});
cmd.ConfigWriteScheduled = true;
return Task.CompletedTask;
}
}

View file

@ -1,3 +1,4 @@
using Boyfriend.Data;
using Discord; using Discord;
namespace Boyfriend.Commands; namespace Boyfriend.Commands;
@ -9,14 +10,17 @@ 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 config = Boyfriend.GetGuildConfig(guild.Id); var data = GuildData.Get(guild);
var config = data.Preferences;
if (args.Length is 0) { if (args.Length is 0) {
var currentSettings = Boyfriend.StringBuilder.AppendLine(Messages.CurrentSettings); var currentSettings = Boyfriend.StringBuilder.AppendLine(Messages.CurrentSettings);
foreach (var setting in Boyfriend.DefaultConfig) { foreach (var setting in GuildData.DefaultPreferences) {
var format = "{0}"; var format = "{0}";
var currentValue = config[setting.Key] is "default" ? Messages.DefaultWelcomeMessage : config[setting.Key]; var currentValue = config[setting.Key] is "default"
? Messages.DefaultWelcomeMessage
: config[setting.Key];
if (setting.Key.EndsWith("Channel")) { if (setting.Key.EndsWith("Channel")) {
if (guild.GetTextChannel(ulong.Parse(currentValue)) is not null) format = "<#{0}>"; if (guild.GetTextChannel(ulong.Parse(currentValue)) is not null) format = "<#{0}>";
@ -41,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 Boyfriend.DefaultConfig.Keys) {
if (selectedSetting != setting.ToLower()) continue;
selectedSetting = setting; selectedSetting = setting;
exists = true; exists = true;
break; break;
@ -70,7 +71,7 @@ public sealed class SettingsCommand : ICommand {
} }
} else { value = "reset"; } } else { value = "reset"; }
if (IsBool(Boyfriend.DefaultConfig[selectedSetting]) && !IsBool(value)) { if (IsBool(GuildData.DefaultPreferences[selectedSetting]) && !IsBool(value)) {
value = value switch { value = value switch {
"y" or "yes" or "д" or "да" => "true", "y" or "yes" or "д" or "да" => "true",
"n" or "no" or "н" or "нет" => "false", "n" or "no" or "н" or "нет" => "false",
@ -95,14 +96,14 @@ public sealed class SettingsCommand : ICommand {
var formattedValue = selectedSetting switch { var formattedValue = selectedSetting switch {
"WelcomeMessage" => Utils.Wrap(Messages.DefaultWelcomeMessage), "WelcomeMessage" => Utils.Wrap(Messages.DefaultWelcomeMessage),
"EventStartedReceivers" => Utils.Wrap(Boyfriend.DefaultConfig[selectedSetting])!, "EventStartedReceivers" => Utils.Wrap(GuildData.DefaultPreferences[selectedSetting])!,
_ => value is "reset" or "default" ? Messages.SettingNotDefined _ => value is "reset" or "default" ? Messages.SettingNotDefined
: IsBool(value) ? YesOrNo(value is "true") : IsBool(value) ? YesOrNo(value is "true")
: string.Format(formatting, value) : string.Format(formatting, value)
}; };
if (value is "reset" or "default") { if (value is "reset" or "default") {
config[selectedSetting] = Boyfriend.DefaultConfig[selectedSetting]; config[selectedSetting] = GuildData.DefaultPreferences[selectedSetting];
} else { } else {
if (value == config[selectedSetting]) { if (value == config[selectedSetting]) {
cmd.Reply(string.Format(Messages.SettingsNothingChanged, localizedSelectedSetting, formattedValue), cmd.Reply(string.Format(Messages.SettingsNothingChanged, localizedSelectedSetting, formattedValue),
@ -129,13 +130,28 @@ public sealed class SettingsCommand : ICommand {
return Task.CompletedTask; return Task.CompletedTask;
} }
if (selectedSetting is "MuteRole") Utils.RemoveMuteRoleFromCache(ulong.Parse(config[selectedSetting])); if (selectedSetting.EndsWith("Offset") && !int.TryParse(value, out _)) {
cmd.Reply(Messages.InvalidSettingValue, ReplyEmojis.Error);
return Task.CompletedTask;
}
switch (selectedSetting) {
case "MuteRole":
data.MuteRole = guild.GetRole(mention);
break;
case "PublicFeedbackChannel":
data.PublicFeedbackChannel = guild.GetTextChannel(mention);
break;
case "PrivateFeedbackChannel":
data.PrivateFeedbackChannel = guild.GetTextChannel(mention);
break;
}
config[selectedSetting] = value; config[selectedSetting] = value;
} }
if (selectedSetting is "Lang") { if (selectedSetting is "Lang") {
Utils.SetCurrentLanguage(guild.Id); Utils.SetCurrentLanguage(guild);
localizedSelectedSetting = Utils.GetMessage($"Settings{selectedSetting}"); localizedSelectedSetting = Utils.GetMessage($"Settings{selectedSetting}");
} }

View file

@ -14,7 +14,7 @@ public sealed class UnbanCommand : ICommand {
if (reason is not null) await UnbanUserAsync(cmd, id.Value, reason); if (reason is not null) await UnbanUserAsync(cmd, id.Value, reason);
} }
public static async Task UnbanUserAsync(CommandProcessor cmd, ulong id, string reason) { private static async Task UnbanUserAsync(CommandProcessor cmd, ulong id, string reason) {
var requestOptions = Utils.GetRequestOptions($"({cmd.Context.User}) {reason}"); var requestOptions = Utils.GetRequestOptions($"({cmd.Context.User}) {reason}");
await cmd.Context.Guild.RemoveBanAsync(id, requestOptions); await cmd.Context.Guild.RemoveBanAsync(id, requestOptions);

View file

@ -1,3 +1,4 @@
using Boyfriend.Data;
using Discord; using Discord;
using Discord.WebSocket; using Discord.WebSocket;
@ -9,38 +10,25 @@ public sealed class UnmuteCommand : ICommand {
public async Task RunAsync(CommandProcessor cmd, string[] args, string[] cleanArgs) { public async Task RunAsync(CommandProcessor cmd, string[] args, string[] cleanArgs) {
if (!cmd.HasPermission(GuildPermission.ModerateMembers)) return; if (!cmd.HasPermission(GuildPermission.ModerateMembers)) return;
var toUnmute = cmd.GetMember(args, cleanArgs, 0, "ToUnmute"); var toUnmute = cmd.GetMember(args, 0);
if (toUnmute is null) return; if (toUnmute is null) return;
var reason = cmd.GetRemaining(args, 1, "UnmuteReason"); var reason = cmd.GetRemaining(args, 1, "UnmuteReason");
if (reason is not null && cmd.CanInteractWith(toUnmute, "Unmute")) if (reason is not null && cmd.CanInteractWith(toUnmute, "Unmute"))
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 role = Utils.GetMuteRole(cmd.Context.Guild); toUnmute, reason);
if (role is not null && toUnmute.Roles.Contains(role)) { if (!isMuted) {
var rolesRemoved = Boyfriend.GetRemovedRoles(cmd.Context.Guild.Id); cmd.Reply(Messages.MemberNotMuted, ReplyEmojis.Error);
return;
if (rolesRemoved.TryGetValue(toUnmute.Id, out var unmutedRemovedRoles)) {
await toUnmute.AddRolesAsync(unmutedRemovedRoles);
rolesRemoved.Remove(toUnmute.Id);
cmd.ConfigWriteScheduled = true;
}
await toUnmute.RemoveRoleAsync(role, requestOptions);
} else {
if (toUnmute.TimedOutUntil is null || toUnmute.TimedOutUntil.Value.ToUnixTimeSeconds() <
DateTimeOffset.Now.ToUnixTimeSeconds()) {
cmd.Reply(Messages.MemberNotMuted, ReplyEmojis.Error);
return;
}
await toUnmute.RemoveTimeOutAsync();
} }
cmd.ConfigWriteScheduled = true;
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);

142
Boyfriend/Data/GuildData.cs Normal file
View file

@ -0,0 +1,142 @@
using System.Collections.Concurrent;
using System.Diagnostics.CodeAnalysis;
using System.Text.Json;
using Discord.WebSocket;
namespace Boyfriend.Data;
public record GuildData {
public static readonly Dictionary<string, string> DefaultPreferences = new() {
{ "Prefix", "!" },
{ "Lang", "en" },
{ "ReceiveStartupMessages", "false" },
{ "WelcomeMessage", "default" },
{ "SendWelcomeMessages", "true" },
{ "PublicFeedbackChannel", "0" },
{ "PrivateFeedbackChannel", "0" },
{ "StarterRole", "0" },
{ "MuteRole", "0" },
{ "RemoveRolesOnMute", "false" },
{ "ReturnRolesOnRejoin", "false" },
{ "EventStartedReceivers", "interested,role" },
{ "EventNotificationRole", "0" },
{ "EventNotificationChannel", "0" },
{ "EventEarlyNotificationOffset", "0" },
{ "AutoStartEvents", "false" }
};
public static readonly ConcurrentDictionary<ulong, GuildData> GuildDataDictionary = new();
private static readonly JsonSerializerOptions Options = new() {
IncludeFields = true,
WriteIndented = true
};
private readonly string _configurationFile;
private readonly ulong _id;
public readonly List<ulong> EarlyNotifications = new();
public readonly Dictionary<ulong, MemberData> MemberData;
public readonly Dictionary<string, string> Preferences;
private SocketRole? _cachedMuteRole;
private SocketTextChannel? _cachedPrivateFeedbackChannel;
private SocketTextChannel? _cachedPublicFeedbackChannel;
[SuppressMessage("Performance", "CA1853:Unnecessary call to \'Dictionary.ContainsKey(key)\'")]
// https://github.com/dotnet/roslyn-analyzers/issues/6377
private GuildData(SocketGuild guild) {
_id = guild.Id;
var idString = $"{_id}";
var memberDataDir = $"{_id}/MemberData";
_configurationFile = $"{_id}/Configuration.json";
if (!Directory.Exists(idString)) Directory.CreateDirectory(idString);
if (!Directory.Exists(memberDataDir)) Directory.CreateDirectory(memberDataDir);
if (!File.Exists(_configurationFile)) File.WriteAllText(_configurationFile, "{}");
Preferences
= JsonSerializer.Deserialize<Dictionary<string, string>>(File.ReadAllText(_configurationFile)) ??
new Dictionary<string, string>();
if (Preferences.Keys.Count < DefaultPreferences.Keys.Count)
foreach (var key in DefaultPreferences.Keys.Where(key => !Preferences.ContainsKey(key)))
Preferences.Add(key, DefaultPreferences[key]);
if (Preferences.Keys.Count > DefaultPreferences.Keys.Count)
foreach (var key in Preferences.Keys.Where(key => !DefaultPreferences.ContainsKey(key)))
Preferences.Remove(key);
Preferences.TrimExcess();
MemberData = new Dictionary<ulong, MemberData>();
foreach (var data in Directory.GetFiles(memberDataDir)) {
var deserialised
= JsonSerializer.Deserialize<MemberData>(File.ReadAllText(data), Options);
MemberData.Add(deserialised!.Id, deserialised);
}
guild.DownloadUsersAsync().Wait();
foreach (var member in guild.Users.Where(user => !user.IsBot)) {
if (MemberData.TryGetValue(member.Id, out var memberData)) {
if (!memberData.IsInGuild &&
DateTimeOffset.Now.ToUnixTimeSeconds() -
Math.Max(memberData.LeftAt.Last().ToUnixTimeSeconds(),
memberData.BannedUntil?.ToUnixTimeSeconds() ?? 0) >
60 * 60 * 24 * 30) {
File.Delete($"{_id}/MemberData/{memberData.Id}.json");
MemberData.Remove(memberData.Id);
}
continue;
}
MemberData.Add(member.Id, new MemberData(member));
}
MemberData.TrimExcess();
}
public SocketRole? MuteRole {
get {
if (Preferences["MuteRole"] is "0") return null;
return _cachedMuteRole ??= Boyfriend.Client.GetGuild(_id).Roles
.Single(x => x.Id == ulong.Parse(Preferences["MuteRole"]));
}
set => _cachedMuteRole = value;
}
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;
var newData = new GuildData(guild);
while (!GuildDataDictionary.ContainsKey(guild.Id)) GuildDataDictionary.TryAdd(guild.Id, newData);
return newData;
}
public async Task Save(bool saveMemberData) {
Preferences.TrimExcess();
await File.WriteAllTextAsync(_configurationFile,
JsonSerializer.Serialize(Preferences));
if (saveMemberData)
foreach (var data in MemberData.Values)
await File.WriteAllTextAsync($"{_id}/MemberData/{data.Id}.json",
JsonSerializer.Serialize(data, Options));
}
}

View file

@ -0,0 +1,38 @@
using System.Text.Json.Serialization;
using Discord;
namespace Boyfriend.Data;
public record MemberData {
public DateTimeOffset? BannedUntil;
public ulong Id;
public bool IsInGuild;
public List<DateTimeOffset> JoinedAt;
public List<DateTimeOffset> LeftAt;
public DateTimeOffset? MutedUntil;
public List<Reminder> Reminders;
public List<ulong> Roles;
[JsonConstructor]
public MemberData(DateTimeOffset? bannedUntil, ulong id, bool isInGuild, List<DateTimeOffset> joinedAt,
List<DateTimeOffset> leftAt, DateTimeOffset? mutedUntil, List<Reminder> reminders, List<ulong> roles) {
BannedUntil = bannedUntil;
Id = id;
IsInGuild = isInGuild;
JoinedAt = joinedAt;
LeftAt = leftAt;
MutedUntil = mutedUntil;
Reminders = reminders;
Roles = roles;
}
public MemberData(IGuildUser user) {
Id = user.Id;
IsInGuild = true;
JoinedAt = new List<DateTimeOffset> { user.JoinedAt!.Value };
LeftAt = new List<DateTimeOffset>();
Roles = user.RoleIds.ToList();
Roles.Remove(user.Guild.Id);
Reminders = new List<Reminder>();
}
}

View file

@ -0,0 +1,7 @@
namespace Boyfriend.Data;
public struct Reminder {
public DateTimeOffset RemindAt;
public string ReminderText;
public ulong ReminderChannel;
}

View file

@ -1,3 +1,5 @@
using System.Diagnostics;
using Boyfriend.Data;
using Discord; using Discord;
using Discord.Rest; using Discord.Rest;
using Discord.WebSocket; using Discord.WebSocket;
@ -14,20 +16,30 @@ public static class EventHandler {
Client.MessageReceived += MessageReceivedEvent; Client.MessageReceived += MessageReceivedEvent;
Client.MessageUpdated += MessageUpdatedEvent; Client.MessageUpdated += MessageUpdatedEvent;
Client.UserJoined += UserJoinedEvent; Client.UserJoined += UserJoinedEvent;
Client.UserLeft += UserLeftEvent;
Client.GuildMemberUpdated += RolesUpdatedEvent;
Client.GuildScheduledEventCreated += ScheduledEventCreatedEvent; Client.GuildScheduledEventCreated += ScheduledEventCreatedEvent;
Client.GuildScheduledEventCancelled += ScheduledEventCancelledEvent; Client.GuildScheduledEventCancelled += ScheduledEventCancelledEvent;
Client.GuildScheduledEventStarted += ScheduledEventStartedEvent; Client.GuildScheduledEventStarted += ScheduledEventStartedEvent;
Client.GuildScheduledEventCompleted += ScheduledEventCompletedEvent; Client.GuildScheduledEventCompleted += ScheduledEventCompletedEvent;
} }
private static Task RolesUpdatedEvent(Cacheable<SocketGuildUser, ulong> oldUser, SocketGuildUser newUser) {
var data = GuildData.Get(newUser.Guild).MemberData[newUser.Id];
data.Roles = ((IGuildUser)newUser).RoleIds.ToList();
data.Roles.Remove(newUser.Guild.Id);
return Task.CompletedTask;
}
private static Task ReadyEvent() { private static Task ReadyEvent() {
if (!_sendReadyMessages) return Task.CompletedTask; if (!_sendReadyMessages) return Task.CompletedTask;
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 = Boyfriend.GetGuildConfig(guild.Id); var data = GuildData.Get(guild);
var channel = guild.GetTextChannel(Utils.ParseMention(config["BotLogChannel"])); var config = data.Preferences;
Utils.SetCurrentLanguage(guild.Id); var channel = data.PrivateFeedbackChannel;
Utils.SetCurrentLanguage(guild);
if (config["ReceiveStartupMessages"] is not "true" || channel is null) continue; if (config["ReceiveStartupMessages"] is not "true" || channel is null) continue;
_ = channel.SendMessageAsync(string.Format(Messages.Ready, Utils.GetBeep(i))); _ = channel.SendMessageAsync(string.Format(Messages.Ready, Utils.GetBeep(i)));
@ -45,19 +57,20 @@ public static class EventHandler {
var guild = gChannel.Guild; var guild = gChannel.Guild;
Utils.SetCurrentLanguage(guild.Id); Utils.SetCurrentLanguage(guild);
var mention = msg.Author.Mention; var mention = msg.Author.Mention;
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,
Utils.MentionChannel(channel.Id), Utils.MentionChannel(channel.Id),
Utils.Wrap(msg.CleanContent)), guild.Id, mention); Utils.Wrap(msg.CleanContent)), guild, mention);
} }
private static Task MessageReceivedEvent(IDeletable messageParam) { private static Task MessageReceivedEvent(IDeletable messageParam) {
@ -67,7 +80,8 @@ public static class EventHandler {
"whoami" => message.ReplyAsync("`nobody`"), "whoami" => message.ReplyAsync("`nobody`"),
"сука !!" => message.ReplyAsync("`root`"), "сука !!" => message.ReplyAsync("`root`"),
"воооо" => message.ReplyAsync("`removing /...`"), "воооо" => message.ReplyAsync("`removing /...`"),
"op ??" => message.ReplyAsync("некоторые пасхальные цитаты которые вы могли найти были легально взяты у <@573772175572729876>"), "op ??" => message.ReplyAsync(
"некоторые пасхальные цитаты которые вы могли найти были легально взяты у <@573772175572729876>"),
_ => new CommandProcessor(message).HandleCommandAsync() _ => new CommandProcessor(message).HandleCommandAsync()
}; };
return Task.CompletedTask; return Task.CompletedTask;
@ -80,34 +94,60 @@ public static class EventHandler {
msg.CleanContent == messageSocket.CleanContent || msg.Author.IsBot) return; msg.CleanContent == messageSocket.CleanContent || msg.Author.IsBot) return;
var guild = gChannel.Guild; var guild = gChannel.Guild;
Utils.SetCurrentLanguage(guild.Id); Utils.SetCurrentLanguage(guild);
var isLimitedSpace = msg.CleanContent.Length + messageSocket.CleanContent.Length < 1940; var isLimitedSpace = msg.CleanContent.Length + messageSocket.CleanContent.Length < 1940;
await Utils.SendFeedbackAsync(string.Format(Messages.CachedMessageEdited, Utils.MentionChannel(channel.Id), await Utils.SendFeedbackAsync(string.Format(Messages.CachedMessageEdited, Utils.MentionChannel(channel.Id),
Utils.Wrap(msg.CleanContent, isLimitedSpace), Utils.Wrap(messageSocket.CleanContent, isLimitedSpace)), Utils.Wrap(msg.CleanContent, isLimitedSpace), Utils.Wrap(messageSocket.CleanContent, isLimitedSpace)),
guild.Id, msg.Author.Mention); guild, msg.Author.Mention);
} }
private static async Task UserJoinedEvent(SocketGuildUser user) { private static async Task UserJoinedEvent(SocketGuildUser user) {
if (user.IsBot) return;
var guild = user.Guild; var guild = user.Guild;
var config = Boyfriend.GetGuildConfig(guild.Id); var data = GuildData.Get(guild);
Utils.SetCurrentLanguage(guild.Id); var config = data.Preferences;
Utils.SetCurrentLanguage(guild);
if (config["SendWelcomeMessages"] is "true") if (config["SendWelcomeMessages"] is "true" && data.PublicFeedbackChannel is not null)
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));
if (config["StarterRole"] is not "0") await user.AddRoleAsync(ulong.Parse(config["StarterRole"])); if (config["StarterRole"] is not "0") await user.AddRoleAsync(ulong.Parse(config["StarterRole"]));
if (!data.MemberData.ContainsKey(user.Id)) data.MemberData.Add(user.Id, new MemberData(user));
var memberData = data.MemberData[user.Id];
memberData.IsInGuild = true;
memberData.BannedUntil = null;
if (memberData.LeftAt.Count > 0) {
if (memberData.JoinedAt.Contains(user.JoinedAt!.Value))
throw new UnreachableException();
memberData.JoinedAt.Add(user.JoinedAt!.Value);
}
if (memberData.MutedUntil < DateTimeOffset.Now) {
if (data.MuteRole is not null)
await user.AddRoleAsync(data.MuteRole);
if (config["RemoveRolesOnMute"] is "false" && config["ReturnRolesOnRejoin"] is "true")
await user.AddRolesAsync(memberData.Roles);
} else if (config["ReturnRolesOnRejoin"] is "true") { await user.AddRolesAsync(memberData.Roles); }
}
private static Task UserLeftEvent(SocketGuild guild, SocketUser user) {
var data = GuildData.Get(guild).MemberData[user.Id];
data.IsInGuild = false;
data.LeftAt.Add(DateTimeOffset.Now);
return Task.CompletedTask;
} }
private static async Task ScheduledEventCreatedEvent(SocketGuildEvent scheduledEvent) { private static async Task ScheduledEventCreatedEvent(SocketGuildEvent scheduledEvent) {
var guild = scheduledEvent.Guild; var guild = scheduledEvent.Guild;
var eventConfig = Boyfriend.GetGuildConfig(guild.Id); var eventConfig = GuildData.Get(guild).Preferences;
var channel = Utils.GetEventNotificationChannel(guild); var channel = Utils.GetEventNotificationChannel(guild);
Utils.SetCurrentLanguage(guild.Id); Utils.SetCurrentLanguage(guild);
if (channel is not null) { if (channel is not null) {
var role = guild.GetRole(ulong.Parse(eventConfig["EventNotificationRole"])); var role = guild.GetRole(ulong.Parse(eventConfig["EventNotificationRole"]));
@ -125,17 +165,13 @@ public static class EventHandler {
scheduledEvent.StartTime.ToUnixTimeSeconds().ToString(), descAndLink), scheduledEvent.StartTime.ToUnixTimeSeconds().ToString(), descAndLink),
true); true);
} }
if (eventConfig["EventEarlyNotificationOffset"] is not "0")
_ = Utils.SendEarlyEventStartNotificationAsync(channel, scheduledEvent,
int.Parse(eventConfig["EventEarlyNotificationOffset"]));
} }
private static async Task ScheduledEventCancelledEvent(SocketGuildEvent scheduledEvent) { private static async Task ScheduledEventCancelledEvent(SocketGuildEvent scheduledEvent) {
var guild = scheduledEvent.Guild; var guild = scheduledEvent.Guild;
var eventConfig = Boyfriend.GetGuildConfig(guild.Id); var eventConfig = GuildData.Get(guild).Preferences;
var channel = Utils.GetEventNotificationChannel(guild); var channel = Utils.GetEventNotificationChannel(guild);
Utils.SetCurrentLanguage(guild.Id); Utils.SetCurrentLanguage(guild);
if (channel is not null) if (channel is not null)
await channel.SendMessageAsync(string.Format(Messages.EventCancelled, Utils.Wrap(scheduledEvent.Name), await channel.SendMessageAsync(string.Format(Messages.EventCancelled, Utils.Wrap(scheduledEvent.Name),
eventConfig["FrowningFace"] is "true" ? $" {Messages.SettingsFrowningFace}" : "")); eventConfig["FrowningFace"] is "true" ? $" {Messages.SettingsFrowningFace}" : ""));
@ -143,9 +179,9 @@ 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 = Boyfriend.GetGuildConfig(guild.Id); var eventConfig = GuildData.Get(guild).Preferences;
var channel = Utils.GetEventNotificationChannel(guild); var channel = Utils.GetEventNotificationChannel(guild);
Utils.SetCurrentLanguage(guild.Id); Utils.SetCurrentLanguage(guild);
if (channel is not null) { if (channel is not null) {
var receivers = eventConfig["EventStartedReceivers"]; var receivers = eventConfig["EventStartedReceivers"];
@ -154,8 +190,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),
@ -167,9 +204,9 @@ public static class EventHandler {
private static async Task ScheduledEventCompletedEvent(SocketGuildEvent scheduledEvent) { private static async Task ScheduledEventCompletedEvent(SocketGuildEvent scheduledEvent) {
var guild = scheduledEvent.Guild; var guild = scheduledEvent.Guild;
var channel = Utils.GetEventNotificationChannel(guild); var channel = Utils.GetEventNotificationChannel(guild);
Utils.SetCurrentLanguage(guild.Id); Utils.SetCurrentLanguage(guild);
if (channel is not null) if (channel is not null)
await channel.SendMessageAsync(string.Format(Messages.EventCompleted, Utils.Wrap(scheduledEvent.Name), await channel.SendMessageAsync(string.Format(Messages.EventCompleted, Utils.Wrap(scheduledEvent.Name),
Utils.GetHumanizedTimeOffset(DateTimeOffset.Now.Subtract(scheduledEvent.StartTime)))); Utils.GetHumanizedTimeSpan(DateTimeOffset.Now.Subtract(scheduledEvent.StartTime))));
} }
} }

View file

@ -284,6 +284,15 @@ namespace Boyfriend {
} }
} }
/// <summary>
/// Looks up a localized string similar to Adds a reminder.
/// </summary>
internal static string CommandDescriptionRemind {
get {
return ResourceManager.GetString("CommandDescriptionRemind", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to Allows you to change certain preferences for this guild. /// Looks up a localized string similar to Allows you to change certain preferences for this guild.
/// </summary> /// </summary>
@ -492,7 +501,7 @@ namespace Boyfriend {
} }
/// <summary> /// <summary>
/// Looks up a localized string similar to You need to specify a guild member instead of {0}!. /// Looks up a localized string similar to You did not specify a member of this guild!.
/// </summary> /// </summary>
internal static string InvalidMember { internal static string InvalidMember {
get { get {
@ -609,11 +618,11 @@ namespace Boyfriend {
} }
/// <summary> /// <summary>
/// Looks up a localized string similar to You need to specify a setting to change!. /// Looks up a localized string similar to You need to specify reminder text!.
/// </summary> /// </summary>
internal static string MissingSetting { internal static string MissingReminderText {
get { get {
return ResourceManager.GetString("MissingSetting", resourceCulture); return ResourceManager.GetString("MissingReminderText", resourceCulture);
} }
} }
@ -680,24 +689,6 @@ namespace Boyfriend {
} }
} }
/// <summary>
/// Looks up a localized string similar to I couldn&apos;t remove role {0} because of an error! {1}.
/// </summary>
internal static string RoleRemovalFailed {
get {
return ResourceManager.GetString("RoleRemovalFailed", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Someone removed the mute role manually! I added back all roles that I removed during the mute.
/// </summary>
internal static string RolesReturned {
get {
return ResourceManager.GetString("RolesReturned", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to That setting doesn&apos;t exist!. /// Looks up a localized string similar to That setting doesn&apos;t exist!.
/// </summary> /// </summary>
@ -717,11 +708,11 @@ namespace Boyfriend {
} }
/// <summary> /// <summary>
/// Looks up a localized string similar to Bot log channel. /// Looks up a localized string similar to Automatically start scheduled events.
/// </summary> /// </summary>
internal static string SettingsBotLogChannel { internal static string SettingsAutoStartEvents {
get { get {
return ResourceManager.GetString("SettingsBotLogChannel", resourceCulture); return ResourceManager.GetString("SettingsAutoStartEvents", resourceCulture);
} }
} }
@ -806,6 +797,24 @@ namespace Boyfriend {
} }
} }
/// <summary>
/// Looks up a localized string similar to Channel for private notifications.
/// </summary>
internal static string SettingsPrivateFeedbackChannel {
get {
return ResourceManager.GetString("SettingsPrivateFeedbackChannel", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Channel for public notifications.
/// </summary>
internal static string SettingsPublicFeedbackChannel {
get {
return ResourceManager.GetString("SettingsPublicFeedbackChannel", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to Receive startup messages. /// Looks up a localized string similar to Receive startup messages.
/// </summary> /// </summary>
@ -824,6 +833,15 @@ namespace Boyfriend {
} }
} }
/// <summary>
/// Looks up a localized string similar to Return roles on rejoin.
/// </summary>
internal static string SettingsReturnRolesOnRejoin {
get {
return ResourceManager.GetString("SettingsReturnRolesOnRejoin", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to Send welcome messages. /// Looks up a localized string similar to Send welcome messages.
/// </summary> /// </summary>
@ -1050,11 +1068,11 @@ namespace Boyfriend {
} }
/// <summary> /// <summary>
/// Looks up a localized string similar to The specified user is not a member of this server!. /// Looks up a localized string similar to I could not find this user in any guild I&apos;m a member of! Check if the ID is correct and that the user was on this server no longer than 30 days ago.
/// </summary> /// </summary>
internal static string UserNotInGuild { internal static string UserNotFound {
get { get {
return ResourceManager.GetString("UserNotInGuild", resourceCulture); return ResourceManager.GetString("UserNotFound", resourceCulture);
} }
} }
@ -1068,7 +1086,7 @@ namespace Boyfriend {
} }
/// <summary> /// <summary>
/// Looks up a localized string similar to You were banned by {0} in guild {1} for {2}. /// Looks up a localized string similar to You were banned by {0} in guild `{1}` for {2}.
/// </summary> /// </summary>
internal static string YouWereBanned { internal static string YouWereBanned {
get { get {
@ -1077,7 +1095,7 @@ namespace Boyfriend {
} }
/// <summary> /// <summary>
/// Looks up a localized string similar to You were kicked by {0} in guild {1} for {2}. /// Looks up a localized string similar to You were kicked by {0} in guild `{1}` for {2}.
/// </summary> /// </summary>
internal static string YouWereKicked { internal static string YouWereKicked {
get { get {

View file

@ -1,64 +1,64 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<root> <root>
<!-- <!--
Microsoft ResX Schema Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64 Version 2.0
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter The primary goals of this format is to allow a simple XML format
: and then encoded with base64 encoding. that is mostly human readable. The generation and parsing of the
--> various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata"> <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" /> <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true"> <xsd:element name="root" msdata:IsDataSet="true">
@ -135,43 +135,43 @@
<data name="Beep2" xml:space="preserve"> <data name="Beep2" xml:space="preserve">
<value>Bop! </value> <value>Bop! </value>
</data> </data>
<data name="Beep3" xml:space="preserve"> <data name="Beep3" xml:space="preserve">
<value>Beep! </value> <value>Beep! </value>
</data> </data>
<data name="CommandNoPermissionBot" xml:space="preserve"> <data name="CommandNoPermissionBot" xml:space="preserve">
<value>I do not have permission to execute this command!</value> <value>I do not have permission to execute this command!</value>
</data> </data>
<data name="CommandNoPermissionUser" xml:space="preserve"> <data name="CommandNoPermissionUser" xml:space="preserve">
<value>You do not have permission to execute this command!</value> <value>You do not have permission to execute this command!</value>
</data> </data>
<data name="YouWereBanned" xml:space="preserve"> <data name="YouWereBanned" xml:space="preserve">
<value>You were banned by {0} in guild {1} for {2}</value> <value>You were banned by {0} in guild `{1}` for {2}</value>
</data> </data>
<data name="PunishmentExpired" xml:space="preserve"> <data name="PunishmentExpired" xml:space="preserve">
<value>Punishment expired</value> <value>Punishment expired</value>
</data> </data>
<data name="ClearAmountTooSmall" xml:space="preserve"> <data name="ClearAmountTooSmall" xml:space="preserve">
<value>You specified less than {0} messages!</value> <value>You specified less than {0} messages!</value>
</data> </data>
<data name="ClearAmountTooLarge" xml:space="preserve"> <data name="ClearAmountTooLarge" xml:space="preserve">
<value>You specified more than {0} messages!</value> <value>You specified more than {0} messages!</value>
</data> </data>
<data name="CommandHelp" xml:space="preserve"> <data name="CommandHelp" xml:space="preserve">
<value>Command help:</value> <value>Command help:</value>
</data> </data>
<data name="YouWereKicked" xml:space="preserve"> <data name="YouWereKicked" xml:space="preserve">
<value>You were kicked by {0} in guild {1} for {2}</value> <value>You were kicked by {0} in guild `{1}` for {2}</value>
</data> </data>
<data name="Milliseconds" xml:space="preserve"> <data name="Milliseconds" xml:space="preserve">
<value>ms</value> <value>ms</value>
</data> </data>
<data name="MemberAlreadyMuted" xml:space="preserve"> <data name="MemberAlreadyMuted" xml:space="preserve">
<value>Member is already muted!</value> <value>Member is already muted!</value>
</data> </data>
<data name="ChannelNotSpecified" xml:space="preserve"> <data name="ChannelNotSpecified" xml:space="preserve">
<value>Not specified</value> <value>Not specified</value>
</data> </data>
<data name="RoleNotSpecified" xml:space="preserve"> <data name="RoleNotSpecified" xml:space="preserve">
<value>Not specified</value> <value>Not specified</value>
</data> </data>
<data name="CurrentSettings" xml:space="preserve"> <data name="CurrentSettings" xml:space="preserve">
@ -189,16 +189,10 @@
<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"> <data name="SettingsMuteRole" xml:space="preserve">
<value>Starter role</value>
</data>
<data name="SettingsMuteRole" xml:space="preserve">
<value>Mute role</value> <value>Mute role</value>
</data> </data>
<data name="SettingsBotLogChannel" xml:space="preserve"> <data name="LanguageNotSupported" xml:space="preserve">
<value>Bot log channel</value>
</data>
<data name="LanguageNotSupported" xml:space="preserve">
<value>Language not supported! Supported languages:</value> <value>Language not supported! Supported languages:</value>
</data> </data>
<data name="Yes" xml:space="preserve"> <data name="Yes" xml:space="preserve">
@ -213,10 +207,7 @@
<data name="MemberNotMuted" xml:space="preserve"> <data name="MemberNotMuted" xml:space="preserve">
<value>Member not muted!</value> <value>Member not muted!</value>
</data> </data>
<data name="RolesReturned" xml:space="preserve"> <data name="SettingsWelcomeMessage" xml:space="preserve">
<value>Someone removed the mute role manually! I added back all roles that I removed during the mute</value>
</data>
<data name="SettingsWelcomeMessage" xml:space="preserve">
<value>Welcome message</value> <value>Welcome message</value>
</data> </data>
<data name="ClearAmountInvalid" xml:space="preserve"> <data name="ClearAmountInvalid" xml:space="preserve">
@ -225,9 +216,6 @@
<data name="FeedbackUserBanned" xml:space="preserve"> <data name="FeedbackUserBanned" xml:space="preserve">
<value>Banned {0} for{1}: {2}</value> <value>Banned {0} for{1}: {2}</value>
</data> </data>
<data name="UserNotInGuild" xml:space="preserve">
<value>The specified user is not a member of this server!</value>
</data>
<data name="SettingDoesntExist" xml:space="preserve"> <data name="SettingDoesntExist" xml:space="preserve">
<value>That setting doesn't exist!</value> <value>That setting doesn't exist!</value>
</data> </data>
@ -243,10 +231,7 @@
<data name="InvalidChannel" xml:space="preserve"> <data name="InvalidChannel" xml:space="preserve">
<value>This channel does not exist!</value> <value>This channel does not exist!</value>
</data> </data>
<data name="RoleRemovalFailed" xml:space="preserve"> <data name="DurationRequiredForTimeOuts" xml:space="preserve">
<value>I couldn't remove role {0} because of an error! {1}</value>
</data>
<data name="DurationRequiredForTimeOuts" xml:space="preserve">
<value>I cannot mute someone for more than 28 days using timeouts! Either specify a duration shorter than 28 days, or set a mute role in settings</value> <value>I cannot mute someone for more than 28 days using timeouts! Either specify a duration shorter than 28 days, or set a mute role in settings</value>
</data> </data>
<data name="CannotTimeOutBot" xml:space="preserve"> <data name="CannotTimeOutBot" xml:space="preserve">
@ -333,28 +318,28 @@
<data name="MissingNumber" xml:space="preserve"> <data name="MissingNumber" xml:space="preserve">
<value>You need to specify an integer from {0} to {1}!</value> <value>You need to specify an integer from {0} to {1}!</value>
</data> </data>
<data name="MissingUser" xml:space="preserve"> <data name="MissingUser" xml:space="preserve">
<value>You need to specify a user!</value> <value>You need to specify a user!</value>
</data> </data>
<data name="InvalidUser" xml:space="preserve"> <data name="InvalidUser" xml:space="preserve">
<value>You need to specify a user instead of {0}!</value> <value>You need to specify a user instead of {0}!</value>
</data> </data>
<data name="MissingMember" xml:space="preserve"> <data name="MissingMember" xml:space="preserve">
<value>You need to specify a guild member!</value> <value>You need to specify a guild member!</value>
</data> </data>
<data name="InvalidMember" xml:space="preserve"> <data name="InvalidMember" xml:space="preserve">
<value>You need to specify a guild member instead of {0}!</value> <value>You did not specify a member of this guild!</value>
</data> </data>
<data name="UserCannotBanMembers" xml:space="preserve"> <data name="UserCannotBanMembers" xml:space="preserve">
<value>You cannot ban users from this guild!</value> <value>You cannot ban users from this guild!</value>
</data> </data>
<data name="UserCannotManageMessages" xml:space="preserve"> <data name="UserCannotManageMessages" xml:space="preserve">
<value>You cannot manage messages in this guild!</value> <value>You cannot manage messages in this guild!</value>
</data> </data>
<data name="UserCannotKickMembers" xml:space="preserve"> <data name="UserCannotKickMembers" xml:space="preserve">
<value>You cannot kick members from this guild!</value> <value>You cannot kick members from this guild!</value>
</data> </data>
<data name="UserCannotModerateMembers" xml:space="preserve"> <data name="UserCannotModerateMembers" xml:space="preserve">
<value>You cannot moderate members in this guild!</value> <value>You cannot moderate members in this guild!</value>
</data> </data>
<data name="UserCannotManageGuild" xml:space="preserve"> <data name="UserCannotManageGuild" xml:space="preserve">
@ -390,10 +375,7 @@
<data name="MissingUnmuteReason" xml:space="preserve"> <data name="MissingUnmuteReason" xml:space="preserve">
<value>You need to specify a reason for unmute this member!</value> <value>You need to specify a reason for unmute this member!</value>
</data> </data>
<data name="MissingSetting" xml:space="preserve"> <data name="UserCannotBanOwner" xml:space="preserve">
<value>You need to specify a setting to change!</value>
</data>
<data name="UserCannotBanOwner" xml:space="preserve">
<value>You cannot ban the owner of this guild!</value> <value>You cannot ban the owner of this guild!</value>
</data> </data>
<data name="UserCannotBanThemselves" xml:space="preserve"> <data name="UserCannotBanThemselves" xml:space="preserve">
@ -450,13 +432,37 @@
<data name="BotCannotUnmuteTarget" xml:space="preserve"> <data name="BotCannotUnmuteTarget" xml:space="preserve">
<value>I cannot unmute this member!</value> <value>I cannot unmute this member!</value>
</data> </data>
<data name="UserCannotUnmuteTarget" xml:space="preserve"> <data name="UserCannotUnmuteTarget" xml:space="preserve">
<value>You cannot unmute this user!</value> <value>You cannot unmute this user!</value>
</data> </data>
<data name="EventEarlyNotification" xml:space="preserve"> <data name="EventEarlyNotification" xml:space="preserve">
<value>{0}Event {1} will start &lt;t:{2}:R&gt;!</value> <value>{0}Event {1} will start &lt;t:{2}:R&gt;!</value>
</data> </data>
<data name="SettingsEventEarlyNotificationOffset" xml:space="preserve"> <data name="SettingsEventEarlyNotificationOffset" xml:space="preserve">
<value>Early event start notification offset</value> <value>Early event start notification offset</value>
</data> </data>
</root> <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</value>
</data>
<data name="SettingsStarterRole" xml:space="preserve">
<value>Starter role</value>
</data>
<data name="CommandDescriptionRemind" xml:space="preserve">
<value>Adds a reminder</value>
</data>
<data name="SettingsPublicFeedbackChannel" xml:space="preserve">
<value>Channel for public notifications</value>
</data>
<data name="SettingsPrivateFeedbackChannel" xml:space="preserve">
<value>Channel for private notifications</value>
</data>
<data name="SettingsReturnRolesOnRejoin" xml:space="preserve">
<value>Return roles on rejoin</value>
</data>
<data name="SettingsAutoStartEvents" xml:space="preserve">
<value>Automatically start scheduled events</value>
</data>
<data name="MissingReminderText" xml:space="preserve">
<value>You need to specify reminder text!</value>
</data>
</root>

View file

@ -1,64 +1,64 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<root> <root>
<!-- <!--
Microsoft ResX Schema Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64 Version 2.0
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter The primary goals of this format is to allow a simple XML format
: and then encoded with base64 encoding. that is mostly human readable. The generation and parsing of the
--> various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata"> <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" /> <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true"> <xsd:element name="root" msdata:IsDataSet="true">
@ -135,43 +135,43 @@
<data name="Beep2" xml:space="preserve"> <data name="Beep2" xml:space="preserve">
<value>Боп! </value> <value>Боп! </value>
</data> </data>
<data name="Beep3" xml:space="preserve"> <data name="Beep3" xml:space="preserve">
<value>Бип! </value> <value>Бип! </value>
</data> </data>
<data name="CommandNoPermissionBot" xml:space="preserve"> <data name="CommandNoPermissionBot" xml:space="preserve">
<value>У меня недостаточно прав для выполнения этой команды!</value> <value>У меня недостаточно прав для выполнения этой команды!</value>
</data> </data>
<data name="CommandNoPermissionUser" xml:space="preserve"> <data name="CommandNoPermissionUser" xml:space="preserve">
<value>У тебя недостаточно прав для выполнения этой команды!</value> <value>У тебя недостаточно прав для выполнения этой команды!</value>
</data> </data>
<data name="YouWereBanned" xml:space="preserve"> <data name="YouWereBanned" xml:space="preserve">
<value>Тебя забанил {0} на сервере {1} за {2}</value> <value>Тебя забанил {0} на сервере `{1}` за {2}</value>
</data> </data>
<data name="PunishmentExpired" xml:space="preserve"> <data name="PunishmentExpired" xml:space="preserve">
<value>Время наказания истекло</value> <value>Время наказания истекло</value>
</data> </data>
<data name="ClearAmountTooSmall" xml:space="preserve"> <data name="ClearAmountTooSmall" xml:space="preserve">
<value>Указано менее {0} сообщений!</value> <value>Указано менее {0} сообщений!</value>
</data> </data>
<data name="ClearAmountTooLarge" xml:space="preserve"> <data name="ClearAmountTooLarge" xml:space="preserve">
<value>Указано более {0} сообщений!</value> <value>Указано более {0} сообщений!</value>
</data> </data>
<data name="CommandHelp" xml:space="preserve"> <data name="CommandHelp" xml:space="preserve">
<value>Справка по командам:</value> <value>Справка по командам:</value>
</data> </data>
<data name="YouWereKicked" xml:space="preserve"> <data name="YouWereKicked" xml:space="preserve">
<value>Тебя кикнул {0} на сервере {1} за {2}</value> <value>Тебя кикнул {0} на сервере `{1}` за {2}</value>
</data> </data>
<data name="Milliseconds" xml:space="preserve"> <data name="Milliseconds" xml:space="preserve">
<value>мс</value> <value>мс</value>
</data> </data>
<data name="MemberAlreadyMuted" xml:space="preserve"> <data name="MemberAlreadyMuted" xml:space="preserve">
<value>Участник уже заглушен!</value> <value>Участник уже заглушен!</value>
</data> </data>
<data name="ChannelNotSpecified" xml:space="preserve"> <data name="ChannelNotSpecified" xml:space="preserve">
<value>Не указан</value> <value>Не указан</value>
</data> </data>
<data name="RoleNotSpecified" xml:space="preserve"> <data name="RoleNotSpecified" xml:space="preserve">
<value>Не указана</value> <value>Не указана</value>
</data> </data>
<data name="CurrentSettings" xml:space="preserve"> <data name="CurrentSettings" xml:space="preserve">
@ -192,10 +192,7 @@
<data name="SettingsMuteRole" xml:space="preserve"> <data name="SettingsMuteRole" xml:space="preserve">
<value>Роль мута</value> <value>Роль мута</value>
</data> </data>
<data name="SettingsBotLogChannel" xml:space="preserve"> <data name="LanguageNotSupported" xml:space="preserve">
<value>Канал бот-уведомлений</value>
</data>
<data name="LanguageNotSupported" xml:space="preserve">
<value>Язык не поддерживается! Поддерживаемые языки:</value> <value>Язык не поддерживается! Поддерживаемые языки:</value>
</data> </data>
<data name="Yes" xml:space="preserve"> <data name="Yes" xml:space="preserve">
@ -210,10 +207,7 @@
<data name="MemberNotMuted" xml:space="preserve"> <data name="MemberNotMuted" xml:space="preserve">
<value>Участник не заглушен!</value> <value>Участник не заглушен!</value>
</data> </data>
<data name="RolesReturned" xml:space="preserve"> <data name="SettingsWelcomeMessage" xml:space="preserve">
<value>Кто-то убрал роль мута самостоятельно! Я вернул все роли, которые забрал при муте</value>
</data>
<data name="SettingsWelcomeMessage" xml:space="preserve">
<value>Приветствие</value> <value>Приветствие</value>
</data> </data>
<data name="ClearAmountInvalid" xml:space="preserve"> <data name="ClearAmountInvalid" xml:space="preserve">
@ -222,9 +216,6 @@
<data name="FeedbackUserBanned" xml:space="preserve"> <data name="FeedbackUserBanned" xml:space="preserve">
<value>Забанен {0} на{1}: {2}</value> <value>Забанен {0} на{1}: {2}</value>
</data> </data>
<data name="UserNotInGuild" xml:space="preserve">
<value>Указанный пользователь не является участником этого сервера!</value>
</data>
<data name="SettingDoesntExist" xml:space="preserve"> <data name="SettingDoesntExist" xml:space="preserve">
<value>Такая настройка не существует!</value> <value>Такая настройка не существует!</value>
</data> </data>
@ -240,19 +231,13 @@
<data name="InvalidChannel" xml:space="preserve"> <data name="InvalidChannel" xml:space="preserve">
<value>Этот канал не существует!</value> <value>Этот канал не существует!</value>
</data> </data>
<data name="RoleRemovalFailed" xml:space="preserve"> <data name="DurationRequiredForTimeOuts" xml:space="preserve">
<value>Я не смог забрать роль {0} в связи с ошибкой! {1}</value>
</data>
<data name="DurationRequiredForTimeOuts" xml:space="preserve">
<value>Я не могу заглушить кого-то на более чем 28 дней, используя тайм-ауты! Или укажи продолжительность менее 28 дней, или установи роль мута в настройках</value> <value>Я не могу заглушить кого-то на более чем 28 дней, используя тайм-ауты! Или укажи продолжительность менее 28 дней, или установи роль мута в настройках</value>
</data> </data>
<data name="CannotTimeOutBot" xml:space="preserve"> <data name="CannotTimeOutBot" xml:space="preserve">
<value>Я не могу использовать тайм-ауты на других ботах! Попробуй указать роль мута в настройках</value> <value>Я не могу использовать тайм-ауты на других ботах! Попробуй указать роль мута в настройках</value>
</data> </data>
<data name="SettingsStarterRole" xml:space="preserve"> <data name="EventCreated" xml:space="preserve">
<value>Начальная роль</value>
</data>
<data name="EventCreated" xml:space="preserve">
<value>{0} создал событие {1}! Оно пройдёт в {2} и начнётся &lt;t:{3}:R&gt;!{4}</value> <value>{0} создал событие {1}! Оно пройдёт в {2} и начнётся &lt;t:{3}:R&gt;!{4}</value>
</data> </data>
<data name="SettingsEventNotificationRole" xml:space="preserve"> <data name="SettingsEventNotificationRole" xml:space="preserve">
@ -333,28 +318,28 @@
<data name="MissingNumber" xml:space="preserve"> <data name="MissingNumber" xml:space="preserve">
<value>Надо указать целое число от {0} до {1}!</value> <value>Надо указать целое число от {0} до {1}!</value>
</data> </data>
<data name="MissingUser" xml:space="preserve"> <data name="MissingUser" xml:space="preserve">
<value>Надо указать пользователя!</value> <value>Надо указать пользователя!</value>
</data> </data>
<data name="InvalidUser" xml:space="preserve"> <data name="InvalidUser" xml:space="preserve">
<value>Надо указать пользователя вместо {0}!</value> <value>Надо указать пользователя вместо {0}!</value>
</data> </data>
<data name="MissingMember" xml:space="preserve"> <data name="MissingMember" xml:space="preserve">
<value>Надо указать участника сервера!</value> <value>Надо указать участника сервера!</value>
</data> </data>
<data name="InvalidMember" xml:space="preserve"> <data name="InvalidMember" xml:space="preserve">
<value>Надо указать участника сервера вместо {0}!</value> <value>Тебе надо указать участника этого сервера!</value>
</data> </data>
<data name="UserCannotBanMembers" xml:space="preserve"> <data name="UserCannotBanMembers" xml:space="preserve">
<value>Ты не можешь банить пользователей на этом сервере!</value> <value>Ты не можешь банить пользователей на этом сервере!</value>
</data> </data>
<data name="UserCannotManageMessages" xml:space="preserve"> <data name="UserCannotManageMessages" xml:space="preserve">
<value>Ты не можешь управлять сообщениями этого сервера!</value> <value>Ты не можешь управлять сообщениями этого сервера!</value>
</data> </data>
<data name="UserCannotKickMembers" xml:space="preserve"> <data name="UserCannotKickMembers" xml:space="preserve">
<value>Ты не можешь выгонять участников с этого сервера!</value> <value>Ты не можешь выгонять участников с этого сервера!</value>
</data> </data>
<data name="UserCannotModerateMembers" xml:space="preserve"> <data name="UserCannotModerateMembers" xml:space="preserve">
<value>Ты не можешь модерировать участников этого сервера!</value> <value>Ты не можешь модерировать участников этого сервера!</value>
</data> </data>
<data name="UserCannotManageGuild" xml:space="preserve"> <data name="UserCannotManageGuild" xml:space="preserve">
@ -384,10 +369,7 @@
<data name="MissingMuteReason" xml:space="preserve"> <data name="MissingMuteReason" xml:space="preserve">
<value>Надо указать причину для мута этого участника!</value> <value>Надо указать причину для мута этого участника!</value>
</data> </data>
<data name="MissingSetting" xml:space="preserve"> <data name="MissingUnbanReason" xml:space="preserve">
<value>Надо указать настройку, которую нужно изменить!</value>
</data>
<data name="MissingUnbanReason" xml:space="preserve">
<value>Надо указать причину для разбана этого пользователя!</value> <value>Надо указать причину для разбана этого пользователя!</value>
</data> </data>
<data name="MissingUnmuteReason" xml:space="preserve"> <data name="MissingUnmuteReason" xml:space="preserve">
@ -450,13 +432,37 @@
<data name="UserCannotUnmuteTarget" xml:space="preserve"> <data name="UserCannotUnmuteTarget" xml:space="preserve">
<value>Ты не можешь вернуть из мута этого пользователя!</value> <value>Ты не можешь вернуть из мута этого пользователя!</value>
</data> </data>
<data name="BotCannotUnmuteTarget" xml:space="preserve"> <data name="BotCannotUnmuteTarget" xml:space="preserve">
<value>Я не могу вернуть из мута этого пользователя!</value> <value>Я не могу вернуть из мута этого пользователя!</value>
</data> </data>
<data name="EventEarlyNotification" xml:space="preserve"> <data name="EventEarlyNotification" xml:space="preserve">
<value>{0}Событие {1} начнется &lt;t:{2}:R&gt;!</value> <value>{0}Событие {1} начнется &lt;t:{2}:R&gt;!</value>
</data> </data>
<data name="SettingsEventEarlyNotificationOffset" xml:space="preserve"> <data name="SettingsEventEarlyNotificationOffset" xml:space="preserve">
<value>Офсет отправки преждевременного уведомления о начале события</value> <value>Офсет отправки преждевременного уведомления о начале события</value>
</data> </data>
</root> <data name="UserNotFound" xml:space="preserve">
<value>Я не смог найти этого пользователя ни в одном из серверов, в которых я есть. Проверь правильность ID и нахождение пользователя на этом сервере максимум 30 дней назад</value>
</data>
<data name="SettingsStarterRole" xml:space="preserve">
<value>Начальная роль</value>
</data>
<data name="CommandDescriptionRemind" xml:space="preserve">
<value>Добавляет напоминание</value>
</data>
<data name="SettingsPublicFeedbackChannel" xml:space="preserve">
<value>Канал для публичных уведомлений</value>
</data>
<data name="SettingsPrivateFeedbackChannel" xml:space="preserve">
<value>Канал для приватных уведомлений</value>
</data>
<data name="SettingsReturnRolesOnRejoin" xml:space="preserve">
<value>Возвращать роли при перезаходе</value>
</data>
<data name="SettingsAutoStartEvents" xml:space="preserve">
<value>Автоматически начинать события</value>
</data>
<data name="MissingReminderText" xml:space="preserve">
<value>Тебе нужно указать текст напоминания!</value>
</data>
</root>

View file

@ -1,64 +1,64 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<root> <root>
<!-- <!--
Microsoft ResX Schema Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64 Version 2.0
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter The primary goals of this format is to allow a simple XML format
: and then encoded with base64 encoding. that is mostly human readable. The generation and parsing of the
--> various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata"> <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" /> <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true"> <xsd:element name="root" msdata:IsDataSet="true">
@ -135,43 +135,43 @@
<data name="Beep2" xml:space="preserve"> <data name="Beep2" xml:space="preserve">
<value>брох! </value> <value>брох! </value>
</data> </data>
<data name="Beep3" xml:space="preserve"> <data name="Beep3" xml:space="preserve">
<value>брух! </value> <value>брух! </value>
</data> </data>
<data name="CommandNoPermissionBot" xml:space="preserve"> <data name="CommandNoPermissionBot" xml:space="preserve">
<value>у меня прав нету, сделай что нибудь.</value> <value>у меня прав нету, сделай что нибудь.</value>
</data> </data>
<data name="CommandNoPermissionUser" xml:space="preserve"> <data name="CommandNoPermissionUser" xml:space="preserve">
<value>у тебя прав нету, твои проблемы.</value> <value>у тебя прав нету, твои проблемы.</value>
</data> </data>
<data name="YouWereBanned" xml:space="preserve"> <data name="YouWereBanned" xml:space="preserve">
<value>здарова, тебя крч забанил {0} на сервере {1} за {2}</value> <value>здарова, тебя крч забанил {0} на сервере `{1}` за {2}</value>
</data> </data>
<data name="PunishmentExpired" xml:space="preserve"> <data name="PunishmentExpired" xml:space="preserve">
<value>время бана закончиловсь</value> <value>время бана закончиловсь</value>
</data> </data>
<data name="ClearAmountTooSmall" xml:space="preserve"> <data name="ClearAmountTooSmall" xml:space="preserve">
<value>ты выбрал менее {0} сообщений</value> <value>ты выбрал менее {0} сообщений</value>
</data> </data>
<data name="ClearAmountTooLarge" xml:space="preserve"> <data name="ClearAmountTooLarge" xml:space="preserve">
<value>ты выбрал более {0} сообщений</value> <value>ты выбрал более {0} сообщений</value>
</data> </data>
<data name="CommandHelp" xml:space="preserve"> <data name="CommandHelp" xml:space="preserve">
<value>туториал по приколам:</value> <value>туториал по приколам:</value>
</data> </data>
<data name="YouWereKicked" xml:space="preserve"> <data name="YouWereKicked" xml:space="preserve">
<value>здарова, тебя крч кикнул {0} на сервере {1} за {2}</value> <value>здарова, тебя крч кикнул {0} на сервере `{1}` за {2}</value>
</data> </data>
<data name="Milliseconds" xml:space="preserve"> <data name="Milliseconds" xml:space="preserve">
<value>мс</value> <value>мс</value>
</data> </data>
<data name="MemberAlreadyMuted" xml:space="preserve"> <data name="MemberAlreadyMuted" xml:space="preserve">
<value>шизоид уже замучен!</value> <value>шизоид уже замучен!</value>
</data> </data>
<data name="ChannelNotSpecified" xml:space="preserve"> <data name="ChannelNotSpecified" xml:space="preserve">
<value>*тут ничего нет*</value> <value>*тут ничего нет*</value>
</data> </data>
<data name="RoleNotSpecified" xml:space="preserve"> <data name="RoleNotSpecified" xml:space="preserve">
<value>*тут ничего нет*</value> <value>*тут ничего нет*</value>
</data> </data>
<data name="CurrentSettings" xml:space="preserve"> <data name="CurrentSettings" xml:space="preserve">
@ -192,10 +192,7 @@
<data name="SettingsMuteRole" xml:space="preserve"> <data name="SettingsMuteRole" xml:space="preserve">
<value>роль замученного</value> <value>роль замученного</value>
</data> </data>
<data name="SettingsBotLogChannel" xml:space="preserve"> <data name="LanguageNotSupported" xml:space="preserve">
<value>канал бот-уведомлений</value>
</data>
<data name="LanguageNotSupported" xml:space="preserve">
<value>такого языка нету, ты шо, есть только такие:</value> <value>такого языка нету, ты шо, есть только такие:</value>
</data> </data>
<data name="Yes" xml:space="preserve"> <data name="Yes" xml:space="preserve">
@ -210,10 +207,7 @@
<data name="MemberNotMuted" xml:space="preserve"> <data name="MemberNotMuted" xml:space="preserve">
<value>шизоид не замучен!</value> <value>шизоид не замучен!</value>
</data> </data>
<data name="RolesReturned" xml:space="preserve"> <data name="SettingsWelcomeMessage" xml:space="preserve">
<value>кто-то решил поумничать и обошел роль мута. я ее вернул.</value>
</data>
<data name="SettingsWelcomeMessage" xml:space="preserve">
<value>приветствие</value> <value>приветствие</value>
</data> </data>
<data name="ClearAmountInvalid" xml:space="preserve"> <data name="ClearAmountInvalid" xml:space="preserve">
@ -222,9 +216,6 @@
<data name="FeedbackUserBanned" xml:space="preserve"> <data name="FeedbackUserBanned" xml:space="preserve">
<value>забанен {0} на{1}: {2}</value> <value>забанен {0} на{1}: {2}</value>
</data> </data>
<data name="UserNotInGuild" xml:space="preserve">
<value>шизик не на этом сервере</value>
</data>
<data name="SettingDoesntExist" xml:space="preserve"> <data name="SettingDoesntExist" xml:space="preserve">
<value>такой прикол не существует</value> <value>такой прикол не существует</value>
</data> </data>
@ -240,19 +231,13 @@
<data name="InvalidChannel" xml:space="preserve"> <data name="InvalidChannel" xml:space="preserve">
<value>этого канала нету, ты шо</value> <value>этого канала нету, ты шо</value>
</data> </data>
<data name="RoleRemovalFailed" xml:space="preserve"> <data name="DurationRequiredForTimeOuts" xml:space="preserve">
<value>я не украл звание {0} в связи с ошибкой! {1}</value>
</data>
<data name="DurationRequiredForTimeOuts" xml:space="preserve">
<value>ты шо, мутить больше чем на 28 дней таймаут не разрешает, вот настроишь роль мута, тогда поговорим</value> <value>ты шо, мутить больше чем на 28 дней таймаут не разрешает, вот настроишь роль мута, тогда поговорим</value>
</data> </data>
<data name="CannotTimeOutBot" xml:space="preserve"> <data name="CannotTimeOutBot" xml:space="preserve">
<value>я не могу замутить ботов, сделай что нибудь</value> <value>я не могу замутить ботов, сделай что нибудь</value>
</data> </data>
<data name="SettingsStarterRole" xml:space="preserve"> <data name="EventCreated" xml:space="preserve">
<value>базовое звание</value>
</data>
<data name="EventCreated" xml:space="preserve">
<value>{0} приготовил новый квест {1}! он пройдёт в {2} и начнётся &lt;t:{3}:R&gt;!{4}</value> <value>{0} приготовил новый квест {1}! он пройдёт в {2} и начнётся &lt;t:{3}:R&gt;!{4}</value>
</data> </data>
<data name="SettingsEventNotificationRole" xml:space="preserve"> <data name="SettingsEventNotificationRole" xml:space="preserve">
@ -333,28 +318,28 @@
<data name="MissingNumber" xml:space="preserve"> <data name="MissingNumber" xml:space="preserve">
<value>укажи целое число от {0} до {1}</value> <value>укажи целое число от {0} до {1}</value>
</data> </data>
<data name="MissingUser" xml:space="preserve"> <data name="MissingUser" xml:space="preserve">
<value>укажи самого шизика</value> <value>укажи самого шизика</value>
</data> </data>
<data name="InvalidUser" xml:space="preserve"> <data name="InvalidUser" xml:space="preserve">
<value>надо указать юзверя вместо {0}!</value> <value>надо указать юзверя вместо {0}!</value>
</data> </data>
<data name="MissingMember" xml:space="preserve"> <data name="MissingMember" xml:space="preserve">
<value>укажи самого шизика</value> <value>укажи самого шизика</value>
</data> </data>
<data name="InvalidMember" xml:space="preserve"> <data name="InvalidMember" xml:space="preserve">
<value>укажи шизоида сервера вместо {0}!</value> <value>укажи шизоида сервера!</value>
</data> </data>
<data name="UserCannotBanMembers" xml:space="preserve"> <data name="UserCannotBanMembers" xml:space="preserve">
<value>бан</value> <value>бан</value>
</data> </data>
<data name="UserCannotManageMessages" xml:space="preserve"> <data name="UserCannotManageMessages" xml:space="preserve">
<value>тебе нельзя иметь власть над сообщениями шизоидов</value> <value>тебе нельзя иметь власть над сообщениями шизоидов</value>
</data> </data>
<data name="UserCannotKickMembers" xml:space="preserve"> <data name="UserCannotKickMembers" xml:space="preserve">
<value>кик шизиков нельзя</value> <value>кик шизиков нельзя</value>
</data> </data>
<data name="UserCannotModerateMembers" xml:space="preserve"> <data name="UserCannotModerateMembers" xml:space="preserve">
<value>тебе нельзя управлять шизоидами</value> <value>тебе нельзя управлять шизоидами</value>
</data> </data>
<data name="UserCannotManageGuild" xml:space="preserve"> <data name="UserCannotManageGuild" xml:space="preserve">
@ -384,10 +369,7 @@
<data name="MissingMuteReason" xml:space="preserve"> <data name="MissingMuteReason" xml:space="preserve">
<value>укажи зачем мутить шизика</value> <value>укажи зачем мутить шизика</value>
</data> </data>
<data name="MissingSetting" xml:space="preserve"> <data name="MissingUnbanReason" xml:space="preserve">
<value>укажи настройку которую менять нужно</value>
</data>
<data name="MissingUnbanReason" xml:space="preserve">
<value>укажи зачем раззабанивать шизика</value> <value>укажи зачем раззабанивать шизика</value>
</data> </data>
<data name="MissingUnmuteReason" xml:space="preserve"> <data name="MissingUnmuteReason" xml:space="preserve">
@ -450,13 +432,37 @@
<data name="UserCannotUnmuteTarget" xml:space="preserve"> <data name="UserCannotUnmuteTarget" xml:space="preserve">
<value>тебе нельзя раззамучивать</value> <value>тебе нельзя раззамучивать</value>
</data> </data>
<data name="BotCannotUnmuteTarget" xml:space="preserve"> <data name="BotCannotUnmuteTarget" xml:space="preserve">
<value>я не могу его раззамутить...</value> <value>я не могу его раззамутить...</value>
</data> </data>
<data name="EventEarlyNotification" xml:space="preserve"> <data name="EventEarlyNotification" xml:space="preserve">
<value>{0}квест {1} начнется &lt;t:{2}:R&gt;!</value> <value>{0}квест {1} начнется &lt;t:{2}:R&gt;!</value>
</data> </data>
<data name="SettingsEventEarlyNotificationOffset" xml:space="preserve"> <data name="SettingsEventEarlyNotificationOffset" xml:space="preserve">
<value>заранее пнуть в минутах до начала квеста</value> <value>заранее пнуть в минутах до начала квеста</value>
</data> </data>
</root> <data name="UserNotFound" xml:space="preserve">
<value>у нас такого шизоида нету, проверь, валиден ли ID уважаемого (я забываю о шизоидах если они ливнули минимум месяц назад)</value>
</data>
<data name="SettingsStarterRole" xml:space="preserve">
<value>базовое звание</value>
</data>
<data name="CommandDescriptionRemind" xml:space="preserve">
<value>крафтит напоминалку</value>
</data>
<data name="SettingsPrivateFeedbackChannel" xml:space="preserve">
<value>канал для секретных уведомлений</value>
</data>
<data name="SettingsPublicFeedbackChannel" xml:space="preserve">
<value>канал для не секретных уведомлений</value>
</data>
<data name="SettingsReturnRolesOnRejoin" xml:space="preserve">
<value>вернуть звания при переподключении в дурку</value>
</data>
<data name="SettingsAutoStartEvents" xml:space="preserve">
<value>автоматом стартить квесты</value>
</data>
<data name="MissingReminderText" xml:space="preserve">
<value>для крафта напоминалки нужен текст</value>
</data>
</root>

View file

@ -2,7 +2,6 @@ namespace Boyfriend;
public static class ReplyEmojis { public static class ReplyEmojis {
public const string Success = ":white_check_mark:"; public const string Success = ":white_check_mark:";
public const string Warning = ":warning:";
public const string Error = ":x:"; public const string Error = ":x:";
public const string MissingArgument = ":keyboard:"; public const string MissingArgument = ":keyboard:";
public const string InvalidArgument = ":construction:"; public const string InvalidArgument = ":construction:";

View file

@ -1,8 +1,9 @@
using System.Globalization; using System.Diagnostics;
using System.Globalization;
using System.Reflection; using System.Reflection;
using System.Text; using System.Text;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using Boyfriend.Commands; using Boyfriend.Data;
using Discord; using Discord;
using Discord.Net; using Discord.Net;
using Discord.WebSocket; using Discord.WebSocket;
@ -12,15 +13,13 @@ using Humanizer.Localisation;
namespace Boyfriend; namespace Boyfriend;
public static partial class Utils { public static partial class Utils {
private static readonly Dictionary<string, string> ReflectionMessageCache = new();
public static readonly Dictionary<string, CultureInfo> CultureInfoCache = new() { public static readonly Dictionary<string, CultureInfo> CultureInfoCache = new() {
{ "ru", new CultureInfo("ru-RU") }, { "ru", new CultureInfo("ru-RU") },
{ "en", new CultureInfo("en-US") }, { "en", new CultureInfo("en-US") },
{ "mctaylors-ru", new CultureInfo("tt-RU") } { "mctaylors-ru", new CultureInfo("tt-RU") }
}; };
private static readonly Dictionary<ulong, SocketRole> MuteRoleCache = new(); private static readonly Dictionary<string, string> ReflectionMessageCache = new();
private static readonly AllowedMentions AllowRoles = new() { private static readonly AllowedMentions AllowRoles = new() {
AllowedTypes = AllowedMentionTypes.Roles AllowedTypes = AllowedMentionTypes.Roles
@ -30,11 +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(ulong id) {
return Boyfriend.Client.GetGuild(id)
.GetTextChannel(ParseMention(Boyfriend.GetGuildConfig(id)["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;
@ -57,26 +51,10 @@ public static partial class Utils {
} }
} }
public static SocketRole? GetMuteRole(SocketGuild guild) {
var id = ulong.Parse(Boyfriend.GetGuildConfig(guild.Id)["MuteRole"]);
if (MuteRoleCache.TryGetValue(id, out var cachedMuteRole)) return cachedMuteRole;
foreach (var x in guild.Roles) {
if (x.Id != id) continue;
MuteRoleCache.Add(id, x);
return x;
}
return null;
}
public static void RemoveMuteRoleFromCache(ulong id) {
MuteRoleCache.Remove(id);
}
public static async Task SilentSendAsync(SocketTextChannel? channel, string text, bool allowRoles = false) { public static async Task SilentSendAsync(SocketTextChannel? channel, string text, bool allowRoles = false) {
try { try {
if (channel is null || text.Length is 0 or > 2000) if (channel is null || text.Length is 0 or > 2000)
throw new Exception($"Message length is out of range: {text.Length}"); throw new UnreachableException($"Message length is out of range: {text.Length}");
await channel.SendMessageAsync(text, false, null, null, allowRoles ? AllowRoles : AllowedMentions.None); await channel.SendMessageAsync(text, false, null, null, allowRoles ? AllowRoles : AllowedMentions.None);
} catch (Exception e) { } catch (Exception e) {
@ -109,22 +87,23 @@ public static partial class Utils {
} }
public static async Task public static async Task
SendFeedbackAsync(string feedback, ulong guildId, string mention, bool sendPublic = false) { SendFeedbackAsync(string feedback, SocketGuild guild, string mention, bool sendPublic = false) {
var adminChannel = GetBotLogChannel(guildId); var data = GuildData.Get(guild);
var systemChannel = Boyfriend.Client.GetGuild(guildId).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);
} }
public static string GetHumanizedTimeOffset(TimeSpan span) { public static string GetHumanizedTimeSpan(TimeSpan span) {
return span.TotalSeconds > 0 return span.TotalSeconds < 1
? $" {span.Humanize(2, minUnit: TimeUnit.Second, maxUnit: TimeUnit.Month, culture: Messages.Culture.Name.Contains("RU") ? CultureInfoCache["ru"] : Messages.Culture)}" ? Messages.Ever
: Messages.Ever; : $" {span.Humanize(2, minUnit: TimeUnit.Second, maxUnit: TimeUnit.Month, culture: Messages.Culture.Name.Contains("RU") ? CultureInfoCache["ru"] : Messages.Culture)}";
} }
public static void SetCurrentLanguage(ulong guildId) { public static void SetCurrentLanguage(SocketGuild guild) {
Messages.Culture = CultureInfoCache[Boyfriend.GetGuildConfig(guildId)["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) {
@ -146,48 +125,37 @@ public static partial class Utils {
appendTo.AppendLine(appendWhat); appendTo.AppendLine(appendWhat);
} }
public static async Task DelayedUnbanAsync(CommandProcessor cmd, ulong banned, string reason, TimeSpan duration) {
await Task.Delay(duration);
SetCurrentLanguage(cmd.Context.Guild.Id);
await UnbanCommand.UnbanUserAsync(cmd, banned, reason);
}
public static async Task DelayedUnmuteAsync(CommandProcessor cmd, SocketGuildUser muted, string reason,
TimeSpan duration) {
await Task.Delay(duration);
SetCurrentLanguage(cmd.Context.Guild.Id);
await UnmuteCommand.UnmuteMemberAsync(cmd, muted, reason);
}
public static async Task SendEarlyEventStartNotificationAsync(SocketTextChannel? channel,
SocketGuildEvent scheduledEvent, int minuteOffset) {
try {
await Task.Delay(scheduledEvent.StartTime.Subtract(DateTimeOffset.Now)
.Subtract(TimeSpan.FromMinutes(minuteOffset)));
var guild = scheduledEvent.Guild;
if (guild.GetEvent(scheduledEvent.Id) is null) return;
var eventConfig = Boyfriend.GetGuildConfig(guild.Id);
SetCurrentLanguage(guild.Id);
var receivers = eventConfig["EventStartedReceivers"];
var role = guild.GetRole(ulong.Parse(eventConfig["EventNotificationRole"]));
var mentions = Boyfriend.StringBuilder;
if (receivers.Contains("role") && role is not null) mentions.Append($"{role.Mention} ");
if (receivers.Contains("users") || receivers.Contains("interested"))
mentions = (await scheduledEvent.GetUsersAsync(15)).Aggregate(mentions,
(current, user) => current.Append($"{user.Mention} "));
await channel?.SendMessageAsync(string.Format(Messages.EventEarlyNotification, mentions,
Wrap(scheduledEvent.Name), scheduledEvent.StartTime.ToUnixTimeSeconds().ToString()))!;
mentions.Clear();
} catch (Exception e) {
await Boyfriend.Log(new LogMessage(LogSeverity.Error, nameof(Utils),
"Exception while sending early event start notification", e));
}
}
public static SocketTextChannel? GetEventNotificationChannel(SocketGuild guild) { public static SocketTextChannel? GetEventNotificationChannel(SocketGuild guild) {
return guild.GetTextChannel(ParseMention(Boyfriend.GetGuildConfig(guild.Id)["EventNotificationChannel"])); return guild.GetTextChannel(ParseMention(GuildData.Get(guild)
.Preferences["EventNotificationChannel"]));
}
public static bool UserExists(ulong id) {
return Boyfriend.Client.GetUser(id) is not null || UserInMemberData(id);
}
private static bool UserInMemberData(ulong 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;
if (data.Preferences["RemoveRolesOnMute"] is "true")
await toUnmute.AddRolesAsync(data.MemberData[toUnmute.Id].Roles, requestOptions);
await toUnmute.RemoveRoleAsync(role, requestOptions);
data.MemberData[toUnmute.Id].MutedUntil = null;
} 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]")]