1
0
Fork 1
mirror of https://github.com/TeamOctolings/Octobot.git synced 2025-01-31 09:09:00 +03:00

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
- dependency-type: "all"
assignees:
- "l1ttleO"
- "Octol1ttle"
labels:
- "type: dependencies"
- package-ecosystem: "nuget" # See documentation for possible values
directory: "/Boyfriend" # Location of package manifests
@ -24,4 +26,6 @@ updates:
- dependency-type: "all"
# Add assignees
assignees:
- "l1ttleO"
- "Octol1ttle"
labels:
- "type: dependencies"

View file

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

View file

@ -1,8 +1,10 @@
using System.Collections.ObjectModel;
using System.Text;
using System.Timers;
using Boyfriend.Data;
using Discord;
using Discord.Rest;
using Discord.WebSocket;
using Newtonsoft.Json;
using Timer = System.Timers.Timer;
namespace Boyfriend;
@ -20,7 +22,10 @@ public static class Boyfriend {
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),
new TimeSpan(0, 3, 40)),
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);
private static readonly Dictionary<ulong, Dictionary<string, string>> GuildConfigDictionary = 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" }
};
private static readonly List<Task> GuildTickTasks = new();
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();
Client.Log += Log;
@ -68,13 +53,35 @@ public static class Boyfriend {
EventHandler.InitEvents();
while (ActivityList.Count > 0)
foreach (var activity in ActivityList) {
await Client.SetActivityAsync(activity.Item1);
await Task.Delay(activity.Item2);
var timer = new Timer();
timer.Interval = 1000;
timer.AutoReset = true;
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) {
switch (msg.Severity) {
case LogSeverity.Critical:
@ -102,53 +109,63 @@ public static class Boyfriend {
return Task.CompletedTask;
}
public static async Task WriteGuildConfigAsync(ulong id) {
await File.WriteAllTextAsync($"config_{id}.json",
JsonConvert.SerializeObject(GuildConfigDictionary[id], Formatting.Indented));
private static async Task TickGuildAsync(SocketGuild guild) {
var data = GuildData.Get(guild);
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))
await File.WriteAllTextAsync($"removedroles_{id}.json",
JsonConvert.SerializeObject(removedRoles, Formatting.Indented));
}
if (receivers.Contains("role") && role is not null) mentions.Append($"{role.Mention} ");
if (receivers.Contains("users") || receivers.Contains("interested"))
mentions = (await schEvent.GetUsersAsync(15))
.Where(user => role is null || !((RestGuildUser)user).RoleIds.Contains(role.Id))
.Aggregate(mentions, (current, user) => current.Append($"{user.Mention} "));
public static Dictionary<string, string> GetGuildConfig(ulong id) {
if (GuildConfigDictionary.TryGetValue(id, out var cfg)) return cfg;
await Utils.GetEventNotificationChannel(guild)?.SendMessageAsync(string.Format(
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);
var config = JsonConvert.DeserializeObject<Dictionary<string, string>>(json)
?? new Dictionary<string, string>();
for (var i = mData.Reminders.Count - 1; i >= 0; i--) {
var reminder = mData.Reminders[i];
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) {
// ReSharper disable once ForeachCanBePartlyConvertedToQueryUsingAnotherGetEnumerator
// Conversion will result in a lot of memory allocations
foreach (var key in DefaultConfig.Keys)
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);
await channel.SendMessageAsync($"<@{mData.Id}> {Utils.Wrap(reminder.ReminderText)}");
mData.Reminders.RemoveAt(i);
saveData = true;
}
}
}
}
GuildConfigDictionary.Add(id, config);
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;
if (saveData) data.Save(true).Wait();
}
}

View file

@ -1,5 +1,6 @@
using System.Text;
using Boyfriend.Commands;
using Boyfriend.Data;
using Discord;
using Discord.Commands;
using Discord.WebSocket;
@ -12,7 +13,8 @@ public sealed class CommandProcessor {
public static readonly ICommand[] Commands = {
new BanCommand(), new ClearCommand(), new HelpCommand(),
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();
@ -30,19 +32,18 @@ public sealed class CommandProcessor {
public async Task HandleCommandAsync() {
var guild = Context.Guild;
var config = Boyfriend.GetGuildConfig(guild.Id);
var muteRole = Utils.GetMuteRole(guild);
Utils.SetCurrentLanguage(guild.Id);
var data = GuildData.Get(guild);
Utils.SetCurrentLanguage(guild);
if (GetMember().Roles.Contains(muteRole)) {
_ = Context.Message.ReplyAsync(Messages.UserCannotUnmuteThemselves);
if (GetMember().Roles.Contains(data.MuteRole)) {
_ = Context.Message.DeleteAsync();
return;
}
var list = Context.Message.Content.Split("\n");
var cleanList = Context.Message.CleanContent.Split("\n");
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) {
foreach (var ex in e.InnerExceptions)
@ -52,7 +53,7 @@ public sealed class CommandProcessor {
_tasks.Clear();
if (ConfigWriteScheduled) await Boyfriend.WriteGuildConfigAsync(guild.Id);
if (ConfigWriteScheduled) await data.Save(true);
SendFeedbacks();
}
@ -79,8 +80,9 @@ public sealed class CommandProcessor {
public void Audit(string action, bool isPublic = true) {
var format = $"*[{Context.User.Mention}: {action}]*";
if (isPublic) Utils.SafeAppendToBuilder(_stackedPublicFeedback, format, Context.Guild.SystemChannel);
Utils.SafeAppendToBuilder(_stackedPrivateFeedback, format, Utils.GetBotLogChannel(Context.Guild.Id));
var data = GuildData.Get(Context.Guild);
if (isPublic) Utils.SafeAppendToBuilder(_stackedPublicFeedback, format, data.PublicFeedbackChannel);
Utils.SafeAppendToBuilder(_stackedPrivateFeedback, format, data.PrivateFeedbackChannel);
if (_tasks.Count is 0) SendFeedbacks(false);
}
@ -88,8 +90,9 @@ public sealed class CommandProcessor {
if (reply && _stackedReplyMessage.Length > 0)
_ = Context.Message.ReplyAsync(_stackedReplyMessage.ToString(), false, null, AllowedMentions.None);
var adminChannel = Utils.GetBotLogChannel(Context.Guild.Id);
var systemChannel = Context.Guild.SystemChannel;
var data = GuildData.Get(Context.Guild);
var adminChannel = data.PublicFeedbackChannel;
var systemChannel = data.PrivateFeedbackChannel;
if (_stackedPrivateFeedback.Length > 0 && adminChannel is not null &&
adminChannel.Id != Context.Message.Channel.Id) {
_ = Utils.SilentSendAsync(adminChannel, _stackedPrivateFeedback.ToString());
@ -111,19 +114,30 @@ public sealed class CommandProcessor {
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) {
Utils.SafeAppendToBuilder(_stackedReplyMessage, $"{ReplyEmojis.MissingArgument} {Messages.MissingUser}",
Context.Message);
return null;
}
var user = Boyfriend.Client.GetUser(Utils.ParseMention(args[index]));
if (user is null && argument is not null)
var mention = Utils.ParseMention(args[index]);
if (mention is 0) {
Utils.SafeAppendToBuilder(_stackedReplyMessage,
$"{ReplyEmojis.InvalidArgument} {string.Format(Messages.InvalidUser, Utils.Wrap(cleanArgs[index]))}",
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) {
@ -134,7 +148,7 @@ public sealed class CommandProcessor {
return false;
}
if (!Context.Guild.GetUser(Context.User.Id).GuildPermissions.Has(permission)
if (!GetMember().GuildPermissions.Has(permission)
&& Context.Guild.OwnerId != Context.User.Id) {
Utils.SafeAppendToBuilder(_stackedReplyMessage,
$"{ReplyEmojis.NoPermission} {Utils.GetMessage($"UserCannot{permission}")}",
@ -145,11 +159,15 @@ public sealed class CommandProcessor {
return true;
}
public SocketGuildUser? GetMember(SocketUser user) {
return Context.Guild.GetUser(user.Id);
private SocketGuildUser GetMember() {
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) {
Utils.SafeAppendToBuilder(_stackedReplyMessage, $"{ReplyEmojis.MissingArgument} {Messages.MissingMember}",
Context.Message);
@ -157,17 +175,13 @@ public sealed class CommandProcessor {
}
var member = Context.Guild.GetUser(Utils.ParseMention(args[index]));
if (member is null && argument is not null)
if (member is null)
Utils.SafeAppendToBuilder(_stackedReplyMessage,
$"{ReplyEmojis.InvalidArgument} {string.Format(Messages.InvalidMember, Utils.Wrap(cleanArgs[index]))}",
$"{ReplyEmojis.InvalidArgument} {Messages.InvalidMember}",
Context.Message);
return member;
}
private SocketGuildUser GetMember() {
return Context.Guild.GetUser(Context.User.Id);
}
public ulong? GetBan(string[] args, int index) {
if (index >= args.Length) {
Utils.SafeAppendToBuilder(_stackedReplyMessage, $"{ReplyEmojis.MissingArgument} {Messages.MissingUser}",

View file

@ -1,3 +1,4 @@
using Boyfriend.Data;
using Discord;
using Discord.WebSocket;
@ -7,10 +8,10 @@ public sealed class BanCommand : ICommand {
public string[] Aliases { get; } = { "ban", "бан" };
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;
var memberToBan = cmd.GetMember(toBan);
var memberToBan = cmd.GetMember(toBan.Item1);
if (memberToBan is not null && !cmd.CanInteractWith(memberToBan, "Ban")) return;
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);
}
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 guild = cmd.Context.Guild;
await Utils.SendDirectMessage(toBan,
string.Format(Messages.YouWereBanned, author.Mention, guild.Name, Utils.Wrap(reason)));
if (toBan.Item2 is not null)
await Utils.SendDirectMessage(toBan.Item2,
string.Format(Messages.YouWereBanned, author.Mention, guild.Name, Utils.Wrap(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,
Utils.GetHumanizedTimeOffset(duration), Utils.Wrap(reason));
var memberData = GuildData.Get(guild).MemberData[toBan.Item1];
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.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;
namespace Boyfriend.Commands;
@ -7,7 +8,7 @@ public sealed class ClearCommand : ICommand {
public string[] Aliases { get; } = { "clear", "purge", "очистить", "стереть" };
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;
@ -18,6 +19,7 @@ public sealed class ClearCommand : ICommand {
var user = (SocketGuildUser)cmd.Context.User;
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;
namespace Boyfriend.Commands;
@ -6,7 +7,7 @@ public sealed class HelpCommand : ICommand {
public string[] Aliases { get; } = { "help", "помощь", "справка" };
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);
foreach (var command in CommandProcessor.Commands)

View file

@ -1,3 +1,4 @@
using Boyfriend.Data;
using Discord;
using Discord.WebSocket;
@ -7,7 +8,7 @@ public sealed class KickCommand : ICommand {
public string[] Aliases { get; } = { "kick", "кик", "выгнать" };
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 (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,
Utils.Wrap(reason)));
GuildData.Get(cmd.Context.Guild).MemberData[toKick.Id].Roles.Clear();
cmd.ConfigWriteScheduled = true;
await toKick.KickAsync(guildKickMessage);
var format = string.Format(Messages.FeedbackMemberKicked, toKick.Mention, Utils.Wrap(reason));
cmd.Reply(format, ReplyEmojis.Kicked);

View file

@ -1,5 +1,5 @@
using Boyfriend.Data;
using Discord;
using Discord.Net;
using Discord.WebSocket;
namespace Boyfriend.Commands;
@ -8,64 +8,38 @@ public sealed class MuteCommand : ICommand {
public string[] Aliases { get; } = { "mute", "timeout", "заглушить", "мут" };
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;
var duration = CommandProcessor.GetTimeSpan(args, 1);
var reason = cmd.GetRemaining(args, duration.TotalSeconds < 1 ? 1 : 2, "MuteReason");
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))
|| (toMute.TimedOutUntil is not null
&& toMute.TimedOutUntil.Value.ToUnixTimeSeconds()
> DateTimeOffset.Now.ToUnixTimeSeconds())) {
&& toMute.TimedOutUntil.Value
> DateTimeOffset.Now)) {
cmd.Reply(Messages.MemberAlreadyMuted, ReplyEmojis.Error);
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"))
await MuteMemberAsync(cmd, toMute, duration, reason);
await MuteMemberAsync(cmd, toMute, duration, guildData, reason);
}
private static async Task MuteMemberAsync(CommandProcessor cmd, SocketGuildUser toMute,
TimeSpan duration, string reason) {
var guild = cmd.Context.Guild;
var config = Boyfriend.GetGuildConfig(guild.Id);
TimeSpan duration, GuildData data, string reason) {
var requestOptions = Utils.GetRequestOptions($"({cmd.Context.User}) {reason}");
var role = Utils.GetMuteRole(guild);
var role = data.MuteRole;
var hasDuration = duration.TotalSeconds > 0;
if (role is not null) {
if (config["RemoveRolesOnMute"] is "true") {
var rolesRemoved = new List<ulong>();
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;
}
if (data.Preferences["RemoveRolesOnMute"] is "true")
await toMute.RemoveRolesAsync(toMute.Roles, requestOptions);
await toMute.AddRoleAsync(role, requestOptions);
if (hasDuration)
await Task.FromResult(Utils.DelayedUnmuteAsync(cmd, toMute, Messages.PunishmentExpired, duration));
} else {
if (!hasDuration || duration.TotalDays > 28) {
cmd.Reply(Messages.DurationRequiredForTimeOuts, ReplyEmojis.Error);
@ -80,8 +54,11 @@ public sealed class MuteCommand : ICommand {
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,
Utils.GetHumanizedTimeOffset(duration),
Utils.GetHumanizedTimeSpan(duration),
Utils.Wrap(reason));
cmd.Reply(feedback, ReplyEmojis.Muted);
cmd.Audit(feedback);

View file

@ -7,7 +7,7 @@ public sealed class PingCommand : ICommand {
var builder = Boyfriend.StringBuilder;
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);
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;
namespace Boyfriend.Commands;
@ -9,14 +10,17 @@ public sealed class SettingsCommand : ICommand {
if (!cmd.HasPermission(GuildPermission.ManageGuild)) return Task.CompletedTask;
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) {
var currentSettings = Boyfriend.StringBuilder.AppendLine(Messages.CurrentSettings);
foreach (var setting in Boyfriend.DefaultConfig) {
foreach (var setting in GuildData.DefaultPreferences) {
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 (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 exists = false;
// ReSharper disable once ForeachCanBePartlyConvertedToQueryUsingAnotherGetEnumerator
// Too many allocations
foreach (var setting in Boyfriend.DefaultConfig.Keys) {
if (selectedSetting != setting.ToLower()) continue;
foreach (var setting in GuildData.DefaultPreferences.Keys.Where(x => x.ToLower() == selectedSetting)) {
selectedSetting = setting;
exists = true;
break;
@ -70,7 +71,7 @@ public sealed class SettingsCommand : ICommand {
}
} else { value = "reset"; }
if (IsBool(Boyfriend.DefaultConfig[selectedSetting]) && !IsBool(value)) {
if (IsBool(GuildData.DefaultPreferences[selectedSetting]) && !IsBool(value)) {
value = value switch {
"y" or "yes" or "д" or "да" => "true",
"n" or "no" or "н" or "нет" => "false",
@ -95,14 +96,14 @@ public sealed class SettingsCommand : ICommand {
var formattedValue = selectedSetting switch {
"WelcomeMessage" => Utils.Wrap(Messages.DefaultWelcomeMessage),
"EventStartedReceivers" => Utils.Wrap(Boyfriend.DefaultConfig[selectedSetting])!,
"EventStartedReceivers" => Utils.Wrap(GuildData.DefaultPreferences[selectedSetting])!,
_ => value is "reset" or "default" ? Messages.SettingNotDefined
: IsBool(value) ? YesOrNo(value is "true")
: string.Format(formatting, value)
};
if (value is "reset" or "default") {
config[selectedSetting] = Boyfriend.DefaultConfig[selectedSetting];
config[selectedSetting] = GuildData.DefaultPreferences[selectedSetting];
} else {
if (value == config[selectedSetting]) {
cmd.Reply(string.Format(Messages.SettingsNothingChanged, localizedSelectedSetting, formattedValue),
@ -129,13 +130,28 @@ public sealed class SettingsCommand : ICommand {
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;
}
if (selectedSetting is "Lang") {
Utils.SetCurrentLanguage(guild.Id);
Utils.SetCurrentLanguage(guild);
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);
}
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}");
await cmd.Context.Guild.RemoveBanAsync(id, requestOptions);

View file

@ -1,3 +1,4 @@
using Boyfriend.Data;
using Discord;
using Discord.WebSocket;
@ -9,38 +10,25 @@ public sealed class UnmuteCommand : ICommand {
public async Task RunAsync(CommandProcessor cmd, string[] args, string[] cleanArgs) {
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;
var reason = cmd.GetRemaining(args, 1, "UnmuteReason");
if (reason is not null && cmd.CanInteractWith(toUnmute, "Unmute"))
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) {
var requestOptions = Utils.GetRequestOptions($"({cmd.Context.User}) {reason}");
var role = Utils.GetMuteRole(cmd.Context.Guild);
var isMuted = await Utils.UnmuteMemberAsync(GuildData.Get(cmd.Context.Guild), cmd.Context.User.ToString(),
toUnmute, reason);
if (role is not null && toUnmute.Roles.Contains(role)) {
var rolesRemoved = Boyfriend.GetRemovedRoles(cmd.Context.Guild.Id);
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();
if (!isMuted) {
cmd.Reply(Messages.MemberNotMuted, ReplyEmojis.Error);
return;
}
cmd.ConfigWriteScheduled = true;
var feedback = string.Format(Messages.FeedbackMemberUnmuted, toUnmute.Mention, Utils.Wrap(reason));
cmd.Reply(feedback, ReplyEmojis.Unmuted);
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.Rest;
using Discord.WebSocket;
@ -14,20 +16,30 @@ public static class EventHandler {
Client.MessageReceived += MessageReceivedEvent;
Client.MessageUpdated += MessageUpdatedEvent;
Client.UserJoined += UserJoinedEvent;
Client.UserLeft += UserLeftEvent;
Client.GuildMemberUpdated += RolesUpdatedEvent;
Client.GuildScheduledEventCreated += ScheduledEventCreatedEvent;
Client.GuildScheduledEventCancelled += ScheduledEventCancelledEvent;
Client.GuildScheduledEventStarted += ScheduledEventStartedEvent;
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() {
if (!_sendReadyMessages) return Task.CompletedTask;
var i = Random.Shared.Next(3);
foreach (var guild in Client.Guilds) {
var config = Boyfriend.GetGuildConfig(guild.Id);
var channel = guild.GetTextChannel(Utils.ParseMention(config["BotLogChannel"]));
Utils.SetCurrentLanguage(guild.Id);
var data = GuildData.Get(guild);
var config = data.Preferences;
var channel = data.PrivateFeedbackChannel;
Utils.SetCurrentLanguage(guild);
if (config["ReceiveStartupMessages"] is not "true" || channel is null) continue;
_ = channel.SendMessageAsync(string.Format(Messages.Ready, Utils.GetBeep(i)));
@ -45,19 +57,20 @@ public static class EventHandler {
var guild = gChannel.Guild;
Utils.SetCurrentLanguage(guild.Id);
Utils.SetCurrentLanguage(guild);
var mention = msg.Author.Mention;
await Task.Delay(500);
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;
await Utils.SendFeedbackAsync(string.Format(Messages.CachedMessageDeleted, msg.Author.Mention,
Utils.MentionChannel(channel.Id),
Utils.Wrap(msg.CleanContent)), guild.Id, mention);
Utils.Wrap(msg.CleanContent)), guild, mention);
}
private static Task MessageReceivedEvent(IDeletable messageParam) {
@ -67,7 +80,8 @@ public static class EventHandler {
"whoami" => message.ReplyAsync("`nobody`"),
"сука !!" => message.ReplyAsync("`root`"),
"воооо" => message.ReplyAsync("`removing /...`"),
"op ??" => message.ReplyAsync("некоторые пасхальные цитаты которые вы могли найти были легально взяты у <@573772175572729876>"),
"op ??" => message.ReplyAsync(
"некоторые пасхальные цитаты которые вы могли найти были легально взяты у <@573772175572729876>"),
_ => new CommandProcessor(message).HandleCommandAsync()
};
return Task.CompletedTask;
@ -80,34 +94,60 @@ public static class EventHandler {
msg.CleanContent == messageSocket.CleanContent || msg.Author.IsBot) return;
var guild = gChannel.Guild;
Utils.SetCurrentLanguage(guild.Id);
Utils.SetCurrentLanguage(guild);
var isLimitedSpace = msg.CleanContent.Length + messageSocket.CleanContent.Length < 1940;
await Utils.SendFeedbackAsync(string.Format(Messages.CachedMessageEdited, Utils.MentionChannel(channel.Id),
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) {
if (user.IsBot) return;
var guild = user.Guild;
var config = Boyfriend.GetGuildConfig(guild.Id);
Utils.SetCurrentLanguage(guild.Id);
var data = GuildData.Get(guild);
var config = data.Preferences;
Utils.SetCurrentLanguage(guild);
if (config["SendWelcomeMessages"] is "true")
await Utils.SilentSendAsync(guild.SystemChannel,
if (config["SendWelcomeMessages"] is "true" && data.PublicFeedbackChannel is not null)
await Utils.SilentSendAsync(data.PublicFeedbackChannel,
string.Format(config["WelcomeMessage"] is "default"
? Messages.DefaultWelcomeMessage
: config["WelcomeMessage"], user.Mention, guild.Name));
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) {
var guild = scheduledEvent.Guild;
var eventConfig = Boyfriend.GetGuildConfig(guild.Id);
var eventConfig = GuildData.Get(guild).Preferences;
var channel = Utils.GetEventNotificationChannel(guild);
Utils.SetCurrentLanguage(guild.Id);
Utils.SetCurrentLanguage(guild);
if (channel is not null) {
var role = guild.GetRole(ulong.Parse(eventConfig["EventNotificationRole"]));
@ -125,17 +165,13 @@ public static class EventHandler {
scheduledEvent.StartTime.ToUnixTimeSeconds().ToString(), descAndLink),
true);
}
if (eventConfig["EventEarlyNotificationOffset"] is not "0")
_ = Utils.SendEarlyEventStartNotificationAsync(channel, scheduledEvent,
int.Parse(eventConfig["EventEarlyNotificationOffset"]));
}
private static async Task ScheduledEventCancelledEvent(SocketGuildEvent scheduledEvent) {
var guild = scheduledEvent.Guild;
var eventConfig = Boyfriend.GetGuildConfig(guild.Id);
var eventConfig = GuildData.Get(guild).Preferences;
var channel = Utils.GetEventNotificationChannel(guild);
Utils.SetCurrentLanguage(guild.Id);
Utils.SetCurrentLanguage(guild);
if (channel is not null)
await channel.SendMessageAsync(string.Format(Messages.EventCancelled, Utils.Wrap(scheduledEvent.Name),
eventConfig["FrowningFace"] is "true" ? $" {Messages.SettingsFrowningFace}" : ""));
@ -143,9 +179,9 @@ public static class EventHandler {
private static async Task ScheduledEventStartedEvent(SocketGuildEvent scheduledEvent) {
var guild = scheduledEvent.Guild;
var eventConfig = Boyfriend.GetGuildConfig(guild.Id);
var eventConfig = GuildData.Get(guild).Preferences;
var channel = Utils.GetEventNotificationChannel(guild);
Utils.SetCurrentLanguage(guild.Id);
Utils.SetCurrentLanguage(guild);
if (channel is not null) {
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("users") || receivers.Contains("interested"))
mentions = (await scheduledEvent.GetUsersAsync(15)).Aggregate(mentions,
(current, user) => current.Append($"{user.Mention} "));
mentions = (await scheduledEvent.GetUsersAsync(15))
.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,
Utils.Wrap(scheduledEvent.Name),
@ -167,9 +204,9 @@ public static class EventHandler {
private static async Task ScheduledEventCompletedEvent(SocketGuildEvent scheduledEvent) {
var guild = scheduledEvent.Guild;
var channel = Utils.GetEventNotificationChannel(guild);
Utils.SetCurrentLanguage(guild.Id);
Utils.SetCurrentLanguage(guild);
if (channel is not null)
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>
/// Looks up a localized string similar to Allows you to change certain preferences for this guild.
/// </summary>
@ -492,7 +501,7 @@ namespace Boyfriend {
}
/// <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>
internal static string InvalidMember {
get {
@ -609,11 +618,11 @@ namespace Boyfriend {
}
/// <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>
internal static string MissingSetting {
internal static string MissingReminderText {
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>
/// Looks up a localized string similar to That setting doesn&apos;t exist!.
/// </summary>
@ -717,11 +708,11 @@ namespace Boyfriend {
}
/// <summary>
/// Looks up a localized string similar to Bot log channel.
/// Looks up a localized string similar to Automatically start scheduled events.
/// </summary>
internal static string SettingsBotLogChannel {
internal static string SettingsAutoStartEvents {
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>
/// Looks up a localized string similar to Receive startup messages.
/// </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>
/// Looks up a localized string similar to Send welcome messages.
/// </summary>
@ -1050,11 +1068,11 @@ namespace Boyfriend {
}
/// <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>
internal static string UserNotInGuild {
internal static string UserNotFound {
get {
return ResourceManager.GetString("UserNotInGuild", resourceCulture);
return ResourceManager.GetString("UserNotFound", resourceCulture);
}
}
@ -1068,7 +1086,7 @@ namespace Boyfriend {
}
/// <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>
internal static string YouWereBanned {
get {
@ -1077,7 +1095,7 @@ namespace Boyfriend {
}
/// <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>
internal static string YouWereKicked {
get {

View file

@ -1,64 +1,64 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
<!--
Microsoft ResX Schema
Version 2.0
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.
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:
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>
... 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.
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.
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:
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.
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.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.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.
-->
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:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
@ -135,43 +135,43 @@
<data name="Beep2" xml:space="preserve">
<value>Bop! </value>
</data>
<data name="Beep3" xml:space="preserve">
<data name="Beep3" xml:space="preserve">
<value>Beep! </value>
</data>
<data name="CommandNoPermissionBot" xml:space="preserve">
<data name="CommandNoPermissionBot" xml:space="preserve">
<value>I do not have permission to execute this command!</value>
</data>
<data name="CommandNoPermissionUser" xml:space="preserve">
<data name="CommandNoPermissionUser" xml:space="preserve">
<value>You do not have permission to execute this command!</value>
</data>
<data name="YouWereBanned" xml:space="preserve">
<value>You were banned by {0} in guild {1} for {2}</value>
</data>
<data name="PunishmentExpired" xml:space="preserve">
<data name="YouWereBanned" xml:space="preserve">
<value>You were banned by {0} in guild `{1}` for {2}</value>
</data>
<data name="PunishmentExpired" xml:space="preserve">
<value>Punishment expired</value>
</data>
<data name="ClearAmountTooSmall" xml:space="preserve">
<data name="ClearAmountTooSmall" xml:space="preserve">
<value>You specified less than {0} messages!</value>
</data>
<data name="ClearAmountTooLarge" xml:space="preserve">
<data name="ClearAmountTooLarge" xml:space="preserve">
<value>You specified more than {0} messages!</value>
</data>
<data name="CommandHelp" xml:space="preserve">
<data name="CommandHelp" xml:space="preserve">
<value>Command help:</value>
</data>
<data name="YouWereKicked" xml:space="preserve">
<value>You were kicked by {0} in guild {1} for {2}</value>
</data>
<data name="Milliseconds" xml:space="preserve">
<data name="YouWereKicked" xml:space="preserve">
<value>You were kicked by {0} in guild `{1}` for {2}</value>
</data>
<data name="Milliseconds" xml:space="preserve">
<value>ms</value>
</data>
<data name="MemberAlreadyMuted" xml:space="preserve">
<data name="MemberAlreadyMuted" xml:space="preserve">
<value>Member is already muted!</value>
</data>
<data name="ChannelNotSpecified" xml:space="preserve">
<data name="ChannelNotSpecified" xml:space="preserve">
<value>Not specified</value>
</data>
<data name="RoleNotSpecified" xml:space="preserve">
<data name="RoleNotSpecified" xml:space="preserve">
<value>Not specified</value>
</data>
<data name="CurrentSettings" xml:space="preserve">
@ -189,16 +189,10 @@
<data name="SettingsSendWelcomeMessages" xml:space="preserve">
<value>Send welcome messages</value>
</data>
<data name="SettingsStarterRole" xml:space="preserve">
<value>Starter role</value>
</data>
<data name="SettingsMuteRole" xml:space="preserve">
<data name="SettingsMuteRole" xml:space="preserve">
<value>Mute role</value>
</data>
<data name="SettingsBotLogChannel" xml:space="preserve">
<value>Bot log channel</value>
</data>
<data name="LanguageNotSupported" xml:space="preserve">
<data name="LanguageNotSupported" xml:space="preserve">
<value>Language not supported! Supported languages:</value>
</data>
<data name="Yes" xml:space="preserve">
@ -213,10 +207,7 @@
<data name="MemberNotMuted" xml:space="preserve">
<value>Member not muted!</value>
</data>
<data name="RolesReturned" 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">
<data name="SettingsWelcomeMessage" xml:space="preserve">
<value>Welcome message</value>
</data>
<data name="ClearAmountInvalid" xml:space="preserve">
@ -225,9 +216,6 @@
<data name="FeedbackUserBanned" xml:space="preserve">
<value>Banned {0} for{1}: {2}</value>
</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">
<value>That setting doesn't exist!</value>
</data>
@ -243,10 +231,7 @@
<data name="InvalidChannel" xml:space="preserve">
<value>This channel does not exist!</value>
</data>
<data name="RoleRemovalFailed" xml:space="preserve">
<value>I couldn't remove role {0} because of an error! {1}</value>
</data>
<data name="DurationRequiredForTimeOuts" xml:space="preserve">
<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>
</data>
<data name="CannotTimeOutBot" xml:space="preserve">
@ -333,28 +318,28 @@
<data name="MissingNumber" xml:space="preserve">
<value>You need to specify an integer from {0} to {1}!</value>
</data>
<data name="MissingUser" xml:space="preserve">
<data name="MissingUser" xml:space="preserve">
<value>You need to specify a user!</value>
</data>
<data name="InvalidUser" xml:space="preserve">
<data name="InvalidUser" xml:space="preserve">
<value>You need to specify a user instead of {0}!</value>
</data>
<data name="MissingMember" xml:space="preserve">
<data name="MissingMember" xml:space="preserve">
<value>You need to specify a guild member!</value>
</data>
<data name="InvalidMember" xml:space="preserve">
<value>You need to specify a guild member instead of {0}!</value>
</data>
<data name="UserCannotBanMembers" xml:space="preserve">
<data name="InvalidMember" xml:space="preserve">
<value>You did not specify a member of this guild!</value>
</data>
<data name="UserCannotBanMembers" xml:space="preserve">
<value>You cannot ban users from this guild!</value>
</data>
<data name="UserCannotManageMessages" xml:space="preserve">
<data name="UserCannotManageMessages" xml:space="preserve">
<value>You cannot manage messages in this guild!</value>
</data>
<data name="UserCannotKickMembers" xml:space="preserve">
<data name="UserCannotKickMembers" xml:space="preserve">
<value>You cannot kick members from this guild!</value>
</data>
<data name="UserCannotModerateMembers" xml:space="preserve">
<data name="UserCannotModerateMembers" xml:space="preserve">
<value>You cannot moderate members in this guild!</value>
</data>
<data name="UserCannotManageGuild" xml:space="preserve">
@ -390,10 +375,7 @@
<data name="MissingUnmuteReason" xml:space="preserve">
<value>You need to specify a reason for unmute this member!</value>
</data>
<data name="MissingSetting" xml:space="preserve">
<value>You need to specify a setting to change!</value>
</data>
<data name="UserCannotBanOwner" xml:space="preserve">
<data name="UserCannotBanOwner" xml:space="preserve">
<value>You cannot ban the owner of this guild!</value>
</data>
<data name="UserCannotBanThemselves" xml:space="preserve">
@ -450,13 +432,37 @@
<data name="BotCannotUnmuteTarget" xml:space="preserve">
<value>I cannot unmute this member!</value>
</data>
<data name="UserCannotUnmuteTarget" xml:space="preserve">
<data name="UserCannotUnmuteTarget" xml:space="preserve">
<value>You cannot unmute this user!</value>
</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>
</data>
<data name="SettingsEventEarlyNotificationOffset" xml:space="preserve">
<data name="SettingsEventEarlyNotificationOffset" xml:space="preserve">
<value>Early event start notification offset</value>
</data>
<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"?>
<root>
<!--
Microsoft ResX Schema
<!--
Microsoft ResX Schema
Version 2.0
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.
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:
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>
... 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.
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.
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:
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.
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.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.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.
-->
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:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
@ -135,43 +135,43 @@
<data name="Beep2" xml:space="preserve">
<value>Боп! </value>
</data>
<data name="Beep3" xml:space="preserve">
<data name="Beep3" xml:space="preserve">
<value>Бип! </value>
</data>
<data name="CommandNoPermissionBot" xml:space="preserve">
<data name="CommandNoPermissionBot" xml:space="preserve">
<value>У меня недостаточно прав для выполнения этой команды!</value>
</data>
<data name="CommandNoPermissionUser" xml:space="preserve">
<data name="CommandNoPermissionUser" xml:space="preserve">
<value>У тебя недостаточно прав для выполнения этой команды!</value>
</data>
<data name="YouWereBanned" xml:space="preserve">
<value>Тебя забанил {0} на сервере {1} за {2}</value>
</data>
<data name="PunishmentExpired" xml:space="preserve">
<data name="YouWereBanned" xml:space="preserve">
<value>Тебя забанил {0} на сервере `{1}` за {2}</value>
</data>
<data name="PunishmentExpired" xml:space="preserve">
<value>Время наказания истекло</value>
</data>
<data name="ClearAmountTooSmall" xml:space="preserve">
<data name="ClearAmountTooSmall" xml:space="preserve">
<value>Указано менее {0} сообщений!</value>
</data>
<data name="ClearAmountTooLarge" xml:space="preserve">
<data name="ClearAmountTooLarge" xml:space="preserve">
<value>Указано более {0} сообщений!</value>
</data>
<data name="CommandHelp" xml:space="preserve">
<data name="CommandHelp" xml:space="preserve">
<value>Справка по командам:</value>
</data>
<data name="YouWereKicked" xml:space="preserve">
<value>Тебя кикнул {0} на сервере {1} за {2}</value>
</data>
<data name="Milliseconds" xml:space="preserve">
<data name="YouWereKicked" xml:space="preserve">
<value>Тебя кикнул {0} на сервере `{1}` за {2}</value>
</data>
<data name="Milliseconds" xml:space="preserve">
<value>мс</value>
</data>
<data name="MemberAlreadyMuted" xml:space="preserve">
<data name="MemberAlreadyMuted" xml:space="preserve">
<value>Участник уже заглушен!</value>
</data>
<data name="ChannelNotSpecified" xml:space="preserve">
<data name="ChannelNotSpecified" xml:space="preserve">
<value>Не указан</value>
</data>
<data name="RoleNotSpecified" xml:space="preserve">
<data name="RoleNotSpecified" xml:space="preserve">
<value>Не указана</value>
</data>
<data name="CurrentSettings" xml:space="preserve">
@ -192,10 +192,7 @@
<data name="SettingsMuteRole" xml:space="preserve">
<value>Роль мута</value>
</data>
<data name="SettingsBotLogChannel" xml:space="preserve">
<value>Канал бот-уведомлений</value>
</data>
<data name="LanguageNotSupported" xml:space="preserve">
<data name="LanguageNotSupported" xml:space="preserve">
<value>Язык не поддерживается! Поддерживаемые языки:</value>
</data>
<data name="Yes" xml:space="preserve">
@ -210,10 +207,7 @@
<data name="MemberNotMuted" xml:space="preserve">
<value>Участник не заглушен!</value>
</data>
<data name="RolesReturned" xml:space="preserve">
<value>Кто-то убрал роль мута самостоятельно! Я вернул все роли, которые забрал при муте</value>
</data>
<data name="SettingsWelcomeMessage" xml:space="preserve">
<data name="SettingsWelcomeMessage" xml:space="preserve">
<value>Приветствие</value>
</data>
<data name="ClearAmountInvalid" xml:space="preserve">
@ -222,9 +216,6 @@
<data name="FeedbackUserBanned" xml:space="preserve">
<value>Забанен {0} на{1}: {2}</value>
</data>
<data name="UserNotInGuild" xml:space="preserve">
<value>Указанный пользователь не является участником этого сервера!</value>
</data>
<data name="SettingDoesntExist" xml:space="preserve">
<value>Такая настройка не существует!</value>
</data>
@ -240,19 +231,13 @@
<data name="InvalidChannel" xml:space="preserve">
<value>Этот канал не существует!</value>
</data>
<data name="RoleRemovalFailed" xml:space="preserve">
<value>Я не смог забрать роль {0} в связи с ошибкой! {1}</value>
</data>
<data name="DurationRequiredForTimeOuts" xml:space="preserve">
<data name="DurationRequiredForTimeOuts" xml:space="preserve">
<value>Я не могу заглушить кого-то на более чем 28 дней, используя тайм-ауты! Или укажи продолжительность менее 28 дней, или установи роль мута в настройках</value>
</data>
<data name="CannotTimeOutBot" xml:space="preserve">
<value>Я не могу использовать тайм-ауты на других ботах! Попробуй указать роль мута в настройках</value>
</data>
<data name="SettingsStarterRole" xml:space="preserve">
<value>Начальная роль</value>
</data>
<data name="EventCreated" xml:space="preserve">
<data name="EventCreated" xml:space="preserve">
<value>{0} создал событие {1}! Оно пройдёт в {2} и начнётся &lt;t:{3}:R&gt;!{4}</value>
</data>
<data name="SettingsEventNotificationRole" xml:space="preserve">
@ -333,28 +318,28 @@
<data name="MissingNumber" xml:space="preserve">
<value>Надо указать целое число от {0} до {1}!</value>
</data>
<data name="MissingUser" xml:space="preserve">
<data name="MissingUser" xml:space="preserve">
<value>Надо указать пользователя!</value>
</data>
<data name="InvalidUser" xml:space="preserve">
<data name="InvalidUser" xml:space="preserve">
<value>Надо указать пользователя вместо {0}!</value>
</data>
<data name="MissingMember" xml:space="preserve">
<data name="MissingMember" xml:space="preserve">
<value>Надо указать участника сервера!</value>
</data>
<data name="InvalidMember" xml:space="preserve">
<value>Надо указать участника сервера вместо {0}!</value>
</data>
<data name="UserCannotBanMembers" xml:space="preserve">
<data name="InvalidMember" xml:space="preserve">
<value>Тебе надо указать участника этого сервера!</value>
</data>
<data name="UserCannotBanMembers" xml:space="preserve">
<value>Ты не можешь банить пользователей на этом сервере!</value>
</data>
<data name="UserCannotManageMessages" xml:space="preserve">
<data name="UserCannotManageMessages" xml:space="preserve">
<value>Ты не можешь управлять сообщениями этого сервера!</value>
</data>
<data name="UserCannotKickMembers" xml:space="preserve">
<data name="UserCannotKickMembers" xml:space="preserve">
<value>Ты не можешь выгонять участников с этого сервера!</value>
</data>
<data name="UserCannotModerateMembers" xml:space="preserve">
<data name="UserCannotModerateMembers" xml:space="preserve">
<value>Ты не можешь модерировать участников этого сервера!</value>
</data>
<data name="UserCannotManageGuild" xml:space="preserve">
@ -384,10 +369,7 @@
<data name="MissingMuteReason" xml:space="preserve">
<value>Надо указать причину для мута этого участника!</value>
</data>
<data name="MissingSetting" xml:space="preserve">
<value>Надо указать настройку, которую нужно изменить!</value>
</data>
<data name="MissingUnbanReason" xml:space="preserve">
<data name="MissingUnbanReason" xml:space="preserve">
<value>Надо указать причину для разбана этого пользователя!</value>
</data>
<data name="MissingUnmuteReason" xml:space="preserve">
@ -450,13 +432,37 @@
<data name="UserCannotUnmuteTarget" xml:space="preserve">
<value>Ты не можешь вернуть из мута этого пользователя!</value>
</data>
<data name="BotCannotUnmuteTarget" xml:space="preserve">
<data name="BotCannotUnmuteTarget" xml:space="preserve">
<value>Я не могу вернуть из мута этого пользователя!</value>
</data>
<data name="EventEarlyNotification" xml:space="preserve">
<data name="EventEarlyNotification" xml:space="preserve">
<value>{0}Событие {1} начнется &lt;t:{2}:R&gt;!</value>
</data>
<data name="SettingsEventEarlyNotificationOffset" xml:space="preserve">
<data name="SettingsEventEarlyNotificationOffset" xml:space="preserve">
<value>Офсет отправки преждевременного уведомления о начале события</value>
</data>
<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"?>
<root>
<!--
Microsoft ResX Schema
<!--
Microsoft ResX Schema
Version 2.0
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.
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:
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>
... 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.
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.
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:
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.
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.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.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.
-->
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:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
@ -135,43 +135,43 @@
<data name="Beep2" xml:space="preserve">
<value>брох! </value>
</data>
<data name="Beep3" xml:space="preserve">
<data name="Beep3" xml:space="preserve">
<value>брух! </value>
</data>
<data name="CommandNoPermissionBot" xml:space="preserve">
<data name="CommandNoPermissionBot" xml:space="preserve">
<value>у меня прав нету, сделай что нибудь.</value>
</data>
<data name="CommandNoPermissionUser" xml:space="preserve">
<data name="CommandNoPermissionUser" xml:space="preserve">
<value>у тебя прав нету, твои проблемы.</value>
</data>
<data name="YouWereBanned" xml:space="preserve">
<value>здарова, тебя крч забанил {0} на сервере {1} за {2}</value>
</data>
<data name="PunishmentExpired" xml:space="preserve">
<data name="YouWereBanned" xml:space="preserve">
<value>здарова, тебя крч забанил {0} на сервере `{1}` за {2}</value>
</data>
<data name="PunishmentExpired" xml:space="preserve">
<value>время бана закончиловсь</value>
</data>
<data name="ClearAmountTooSmall" xml:space="preserve">
<data name="ClearAmountTooSmall" xml:space="preserve">
<value>ты выбрал менее {0} сообщений</value>
</data>
<data name="ClearAmountTooLarge" xml:space="preserve">
<data name="ClearAmountTooLarge" xml:space="preserve">
<value>ты выбрал более {0} сообщений</value>
</data>
<data name="CommandHelp" xml:space="preserve">
<data name="CommandHelp" xml:space="preserve">
<value>туториал по приколам:</value>
</data>
<data name="YouWereKicked" xml:space="preserve">
<value>здарова, тебя крч кикнул {0} на сервере {1} за {2}</value>
</data>
<data name="Milliseconds" xml:space="preserve">
<data name="YouWereKicked" xml:space="preserve">
<value>здарова, тебя крч кикнул {0} на сервере `{1}` за {2}</value>
</data>
<data name="Milliseconds" xml:space="preserve">
<value>мс</value>
</data>
<data name="MemberAlreadyMuted" xml:space="preserve">
<data name="MemberAlreadyMuted" xml:space="preserve">
<value>шизоид уже замучен!</value>
</data>
<data name="ChannelNotSpecified" xml:space="preserve">
<data name="ChannelNotSpecified" xml:space="preserve">
<value>*тут ничего нет*</value>
</data>
<data name="RoleNotSpecified" xml:space="preserve">
<data name="RoleNotSpecified" xml:space="preserve">
<value>*тут ничего нет*</value>
</data>
<data name="CurrentSettings" xml:space="preserve">
@ -192,10 +192,7 @@
<data name="SettingsMuteRole" xml:space="preserve">
<value>роль замученного</value>
</data>
<data name="SettingsBotLogChannel" xml:space="preserve">
<value>канал бот-уведомлений</value>
</data>
<data name="LanguageNotSupported" xml:space="preserve">
<data name="LanguageNotSupported" xml:space="preserve">
<value>такого языка нету, ты шо, есть только такие:</value>
</data>
<data name="Yes" xml:space="preserve">
@ -210,10 +207,7 @@
<data name="MemberNotMuted" xml:space="preserve">
<value>шизоид не замучен!</value>
</data>
<data name="RolesReturned" xml:space="preserve">
<value>кто-то решил поумничать и обошел роль мута. я ее вернул.</value>
</data>
<data name="SettingsWelcomeMessage" xml:space="preserve">
<data name="SettingsWelcomeMessage" xml:space="preserve">
<value>приветствие</value>
</data>
<data name="ClearAmountInvalid" xml:space="preserve">
@ -222,9 +216,6 @@
<data name="FeedbackUserBanned" xml:space="preserve">
<value>забанен {0} на{1}: {2}</value>
</data>
<data name="UserNotInGuild" xml:space="preserve">
<value>шизик не на этом сервере</value>
</data>
<data name="SettingDoesntExist" xml:space="preserve">
<value>такой прикол не существует</value>
</data>
@ -240,19 +231,13 @@
<data name="InvalidChannel" xml:space="preserve">
<value>этого канала нету, ты шо</value>
</data>
<data name="RoleRemovalFailed" xml:space="preserve">
<value>я не украл звание {0} в связи с ошибкой! {1}</value>
</data>
<data name="DurationRequiredForTimeOuts" xml:space="preserve">
<data name="DurationRequiredForTimeOuts" xml:space="preserve">
<value>ты шо, мутить больше чем на 28 дней таймаут не разрешает, вот настроишь роль мута, тогда поговорим</value>
</data>
<data name="CannotTimeOutBot" xml:space="preserve">
<value>я не могу замутить ботов, сделай что нибудь</value>
</data>
<data name="SettingsStarterRole" xml:space="preserve">
<value>базовое звание</value>
</data>
<data name="EventCreated" xml:space="preserve">
<data name="EventCreated" xml:space="preserve">
<value>{0} приготовил новый квест {1}! он пройдёт в {2} и начнётся &lt;t:{3}:R&gt;!{4}</value>
</data>
<data name="SettingsEventNotificationRole" xml:space="preserve">
@ -333,28 +318,28 @@
<data name="MissingNumber" xml:space="preserve">
<value>укажи целое число от {0} до {1}</value>
</data>
<data name="MissingUser" xml:space="preserve">
<data name="MissingUser" xml:space="preserve">
<value>укажи самого шизика</value>
</data>
<data name="InvalidUser" xml:space="preserve">
<data name="InvalidUser" xml:space="preserve">
<value>надо указать юзверя вместо {0}!</value>
</data>
<data name="MissingMember" xml:space="preserve">
<data name="MissingMember" xml:space="preserve">
<value>укажи самого шизика</value>
</data>
<data name="InvalidMember" xml:space="preserve">
<value>укажи шизоида сервера вместо {0}!</value>
</data>
<data name="UserCannotBanMembers" xml:space="preserve">
<data name="InvalidMember" xml:space="preserve">
<value>укажи шизоида сервера!</value>
</data>
<data name="UserCannotBanMembers" xml:space="preserve">
<value>бан</value>
</data>
<data name="UserCannotManageMessages" xml:space="preserve">
<data name="UserCannotManageMessages" xml:space="preserve">
<value>тебе нельзя иметь власть над сообщениями шизоидов</value>
</data>
<data name="UserCannotKickMembers" xml:space="preserve">
<data name="UserCannotKickMembers" xml:space="preserve">
<value>кик шизиков нельзя</value>
</data>
<data name="UserCannotModerateMembers" xml:space="preserve">
<data name="UserCannotModerateMembers" xml:space="preserve">
<value>тебе нельзя управлять шизоидами</value>
</data>
<data name="UserCannotManageGuild" xml:space="preserve">
@ -384,10 +369,7 @@
<data name="MissingMuteReason" xml:space="preserve">
<value>укажи зачем мутить шизика</value>
</data>
<data name="MissingSetting" xml:space="preserve">
<value>укажи настройку которую менять нужно</value>
</data>
<data name="MissingUnbanReason" xml:space="preserve">
<data name="MissingUnbanReason" xml:space="preserve">
<value>укажи зачем раззабанивать шизика</value>
</data>
<data name="MissingUnmuteReason" xml:space="preserve">
@ -450,13 +432,37 @@
<data name="UserCannotUnmuteTarget" xml:space="preserve">
<value>тебе нельзя раззамучивать</value>
</data>
<data name="BotCannotUnmuteTarget" xml:space="preserve">
<data name="BotCannotUnmuteTarget" xml:space="preserve">
<value>я не могу его раззамутить...</value>
</data>
<data name="EventEarlyNotification" xml:space="preserve">
<data name="EventEarlyNotification" xml:space="preserve">
<value>{0}квест {1} начнется &lt;t:{2}:R&gt;!</value>
</data>
<data name="SettingsEventEarlyNotificationOffset" xml:space="preserve">
<data name="SettingsEventEarlyNotificationOffset" xml:space="preserve">
<value>заранее пнуть в минутах до начала квеста</value>
</data>
<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 const string Success = ":white_check_mark:";
public const string Warning = ":warning:";
public const string Error = ":x:";
public const string MissingArgument = ":keyboard:";
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.Text;
using System.Text.RegularExpressions;
using Boyfriend.Commands;
using Boyfriend.Data;
using Discord;
using Discord.Net;
using Discord.WebSocket;
@ -12,15 +13,13 @@ using Humanizer.Localisation;
namespace Boyfriend;
public static partial class Utils {
private static readonly Dictionary<string, string> ReflectionMessageCache = new();
public static readonly Dictionary<string, CultureInfo> CultureInfoCache = new() {
{ "ru", new CultureInfo("ru-RU") },
{ "en", new CultureInfo("en-US") },
{ "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() {
AllowedTypes = AllowedMentionTypes.Roles
@ -30,11 +29,6 @@ public static partial class Utils {
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) {
if (original is null) return null;
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) {
try {
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);
} catch (Exception e) {
@ -109,22 +87,23 @@ public static partial class Utils {
}
public static async Task
SendFeedbackAsync(string feedback, ulong guildId, string mention, bool sendPublic = false) {
var adminChannel = GetBotLogChannel(guildId);
var systemChannel = Boyfriend.Client.GetGuild(guildId).SystemChannel;
SendFeedbackAsync(string feedback, SocketGuild guild, string mention, bool sendPublic = false) {
var data = GuildData.Get(guild);
var adminChannel = data.PrivateFeedbackChannel;
var systemChannel = data.PublicFeedbackChannel;
var toSend = $"*[{mention}: {feedback}]*";
if (adminChannel is not null) await SilentSendAsync(adminChannel, toSend);
if (sendPublic && systemChannel is not null) await SilentSendAsync(systemChannel, toSend);
}
public static string GetHumanizedTimeOffset(TimeSpan span) {
return span.TotalSeconds > 0
? $" {span.Humanize(2, minUnit: TimeUnit.Second, maxUnit: TimeUnit.Month, culture: Messages.Culture.Name.Contains("RU") ? CultureInfoCache["ru"] : Messages.Culture)}"
: Messages.Ever;
public static string GetHumanizedTimeSpan(TimeSpan span) {
return span.TotalSeconds < 1
? 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) {
Messages.Culture = CultureInfoCache[Boyfriend.GetGuildConfig(guildId)["Lang"]];
public static void SetCurrentLanguage(SocketGuild guild) {
Messages.Culture = CultureInfoCache[GuildData.Get(guild).Preferences["Lang"]];
}
public static void SafeAppendToBuilder(StringBuilder appendTo, string appendWhat, SocketTextChannel? channel) {
@ -146,48 +125,37 @@ public static partial class Utils {
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) {
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]")]