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:
parent
02949656bf
commit
201a1ce079
24 changed files with 141 additions and 1588 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -1,6 +1,5 @@
|
|||
.idea/
|
||||
*.user
|
||||
token.txt
|
||||
bin/
|
||||
obj/
|
||||
/packages/
|
||||
|
|
220
Boyfriend.cs
220
Boyfriend.cs
|
@ -1,180 +1,78 @@
|
|||
using System.Text;
|
||||
using System.Timers;
|
||||
using Boyfriend.Data;
|
||||
using Discord;
|
||||
using Discord.Rest;
|
||||
using Discord.WebSocket;
|
||||
using Timer = System.Timers.Timer;
|
||||
using System.Reflection;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Remora.Discord.API.Abstractions.Objects;
|
||||
using Remora.Discord.Caching.Extensions;
|
||||
using Remora.Discord.Caching.Services;
|
||||
using Remora.Discord.Gateway.Extensions;
|
||||
using Remora.Discord.Hosting.Extensions;
|
||||
|
||||
namespace Boyfriend;
|
||||
|
||||
public static class Boyfriend {
|
||||
public static readonly StringBuilder StringBuilder = new();
|
||||
public class Boyfriend {
|
||||
public static ILogger<Boyfriend> Logger = null!;
|
||||
public static IConfiguration GuildConfiguration = null!;
|
||||
|
||||
private static readonly DiscordSocketConfig Config = new() {
|
||||
MessageCacheSize = 250,
|
||||
GatewayIntents
|
||||
= (GatewayIntents.AllUnprivileged | GatewayIntents.MessageContent | GatewayIntents.GuildMembers)
|
||||
& ~GatewayIntents.GuildInvites,
|
||||
AlwaysDownloadUsers = true,
|
||||
AlwaysResolveStickers = false,
|
||||
AlwaysDownloadDefaultStickers = false,
|
||||
LargeThreshold = 500
|
||||
};
|
||||
private static readonly Dictionary<string, string> ReflectionMessageCache = new();
|
||||
|
||||
private static DateTimeOffset _nextSongAt = DateTimeOffset.MinValue;
|
||||
private static uint _nextSongIndex;
|
||||
public static async Task Main(string[] args) {
|
||||
var host = CreateHostBuilder(args).UseConsoleLifetime().Build();
|
||||
|
||||
private static readonly (Game Song, TimeSpan Duration)[] ActivityList = {
|
||||
(new Game("Masayoshi Minoshima (ft. nomico) - Bad Apple!!", ActivityType.Listening), new TimeSpan(0, 3, 40)),
|
||||
(new Game("Xi - Blue Zenith", ActivityType.Listening), new TimeSpan(0, 4, 16)),
|
||||
(new Game("UNDEAD CORPORATION - Everything will freeze", ActivityType.Listening), new TimeSpan(0, 3, 18)),
|
||||
(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))
|
||||
};
|
||||
var services = host.Services;
|
||||
Logger = services.GetRequiredService<ILogger<Boyfriend>>();
|
||||
GuildConfiguration = services.GetRequiredService<IConfigurationBuilder>().AddJsonFile("guild_configs.json")
|
||||
.Build();
|
||||
|
||||
public static readonly DiscordSocketClient Client = new(Config);
|
||||
|
||||
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);
|
||||
await host.RunAsync();
|
||||
}
|
||||
|
||||
private static async void TickAllGuildsAsync(object? sender, ElapsedEventArgs e) {
|
||||
if (GuildTickTasks.Count is not 0) return;
|
||||
private static IHostBuilder CreateHostBuilder(string[] args) {
|
||||
return Host.CreateDefaultBuilder(args)
|
||||
.AddDiscordService(
|
||||
services => {
|
||||
var configuration = services.GetRequiredService<IConfiguration>();
|
||||
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
foreach (var guild in Client.Guilds) GuildTickTasks.Add(TickGuildAsync(guild, now));
|
||||
return configuration.GetValue<string?>("BOT_TOKEN")
|
||||
?? 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) {
|
||||
var nextSong = ActivityList[_nextSongIndex];
|
||||
await Client.SetActivityAsync(nextSong.Song);
|
||||
_nextSongAt = now.Add(nextSong.Duration);
|
||||
_nextSongIndex++;
|
||||
if (_nextSongIndex >= ActivityList.Length) _nextSongIndex = 0;
|
||||
}
|
||||
services.AddDiscordCaching();
|
||||
services.Configure<CacheSettings>(
|
||||
settings => { settings.SetAbsoluteExpiration<IMessage>(TimeSpan.FromDays(7)); });
|
||||
|
||||
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();
|
||||
services.AddSingleton<IConfigurationBuilder, ConfigurationBuilder>();
|
||||
}
|
||||
).ConfigureLogging(
|
||||
c => c.AddConsole()
|
||||
.AddFilter("System.Net.Http.HttpClient.*.LogicalHandler", LogLevel.Warning)
|
||||
.AddFilter("System.Net.Http.HttpClient.*.ClientHandler", LogLevel.Warning)
|
||||
);
|
||||
}
|
||||
|
||||
public static Task Log(LogMessage msg) {
|
||||
switch (msg.Severity) {
|
||||
case LogSeverity.Critical:
|
||||
Console.ForegroundColor = ConsoleColor.DarkRed;
|
||||
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;
|
||||
public static string GetLocalized(string key) {
|
||||
var propertyName = key;
|
||||
key = $"{Messages.Culture}/{key}";
|
||||
if (ReflectionMessageCache.TryGetValue(key, out var cached)) return cached;
|
||||
|
||||
case LogSeverity.Verbose:
|
||||
case LogSeverity.Debug:
|
||||
default: return Task.CompletedTask;
|
||||
var toReturn =
|
||||
typeof(Messages).GetProperty(propertyName, BindingFlags.NonPublic | BindingFlags.Static)?.GetValue(null)
|
||||
?.ToString();
|
||||
if (toReturn is null) {
|
||||
Logger.LogError("Could not find localized property: {Name}", propertyName);
|
||||
return key;
|
||||
}
|
||||
|
||||
Console.ResetColor();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
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);
|
||||
ReflectionMessageCache.Add(key, toReturn);
|
||||
return toReturn;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,8 +19,20 @@
|
|||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Discord.Net" Version="3.10.0"/>
|
||||
<PackageReference Include="Humanizer.Core" Version="2.14.1" />
|
||||
<PackageReference Include="Humanizer.Core.ru" Version="2.14.1" />
|
||||
<PackageReference Include="Humanizer.Core.ru" Version="2.14.1"/>
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0-preview.3.23174.8"/>
|
||||
<PackageReference Include="Remora.Discord" Version="2023.3.0"/>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Remove="old\**"/>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Remove="old\**"/>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Remove="old\**"/>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
namespace Boyfriend.Commands;
|
||||
|
||||
public interface ICommand {
|
||||
public string[] Aliases { get; }
|
||||
|
||||
public Task RunAsync(CommandProcessor cmd, string[] args, string[] cleanArgs);
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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";
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
}
|
|
@ -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>();
|
||||
}
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
namespace Boyfriend.Data;
|
||||
|
||||
public struct Reminder {
|
||||
public DateTimeOffset RemindAt;
|
||||
public string ReminderText;
|
||||
public ulong ReminderChannel;
|
||||
}
|
238
EventHandler.cs
238
EventHandler.cs
|
@ -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
31
EventResponders.cs
Normal 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
35
Extensions.cs
Normal 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
2
Messages.Designer.cs
generated
|
@ -21,7 +21,7 @@ namespace Boyfriend {
|
|||
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")]
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
|
||||
internal class Messages {
|
||||
public class Messages {
|
||||
|
||||
private static global::System.Resources.ResourceManager resourceMan;
|
||||
|
||||
|
|
|
@ -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
168
Utils.cs
|
@ -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();
|
||||
}
|
Loading…
Add table
Reference in a new issue