Refactor guild data storage (#13)

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

View file

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

View file

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

View file

@ -1,8 +1,10 @@
using System.Collections.ObjectModel;
using System.Text; using System.Text;
using System.Timers;
using Boyfriend.Data;
using Discord; using Discord;
using Discord.Rest;
using Discord.WebSocket; using Discord.WebSocket;
using Newtonsoft.Json; using Timer = System.Timers.Timer;
namespace Boyfriend; namespace Boyfriend;
@ -20,7 +22,10 @@ public static class Boyfriend {
LargeThreshold = 500 LargeThreshold = 500
}; };
private static readonly List<Tuple<Game, TimeSpan>> ActivityList = new() { private static DateTimeOffset _nextSongAt = DateTimeOffset.MinValue;
private static uint _nextSongIndex;
private static readonly Tuple<Game, TimeSpan>[] ActivityList = {
Tuple.Create(new Game("Masayoshi Minoshima (ft. nomico) - Bad Apple!!", ActivityType.Listening), Tuple.Create(new Game("Masayoshi Minoshima (ft. nomico) - Bad Apple!!", ActivityType.Listening),
new TimeSpan(0, 3, 40)), new TimeSpan(0, 3, 40)),
Tuple.Create(new Game("Xi - Blue Zenith", ActivityType.Listening), new TimeSpan(0, 4, 16)), Tuple.Create(new Game("Xi - Blue Zenith", ActivityType.Listening), new TimeSpan(0, 4, 16)),
@ -32,33 +37,13 @@ public static class Boyfriend {
public static readonly DiscordSocketClient Client = new(Config); public static readonly DiscordSocketClient Client = new(Config);
private static readonly Dictionary<ulong, Dictionary<string, string>> GuildConfigDictionary = new(); private static readonly List<Task> GuildTickTasks = new();
private static readonly Dictionary<ulong, Dictionary<ulong, ReadOnlyCollection<ulong>>> RemovedRolesDictionary =
new();
public static readonly Dictionary<string, string> DefaultConfig = new() {
{ "Prefix", "!" },
{ "Lang", "en" },
{ "ReceiveStartupMessages", "false" },
{ "WelcomeMessage", "default" },
{ "SendWelcomeMessages", "true" },
{ "BotLogChannel", "0" },
{ "StarterRole", "0" },
{ "MuteRole", "0" },
{ "RemoveRolesOnMute", "false" },
{ "FrowningFace", "true" },
{ "EventStartedReceivers", "interested,role" },
{ "EventNotificationRole", "0" },
{ "EventNotificationChannel", "0" },
{ "EventEarlyNotificationOffset", "0" }
};
public static void Main() { public static void Main() {
Init().GetAwaiter().GetResult(); InitAsync().GetAwaiter().GetResult();
} }
private static async Task Init() { private static async Task InitAsync() {
var token = (await File.ReadAllTextAsync("token.txt")).Trim(); var token = (await File.ReadAllTextAsync("token.txt")).Trim();
Client.Log += Log; Client.Log += Log;
@ -68,13 +53,35 @@ public static class Boyfriend {
EventHandler.InitEvents(); EventHandler.InitEvents();
while (ActivityList.Count > 0) var timer = new Timer();
foreach (var activity in ActivityList) { timer.Interval = 1000;
await Client.SetActivityAsync(activity.Item1); timer.AutoReset = true;
await Task.Delay(activity.Item2); timer.Elapsed += TickAllGuildsAsync;
if (ActivityList.Length is 0) timer.Dispose(); // CodeQL moment
timer.Start();
while (ActivityList.Length > 0)
if (DateTimeOffset.Now >= _nextSongAt) {
var nextSong = ActivityList[_nextSongIndex];
await Client.SetActivityAsync(nextSong.Item1);
_nextSongAt = DateTimeOffset.Now.Add(nextSong.Item2);
_nextSongIndex++;
if (_nextSongIndex >= ActivityList.Length) _nextSongIndex = 0;
} }
} }
private static async void TickAllGuildsAsync(object? sender, ElapsedEventArgs e) {
foreach (var guild in Client.Guilds) GuildTickTasks.Add(TickGuildAsync(guild));
try { Task.WaitAll(GuildTickTasks.ToArray()); } catch (AggregateException ex) {
foreach (var exc in ex.InnerExceptions)
await Log(new LogMessage(LogSeverity.Error, nameof(Boyfriend),
"Exception while ticking guilds", exc));
}
GuildTickTasks.Clear();
}
public static Task Log(LogMessage msg) { public static Task Log(LogMessage msg) {
switch (msg.Severity) { switch (msg.Severity) {
case LogSeverity.Critical: case LogSeverity.Critical:
@ -102,53 +109,63 @@ public static class Boyfriend {
return Task.CompletedTask; return Task.CompletedTask;
} }
public static async Task WriteGuildConfigAsync(ulong id) { private static async Task TickGuildAsync(SocketGuild guild) {
await File.WriteAllTextAsync($"config_{id}.json", var data = GuildData.Get(guild);
JsonConvert.SerializeObject(GuildConfigDictionary[id], Formatting.Indented)); var config = data.Preferences;
var saveData = false;
_ = int.TryParse(config["EventEarlyNotificationOffset"], out var offset);
foreach (var schEvent in guild.Events)
if (schEvent.Status is GuildScheduledEventStatus.Scheduled && config["AutoStartEvents"] is "true" &&
DateTimeOffset.Now >= schEvent.StartTime) { await schEvent.StartAsync(); } else if
(!data.EarlyNotifications.Contains(schEvent.Id) &&
DateTimeOffset.Now >= schEvent.StartTime.Subtract(new TimeSpan(0, offset, 0))) {
data.EarlyNotifications.Add(schEvent.Id);
var receivers = config["EventStartedReceivers"];
var role = guild.GetRole(ulong.Parse(config["EventNotificationRole"]));
var mentions = StringBuilder;
if (RemovedRolesDictionary.TryGetValue(id, out var removedRoles)) if (receivers.Contains("role") && role is not null) mentions.Append($"{role.Mention} ");
await File.WriteAllTextAsync($"removedroles_{id}.json", if (receivers.Contains("users") || receivers.Contains("interested"))
JsonConvert.SerializeObject(removedRoles, Formatting.Indented)); mentions = (await schEvent.GetUsersAsync(15))
.Where(user => role is null || !((RestGuildUser)user).RoleIds.Contains(role.Id))
.Aggregate(mentions, (current, user) => current.Append($"{user.Mention} "));
await Utils.GetEventNotificationChannel(guild)?.SendMessageAsync(string.Format(
Messages.EventEarlyNotification,
mentions,
Utils.Wrap(schEvent.Name),
schEvent.StartTime.ToUnixTimeSeconds().ToString()))!;
mentions.Clear();
} }
public static Dictionary<string, string> GetGuildConfig(ulong id) { foreach (var mData in data.MemberData.Values) {
if (GuildConfigDictionary.TryGetValue(id, out var cfg)) return cfg; if (DateTimeOffset.Now >= mData.BannedUntil) _ = guild.RemoveBanAsync(mData.Id);
var path = $"config_{id}.json"; if (mData.IsInGuild) {
if (DateTimeOffset.Now >= mData.MutedUntil) {
if (!File.Exists(path)) File.Create(path).Dispose(); await Utils.UnmuteMemberAsync(data, Client.CurrentUser.ToString(), guild.GetUser(mData.Id),
Messages.PunishmentExpired);
var json = File.ReadAllText(path); saveData = true;
var config = JsonConvert.DeserializeObject<Dictionary<string, string>>(json)
?? new Dictionary<string, string>();
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);
} }
GuildConfigDictionary.Add(id, config); for (var i = mData.Reminders.Count - 1; i >= 0; i--) {
var reminder = mData.Reminders[i];
return config; 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;
} }
public static Dictionary<ulong, ReadOnlyCollection<ulong>> GetRemovedRoles(ulong id) { await channel.SendMessageAsync($"<@{mData.Id}> {Utils.Wrap(reminder.ReminderText)}");
if (RemovedRolesDictionary.TryGetValue(id, out var dict)) return dict; mData.Reminders.RemoveAt(i);
var path = $"removedroles_{id}.json";
if (!File.Exists(path)) File.Create(path).Dispose(); saveData = true;
}
}
}
}
var json = File.ReadAllText(path); if (saveData) data.Save(true).Wait();
var removedRoles = JsonConvert.DeserializeObject<Dictionary<ulong, ReadOnlyCollection<ulong>>>(json)
?? new Dictionary<ulong, ReadOnlyCollection<ulong>>();
RemovedRolesDictionary.Add(id, removedRoles);
return removedRoles;
} }
} }

View file

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

View file

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

View file

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

View file

@ -1,3 +1,4 @@
using Boyfriend.Data;
using Humanizer; using Humanizer;
namespace Boyfriend.Commands; namespace Boyfriend.Commands;
@ -6,7 +7,7 @@ public sealed class HelpCommand : ICommand {
public string[] Aliases { get; } = { "help", "помощь", "справка" }; public string[] Aliases { get; } = { "help", "помощь", "справка" };
public Task RunAsync(CommandProcessor cmd, string[] args, string[] cleanArgs) { public Task RunAsync(CommandProcessor cmd, string[] args, string[] cleanArgs) {
var prefix = Boyfriend.GetGuildConfig(cmd.Context.Guild.Id)["Prefix"]; var prefix = GuildData.Get(cmd.Context.Guild).Preferences["Prefix"];
var toSend = Boyfriend.StringBuilder.Append(Messages.CommandHelp); var toSend = Boyfriend.StringBuilder.Append(Messages.CommandHelp);
foreach (var command in CommandProcessor.Commands) foreach (var command in CommandProcessor.Commands)

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,3 +1,4 @@
using Boyfriend.Data;
using Discord; using Discord;
using Discord.WebSocket; using Discord.WebSocket;
@ -9,37 +10,24 @@ public sealed class UnmuteCommand : ICommand {
public async Task RunAsync(CommandProcessor cmd, string[] args, string[] cleanArgs) { public async Task RunAsync(CommandProcessor cmd, string[] args, string[] cleanArgs) {
if (!cmd.HasPermission(GuildPermission.ModerateMembers)) return; if (!cmd.HasPermission(GuildPermission.ModerateMembers)) return;
var toUnmute = cmd.GetMember(args, cleanArgs, 0, "ToUnmute"); var toUnmute = cmd.GetMember(args, 0);
if (toUnmute is null) return; if (toUnmute is null) return;
var reason = cmd.GetRemaining(args, 1, "UnmuteReason"); var reason = cmd.GetRemaining(args, 1, "UnmuteReason");
if (reason is not null && cmd.CanInteractWith(toUnmute, "Unmute")) if (reason is not null && cmd.CanInteractWith(toUnmute, "Unmute"))
await UnmuteMemberAsync(cmd, toUnmute, reason); await UnmuteMemberAsync(cmd, toUnmute, reason);
} }
public static async Task UnmuteMemberAsync(CommandProcessor cmd, SocketGuildUser toUnmute, private static async Task UnmuteMemberAsync(CommandProcessor cmd, SocketGuildUser toUnmute,
string reason) { string reason) {
var requestOptions = Utils.GetRequestOptions($"({cmd.Context.User}) {reason}"); var isMuted = await Utils.UnmuteMemberAsync(GuildData.Get(cmd.Context.Guild), cmd.Context.User.ToString(),
var role = Utils.GetMuteRole(cmd.Context.Guild); toUnmute, reason);
if (role is not null && toUnmute.Roles.Contains(role)) { if (!isMuted) {
var rolesRemoved = Boyfriend.GetRemovedRoles(cmd.Context.Guild.Id);
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); cmd.Reply(Messages.MemberNotMuted, ReplyEmojis.Error);
return; return;
} }
await toUnmute.RemoveTimeOutAsync(); cmd.ConfigWriteScheduled = true;
}
var feedback = string.Format(Messages.FeedbackMemberUnmuted, toUnmute.Mention, Utils.Wrap(reason)); var feedback = string.Format(Messages.FeedbackMemberUnmuted, toUnmute.Mention, Utils.Wrap(reason));
cmd.Reply(feedback, ReplyEmojis.Unmuted); cmd.Reply(feedback, ReplyEmojis.Unmuted);

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

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

View file

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

View file

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

View file

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

View file

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

View file

@ -145,7 +145,7 @@
<value>You do not have permission to execute this command!</value> <value>You do not have permission to execute this command!</value>
</data> </data>
<data name="YouWereBanned" xml:space="preserve"> <data name="YouWereBanned" xml:space="preserve">
<value>You were banned by {0} in guild {1} for {2}</value> <value>You were banned by {0} in guild `{1}` for {2}</value>
</data> </data>
<data name="PunishmentExpired" xml:space="preserve"> <data name="PunishmentExpired" xml:space="preserve">
<value>Punishment expired</value> <value>Punishment expired</value>
@ -160,7 +160,7 @@
<value>Command help:</value> <value>Command help:</value>
</data> </data>
<data name="YouWereKicked" xml:space="preserve"> <data name="YouWereKicked" xml:space="preserve">
<value>You were kicked by {0} in guild {1} for {2}</value> <value>You were kicked by {0} in guild `{1}` for {2}</value>
</data> </data>
<data name="Milliseconds" xml:space="preserve"> <data name="Milliseconds" xml:space="preserve">
<value>ms</value> <value>ms</value>
@ -189,15 +189,9 @@
<data name="SettingsSendWelcomeMessages" xml:space="preserve"> <data name="SettingsSendWelcomeMessages" xml:space="preserve">
<value>Send welcome messages</value> <value>Send welcome messages</value>
</data> </data>
<data name="SettingsStarterRole" xml:space="preserve">
<value>Starter role</value>
</data>
<data name="SettingsMuteRole" xml:space="preserve"> <data name="SettingsMuteRole" xml:space="preserve">
<value>Mute role</value> <value>Mute role</value>
</data> </data>
<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> <value>Language not supported! Supported languages:</value>
</data> </data>
@ -213,9 +207,6 @@
<data name="MemberNotMuted" xml:space="preserve"> <data name="MemberNotMuted" xml:space="preserve">
<value>Member not muted!</value> <value>Member not muted!</value>
</data> </data>
<data name="RolesReturned" xml:space="preserve">
<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> <value>Welcome message</value>
</data> </data>
@ -225,9 +216,6 @@
<data name="FeedbackUserBanned" xml:space="preserve"> <data name="FeedbackUserBanned" xml:space="preserve">
<value>Banned {0} for{1}: {2}</value> <value>Banned {0} for{1}: {2}</value>
</data> </data>
<data name="UserNotInGuild" xml:space="preserve">
<value>The specified user is not a member of this server!</value>
</data>
<data name="SettingDoesntExist" xml:space="preserve"> <data name="SettingDoesntExist" xml:space="preserve">
<value>That setting doesn't exist!</value> <value>That setting doesn't exist!</value>
</data> </data>
@ -243,9 +231,6 @@
<data name="InvalidChannel" xml:space="preserve"> <data name="InvalidChannel" xml:space="preserve">
<value>This channel does not exist!</value> <value>This channel does not exist!</value>
</data> </data>
<data name="RoleRemovalFailed" xml:space="preserve">
<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> <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>
@ -343,7 +328,7 @@
<value>You need to specify a guild member!</value> <value>You need to specify a guild member!</value>
</data> </data>
<data name="InvalidMember" xml:space="preserve"> <data name="InvalidMember" xml:space="preserve">
<value>You need to specify a guild member instead of {0}!</value> <value>You did not specify a member of this guild!</value>
</data> </data>
<data name="UserCannotBanMembers" xml:space="preserve"> <data name="UserCannotBanMembers" xml:space="preserve">
<value>You cannot ban users from this guild!</value> <value>You cannot ban users from this guild!</value>
@ -390,9 +375,6 @@
<data name="MissingUnmuteReason" xml:space="preserve"> <data name="MissingUnmuteReason" xml:space="preserve">
<value>You need to specify a reason for unmute this member!</value> <value>You need to specify a reason for unmute this member!</value>
</data> </data>
<data name="MissingSetting" xml:space="preserve">
<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> <value>You cannot ban the owner of this guild!</value>
</data> </data>
@ -459,4 +441,28 @@
<data name="SettingsEventEarlyNotificationOffset" xml:space="preserve"> <data name="SettingsEventEarlyNotificationOffset" xml:space="preserve">
<value>Early event start notification offset</value> <value>Early event start notification offset</value>
</data> </data>
<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> </root>

View file

@ -145,7 +145,7 @@
<value>У тебя недостаточно прав для выполнения этой команды!</value> <value>У тебя недостаточно прав для выполнения этой команды!</value>
</data> </data>
<data name="YouWereBanned" xml:space="preserve"> <data name="YouWereBanned" xml:space="preserve">
<value>Тебя забанил {0} на сервере {1} за {2}</value> <value>Тебя забанил {0} на сервере `{1}` за {2}</value>
</data> </data>
<data name="PunishmentExpired" xml:space="preserve"> <data name="PunishmentExpired" xml:space="preserve">
<value>Время наказания истекло</value> <value>Время наказания истекло</value>
@ -160,7 +160,7 @@
<value>Справка по командам:</value> <value>Справка по командам:</value>
</data> </data>
<data name="YouWereKicked" xml:space="preserve"> <data name="YouWereKicked" xml:space="preserve">
<value>Тебя кикнул {0} на сервере {1} за {2}</value> <value>Тебя кикнул {0} на сервере `{1}` за {2}</value>
</data> </data>
<data name="Milliseconds" xml:space="preserve"> <data name="Milliseconds" xml:space="preserve">
<value>мс</value> <value>мс</value>
@ -192,9 +192,6 @@
<data name="SettingsMuteRole" xml:space="preserve"> <data name="SettingsMuteRole" xml:space="preserve">
<value>Роль мута</value> <value>Роль мута</value>
</data> </data>
<data name="SettingsBotLogChannel" xml:space="preserve">
<value>Канал бот-уведомлений</value>
</data>
<data name="LanguageNotSupported" xml:space="preserve"> <data name="LanguageNotSupported" xml:space="preserve">
<value>Язык не поддерживается! Поддерживаемые языки:</value> <value>Язык не поддерживается! Поддерживаемые языки:</value>
</data> </data>
@ -210,9 +207,6 @@
<data name="MemberNotMuted" xml:space="preserve"> <data name="MemberNotMuted" xml:space="preserve">
<value>Участник не заглушен!</value> <value>Участник не заглушен!</value>
</data> </data>
<data name="RolesReturned" xml:space="preserve">
<value>Кто-то убрал роль мута самостоятельно! Я вернул все роли, которые забрал при муте</value>
</data>
<data name="SettingsWelcomeMessage" xml:space="preserve"> <data name="SettingsWelcomeMessage" xml:space="preserve">
<value>Приветствие</value> <value>Приветствие</value>
</data> </data>
@ -222,9 +216,6 @@
<data name="FeedbackUserBanned" xml:space="preserve"> <data name="FeedbackUserBanned" xml:space="preserve">
<value>Забанен {0} на{1}: {2}</value> <value>Забанен {0} на{1}: {2}</value>
</data> </data>
<data name="UserNotInGuild" xml:space="preserve">
<value>Указанный пользователь не является участником этого сервера!</value>
</data>
<data name="SettingDoesntExist" xml:space="preserve"> <data name="SettingDoesntExist" xml:space="preserve">
<value>Такая настройка не существует!</value> <value>Такая настройка не существует!</value>
</data> </data>
@ -240,18 +231,12 @@
<data name="InvalidChannel" xml:space="preserve"> <data name="InvalidChannel" xml:space="preserve">
<value>Этот канал не существует!</value> <value>Этот канал не существует!</value>
</data> </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> <value>Я не могу заглушить кого-то на более чем 28 дней, используя тайм-ауты! Или укажи продолжительность менее 28 дней, или установи роль мута в настройках</value>
</data> </data>
<data name="CannotTimeOutBot" xml:space="preserve"> <data name="CannotTimeOutBot" xml:space="preserve">
<value>Я не могу использовать тайм-ауты на других ботах! Попробуй указать роль мута в настройках</value> <value>Я не могу использовать тайм-ауты на других ботах! Попробуй указать роль мута в настройках</value>
</data> </data>
<data name="SettingsStarterRole" xml:space="preserve">
<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> <value>{0} создал событие {1}! Оно пройдёт в {2} и начнётся &lt;t:{3}:R&gt;!{4}</value>
</data> </data>
@ -343,7 +328,7 @@
<value>Надо указать участника сервера!</value> <value>Надо указать участника сервера!</value>
</data> </data>
<data name="InvalidMember" xml:space="preserve"> <data name="InvalidMember" xml:space="preserve">
<value>Надо указать участника сервера вместо {0}!</value> <value>Тебе надо указать участника этого сервера!</value>
</data> </data>
<data name="UserCannotBanMembers" xml:space="preserve"> <data name="UserCannotBanMembers" xml:space="preserve">
<value>Ты не можешь банить пользователей на этом сервере!</value> <value>Ты не можешь банить пользователей на этом сервере!</value>
@ -384,9 +369,6 @@
<data name="MissingMuteReason" xml:space="preserve"> <data name="MissingMuteReason" xml:space="preserve">
<value>Надо указать причину для мута этого участника!</value> <value>Надо указать причину для мута этого участника!</value>
</data> </data>
<data name="MissingSetting" xml:space="preserve">
<value>Надо указать настройку, которую нужно изменить!</value>
</data>
<data name="MissingUnbanReason" xml:space="preserve"> <data name="MissingUnbanReason" xml:space="preserve">
<value>Надо указать причину для разбана этого пользователя!</value> <value>Надо указать причину для разбана этого пользователя!</value>
</data> </data>
@ -459,4 +441,28 @@
<data name="SettingsEventEarlyNotificationOffset" xml:space="preserve"> <data name="SettingsEventEarlyNotificationOffset" xml:space="preserve">
<value>Офсет отправки преждевременного уведомления о начале события</value> <value>Офсет отправки преждевременного уведомления о начале события</value>
</data> </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> </root>

View file

@ -145,7 +145,7 @@
<value>у тебя прав нету, твои проблемы.</value> <value>у тебя прав нету, твои проблемы.</value>
</data> </data>
<data name="YouWereBanned" xml:space="preserve"> <data name="YouWereBanned" xml:space="preserve">
<value>здарова, тебя крч забанил {0} на сервере {1} за {2}</value> <value>здарова, тебя крч забанил {0} на сервере `{1}` за {2}</value>
</data> </data>
<data name="PunishmentExpired" xml:space="preserve"> <data name="PunishmentExpired" xml:space="preserve">
<value>время бана закончиловсь</value> <value>время бана закончиловсь</value>
@ -160,7 +160,7 @@
<value>туториал по приколам:</value> <value>туториал по приколам:</value>
</data> </data>
<data name="YouWereKicked" xml:space="preserve"> <data name="YouWereKicked" xml:space="preserve">
<value>здарова, тебя крч кикнул {0} на сервере {1} за {2}</value> <value>здарова, тебя крч кикнул {0} на сервере `{1}` за {2}</value>
</data> </data>
<data name="Milliseconds" xml:space="preserve"> <data name="Milliseconds" xml:space="preserve">
<value>мс</value> <value>мс</value>
@ -192,9 +192,6 @@
<data name="SettingsMuteRole" xml:space="preserve"> <data name="SettingsMuteRole" xml:space="preserve">
<value>роль замученного</value> <value>роль замученного</value>
</data> </data>
<data name="SettingsBotLogChannel" xml:space="preserve">
<value>канал бот-уведомлений</value>
</data>
<data name="LanguageNotSupported" xml:space="preserve"> <data name="LanguageNotSupported" xml:space="preserve">
<value>такого языка нету, ты шо, есть только такие:</value> <value>такого языка нету, ты шо, есть только такие:</value>
</data> </data>
@ -210,9 +207,6 @@
<data name="MemberNotMuted" xml:space="preserve"> <data name="MemberNotMuted" xml:space="preserve">
<value>шизоид не замучен!</value> <value>шизоид не замучен!</value>
</data> </data>
<data name="RolesReturned" xml:space="preserve">
<value>кто-то решил поумничать и обошел роль мута. я ее вернул.</value>
</data>
<data name="SettingsWelcomeMessage" xml:space="preserve"> <data name="SettingsWelcomeMessage" xml:space="preserve">
<value>приветствие</value> <value>приветствие</value>
</data> </data>
@ -222,9 +216,6 @@
<data name="FeedbackUserBanned" xml:space="preserve"> <data name="FeedbackUserBanned" xml:space="preserve">
<value>забанен {0} на{1}: {2}</value> <value>забанен {0} на{1}: {2}</value>
</data> </data>
<data name="UserNotInGuild" xml:space="preserve">
<value>шизик не на этом сервере</value>
</data>
<data name="SettingDoesntExist" xml:space="preserve"> <data name="SettingDoesntExist" xml:space="preserve">
<value>такой прикол не существует</value> <value>такой прикол не существует</value>
</data> </data>
@ -240,18 +231,12 @@
<data name="InvalidChannel" xml:space="preserve"> <data name="InvalidChannel" xml:space="preserve">
<value>этого канала нету, ты шо</value> <value>этого канала нету, ты шо</value>
</data> </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> <value>ты шо, мутить больше чем на 28 дней таймаут не разрешает, вот настроишь роль мута, тогда поговорим</value>
</data> </data>
<data name="CannotTimeOutBot" xml:space="preserve"> <data name="CannotTimeOutBot" xml:space="preserve">
<value>я не могу замутить ботов, сделай что нибудь</value> <value>я не могу замутить ботов, сделай что нибудь</value>
</data> </data>
<data name="SettingsStarterRole" xml:space="preserve">
<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> <value>{0} приготовил новый квест {1}! он пройдёт в {2} и начнётся &lt;t:{3}:R&gt;!{4}</value>
</data> </data>
@ -343,7 +328,7 @@
<value>укажи самого шизика</value> <value>укажи самого шизика</value>
</data> </data>
<data name="InvalidMember" xml:space="preserve"> <data name="InvalidMember" xml:space="preserve">
<value>укажи шизоида сервера вместо {0}!</value> <value>укажи шизоида сервера!</value>
</data> </data>
<data name="UserCannotBanMembers" xml:space="preserve"> <data name="UserCannotBanMembers" xml:space="preserve">
<value>бан</value> <value>бан</value>
@ -384,9 +369,6 @@
<data name="MissingMuteReason" xml:space="preserve"> <data name="MissingMuteReason" xml:space="preserve">
<value>укажи зачем мутить шизика</value> <value>укажи зачем мутить шизика</value>
</data> </data>
<data name="MissingSetting" xml:space="preserve">
<value>укажи настройку которую менять нужно</value>
</data>
<data name="MissingUnbanReason" xml:space="preserve"> <data name="MissingUnbanReason" xml:space="preserve">
<value>укажи зачем раззабанивать шизика</value> <value>укажи зачем раззабанивать шизика</value>
</data> </data>
@ -459,4 +441,28 @@
<data name="SettingsEventEarlyNotificationOffset" xml:space="preserve"> <data name="SettingsEventEarlyNotificationOffset" xml:space="preserve">
<value>заранее пнуть в минутах до начала квеста</value> <value>заранее пнуть в минутах до начала квеста</value>
</data> </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> </root>

View file

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

View file

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