1
0
Fork 1
mirror of https://github.com/TeamOctolings/Octobot.git synced 2025-04-20 00:43:36 +03:00

Remora.Discord part 1 out of ∞

Signed-off-by: Octol1ttle <l1ttleofficial@outlook.com>
This commit is contained in:
Octol1ttle 2023-05-16 00:11:11 +05:00
parent 02949656bf
commit 201a1ce079
Signed by: Octol1ttle
GPG key ID: B77C34313AEE1FFF
24 changed files with 141 additions and 1588 deletions

1
.gitignore vendored
View file

@ -1,6 +1,5 @@
.idea/ .idea/
*.user *.user
token.txt
bin/ bin/
obj/ obj/
/packages/ /packages/

View file

@ -1,180 +1,78 @@
using System.Text; using System.Reflection;
using System.Timers; using Microsoft.Extensions.Configuration;
using Boyfriend.Data; using Microsoft.Extensions.DependencyInjection;
using Discord; using Microsoft.Extensions.Hosting;
using Discord.Rest; using Microsoft.Extensions.Logging;
using Discord.WebSocket; using Remora.Discord.API.Abstractions.Objects;
using Timer = System.Timers.Timer; using Remora.Discord.Caching.Extensions;
using Remora.Discord.Caching.Services;
using Remora.Discord.Gateway.Extensions;
using Remora.Discord.Hosting.Extensions;
namespace Boyfriend; namespace Boyfriend;
public static class Boyfriend { public class Boyfriend {
public static readonly StringBuilder StringBuilder = new(); public static ILogger<Boyfriend> Logger = null!;
public static IConfiguration GuildConfiguration = null!;
private static readonly DiscordSocketConfig Config = new() { private static readonly Dictionary<string, string> ReflectionMessageCache = new();
MessageCacheSize = 250,
GatewayIntents
= (GatewayIntents.AllUnprivileged | GatewayIntents.MessageContent | GatewayIntents.GuildMembers)
& ~GatewayIntents.GuildInvites,
AlwaysDownloadUsers = true,
AlwaysResolveStickers = false,
AlwaysDownloadDefaultStickers = false,
LargeThreshold = 500
};
private static DateTimeOffset _nextSongAt = DateTimeOffset.MinValue; public static async Task Main(string[] args) {
private static uint _nextSongIndex; var host = CreateHostBuilder(args).UseConsoleLifetime().Build();
private static readonly (Game Song, TimeSpan Duration)[] ActivityList = { var services = host.Services;
(new Game("Masayoshi Minoshima (ft. nomico) - Bad Apple!!", ActivityType.Listening), new TimeSpan(0, 3, 40)), Logger = services.GetRequiredService<ILogger<Boyfriend>>();
(new Game("Xi - Blue Zenith", ActivityType.Listening), new TimeSpan(0, 4, 16)), GuildConfiguration = services.GetRequiredService<IConfigurationBuilder>().AddJsonFile("guild_configs.json")
(new Game("UNDEAD CORPORATION - Everything will freeze", ActivityType.Listening), new TimeSpan(0, 3, 18)), .Build();
(new Game("Splatoon 3 - Candy-Coated Rocks", ActivityType.Listening), new TimeSpan(0, 2, 39)),
(new Game("RetroSpecter - Overtime", ActivityType.Listening), new TimeSpan(0, 4, 33)),
(new Game("SOOOO - Happppy song", ActivityType.Listening), new TimeSpan(0, 5, 24))
};
public static readonly DiscordSocketClient Client = new(Config); await host.RunAsync();
private static readonly List<Task> GuildTickTasks = new();
private static async Task Main() {
var token = (await File.ReadAllTextAsync("token.txt")).Trim();
Client.Log += Log;
await Client.LoginAsync(TokenType.Bot, token);
await Client.StartAsync();
EventHandler.InitEvents();
var timer = new Timer();
timer.Interval = 1000;
timer.AutoReset = true;
timer.Elapsed += TickAllGuildsAsync;
if (ActivityList.Length is 0) timer.Dispose(); // CodeQL moment
timer.Start();
await Task.Delay(-1);
} }
private static async void TickAllGuildsAsync(object? sender, ElapsedEventArgs e) { private static IHostBuilder CreateHostBuilder(string[] args) {
if (GuildTickTasks.Count is not 0) return; return Host.CreateDefaultBuilder(args)
.AddDiscordService(
services => {
var configuration = services.GetRequiredService<IConfiguration>();
var now = DateTimeOffset.UtcNow; return configuration.GetValue<string?>("BOT_TOKEN")
foreach (var guild in Client.Guilds) GuildTickTasks.Add(TickGuildAsync(guild, now)); ?? throw new InvalidOperationException(
"No bot token has been provided. Set the "
+ "BOT_TOKEN environment variable to a valid token.");
}
).ConfigureServices(
(_, services) => {
var responderTypes = typeof(Boyfriend).Assembly
.GetExportedTypes()
.Where(t => t.IsResponder());
foreach (var responderType in responderTypes) services.AddResponder(responderType);
if (now >= _nextSongAt) { services.AddDiscordCaching();
var nextSong = ActivityList[_nextSongIndex]; services.Configure<CacheSettings>(
await Client.SetActivityAsync(nextSong.Song); settings => { settings.SetAbsoluteExpiration<IMessage>(TimeSpan.FromDays(7)); });
_nextSongAt = now.Add(nextSong.Duration);
_nextSongIndex++;
if (_nextSongIndex >= ActivityList.Length) _nextSongIndex = 0;
}
try { Task.WaitAll(GuildTickTasks.ToArray()); } catch (AggregateException ex) { services.AddSingleton<IConfigurationBuilder, ConfigurationBuilder>();
foreach (var exc in ex.InnerExceptions) }
await Log( ).ConfigureLogging(
new LogMessage( c => c.AddConsole()
LogSeverity.Error, nameof(Boyfriend), .AddFilter("System.Net.Http.HttpClient.*.LogicalHandler", LogLevel.Warning)
"Exception while ticking guilds", exc)); .AddFilter("System.Net.Http.HttpClient.*.ClientHandler", LogLevel.Warning)
} );
GuildTickTasks.Clear();
} }
public static Task Log(LogMessage msg) { public static string GetLocalized(string key) {
switch (msg.Severity) { var propertyName = key;
case LogSeverity.Critical: key = $"{Messages.Culture}/{key}";
Console.ForegroundColor = ConsoleColor.DarkRed; if (ReflectionMessageCache.TryGetValue(key, out var cached)) return cached;
Console.Error.WriteLine(msg.ToString());
break;
case LogSeverity.Error:
Console.ForegroundColor = ConsoleColor.Red;
Console.Error.WriteLine(msg.ToString());
break;
case LogSeverity.Warning:
Console.ForegroundColor = ConsoleColor.Yellow;
Console.WriteLine(msg.ToString());
break;
case LogSeverity.Info:
Console.WriteLine(msg.ToString());
break;
case LogSeverity.Verbose: var toReturn =
case LogSeverity.Debug: typeof(Messages).GetProperty(propertyName, BindingFlags.NonPublic | BindingFlags.Static)?.GetValue(null)
default: return Task.CompletedTask; ?.ToString();
if (toReturn is null) {
Logger.LogError("Could not find localized property: {Name}", propertyName);
return key;
} }
Console.ResetColor(); ReflectionMessageCache.Add(key, toReturn);
return Task.CompletedTask; return toReturn;
}
private static async Task TickGuildAsync(SocketGuild guild, DateTimeOffset now) {
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)
&& 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 (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} "));
await Utils.GetEventNotificationChannel(guild)?.SendMessageAsync(
string.Format(
Messages.EventEarlyNotification,
mentions,
Utils.Wrap(schEvent.Name),
schEvent.StartTime.ToUnixTimeSeconds().ToString()))!;
mentions.Clear();
}
foreach (var mData in data.MemberData.Values) {
var user = guild.GetUser(mData.Id);
if (now >= mData.BannedUntil && await guild.GetBanAsync(mData.Id) is not null)
_ = guild.RemoveBanAsync(mData.Id);
if (!mData.IsInGuild) continue;
if (mData.MutedUntil is null
&& ulong.TryParse(config["StarterRole"], out var starterRoleId)
&& guild.GetRole(starterRoleId) is not null
&& !mData.Roles.Contains(starterRoleId)) _ = user.AddRoleAsync(starterRoleId);
if (now >= mData.MutedUntil) {
saveData = await Utils.UnmuteMemberAsync(
data, Client.CurrentUser.ToString(), user,
Messages.PunishmentExpired);
}
for (var i = mData.Reminders.Count - 1; i >= 0; i--) {
var reminder = mData.Reminders[i];
if (now < reminder.RemindAt) continue;
var channel = guild.GetTextChannel(reminder.ReminderChannel);
var toSend = $"{ReplyEmojis.Reminder} {user.Mention} {Utils.Wrap(reminder.ReminderText)}";
if (channel is not null)
await channel.SendMessageAsync(toSend);
else
await Utils.SendDirectMessage(user, toSend);
mData.Reminders.RemoveAt(i);
saveData = true;
}
}
if (saveData) await data.Save(true);
} }
} }

View file

@ -19,8 +19,20 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Discord.Net" Version="3.10.0"/> <PackageReference Include="Humanizer.Core.ru" Version="2.14.1"/>
<PackageReference Include="Humanizer.Core" Version="2.14.1" /> <PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0-preview.3.23174.8"/>
<PackageReference Include="Humanizer.Core.ru" Version="2.14.1" /> <PackageReference Include="Remora.Discord" Version="2023.3.0"/>
</ItemGroup>
<ItemGroup>
<Compile Remove="old\**"/>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Remove="old\**"/>
</ItemGroup>
<ItemGroup>
<None Remove="old\**"/>
</ItemGroup> </ItemGroup>
</Project> </Project>

View file

@ -1,321 +0,0 @@
using System.Text;
using Boyfriend.Commands;
using Boyfriend.Data;
using Discord;
using Discord.Commands;
using Discord.WebSocket;
namespace Boyfriend;
public sealed class CommandProcessor {
private static readonly string Mention = $"<@{Boyfriend.Client.CurrentUser.Id}>";
private static readonly TimeSpan Infinity = TimeSpan.FromMilliseconds(-1);
public static readonly ICommand[] Commands = {
new BanCommand(), new ClearCommand(), new HelpCommand(),
new KickCommand(), new MuteCommand(), new PingCommand(),
new SettingsCommand(), new UnbanCommand(), new UnmuteCommand(),
new RemindCommand()
};
private readonly StringBuilder _stackedPrivateFeedback = new();
private readonly StringBuilder _stackedPublicFeedback = new();
private readonly StringBuilder _stackedReplyMessage = new();
private readonly List<Task> _tasks = new();
public readonly SocketCommandContext Context;
public bool ConfigWriteScheduled = false;
public CommandProcessor(SocketUserMessage message) {
Context = new SocketCommandContext(Boyfriend.Client, message);
}
public async Task HandleCommandAsync() {
var guild = Context.Guild;
var data = GuildData.Get(guild);
Utils.SetCurrentLanguage(guild);
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], data.Preferences["Prefix"]));
try { Task.WaitAll(_tasks.ToArray()); } catch (AggregateException e) {
foreach (var ex in e.InnerExceptions)
await Boyfriend.Log(
new LogMessage(
LogSeverity.Error, nameof(CommandProcessor),
"Exception while executing commands", ex));
}
_tasks.Clear();
if (ConfigWriteScheduled) await data.Save(true);
SendFeedbacks();
}
private async Task RunCommandOnLine(string line, string cleanLine, string prefix) {
var prefixed = line.StartsWith(prefix);
if (!prefixed && !line.StartsWith(Mention)) return;
foreach (var command in Commands) {
var lineNoMention = line.Remove(0, prefixed ? prefix.Length : Mention.Length);
if (!command.Aliases.Contains(lineNoMention.Trim().Split()[0])) continue;
var args = lineNoMention.Trim().Split().Skip(1).ToArray();
var cleanArgs = cleanLine.Split().Skip(lineNoMention.StartsWith(" ") ? 2 : 1).ToArray();
await command.RunAsync(this, args, cleanArgs);
if (_stackedReplyMessage.Length > 0) _ = Context.Channel.TriggerTypingAsync();
return;
}
}
public void Reply(string response, string? customEmoji = null) {
Utils.SafeAppendToBuilder(
_stackedReplyMessage, $"{customEmoji ?? ReplyEmojis.Success} {response}",
Context.Message);
}
public void Audit(string action, bool isPublic = true) {
var format = $"*[{Context.User.Mention}: {action}]*";
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);
}
private void SendFeedbacks(bool reply = true) {
var hasReply = _stackedReplyMessage.Length > 0;
if (reply && hasReply)
_ = Context.Message.ReplyAsync(_stackedReplyMessage.ToString(), false, null, AllowedMentions.None);
var data = GuildData.Get(Context.Guild);
var adminChannel = data.PrivateFeedbackChannel;
var systemChannel = data.PublicFeedbackChannel;
if (_stackedPrivateFeedback.Length > 0
&& adminChannel is not null
&& (adminChannel.Id != Context.Message.Channel.Id || !hasReply)) {
_ = Utils.SilentSendAsync(adminChannel, _stackedPrivateFeedback.ToString());
_stackedPrivateFeedback.Clear();
}
if (_stackedPublicFeedback.Length > 0
&& systemChannel is not null
&& systemChannel.Id != adminChannel?.Id
&& (systemChannel.Id != Context.Message.Channel.Id || !hasReply)) {
_ = Utils.SilentSendAsync(systemChannel, _stackedPublicFeedback.ToString());
_stackedPublicFeedback.Clear();
}
}
public string? GetRemaining(string[] from, int startIndex, string? argument) {
if (startIndex >= from.Length && argument is not null)
Utils.SafeAppendToBuilder(
_stackedReplyMessage,
$"{ReplyEmojis.MissingArgument} {Utils.GetMessage($"Missing{argument}")}", Context.Message);
else return string.Join(" ", from, startIndex, from.Length - startIndex);
return null;
}
public (ulong Id, SocketUser? User)? GetUser(string[] args, string[] cleanArgs, int index) {
if (index >= args.Length) {
Utils.SafeAppendToBuilder(
_stackedReplyMessage, $"{ReplyEmojis.MissingArgument} {Messages.MissingUser}",
Context.Message);
return 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 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 (mention, Boyfriend.Client.GetUser(mention));
}
public bool HasPermission(GuildPermission permission) {
if (!Context.Guild.CurrentUser.GuildPermissions.Has(permission)) {
Utils.SafeAppendToBuilder(
_stackedReplyMessage,
$"{ReplyEmojis.NoPermission} {Utils.GetMessage($"BotCannot{permission}")}",
Context.Message);
return false;
}
if (!GetMember().GuildPermissions.Has(permission)
&& Context.Guild.OwnerId != Context.User.Id) {
Utils.SafeAppendToBuilder(
_stackedReplyMessage,
$"{ReplyEmojis.NoPermission} {Utils.GetMessage($"UserCannot{permission}")}",
Context.Message);
return false;
}
return true;
}
private SocketGuildUser GetMember() {
return GetMember(Context.User.Id)!;
}
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);
return null;
}
var member = Context.Guild.GetUser(Utils.ParseMention(args[index]));
if (member is null)
Utils.SafeAppendToBuilder(
_stackedReplyMessage,
$"{ReplyEmojis.InvalidArgument} {Messages.InvalidMember}",
Context.Message);
return member;
}
public ulong? GetBan(string[] args, int index) {
if (index >= args.Length) {
Utils.SafeAppendToBuilder(
_stackedReplyMessage, $"{ReplyEmojis.MissingArgument} {Messages.MissingUser}",
Context.Message);
return null;
}
var id = Utils.ParseMention(args[index]);
if (Context.Guild.GetBanAsync(id) is null) {
Utils.SafeAppendToBuilder(_stackedReplyMessage, Messages.UserNotBanned, Context.Message);
return null;
}
return id;
}
public int? GetNumberRange(string[] args, int index, int min, int max, string? argument) {
if (index >= args.Length) {
Utils.SafeAppendToBuilder(
_stackedReplyMessage,
$"{ReplyEmojis.MissingArgument} {string.Format(Messages.MissingNumber, min.ToString(), max.ToString())}",
Context.Message);
return null;
}
if (!int.TryParse(args[index], out var i)) {
Utils.SafeAppendToBuilder(
_stackedReplyMessage,
$"{ReplyEmojis.InvalidArgument} {string.Format(Utils.GetMessage($"{argument}Invalid"), min.ToString(), max.ToString(), Utils.Wrap(args[index]))}",
Context.Message);
return null;
}
if (argument is null) return i;
if (i < min) {
Utils.SafeAppendToBuilder(
_stackedReplyMessage,
$"{ReplyEmojis.InvalidArgument} {string.Format(Utils.GetMessage($"{argument}TooSmall"), min.ToString())}",
Context.Message);
return null;
}
if (i > max) {
Utils.SafeAppendToBuilder(
_stackedReplyMessage,
$"{ReplyEmojis.InvalidArgument} {string.Format(Utils.GetMessage($"{argument}TooLarge"), max.ToString())}",
Context.Message);
return null;
}
return i;
}
public static TimeSpan GetTimeSpan(string[] args, int index) {
if (index >= args.Length) return Infinity;
var chars = args[index].AsSpan();
var numberBuilder = Boyfriend.StringBuilder;
int days = 0, hours = 0, minutes = 0, seconds = 0;
foreach (var c in chars)
if (char.IsDigit(c)) { numberBuilder.Append(c); } else {
if (numberBuilder.Length is 0) return Infinity;
switch (c) {
case 'd' or 'D' or 'д' or 'Д':
days += int.Parse(numberBuilder.ToString());
numberBuilder.Clear();
break;
case 'h' or 'H' or 'ч' or 'Ч':
hours += int.Parse(numberBuilder.ToString());
numberBuilder.Clear();
break;
case 'm' or 'M' or 'м' or 'М':
minutes += int.Parse(numberBuilder.ToString());
numberBuilder.Clear();
break;
case 's' or 'S' or 'с' or 'С':
seconds += int.Parse(numberBuilder.ToString());
numberBuilder.Clear();
break;
default: return Infinity;
}
}
numberBuilder.Clear();
return new TimeSpan(days, hours, minutes, seconds);
}
public bool CanInteractWith(SocketGuildUser user, string action) {
if (Context.User.Id == user.Id) {
Utils.SafeAppendToBuilder(
_stackedReplyMessage,
$"{ReplyEmojis.CantInteract} {Utils.GetMessage($"UserCannot{action}Themselves")}", Context.Message);
return false;
}
if (Context.Guild.CurrentUser.Id == user.Id) {
Utils.SafeAppendToBuilder(
_stackedReplyMessage,
$"{ReplyEmojis.CantInteract} {Utils.GetMessage($"UserCannot{action}Bot")}", Context.Message);
return false;
}
if (Context.Guild.Owner.Id == user.Id) {
Utils.SafeAppendToBuilder(
_stackedReplyMessage,
$"{ReplyEmojis.CantInteract} {Utils.GetMessage($"UserCannot{action}Owner")}", Context.Message);
return false;
}
if (Context.Guild.CurrentUser.Hierarchy <= user.Hierarchy) {
Utils.SafeAppendToBuilder(
_stackedReplyMessage,
$"{ReplyEmojis.CantInteract} {Utils.GetMessage($"BotCannot{action}Target")}", Context.Message);
return false;
}
if (Context.Guild.Owner.Id != Context.User.Id && GetMember().Hierarchy <= user.Hierarchy) {
Utils.SafeAppendToBuilder(
_stackedReplyMessage,
$"{ReplyEmojis.CantInteract} {Utils.GetMessage($"UserCannot{action}Target")}", Context.Message);
return false;
}
return true;
}
}

View file

@ -1,48 +0,0 @@
using Boyfriend.Data;
using Discord;
using Discord.WebSocket;
namespace Boyfriend.Commands;
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);
if (toBan is null || !cmd.HasPermission(GuildPermission.BanMembers)) return;
var memberToBan = cmd.GetMember(toBan.Value.Id);
if (memberToBan is not null && !cmd.CanInteractWith(memberToBan, "Ban")) return;
var duration = CommandProcessor.GetTimeSpan(args, 1);
var reason = cmd.GetRemaining(args, duration.TotalSeconds < 1 ? 1 : 2, "BanReason");
if (reason is not null) await BanUserAsync(cmd, toBan.Value, duration, reason);
}
private static async Task BanUserAsync(
CommandProcessor cmd, (ulong Id, SocketUser? User) toBan, TimeSpan duration,
string reason) {
var author = cmd.Context.User;
var guild = cmd.Context.Guild;
if (toBan.User is not null)
await Utils.SendDirectMessage(
toBan.User,
string.Format(Messages.YouWereBanned, author.Mention, guild.Name, Utils.Wrap(reason)));
var guildBanMessage = $"({author}) {reason}";
await guild.AddBanAsync(toBan.Id, 0, guildBanMessage);
var memberData = GuildData.Get(guild).MemberData[toBan.Id];
memberData.BannedUntil
= duration.TotalSeconds < 1 ? DateTimeOffset.MaxValue : DateTimeOffset.UtcNow.Add(duration);
memberData.Roles.Clear();
cmd.ConfigWriteScheduled = true;
var feedback = string.Format(
Messages.FeedbackUserBanned, $"<@{toBan.Id.ToString()}>",
Utils.GetHumanizedTimeSpan(duration), Utils.Wrap(reason));
cmd.Reply(feedback, ReplyEmojis.Banned);
cmd.Audit(feedback);
}
}

View file

@ -1,30 +0,0 @@
using System.Diagnostics;
using Discord;
using Discord.WebSocket;
namespace Boyfriend.Commands;
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 UnreachableException();
if (!cmd.HasPermission(GuildPermission.ManageMessages)) return;
var toDelete = cmd.GetNumberRange(cleanArgs, 0, 1, 200, "ClearAmount");
if (toDelete is null) return;
var messages = await channel.GetMessagesAsync((int)(toDelete + 1)).FlattenAsync();
var user = (SocketGuildUser)cmd.Context.User;
var msgArray = messages.Reverse().ToArray();
await channel.DeleteMessagesAsync(msgArray, Utils.GetRequestOptions(user.ToString()!));
foreach (var msg in msgArray.Where(m => !m.Author.IsBot))
cmd.Audit(
string.Format(
Messages.CachedMessageCleared, msg.Author.Mention,
Utils.MentionChannel(channel.Id),
Utils.Wrap(msg.CleanContent)), false);
}
}

View file

@ -1,21 +0,0 @@
using Boyfriend.Data;
using Humanizer;
namespace Boyfriend.Commands;
public sealed class HelpCommand : ICommand {
public string[] Aliases { get; } = { "help", "помощь", "справка" };
public Task RunAsync(CommandProcessor cmd, string[] args, string[] cleanArgs) {
var prefix = GuildData.Get(cmd.Context.Guild).Preferences["Prefix"];
var toSend = Boyfriend.StringBuilder.Append(Messages.CommandHelp);
foreach (var command in CommandProcessor.Commands)
toSend.Append(
$"\n`{prefix}{command.Aliases[0]}`: {Utils.GetMessage($"CommandDescription{command.Aliases[0].Titleize()}")}");
cmd.Reply(toSend.ToString(), ReplyEmojis.Help);
toSend.Clear();
return Task.CompletedTask;
}
}

View file

@ -1,7 +0,0 @@
namespace Boyfriend.Commands;
public interface ICommand {
public string[] Aliases { get; }
public Task RunAsync(CommandProcessor cmd, string[] args, string[] cleanArgs);
}

View file

@ -1,34 +0,0 @@
using Boyfriend.Data;
using Discord;
using Discord.WebSocket;
namespace Boyfriend.Commands;
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, 0);
if (toKick is null || !cmd.HasPermission(GuildPermission.KickMembers)) return;
if (cmd.CanInteractWith(toKick, "Kick"))
await KickMemberAsync(cmd, toKick, cmd.GetRemaining(args, 1, "KickReason"));
}
private static async Task KickMemberAsync(CommandProcessor cmd, SocketGuildUser toKick, string? reason) {
if (reason is null) return;
var guildKickMessage = $"({cmd.Context.User}) {reason}";
await Utils.SendDirectMessage(toKick,
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);
cmd.Audit(format);
}
}

View file

@ -1,71 +0,0 @@
using Boyfriend.Data;
using Discord;
namespace Boyfriend.Commands;
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, 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 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
> DateTimeOffset.UtcNow)) {
cmd.Reply(Messages.MemberAlreadyMuted, ReplyEmojis.Error);
return;
}
if (cmd.HasPermission(GuildPermission.ModerateMembers) && cmd.CanInteractWith(toMute, "Mute"))
await MuteMemberAsync(cmd, toMute, duration, guildData, reason);
}
private static async Task MuteMemberAsync(
CommandProcessor cmd, IGuildUser toMute,
TimeSpan duration, GuildData data, string reason) {
var requestOptions = Utils.GetRequestOptions($"({cmd.Context.User}) {reason}");
var role = data.MuteRole;
var hasDuration = duration.TotalSeconds > 0;
var memberData = data.MemberData[toMute.Id];
if (role is not null) {
memberData.MutedUntil = DateTimeOffset.UtcNow.Add(duration);
if (data.Preferences["RemoveRolesOnMute"] is "true") {
memberData.Roles = toMute.RoleIds.ToList();
memberData.Roles.Remove(cmd.Context.Guild.Id);
await toMute.RemoveRolesAsync(memberData.Roles, requestOptions);
}
await toMute.AddRoleAsync(role, requestOptions);
} else {
if (!hasDuration || duration.TotalDays > 28) {
cmd.Reply(Messages.DurationRequiredForTimeOuts, ReplyEmojis.Error);
return;
}
if (toMute.IsBot) {
cmd.Reply(Messages.CannotTimeOutBot, ReplyEmojis.Error);
return;
}
await toMute.SetTimeOutAsync(duration, requestOptions);
}
cmd.ConfigWriteScheduled = true;
var feedback = string.Format(
Messages.FeedbackMemberMuted, toMute.Mention,
Utils.GetHumanizedTimeSpan(duration),
Utils.Wrap(reason));
cmd.Reply(feedback, ReplyEmojis.Muted);
cmd.Audit(feedback);
}
}

View file

@ -1,19 +0,0 @@
namespace Boyfriend.Commands;
public sealed class PingCommand : ICommand {
public string[] Aliases { get; } = { "ping", "latency", "pong", "пинг", "задержка", "понг" };
public Task RunAsync(CommandProcessor cmd, string[] args, string[] cleanArgs) {
var builder = Boyfriend.StringBuilder;
builder.Append(Utils.GetBeep())
.Append(
Math.Round(Math.Abs(DateTimeOffset.UtcNow.Subtract(cmd.Context.Message.Timestamp).TotalMilliseconds)))
.Append(Messages.Milliseconds);
cmd.Reply(builder.ToString(), ReplyEmojis.Ping);
builder.Clear();
return Task.CompletedTask;
}
}

View file

@ -1,34 +0,0 @@
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);
if (remindIn.TotalSeconds < 1) {
cmd.Reply(Messages.InvalidRemindIn, ReplyEmojis.InvalidArgument);
return Task.CompletedTask;
}
var reminderText = cmd.GetRemaining(cleanArgs, 1, "ReminderText");
if (reminderText is not null) {
var reminderOffset = DateTimeOffset.UtcNow.Add(remindIn);
GuildData.Get(cmd.Context.Guild).MemberData[cmd.Context.User.Id].Reminders.Add(
new Reminder {
RemindAt = reminderOffset,
ReminderText = reminderText,
ReminderChannel = cmd.Context.Channel.Id
});
cmd.ConfigWriteScheduled = true;
var feedback = string.Format(Messages.FeedbackReminderAdded, reminderOffset.ToUnixTimeSeconds().ToString());
cmd.Reply(feedback, ReplyEmojis.Reminder);
}
return Task.CompletedTask;
}
}

View file

@ -1,164 +0,0 @@
using Boyfriend.Data;
using Discord;
namespace Boyfriend.Commands;
public sealed class SettingsCommand : ICommand {
public string[] Aliases { get; } = { "settings", "config", "настройки", "конфиг" };
public Task RunAsync(CommandProcessor cmd, string[] args, string[] cleanArgs) {
if (!cmd.HasPermission(GuildPermission.ManageGuild)) return Task.CompletedTask;
var guild = cmd.Context.Guild;
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 GuildData.DefaultPreferences) {
var format = "{0}";
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}>";
else currentValue = Messages.ChannelNotSpecified;
} else if (setting.Key.EndsWith("Role")) {
if (guild.GetRole(ulong.Parse(currentValue)) is not null) format = "<@&{0}>";
else currentValue = Messages.RoleNotSpecified;
} else {
if (!IsBool(currentValue)) format = Utils.Wrap("{0}")!;
else currentValue = YesOrNo(currentValue is "true");
}
currentSettings.Append($"{Utils.GetMessage($"Settings{setting.Key}")} (`{setting.Key}`): ")
.AppendFormat(format, currentValue).AppendLine();
}
cmd.Reply(currentSettings.ToString(), ReplyEmojis.SettingsList);
currentSettings.Clear();
return Task.CompletedTask;
}
var selectedSetting = args[0].ToLower();
var exists = false;
foreach (var setting in GuildData.DefaultPreferences.Keys.Where(x => x.ToLower() == selectedSetting)) {
selectedSetting = setting;
exists = true;
break;
}
if (!exists) {
cmd.Reply(Messages.SettingDoesntExist, ReplyEmojis.Error);
return Task.CompletedTask;
}
string? value;
if (args.Length >= 2) {
value = cmd.GetRemaining(args, 1, "Setting");
if (value is null) return Task.CompletedTask;
if (selectedSetting is "EventStartedReceivers") {
value = value.Replace(" ", "").ToLower();
if (value.StartsWith(",")
|| value.Count(x => x is ',') > 1
|| (!value.Contains("interested") && !value.Contains("users") && !value.Contains("role"))) {
cmd.Reply(Messages.InvalidSettingValue, ReplyEmojis.Error);
return Task.CompletedTask;
}
}
} else { value = "reset"; }
if (IsBool(GuildData.DefaultPreferences[selectedSetting]) && !IsBool(value)) {
value = value switch {
"y" or "yes" or "д" or "да" => "true", "n" or "no" or "н" or "нет" => "false", _ => value
};
if (!IsBool(value)) {
cmd.Reply(Messages.InvalidSettingValue, ReplyEmojis.Error);
return Task.CompletedTask;
}
}
var localizedSelectedSetting = Utils.GetMessage($"Settings{selectedSetting}");
var mention = Utils.ParseMention(value);
if (mention is not 0 && selectedSetting is not "WelcomeMessage") value = mention.ToString();
var formatting = Utils.Wrap("{0}")!;
if (selectedSetting is not "WelcomeMessage") {
if (selectedSetting.EndsWith("Channel")) formatting = "<#{0}>";
if (selectedSetting.EndsWith("Role")) formatting = "<@&{0}>";
}
var formattedValue = selectedSetting switch {
"WelcomeMessage" => Utils.Wrap(Messages.DefaultWelcomeMessage),
"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] = GuildData.DefaultPreferences[selectedSetting];
} else {
if (value == config[selectedSetting]) {
cmd.Reply(
string.Format(Messages.SettingsNothingChanged, localizedSelectedSetting, formattedValue),
ReplyEmojis.Error);
return Task.CompletedTask;
}
if (selectedSetting is "Lang" && !Utils.CultureInfoCache.ContainsKey(value)) {
var langNotSupported = Boyfriend.StringBuilder.Append($"{Messages.LanguageNotSupported} ");
foreach (var lang in Utils.CultureInfoCache) langNotSupported.Append($"`{lang.Key}`, ");
langNotSupported.Remove(langNotSupported.Length - 2, 2);
cmd.Reply(langNotSupported.ToString(), ReplyEmojis.Error);
langNotSupported.Clear();
return Task.CompletedTask;
}
if (selectedSetting.EndsWith("Channel") && guild.GetTextChannel(mention) is null) {
cmd.Reply(Messages.InvalidChannel, ReplyEmojis.Error);
return Task.CompletedTask;
}
if (selectedSetting.EndsWith("Role") && guild.GetRole(mention) is null) {
cmd.Reply(Messages.InvalidRole, ReplyEmojis.Error);
return Task.CompletedTask;
}
if (selectedSetting.EndsWith("Offset") && !int.TryParse(value, out _)) {
cmd.Reply(Messages.InvalidSettingValue, ReplyEmojis.Error);
return Task.CompletedTask;
}
if (selectedSetting is "MuteRole")
data.MuteRole = guild.GetRole(mention);
config[selectedSetting] = value;
}
if (selectedSetting is "Lang") {
Utils.SetCurrentLanguage(guild);
localizedSelectedSetting = Utils.GetMessage($"Settings{selectedSetting}");
}
cmd.ConfigWriteScheduled = true;
var replyFormat = string.Format(Messages.FeedbackSettingsUpdated, localizedSelectedSetting, formattedValue);
cmd.Reply(replyFormat, ReplyEmojis.SettingsSet);
cmd.Audit(replyFormat, false);
return Task.CompletedTask;
}
private static string YesOrNo(bool isYes) {
return isYes ? Messages.Yes : Messages.No;
}
private static bool IsBool(string value) {
return value is "true" or "false";
}
}

View file

@ -1,25 +0,0 @@
using Discord;
namespace Boyfriend.Commands;
public sealed class UnbanCommand : ICommand {
public string[] Aliases { get; } = { "unban", "pardon", "разбан" };
public async Task RunAsync(CommandProcessor cmd, string[] args, string[] cleanArgs) {
if (!cmd.HasPermission(GuildPermission.BanMembers)) return;
var id = cmd.GetBan(args, 0);
if (id is null) return;
var reason = cmd.GetRemaining(args, 1, "UnbanReason");
if (reason is not null) await UnbanUserAsync(cmd, id.Value, 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);
var feedback = string.Format(Messages.FeedbackUserUnbanned, $"<@{id.ToString()}>", Utils.Wrap(reason));
cmd.Reply(feedback);
cmd.Audit(feedback);
}
}

View file

@ -1,36 +0,0 @@
using Boyfriend.Data;
using Discord;
using Discord.WebSocket;
namespace Boyfriend.Commands;
public sealed class UnmuteCommand : ICommand {
public string[] Aliases { get; } = { "unmute", "размут" };
public async Task RunAsync(CommandProcessor cmd, string[] args, string[] cleanArgs) {
if (!cmd.HasPermission(GuildPermission.ModerateMembers)) return;
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);
}
private static async Task UnmuteMemberAsync(CommandProcessor cmd, SocketGuildUser toUnmute,
string reason) {
var isMuted = await Utils.UnmuteMemberAsync(GuildData.Get(cmd.Context.Guild), cmd.Context.User.ToString(),
toUnmute, reason);
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);
}
}

View file

@ -1,142 +0,0 @@
using System.Collections.Concurrent;
using System.Diagnostics.CodeAnalysis;
using System.Text.Json;
using Discord;
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;
[SuppressMessage("Performance", "CA1853:Unnecessary call to \'Dictionary.ContainsKey(key)\'")]
// https://github.com/dotnet/roslyn-analyzers/issues/6377
private GuildData(SocketGuild guild) {
var downloaderTask = guild.DownloadUsersAsync();
_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);
}
downloaderTask.Wait();
foreach (var member in guild.Users) {
if (MemberData.TryGetValue(member.Id, out var memberData)) {
if (!memberData.IsInGuild
&& DateTimeOffset.UtcNow.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);
}
if (memberData.MutedUntil is null) {
memberData.Roles = ((IGuildUser)member).RoleIds.ToList();
memberData.Roles.Remove(guild.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
=> Boyfriend.Client.GetGuild(_id)
.GetTextChannel(
ulong.Parse(Preferences["PublicFeedbackChannel"]));
public SocketTextChannel? PrivateFeedbackChannel => Boyfriend.Client.GetGuild(_id)
.GetTextChannel(
ulong.Parse(
Preferences["PrivateFeedbackChannel"]));
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

@ -1,38 +0,0 @@
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

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

View file

@ -1,238 +0,0 @@
using System.Diagnostics;
using Boyfriend.Data;
using Discord;
using Discord.Rest;
using Discord.WebSocket;
namespace Boyfriend;
public static class EventHandler {
private static readonly DiscordSocketClient Client = Boyfriend.Client;
private static bool _sendReadyMessages = true;
public static void InitEvents() {
Client.Ready += ReadyEvent;
Client.MessageDeleted += MessageDeletedEvent;
Client.MessageReceived += MessageReceivedEvent;
Client.MessageUpdated += MessageUpdatedEvent;
Client.UserJoined += UserJoinedEvent;
Client.UserLeft += UserLeftEvent;
Client.GuildMemberUpdated += MemberRolesUpdatedEvent;
Client.GuildScheduledEventCreated += ScheduledEventCreatedEvent;
Client.GuildScheduledEventCancelled += ScheduledEventCancelledEvent;
Client.GuildScheduledEventStarted += ScheduledEventStartedEvent;
Client.GuildScheduledEventCompleted += ScheduledEventCompletedEvent;
}
private static Task MemberRolesUpdatedEvent(Cacheable<SocketGuildUser, ulong> oldUser, SocketGuildUser newUser) {
var data = GuildData.Get(newUser.Guild).MemberData[newUser.Id];
if (data.MutedUntil is null) {
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) {
Boyfriend.Log(new LogMessage(LogSeverity.Info, nameof(EventHandler), $"Guild \"{guild.Name}\" is READY"));
var data = GuildData.Get(guild);
var config = data.Preferences;
var channel = data.PrivateFeedbackChannel;
if (config["ReceiveStartupMessages"] is not "true" || channel is null) continue;
Utils.SetCurrentLanguage(guild);
_ = channel.SendMessageAsync(string.Format(Messages.Ready, Utils.GetBeep(i)));
}
_sendReadyMessages = false;
return Task.CompletedTask;
}
private static async Task MessageDeletedEvent(
Cacheable<IMessage, ulong> message,
Cacheable<IMessageChannel, ulong> channel) {
var msg = message.Value;
if (channel.Value is not SocketGuildChannel gChannel
|| msg is null or ISystemMessage
|| msg.Author.IsBot) return;
var guild = gChannel.Guild;
Utils.SetCurrentLanguage(guild);
var mention = msg.Author.Mention;
await Task.Delay(500);
var auditLogEnumerator
= (await guild.GetAuditLogsAsync(1, actionType: ActionType.MessageDeleted).FlattenAsync()).GetEnumerator();
if (auditLogEnumerator.MoveNext()) {
var auditLogEntry = auditLogEnumerator.Current!;
if (auditLogEntry.CreatedAt >= DateTimeOffset.UtcNow.Subtract(TimeSpan.FromSeconds(1))
&& auditLogEntry.Data is MessageDeleteAuditLogData data
&& msg.Author.Id == data.Target.Id)
mention = auditLogEntry.User.Mention;
}
auditLogEnumerator.Dispose();
await Utils.SendFeedbackAsync(
string.Format(
Messages.CachedMessageDeleted, msg.Author.Mention,
Utils.MentionChannel(channel.Id),
Utils.Wrap(msg.CleanContent)), guild, mention);
}
private static Task MessageReceivedEvent(IDeletable messageParam) {
if (messageParam is not SocketUserMessage message || message.Author.IsWebhook) return Task.CompletedTask;
_ = message.CleanContent.ToLower() switch {
"whoami" => message.ReplyAsync("`nobody`"),
"сука !!" => message.ReplyAsync("`root`"),
"воооо" => message.ReplyAsync("`removing /...`"),
"пон" => message.ReplyAsync(
"https://cdn.discordapp.com/attachments/837385840946053181/1087236080950055023/vUORS10xPaY-1.jpg"),
"++++" => message.ReplyAsync("#"),
_ => new CommandProcessor(message).HandleCommandAsync()
};
return Task.CompletedTask;
}
private static async Task MessageUpdatedEvent(
Cacheable<IMessage, ulong> messageCached, IMessage messageSocket,
ISocketMessageChannel channel) {
var msg = messageCached.Value;
if (channel is not SocketGuildChannel gChannel
|| msg is null or ISystemMessage
|| msg.CleanContent == messageSocket.CleanContent
|| msg.Author.IsBot) return;
var guild = gChannel.Guild;
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, msg.Author.Mention);
}
private static async Task UserJoinedEvent(SocketGuildUser user) {
var guild = user.Guild;
var data = GuildData.Get(guild);
var config = data.Preferences;
Utils.SetCurrentLanguage(guild);
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 (!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 (DateTimeOffset.UtcNow < memberData.MutedUntil) {
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.UtcNow);
return Task.CompletedTask;
}
private static async Task ScheduledEventCreatedEvent(SocketGuildEvent scheduledEvent) {
var guild = scheduledEvent.Guild;
var eventConfig = GuildData.Get(guild).Preferences;
var channel = Utils.GetEventNotificationChannel(guild);
Utils.SetCurrentLanguage(guild);
if (channel is null) return;
var role = guild.GetRole(ulong.Parse(eventConfig["EventNotificationRole"]));
var mentions = role is not null
? $"{role.Mention} {scheduledEvent.Creator.Mention}"
: $"{scheduledEvent.Creator.Mention}";
var location = Utils.Wrap(scheduledEvent.Location) ?? Utils.MentionChannel(scheduledEvent.Channel.Id);
var descAndLink
= $"\n{Utils.Wrap(scheduledEvent.Description)}\nhttps://discord.com/events/{guild.Id}/{scheduledEvent.Id}";
await Utils.SilentSendAsync(
channel,
string.Format(
Messages.EventCreated, mentions,
Utils.Wrap(scheduledEvent.Name), location,
scheduledEvent.StartTime.ToUnixTimeSeconds().ToString(), descAndLink),
true);
}
private static async Task ScheduledEventCancelledEvent(SocketGuildEvent scheduledEvent) {
var guild = scheduledEvent.Guild;
var eventConfig = GuildData.Get(guild).Preferences;
var channel = Utils.GetEventNotificationChannel(guild);
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}" : ""));
}
private static async Task ScheduledEventStartedEvent(SocketGuildEvent scheduledEvent) {
var guild = scheduledEvent.Guild;
var eventConfig = GuildData.Get(guild).Preferences;
var channel = Utils.GetEventNotificationChannel(guild);
Utils.SetCurrentLanguage(guild);
if (channel is null) return;
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))
.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),
Utils.Wrap(scheduledEvent.Location) ?? Utils.MentionChannel(scheduledEvent.Channel.Id)));
mentions.Clear();
}
private static async Task ScheduledEventCompletedEvent(SocketGuildEvent scheduledEvent) {
var guild = scheduledEvent.Guild;
var channel = Utils.GetEventNotificationChannel(guild);
Utils.SetCurrentLanguage(guild);
if (channel is not null)
await channel.SendMessageAsync(
string.Format(
Messages.EventCompleted, Utils.Wrap(scheduledEvent.Name),
Utils.GetHumanizedTimeSpan(DateTimeOffset.UtcNow.Subtract(scheduledEvent.StartTime))));
}
}

31
EventResponders.cs Normal file
View file

@ -0,0 +1,31 @@
using Remora.Discord.API.Abstractions.Gateway.Events;
using Remora.Discord.API.Abstractions.Rest;
using Remora.Discord.Gateway.Responders;
using Remora.Results;
namespace Boyfriend;
public class ReadyResponder : IResponder<IGuildCreate> {
private readonly IDiscordRestChannelAPI _channelApi;
public ReadyResponder(IDiscordRestChannelAPI channelApi) {
_channelApi = channelApi;
}
public async Task<Result> RespondAsync(IGuildCreate gatewayEvent, CancellationToken ct = default) {
if (!gatewayEvent.Guild.IsT0) return Result.FromSuccess(); // is IAvailableGuild
var guild = gatewayEvent.Guild.AsT0;
if (guild.GetConfigBool("SendReadyMessages").IsDefined(out var enabled)
&& enabled
&& guild.GetChannel("PrivateFeedbackChannel").IsDefined(out var channel)) {
Messages.Culture = guild.GetCulture();
var i = Random.Shared.Next(1, 4);
return (Result)await _channelApi.CreateMessageAsync(
channel.ID, string.Format(Messages.Ready, Boyfriend.GetLocalized($"Beep{i}")), ct: ct);
}
return Result.FromSuccess();
}
}

35
Extensions.cs Normal file
View file

@ -0,0 +1,35 @@
using System.Globalization;
using Microsoft.Extensions.Configuration;
using Remora.Discord.API.Abstractions.Gateway.Events;
using Remora.Discord.API.Abstractions.Objects;
using Remora.Results;
namespace Boyfriend;
public static class Extensions {
private static readonly Dictionary<string, CultureInfo> CultureInfoCache = new() {
{ "en", new CultureInfo("en-US") },
{ "ru", new CultureInfo("ru-RU") },
{ "mctaylors-ru", new CultureInfo("tt-RU") }
};
public static Result<bool> GetConfigBool(this IGuild guild, string key) {
var value = Boyfriend.GuildConfiguration.GetValue<bool?>($"GuildConfigs:{guild.ID}:{key}");
return value is not null ? Result<bool>.FromSuccess(value.Value) : Result<bool>.FromError(new NotFoundError());
}
public static Result<IChannel> GetChannel(this IGuildCreate.IAvailableGuild guild, string key) {
var value = Boyfriend.GuildConfiguration.GetValue<ulong?>($"GuildConfigs:{guild.ID}:{key}");
if (value is null) return Result<IChannel>.FromError(new NotFoundError());
var match = guild.Channels.SingleOrDefault(channel => channel!.ID.Equals(value.Value), null);
return match is not null
? Result<IChannel>.FromSuccess(match)
: Result<IChannel>.FromError(new NotFoundError());
}
public static CultureInfo GetCulture(this IGuild guild) {
var value = Boyfriend.GuildConfiguration.GetValue<string?>($"GuildConfigs:{guild.ID}:Language");
return value is not null ? CultureInfoCache[value] : CultureInfoCache["en"];
}
}

2
Messages.Designer.cs generated
View file

@ -21,7 +21,7 @@ namespace Boyfriend {
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
internal class Messages { public class Messages {
private static global::System.Resources.ResourceManager resourceMan; private static global::System.Resources.ResourceManager resourceMan;

View file

@ -1,19 +0,0 @@
namespace Boyfriend;
public static class ReplyEmojis {
public const string Success = ":white_check_mark:";
public const string Error = ":x:";
public const string MissingArgument = ":keyboard:";
public const string InvalidArgument = ":construction:";
public const string NoPermission = ":no_entry_sign:";
public const string CantInteract = ":vertical_traffic_light:";
public const string Help = ":page_facing_up:";
public const string SettingsList = ":gear:";
public const string SettingsSet = ":control_knobs:";
public const string Ping = ":signal_strength:";
public const string Banned = ":hammer:";
public const string Kicked = ":police_car:";
public const string Muted = ":mute:";
public const string Unmuted = ":loud_sound:";
public const string Reminder = ":alarm_clock:";
}

168
Utils.cs
View file

@ -1,168 +0,0 @@
using System.Diagnostics;
using System.Globalization;
using System.Reflection;
using System.Text;
using System.Text.RegularExpressions;
using Boyfriend.Data;
using Discord;
using Discord.Net;
using Discord.WebSocket;
using Humanizer;
using Humanizer.Localisation;
namespace Boyfriend;
public static partial class Utils {
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<string, string> ReflectionMessageCache = new();
private static readonly AllowedMentions AllowRoles = new() {
AllowedTypes = AllowedMentionTypes.Roles
};
public static string GetBeep(int i = -1) {
return GetMessage($"Beep{(i < 0 ? Random.Shared.Next(3) + 1 : ++i)}");
}
public static string? Wrap(string? original, bool limitedSpace = false) {
if (original is null) return null;
var maxChars = limitedSpace ? 970 : 1940;
if (original.Length > maxChars) original = original[..maxChars];
var style = original.Contains('\n') ? "```" : "`";
return $"{style}{original}{(original.Equals("") ? " " : "")}{style}";
}
public static string MentionChannel(ulong id) {
return $"<#{id}>";
}
public static ulong ParseMention(string mention) {
return ulong.TryParse(NumbersOnlyRegex().Replace(mention, ""), out var id) ? id : 0;
}
public static async Task SendDirectMessage(SocketUser user, string toSend) {
try { await user.SendMessageAsync(toSend); } catch (HttpException e) {
if (e.DiscordCode is not DiscordErrorCode.CannotSendMessageToUser) throw;
}
}
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 UnreachableException($"Message length is out of range: {text.Length}");
await channel.SendMessageAsync(text, false, null, null, allowRoles ? AllowRoles : AllowedMentions.None);
} catch (Exception e) {
await Boyfriend.Log(
new LogMessage(
LogSeverity.Error, nameof(Utils),
"Exception while silently sending message", e));
}
}
public static RequestOptions GetRequestOptions(string reason) {
var options = RequestOptions.Default;
options.AuditLogReason = reason;
return options;
}
public static string GetMessage(string name) {
var propertyName = name;
name = $"{Messages.Culture}/{name}";
if (ReflectionMessageCache.TryGetValue(name, out var cachedMessage)) return cachedMessage;
var toReturn =
typeof(Messages).GetProperty(propertyName, BindingFlags.NonPublic | BindingFlags.Static)?.GetValue(null)
?.ToString();
if (toReturn is null) {
Console.Error.WriteLine($@"Could not find localized property: {propertyName}");
return name;
}
ReflectionMessageCache.Add(name, toReturn);
return toReturn;
}
public static async Task
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 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(SocketGuild guild) {
Messages.Culture = CultureInfoCache[GuildData.Get(guild).Preferences["Lang"]];
}
public static void SafeAppendToBuilder(StringBuilder appendTo, string appendWhat, SocketTextChannel? channel) {
if (channel is null) return;
if (appendTo.Length + appendWhat.Length > 2000) {
_ = SilentSendAsync(channel, appendTo.ToString());
appendTo.Clear();
}
appendTo.AppendLine(appendWhat);
}
public static void SafeAppendToBuilder(StringBuilder appendTo, string appendWhat, SocketUserMessage message) {
if (appendTo.Length + appendWhat.Length > 2000) {
_ = message.ReplyAsync(appendTo.ToString(), false, null, AllowedMentions.None);
appendTo.Clear();
}
appendTo.AppendLine(appendWhat);
}
public static SocketTextChannel? GetEventNotificationChannel(SocketGuild guild) {
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.UtcNow) return false;
await toUnmute.RemoveTimeOutAsync(requestOptions);
}
return true;
}
[GeneratedRegex("[^0-9]")]
private static partial Regex NumbersOnlyRegex();
}