mirror of
https://github.com/PluralKit/PluralKit.git
synced 2026-02-04 13:06:50 +00:00
Large refactor and project restructuring
This commit is contained in:
parent
c10e197c39
commit
6d5004bf54
71 changed files with 1664 additions and 1607 deletions
|
|
@ -1,33 +1,23 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using App.Metrics;
|
||||
|
||||
using Autofac;
|
||||
using Autofac.Core;
|
||||
|
||||
using Dapper;
|
||||
using Discord;
|
||||
using Discord.WebSocket;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
using PluralKit.Bot.Commands;
|
||||
using PluralKit.Bot.CommandSystem;
|
||||
using PluralKit.Core;
|
||||
|
||||
using Sentry;
|
||||
using Sentry.Infrastructure;
|
||||
|
||||
using Serilog;
|
||||
using Serilog.Events;
|
||||
|
||||
using SystemClock = NodaTime.SystemClock;
|
||||
|
||||
namespace PluralKit.Bot
|
||||
{
|
||||
class Initialize
|
||||
|
|
@ -109,11 +99,9 @@ namespace PluralKit.Bot
|
|||
private IMetrics _metrics;
|
||||
private PeriodicStatCollector _collector;
|
||||
private ILogger _logger;
|
||||
private PKPerformanceEventListener _pl;
|
||||
|
||||
public Bot(ILifetimeScope services, IDiscordClient client, IMetrics metrics, PeriodicStatCollector collector, ILogger logger)
|
||||
{
|
||||
_pl = new PKPerformanceEventListener();
|
||||
_services = services;
|
||||
_client = client as DiscordShardedClient;
|
||||
_metrics = metrics;
|
||||
|
|
@ -306,7 +294,7 @@ namespace PluralKit.Bot
|
|||
// Check if message starts with the command prefix
|
||||
if (msg.Content.StartsWith("pk;", StringComparison.InvariantCultureIgnoreCase)) argPos = 3;
|
||||
else if (msg.Content.StartsWith("pk!", StringComparison.InvariantCultureIgnoreCase)) argPos = 3;
|
||||
else if (msg.Content != null && Utils.HasMentionPrefix(msg.Content, ref argPos, out var id)) // Set argPos to the proper value
|
||||
else if (msg.Content != null && StringUtils.HasMentionPrefix(msg.Content, ref argPos, out var id)) // Set argPos to the proper value
|
||||
if (id != _client.CurrentUser.Id) // But undo it if it's someone else's ping
|
||||
argPos = -1;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
using App.Metrics;
|
||||
using App.Metrics.Gauge;
|
||||
using App.Metrics.Histogram;
|
||||
using App.Metrics.Meter;
|
||||
using App.Metrics.Timer;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
namespace PluralKit.Bot.CommandSystem
|
||||
namespace PluralKit.Bot
|
||||
{
|
||||
public class Command
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
using System.Collections.Generic;
|
||||
|
||||
namespace PluralKit.Bot.CommandSystem
|
||||
namespace PluralKit.Bot
|
||||
{
|
||||
public class CommandGroup
|
||||
{
|
||||
|
|
|
|||
|
|
@ -4,14 +4,13 @@ using System.Threading.Tasks;
|
|||
using App.Metrics;
|
||||
|
||||
using Autofac;
|
||||
using Autofac.Core;
|
||||
|
||||
using Discord;
|
||||
using Discord.WebSocket;
|
||||
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using PluralKit.Core;
|
||||
|
||||
namespace PluralKit.Bot.CommandSystem
|
||||
namespace PluralKit.Bot
|
||||
{
|
||||
public class Context
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace PluralKit.Bot.CommandSystem
|
||||
namespace PluralKit.Bot
|
||||
{
|
||||
public class Parameters
|
||||
{
|
||||
|
|
|
|||
|
|
@ -4,9 +4,9 @@ using System.Threading.Tasks;
|
|||
|
||||
using Discord;
|
||||
|
||||
using PluralKit.Bot.CommandSystem;
|
||||
using PluralKit.Core;
|
||||
|
||||
namespace PluralKit.Bot.Commands
|
||||
namespace PluralKit.Bot
|
||||
{
|
||||
public class Autoproxy
|
||||
{
|
||||
|
|
|
|||
|
|
@ -3,9 +3,9 @@ using System.Threading.Tasks;
|
|||
|
||||
using Discord.WebSocket;
|
||||
|
||||
using PluralKit.Bot.CommandSystem;
|
||||
using PluralKit.Core;
|
||||
|
||||
namespace PluralKit.Bot.Commands
|
||||
namespace PluralKit.Bot
|
||||
{
|
||||
public class CommandTree
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
using System.Threading.Tasks;
|
||||
|
||||
using PluralKit.Bot.CommandSystem;
|
||||
|
||||
namespace PluralKit.Bot.Commands
|
||||
namespace PluralKit.Bot
|
||||
{
|
||||
public class Fun
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
using System.Threading.Tasks;
|
||||
|
||||
using Discord;
|
||||
|
||||
using PluralKit.Bot.CommandSystem;
|
||||
using PluralKit.Core;
|
||||
|
||||
namespace PluralKit.Bot.Commands
|
||||
namespace PluralKit.Bot
|
||||
{
|
||||
public class Help
|
||||
{
|
||||
|
|
|
|||
|
|
@ -4,13 +4,15 @@ using System.Linq;
|
|||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Discord;
|
||||
using Discord.Net;
|
||||
|
||||
using Newtonsoft.Json;
|
||||
|
||||
using PluralKit.Bot.CommandSystem;
|
||||
using PluralKit.Core;
|
||||
|
||||
namespace PluralKit.Bot.Commands
|
||||
namespace PluralKit.Bot
|
||||
{
|
||||
public class ImportExport
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,10 +1,9 @@
|
|||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using PluralKit.Bot.CommandSystem;
|
||||
using PluralKit.Core;
|
||||
|
||||
namespace PluralKit.Bot.Commands
|
||||
namespace PluralKit.Bot
|
||||
{
|
||||
public class Member
|
||||
{
|
||||
|
|
|
|||
|
|
@ -3,9 +3,9 @@ using System.Threading.Tasks;
|
|||
|
||||
using Discord;
|
||||
|
||||
using PluralKit.Bot.CommandSystem;
|
||||
using PluralKit.Core;
|
||||
|
||||
namespace PluralKit.Bot.Commands
|
||||
namespace PluralKit.Bot
|
||||
{
|
||||
public class MemberAvatar
|
||||
{
|
||||
|
|
@ -62,7 +62,7 @@ namespace PluralKit.Bot.Commands
|
|||
}
|
||||
else if (ctx.RemainderOrNull() is string url)
|
||||
{
|
||||
await Utils.VerifyAvatarOrThrow(url);
|
||||
await AvatarUtils.VerifyAvatarOrThrow(url);
|
||||
target.AvatarUrl = url;
|
||||
await _data.SaveMember(target);
|
||||
|
||||
|
|
@ -71,7 +71,7 @@ namespace PluralKit.Bot.Commands
|
|||
}
|
||||
else if (ctx.Message.Attachments.FirstOrDefault() is Attachment attachment)
|
||||
{
|
||||
await Utils.VerifyAvatarOrThrow(attachment.Url);
|
||||
await AvatarUtils.VerifyAvatarOrThrow(attachment.Url);
|
||||
target.AvatarUrl = attachment.Url;
|
||||
await _data.SaveMember(target);
|
||||
|
||||
|
|
|
|||
|
|
@ -3,10 +3,9 @@ using System.Threading.Tasks;
|
|||
|
||||
using NodaTime;
|
||||
|
||||
using PluralKit.Bot.CommandSystem;
|
||||
using PluralKit.Core;
|
||||
|
||||
namespace PluralKit.Bot.Commands
|
||||
namespace PluralKit.Bot
|
||||
{
|
||||
public class MemberEdit
|
||||
{
|
||||
|
|
@ -103,7 +102,7 @@ namespace PluralKit.Bot.Commands
|
|||
var birthday = ctx.RemainderOrNull();
|
||||
if (birthday != null)
|
||||
{
|
||||
date = PluralKit.Utils.ParseDate(birthday, true);
|
||||
date = DateUtils.ParseDate(birthday, true);
|
||||
if (date == null) throw Errors.BirthdayParseError(birthday);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,9 @@
|
|||
using System;
|
||||
using System.Linq;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using PluralKit.Bot.CommandSystem;
|
||||
using PluralKit.Core;
|
||||
|
||||
namespace PluralKit.Bot.Commands
|
||||
namespace PluralKit.Bot
|
||||
{
|
||||
public class MemberProxy
|
||||
{
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ using System.Diagnostics;
|
|||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using App.Metrics;
|
||||
|
||||
using Discord;
|
||||
|
|
@ -11,10 +12,9 @@ using Humanizer;
|
|||
|
||||
using NodaTime;
|
||||
|
||||
using PluralKit.Bot.CommandSystem;
|
||||
using PluralKit.Core;
|
||||
|
||||
namespace PluralKit.Bot.Commands {
|
||||
namespace PluralKit.Bot {
|
||||
public class Misc
|
||||
{
|
||||
private BotConfig _botConfig;
|
||||
|
|
@ -79,7 +79,7 @@ namespace PluralKit.Bot.Commands {
|
|||
.AddField("Messages proxied", $"{messagesProxied.OneMinuteRate * 60:F1}/m ({messagesProxied.FifteenMinuteRate * 60:F1}/m over 15m)", true)
|
||||
.AddField("Commands executed", $"{commandsRun.OneMinuteRate * 60:F1}/m ({commandsRun.FifteenMinuteRate * 60:F1}/m over 15m)", true)
|
||||
.AddField("Current shard", $"Shard #{shardId} (of {shardTotal} total, {shardUpTotal} are up)", true)
|
||||
.AddField("Shard uptime", $"{Formats.DurationFormat.Format(shardUptime)} ({shardInfo.DisconnectionCount} disconnections)", true)
|
||||
.AddField("Shard uptime", $"{DateTimeFormats.DurationFormat.Format(shardUptime)} ({shardInfo.DisconnectionCount} disconnections)", true)
|
||||
.AddField("CPU usage", $"{_cpu.LastCpuMeasure:P1}", true)
|
||||
.AddField("Memory usage", $"{memoryUsage / 1024 / 1024} MiB", true)
|
||||
.AddField("Latency", $"API: {(msg.Timestamp - ctx.Message.Timestamp).TotalMilliseconds:F0} ms, shard: {shardInfo.ShardLatency} ms", true)
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Discord;
|
||||
|
||||
using PluralKit.Bot.CommandSystem;
|
||||
using PluralKit.Core;
|
||||
|
||||
namespace PluralKit.Bot.Commands
|
||||
namespace PluralKit.Bot
|
||||
{
|
||||
public class ServerConfig
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,14 +1,15 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Discord;
|
||||
|
||||
using NodaTime;
|
||||
using NodaTime.TimeZones;
|
||||
|
||||
using PluralKit.Bot.CommandSystem;
|
||||
using PluralKit.Core;
|
||||
|
||||
namespace PluralKit.Bot.Commands
|
||||
namespace PluralKit.Bot
|
||||
{
|
||||
public class Switch
|
||||
{
|
||||
|
|
@ -79,7 +80,7 @@ namespace PluralKit.Bot.Commands
|
|||
var timeToMove = ctx.RemainderOrNull() ?? throw new PKSyntaxError("Must pass a date or time to move the switch to.");
|
||||
var tz = TzdbDateTimeZoneSource.Default.ForId(ctx.System.UiTz ?? "UTC");
|
||||
|
||||
var result = PluralKit.Utils.ParseDateTime(timeToMove, true, tz);
|
||||
var result = DateUtils.ParseDateTime(timeToMove, true, tz);
|
||||
if (result == null) throw Errors.InvalidDateTime(timeToMove);
|
||||
|
||||
var time = result.Value;
|
||||
|
|
@ -102,10 +103,10 @@ namespace PluralKit.Bot.Commands
|
|||
// But, we do a prompt to confirm.
|
||||
var lastSwitchMembers = _data.GetSwitchMembers(lastTwoSwitches[0]);
|
||||
var lastSwitchMemberStr = string.Join(", ", await lastSwitchMembers.Select(m => m.Name).ToListAsync());
|
||||
var lastSwitchTimeStr = Formats.ZonedDateTimeFormat.Format(lastTwoSwitches[0].Timestamp.InZone(ctx.System.Zone));
|
||||
var lastSwitchDeltaStr = Formats.DurationFormat.Format(SystemClock.Instance.GetCurrentInstant() - lastTwoSwitches[0].Timestamp);
|
||||
var newSwitchTimeStr = Formats.ZonedDateTimeFormat.Format(time);
|
||||
var newSwitchDeltaStr = Formats.DurationFormat.Format(SystemClock.Instance.GetCurrentInstant() - time.ToInstant());
|
||||
var lastSwitchTimeStr = DateTimeFormats.ZonedDateTimeFormat.Format(lastTwoSwitches[0].Timestamp.InZone(ctx.System.Zone));
|
||||
var lastSwitchDeltaStr = DateTimeFormats.DurationFormat.Format(SystemClock.Instance.GetCurrentInstant() - lastTwoSwitches[0].Timestamp);
|
||||
var newSwitchTimeStr = DateTimeFormats.ZonedDateTimeFormat.Format(time);
|
||||
var newSwitchDeltaStr = DateTimeFormats.DurationFormat.Format(SystemClock.Instance.GetCurrentInstant() - time.ToInstant());
|
||||
|
||||
// yeet
|
||||
var msg = await ctx.Reply($"{Emojis.Warn} This will move the latest switch ({lastSwitchMemberStr.SanitizeMentions()}) from {lastSwitchTimeStr} ({lastSwitchDeltaStr} ago) to {newSwitchTimeStr} ({newSwitchDeltaStr} ago). Is this OK?");
|
||||
|
|
@ -137,7 +138,7 @@ namespace PluralKit.Bot.Commands
|
|||
|
||||
var lastSwitchMembers = _data.GetSwitchMembers(lastTwoSwitches[0]);
|
||||
var lastSwitchMemberStr = string.Join(", ", await lastSwitchMembers.Select(m => m.Name).ToListAsync());
|
||||
var lastSwitchDeltaStr = Formats.DurationFormat.Format(SystemClock.Instance.GetCurrentInstant() - lastTwoSwitches[0].Timestamp);
|
||||
var lastSwitchDeltaStr = DateTimeFormats.DurationFormat.Format(SystemClock.Instance.GetCurrentInstant() - lastTwoSwitches[0].Timestamp);
|
||||
|
||||
IUserMessage msg;
|
||||
if (lastTwoSwitches.Count == 1)
|
||||
|
|
@ -149,7 +150,7 @@ namespace PluralKit.Bot.Commands
|
|||
{
|
||||
var secondSwitchMembers = _data.GetSwitchMembers(lastTwoSwitches[1]);
|
||||
var secondSwitchMemberStr = string.Join(", ", await secondSwitchMembers.Select(m => m.Name).ToListAsync());
|
||||
var secondSwitchDeltaStr = Formats.DurationFormat.Format(SystemClock.Instance.GetCurrentInstant() - lastTwoSwitches[1].Timestamp);
|
||||
var secondSwitchDeltaStr = DateTimeFormats.DurationFormat.Format(SystemClock.Instance.GetCurrentInstant() - lastTwoSwitches[1].Timestamp);
|
||||
msg = await ctx.Reply(
|
||||
$"{Emojis.Warn} This will delete the latest switch ({lastSwitchMemberStr.SanitizeMentions()}, {lastSwitchDeltaStr} ago). The next latest switch is {secondSwitchMemberStr.SanitizeMentions()} ({secondSwitchDeltaStr} ago). Is this okay?");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,16 +1,8 @@
|
|||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Discord;
|
||||
using Humanizer;
|
||||
using NodaTime;
|
||||
using NodaTime.Text;
|
||||
using NodaTime.TimeZones;
|
||||
|
||||
using PluralKit.Bot.CommandSystem;
|
||||
using PluralKit.Core;
|
||||
|
||||
namespace PluralKit.Bot.Commands
|
||||
namespace PluralKit.Bot
|
||||
{
|
||||
public class System
|
||||
{
|
||||
|
|
|
|||
|
|
@ -8,10 +8,9 @@ using NodaTime;
|
|||
using NodaTime.Text;
|
||||
using NodaTime.TimeZones;
|
||||
|
||||
using PluralKit.Bot.CommandSystem;
|
||||
using PluralKit.Core;
|
||||
|
||||
namespace PluralKit.Bot.Commands
|
||||
namespace PluralKit.Bot
|
||||
{
|
||||
public class SystemEdit
|
||||
{
|
||||
|
|
@ -103,7 +102,7 @@ namespace PluralKit.Bot.Commands
|
|||
{
|
||||
// They can't both be null - otherwise we would've hit the conditional at the very top
|
||||
string url = ctx.RemainderOrNull() ?? ctx.Message.Attachments.FirstOrDefault()?.ProxyUrl;
|
||||
await ctx.BusyIndicator(() => Utils.VerifyAvatarOrThrow(url));
|
||||
await ctx.BusyIndicator(() => AvatarUtils.VerifyAvatarOrThrow(url));
|
||||
|
||||
ctx.System.AvatarUrl = url;
|
||||
await _data.SaveSystem(ctx.System);
|
||||
|
|
@ -162,7 +161,7 @@ namespace PluralKit.Bot.Commands
|
|||
|
||||
var currentTime = SystemClock.Instance.GetCurrentInstant().InZone(zone);
|
||||
var msg = await ctx.Reply(
|
||||
$"This will change the system time zone to {zone.Id}. The current time is {Formats.ZonedDateTimeFormat.Format(currentTime)}. Is this correct?");
|
||||
$"This will change the system time zone to {zone.Id}. The current time is {DateTimeFormats.ZonedDateTimeFormat.Format(currentTime)}. Is this correct?");
|
||||
if (!await ctx.PromptYesNo(msg)) throw Errors.TimezoneChangeCancelled;
|
||||
ctx.System.UiTz = zone.Id;
|
||||
await _data.SaveSystem(ctx.System);
|
||||
|
|
@ -246,7 +245,7 @@ namespace PluralKit.Bot.Commands
|
|||
|
||||
public async Task<DateTimeZone> FindTimeZone(Context ctx, string zoneStr) {
|
||||
// First, if we're given a flag emoji, we extract the flag emoji code from it.
|
||||
zoneStr = PluralKit.Utils.ExtractCountryFlag(zoneStr) ?? zoneStr;
|
||||
zoneStr = Core.StringUtils.ExtractCountryFlag(zoneStr) ?? zoneStr;
|
||||
|
||||
// Then, we find all *locations* matching either the given country code or the country name.
|
||||
var locations = TzdbDateTimeZoneSource.Default.Zone1970Locations;
|
||||
|
|
|
|||
|
|
@ -5,9 +5,9 @@ using Discord;
|
|||
|
||||
using NodaTime;
|
||||
|
||||
using PluralKit.Bot.CommandSystem;
|
||||
using PluralKit.Core;
|
||||
|
||||
namespace PluralKit.Bot.Commands
|
||||
namespace PluralKit.Bot
|
||||
{
|
||||
public class SystemFront
|
||||
{
|
||||
|
|
@ -81,12 +81,12 @@ namespace PluralKit.Bot.Commands
|
|||
// Calculate the time between the last switch (that we iterated - ie. the next one on the timeline) and the current one
|
||||
var switchDuration = lastSw.Value - sw.Timestamp;
|
||||
stringToAdd =
|
||||
$"**{membersStr}** ({Formats.ZonedDateTimeFormat.Format(sw.Timestamp.InZone(system.Zone))}, {Formats.DurationFormat.Format(switchSince)} ago, for {Formats.DurationFormat.Format(switchDuration)})\n";
|
||||
$"**{membersStr}** ({DateTimeFormats.ZonedDateTimeFormat.Format(sw.Timestamp.InZone(system.Zone))}, {DateTimeFormats.DurationFormat.Format(switchSince)} ago, for {DateTimeFormats.DurationFormat.Format(switchDuration)})\n";
|
||||
}
|
||||
else
|
||||
{
|
||||
stringToAdd =
|
||||
$"**{membersStr}** ({Formats.ZonedDateTimeFormat.Format(sw.Timestamp.InZone(system.Zone))}, {Formats.DurationFormat.Format(switchSince)} ago)\n";
|
||||
$"**{membersStr}** ({DateTimeFormats.ZonedDateTimeFormat.Format(sw.Timestamp.InZone(system.Zone))}, {DateTimeFormats.DurationFormat.Format(switchSince)} ago)\n";
|
||||
}
|
||||
|
||||
if (outputStr.Length + stringToAdd.Length > EmbedBuilder.MaxDescriptionLength) break;
|
||||
|
|
@ -107,7 +107,7 @@ namespace PluralKit.Bot.Commands
|
|||
|
||||
var now = SystemClock.Instance.GetCurrentInstant();
|
||||
|
||||
var rangeStart = PluralKit.Utils.ParseDateTime(durationStr, true, system.Zone);
|
||||
var rangeStart = DateUtils.ParseDateTime(durationStr, true, system.Zone);
|
||||
if (rangeStart == null) throw Errors.InvalidDateTime(durationStr);
|
||||
if (rangeStart.Value.ToInstant() > now) throw Errors.FrontPercentTimeInFuture;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using PluralKit.Bot.CommandSystem;
|
||||
using PluralKit.Core;
|
||||
|
||||
namespace PluralKit.Bot.Commands
|
||||
namespace PluralKit.Bot
|
||||
{
|
||||
public class SystemLink
|
||||
{
|
||||
|
|
|
|||
|
|
@ -4,9 +4,9 @@ using System.Threading.Tasks;
|
|||
|
||||
using Humanizer;
|
||||
|
||||
using PluralKit.Bot.CommandSystem;
|
||||
using PluralKit.Core;
|
||||
|
||||
namespace PluralKit.Bot.Commands
|
||||
namespace PluralKit.Bot
|
||||
{
|
||||
public class SystemList
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
using System.Threading.Tasks;
|
||||
|
||||
using Discord;
|
||||
|
||||
using PluralKit.Bot.CommandSystem;
|
||||
using PluralKit.Core;
|
||||
|
||||
namespace PluralKit.Bot.Commands
|
||||
namespace PluralKit.Bot
|
||||
{
|
||||
public class Token
|
||||
{
|
||||
|
|
@ -33,7 +34,7 @@ namespace PluralKit.Bot.Commands
|
|||
|
||||
private async Task<string> MakeAndSetNewToken(PKSystem system)
|
||||
{
|
||||
system.Token = PluralKit.Utils.GenerateToken();
|
||||
system.Token = Core.StringUtils.GenerateToken();
|
||||
await _data.SaveSystem(system);
|
||||
return system.Token;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,27 @@ using NodaTime;
|
|||
using PluralKit.Core;
|
||||
|
||||
namespace PluralKit.Bot {
|
||||
/// <summary>
|
||||
/// An exception class representing user-facing errors caused when parsing and executing commands.
|
||||
/// </summary>
|
||||
public class PKError : Exception
|
||||
{
|
||||
public PKError(string message) : base(message)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A subclass of <see cref="PKError"/> that represent command syntax errors, meaning they'll have their command
|
||||
/// usages printed in the message.
|
||||
/// </summary>
|
||||
public class PKSyntaxError : PKError
|
||||
{
|
||||
public PKSyntaxError(string message) : base(message)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public static class Errors {
|
||||
// TODO: is returning constructed errors and throwing them at call site a good idea, or should these be methods that insta-throw instead?
|
||||
// or should we just like... go back to inlining them? at least for the one-time-use commands
|
||||
|
|
@ -60,7 +81,7 @@ namespace PluralKit.Bot {
|
|||
public static PKError SwitchTimeInFuture => new PKError("Can't move switch to a time in the future.");
|
||||
public static PKError NoRegisteredSwitches => new PKError("There are no registered switches for this system.");
|
||||
|
||||
public static PKError SwitchMoveBeforeSecondLast(ZonedDateTime time) => new PKError($"Can't move switch to before last switch time ({Formats.ZonedDateTimeFormat.Format(time)}), as it would cause conflicts.");
|
||||
public static PKError SwitchMoveBeforeSecondLast(ZonedDateTime time) => new PKError($"Can't move switch to before last switch time ({DateTimeFormats.ZonedDateTimeFormat.Format(time)}), as it would cause conflicts.");
|
||||
public static PKError SwitchMoveCancelled => new PKError("Switch move cancelled.");
|
||||
public static PKError SwitchDeleteCancelled => new PKError("Switch deletion cancelled.");
|
||||
public static PKError TimezoneParseError(string timezone) => new PKError($"Could not parse timezone offset {timezone.SanitizeMentions()}. Offset must be a value like 'UTC+5' or 'GMT-4:30'.");
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ using Discord;
|
|||
using Discord.Rest;
|
||||
using Discord.WebSocket;
|
||||
|
||||
using PluralKit.Bot.Commands;
|
||||
using PluralKit.Core;
|
||||
|
||||
using Sentry;
|
||||
|
||||
|
|
@ -44,7 +44,7 @@ namespace PluralKit.Bot
|
|||
builder.RegisterType<Misc>().AsSelf();
|
||||
builder.RegisterType<ServerConfig>().AsSelf();
|
||||
builder.RegisterType<Switch>().AsSelf();
|
||||
builder.RegisterType<Commands.System>().AsSelf();
|
||||
builder.RegisterType<System>().AsSelf();
|
||||
builder.RegisterType<SystemEdit>().AsSelf();
|
||||
builder.RegisterType<SystemFront>().AsSelf();
|
||||
builder.RegisterType<SystemLink>().AsSelf();
|
||||
|
|
|
|||
|
|
@ -1,20 +0,0 @@
|
|||
using System;
|
||||
using System.Diagnostics.Tracing;
|
||||
using System.Linq;
|
||||
|
||||
namespace PluralKit.Bot {
|
||||
class PKPerformanceEventListener: EventListener
|
||||
{
|
||||
public PKPerformanceEventListener()
|
||||
{
|
||||
foreach (var s in EventSource.GetSources())
|
||||
EnableEvents(s, EventLevel.Informational);
|
||||
}
|
||||
|
||||
protected override void OnEventWritten(EventWrittenEventArgs eventData)
|
||||
{
|
||||
base.OnEventWritten(eventData);
|
||||
// Console.WriteLine($"{eventData.EventSource.Name}/{eventData.EventName}: {string.Join(", ", eventData.PayloadNames.Zip(eventData.Payload).Select(v => $"{v.First}={v.Second}" ))}");
|
||||
}
|
||||
}
|
||||
}
|
||||
5
PluralKit.Bot/PluralKit.Bot.csproj.DotSettings
Normal file
5
PluralKit.Bot/PluralKit.Bot.csproj.DotSettings
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
|
||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=commands/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=commandsystem/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=services/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=utils/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
|
||||
|
|
@ -8,6 +8,8 @@ using Discord.WebSocket;
|
|||
using Humanizer;
|
||||
using NodaTime;
|
||||
|
||||
using PluralKit.Core;
|
||||
|
||||
namespace PluralKit.Bot {
|
||||
public class EmbedService
|
||||
{
|
||||
|
|
@ -31,7 +33,7 @@ namespace PluralKit.Bot {
|
|||
.WithColor(Color.Blue)
|
||||
.WithTitle(system.Name ?? null)
|
||||
.WithThumbnailUrl(system.AvatarUrl ?? null)
|
||||
.WithFooter($"System ID: {system.Hid} | Created on {Formats.ZonedDateTimeFormat.Format(system.Created.InZone(system.Zone))}");
|
||||
.WithFooter($"System ID: {system.Hid} | Created on {DateTimeFormats.ZonedDateTimeFormat.Format(system.Created.InZone(system.Zone))}");
|
||||
|
||||
var latestSwitch = await _data.GetLatestSwitch(system);
|
||||
if (latestSwitch != null && system.FrontPrivacy.CanAccess(ctx))
|
||||
|
|
@ -100,7 +102,7 @@ namespace PluralKit.Bot {
|
|||
// TODO: add URL of website when that's up
|
||||
.WithAuthor(name, member.AvatarUrl)
|
||||
.WithColor(member.MemberPrivacy.CanAccess(ctx) ? color : Color.Default)
|
||||
.WithFooter($"System ID: {system.Hid} | Member ID: {member.Hid} | Created on {Formats.ZonedDateTimeFormat.Format(member.Created.InZone(system.Zone))}");
|
||||
.WithFooter($"System ID: {system.Hid} | Member ID: {member.Hid} | Created on {DateTimeFormats.ZonedDateTimeFormat.Format(member.Created.InZone(system.Zone))}");
|
||||
|
||||
if (member.MemberPrivacy == PrivacyLevel.Private) eb.WithDescription("*(this member is private)*");
|
||||
|
||||
|
|
@ -125,7 +127,7 @@ namespace PluralKit.Bot {
|
|||
return new EmbedBuilder()
|
||||
.WithColor(members.FirstOrDefault()?.Color?.ToDiscordColor() ?? Color.Blue)
|
||||
.AddField($"Current {"fronter".ToQuantity(members.Count, ShowQuantityAs.None)}", members.Count > 0 ? string.Join(", ", members.Select(m => m.Name)) : "*(no fronter)*")
|
||||
.AddField("Since", $"{Formats.ZonedDateTimeFormat.Format(sw.Timestamp.InZone(zone))} ({Formats.DurationFormat.Format(timeSinceSwitch)} ago)")
|
||||
.AddField("Since", $"{DateTimeFormats.ZonedDateTimeFormat.Format(sw.Timestamp.InZone(zone))} ({DateTimeFormats.DurationFormat.Format(timeSinceSwitch)} ago)")
|
||||
.Build();
|
||||
}
|
||||
|
||||
|
|
@ -179,7 +181,7 @@ namespace PluralKit.Bot {
|
|||
var actualPeriod = breakdown.RangeEnd - breakdown.RangeStart;
|
||||
var eb = new EmbedBuilder()
|
||||
.WithColor(Color.Blue)
|
||||
.WithFooter($"Since {Formats.ZonedDateTimeFormat.Format(breakdown.RangeStart.InZone(tz))} ({Formats.DurationFormat.Format(actualPeriod)} ago)");
|
||||
.WithFooter($"Since {DateTimeFormats.ZonedDateTimeFormat.Format(breakdown.RangeStart.InZone(tz))} ({DateTimeFormats.DurationFormat.Format(actualPeriod)} ago)");
|
||||
|
||||
var maxEntriesToDisplay = 24; // max 25 fields allowed in embed - reserve 1 for "others"
|
||||
|
||||
|
|
@ -193,13 +195,13 @@ namespace PluralKit.Bot {
|
|||
foreach (var pair in membersOrdered)
|
||||
{
|
||||
var frac = pair.Value / actualPeriod;
|
||||
eb.AddField(pair.Key?.Name ?? "*(no fronter)*", $"{frac*100:F0}% ({Formats.DurationFormat.Format(pair.Value)})");
|
||||
eb.AddField(pair.Key?.Name ?? "*(no fronter)*", $"{frac*100:F0}% ({DateTimeFormats.DurationFormat.Format(pair.Value)})");
|
||||
}
|
||||
|
||||
if (membersOrdered.Count > maxEntriesToDisplay)
|
||||
{
|
||||
eb.AddField("(others)",
|
||||
Formats.DurationFormat.Format(membersOrdered.Skip(maxEntriesToDisplay)
|
||||
DateTimeFormats.DurationFormat.Format(membersOrdered.Skip(maxEntriesToDisplay)
|
||||
.Aggregate(Duration.Zero, (prod, next) => prod + next.Value)), true);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
using System.Threading.Tasks;
|
||||
using Dapper;
|
||||
|
||||
using Discord;
|
||||
|
||||
using PluralKit.Core;
|
||||
|
||||
using Serilog;
|
||||
|
||||
namespace PluralKit.Bot {
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using App.Metrics;
|
||||
using Discord;
|
||||
using Discord.WebSocket;
|
||||
using NodaTime.Extensions;
|
||||
using PluralKit.Core;
|
||||
|
||||
using Serilog;
|
||||
|
||||
namespace PluralKit.Bot
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ namespace PluralKit.Bot
|
|||
// eg. @Ske [text] => [@Ske text]
|
||||
int matchStartPosition = 0;
|
||||
string leadingMention = null;
|
||||
if (Utils.HasMentionPrefix(message, ref matchStartPosition, out _))
|
||||
if (StringUtils.HasMentionPrefix(message, ref matchStartPosition, out _))
|
||||
{
|
||||
leadingMention = message.Substring(0, matchStartPosition);
|
||||
message = message.Substring(matchStartPosition);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
|
|
|
|||
|
|
@ -1,165 +0,0 @@
|
|||
using System;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Net.Sockets;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using Discord;
|
||||
using Discord.Net;
|
||||
|
||||
using PluralKit.Core;
|
||||
using Image = SixLabors.ImageSharp.Image;
|
||||
|
||||
namespace PluralKit.Bot
|
||||
{
|
||||
public static class Utils {
|
||||
public static string NameAndMention(this IUser user) {
|
||||
return $"{user.Username}#{user.Discriminator} ({user.Mention})";
|
||||
}
|
||||
|
||||
public static Color? ToDiscordColor(this string color)
|
||||
{
|
||||
if (uint.TryParse(color, NumberStyles.HexNumber, null, out var colorInt))
|
||||
return new Color(colorInt);
|
||||
throw new ArgumentException($"Invalid color string '{color}'.");
|
||||
}
|
||||
|
||||
public static async Task VerifyAvatarOrThrow(string url)
|
||||
{
|
||||
// List of MIME types we consider acceptable
|
||||
var acceptableMimeTypes = new[]
|
||||
{
|
||||
"image/jpeg",
|
||||
"image/gif",
|
||||
"image/png"
|
||||
// TODO: add image/webp once ImageSharp supports this
|
||||
};
|
||||
|
||||
using (var client = new HttpClient())
|
||||
{
|
||||
Uri uri;
|
||||
try
|
||||
{
|
||||
uri = new Uri(url);
|
||||
if (!uri.IsAbsoluteUri) throw Errors.InvalidUrl(url);
|
||||
}
|
||||
catch (UriFormatException)
|
||||
{
|
||||
throw Errors.InvalidUrl(url);
|
||||
}
|
||||
|
||||
var response = await client.GetAsync(uri);
|
||||
if (!response.IsSuccessStatusCode) // Check status code
|
||||
throw Errors.AvatarServerError(response.StatusCode);
|
||||
if (response.Content.Headers.ContentLength == null) // Check presence of content length
|
||||
throw Errors.AvatarNotAnImage(null);
|
||||
if (response.Content.Headers.ContentLength > Limits.AvatarFileSizeLimit) // Check content length
|
||||
throw Errors.AvatarFileSizeLimit(response.Content.Headers.ContentLength.Value);
|
||||
if (!acceptableMimeTypes.Contains(response.Content.Headers.ContentType.MediaType)) // Check MIME type
|
||||
throw Errors.AvatarNotAnImage(response.Content.Headers.ContentType.MediaType);
|
||||
|
||||
// Parse the image header in a worker
|
||||
var stream = await response.Content.ReadAsStreamAsync();
|
||||
var image = await Task.Run(() => Image.Identify(stream));
|
||||
if (image == null) throw Errors.AvatarInvalid;
|
||||
if (image.Width > Limits.AvatarDimensionLimit || image.Height > Limits.AvatarDimensionLimit) // Check image size
|
||||
throw Errors.AvatarDimensionsTooLarge(image.Width, image.Height);
|
||||
}
|
||||
}
|
||||
|
||||
public static bool HasMentionPrefix(string content, ref int argPos, out ulong mentionId)
|
||||
{
|
||||
mentionId = 0;
|
||||
|
||||
// Roughly ported from Discord.Commands.MessageExtensions.HasMentionPrefix
|
||||
if (string.IsNullOrEmpty(content) || content.Length <= 3 || (content[0] != '<' || content[1] != '@'))
|
||||
return false;
|
||||
int num = content.IndexOf('>');
|
||||
if (num == -1 || content.Length < num + 2 || content[num + 1] != ' ' || !MentionUtils.TryParseUser(content.Substring(0, num + 1), out mentionId))
|
||||
return false;
|
||||
argPos = num + 2;
|
||||
return true;
|
||||
}
|
||||
|
||||
public static bool TryParseMention(this string potentialMention, out ulong id)
|
||||
{
|
||||
if (ulong.TryParse(potentialMention, out id)) return true;
|
||||
if (MentionUtils.TryParseUser(potentialMention, out id)) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
public static string SanitizeMentions(this string input) =>
|
||||
Regex.Replace(Regex.Replace(input, "<@[!&]?(\\d{17,19})>", "<\u200B@$1>"), "@(everyone|here)", "@\u200B$1");
|
||||
|
||||
public static string SanitizeEveryone(this string input) =>
|
||||
Regex.Replace(input, "@(everyone|here)", "@\u200B$1");
|
||||
|
||||
public static string EscapeMarkdown(this string input)
|
||||
{
|
||||
Regex pattern = new Regex(@"[*_~>`(||)\\]", RegexOptions.Multiline);
|
||||
if (input != null) return pattern.Replace(input, @"\$&");
|
||||
else return input;
|
||||
}
|
||||
|
||||
public static string ProxyTagsString(this PKMember member) => string.Join(", ", member.ProxyTags.Select(t => $"`{t.ProxyString.EscapeMarkdown()}`"));
|
||||
|
||||
public static async Task<ChannelPermissions> PermissionsIn(this IChannel channel)
|
||||
{
|
||||
switch (channel)
|
||||
{
|
||||
case IDMChannel _:
|
||||
return ChannelPermissions.DM;
|
||||
case IGroupChannel _:
|
||||
return ChannelPermissions.Group;
|
||||
case IGuildChannel gc:
|
||||
var currentUser = await gc.Guild.GetCurrentUserAsync();
|
||||
return currentUser.GetPermissions(gc);
|
||||
default:
|
||||
return ChannelPermissions.None;
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task<bool> HasPermission(this IChannel channel, ChannelPermission permission) =>
|
||||
(await PermissionsIn(channel)).Has(permission);
|
||||
|
||||
public static bool IsOurProblem(this Exception e)
|
||||
{
|
||||
// This function filters out sporadic errors out of our control from being reported to Sentry
|
||||
// otherwise we'd blow out our error reporting budget as soon as Discord takes a dump, or something.
|
||||
|
||||
// Discord server errors are *not our problem*
|
||||
if (e is HttpException he && ((int) he.HttpCode) >= 500) return false;
|
||||
|
||||
// Socket errors are *not our problem*
|
||||
if (e is SocketException) return false;
|
||||
|
||||
// Tasks being cancelled for whatver reason are, you guessed it, also not our problem.
|
||||
if (e is TaskCanceledException) return false;
|
||||
|
||||
// This may expanded at some point.
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// An exception class representing user-facing errors caused when parsing and executing commands.
|
||||
/// </summary>
|
||||
public class PKError : Exception
|
||||
{
|
||||
public PKError(string message) : base(message)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A subclass of <see cref="PKError"/> that represent command syntax errors, meaning they'll have their command
|
||||
/// usages printed in the message.
|
||||
/// </summary>
|
||||
public class PKSyntaxError : PKError
|
||||
{
|
||||
public PKSyntaxError(string message) : base(message)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
55
PluralKit.Bot/Utils/AvatarUtils.cs
Normal file
55
PluralKit.Bot/Utils/AvatarUtils.cs
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
using System;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using PluralKit.Core;
|
||||
|
||||
using SixLabors.ImageSharp;
|
||||
|
||||
namespace PluralKit.Bot {
|
||||
public static class AvatarUtils {
|
||||
public static async Task VerifyAvatarOrThrow(string url)
|
||||
{
|
||||
// List of MIME types we consider acceptable
|
||||
var acceptableMimeTypes = new[]
|
||||
{
|
||||
"image/jpeg",
|
||||
"image/gif",
|
||||
"image/png"
|
||||
// TODO: add image/webp once ImageSharp supports this
|
||||
};
|
||||
|
||||
using (var client = new HttpClient())
|
||||
{
|
||||
Uri uri;
|
||||
try
|
||||
{
|
||||
uri = new Uri(url);
|
||||
if (!uri.IsAbsoluteUri) throw Errors.InvalidUrl(url);
|
||||
}
|
||||
catch (UriFormatException)
|
||||
{
|
||||
throw Errors.InvalidUrl(url);
|
||||
}
|
||||
|
||||
var response = await client.GetAsync(uri);
|
||||
if (!response.IsSuccessStatusCode) // Check status code
|
||||
throw Errors.AvatarServerError(response.StatusCode);
|
||||
if (response.Content.Headers.ContentLength == null) // Check presence of content length
|
||||
throw Errors.AvatarNotAnImage(null);
|
||||
if (response.Content.Headers.ContentLength > Limits.AvatarFileSizeLimit) // Check content length
|
||||
throw Errors.AvatarFileSizeLimit(response.Content.Headers.ContentLength.Value);
|
||||
if (!acceptableMimeTypes.Contains(response.Content.Headers.ContentType.MediaType)) // Check MIME type
|
||||
throw Errors.AvatarNotAnImage(response.Content.Headers.ContentType.MediaType);
|
||||
|
||||
// Parse the image header in a worker
|
||||
var stream = await response.Content.ReadAsStreamAsync();
|
||||
var image = await Task.Run(() => Image.Identify(stream));
|
||||
if (image == null) throw Errors.AvatarInvalid;
|
||||
if (image.Width > Limits.AvatarDimensionLimit || image.Height > Limits.AvatarDimensionLimit) // Check image size
|
||||
throw Errors.AvatarDimensionsTooLarge(image.Width, image.Height);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -7,7 +7,7 @@ using Discord;
|
|||
using Discord.Net;
|
||||
using Discord.WebSocket;
|
||||
|
||||
using PluralKit.Bot.CommandSystem;
|
||||
using PluralKit.Core;
|
||||
|
||||
namespace PluralKit.Bot {
|
||||
public static class ContextUtils {
|
||||
32
PluralKit.Bot/Utils/DiscordUtils.cs
Normal file
32
PluralKit.Bot/Utils/DiscordUtils.cs
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
using System.Threading.Tasks;
|
||||
|
||||
using Discord;
|
||||
|
||||
namespace PluralKit.Bot
|
||||
{
|
||||
public static class DiscordUtils
|
||||
{
|
||||
public static string NameAndMention(this IUser user) {
|
||||
return $"{user.Username}#{user.Discriminator} ({user.Mention})";
|
||||
}
|
||||
|
||||
public static async Task<ChannelPermissions> PermissionsIn(this IChannel channel)
|
||||
{
|
||||
switch (channel)
|
||||
{
|
||||
case IDMChannel _:
|
||||
return ChannelPermissions.DM;
|
||||
case IGroupChannel _:
|
||||
return ChannelPermissions.Group;
|
||||
case IGuildChannel gc:
|
||||
var currentUser = await gc.Guild.GetCurrentUserAsync();
|
||||
return currentUser.GetPermissions(gc);
|
||||
default:
|
||||
return ChannelPermissions.None;
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task<bool> HasPermission(this IChannel channel, ChannelPermission permission) =>
|
||||
(await PermissionsIn(channel)).Has(permission);
|
||||
}
|
||||
}
|
||||
33
PluralKit.Bot/Utils/MiscUtils.cs
Normal file
33
PluralKit.Bot/Utils/MiscUtils.cs
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
using System;
|
||||
using System.Linq;
|
||||
using System.Net.Sockets;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Discord.Net;
|
||||
|
||||
using PluralKit.Core;
|
||||
|
||||
namespace PluralKit.Bot
|
||||
{
|
||||
public static class MiscUtils {
|
||||
public static string ProxyTagsString(this PKMember member) => string.Join(", ", member.ProxyTags.Select(t => $"`{t.ProxyString.EscapeMarkdown()}`"));
|
||||
|
||||
public static bool IsOurProblem(this Exception e)
|
||||
{
|
||||
// This function filters out sporadic errors out of our control from being reported to Sentry
|
||||
// otherwise we'd blow out our error reporting budget as soon as Discord takes a dump, or something.
|
||||
|
||||
// Discord server errors are *not our problem*
|
||||
if (e is HttpException he && ((int) he.HttpCode) >= 500) return false;
|
||||
|
||||
// Socket errors are *not our problem*
|
||||
if (e is SocketException) return false;
|
||||
|
||||
// Tasks being cancelled for whatver reason are, you guessed it, also not our problem.
|
||||
if (e is TaskCanceledException) return false;
|
||||
|
||||
// This may expanded at some point.
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
52
PluralKit.Bot/Utils/StringUtils.cs
Normal file
52
PluralKit.Bot/Utils/StringUtils.cs
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
using System;
|
||||
using System.Globalization;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
using Discord;
|
||||
|
||||
namespace PluralKit.Bot
|
||||
{
|
||||
public static class StringUtils
|
||||
{
|
||||
public static Color? ToDiscordColor(this string color)
|
||||
{
|
||||
if (uint.TryParse(color, NumberStyles.HexNumber, null, out var colorInt))
|
||||
return new Color(colorInt);
|
||||
throw new ArgumentException($"Invalid color string '{color}'.");
|
||||
}
|
||||
|
||||
public static bool HasMentionPrefix(string content, ref int argPos, out ulong mentionId)
|
||||
{
|
||||
mentionId = 0;
|
||||
|
||||
// Roughly ported from Discord.Commands.MessageExtensions.HasMentionPrefix
|
||||
if (string.IsNullOrEmpty(content) || content.Length <= 3 || (content[0] != '<' || content[1] != '@'))
|
||||
return false;
|
||||
int num = content.IndexOf('>');
|
||||
if (num == -1 || content.Length < num + 2 || content[num + 1] != ' ' || !MentionUtils.TryParseUser(content.Substring(0, num + 1), out mentionId))
|
||||
return false;
|
||||
argPos = num + 2;
|
||||
return true;
|
||||
}
|
||||
|
||||
public static bool TryParseMention(this string potentialMention, out ulong id)
|
||||
{
|
||||
if (ulong.TryParse(potentialMention, out id)) return true;
|
||||
if (MentionUtils.TryParseUser(potentialMention, out id)) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
public static string SanitizeMentions(this string input) =>
|
||||
Regex.Replace(Regex.Replace(input, "<@[!&]?(\\d{17,19})>", "<\u200B@$1>"), "@(everyone|here)", "@\u200B$1");
|
||||
|
||||
public static string SanitizeEveryone(this string input) =>
|
||||
Regex.Replace(input, "@(everyone|here)", "@\u200B$1");
|
||||
|
||||
public static string EscapeMarkdown(this string input)
|
||||
{
|
||||
Regex pattern = new Regex(@"[*_~>`(||)\\]", RegexOptions.Multiline);
|
||||
if (input != null) return pattern.Replace(input, @"\$&");
|
||||
else return input;
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue