run dotnet format

This commit is contained in:
spiral 2021-08-27 11:03:47 -04:00
parent 05989242f9
commit ac2671452d
No known key found for this signature in database
GPG key ID: A6059F0CA0E1BD31
278 changed files with 1913 additions and 1808 deletions

View file

@ -30,7 +30,7 @@ namespace PluralKit.Bot
public class Bot
{
private readonly ConcurrentDictionary<ulong, GuildMemberPartial> _guildMembers = new();
private readonly Cluster _cluster;
private readonly DiscordApiClient _rest;
private readonly ILogger _logger;
@ -44,7 +44,7 @@ namespace PluralKit.Bot
private bool _hasReceivedReady = false;
private Timer _periodicTask; // Never read, just kept here for GC reasons
public Bot(ILifetimeScope services, ILogger logger, PeriodicStatCollector collector, IMetrics metrics,
public Bot(ILifetimeScope services, ILogger logger, PeriodicStatCollector collector, IMetrics metrics,
ErrorMessageService errorMessageService, CommandMessageService commandMessageService, Cluster cluster, DiscordApiClient rest, IDiscordCache cache)
{
_logger = logger.ForContext<Bot>();
@ -61,7 +61,7 @@ namespace PluralKit.Bot
public void Init()
{
_cluster.EventReceived += OnEventReceived;
// Init the shard stuff
_services.Resolve<ShardInfoService>().Init();
@ -72,7 +72,7 @@ namespace PluralKit.Bot
var timeTillNextWholeMinute = TimeSpan.FromMilliseconds(60000 - timeNow.ToUnixTimeMilliseconds() % 60000 + 250);
_periodicTask = new Timer(_ =>
{
var __ = UpdatePeriodic();
var __ = UpdatePeriodic();
}, null, timeTillNextWholeMinute, TimeSpan.FromMinutes(1));
}
@ -88,7 +88,7 @@ namespace PluralKit.Bot
return PermissionSet.Dm;
}
private async Task OnEventReceived(Shard shard, IGatewayEvent evt)
{
await _cache.HandleGatewayEvent(evt);
@ -158,7 +158,7 @@ namespace PluralKit.Bot
{
new ActivityPartial
{
Name = "Restarting... (please wait)",
Name = "Restarting... (please wait)",
Type = ActivityType.Game
}
},
@ -167,7 +167,7 @@ namespace PluralKit.Bot
}
}
private Task HandleEvent<T>(Shard shard, T evt) where T: IGatewayEvent
private Task HandleEvent<T>(Shard shard, T evt) where T : IGatewayEvent
{
// We don't want to stall the event pipeline, so we'll "fork" inside here
var _ = HandleEventInner();
@ -203,13 +203,13 @@ namespace PluralKit.Bot
var sentryEnricher = serviceScope.ResolveOptional<ISentryEnricher<T>>();
sentryEnricher?.Enrich(serviceScope.Resolve<Scope>(), shard, evt);
using var timer = _metrics.Measure.Timer.Time(BotMetrics.EventsHandled,
using var timer = _metrics.Measure.Timer.Time(BotMetrics.EventsHandled,
new MetricTags("event", typeof(T).Name.Replace("Event", "")));
// Delegate to the queue to see if it wants to handle this event
// the TryHandle call returns true if it's handled the event
// Usually it won't, so just pass it on to the main handler
if (queue == null || !await queue.TryHandle(evt))
if (queue == null || !await queue.TryHandle(evt))
await handler.Handle(shard, evt);
}
catch (Exception exc)
@ -218,12 +218,12 @@ namespace PluralKit.Bot
}
}
}
private async Task HandleError<T>(Shard shard, IEventHandler<T> handler, T evt, ILifetimeScope serviceScope, Exception exc)
where T: IGatewayEvent
where T : IGatewayEvent
{
_metrics.Measure.Meter.Mark(BotMetrics.BotErrors, exc.GetType().FullName);
// Make this beforehand so we can access the event ID for logging
var sentryEvent = new SentryEvent(exc);
@ -239,7 +239,7 @@ namespace PluralKit.Bot
// Report error to Sentry
// This will just no-op if there's no URL set
var sentryScope = serviceScope.Resolve<Scope>();
// Add some specific info about Discord error responses, as a breadcrumb
// TODO: headers to dict
// if (exc is BadRequestException bre)
@ -248,7 +248,7 @@ namespace PluralKit.Bot
// sentryScope.AddBreadcrumb(nfe.Response, "response.error", data: new Dictionary<string, string>(nfe.Response.Headers));
// if (exc is UnauthorizedException ue)
// sentryScope.AddBreadcrumb(ue.Response, "response.error", data: new Dictionary<string, string>(ue.Response.Headers));
SentrySdk.CaptureEvent(sentryEvent, sentryScope);
// Once we've sent it to Sentry, report it to the user (if we have permission to)
@ -261,7 +261,7 @@ namespace PluralKit.Bot
}
}
}
private async Task UpdatePeriodic()
{
_logger.Debug("Running once-per-minute scheduled tasks");
@ -273,7 +273,7 @@ namespace PluralKit.Bot
// Collect some stats, submit them to the metrics backend
await _collector.CollectStats();
await Task.WhenAll(((IMetricsRoot) _metrics).ReportRunner.RunAllAsync());
await Task.WhenAll(((IMetricsRoot)_metrics).ReportRunner.RunAllAsync());
_logger.Debug("Submitted metrics to backend");
}

View file

@ -2,22 +2,22 @@ namespace PluralKit.Bot
{
public class BotConfig
{
public static readonly string[] DefaultPrefixes = {"pk;", "pk!"};
public static readonly string[] DefaultPrefixes = { "pk;", "pk!" };
public string Token { get; set; }
public ulong? ClientId { get; set; }
// ASP.NET configuration merges arrays with defaults, so we leave this field nullable
// and fall back to the separate default array at the use site :)
// This does bind [] as null (therefore default) instead of an empty array, but I can live w/ that.
public string[] Prefixes { get; set; }
public int? MaxShardConcurrency { get; set; }
public ulong? AdminRole { get; set; }
public ClusterSettings? Cluster { get; set; }
public string? GatewayQueueUrl { get; set; }
public record ClusterSettings

View file

@ -7,23 +7,23 @@ namespace PluralKit.Bot
{
public static class BotMetrics
{
public static MeterOptions MessagesReceived => new MeterOptions {Name = "Messages processed", MeasurementUnit = Unit.Events, RateUnit = TimeUnit.Seconds, Context = "Bot"};
public static MeterOptions MessagesProxied => new MeterOptions {Name = "Messages proxied", MeasurementUnit = Unit.Events, RateUnit = TimeUnit.Seconds, Context = "Bot"};
public static MeterOptions CommandsRun => new MeterOptions {Name = "Commands run", MeasurementUnit = Unit.Commands, RateUnit = TimeUnit.Seconds, Context = "Bot"};
public static GaugeOptions MembersTotal => new GaugeOptions {Name = "Members total", MeasurementUnit = Unit.None, Context = "Bot"};
public static GaugeOptions MembersOnline => new GaugeOptions {Name = "Members online", MeasurementUnit = Unit.None, Context = "Bot"};
public static GaugeOptions Guilds => new GaugeOptions {Name = "Guilds", MeasurementUnit = Unit.None, Context = "Bot"};
public static GaugeOptions Channels => new GaugeOptions {Name = "Channels", MeasurementUnit = Unit.None, Context = "Bot"};
public static GaugeOptions ShardLatency => new GaugeOptions { Name = "Shard Latency", Context = "Bot" };
public static MeterOptions MessagesReceived => new MeterOptions { Name = "Messages processed", MeasurementUnit = Unit.Events, RateUnit = TimeUnit.Seconds, Context = "Bot" };
public static MeterOptions MessagesProxied => new MeterOptions { Name = "Messages proxied", MeasurementUnit = Unit.Events, RateUnit = TimeUnit.Seconds, Context = "Bot" };
public static MeterOptions CommandsRun => new MeterOptions { Name = "Commands run", MeasurementUnit = Unit.Commands, RateUnit = TimeUnit.Seconds, Context = "Bot" };
public static GaugeOptions MembersTotal => new GaugeOptions { Name = "Members total", MeasurementUnit = Unit.None, Context = "Bot" };
public static GaugeOptions MembersOnline => new GaugeOptions { Name = "Members online", MeasurementUnit = Unit.None, Context = "Bot" };
public static GaugeOptions Guilds => new GaugeOptions { Name = "Guilds", MeasurementUnit = Unit.None, Context = "Bot" };
public static GaugeOptions Channels => new GaugeOptions { Name = "Channels", MeasurementUnit = Unit.None, Context = "Bot" };
public static GaugeOptions ShardLatency => new GaugeOptions { Name = "Shard Latency", Context = "Bot" };
public static GaugeOptions ShardsConnected => new GaugeOptions { Name = "Shards Connected", Context = "Bot", MeasurementUnit = Unit.Connections };
public static MeterOptions WebhookCacheMisses => new MeterOptions { Name = "Webhook cache misses", Context = "Bot", MeasurementUnit = Unit.Calls };
public static GaugeOptions WebhookCacheSize => new GaugeOptions { Name = "Webhook Cache Size", Context = "Bot", MeasurementUnit = Unit.Items };
public static TimerOptions WebhookResponseTime => new TimerOptions { Name = "Webhook Response Time", Context = "Bot", RateUnit = TimeUnit.Seconds, MeasurementUnit = Unit.Requests, DurationUnit = TimeUnit.Seconds };
public static TimerOptions MessageContextQueryTime => new TimerOptions { Name = "Message context query duration", Context = "Bot", RateUnit = TimeUnit.Seconds, DurationUnit = TimeUnit.Seconds, MeasurementUnit = Unit.Calls };
public static TimerOptions ProxyMembersQueryTime => new TimerOptions { Name = "Proxy member query duration", Context = "Bot", RateUnit = TimeUnit.Seconds, DurationUnit = TimeUnit.Seconds, MeasurementUnit = Unit.Calls };
public static TimerOptions DiscordApiRequests => new TimerOptions { Name = "Discord API requests", MeasurementUnit = Unit.Requests, DurationUnit = TimeUnit.Milliseconds, Context = "Bot"};
public static MeterOptions BotErrors => new MeterOptions { Name = "Bot errors", MeasurementUnit = Unit.Errors, RateUnit = TimeUnit.Seconds, Context = "Bot"};
public static MeterOptions ErrorMessagesSent => new MeterOptions { Name = "Error messages sent", MeasurementUnit = Unit.Errors, RateUnit = TimeUnit.Seconds, Context = "Bot"};
public static TimerOptions EventsHandled => new TimerOptions { Name = "Events handled", MeasurementUnit = Unit.Errors, RateUnit = TimeUnit.Seconds, DurationUnit = TimeUnit.Seconds, Context = "Bot"};
public static TimerOptions DiscordApiRequests => new TimerOptions { Name = "Discord API requests", MeasurementUnit = Unit.Requests, DurationUnit = TimeUnit.Milliseconds, Context = "Bot" };
public static MeterOptions BotErrors => new MeterOptions { Name = "Bot errors", MeasurementUnit = Unit.Errors, RateUnit = TimeUnit.Seconds, Context = "Bot" };
public static MeterOptions ErrorMessagesSent => new MeterOptions { Name = "Error messages sent", MeasurementUnit = Unit.Errors, RateUnit = TimeUnit.Seconds, Context = "Bot" };
public static TimerOptions EventsHandled => new TimerOptions { Name = "Events handled", MeasurementUnit = Unit.Errors, RateUnit = TimeUnit.Seconds, DurationUnit = TimeUnit.Seconds, Context = "Bot" };
}
}

View file

@ -5,7 +5,7 @@ namespace PluralKit.Bot
public string Key { get; }
public string Usage { get; }
public string Description { get; }
public Command(string key, string usage, string description)
{
Key = key;

View file

@ -6,7 +6,7 @@ namespace PluralKit.Bot
{
public string Key { get; }
public string Description { get; }
public ICollection<Command> Children { get; }
public CommandGroup(string key, string description, ICollection<Command> children)

View file

@ -77,7 +77,7 @@ namespace PluralKit.Bot
public DiscordApiClient Rest => _rest;
public PKSystem System => _senderSystem;
public Parameters Parameters => _parameters;
internal IDatabase Database => _db;
@ -109,7 +109,7 @@ namespace PluralKit.Bot
return msg;
}
public async Task Execute<T>(Command commandDef, Func<T, Task> handler)
{
_currentCommand = commandDef;
@ -134,15 +134,15 @@ namespace PluralKit.Bot
}
}
public LookupContext LookupContextFor(PKSystem target) =>
public LookupContext LookupContextFor(PKSystem target) =>
System?.Id == target.Id ? LookupContext.ByOwner : LookupContext.ByNonOwner;
public LookupContext LookupContextFor(SystemId systemId) =>
public LookupContext LookupContextFor(SystemId systemId) =>
System?.Id == systemId ? LookupContext.ByOwner : LookupContext.ByNonOwner;
public LookupContext LookupContextFor(PKMember target) =>
System?.Id == target.System ? LookupContext.ByOwner : LookupContext.ByNonOwner;
public IComponentContext Services => _provider;
}
}

View file

@ -14,8 +14,8 @@ namespace PluralKit.Bot
{
public static string PopArgument(this Context ctx) =>
ctx.Parameters.Pop();
public static string PeekArgument(this Context ctx) =>
public static string PeekArgument(this Context ctx) =>
ctx.Parameters.Peek();
public static string RemainderOrNull(this Context ctx, bool skipFlags = true) =>
@ -23,10 +23,10 @@ namespace PluralKit.Bot
public static bool HasNext(this Context ctx, bool skipFlags = true) =>
ctx.RemainderOrNull(skipFlags) != null;
public static string FullCommand(this Context ctx) =>
public static string FullCommand(this Context ctx) =>
ctx.Parameters.FullCommand;
/// <summary>
/// Checks if the next parameter is equal to one of the given keywords. Case-insensitive.
/// </summary>
@ -58,7 +58,7 @@ namespace PluralKit.Bot
{
// Flags are *ALWAYS PARSED LOWERCASE*. This means we skip out on a "ToLower" call here.
// Can assume the caller array only contains lowercase *and* the set below only contains lowercase
var flags = ctx.Parameters.Flags();
return potentialMatches.Any(potentialMatch => flags.Contains(potentialMatch));
}
@ -66,7 +66,7 @@ namespace PluralKit.Bot
public static async Task<bool> MatchClear(this Context ctx, string toClear = null)
{
var matched = ctx.Match("clear", "reset") || ctx.MatchFlag("c", "clear");
if (matched && toClear != null)
if (matched && toClear != null)
return await ctx.ConfirmClear(toClear);
return matched;
}
@ -82,11 +82,11 @@ namespace PluralKit.Bot
if (parseRawMessageId && ulong.TryParse(word, out var mid))
return (mid, null);
var match = Regex.Match(word, "https://(?:\\w+.)?discord(?:app)?.com/channels/\\d+/(\\d+)/(\\d+)");
if (!match.Success)
return (null, null);
var channelId = ulong.Parse(match.Groups[1].Value);
var messageId = ulong.Parse(match.Groups[2].Value);
ctx.PopArgument();
@ -108,11 +108,11 @@ namespace PluralKit.Bot
if (restrictToSystem != null && member.System != restrictToSystem)
throw Errors.NotOwnMemberError; // TODO: name *which* member?
members.Add(member); // Then add to the final output list
}
if (members.Count == 0) throw new PKSyntaxError($"You must input at least one member.");
return members;
}
@ -131,13 +131,13 @@ namespace PluralKit.Bot
if (restrictToSystem != null && group.System != restrictToSystem)
throw Errors.NotOwnGroupError; // TODO: name *which* group?
groups.Add(group); // Then add to the final output list
}
if (groups.Count == 0) throw new PKSyntaxError($"You must input at least one group.");
return groups;
}
}
}
}

View file

@ -1,4 +1,4 @@
using Myriad.Types;
using Myriad.Types;
using PluralKit.Core;
@ -11,27 +11,27 @@ namespace PluralKit.Bot
if (ctx.Channel.GuildId != null) return ctx;
throw new PKError("This command can not be run in a DM.");
}
public static Context CheckSystemPrivacy(this Context ctx, PKSystem target, PrivacyLevel level)
{
if (level.CanAccess(ctx.LookupContextFor(target))) return ctx;
throw new PKError("You do not have permission to access this information.");
}
public static Context CheckOwnMember(this Context ctx, PKMember member)
{
if (member.System != ctx.System?.Id)
throw Errors.NotOwnMemberError;
return ctx;
}
public static Context CheckOwnGroup(this Context ctx, PKGroup group)
{
if (group.System != ctx.System?.Id)
throw Errors.NotOwnGroupError;
return ctx;
}
public static Context CheckSystem(this Context ctx)
{
if (ctx.System == null)
@ -45,7 +45,7 @@ namespace PluralKit.Bot
throw Errors.ExistingSystemError;
return ctx;
}
public static Context CheckAuthorPermission(this Context ctx, PermissionSet neededPerms, string permissionName)
{
if ((ctx.UserPermissions & neededPerms) != neededPerms)

View file

@ -23,14 +23,14 @@ namespace PluralKit.Bot
public static bool MatchUserRaw(this Context ctx, out ulong id)
{
id = 0;
var text = ctx.PeekArgument();
if (text.TryParseMention(out var mentionId))
id = mentionId;
return id != 0;
}
public static Task<PKSystem> PeekSystem(this Context ctx) => ctx.MatchSystemInner();
public static async Task<PKSystem> MatchSystem(this Context ctx)
@ -50,7 +50,7 @@ namespace PluralKit.Bot
// - A system hid
await using var conn = await ctx.Database.Obtain();
// Direct IDs and mentions are both handled by the below method:
if (input.TryParseMention(out var id))
return await ctx.Repository.GetSystemByAccount(conn, id);
@ -82,7 +82,7 @@ namespace PluralKit.Bot
// And if that again fails, we try finding a member with a display name matching the argument from the system
if (ctx.System != null && await ctx.Repository.GetMemberByDisplayName(conn, ctx.System.Id, input) is PKMember memberByDisplayName)
return memberByDisplayName;
// We didn't find anything, so we return null.
return null;
}
@ -102,17 +102,17 @@ namespace PluralKit.Bot
// Finally, we return the member value.
return member;
}
public static async Task<PKGroup> PeekGroup(this Context ctx)
{
var input = ctx.PeekArgument();
await using var conn = await ctx.Database.Obtain();
if (ctx.System != null && await ctx.Repository.GetGroupByName(conn, ctx.System.Id, input) is {} byName)
if (ctx.System != null && await ctx.Repository.GetGroupByName(conn, ctx.System.Id, input) is { } byName)
return byName;
if (await ctx.Repository.GetGroupByHid(conn, input) is {} byHid)
if (await ctx.Repository.GetGroupByHid(conn, input) is { } byHid)
return byHid;
if (await ctx.Repository.GetGroupByDisplayName(conn, ctx.System.Id, input) is {} byDisplayName)
if (await ctx.Repository.GetGroupByDisplayName(conn, ctx.System.Id, input) is { } byDisplayName)
return byDisplayName;
return null;
@ -139,7 +139,7 @@ namespace PluralKit.Bot
return $"Member with name \"{input}\" not found. Note that a member ID is 5 characters long.";
return $"Member not found. Note that a member ID is 5 characters long.";
}
public static string CreateGroupNotFoundError(this Context ctx, string input)
{
// TODO: does this belong here?
@ -154,18 +154,18 @@ namespace PluralKit.Bot
return $"Group with name \"{input}\" not found. Note that a group ID is 5 characters long.";
return $"Group not found. Note that a group ID is 5 characters long.";
}
public static Task<Channel> MatchChannel(this Context ctx)
{
if (!MentionUtils.TryParseChannel(ctx.PeekArgument(), out var id))
if (!MentionUtils.TryParseChannel(ctx.PeekArgument(), out var id))
return Task.FromResult<Channel>(null);
if (!ctx.Cache.TryGetChannel(id, out var channel))
return Task.FromResult<Channel>(null);
if (!DiscordUtils.IsValidGuildChannel(channel))
return Task.FromResult<Channel>(null);
ctx.PopArgument();
return Task.FromResult(channel);
}

View file

@ -37,10 +37,10 @@ namespace PluralKit.Bot
{
// Start of the word
internal readonly int startPos;
// End of the word
internal readonly int endPos;
// How much to advance word pointer afterwards to point at the start of the *next* word
internal readonly int advanceAfterWord;
@ -66,15 +66,15 @@ namespace PluralKit.Bot
private void ParseFlags()
{
_flags = new HashSet<string>();
var ptr = 0;
while (NextWordPosition(ptr) is { } wp)
{
ptr = wp.endPos + wp.advanceAfterWord;
// Is this word a *flag* (as in, starts with a - AND is not quoted)
if (_cmd[wp.startPos] != '-' || wp.wasQuoted) continue; // (if not, carry on w/ next word)
// Find the *end* of the flag start (technically allowing arbitrary amounts of dashes)
var flagNameStart = wp.startPos;
while (flagNameStart < _cmd.Length && _cmd[flagNameStart] == '-')
@ -125,7 +125,7 @@ namespace PluralKit.Bot
if (skipFlags)
{
// Skip all *leading* flags when taking the remainder
while (NextWordPosition(_ptr) is {} wp)
while (NextWordPosition(_ptr) is { } wp)
{
if (_cmd[wp.startPos] != '-' || wp.wasQuoted) break;
_ptr = wp.endPos + wp.advanceAfterWord;
@ -135,7 +135,7 @@ namespace PluralKit.Bot
// *Then* get the remainder
return _cmd.Substring(Math.Min(_ptr, _cmd.Length)).Trim();
}
public string FullCommand => _cmd;
private WordPosition? NextWordPosition(int position)
@ -163,7 +163,7 @@ namespace PluralKit.Bot
// Not a quoted word, just find the next space and return if it's the end of the command
var wordEnd = _cmd.IndexOf(' ', position + 1);
return wordEnd == -1
? new WordPosition(position, _cmd.Length, 0, false)
: new WordPosition(position, wordEnd, 1, false);
@ -179,7 +179,7 @@ namespace PluralKit.Bot
return true;
}
}
correspondingRightQuotes = null;
return false;
}

View file

@ -1,4 +1,4 @@
using System.Linq;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
@ -22,7 +22,7 @@ namespace PluralKit.Bot
public async Task UpdateSystemId(Context ctx)
{
AssertBotAdmin(ctx);
var target = await ctx.MatchSystem();
if (target == null)
throw new PKError("Unknown system.");
@ -38,14 +38,14 @@ namespace PluralKit.Bot
if (!await ctx.PromptYesNo($"Change system ID of `{target.Hid}` to `{newHid}`?", "Change"))
throw new PKError("ID change cancelled.");
await _db.Execute(c => _repo.UpdateSystem(c, target.Id, new SystemPatch {Hid = newHid}));
await _db.Execute(c => _repo.UpdateSystem(c, target.Id, new SystemPatch { Hid = newHid }));
await ctx.Reply($"{Emojis.Success} System ID updated (`{target.Hid}` -> `{newHid}`).");
}
public async Task UpdateMemberId(Context ctx)
{
AssertBotAdmin(ctx);
var target = await ctx.MatchMember();
if (target == null)
throw new PKError("Unknown member.");
@ -60,8 +60,8 @@ namespace PluralKit.Bot
if (!await ctx.PromptYesNo($"Change member ID of **{target.NameFor(LookupContext.ByNonOwner)}** (`{target.Hid}`) to `{newHid}`?", "Change"))
throw new PKError("ID change cancelled.");
await _db.Execute(c => _repo.UpdateMember(c, target.Id, new MemberPatch {Hid = newHid}));
await _db.Execute(c => _repo.UpdateMember(c, target.Id, new MemberPatch { Hid = newHid }));
await ctx.Reply($"{Emojis.Success} Member ID updated (`{target.Hid}` -> `{newHid}`).");
}
@ -84,7 +84,7 @@ namespace PluralKit.Bot
if (!await ctx.PromptYesNo($"Change group ID of **{target.Name}** (`{target.Hid}`) to `{newHid}`?", "Change"))
throw new PKError("ID change cancelled.");
await _db.Execute(c => _repo.UpdateGroup(c, target.Id, new GroupPatch {Hid = newHid}));
await _db.Execute(c => _repo.UpdateGroup(c, target.Id, new GroupPatch { Hid = newHid }));
await ctx.Reply($"{Emojis.Success} Group ID updated (`{target.Hid}` -> `{newHid}`).");
}
@ -109,7 +109,7 @@ namespace PluralKit.Bot
if (!await ctx.PromptYesNo($"Update member limit from **{currentLimit}** to **{newLimit}**?", "Update"))
throw new PKError("Member limit change cancelled.");
await using var conn = await _db.Obtain();
await _repo.UpdateSystem(conn, target.Id, new SystemPatch
{

View file

@ -27,7 +27,7 @@ namespace PluralKit.Bot
{
// no need to check account here, it's already done at CommandTree
ctx.CheckGuildContext();
if (ctx.Match("off", "stop", "cancel", "no", "disable", "remove"))
await AutoproxyOff(ctx);
else if (ctx.Match("latch", "last", "proxy", "stick", "sticky"))
@ -90,7 +90,7 @@ namespace PluralKit.Bot
var commandList = "**pk;autoproxy latch** - Autoproxies as last-proxied member\n**pk;autoproxy front** - Autoproxies as current (first) fronter\n**pk;autoproxy <member>** - Autoproxies as a specific member";
var eb = new EmbedBuilder()
.Title($"Current autoproxy status (for {ctx.Guild.Name.EscapeMarkdown()})");
var fronters = ctx.MessageContext.LastSwitchMembers;
var relevantMember = ctx.MessageContext.AutoproxyMode switch
{
@ -99,36 +99,38 @@ namespace PluralKit.Bot
_ => null
};
switch (ctx.MessageContext.AutoproxyMode) {
switch (ctx.MessageContext.AutoproxyMode)
{
case AutoproxyMode.Off:
eb.Description($"Autoproxy is currently **off** in this server. To enable it, use one of the following commands:\n{commandList}");
break;
case AutoproxyMode.Front:
{
if (fronters.Length == 0)
eb.Description("Autoproxy is currently set to **front mode** in this server, but there are currently no fronters registered. Use the `pk;switch` command to log a switch.");
else
{
if (relevantMember == null)
throw new ArgumentException("Attempted to print member autoproxy status, but the linked member ID wasn't found in the database. Should be handled appropriately.");
eb.Description($"Autoproxy is currently set to **front mode** in this server. The current (first) fronter is **{relevantMember.NameFor(ctx).EscapeMarkdown()}** (`{relevantMember.Hid}`). To disable, type `pk;autoproxy off`.");
}
if (fronters.Length == 0)
eb.Description("Autoproxy is currently set to **front mode** in this server, but there are currently no fronters registered. Use the `pk;switch` command to log a switch.");
else
{
if (relevantMember == null)
throw new ArgumentException("Attempted to print member autoproxy status, but the linked member ID wasn't found in the database. Should be handled appropriately.");
eb.Description($"Autoproxy is currently set to **front mode** in this server. The current (first) fronter is **{relevantMember.NameFor(ctx).EscapeMarkdown()}** (`{relevantMember.Hid}`). To disable, type `pk;autoproxy off`.");
}
break;
}
break;
}
// AutoproxyMember is never null if Mode is Member, this is just to make the compiler shut up
case AutoproxyMode.Member when relevantMember != null: {
eb.Description($"Autoproxy is active for member **{relevantMember.NameFor(ctx)}** (`{relevantMember.Hid}`) in this server. To disable, type `pk;autoproxy off`.");
break;
}
case AutoproxyMode.Member when relevantMember != null:
{
eb.Description($"Autoproxy is active for member **{relevantMember.NameFor(ctx)}** (`{relevantMember.Hid}`) in this server. To disable, type `pk;autoproxy off`.");
break;
}
case AutoproxyMode.Latch:
eb.Description("Autoproxy is currently set to **latch mode**, meaning the *last-proxied member* will be autoproxied. To disable, type `pk;autoproxy off`.");
break;
default: throw new ArgumentOutOfRangeException();
}
if (!ctx.MessageContext.AllowAutoproxy)
if (!ctx.MessageContext.AllowAutoproxy)
eb.Field(new("\u200b", $"{Emojis.Note} Autoproxy is currently **disabled** for your account (<@{ctx.Author.Id}>). To enable it, use `pk;autoproxy account enable`."));
return eb.Build();
@ -139,9 +141,9 @@ namespace PluralKit.Bot
if (!ctx.HasNext())
{
var timeout = ctx.System.LatchTimeout.HasValue
? Duration.FromSeconds(ctx.System.LatchTimeout.Value)
: (Duration?) null;
? Duration.FromSeconds(ctx.System.LatchTimeout.Value)
: (Duration?)null;
if (timeout == null)
await ctx.Reply($"You do not have a custom autoproxy timeout duration set. The default latch timeout duration is {ProxyMatcher.DefaultLatchExpiryTime.ToTimeSpan().Humanize(4)}.");
else if (timeout == Duration.Zero)
@ -169,9 +171,9 @@ namespace PluralKit.Bot
else newTimeout = timeoutPeriod;
}
await _db.Execute(conn => _repo.UpdateSystem(conn, ctx.System.Id,
new SystemPatch { LatchTimeout = (int?) newTimeout?.TotalSeconds }));
await _db.Execute(conn => _repo.UpdateSystem(conn, ctx.System.Id,
new SystemPatch { LatchTimeout = (int?)newTimeout?.TotalSeconds }));
if (newTimeout == null)
await ctx.Reply($"{Emojis.Success} Latch timeout reset to default ({ProxyMatcher.DefaultLatchExpiryTime.ToTimeSpan().Humanize(4)}).");
else if (newTimeout == Duration.Zero && overflow != Duration.Zero)
@ -213,7 +215,7 @@ namespace PluralKit.Bot
private Task UpdateAutoproxy(Context ctx, AutoproxyMode autoproxyMode, MemberId? autoproxyMember)
{
var patch = new SystemGuildPatch {AutoproxyMode = autoproxyMode, AutoproxyMember = autoproxyMember};
var patch = new SystemGuildPatch { AutoproxyMode = autoproxyMode, AutoproxyMember = autoproxyMember };
return _db.Execute(conn => _repo.UpsertSystemGuild(conn, ctx.System.Id, ctx.Guild.Id, patch));
}
}

View file

@ -16,9 +16,9 @@ namespace PluralKit.Bot
if (await ctx.MatchUser() is { } user)
{
var url = user.AvatarUrl("png", 256);
return new ParsedImage {Url = url, Source = AvatarSource.User, SourceUser = user};
return new ParsedImage { Url = url, Source = AvatarSource.User, SourceUser = user };
}
// If we have a positional argument, try to parse it as a URL
var arg = ctx.RemainderOrNull();
if (arg != null)
@ -26,24 +26,24 @@ namespace PluralKit.Bot
// Allow surrounding the URL with <angle brackets> to "de-embed"
if (arg.StartsWith("<") && arg.EndsWith(">"))
arg = arg.Substring(1, arg.Length - 2);
if (!Uri.TryCreate(arg, UriKind.Absolute, out var uri))
throw Errors.InvalidUrl(arg);
if (uri.Scheme != "http" && uri.Scheme != "https")
throw Errors.InvalidUrl(arg);
// ToString URL-decodes, which breaks URLs to spaces; AbsoluteUri doesn't
return new ParsedImage {Url = uri.AbsoluteUri, Source = AvatarSource.Url};
return new ParsedImage { Url = uri.AbsoluteUri, Source = AvatarSource.Url };
}
// If we have an attachment, use that
if (ctx.Message.Attachments.FirstOrDefault() is {} attachment)
if (ctx.Message.Attachments.FirstOrDefault() is { } attachment)
{
var url = attachment.ProxyUrl;
return new ParsedImage {Url = url, Source = AvatarSource.Attachment};
return new ParsedImage { Url = url, Source = AvatarSource.Attachment };
}
// We should only get here if there are no arguments (which would get parsed as URL + throw if error)
// and if there are no attachments (which would have been caught just before)
return null;
@ -63,4 +63,4 @@ namespace PluralKit.Bot
User,
Attachment
}
}
}

View file

@ -55,19 +55,22 @@ namespace PluralKit.Bot
if (!ulong.TryParse(guildIdStr, out var guildId))
throw new PKSyntaxError($"Could not parse {guildIdStr.AsCode()} as an ID.");
try {
try
{
guild = await _rest.GetGuild(guildId);
} catch (Myriad.Rest.Exceptions.ForbiddenException) {
}
catch (Myriad.Rest.Exceptions.ForbiddenException)
{
throw Errors.GuildNotFound(guildId);
}
if (guild != null)
if (guild != null)
senderGuildUser = await _rest.GetGuildMember(guildId, ctx.Author.Id);
if (guild == null || senderGuildUser == null)
if (guild == null || senderGuildUser == null)
throw Errors.GuildNotFound(guildId);
}
var requiredPermissions = new []
var requiredPermissions = new[]
{
PermissionSet.ViewChannel,
PermissionSet.SendMessages,
@ -87,7 +90,7 @@ namespace PluralKit.Bot
var botPermissions = _bot.PermissionsIn(channel.Id);
var webhookPermissions = _cache.EveryonePermissions(channel);
var userPermissions = PermissionExtensions.PermissionsFor(guild, channel, ctx.Author.Id, senderGuildUser);
if ((userPermissions & PermissionSet.ViewChannel) == 0)
{
// If the user can't see this channel, don't calculate permissions for it
@ -100,14 +103,14 @@ namespace PluralKit.Bot
// We use a bitfield so we can set individual permission bits in the loop
// TODO: Rewrite with proper bitfield math
ulong missingPermissionField = 0;
foreach (var requiredPermission in requiredPermissions)
if ((botPermissions & requiredPermission) == 0)
missingPermissionField |= (ulong) requiredPermission;
missingPermissionField |= (ulong)requiredPermission;
if ((webhookPermissions & PermissionSet.UseExternalEmojis) == 0)
{
missingPermissionField |= (ulong) PermissionSet.UseExternalEmojis;
missingPermissionField |= (ulong)PermissionSet.UseExternalEmojis;
missingEmojiPermissions = true;
}
@ -119,7 +122,7 @@ namespace PluralKit.Bot
permissionsMissing[missingPermissionField].Add(channel);
}
}
// Generate the output embed
var eb = new EmbedBuilder()
.Title($"Permission check for **{guild.Name}**");
@ -134,7 +137,7 @@ namespace PluralKit.Bot
{
// Each missing permission field can have multiple missing channels
// so we extract them all and generate a comma-separated list
var missingPermissionNames = ((PermissionSet) missingPermissionField).ToPermissionString();
var missingPermissionNames = ((PermissionSet)missingPermissionField).ToPermissionString();
var channelsList = string.Join("\n", channels
.OrderBy(c => c.Position)
@ -155,7 +158,7 @@ namespace PluralKit.Bot
if (footer.Length > 0)
eb.Footer(new(footer));
// Send! :)
await ctx.Reply(embed: eb.Build());
}
@ -164,7 +167,7 @@ namespace PluralKit.Bot
{
if (!ctx.HasNext() && ctx.Message.MessageReference == null)
throw new PKError("You need to specify a message.");
var failedToGetMessage = "Could not find a valid message to check, was not able to fetch the message, or the message was not sent by you.";
var (messageId, channelId) = ctx.MatchMessage(false);
@ -182,7 +185,7 @@ namespace PluralKit.Bot
// get the message info
var msg = ctx.Message;
try
try
{
msg = await _rest.GetMessage(channelId.Value, messageId.Value);
}
@ -212,13 +215,14 @@ namespace PluralKit.Bot
var members = (await _repo.GetProxyMembers(conn, msg.Author.Id, channel.GuildId.Value)).ToList();
// Run everything through the checks, catch the ProxyCheckFailedException, and reply with the error message.
try
{
try
{
_proxy.ShouldProxy(channel, msg, context);
_matcher.TryMatch(context, members, out var match, msg.Content, msg.Attachments.Length > 0, context.AllowAutoproxy);
await ctx.Reply("I'm not sure why this message was not proxied, sorry.");
} catch (ProxyService.ProxyChecksFailedException e)
}
catch (ProxyService.ProxyChecksFailedException e)
{
await ctx.Reply($"{e.Message}");
}

View file

@ -120,13 +120,13 @@ namespace PluralKit.Bot
GroupDelete, GroupMemberRandom, GroupFrontPercent
};
public static Command[] SwitchCommands = {Switch, SwitchOut, SwitchMove, SwitchDelete, SwitchDeleteAll};
public static Command[] SwitchCommands = { Switch, SwitchOut, SwitchMove, SwitchDelete, SwitchDeleteAll };
public static Command[] AutoproxyCommands = {AutoproxySet, AutoproxyTimeout, AutoproxyAccount};
public static Command[] LogCommands = {LogChannel, LogChannelClear, LogEnable, LogDisable};
public static Command[] AutoproxyCommands = { AutoproxySet, AutoproxyTimeout, AutoproxyAccount };
public static Command[] BlacklistCommands = {BlacklistAdd, BlacklistRemove, BlacklistShow};
public static Command[] LogCommands = { LogChannel, LogChannelClear, LogEnable, LogDisable };
public static Command[] BlacklistCommands = { BlacklistAdd, BlacklistRemove, BlacklistShow };
public Task ExecuteCommand(Context ctx)
{
@ -214,9 +214,9 @@ namespace PluralKit.Bot
return HandleAdminCommand(ctx);
if (ctx.Match("random", "r"))
if (ctx.Match("group", "g") || ctx.MatchFlag("group", "g"))
return ctx.Execute<Random>(GroupRandom, r => r.Group(ctx));
else
return ctx.Execute<Random>(MemberRandom, m => m.Member(ctx));
return ctx.Execute<Random>(GroupRandom, r => r.Group(ctx));
else
return ctx.Execute<Random>(MemberRandom, m => m.Member(ctx));
// remove compiler warning
return ctx.Reply(
@ -348,7 +348,7 @@ namespace PluralKit.Bot
await PrintCommandNotFoundError(ctx, SystemList, SystemFronter, SystemFrontHistory, SystemFrontPercent,
SystemInfo);
}
private async Task HandleMemberCommand(Context ctx)
{
if (ctx.Match("new", "n", "add", "create", "register"))
@ -392,7 +392,7 @@ namespace PluralKit.Bot
await ctx.Execute<MemberGroup>(MemberGroupAdd, m => m.AddRemove(ctx, target, Groups.AddRemoveOperation.Add));
else if (ctx.Match("remove", "rem"))
await ctx.Execute<MemberGroup>(MemberGroupRemove, m => m.AddRemove(ctx, target, Groups.AddRemoveOperation.Remove));
else
else
await ctx.Execute<MemberGroup>(MemberGroups, m => m.List(ctx, target));
else if (ctx.Match("serveravatar", "servericon", "serverimage", "serverpfp", "serverpic", "savatar", "spic", "guildavatar", "guildpic", "guildicon", "sicon"))
await ctx.Execute<MemberAvatar>(MemberServerAvatar, m => m.ServerAvatar(ctx, target));
@ -414,8 +414,8 @@ namespace PluralKit.Bot
await ctx.Execute<Member>(MemberInfo, m => m.Soulscream(ctx, target));
else if (!ctx.HasNext()) // Bare command
await ctx.Execute<Member>(MemberInfo, m => m.ViewMember(ctx, target));
else
await PrintCommandNotFoundError(ctx, MemberInfo, MemberRename, MemberDisplayName, MemberServerName ,MemberDesc, MemberPronouns, MemberColor, MemberBirthday, MemberProxy, MemberDelete, MemberAvatar, SystemList);
else
await PrintCommandNotFoundError(ctx, MemberInfo, MemberRename, MemberDisplayName, MemberServerName, MemberDesc, MemberPronouns, MemberColor, MemberBirthday, MemberProxy, MemberDelete, MemberAvatar, SystemList);
}
private async Task HandleGroupCommand(Context ctx)
@ -427,7 +427,7 @@ namespace PluralKit.Bot
await ctx.Execute<Groups>(GroupList, g => g.ListSystemGroups(ctx, null));
else if (ctx.Match("commands", "help"))
await PrintCommandList(ctx, "groups", GroupCommands);
else if (await ctx.MatchGroup() is {} target)
else if (await ctx.MatchGroup() is { } target)
{
// Commands with group argument
if (ctx.Match("rename", "name", "changename", "setname"))
@ -437,7 +437,7 @@ namespace PluralKit.Bot
else if (ctx.Match("description", "info", "bio", "text", "desc"))
await ctx.Execute<Groups>(GroupDesc, g => g.GroupDescription(ctx, target));
else if (ctx.Match("add", "a"))
await ctx.Execute<Groups>(GroupAdd,g => g.AddRemoveMembers(ctx, target, Groups.AddRemoveOperation.Add));
await ctx.Execute<Groups>(GroupAdd, g => g.AddRemoveMembers(ctx, target, Groups.AddRemoveOperation.Add));
else if (ctx.Match("remove", "rem", "r"))
await ctx.Execute<Groups>(GroupRemove, g => g.AddRemoveMembers(ctx, target, Groups.AddRemoveOperation.Remove));
else if (ctx.Match("members", "list", "ms", "l"))
@ -488,14 +488,15 @@ namespace PluralKit.Bot
}
private async Task CommandHelpRoot(Context ctx)
{
{
if (!ctx.HasNext())
{
await ctx.Reply($"{Emojis.Error} You need to pass a target command.\nAvailable command help targets: `system`, `member`, `group`, `switch`, `log`, `blacklist`.\nFor the full list of commands, see the website: <https://pluralkit.me/commands>");
return;
}
switch (ctx.PeekArgument()) {
switch (ctx.PeekArgument())
{
case "system":
case "systems":
case "s":
@ -561,14 +562,14 @@ namespace PluralKit.Bot
await ctx.Reply(
$"{Emojis.Error} Unknown command `pk;{ctx.FullCommand().Truncate(100)}`. Perhaps you meant to use one of the following commands?\n{commandListStr}\n\nFor a full list of possible commands, see <https://pluralkit.me/commands>.");
}
private async Task PrintCommandExpectedError(Context ctx, params Command[] potentialCommands)
{
var commandListStr = CreatePotentialCommandList(potentialCommands);
await ctx.Reply(
$"{Emojis.Error} You need to pass a command. Perhaps you meant to use one of the following commands?\n{commandListStr}\n\nFor a full list of possible commands, see <https://pluralkit.me/commands>.");
}
private static string CreatePotentialCommandList(params Command[] potentialCommands)
{
return string.Join("\n", potentialCommands.Select(cmd => $"- **pk;{cmd.Usage}** - *{cmd.Description}*"));
@ -597,4 +598,4 @@ namespace PluralKit.Bot
return $"System with ID {input.AsCode()} not found.";
}
}
}
}

View file

@ -1,4 +1,4 @@
using System.Threading.Tasks;
using System.Threading.Tasks;
namespace PluralKit.Bot
{

View file

@ -36,14 +36,14 @@ namespace PluralKit.Bot
public async Task CreateGroup(Context ctx)
{
ctx.CheckSystem();
// Check group name length
var groupName = ctx.RemainderOrNull() ?? throw new PKSyntaxError("You must pass a group name.");
if (groupName.Length > Limits.MaxGroupNameLength)
throw new PKError($"Group name too long ({groupName.Length}/{Limits.MaxGroupNameLength} characters).");
await using var conn = await _db.Obtain();
// Check group cap
var existingGroupCount = await conn.QuerySingleAsync<int>("select count(*) from groups where system = @System", new { System = ctx.System.Id });
var groupLimit = ctx.System.GroupLimitOverride ?? Limits.MaxGroupCount;
@ -52,14 +52,15 @@ namespace PluralKit.Bot
// Warn if there's already a group by this name
var existingGroup = await _repo.GetGroupByName(conn, ctx.System.Id, groupName);
if (existingGroup != null) {
if (existingGroup != null)
{
var msg = $"{Emojis.Warn} You already have a group in your system with the name \"{existingGroup.Name}\" (with ID `{existingGroup.Hid}`). Do you want to create another group with the same name?";
if (!await ctx.PromptYesNo(msg, "Create"))
throw new PKError("Group creation cancelled.");
}
var newGroup = await _repo.CreateGroup(conn, ctx.System.Id, groupName);
var eb = new EmbedBuilder()
.Description($"Your new group, **{groupName}**, has been created, with the group ID **`{newGroup.Hid}`**.\nBelow are a couple of useful commands:")
.Field(new("View the group card", $"> pk;group **{newGroup.Reference()}**"))
@ -72,23 +73,24 @@ namespace PluralKit.Bot
public async Task RenameGroup(Context ctx, PKGroup target)
{
ctx.CheckOwnGroup(target);
// Check group name length
var newName = ctx.RemainderOrNull() ?? throw new PKSyntaxError("You must pass a new group name.");
if (newName.Length > Limits.MaxGroupNameLength)
throw new PKError($"New group name too long ({newName.Length}/{Limits.MaxMemberNameLength} characters).");
await using var conn = await _db.Obtain();
// Warn if there's already a group by this name
var existingGroup = await _repo.GetGroupByName(conn, ctx.System.Id, newName);
if (existingGroup != null && existingGroup.Id != target.Id) {
if (existingGroup != null && existingGroup.Id != target.Id)
{
var msg = $"{Emojis.Warn} You already have a group in your system with the name \"{existingGroup.Name}\" (with ID `{existingGroup.Hid}`). Do you want to rename this group to that name too?";
if (!await ctx.PromptYesNo(msg, "Rename"))
throw new PKError("Group rename cancelled.");
}
await _repo.UpdateGroup(conn, target.Id, new GroupPatch {Name = newName});
await _repo.UpdateGroup(conn, target.Id, new GroupPatch { Name = newName });
await ctx.Reply($"{Emojis.Success} Group name changed from **{target.Name}** to **{newName}**.");
}
@ -98,8 +100,8 @@ namespace PluralKit.Bot
if (await ctx.MatchClear("this group's display name"))
{
ctx.CheckOwnGroup(target);
var patch = new GroupPatch {DisplayName = Partial<string>.Null()};
var patch = new GroupPatch { DisplayName = Partial<string>.Null() };
await _db.Execute(conn => _repo.UpdateGroup(conn, target.Id, patch));
await ctx.Reply($"{Emojis.Success} Group display name cleared.");
@ -124,40 +126,40 @@ namespace PluralKit.Bot
var eb = new EmbedBuilder()
.Field(new("Name", target.Name))
.Field(new("Display Name", target.DisplayName ?? "*(none)*"));
if (ctx.System?.Id == target.System)
eb.Description($"To change display name, type `pk;group {target.Reference()} displayname <display name>`.\nTo clear it, type `pk;group {target.Reference()} displayname -clear`.\nTo print the raw display name, type `pk;group {target.Reference()} displayname -raw`.");
await ctx.Reply(embed: eb.Build());
}
}
else
{
ctx.CheckOwnGroup(target);
var newDisplayName = ctx.RemainderOrNull();
var patch = new GroupPatch {DisplayName = Partial<string>.Present(newDisplayName)};
var patch = new GroupPatch { DisplayName = Partial<string>.Present(newDisplayName) };
await _db.Execute(conn => _repo.UpdateGroup(conn, target.Id, patch));
await ctx.Reply($"{Emojis.Success} Group display name changed.");
}
}
public async Task GroupDescription(Context ctx, PKGroup target)
{
if (await ctx.MatchClear("this group's description"))
{
ctx.CheckOwnGroup(target);
var patch = new GroupPatch {Description = Partial<string>.Null()};
var patch = new GroupPatch { Description = Partial<string>.Null() };
await _db.Execute(conn => _repo.UpdateGroup(conn, target.Id, patch));
await ctx.Reply($"{Emojis.Success} Group description cleared.");
}
}
else if (!ctx.HasNext())
{
if (!target.DescriptionPrivacy.CanAccess(ctx.LookupContextFor(target.System)))
throw Errors.LookupNotAllowed;
throw Errors.LookupNotAllowed;
if (target.Description == null)
if (ctx.System?.Id == target.System)
@ -170,7 +172,7 @@ namespace PluralKit.Bot
await ctx.Reply(embed: new EmbedBuilder()
.Title("Group description")
.Description(target.Description)
.Field(new("\u200B", $"To print the description with formatting, type `pk;group {target.Reference()} description -raw`."
.Field(new("\u200B", $"To print the description with formatting, type `pk;group {target.Reference()} description -raw`."
+ (ctx.System?.Id == target.System ? $" To clear it, type `pk;group {target.Reference()} description -clear`." : "")))
.Build());
}
@ -181,10 +183,10 @@ namespace PluralKit.Bot
var description = ctx.RemainderOrNull().NormalizeLineEndSpacing();
if (description.IsLongerThan(Limits.MaxDescriptionLength))
throw Errors.DescriptionTooLongError(description.Length);
var patch = new GroupPatch {Description = Partial<string>.Present(description)};
var patch = new GroupPatch { Description = Partial<string>.Present(description) };
await _db.Execute(conn => _repo.UpdateGroup(conn, target.Id, patch));
await ctx.Reply($"{Emojis.Success} Group description changed.");
}
}
@ -194,19 +196,19 @@ namespace PluralKit.Bot
async Task ClearIcon()
{
ctx.CheckOwnGroup(target);
await _db.Execute(c => _repo.UpdateGroup(c, target.Id, new GroupPatch {Icon = null}));
await _db.Execute(c => _repo.UpdateGroup(c, target.Id, new GroupPatch { Icon = null }));
await ctx.Reply($"{Emojis.Success} Group icon cleared.");
}
async Task SetIcon(ParsedImage img)
{
ctx.CheckOwnGroup(target);
await AvatarUtils.VerifyAvatarOrThrow(_client, img.Url);
await _db.Execute(c => _repo.UpdateGroup(c, target.Id, new GroupPatch {Icon = img.Url}));
await _db.Execute(c => _repo.UpdateGroup(c, target.Id, new GroupPatch { Icon = img.Url }));
var msg = img.Source switch
{
AvatarSource.User => $"{Emojis.Success} Group icon changed to {img.SourceUser?.Username}'s avatar!\n{Emojis.Warn} If {img.SourceUser?.Username} changes their avatar, the group icon will need to be re-set.",
@ -214,11 +216,11 @@ namespace PluralKit.Bot
AvatarSource.Attachment => $"{Emojis.Success} Group icon changed to attached image.\n{Emojis.Warn} If you delete the message containing the attachment, the group icon will stop working.",
_ => throw new ArgumentOutOfRangeException()
};
// The attachment's already right there, no need to preview it.
var hasEmbed = img.Source != AvatarSource.Attachment;
await (hasEmbed
? ctx.Reply(msg, embed: new EmbedBuilder().Image(new(img.Url)).Build())
await (hasEmbed
? ctx.Reply(msg, embed: new EmbedBuilder().Image(new(img.Url)).Build())
: ctx.Reply(msg));
}
@ -231,7 +233,7 @@ namespace PluralKit.Bot
var eb = new EmbedBuilder()
.Title("Group icon")
.Image(new(target.Icon.TryGetCleanCdnUrl()));
if (target.System == ctx.System?.Id)
{
eb.Description($"To clear, use `pk;group {target.Reference()} icon -clear`.");
@ -245,7 +247,7 @@ namespace PluralKit.Bot
if (await ctx.MatchClear("this group's icon"))
await ClearIcon();
else if (await ctx.MatchImage() is {} img)
else if (await ctx.MatchImage() is { } img)
await SetIcon(img);
else
await ShowIcon();
@ -257,7 +259,7 @@ namespace PluralKit.Bot
{
ctx.CheckOwnGroup(target);
await _db.Execute(c => _repo.UpdateGroup(c, target.Id, new GroupPatch {BannerImage = null}));
await _db.Execute(c => _repo.UpdateGroup(c, target.Id, new GroupPatch { BannerImage = null }));
await ctx.Reply($"{Emojis.Success} Group banner image cleared.");
}
@ -267,7 +269,7 @@ namespace PluralKit.Bot
await AvatarUtils.VerifyAvatarOrThrow(_client, img.Url, isFullSizeImage: true);
await _db.Execute(c => _repo.UpdateGroup(c, target.Id, new GroupPatch {BannerImage = img.Url}));
await _db.Execute(c => _repo.UpdateGroup(c, target.Id, new GroupPatch { BannerImage = img.Url }));
var msg = img.Source switch
{
@ -279,8 +281,8 @@ namespace PluralKit.Bot
// The attachment's already right there, no need to preview it.
var hasEmbed = img.Source != AvatarSource.Attachment;
await (hasEmbed
? ctx.Reply(msg, embed: new EmbedBuilder().Image(new(img.Url)).Build())
await (hasEmbed
? ctx.Reply(msg, embed: new EmbedBuilder().Image(new(img.Url)).Build())
: ctx.Reply(msg));
}
@ -307,7 +309,7 @@ namespace PluralKit.Bot
if (await ctx.MatchClear("this group's banner image"))
await ClearBannerImage();
else if (await ctx.MatchImage() is {} img)
else if (await ctx.MatchImage() is { } img)
await SetBannerImage(img);
else
await ShowBannerImage();
@ -319,10 +321,10 @@ namespace PluralKit.Bot
if (await ctx.MatchClear())
{
ctx.CheckOwnGroup(target);
var patch = new GroupPatch {Color = Partial<string>.Null()};
var patch = new GroupPatch { Color = Partial<string>.Null() };
await _db.Execute(conn => _repo.UpdateGroup(conn, target.Id, patch));
await ctx.Reply($"{Emojis.Success} Group color cleared.");
}
else if (!ctx.HasNext())
@ -349,8 +351,8 @@ namespace PluralKit.Bot
if (color.StartsWith("#")) color = color.Substring(1);
if (!Regex.IsMatch(color, "^[0-9a-fA-F]{6}$")) throw Errors.InvalidColorError(color);
var patch = new GroupPatch {Color = Partial<string>.Present(color.ToLowerInvariant())};
var patch = new GroupPatch { Color = Partial<string>.Present(color.ToLowerInvariant()) };
await _db.Execute(conn => _repo.UpdateGroup(conn, target.Id, patch));
await ctx.Reply(embed: new EmbedBuilder()
@ -368,9 +370,9 @@ namespace PluralKit.Bot
ctx.CheckSystem();
system = ctx.System;
}
ctx.CheckSystemPrivacy(system, system.GroupListPrivacy);
// TODO: integrate with the normal "search" system
await using var conn = await _db.Obtain();
@ -382,25 +384,25 @@ namespace PluralKit.Bot
else
throw new PKError("You do not have permission to access this information.");
}
var groups = (await conn.QueryGroupList(system.Id))
.Where(g => g.Visibility.CanAccess(pctx))
.OrderBy(g => g.Name, StringComparer.InvariantCultureIgnoreCase)
.ToList();
if (groups.Count == 0)
{
if (system.Id == ctx.System?.Id)
await ctx.Reply("This system has no groups. To create one, use the command `pk;group new <name>`.");
else
await ctx.Reply("This system has no groups.");
return;
}
var title = system.Name != null ? $"Groups of {system.Name} (`{system.Hid}`)" : $"Groups of `{system.Hid}`";
await ctx.Paginate(groups.ToAsyncEnumerable(), groups.Count, 25, title, ctx.System.Color, Renderer);
Task Renderer(EmbedBuilder eb, IEnumerable<ListedGroup> page)
{
eb.WithSimpleLineContent(page.Select(g =>
@ -430,15 +432,15 @@ namespace PluralKit.Bot
.Select(m => m.Id)
.Distinct()
.ToList();
await using var conn = await _db.Obtain();
var existingMembersInGroup = (await conn.QueryMemberList(target.System,
new DatabaseViewsExt.MemberListQueryOptions {GroupFilter = target.Id}))
new DatabaseViewsExt.MemberListQueryOptions { GroupFilter = target.Id }))
.Select(m => m.Id.Value)
.Distinct()
.ToHashSet();
List<MemberId> toAction;
if (op == AddRemoveOperation.Add)
@ -463,21 +465,21 @@ namespace PluralKit.Bot
public async Task ListGroupMembers(Context ctx, PKGroup target)
{
await using var conn = await _db.Obtain();
var targetSystem = await GetGroupSystem(ctx, target, conn);
ctx.CheckSystemPrivacy(targetSystem, target.ListPrivacy);
var opts = ctx.ParseMemberListOptions(ctx.LookupContextFor(target.System));
opts.GroupFilter = target.Id;
var title = new StringBuilder($"Members of {target.DisplayName ?? target.Name} (`{target.Hid}`) in ");
if (targetSystem.Name != null)
if (targetSystem.Name != null)
title.Append($"{targetSystem.Name} (`{targetSystem.Hid}`)");
else
title.Append($"`{targetSystem.Hid}`");
if (opts.Search != null)
if (opts.Search != null)
title.Append($" matching **{opts.Search}**");
await ctx.RenderMemberList(ctx.LookupContextFor(target.System), _db, target.System, title.ToString(), target.Color, opts);
}
@ -495,29 +497,29 @@ namespace PluralKit.Bot
{
await ctx.Reply(embed: new EmbedBuilder()
.Title($"Current privacy settings for {target.Name}")
.Field(new("Description", target.DescriptionPrivacy.Explanation()) )
.Field(new("Description", target.DescriptionPrivacy.Explanation()))
.Field(new("Icon", target.IconPrivacy.Explanation()))
.Field(new("Member list", target.ListPrivacy.Explanation()))
.Field(new("Visibility", target.Visibility.Explanation()))
.Description($"To edit privacy settings, use the command:\n> pk;group **{target.Reference()}** privacy **<subject>** **<level>**\n\n- `subject` is one of `description`, `icon`, `members`, `visibility`, or `all`\n- `level` is either `public` or `private`.")
.Build());
.Build());
return;
}
async Task SetAll(PrivacyLevel level)
{
await _db.Execute(c => _repo.UpdateGroup(c, target.Id, new GroupPatch().WithAllPrivacy(level)));
if (level == PrivacyLevel.Private)
await ctx.Reply($"{Emojis.Success} All {target.Name}'s privacy settings have been set to **{level.LevelName()}**. Other accounts will now see nothing on the group card.");
else
else
await ctx.Reply($"{Emojis.Success} All {target.Name}'s privacy settings have been set to **{level.LevelName()}**. Other accounts will now see everything on the group card.");
}
async Task SetLevel(GroupPrivacySubject subject, PrivacyLevel level)
{
await _db.Execute(c => _repo.UpdateGroup(c, target.Id, new GroupPatch().WithPrivacy(subject, level)));
var subjectName = subject switch
{
GroupPrivacySubject.Description => "description privacy",
@ -526,22 +528,22 @@ namespace PluralKit.Bot
GroupPrivacySubject.Visibility => "visibility",
_ => throw new ArgumentOutOfRangeException($"Unknown privacy subject {subject}")
};
var explanation = (subject, level) switch
{
(GroupPrivacySubject.Description, PrivacyLevel.Private) => "This group's description is now hidden from other systems.",
(GroupPrivacySubject.Icon, PrivacyLevel.Private) => "This group's icon is now hidden from other systems.",
(GroupPrivacySubject.Visibility, PrivacyLevel.Private) => "This group is now hidden from group lists and member cards.",
(GroupPrivacySubject.List, PrivacyLevel.Private) => "This group's member list is now hidden from other systems.",
(GroupPrivacySubject.Description, PrivacyLevel.Public) => "This group's description is no longer hidden from other systems.",
(GroupPrivacySubject.Icon, PrivacyLevel.Public) => "This group's icon is no longer hidden from other systems.",
(GroupPrivacySubject.Visibility, PrivacyLevel.Public) => "This group is no longer hidden from group lists and member cards.",
(GroupPrivacySubject.List, PrivacyLevel.Public) => "This group's member list is no longer hidden from other systems.",
_ => throw new InvalidOperationException($"Invalid subject/level tuple ({subject}, {level})")
};
await ctx.Reply($"{Emojis.Success} {target.Name}'s **{subjectName}** has been set to **{level.LevelName()}**. {explanation}");
}
@ -560,14 +562,14 @@ namespace PluralKit.Bot
throw new PKError($"Group deletion cancelled. Note that you must reply with your group ID (`{target.Hid}`) *verbatim*.");
await _db.Execute(conn => _repo.DeleteGroup(conn, target.Id));
await ctx.Reply($"{Emojis.Success} Group deleted.");
}
public async Task GroupFrontPercent(Context ctx, PKGroup target)
public async Task GroupFrontPercent(Context ctx, PKGroup target)
{
await using var conn = await _db.Obtain();
var targetSystem = await GetGroupSystem(ctx, target, conn);
ctx.CheckSystemPrivacy(targetSystem, targetSystem.FrontHistoryPrivacy);
@ -575,7 +577,7 @@ namespace PluralKit.Bot
if (totalSwitches == 0) throw Errors.NoRegisteredSwitches;
string durationStr = ctx.RemainderOrNull() ?? "30d";
var now = SystemClock.Instance.GetCurrentInstant();
var rangeStart = DateUtils.ParseDateTime(durationStr, true, targetSystem.Zone);
@ -583,7 +585,7 @@ namespace PluralKit.Bot
if (rangeStart.Value.ToInstant() > now) throw Errors.FrontPercentTimeInFuture;
var title = new StringBuilder($"Frontpercent of {target.DisplayName ?? target.Name} (`{target.Hid}`) in ");
if (targetSystem.Name != null)
if (targetSystem.Name != null)
title.Append($"{targetSystem.Name} (`{targetSystem.Hid}`)");
else
title.Append($"`{targetSystem.Hid}`");

View file

@ -15,7 +15,7 @@ namespace PluralKit.Bot
.Description("PluralKit is a bot designed for plural communities on Discord. It allows you to register systems, maintain system information, set up message proxying, log switches, and more.")
.Field(new("What is this for? What are systems?", "This bot detects messages with certain tags associated with a profile, then replaces that message under a \"pseudo-account\" of that profile using webhooks. This is useful for multiple people sharing one body (aka \"systems\"), people who wish to roleplay as different characters without having several accounts, or anyone else who may want to post messages as a different person from the same account."))
.Field(new("Why are people's names saying [BOT] next to them?", "These people are not actually bots, this is just a Discord limitation. See [the documentation](https://pluralkit.me/guide#proxying) for an in-depth explanation."))
.Field(new("How do I get started?", "To get started using PluralKit, try running the following commands (of course replacing the relevant names with your own):\n**1**. `pk;system new` - Create a system (if you haven't already)\n**2**. `pk;member add John` - Add a new member to your system\n**3**. `pk;member John proxy [text]` - Set up [square brackets] as proxy tags\n**4**. You're done! You can now type [a message in brackets] and it'll be proxied appropriately.\n**5**. Optionally, you may set an avatar from the URL of an image with `pk;member John avatar [link to image]`, or from a file by typing `pk;member John avatar` and sending the message with an attached image.\n\nSee [the Getting Started guide](https://pluralkit.me/start) for more information."))
.Field(new("How do I get started?", "To get started using PluralKit, try running the following commands (of course replacing the relevant names with your own):\n**1**. `pk;system new` - Create a system (if you haven't already)\n**2**. `pk;member add John` - Add a new member to your system\n**3**. `pk;member John proxy [text]` - Set up [square brackets] as proxy tags\n**4**. You're done! You can now type [a message in brackets] and it'll be proxied appropriately.\n**5**. Optionally, you may set an avatar from the URL of an image with `pk;member John avatar [link to image]`, or from a file by typing `pk;member John avatar` and sending the message with an attached image.\n\nSee [the Getting Started guide](https://pluralkit.me/start) for more information."))
.Field(new("Useful tips", $"React with {Emojis.Error} on a proxied message to delete it (only if you sent it!)\nReact with {Emojis.RedQuestion} on a proxied message to look up information about it (like who sent it)\nReact with {Emojis.Bell} on a proxied message to \"ping\" the sender\nType **`pk;invite`** to get a link to invite this bot to your own server!"))
.Field(new("More information", "For a full list of commands, see [the command list](https://pluralkit.me/commands).\nFor a more in-depth explanation of message proxying, see [the documentation](https://pluralkit.me/guide#proxying).\nIf you're an existing user of Tupperbox, type `pk;import` and attach a Tupperbox export file (from `tul!export`) to import your data from there."))
.Field(new("Support server", "We also have a Discord server for support, discussion, suggestions, announcements, etc: https://discord.gg/PczBt78"))

View file

@ -28,7 +28,7 @@ namespace PluralKit.Bot
// Otherwise it'll mess up/reformat the ISO strings for ???some??? reason >.>
DateParseHandling = DateParseHandling.None
};
public ImportExport(DataFileService dataFiles, HttpClient client)
{
_dataFiles = dataFiles;
@ -41,7 +41,7 @@ namespace PluralKit.Bot
if (url == null) throw Errors.NoImportFilePassed;
await ctx.BusyIndicator(async () =>
{
{
JObject data;
try
{
@ -68,10 +68,10 @@ namespace PluralKit.Bot
if (!await ctx.PromptYesNo(msg, "Proceed"))
throw Errors.ImportCancelled;
}
if (data.ContainsKey("accounts")
&& data.Value<JArray>("accounts").Type != JTokenType.Null
&& data.Value<JArray>("accounts").Contains((JToken) ctx.Author.Id.ToString()))
&& data.Value<JArray>("accounts").Contains((JToken)ctx.Author.Id.ToString()))
{
var msg = $"{Emojis.Warn} You seem to importing a system profile belonging to another account. Are you sure you want to proceed?";
if (!await ctx.PromptYesNo(msg, "Import")) throw Errors.ImportCancelled;
@ -95,15 +95,15 @@ namespace PluralKit.Bot
public async Task Export(Context ctx)
{
ctx.CheckSystem();
var json = await ctx.BusyIndicator(async () =>
{
// Make the actual data file
var data = await _dataFiles.ExportSystem(ctx.System);
return JsonConvert.SerializeObject(data, Formatting.None);
});
// Send it as a Discord attachment *in DMs*
var stream = new MemoryStream(Encoding.UTF8.GetBytes(json));
@ -112,10 +112,10 @@ namespace PluralKit.Bot
var dm = await ctx.Cache.GetOrCreateDmChannel(ctx.Rest, ctx.Author.Id);
var msg = await ctx.Rest.CreateMessage(dm.Id,
new MessageRequest {Content = $"{Emojis.Success} Here you go!"},
new[] {new MultipartFile("system.json", stream)});
new MessageRequest { Content = $"{Emojis.Success} Here you go!" },
new[] { new MultipartFile("system.json", stream) });
await ctx.Rest.CreateMessage(dm.Id, new MessageRequest { Content = $"<{msg.Attachments[0].Url}>" });
// If the original message wasn't posted in DMs, send a public reminder
if (ctx.Channel.Type != Channel.ChannelType.Dm)
await ctx.Reply($"{Emojis.Success} Check your DMs!");
@ -128,4 +128,4 @@ namespace PluralKit.Bot
}
}
}
}
}

View file

@ -1,4 +1,4 @@
using System.Collections.Generic;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
@ -18,15 +18,15 @@ namespace PluralKit.Bot
public static MemberListOptions ParseMemberListOptions(this Context ctx, LookupContext lookupCtx)
{
var p = new MemberListOptions();
// Short or long list? (parse this first, as it can potentially take a positional argument)
var isFull = ctx.Match("f", "full", "big", "details", "long") || ctx.MatchFlag("f", "full");
p.Type = isFull ? ListType.Long : ListType.Short;
// Search query
if (ctx.HasNext())
p.Search = ctx.RemainderOrNull();
// Include description in search?
if (ctx.MatchFlag("search-description", "filter-description", "in-description", "sd", "description", "desc"))
p.SearchDescription = true;
@ -47,7 +47,7 @@ namespace PluralKit.Bot
p.Reverse = true;
// Privacy filter (default is public only)
if (ctx.MatchFlag("a", "all")) p.PrivacyFilter = null;
if (ctx.MatchFlag("a", "all")) p.PrivacyFilter = null;
if (ctx.MatchFlag("private-only", "private", "priv")) p.PrivacyFilter = PrivacyLevel.Private;
if (ctx.MatchFlag("public-only", "public", "pub")) p.PrivacyFilter = PrivacyLevel.Public;
@ -55,7 +55,7 @@ namespace PluralKit.Bot
if (p.PrivacyFilter != PrivacyLevel.Public && lookupCtx != LookupContext.ByOwner)
// TODO: should this just return null instead of throwing or something? >.>
throw new PKError("You cannot look up private members of another system.");
// Additional fields to include in the search results
if (ctx.MatchFlag("with-last-switch", "with-last-fronted", "with-last-front", "wls", "wlf"))
p.IncludeLastSwitch = true;
@ -69,13 +69,13 @@ namespace PluralKit.Bot
p.IncludeAvatar = true;
if (ctx.MatchFlag("with-pronouns", "wp"))
p.IncludePronouns = true;
// Always show the sort property, too
if (p.SortProperty == SortProperty.LastSwitch) p.IncludeLastSwitch = true;
if (p.SortProperty == SortProperty.LastMessage) p.IncludeLastMessage= true;
if (p.SortProperty == SortProperty.LastMessage) p.IncludeLastMessage = true;
if (p.SortProperty == SortProperty.MessageCount) p.IncludeMessageCount = true;
if (p.SortProperty == SortProperty.CreationDate) p.IncludeCreated = true;
// Done!
return p;
}
@ -96,119 +96,126 @@ namespace PluralKit.Bot
{
// Add a global footer with the filter/sort string + result count
eb.Footer(new($"{opts.CreateFilterString()}. {"result".ToQuantity(members.Count)}."));
// Then call the specific renderers
if (opts.Type == ListType.Short)
ShortRenderer(eb, page);
else
LongRenderer(eb, page);
return Task.CompletedTask;
}
void ShortRenderer(EmbedBuilder eb, IEnumerable<ListedMember> page)
{
{
// We may end up over the description character limit
// so run it through a helper that "makes it work" :)
eb.WithSimpleLineContent(page.Select(m =>
{
var ret = $"[`{m.Hid}`] **{m.NameFor(ctx)}** ";
switch (opts.SortProperty) {
case SortProperty.Birthdate: {
var birthday = m.BirthdayFor(lookupCtx);
if (birthday != null)
ret += $"(birthday: {m.BirthdayString})";
break;
}
case SortProperty.MessageCount: {
if (m.MessageCountFor(lookupCtx) is {} count)
ret += $"({count} messages)";
break;
}
case SortProperty.LastSwitch: {
if (m.MetadataPrivacy.TryGet(lookupCtx, m.LastSwitchTime, out var lastSw))
ret += $"(last switched in: <t:{lastSw.Value.ToUnixTimeSeconds()}>)";
break;
}
case SortProperty.LastMessage: {
if (m.MetadataPrivacy.TryGet(lookupCtx, m.LastMessage, out var lastMsg))
ret += $"(last message: <t:{DiscordUtils.SnowflakeToInstant(lastMsg.Value).ToUnixTimeSeconds()}>)";
break;
}
case SortProperty.CreationDate: {
if (m.MetadataPrivacy.TryGet(lookupCtx, m.Created, out var created))
ret += $"(created at <t:{created.ToUnixTimeSeconds()}>)";
break;
}
default: {
if (opts.IncludeMessageCount && m.MessageCountFor(lookupCtx) is {} count)
ret += $"({count} messages)";
else if (opts.IncludeLastSwitch && m.MetadataPrivacy.TryGet(lookupCtx, m.LastSwitchTime, out var lastSw))
ret += $"(last switched in: <t:{lastSw.Value.ToUnixTimeSeconds()}>)";
else if (opts.IncludeLastMessage && m.MetadataPrivacy.TryGet(lookupCtx, m.LastMessage, out var lastMsg))
ret += $"(last message: <t:{DiscordUtils.SnowflakeToInstant(lastMsg.Value).ToUnixTimeSeconds()}>)";
else if (opts.IncludeCreated && m.MetadataPrivacy.TryGet(lookupCtx, m.Created, out var created))
ret += $"(created at <t:{created.ToUnixTimeSeconds()}>)";
else if (opts.IncludePronouns && m.PronounsFor(lookupCtx) is {} pronouns)
ret += $"({pronouns})";
else if (m.HasProxyTags)
switch (opts.SortProperty)
{
case SortProperty.Birthdate:
{
var proxyTagsString = m.ProxyTagsString();
if (proxyTagsString.Length > 100) // arbitrary threshold for now, tweak?
proxyTagsString = "tags too long, see member card";
ret += $"*(*{proxyTagsString}*)*";
var birthday = m.BirthdayFor(lookupCtx);
if (birthday != null)
ret += $"(birthday: {m.BirthdayString})";
break;
}
case SortProperty.MessageCount:
{
if (m.MessageCountFor(lookupCtx) is { } count)
ret += $"({count} messages)";
break;
}
case SortProperty.LastSwitch:
{
if (m.MetadataPrivacy.TryGet(lookupCtx, m.LastSwitchTime, out var lastSw))
ret += $"(last switched in: <t:{lastSw.Value.ToUnixTimeSeconds()}>)";
break;
}
case SortProperty.LastMessage:
{
if (m.MetadataPrivacy.TryGet(lookupCtx, m.LastMessage, out var lastMsg))
ret += $"(last message: <t:{DiscordUtils.SnowflakeToInstant(lastMsg.Value).ToUnixTimeSeconds()}>)";
break;
}
case SortProperty.CreationDate:
{
if (m.MetadataPrivacy.TryGet(lookupCtx, m.Created, out var created))
ret += $"(created at <t:{created.ToUnixTimeSeconds()}>)";
break;
}
default:
{
if (opts.IncludeMessageCount && m.MessageCountFor(lookupCtx) is { } count)
ret += $"({count} messages)";
else if (opts.IncludeLastSwitch && m.MetadataPrivacy.TryGet(lookupCtx, m.LastSwitchTime, out var lastSw))
ret += $"(last switched in: <t:{lastSw.Value.ToUnixTimeSeconds()}>)";
else if (opts.IncludeLastMessage && m.MetadataPrivacy.TryGet(lookupCtx, m.LastMessage, out var lastMsg))
ret += $"(last message: <t:{DiscordUtils.SnowflakeToInstant(lastMsg.Value).ToUnixTimeSeconds()}>)";
else if (opts.IncludeCreated && m.MetadataPrivacy.TryGet(lookupCtx, m.Created, out var created))
ret += $"(created at <t:{created.ToUnixTimeSeconds()}>)";
else if (opts.IncludePronouns && m.PronounsFor(lookupCtx) is { } pronouns)
ret += $"({pronouns})";
else if (m.HasProxyTags)
{
var proxyTagsString = m.ProxyTagsString();
if (proxyTagsString.Length > 100) // arbitrary threshold for now, tweak?
proxyTagsString = "tags too long, see member card";
ret += $"*(*{proxyTagsString}*)*";
}
break;
}
break;
}
}
return ret;
}));
}
void LongRenderer(EmbedBuilder eb, IEnumerable<ListedMember> page)
{
var zone = ctx.System?.Zone ?? DateTimeZone.Utc;
foreach (var m in page)
{
var profile = new StringBuilder($"**ID**: {m.Hid}");
if (m.DisplayName != null && m.NamePrivacy.CanAccess(lookupCtx))
profile.Append($"\n**Display name**: {m.DisplayName}");
if (m.PronounsFor(lookupCtx) is {} pronouns)
if (m.PronounsFor(lookupCtx) is { } pronouns)
profile.Append($"\n**Pronouns**: {pronouns}");
if (m.BirthdayFor(lookupCtx) != null)
if (m.BirthdayFor(lookupCtx) != null)
profile.Append($"\n**Birthdate**: {m.BirthdayString}");
if (m.ProxyTags.Count > 0)
if (m.ProxyTags.Count > 0)
profile.Append($"\n**Proxy tags**: {m.ProxyTagsString()}");
if ((opts.IncludeMessageCount || opts.SortProperty == SortProperty.MessageCount) && m.MessageCountFor(lookupCtx) is {} count && count > 0)
if ((opts.IncludeMessageCount || opts.SortProperty == SortProperty.MessageCount) && m.MessageCountFor(lookupCtx) is { } count && count > 0)
profile.Append($"\n**Message count:** {count}");
if ((opts.IncludeLastMessage || opts.SortProperty == SortProperty.LastMessage) && m.MetadataPrivacy.TryGet(lookupCtx, m.LastMessage, out var lastMsg))
profile.Append($"\n**Last message:** {DiscordUtils.SnowflakeToInstant(lastMsg.Value).FormatZoned(zone)}");
if ((opts.IncludeLastSwitch || opts.SortProperty == SortProperty.LastSwitch) && m.MetadataPrivacy.TryGet(lookupCtx, m.LastSwitchTime, out var lastSw))
profile.Append($"\n**Last switched in:** {lastSw.Value.FormatZoned(zone)}");
if ((opts.IncludeCreated || opts.SortProperty == SortProperty.CreationDate) && m.MetadataPrivacy.TryGet(lookupCtx, m.Created, out var created))
profile.Append($"\n**Created on:** {created.FormatZoned(zone)}");
if (opts.IncludeAvatar && m.AvatarFor(lookupCtx) is {} avatar)
if (opts.IncludeAvatar && m.AvatarFor(lookupCtx) is { } avatar)
profile.Append($"\n**Avatar URL:** {avatar.TryGetCleanCdnUrl()}");
if (m.DescriptionFor(lookupCtx) is {} desc)
if (m.DescriptionFor(lookupCtx) is { } desc)
profile.Append($"\n\n{desc}");
if (m.MemberVisibility == PrivacyLevel.Private)
profile.Append("\n*(this member is hidden)*");
eb.Field(new(m.NameFor(ctx), profile.ToString().Truncate(1024)));
}
}
}
}
}
}

View file

@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
@ -19,7 +19,7 @@ namespace PluralKit.Bot
public GroupId? GroupFilter { get; set; }
public string? Search { get; set; }
public bool SearchDescription { get; set; }
public ListType Type { get; set; }
public bool IncludeMessageCount { get; set; }
public bool IncludeLastSwitch { get; set; }
@ -27,7 +27,7 @@ namespace PluralKit.Bot
public bool IncludeCreated { get; set; }
public bool IncludeAvatar { get; set; }
public bool IncludePronouns { get; set; }
public string CreateFilterString()
{
var str = new StringBuilder();
@ -46,7 +46,7 @@ namespace PluralKit.Bot
SortProperty.Random => "randomly",
_ => new ArgumentOutOfRangeException($"Couldn't find readable string for sort property {SortProperty}")
});
if (Search != null)
{
str.Append($", searching for \"{Search}\"");
@ -67,7 +67,7 @@ namespace PluralKit.Bot
public DatabaseViewsExt.MemberListQueryOptions ToQueryOptions() =>
new DatabaseViewsExt.MemberListQueryOptions
{
PrivacyFilter = PrivacyFilter,
PrivacyFilter = PrivacyFilter,
GroupFilter = GroupFilter,
Search = Search,
SearchDescription = SearchDescription
@ -80,7 +80,7 @@ namespace PluralKit.Bot
{
IComparer<T> ReverseMaybe<T>(IComparer<T> c) =>
opts.Reverse ? Comparer<T>.Create((a, b) => c.Compare(b, a)) : c;
var randGen = new global::System.Random();
var culture = StringComparer.InvariantCultureIgnoreCase;
@ -112,7 +112,7 @@ namespace PluralKit.Bot
.ThenBy(m => m.NameFor(ctx), culture);
}
}
public enum SortProperty
{
Name,

View file

@ -21,7 +21,7 @@ namespace PluralKit.Bot
private readonly ModelRepository _repo;
private readonly EmbedService _embeds;
private readonly HttpClient _client;
public Member(EmbedService embeds, IDatabase db, ModelRepository repo, HttpClient client)
{
_embeds = embeds;
@ -30,16 +30,18 @@ namespace PluralKit.Bot
_client = client;
}
public async Task NewMember(Context ctx) {
public async Task NewMember(Context ctx)
{
if (ctx.System == null) throw Errors.NoSystemError;
var memberName = ctx.RemainderOrNull() ?? throw new PKSyntaxError("You must pass a member name.");
// Hard name length cap
if (memberName.Length > Limits.MaxMemberNameLength) throw Errors.MemberNameTooLongError(memberName.Length);
// Warn if there's already a member by this name
var existingMember = await _db.Execute(c => _repo.GetMemberByName(c, ctx.System.Id, memberName));
if (existingMember != null) {
if (existingMember != null)
{
var msg = $"{Emojis.Warn} You already have a member in your system with the name \"{existingMember.NameFor(ctx)}\" (with ID `{existingMember.Hid}`). Do you want to create another member with the same name?";
if (!await ctx.PromptYesNo(msg, "Create")) throw new PKError("Member creation cancelled.");
}
@ -61,10 +63,13 @@ namespace PluralKit.Bot
Exception imageMatchError = null;
if (avatarArg != null)
{
try {
try
{
await AvatarUtils.VerifyAvatarOrThrow(_client, avatarArg.Url);
await _db.Execute(conn => _repo.UpdateMember(conn, member.Id, new MemberPatch { AvatarUrl = avatarArg.Url }));
} catch (Exception e) {
}
catch (Exception e)
{
imageMatchError = e;
}
}
@ -72,7 +77,7 @@ namespace PluralKit.Bot
// Send confirmation and space hint
await ctx.Reply($"{Emojis.Success} Member \"{memberName}\" (`{member.Hid}`) registered! Check out the getting started page for how to get a member up and running: https://pluralkit.me/start#create-a-member");
if (await _db.Execute(conn => conn.QuerySingleAsync<bool>("select has_private_members(@System)",
new {System = ctx.System.Id}))) //if has private members
new { System = ctx.System.Id }))) //if has private members
await ctx.Reply($"{Emojis.Warn} This member is currently **public**. To change this, use `pk;member {member.Hid} private`.");
if (avatarArg != null)
if (imageMatchError == null)
@ -86,7 +91,7 @@ namespace PluralKit.Bot
else if (memberCount >= Limits.MaxMembersWarnThreshold(memberLimit))
await ctx.Reply($"{Emojis.Warn} You are approaching the per-system member limit ({memberCount} / {memberLimit} members). Please review your member list for unused or duplicate members.");
}
public async Task ViewMember(Context ctx, PKMember target)
{
var system = await _db.Execute(c => _repo.GetSystem(c, target.System));
@ -96,10 +101,10 @@ namespace PluralKit.Bot
public async Task Soulscream(Context ctx, PKMember target)
{
// this is for a meme, please don't take this code seriously. :)
var name = target.NameFor(ctx.LookupContextFor(target));
var encoded = HttpUtility.UrlEncode(name);
var resp = await _client.GetAsync($"https://onomancer.sibr.dev/api/generateStats2?name={encoded}");
if (resp.StatusCode != HttpStatusCode.OK)
// lol
@ -116,4 +121,4 @@ namespace PluralKit.Bot
await ctx.Reply(embed: eb.Build());
}
}
}
}

View file

@ -21,7 +21,7 @@ namespace PluralKit.Bot
_repo = repo;
_client = client;
}
private async Task AvatarClear(AvatarLocation location, Context ctx, PKMember target, MemberGuildSettings? mgs)
{
await UpdateAvatar(location, ctx, target, null);
@ -36,7 +36,7 @@ namespace PluralKit.Bot
{
if (mgs?.AvatarUrl != null)
await ctx.Reply($"{Emojis.Success} Member avatar cleared. Note that this member has a server-specific avatar set here, type `pk;member {target.Reference()} serveravatar clear` if you wish to clear that too.");
else
else
await ctx.Reply($"{Emojis.Success} Member avatar cleared.");
}
}
@ -57,10 +57,10 @@ namespace PluralKit.Bot
if (location == AvatarLocation.Server)
throw new PKError($"This member does not have a server avatar set. Type `pk;member {target.Reference()} avatar` to see their global avatar.");
}
var field = location == AvatarLocation.Server ? $"server avatar (for {ctx.Guild.Name})" : "avatar";
var cmd = location == AvatarLocation.Server ? "serveravatar" : "avatar";
var eb = new EmbedBuilder()
.Title($"{target.NameFor(ctx)}'s {field}")
.Image(new(currentValue?.TryGetCleanCdnUrl()));
@ -75,7 +75,7 @@ namespace PluralKit.Bot
var guildData = await _db.Execute(c => _repo.GetMemberGuild(c, ctx.Guild.Id, target.Id));
await AvatarCommandTree(AvatarLocation.Server, ctx, target, guildData);
}
public async Task Avatar(Context ctx, PKMember target)
{
var guildData = ctx.Guild != null ?
@ -119,7 +119,7 @@ namespace PluralKit.Bot
AvatarLocation.Member => "avatar",
_ => throw new ArgumentOutOfRangeException(nameof(location))
};
var serverFrag = location switch
{
AvatarLocation.Server => $" This avatar will now be used when proxying in this server (**{ctx.Guild.Name}**).",
@ -137,8 +137,8 @@ namespace PluralKit.Bot
// The attachment's already right there, no need to preview it.
var hasEmbed = avatar.Source != AvatarSource.Attachment;
return hasEmbed
? ctx.Reply(msg, embed: new EmbedBuilder().Image(new(avatar.Url)).Build())
return hasEmbed
? ctx.Reply(msg, embed: new EmbedBuilder().Image(new(avatar.Url)).Build())
: ctx.Reply(msg);
}

View file

@ -24,10 +24,10 @@ namespace PluralKit.Bot
_client = client;
}
public async Task Name(Context ctx, PKMember target)
public async Task Name(Context ctx, PKMember target)
{
ctx.CheckSystem().CheckOwnMember(target);
ctx.CheckSystem().CheckOwnMember(target);
var newName = ctx.RemainderOrNull() ?? throw new PKSyntaxError("You must pass a new name for the member.");
// Hard name length cap
@ -35,14 +35,14 @@ namespace PluralKit.Bot
// Warn if there's already a member by this name
var existingMember = await _db.Execute(conn => _repo.GetMemberByName(conn, ctx.System.Id, newName));
if (existingMember != null && existingMember.Id != target.Id)
if (existingMember != null && existingMember.Id != target.Id)
{
var msg = $"{Emojis.Warn} You already have a member in your system with the name \"{existingMember.NameFor(ctx)}\" (`{existingMember.Hid}`). Do you want to rename this member to that name too?";
if (!await ctx.PromptYesNo(msg, "Rename")) throw new PKError("Member renaming cancelled.");
}
// Rename the member
var patch = new MemberPatch {Name = Partial<string>.Present(newName)};
var patch = new MemberPatch { Name = Partial<string>.Present(newName) };
await _db.Execute(conn => _repo.UpdateMember(conn, target.Id, patch));
await ctx.Reply($"{Emojis.Success} Member renamed.");
@ -57,15 +57,16 @@ namespace PluralKit.Bot
}
}
public async Task Description(Context ctx, PKMember target) {
public async Task Description(Context ctx, PKMember target)
{
if (await ctx.MatchClear("this member's description"))
{
ctx.CheckOwnMember(target);
var patch = new MemberPatch {Description = Partial<string>.Null()};
var patch = new MemberPatch { Description = Partial<string>.Null() };
await _db.Execute(conn => _repo.UpdateMember(conn, target.Id, patch));
await ctx.Reply($"{Emojis.Success} Member description cleared.");
}
}
else if (!ctx.HasNext())
{
if (!target.DescriptionPrivacy.CanAccess(ctx.LookupContextFor(target.System)))
@ -81,7 +82,7 @@ namespace PluralKit.Bot
await ctx.Reply(embed: new EmbedBuilder()
.Title("Member description")
.Description(target.Description)
.Field(new("\u200B", $"To print the description with formatting, type `pk;member {target.Reference()} description -raw`."
.Field(new("\u200B", $"To print the description with formatting, type `pk;member {target.Reference()} description -raw`."
+ (ctx.System?.Id == target.System ? $" To clear it, type `pk;member {target.Reference()} description -clear`." : "")))
.Build());
}
@ -92,23 +93,24 @@ namespace PluralKit.Bot
var description = ctx.RemainderOrNull().NormalizeLineEndSpacing();
if (description.IsLongerThan(Limits.MaxDescriptionLength))
throw Errors.DescriptionTooLongError(description.Length);
var patch = new MemberPatch {Description = Partial<string>.Present(description)};
var patch = new MemberPatch { Description = Partial<string>.Present(description) };
await _db.Execute(conn => _repo.UpdateMember(conn, target.Id, patch));
await ctx.Reply($"{Emojis.Success} Member description changed.");
}
}
public async Task Pronouns(Context ctx, PKMember target) {
public async Task Pronouns(Context ctx, PKMember target)
{
if (await ctx.MatchClear("this member's pronouns"))
{
ctx.CheckOwnMember(target);
var patch = new MemberPatch {Pronouns = Partial<string>.Null()};
var patch = new MemberPatch { Pronouns = Partial<string>.Null() };
await _db.Execute(conn => _repo.UpdateMember(conn, target.Id, patch));
await ctx.Reply($"{Emojis.Success} Member pronouns cleared.");
}
}
else if (!ctx.HasNext())
{
if (!target.PronounPrivacy.CanAccess(ctx.LookupContextFor(target.System)))
@ -131,10 +133,10 @@ namespace PluralKit.Bot
var pronouns = ctx.RemainderOrNull().NormalizeLineEndSpacing();
if (pronouns.IsLongerThan(Limits.MaxPronounsLength))
throw Errors.MemberPronounsTooLongError(pronouns.Length);
var patch = new MemberPatch {Pronouns = Partial<string>.Present(pronouns)};
var patch = new MemberPatch { Pronouns = Partial<string>.Present(pronouns) };
await _db.Execute(conn => _repo.UpdateMember(conn, target.Id, patch));
await ctx.Reply($"{Emojis.Success} Member pronouns changed.");
}
}
@ -145,7 +147,7 @@ namespace PluralKit.Bot
async Task ClearBannerImage()
{
await _db.Execute(c => _repo.UpdateMember(c, target.Id, new MemberPatch {BannerImage = null}));
await _db.Execute(c => _repo.UpdateMember(c, target.Id, new MemberPatch { BannerImage = null }));
await ctx.Reply($"{Emojis.Success} Member banner image cleared.");
}
@ -153,7 +155,7 @@ namespace PluralKit.Bot
{
await AvatarUtils.VerifyAvatarOrThrow(_client, img.Url, isFullSizeImage: true);
await _db.Execute(c => _repo.UpdateMember(c, target.Id, new MemberPatch {BannerImage = img.Url}));
await _db.Execute(c => _repo.UpdateMember(c, target.Id, new MemberPatch { BannerImage = img.Url }));
var msg = img.Source switch
{
@ -165,8 +167,8 @@ namespace PluralKit.Bot
// The attachment's already right there, no need to preview it.
var hasEmbed = img.Source != AvatarSource.Attachment;
await (hasEmbed
? ctx.Reply(msg, embed: new EmbedBuilder().Image(new(img.Url)).Build())
await (hasEmbed
? ctx.Reply(msg, embed: new EmbedBuilder().Image(new(img.Url)).Build())
: ctx.Reply(msg));
}
@ -186,7 +188,7 @@ namespace PluralKit.Bot
if (await ctx.MatchClear("this member's banner image"))
await ClearBannerImage();
else if (await ctx.MatchImage() is {} img)
else if (await ctx.MatchImage() is { } img)
await SetBannerImage(img);
else
await ShowBannerImage();
@ -198,10 +200,10 @@ namespace PluralKit.Bot
if (await ctx.MatchClear())
{
ctx.CheckOwnMember(target);
var patch = new MemberPatch {Color = Partial<string>.Null()};
var patch = new MemberPatch { Color = Partial<string>.Null() };
await _db.Execute(conn => _repo.UpdateMember(conn, target.Id, patch));
await ctx.Reply($"{Emojis.Success} Member color cleared.");
}
else if (!ctx.HasNext())
@ -230,8 +232,8 @@ namespace PluralKit.Bot
if (color.StartsWith("#")) color = color.Substring(1);
if (!Regex.IsMatch(color, "^[0-9a-fA-F]{6}$")) throw Errors.InvalidColorError(color);
var patch = new MemberPatch {Color = Partial<string>.Present(color.ToLowerInvariant())};
var patch = new MemberPatch { Color = Partial<string>.Present(color.ToLowerInvariant()) };
await _db.Execute(conn => _repo.UpdateMember(conn, target.Id, patch));
await ctx.Reply(embed: new EmbedBuilder()
@ -246,17 +248,17 @@ namespace PluralKit.Bot
if (await ctx.MatchClear("this member's birthday"))
{
ctx.CheckOwnMember(target);
var patch = new MemberPatch {Birthday = Partial<LocalDate?>.Null()};
var patch = new MemberPatch { Birthday = Partial<LocalDate?>.Null() };
await _db.Execute(conn => _repo.UpdateMember(conn, target.Id, patch));
await ctx.Reply($"{Emojis.Success} Member birthdate cleared.");
}
}
else if (!ctx.HasNext())
{
if (!target.BirthdayPrivacy.CanAccess(ctx.LookupContextFor(target.System)))
throw Errors.LookupNotAllowed;
if (target.Birthday == null)
await ctx.Reply("This member does not have a birthdate set."
+ (ctx.System?.Id == target.System ? $" To set one, type `pk;member {target.Reference()} birthdate <birthdate>`." : ""));
@ -267,22 +269,22 @@ namespace PluralKit.Bot
else
{
ctx.CheckOwnMember(target);
var birthdayStr = ctx.RemainderOrNull();
var birthday = DateUtils.ParseDate(birthdayStr, true);
if (birthday == null) throw Errors.BirthdayParseError(birthdayStr);
var patch = new MemberPatch {Birthday = Partial<LocalDate?>.Present(birthday)};
var patch = new MemberPatch { Birthday = Partial<LocalDate?>.Present(birthday) };
await _db.Execute(conn => _repo.UpdateMember(conn, target.Id, patch));
await ctx.Reply($"{Emojis.Success} Member birthdate changed.");
}
}
private async Task<EmbedBuilder> CreateMemberNameInfoEmbed(Context ctx, PKMember target)
{
var lcx = ctx.LookupContextFor(target);
MemberGuildSettings memberGuildConfig = null;
if (ctx.Guild != null)
memberGuildConfig = await _db.Execute(c => _repo.GetMemberGuild(c, ctx.Guild.Id, target.Id));
@ -329,12 +331,12 @@ namespace PluralKit.Bot
await ctx.Reply(successStr);
}
if (await ctx.MatchClear("this member's display name"))
{
ctx.CheckOwnMember(target);
var patch = new MemberPatch {DisplayName = Partial<string>.Null()};
var patch = new MemberPatch { DisplayName = Partial<string>.Null() };
await _db.Execute(conn => _repo.UpdateMember(conn, target.Id, patch));
await PrintSuccess($"{Emojis.Success} Member display name cleared. This member will now be proxied using their member name \"{target.NameFor(ctx)}\".");
@ -365,25 +367,25 @@ namespace PluralKit.Bot
else
{
ctx.CheckOwnMember(target);
var newDisplayName = ctx.RemainderOrNull();
var patch = new MemberPatch {DisplayName = Partial<string>.Present(newDisplayName)};
var patch = new MemberPatch { DisplayName = Partial<string>.Present(newDisplayName) };
await _db.Execute(conn => _repo.UpdateMember(conn, target.Id, patch));
await PrintSuccess($"{Emojis.Success} Member display name changed. This member will now be proxied using the name \"{newDisplayName}\".");
}
}
public async Task ServerName(Context ctx, PKMember target)
{
ctx.CheckGuildContext();
if (await ctx.MatchClear("this member's server name"))
{
ctx.CheckOwnMember(target);
var patch = new MemberGuildPatch {DisplayName = null};
var patch = new MemberGuildPatch { DisplayName = null };
await _db.Execute(conn => _repo.UpsertMemberGuild(conn, target.Id, ctx.Guild.Id, patch));
if (target.DisplayName != null)
@ -419,16 +421,16 @@ namespace PluralKit.Bot
else
{
ctx.CheckOwnMember(target);
var newServerName = ctx.RemainderOrNull();
var patch = new MemberGuildPatch {DisplayName = newServerName};
var patch = new MemberGuildPatch { DisplayName = newServerName };
await _db.Execute(conn => _repo.UpsertMemberGuild(conn, target.Id, ctx.Guild.Id, patch));
await ctx.Reply($"{Emojis.Success} Member server name changed. This member will now be proxied using the name \"{newServerName}\" in this server ({ctx.Guild.Name}).");
}
}
public async Task KeepProxy(Context ctx, PKMember target)
{
ctx.CheckSystem().CheckOwnMember(target);
@ -446,9 +448,9 @@ namespace PluralKit.Bot
return;
};
var patch = new MemberPatch {KeepProxy = Partial<bool>.Present(newValue)};
var patch = new MemberPatch { KeepProxy = Partial<bool>.Present(newValue) };
await _db.Execute(conn => _repo.UpdateMember(conn, target.Id, patch));
if (newValue)
await ctx.Reply($"{Emojis.Success} Member proxy tags will now be included in the resulting message when proxying.");
else
@ -473,7 +475,7 @@ namespace PluralKit.Bot
return;
};
var patch = new MemberPatch {AllowAutoproxy = Partial<bool>.Present(newValue)};
var patch = new MemberPatch { AllowAutoproxy = Partial<bool>.Present(newValue) };
await _db.Execute(conn => _repo.UpdateMember(conn, target.Id, patch));
if (newValue)
@ -491,18 +493,18 @@ namespace PluralKit.Bot
{
await ctx.Reply(embed: new EmbedBuilder()
.Title($"Current privacy settings for {target.NameFor(ctx)}")
.Field(new("Name (replaces name with display name if member has one)",target.NamePrivacy.Explanation()))
.Field(new("Name (replaces name with display name if member has one)", target.NamePrivacy.Explanation()))
.Field(new("Description", target.DescriptionPrivacy.Explanation()))
.Field(new("Avatar", target.AvatarPrivacy.Explanation()))
.Field(new("Birthday", target.BirthdayPrivacy.Explanation()))
.Field(new("Pronouns", target.PronounPrivacy.Explanation()))
.Field(new("Meta (message count, last front, last message)",target.MetadataPrivacy.Explanation()))
.Field(new("Meta (message count, last front, last message)", target.MetadataPrivacy.Explanation()))
.Field(new("Visibility", target.MemberVisibility.Explanation()))
.Description("To edit privacy settings, use the command:\n`pk;member <member> privacy <subject> <level>`\n\n- `subject` is one of `name`, `description`, `avatar`, `birthday`, `pronouns`, `created`, `messages`, `visibility`, or `all`\n- `level` is either `public` or `private`.")
.Build());
.Build());
return;
}
// Get guild settings (mostly for warnings and such)
MemberGuildSettings guildSettings = null;
if (ctx.Guild != null)
@ -511,17 +513,17 @@ namespace PluralKit.Bot
async Task SetAll(PrivacyLevel level)
{
await _db.Execute(c => _repo.UpdateMember(c, target.Id, new MemberPatch().WithAllPrivacy(level)));
if (level == PrivacyLevel.Private)
await ctx.Reply($"{Emojis.Success} All {target.NameFor(ctx)}'s privacy settings have been set to **{level.LevelName()}**. Other accounts will now see nothing on the member card.");
else
else
await ctx.Reply($"{Emojis.Success} All {target.NameFor(ctx)}'s privacy settings have been set to **{level.LevelName()}**. Other accounts will now see everything on the member card.");
}
async Task SetLevel(MemberPrivacySubject subject, PrivacyLevel level)
{
await _db.Execute(c => _repo.UpdateMember(c, target.Id, new MemberPatch().WithPrivacy(subject, level)));
var subjectName = subject switch
{
MemberPrivacySubject.Name => "name privacy",
@ -533,7 +535,7 @@ namespace PluralKit.Bot
MemberPrivacySubject.Visibility => "visibility",
_ => throw new ArgumentOutOfRangeException($"Unknown privacy subject {subject}")
};
var explanation = (subject, level) switch
{
(MemberPrivacySubject.Name, PrivacyLevel.Private) => "This member's name is now hidden from other systems, and will be replaced by the member's display name.",
@ -543,7 +545,7 @@ namespace PluralKit.Bot
(MemberPrivacySubject.Pronouns, PrivacyLevel.Private) => "This member's pronouns are now hidden from other systems.",
(MemberPrivacySubject.Metadata, PrivacyLevel.Private) => "This member's metadata (eg. created timestamp, message count, etc) is now hidden from other systems.",
(MemberPrivacySubject.Visibility, PrivacyLevel.Private) => "This member is now hidden from member lists.",
(MemberPrivacySubject.Name, PrivacyLevel.Public) => "This member's name is no longer hidden from other systems.",
(MemberPrivacySubject.Description, PrivacyLevel.Public) => "This member's description is no longer hidden from other systems.",
(MemberPrivacySubject.Avatar, PrivacyLevel.Public) => "This member's avatar is no longer hidden from other systems.",
@ -551,16 +553,16 @@ namespace PluralKit.Bot
(MemberPrivacySubject.Pronouns, PrivacyLevel.Public) => "This member's pronouns are no longer hidden other systems.",
(MemberPrivacySubject.Metadata, PrivacyLevel.Public) => "This member's metadata (eg. created timestamp, message count, etc) is no longer hidden from other systems.",
(MemberPrivacySubject.Visibility, PrivacyLevel.Public) => "This member is no longer hidden from member lists.",
_ => throw new InvalidOperationException($"Invalid subject/level tuple ({subject}, {level})")
};
await ctx.Reply($"{Emojis.Success} {target.NameFor(ctx)}'s **{subjectName}** has been set to **{level.LevelName()}**. {explanation}");
// Name privacy only works given a display name
if (subject == MemberPrivacySubject.Name && level == PrivacyLevel.Private && target.DisplayName == null)
await ctx.Reply($"{Emojis.Warn} This member does not have a display name set, and name privacy **will not take effect**.");
// Avatar privacy doesn't apply when proxying if no server avatar is set
if (subject == MemberPrivacySubject.Avatar && level == PrivacyLevel.Private && guildSettings?.AvatarUrl == null)
await ctx.Reply($"{Emojis.Warn} This member does not have a server avatar set, so *proxying* will **still show the member avatar**. If you want to hide your avatar when proxying here, set a server avatar: `pk;member {target.Reference()} serveravatar`");
@ -571,17 +573,17 @@ namespace PluralKit.Bot
else
await SetLevel(ctx.PopMemberPrivacySubject(), ctx.PopPrivacyLevel());
}
public async Task Delete(Context ctx, PKMember target)
{
ctx.CheckSystem().CheckOwnMember(target);
await ctx.Reply($"{Emojis.Warn} Are you sure you want to delete \"{target.NameFor(ctx)}\"? If so, reply to this message with the member's ID (`{target.Hid}`). __***This cannot be undone!***__");
if (!await ctx.ConfirmWithReply(target.Hid)) throw Errors.MemberDeleteCancelled;
await _db.Execute(conn => _repo.DeleteMember(conn, target.Id));
await ctx.Reply($"{Emojis.Success} Member deleted.");
}
}
}
}

View file

@ -76,14 +76,14 @@ namespace PluralKit.Bot
description = "This member has no groups.";
else
description = string.Join("\n", groups.Select(g => $"[`{g.Hid}`] **{g.DisplayName ?? g.Name}**"));
if (pctx == LookupContext.ByOwner)
{
msg += $"\n\nTo add this member to one or more groups, use `pk;m {target.Reference()} group add <group> [group 2] [group 3...]`";
if (groups.Count > 0)
msg += $"\nTo remove this member from one or more groups, use `pk;m {target.Reference()} group remove <group> [group 2] [group 3...]`";
}
await ctx.Reply(msg, (new EmbedBuilder().Title($"{target.Name}'s groups").Description(description)).Build());
}
}

View file

@ -11,7 +11,7 @@ namespace PluralKit.Bot
{
private readonly IDatabase _db;
private readonly ModelRepository _repo;
public MemberProxy(IDatabase db, ModelRepository repo)
{
_db = db;
@ -31,20 +31,20 @@ namespace PluralKit.Bot
if (prefixAndSuffix.Length > 2) throw Errors.ProxyMultipleText;
return new ProxyTag(prefixAndSuffix[0], prefixAndSuffix[1]);
}
async Task<bool> WarnOnConflict(ProxyTag newTag)
{
var query = "select * from (select *, (unnest(proxy_tags)).prefix as prefix, (unnest(proxy_tags)).suffix as suffix from members where system = @System) as _ where prefix is not distinct from @Prefix and suffix is not distinct from @Suffix and id != @Existing";
var conflicts = (await _db.Execute(conn => conn.QueryAsync<PKMember>(query,
new {Prefix = newTag.Prefix, Suffix = newTag.Suffix, Existing = target.Id, system = target.System}))).ToList();
new { Prefix = newTag.Prefix, Suffix = newTag.Suffix, Existing = target.Id, system = target.System }))).ToList();
if (conflicts.Count <= 0) return true;
var conflictList = conflicts.Select(m => $"- **{m.NameFor(ctx)}**");
var msg = $"{Emojis.Warn} The following members have conflicting proxy tags:\n{string.Join('\n', conflictList)}\nDo you want to proceed anyway?";
return await ctx.PromptYesNo(msg, "Proceed");
}
// "Sub"command: clear flag
if (await ctx.MatchClear())
{
@ -55,10 +55,10 @@ namespace PluralKit.Bot
if (!await ctx.PromptYesNo(msg, "Clear"))
throw Errors.GenericCancelled();
}
var patch = new MemberPatch {ProxyTags = Partial<ProxyTag[]>.Present(new ProxyTag[0])};
var patch = new MemberPatch { ProxyTags = Partial<ProxyTag[]>.Present(new ProxyTag[0]) };
await _db.Execute(conn => _repo.UpdateMember(conn, target.Id, patch));
await ctx.Reply($"{Emojis.Success} Proxy tags cleared.");
}
// "Sub"command: no arguments; will print proxy tags
@ -73,20 +73,20 @@ namespace PluralKit.Bot
else if (ctx.Match("add", "append"))
{
if (!ctx.HasNext(skipFlags: false)) throw new PKSyntaxError("You must pass an example proxy to add (eg. `[text]` or `J:text`).");
var tagToAdd = ParseProxyTags(ctx.RemainderOrNull(skipFlags: false));
if (tagToAdd.IsEmpty) throw Errors.EmptyProxyTags(target);
if (target.ProxyTags.Contains(tagToAdd))
throw Errors.ProxyTagAlreadyExists(tagToAdd, target);
if (tagToAdd.ProxyString.Length > Limits.MaxProxyTagLength)
throw new PKError($"Proxy tag too long ({tagToAdd.ProxyString.Length} > {Limits.MaxProxyTagLength} characters).");
if (!await WarnOnConflict(tagToAdd))
throw Errors.GenericCancelled();
var newTags = target.ProxyTags.ToList();
newTags.Add(tagToAdd);
var patch = new MemberPatch {ProxyTags = Partial<ProxyTag[]>.Present(newTags.ToArray())};
var patch = new MemberPatch { ProxyTags = Partial<ProxyTag[]>.Present(newTags.ToArray()) };
await _db.Execute(conn => _repo.UpdateMember(conn, target.Id, patch));
await ctx.Reply($"{Emojis.Success} Added proxy tags {tagToAdd.ProxyString.AsCode()}.");
@ -103,7 +103,7 @@ namespace PluralKit.Bot
var newTags = target.ProxyTags.ToList();
newTags.Remove(tagToRemove);
var patch = new MemberPatch {ProxyTags = Partial<ProxyTag[]>.Present(newTags.ToArray())};
var patch = new MemberPatch { ProxyTags = Partial<ProxyTag[]>.Present(newTags.ToArray()) };
await _db.Execute(conn => _repo.UpdateMember(conn, target.Id, patch));
await ctx.Reply($"{Emojis.Success} Removed proxy tags {tagToRemove.ProxyString.AsCode()}.");
@ -122,14 +122,14 @@ namespace PluralKit.Bot
if (!await ctx.PromptYesNo(msg, "Replace"))
throw Errors.GenericCancelled();
}
if (!await WarnOnConflict(requestedTag))
throw Errors.GenericCancelled();
var newTags = new[] {requestedTag};
var patch = new MemberPatch {ProxyTags = Partial<ProxyTag[]>.Present(newTags)};
var newTags = new[] { requestedTag };
var patch = new MemberPatch { ProxyTags = Partial<ProxyTag[]>.Present(newTags) };
await _db.Execute(conn => _repo.UpdateMember(conn, target.Id, patch));
await ctx.Reply($"{Emojis.Success} Member proxy tags set to {requestedTag.ProxyString.AsCode()}.");
}
}

View file

@ -1,4 +1,4 @@
#nullable enable
#nullable enable
using System.Threading.Tasks;
using Myriad.Builders;
@ -17,7 +17,7 @@ namespace PluralKit.Bot
public class ProxiedMessage
{
private static readonly Duration EditTimeout = Duration.FromMinutes(10);
private readonly IDatabase _db;
private readonly ModelRepository _repo;
private readonly EmbedService _embeds;
@ -48,7 +48,7 @@ namespace PluralKit.Bot
if (ctx.System.Id != msg.System.Id)
throw new PKError("Can't edit a message sent by a different system.");
if (_cache.GetRootChannel(msg.Message.Channel).Id != msg.Message.Channel)
throw new PKError("PluralKit cannot edit messages in threads.");
@ -61,7 +61,7 @@ namespace PluralKit.Bot
try
{
var editedMsg = await _webhookExecutor.EditWebhookMessage(msg.Message.Channel, msg.Message.Mid, newContent);
if (ctx.Guild == null)
await _rest.CreateReaction(ctx.Channel.Id, ctx.Message.Id, new() { Name = Emojis.Success });
@ -75,7 +75,7 @@ namespace PluralKit.Bot
throw new PKError("Could not edit message.");
}
}
private async Task<FullMessage> GetMessageToEdit(Context ctx)
{
await using var conn = await _db.Obtain();
@ -112,14 +112,14 @@ namespace PluralKit.Bot
var lastMessage = await _repo.GetLastMessage(conn, ctx.Guild.Id, ctx.Channel.Id, ctx.Author.Id);
if (lastMessage == null)
return null;
var timestamp = DiscordUtils.SnowflakeToInstant(lastMessage.Mid);
if (_clock.GetCurrentInstant() - timestamp > EditTimeout)
return null;
return lastMessage;
}
public async Task GetMessage(Context ctx)
{
var (messageId, _) = ctx.MatchMessage(true);
@ -129,7 +129,7 @@ namespace PluralKit.Bot
throw new PKSyntaxError("You must pass a message ID or link.");
throw new PKSyntaxError($"Could not parse {ctx.PeekArgument().AsCode()} as a message ID or link.");
}
var message = await _db.Execute(c => _repo.GetMessage(c, messageId.Value));
if (message == null) throw Errors.MessageNotFound(messageId.Value);
@ -155,4 +155,4 @@ namespace PluralKit.Bot
await ctx.Reply(embed: await _embeds.CreateMessageInfoEmbed(message));
}
}
}
}

View file

@ -21,7 +21,8 @@ using Myriad.Rest.Exceptions;
using Myriad.Rest.Types.Requests;
using Myriad.Types;
namespace PluralKit.Bot {
namespace PluralKit.Bot
{
public class Misc
{
private readonly BotConfig _botConfig;
@ -55,31 +56,31 @@ namespace PluralKit.Bot {
_proxy = proxy;
_matcher = matcher;
}
public async Task Invite(Context ctx)
{
var clientId = _botConfig.ClientId ?? _cluster.Application?.Id;
var permissions =
var permissions =
PermissionSet.AddReactions |
PermissionSet.AttachFiles |
PermissionSet.AttachFiles |
PermissionSet.EmbedLinks |
PermissionSet.ManageMessages |
PermissionSet.ManageWebhooks |
PermissionSet.ReadMessageHistory |
PermissionSet.ReadMessageHistory |
PermissionSet.SendMessages;
var invite = $"https://discord.com/oauth2/authorize?client_id={clientId}&scope=bot%20applications.commands&permissions={(ulong)permissions}";
await ctx.Reply($"{Emojis.Success} Use this link to add PluralKit to your server:\n<{invite}>");
}
public async Task Stats(Context ctx)
{
var timeBefore = SystemClock.Instance.GetCurrentInstant();
var msg = await ctx.Reply($"...");
var timeAfter = SystemClock.Instance.GetCurrentInstant();
var apiLatency = timeAfter - timeBefore;
var messagesReceived = _metrics.Snapshot.GetForContext("Bot").Meters.FirstOrDefault(m => m.MultidimensionalName == BotMetrics.MessagesReceived.Name)?.Value;
var messagesProxied = _metrics.Snapshot.GetForContext("Bot").Meters.FirstOrDefault(m => m.MultidimensionalName == BotMetrics.MessagesProxied.Name)?.Value;
var commandsRun = _metrics.Snapshot.GetForContext("Bot").Meters.FirstOrDefault(m => m.MultidimensionalName == BotMetrics.CommandsRun.Name)?.Value;
@ -94,7 +95,7 @@ namespace PluralKit.Bot {
var shardTotal = ctx.Cluster.Shards.Count;
var shardUpTotal = _shards.Shards.Where(x => x.Connected).Count();
var shardInfo = _shards.GetShardInfo(ctx.Shard);
var process = Process.GetCurrentProcess();
var memoryUsage = process.WorkingSet64;
@ -102,7 +103,7 @@ namespace PluralKit.Bot {
var shardUptime = now - shardInfo.LastConnectionTime;
var embed = new EmbedBuilder();
if (messagesReceived != null) embed.Field(new("Messages processed",$"{messagesReceived.OneMinuteRate * 60:F1}/m ({messagesReceived.FifteenMinuteRate * 60:F1}/m over 15m)", true));
if (messagesReceived != null) embed.Field(new("Messages processed", $"{messagesReceived.OneMinuteRate * 60:F1}/m ({messagesReceived.FifteenMinuteRate * 60:F1}/m over 15m)", true));
if (messagesProxied != null) embed.Field(new("Messages proxied", $"{messagesProxied.OneMinuteRate * 60:F1}/m ({messagesProxied.FifteenMinuteRate * 60:F1}/m over 15m)", true));
if (commandsRun != null) embed.Field(new("Commands executed", $"{commandsRun.OneMinuteRate * 60:F1}/m ({commandsRun.FifteenMinuteRate * 60:F1}/m over 15m)", true));
@ -114,10 +115,10 @@ namespace PluralKit.Bot {
.Field(new("Latency", $"API: {apiLatency.TotalMilliseconds:F0} ms, shard: {shardInfo.ShardLatency.Milliseconds} ms", true))
.Field(new("Total numbers", $"{totalSystems:N0} systems, {totalMembers:N0} members, {totalGroups:N0} groups, {totalSwitches:N0} switches, {totalMessages:N0} messages"))
.Timestamp(now.ToDateTimeOffset().ToString("O"))
.Footer(new($"PluralKit {BuildInfoService.Version} • https://github.com/xSke/PluralKit"));;
.Footer(new($"PluralKit {BuildInfoService.Version} • https://github.com/xSke/PluralKit")); ;
await ctx.Rest.EditMessage(msg.ChannelId, msg.Id,
new MessageEditRequest {Content = "", Embed = embed.Build()});
new MessageEditRequest { Content = "", Embed = embed.Build() });
}
}
}
}

View file

@ -1,4 +1,4 @@
using PluralKit.Core;
using PluralKit.Core;
namespace PluralKit.Bot
{
@ -14,7 +14,7 @@ namespace PluralKit.Bot
if (!ctx.HasNext())
throw new PKSyntaxError("You must pass a privacy level (`public` or `private`)");
throw new PKSyntaxError($"Invalid privacy level {ctx.PopArgument().AsCode()} (must be `public` or `private`).");
}
@ -22,25 +22,25 @@ namespace PluralKit.Bot
{
if (!SystemPrivacyUtils.TryParseSystemPrivacy(ctx.PeekArgument(), out var subject))
throw new PKSyntaxError($"Invalid privacy subject {ctx.PopArgument().AsCode()} (must be `description`, `members`, `front`, `fronthistory`, `groups`, or `all`).");
ctx.PopArgument();
return subject;
}
public static MemberPrivacySubject PopMemberPrivacySubject(this Context ctx)
{
if (!MemberPrivacyUtils.TryParseMemberPrivacy(ctx.PeekArgument(), out var subject))
throw new PKSyntaxError($"Invalid privacy subject {ctx.PopArgument().AsCode()} (must be `name`, `description`, `avatar`, `birthday`, `pronouns`, `metadata`, `visibility`, or `all`).");
ctx.PopArgument();
return subject;
}
public static GroupPrivacySubject PopGroupPrivacySubject(this Context ctx)
{
if (!GroupPrivacyUtils.TryParseGroupPrivacy(ctx.PeekArgument(), out var subject))
throw new PKSyntaxError($"Invalid privacy subject {ctx.PopArgument().AsCode()} (must be `description`, `icon`, `visibility`, or `all`).");
ctx.PopArgument();
return subject;
}
@ -51,7 +51,7 @@ namespace PluralKit.Bot
if (ctx.MatchFlag("a", "all")) privacy = false;
if (pctx == LookupContext.ByNonOwner && !privacy) throw Errors.LookupNotAllowed;
return privacy;
return privacy;
}
}
}
}

View file

@ -33,7 +33,7 @@ namespace PluralKit.Bot
return _repo.GetSystemMembers(c, ctx.System.Id)
.Where(m => m.MemberVisibility == PrivacyLevel.Public);
}).ToListAsync();
if (members == null || !members.Any())
throw new PKError("Your system has no members! Please create at least one member before using this command.");

View file

@ -29,23 +29,23 @@ namespace PluralKit.Bot
public async Task SetLogChannel(Context ctx)
{
ctx.CheckGuildContext().CheckAuthorPermission(PermissionSet.ManageGuild, "Manage Server");
if (await ctx.MatchClear("the server log channel"))
{
await _db.Execute(conn => _repo.UpsertGuild(conn, ctx.Guild.Id, new GuildPatch {LogChannel = null}));
await _db.Execute(conn => _repo.UpsertGuild(conn, ctx.Guild.Id, new GuildPatch { LogChannel = null }));
await ctx.Reply($"{Emojis.Success} Proxy logging channel cleared.");
return;
}
if (!ctx.HasNext())
throw new PKSyntaxError("You must pass a #channel to set, or `clear` to clear it.");
Channel channel = null;
var channelString = ctx.PeekArgument();
channel = await ctx.MatchChannel();
if (channel == null || channel.GuildId != ctx.Guild.Id) throw Errors.ChannelNotFound(channelString);
var patch = new GuildPatch {LogChannel = channel.Id};
var patch = new GuildPatch { LogChannel = channel.Id };
await _db.Execute(conn => _repo.UpsertGuild(conn, ctx.Guild.Id, patch));
await ctx.Reply($"{Emojis.Success} Proxy logging channel set to #{channel.Name}.");
}
@ -59,12 +59,12 @@ namespace PluralKit.Bot
affectedChannels = _cache.GetGuildChannels(ctx.Guild.Id).Where(x => x.Type == Channel.ChannelType.GuildText).ToList();
else if (!ctx.HasNext()) throw new PKSyntaxError("You must pass one or more #channels.");
else while (ctx.HasNext())
{
var channelString = ctx.PeekArgument();
var channel = await ctx.MatchChannel();
if (channel == null || channel.GuildId != ctx.Guild.Id) throw Errors.ChannelNotFound(channelString);
affectedChannels.Add(channel);
}
{
var channelString = ctx.PeekArgument();
var channel = await ctx.MatchChannel();
if (channel == null || channel.GuildId != ctx.Guild.Id) throw Errors.ChannelNotFound(channelString);
affectedChannels.Add(channel);
}
ulong? logChannel = null;
await using (var conn = await _db.Obtain())
@ -76,8 +76,8 @@ namespace PluralKit.Bot
blacklist.ExceptWith(affectedChannels.Select(c => c.Id));
else
blacklist.UnionWith(affectedChannels.Select(c => c.Id));
var patch = new GuildPatch {LogBlacklist = blacklist.ToArray()};
var patch = new GuildPatch { LogBlacklist = blacklist.ToArray() };
await _repo.UpsertGuild(conn, ctx.Guild.Id, patch);
}
@ -91,7 +91,7 @@ namespace PluralKit.Bot
ctx.CheckGuildContext().CheckAuthorPermission(PermissionSet.ManageGuild, "Manage Server");
var blacklist = await _db.Execute(c => _repo.GetGuild(c, ctx.Guild.Id));
// Resolve all channels from the cache and order by position
var channels = blacklist.Blacklist
.Select(id => _cache.GetChannelOrNull(id))
@ -112,7 +112,7 @@ namespace PluralKit.Bot
{
string CategoryName(ulong? id) =>
id != null ? _cache.GetChannel(id.Value).Name : "(no category)";
ulong? lastCategory = null;
var fieldValue = new StringBuilder();
@ -144,13 +144,13 @@ namespace PluralKit.Bot
affectedChannels = _cache.GetGuildChannels(ctx.Guild.Id).Where(x => x.Type == Channel.ChannelType.GuildText).ToList();
else if (!ctx.HasNext()) throw new PKSyntaxError("You must pass one or more #channels.");
else while (ctx.HasNext())
{
var channelString = ctx.PeekArgument();
var channel = await ctx.MatchChannel();
if (channel == null || channel.GuildId != ctx.Guild.Id) throw Errors.ChannelNotFound(channelString);
affectedChannels.Add(channel);
}
{
var channelString = ctx.PeekArgument();
var channel = await ctx.MatchChannel();
if (channel == null || channel.GuildId != ctx.Guild.Id) throw Errors.ChannelNotFound(channelString);
affectedChannels.Add(channel);
}
await using (var conn = await _db.Obtain())
{
var guild = await _repo.GetGuild(conn, ctx.Guild.Id);
@ -159,8 +159,8 @@ namespace PluralKit.Bot
blacklist.UnionWith(affectedChannels.Select(c => c.Id));
else
blacklist.ExceptWith(affectedChannels.Select(c => c.Id));
var patch = new GuildPatch {Blacklist = blacklist.ToArray()};
var patch = new GuildPatch { Blacklist = blacklist.ToArray() };
await _repo.UpsertGuild(conn, ctx.Guild.Id, patch);
}
@ -186,14 +186,14 @@ namespace PluralKit.Bot
var guildCfg = await _db.Execute(c => _repo.GetGuild(c, ctx.Guild.Id));
if (guildCfg.LogCleanupEnabled)
eb.Description("Log cleanup is currently **on** for this server. To disable it, type `pk;logclean off`.");
else
eb.Description("Log cleanup is currently **on** for this server. To disable it, type `pk;logclean off`.");
else
eb.Description("Log cleanup is currently **off** for this server. To enable it, type `pk;logclean on`.");
await ctx.Reply(embed: eb.Build());
return;
}
var patch = new GuildPatch {LogCleanupEnabled = newValue};
var patch = new GuildPatch { LogCleanupEnabled = newValue };
await _db.Execute(conn => _repo.UpsertGuild(conn, ctx.Guild.Id, patch));
if (newValue)

View file

@ -30,7 +30,7 @@ namespace PluralKit.Bot
public async Task SwitchOut(Context ctx)
{
ctx.CheckSystem();
// Switch with no members = switch-out
await DoSwitchCommand(ctx, new PKMember[] { });
}
@ -61,35 +61,35 @@ namespace PluralKit.Bot
else
await ctx.Reply($"{Emojis.Success} Switch registered. Current fronter is now {string.Join(", ", members.Select(m => m.NameFor(ctx)))}.");
}
public async Task SwitchMove(Context ctx)
{
ctx.CheckSystem();
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 = DateUtils.ParseDateTime(timeToMove, true, tz);
if (result == null) throw Errors.InvalidDateTime(timeToMove);
await using var conn = await _db.Obtain();
var time = result.Value;
if (time.ToInstant() > SystemClock.Instance.GetCurrentInstant()) throw Errors.SwitchTimeInFuture;
// Fetch the last two switches for the system to do bounds checking on
var lastTwoSwitches = await _repo.GetSwitches(conn, ctx.System.Id).Take(2).ToListAsync();
// If we don't have a switch to move, don't bother
if (lastTwoSwitches.Count == 0) throw Errors.NoRegisteredSwitches;
// If there's a switch *behind* the one we move, we check to make srue we're not moving the time further back than that
if (lastTwoSwitches.Count == 2)
{
if (lastTwoSwitches[1].Timestamp > time.ToInstant())
throw Errors.SwitchMoveBeforeSecondLast(lastTwoSwitches[1].Timestamp.InZone(tz));
}
// Now we can actually do the move, yay!
// But, we do a prompt to confirm.
var lastSwitchMembers = _repo.GetSwitchMembers(conn, lastTwoSwitches[0].Id);
@ -98,16 +98,16 @@ namespace PluralKit.Bot
var lastSwitchDeltaStr = (SystemClock.Instance.GetCurrentInstant() - lastTwoSwitches[0].Timestamp).FormatDuration();
var newSwitchTime = time.ToInstant().ToUnixTimeSeconds();
var newSwitchDeltaStr = (SystemClock.Instance.GetCurrentInstant() - time.ToInstant()).FormatDuration();
// yeet
var msg = $"{Emojis.Warn} This will move the latest switch ({lastSwitchMemberStr}) from <t:{lastSwitchTime}> ({lastSwitchDeltaStr} ago) to <t:{newSwitchTime}> ({newSwitchDeltaStr} ago). Is this OK?";
if (!await ctx.PromptYesNo(msg, "Move Switch")) throw Errors.SwitchMoveCancelled;
// aaaand *now* we do the move
await _repo.MoveSwitch(conn, lastTwoSwitches[0].Id, time.ToInstant());
await ctx.Reply($"{Emojis.Success} Switch moved to <t:{newSwitchTime}> ({newSwitchDeltaStr} ago).");
}
public async Task SwitchDelete(Context ctx)
{
ctx.CheckSystem();
@ -122,7 +122,7 @@ namespace PluralKit.Bot
await ctx.Reply($"{Emojis.Success} Cleared system switches!");
return;
}
await using var conn = await _db.Obtain();
// Fetch the last two switches for the system to do bounds checking on
@ -148,8 +148,8 @@ namespace PluralKit.Bot
if (!await ctx.PromptYesNo(msg, "Delete Switch")) throw Errors.SwitchDeleteCancelled;
await _repo.DeleteSwitch(conn, lastTwoSwitches[0].Id);
await ctx.Reply($"{Emojis.Success} Switch deleted.");
}
}
}
}

View file

@ -9,15 +9,16 @@ namespace PluralKit.Bot
private readonly EmbedService _embeds;
private readonly IDatabase _db;
private readonly ModelRepository _repo;
public System(EmbedService embeds, IDatabase db, ModelRepository repo)
{
_embeds = embeds;
_db = db;
_repo = repo;
}
public async Task Query(Context ctx, PKSystem system) {
public async Task Query(Context ctx, PKSystem system)
{
if (system == null) throw Errors.NoSystemError;
await ctx.Reply(embed: await _embeds.CreateSystemEmbed(ctx, system, ctx.LookupContextFor(system)));
@ -37,9 +38,9 @@ namespace PluralKit.Bot
await _repo.AddAccount(c, system.Id, ctx.Author.Id);
return system;
});
// TODO: better message, perhaps embed like in groups?
await ctx.Reply($"{Emojis.Success} Your system has been created. Type `pk;system` to view it, and type `pk;system help` for more information about commands you can use now. Now that you have that set up, check out the getting started guide on setting up members and proxies: <https://pluralkit.me/start>");
}
}
}
}

View file

@ -33,7 +33,7 @@ namespace PluralKit.Bot
if (await ctx.MatchClear("your system's name"))
{
var clearPatch = new SystemPatch {Name = null};
var clearPatch = new SystemPatch { Name = null };
await _db.Execute(conn => _repo.UpdateSystem(conn, ctx.System.Id, clearPatch));
await ctx.Reply($"{Emojis.Success} System name cleared.");
@ -49,28 +49,29 @@ namespace PluralKit.Bot
await ctx.Reply("Your system currently does not have a name. Type `pk;system name <name>` to set one.");
return;
}
if (newSystemName != null && newSystemName.Length > Limits.MaxSystemNameLength)
throw Errors.SystemNameTooLongError(newSystemName.Length);
var patch = new SystemPatch {Name = newSystemName};
var patch = new SystemPatch { Name = newSystemName };
await _db.Execute(conn => _repo.UpdateSystem(conn, ctx.System.Id, patch));
await ctx.Reply($"{Emojis.Success} System name changed.");
}
public async Task Description(Context ctx) {
public async Task Description(Context ctx)
{
ctx.CheckSystem();
if (await ctx.MatchClear("your system's description"))
{
var patch = new SystemPatch {Description = null};
var patch = new SystemPatch { Description = null };
await _db.Execute(conn => _repo.UpdateSystem(conn, ctx.System.Id, patch));
await ctx.Reply($"{Emojis.Success} System description cleared.");
return;
}
var newDescription = ctx.RemainderOrNull()?.NormalizeLineEndSpacing();
if (newDescription == null)
{
@ -88,27 +89,28 @@ namespace PluralKit.Bot
else
{
if (newDescription.Length > Limits.MaxDescriptionLength) throw Errors.DescriptionTooLongError(newDescription.Length);
var patch = new SystemPatch {Description = newDescription};
var patch = new SystemPatch { Description = newDescription };
await _db.Execute(conn => _repo.UpdateSystem(conn, ctx.System.Id, patch));
await ctx.Reply($"{Emojis.Success} System description changed.");
}
}
public async Task Color(Context ctx) {
public async Task Color(Context ctx)
{
ctx.CheckSystem();
if (await ctx.MatchClear())
{
var patch = new SystemPatch {Color = Partial<string>.Null()};
var patch = new SystemPatch { Color = Partial<string>.Null() };
await _db.Execute(conn => _repo.UpdateSystem(conn, ctx.System.Id, patch));
await ctx.Reply($"{Emojis.Success} System color cleared.");
}
else if (!ctx.HasNext())
else if (!ctx.HasNext())
{
if (ctx.System.Color == null)
if (ctx.System.Color == null)
await ctx.Reply(
$"Your system does not have a color set. To set one, type `pk;system color <color>`.");
else
@ -126,7 +128,7 @@ namespace PluralKit.Bot
if (color.StartsWith("#")) color = color.Substring(1);
if (!Regex.IsMatch(color, "^[0-9a-fA-F]{6}$")) throw Errors.InvalidColorError(color);
var patch = new SystemPatch {Color = Partial<string>.Present(color.ToLowerInvariant())};
var patch = new SystemPatch { Color = Partial<string>.Present(color.ToLowerInvariant()) };
await _db.Execute(conn => _repo.UpdateSystem(conn, ctx.System.Id, patch));
await ctx.Reply(embed: new EmbedBuilder()
@ -136,18 +138,19 @@ namespace PluralKit.Bot
.Build());
}
}
public async Task Tag(Context ctx)
{
ctx.CheckSystem();
if (await ctx.MatchClear("your system's tag"))
{
var patch = new SystemPatch {Tag = null};
var patch = new SystemPatch { Tag = null };
await _db.Execute(conn => _repo.UpdateSystem(conn, ctx.System.Id, patch));
await ctx.Reply($"{Emojis.Success} System tag cleared.");
} else if (!ctx.HasNext(skipFlags: false))
}
else if (!ctx.HasNext(skipFlags: false))
{
if (ctx.System.Tag == null)
await ctx.Reply($"You currently have no system tag. To set one, type `pk;s tag <tag>`.");
@ -160,10 +163,10 @@ namespace PluralKit.Bot
if (newTag != null)
if (newTag.Length > Limits.MaxSystemTagLength)
throw Errors.SystemTagTooLongError(newTag.Length);
var patch = new SystemPatch {Tag = newTag};
var patch = new SystemPatch { Tag = newTag };
await _db.Execute(conn => _repo.UpdateSystem(conn, ctx.System.Id, patch));
await ctx.Reply($"{Emojis.Success} System tag changed. Member names will now end with {newTag.AsCode()} when proxied.");
}
}
@ -198,9 +201,9 @@ namespace PluralKit.Bot
{
var newTag = ctx.RemainderOrNull(skipFlags: false);
if (newTag != null && newTag.Length > Limits.MaxSystemTagLength)
throw Errors.SystemTagTooLongError(newTag.Length);
throw Errors.SystemTagTooLongError(newTag.Length);
var patch = new SystemGuildPatch {Tag = newTag};
var patch = new SystemGuildPatch { Tag = newTag };
await _db.Execute(conn => _repo.UpsertSystemGuild(conn, ctx.System.Id, ctx.Guild.Id, patch));
await ctx.Reply($"{Emojis.Success} System server tag changed. Member names will now end with {newTag.AsCode()} when proxied in the current server '{ctx.Guild.Name}'.");
@ -211,7 +214,7 @@ namespace PluralKit.Bot
async Task Clear()
{
var patch = new SystemGuildPatch {Tag = null};
var patch = new SystemGuildPatch { Tag = null };
await _db.Execute(conn => _repo.UpsertSystemGuild(conn, ctx.System.Id, ctx.Guild.Id, patch));
await ctx.Reply($"{Emojis.Success} System server tag cleared. Member names will now end with the global system tag, if there is one set.");
@ -222,7 +225,7 @@ namespace PluralKit.Bot
async Task EnableDisable(bool newValue)
{
var patch = new SystemGuildPatch {TagEnabled = newValue};
var patch = new SystemGuildPatch { TagEnabled = newValue };
await _db.Execute(conn => _repo.UpsertSystemGuild(conn, ctx.System.Id, ctx.Guild.Id, patch));
await ctx.Reply(PrintEnableDisableResult(newValue, newValue != ctx.MessageContext.TagEnabled));
@ -253,7 +256,7 @@ namespace PluralKit.Bot
str += $" Member names will now end with the global system tag when proxied in the current server, if there is one set.";
}
}
return str;
}
@ -268,14 +271,14 @@ namespace PluralKit.Bot
else
await Set();
}
public async Task Avatar(Context ctx)
{
ctx.CheckSystem();
async Task ClearIcon()
{
await _db.Execute(c => _repo.UpdateSystem(c, ctx.System.Id, new SystemPatch {AvatarUrl = null}));
await _db.Execute(c => _repo.UpdateSystem(c, ctx.System.Id, new SystemPatch { AvatarUrl = null }));
await ctx.Reply($"{Emojis.Success} System icon cleared.");
}
@ -283,8 +286,8 @@ namespace PluralKit.Bot
{
await AvatarUtils.VerifyAvatarOrThrow(_client, img.Url);
await _db.Execute(c => _repo.UpdateSystem(c, ctx.System.Id, new SystemPatch {AvatarUrl = img.Url}));
await _db.Execute(c => _repo.UpdateSystem(c, ctx.System.Id, new SystemPatch { AvatarUrl = img.Url }));
var msg = img.Source switch
{
AvatarSource.User => $"{Emojis.Success} System icon changed to {img.SourceUser?.Username}'s avatar!\n{Emojis.Warn} If {img.SourceUser?.Username} changes their avatar, the system icon will need to be re-set.",
@ -292,11 +295,11 @@ namespace PluralKit.Bot
AvatarSource.Attachment => $"{Emojis.Success} System icon changed to attached image.\n{Emojis.Warn} If you delete the message containing the attachment, the system icon will stop working.",
_ => throw new ArgumentOutOfRangeException()
};
// The attachment's already right there, no need to preview it.
var hasEmbed = img.Source != AvatarSource.Attachment;
await (hasEmbed
? ctx.Reply(msg, embed: new EmbedBuilder().Image(new(img.Url)).Build())
await (hasEmbed
? ctx.Reply(msg, embed: new EmbedBuilder().Image(new(img.Url)).Build())
: ctx.Reply(msg));
}
@ -316,7 +319,7 @@ namespace PluralKit.Bot
if (await ctx.MatchClear("your system's icon"))
await ClearIcon();
else if (await ctx.MatchImage() is {} img)
else if (await ctx.MatchImage() is { } img)
await SetIcon(img);
else
await ShowIcon();
@ -328,7 +331,7 @@ namespace PluralKit.Bot
async Task ClearImage()
{
await _db.Execute(c => _repo.UpdateSystem(c, ctx.System.Id, new SystemPatch {BannerImage = null}));
await _db.Execute(c => _repo.UpdateSystem(c, ctx.System.Id, new SystemPatch { BannerImage = null }));
await ctx.Reply($"{Emojis.Success} System banner image cleared.");
}
@ -336,7 +339,7 @@ namespace PluralKit.Bot
{
await AvatarUtils.VerifyAvatarOrThrow(_client, img.Url, isFullSizeImage: true);
await _db.Execute(c => _repo.UpdateSystem(c, ctx.System.Id, new SystemPatch {BannerImage = img.Url}));
await _db.Execute(c => _repo.UpdateSystem(c, ctx.System.Id, new SystemPatch { BannerImage = img.Url }));
var msg = img.Source switch
{
@ -348,8 +351,8 @@ namespace PluralKit.Bot
// The attachment's already right there, no need to preview it.
var hasEmbed = img.Source != AvatarSource.Attachment;
await (hasEmbed
? ctx.Reply(msg, embed: new EmbedBuilder().Image(new(img.Url)).Build())
await (hasEmbed
? ctx.Reply(msg, embed: new EmbedBuilder().Image(new(img.Url)).Build())
: ctx.Reply(msg));
}
@ -369,13 +372,14 @@ namespace PluralKit.Bot
if (await ctx.MatchClear("your system's banner image"))
await ClearImage();
else if (await ctx.MatchImage() is {} img)
else if (await ctx.MatchImage() is { } img)
await SetImage(img);
else
await ShowImage();
}
public async Task Delete(Context ctx) {
public async Task Delete(Context ctx)
{
ctx.CheckSystem();
await ctx.Reply($"{Emojis.Warn} Are you sure you want to delete your system? If so, reply to this message with your system's ID (`{ctx.System.Hid}`).\n**Note: this action is permanent.**");
@ -383,10 +387,10 @@ namespace PluralKit.Bot
throw new PKError($"System deletion cancelled. Note that you must reply with your system ID (`{ctx.System.Hid}`) *verbatim*.");
await _db.Execute(conn => _repo.DeleteSystem(conn, ctx.System.Id));
await ctx.Reply($"{Emojis.Success} System deleted.");
}
public async Task SystemProxy(Context ctx)
{
ctx.CheckSystem();
@ -415,7 +419,7 @@ namespace PluralKit.Bot
return;
}
var patch = new SystemGuildPatch {ProxyEnabled = newValue};
var patch = new SystemGuildPatch { ProxyEnabled = newValue };
await _db.Execute(conn => _repo.UpsertSystemGuild(conn, ctx.System.Id, guild.Id, patch));
if (newValue)
@ -423,20 +427,20 @@ namespace PluralKit.Bot
else
await ctx.Reply($"Message proxying in {serverText} is now **disabled** for your system.");
}
public async Task SystemTimezone(Context ctx)
public async Task SystemTimezone(Context ctx)
{
if (ctx.System == null) throw Errors.NoSystemError;
if (await ctx.MatchClear())
{
var clearPatch = new SystemPatch {UiTz = "UTC"};
var clearPatch = new SystemPatch { UiTz = "UTC" };
await _db.Execute(conn => _repo.UpdateSystem(conn, ctx.System.Id, clearPatch));
await ctx.Reply($"{Emojis.Success} System time zone cleared (set to UTC).");
return;
}
var zoneStr = ctx.RemainderOrNull();
if (zoneStr == null)
{
@ -451,8 +455,8 @@ namespace PluralKit.Bot
var currentTime = SystemClock.Instance.GetCurrentInstant().InZone(zone);
var msg = $"This will change the system time zone to **{zone.Id}**. The current time is **{currentTime.FormatZoned()}**. Is this correct?";
if (!await ctx.PromptYesNo(msg, "Change Timezone")) throw Errors.TimezoneChangeCancelled;
var patch = new SystemPatch {UiTz = zone.Id};
var patch = new SystemPatch { UiTz = zone.Id };
await _db.Execute(conn => _repo.UpdateSystem(conn, ctx.System.Id, patch));
await ctx.Reply($"System time zone changed to **{zone.Id}**.");
@ -522,45 +526,49 @@ namespace PluralKit.Bot
await SetLevel(ctx.PopSystemPrivacySubject(), ctx.PopPrivacyLevel());
}
public async Task SystemPing(Context ctx)
{
ctx.CheckSystem();
public async Task SystemPing(Context ctx)
{
ctx.CheckSystem();
if (!ctx.HasNext())
{
if (ctx.System.PingsEnabled) {await ctx.Reply("Reaction pings are currently **enabled** for your system. To disable reaction pings, type `pk;s ping disable`.");}
else {await ctx.Reply("Reaction pings are currently **disabled** for your system. To enable reaction pings, type `pk;s ping enable`.");}
}
else {
if (ctx.Match("on", "enable")) {
var patch = new SystemPatch {PingsEnabled = true};
if (!ctx.HasNext())
{
if (ctx.System.PingsEnabled) { await ctx.Reply("Reaction pings are currently **enabled** for your system. To disable reaction pings, type `pk;s ping disable`."); }
else { await ctx.Reply("Reaction pings are currently **disabled** for your system. To enable reaction pings, type `pk;s ping enable`."); }
}
else
{
if (ctx.Match("on", "enable"))
{
var patch = new SystemPatch { PingsEnabled = true };
await _db.Execute(conn => _repo.UpdateSystem(conn, ctx.System.Id, patch));
await ctx.Reply("Reaction pings have now been enabled.");
}
if (ctx.Match("off", "disable")) {
var patch = new SystemPatch {PingsEnabled = false};
if (ctx.Match("off", "disable"))
{
var patch = new SystemPatch { PingsEnabled = false };
await _db.Execute(conn => _repo.UpdateSystem(conn, ctx.System.Id, patch));
await ctx.Reply("Reaction pings have now been disabled.");
}
}
}
}
public async Task<DateTimeZone> FindTimeZone(Context ctx, string zoneStr) {
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 = 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;
var matchingLocations = locations.Where(l => l.Countries.Any(c =>
string.Equals(c.Code, zoneStr, StringComparison.InvariantCultureIgnoreCase) ||
string.Equals(c.Name, zoneStr, StringComparison.InvariantCultureIgnoreCase)));
// Then, we find all (unique) time zone IDs that match.
var matchingZones = matchingLocations.Select(l => DateTimeZoneProviders.Tzdb.GetZoneOrNull(l.ZoneId))
.Distinct().ToList();
// If the set of matching zones is empty (ie. we didn't find anything), we try a few other things.
if (matchingZones.Count == 0)
{
@ -587,13 +595,13 @@ namespace PluralKit.Bot
matchingZones = allZones.Select(z => DateTimeZoneProviders.Tzdb.GetZoneOrNull(z))
.Where(z => z.GetUtcOffset(SystemClock.Instance.GetCurrentInstant()) == offset).ToList();
}
// If we have a list of viable time zones, we ask the user which is correct.
// If we only have one, return that one.
if (matchingZones.Count == 1)
return matchingZones.First();
// Otherwise, prompt and return!
return await ctx.Choose("There were multiple matches for your time zone query. Please select the region that matches you the closest:", matchingZones,
z =>
@ -605,4 +613,4 @@ namespace PluralKit.Bot
});
}
}
}
}

View file

@ -21,7 +21,7 @@ namespace PluralKit.Bot
_db = db;
_repo = repo;
}
struct FrontHistoryEntry
{
public readonly Instant? LastTime;
@ -40,10 +40,10 @@ namespace PluralKit.Bot
ctx.CheckSystemPrivacy(system, system.FrontPrivacy);
await using var conn = await _db.Obtain();
var sw = await _repo.GetLatestSwitch(conn, system.Id);
if (sw == null) throw Errors.NoRegisteredSwitches;
await ctx.Reply(embed: await _embeds.CreateFronterEmbed(sw, system.Zone, ctx.LookupContextFor(system)));
}
@ -54,7 +54,7 @@ namespace PluralKit.Bot
// Gotta be careful here: if we dispose of the connection while the IAE is alive, boom
await using var conn = await _db.Obtain();
var totalSwitches = await _repo.GetSwitchCount(conn, system.Id);
if (totalSwitches == 0) throw Errors.NoRegisteredSwitches;
@ -78,10 +78,10 @@ namespace PluralKit.Bot
var lastSw = entry.LastTime;
var sw = entry.ThisSwitch;
// Fetch member list and format
await using var conn = await _db.Obtain();
var members = await _db.Execute(c => _repo.GetSwitchMembers(c, sw.Id)).ToListAsync();
var membersStr = members.Any() ? string.Join(", ", members.Select(m => m.NameFor(ctx))) : "no fronter";
@ -111,7 +111,7 @@ namespace PluralKit.Bot
}
);
}
public async Task SystemFrontPercent(Context ctx, PKSystem system)
{
if (system == null) throw Errors.NoSystemError;
@ -121,7 +121,7 @@ namespace PluralKit.Bot
if (totalSwitches == 0) throw Errors.NoRegisteredSwitches;
string durationStr = ctx.RemainderOrNull() ?? "30d";
var now = SystemClock.Instance.GetCurrentInstant();
var rangeStart = DateUtils.ParseDateTime(durationStr, true, system.Zone);
@ -129,7 +129,7 @@ namespace PluralKit.Bot
if (rangeStart.Value.ToInstant() > now) throw Errors.FrontPercentTimeInFuture;
var title = new StringBuilder($"Frontpercent of ");
if (system.Name != null)
if (system.Name != null)
title.Append($"{system.Name} (`{system.Hid}`)");
else
title.Append($"`{system.Hid}`");

View file

@ -18,13 +18,13 @@ namespace PluralKit.Bot
_db = db;
_repo = repo;
}
public async Task LinkSystem(Context ctx)
{
ctx.CheckSystem();
await using var conn = await _db.Obtain();
var account = await ctx.MatchUser() ?? throw new PKSyntaxError("You must pass an account to link with (either ID or @mention).");
var accountIds = await _repo.GetSystemAccounts(conn, ctx.System.Id);
if (accountIds.Contains(account.Id))
@ -32,7 +32,7 @@ namespace PluralKit.Bot
var existingAccount = await _repo.GetSystemByAccount(conn, account.Id);
if (existingAccount != null)
throw Errors.AccountInOtherSystem(existingAccount);
throw Errors.AccountInOtherSystem(existingAccount);
var msg = $"{account.Mention()}, please confirm the link.";
if (!await ctx.PromptYesNo(msg, "Confirm", user: account, matchFlag: false)) throw Errors.MemberLinkCancelled;
@ -43,7 +43,7 @@ namespace PluralKit.Bot
public async Task UnlinkAccount(Context ctx)
{
ctx.CheckSystem();
await using var conn = await _db.Obtain();
ulong id;
@ -55,7 +55,7 @@ namespace PluralKit.Bot
var accountIds = (await _repo.GetSystemAccounts(conn, ctx.System.Id)).ToList();
if (!accountIds.Contains(id)) throw Errors.AccountNotLinked;
if (accountIds.Count == 1) throw Errors.UnlinkingLastAccount;
var msg = $"Are you sure you want to unlink <@{id}> from your system?";
if (!await ctx.PromptYesNo(msg, "Unlink")) throw Errors.MemberUnlinkCancelled;

View file

@ -8,7 +8,7 @@ namespace PluralKit.Bot
public class SystemList
{
private readonly IDatabase _db;
public SystemList(IDatabase db)
{
_db = db;
@ -26,15 +26,15 @@ namespace PluralKit.Bot
private string GetEmbedTitle(PKSystem target, MemberListOptions opts)
{
var title = new StringBuilder("Members of ");
if (target.Name != null)
if (target.Name != null)
title.Append($"{target.Name} (`{target.Hid}`)");
else
else
title.Append($"`{target.Hid}`");
if (opts.Search != null)
title.Append($" matching **{opts.Search}**");
return title.ToString();
}
}

View file

@ -25,7 +25,7 @@ namespace PluralKit.Bot
// Get or make a token
var token = ctx.System.Token ?? await MakeAndSetNewToken(ctx.System);
try
{
// DM the user a security disclaimer, and then the token in a separate message (for easy copying on mobile)
@ -34,7 +34,7 @@ namespace PluralKit.Bot
{
Content = $"{Emojis.Warn} Please note that this grants access to modify (and delete!) all your system data, so keep it safe and secure. If it leaks or you need a new one, you can invalidate this one with `pk;token refresh`.\n\nYour token is below:"
});
await ctx.Rest.CreateMessage(dm.Id, new MessageRequest {Content = token});
await ctx.Rest.CreateMessage(dm.Id, new MessageRequest { Content = token });
// If we're not already in a DM, reply with a reminder to check
if (ctx.Channel.Type != Channel.ChannelType.Dm)
@ -50,15 +50,15 @@ namespace PluralKit.Bot
private async Task<string> MakeAndSetNewToken(PKSystem system)
{
var patch = new SystemPatch {Token = StringUtils.GenerateToken()};
var patch = new SystemPatch { Token = StringUtils.GenerateToken() };
system = await _db.Execute(conn => _repo.UpdateSystem(conn, system.Id, patch));
return system.Token;
}
public async Task RefreshToken(Context ctx)
{
ctx.CheckSystem();
if (ctx.System.Token == null)
{
// If we don't have a token, call the other method instead
@ -67,19 +67,20 @@ namespace PluralKit.Bot
return;
}
try {
try
{
// DM the user an invalidation disclaimer, and then the token in a separate message (for easy copying on mobile)
var dm = await ctx.Cache.GetOrCreateDmChannel(ctx.Rest, ctx.Author.Id);
await ctx.Rest.CreateMessage(dm.Id, new MessageRequest
{
Content = $"{Emojis.Warn} Your previous API token has been invalidated. You will need to change it anywhere it's currently used.\n\nYour token is below:"
});
// Make the new token after sending the first DM; this ensures if we can't DM, we also don't end up
// breaking their existing token as a side effect :)
var token = await MakeAndSetNewToken(ctx.System);
await ctx.Rest.CreateMessage(dm.Id, new MessageRequest { Content = token });
// If we're not already in a DM, reply with a reminder to check
if (ctx.Channel.Type != Channel.ChannelType.Dm)
await ctx.Reply($"{Emojis.Success} Check your DMs!");

View file

@ -6,11 +6,12 @@ using Humanizer;
using NodaTime;
using PluralKit.Core;
namespace PluralKit.Bot {
namespace PluralKit.Bot
{
/// <summary>
/// An exception class representing user-facing errors caused when parsing and executing commands.
/// </summary>
public class PKError : Exception
public class PKError: Exception
{
public PKError(string message) : base(message)
{
@ -21,14 +22,15 @@ namespace PluralKit.Bot {
/// 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 class PKSyntaxError: PKError
{
public PKSyntaxError(string message) : base(message)
{
}
}
public static class Errors {
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
@ -50,9 +52,9 @@ namespace PluralKit.Bot {
public static PKError BirthdayParseError(string birthday) => new PKError($"\"{birthday}\" could not be parsed as a valid date. Try a format like \"2016-12-24\" or \"May 3 1996\".");
public static PKError ProxyMustHaveText => new PKSyntaxError("Example proxy message must contain the string 'text'.");
public static PKError ProxyMultipleText => new PKSyntaxError("Example proxy message must contain the string 'text' exactly once.");
public static PKError MemberDeleteCancelled => new PKError($"Member deletion cancelled. Stay safe! {Emojis.ThumbsUp}");
public static PKError AvatarServerError(HttpStatusCode statusCode) => new PKError($"Server responded with status code {(int) statusCode}, are you sure your link is working?");
public static PKError AvatarServerError(HttpStatusCode statusCode) => new PKError($"Server responded with status code {(int)statusCode}, are you sure your link is working?");
public static PKError AvatarFileSizeLimit(long size) => new PKError($"File size too large ({size.Bytes().ToString("#.#")} > {Limits.AvatarFileSizeLimit.Bytes().ToString("#.#")}), try shrinking or compressing the image.");
public static PKError AvatarNotAnImage(string mimeType) => new PKError($"The given link does not point to an image{(mimeType != null ? $" ({mimeType})" : "")}. Make sure you're using a direct link (ending in .jpg, .png, .gif).");
public static PKError AvatarDimensionsTooLarge(int width, int height) => new PKError($"Image too large ({width}x{height} > {Limits.AvatarDimensionLimit}x{Limits.AvatarDimensionLimit}), try resizing the image.");
@ -60,7 +62,7 @@ namespace PluralKit.Bot {
public static PKError UserHasNoAvatar => new PKError("The given user has no avatar set.");
public static PKError InvalidUrl(string url) => new PKError($"The given URL is invalid.");
public static PKError UrlTooLong(string url) => new PKError($"The given URL is too long ({url.Length}/{Limits.MaxUriLength} characters).");
public static PKError AccountAlreadyLinked => new PKError("That account is already linked to your system.");
public static PKError AccountNotLinked => new PKError("That account isn't linked to your system.");
public static PKError AccountInOtherSystem(PKSystem system) => new PKError($"The mentioned account is already linked to another system (see `pk;system {system.Hid}`).");
@ -94,7 +96,7 @@ namespace PluralKit.Bot {
public static PKError InvalidImportFile => new PKError("Imported data file invalid. Make sure this is a .json file directly exported from PluralKit or Tupperbox.");
public static PKError ImportCancelled => new PKError("Import cancelled.");
public static PKError MessageNotFound(ulong id) => new PKError($"Message with ID '{id}' not found. Are you sure it's a message proxied by PluralKit?");
public static PKError DurationParseError(string durationStr) => new PKError($"Could not parse {durationStr.AsCode()} as a valid duration. Try a format such as `30d`, `1d3h` or `20m30s`.");
public static PKError FrontPercentTimeInFuture => new PKError("Cannot get the front percent between now and a time in the future.");

View file

@ -4,7 +4,7 @@ using Myriad.Gateway;
namespace PluralKit.Bot
{
public interface IEventHandler<in T> where T: IGatewayEvent
public interface IEventHandler<in T> where T : IGatewayEvent
{
Task Handle(Shard shard, T evt);

View file

@ -11,7 +11,7 @@ namespace PluralKit.Bot
{
private readonly InteractionDispatchService _interactionDispatch;
private readonly ILifetimeScope _services;
public InteractionCreated(InteractionDispatchService interactionDispatch, ILifetimeScope services)
{
_interactionDispatch = interactionDispatch;

View file

@ -64,25 +64,25 @@ namespace PluralKit.Bot
var guild = evt.GuildId != null ? _cache.GetGuild(evt.GuildId.Value) : null;
var channel = _cache.GetChannel(evt.ChannelId);
var rootChannel = _cache.GetRootChannel(evt.ChannelId);
// Log metrics and message info
_metrics.Measure.Meter.Mark(BotMetrics.MessagesReceived);
_lastMessageCache.AddMessage(evt);
// Get message context from DB (tracking w/ metrics)
MessageContext ctx;
await using (var conn = await _db.Obtain())
using (_metrics.Measure.Timer.Time(BotMetrics.MessageContextQueryTime))
ctx = await _repo.GetMessageContext(conn, evt.Author.Id, evt.GuildId ?? default, rootChannel.Id);
// Try each handler until we find one that succeeds
if (await TryHandleLogClean(evt, ctx))
if (await TryHandleLogClean(evt, ctx))
return;
// Only do command/proxy handling if it's a user account
if (evt.Author.Bot || evt.WebhookId != null || evt.Author.System == true)
if (evt.Author.Bot || evt.WebhookId != null || evt.Author.System == true)
return;
if (await TryHandleCommand(shard, evt, guild, channel, ctx))
return;
await TryHandleProxy(shard, evt, guild, channel, ctx);
@ -133,11 +133,11 @@ namespace PluralKit.Bot
foreach (var prefix in prefixes)
{
if (!message.StartsWith(prefix, StringComparison.InvariantCultureIgnoreCase)) continue;
argPos = prefix.Length;
return true;
}
// Then, check mention prefix (must be the bot user, ofc)
argPos = -1;
if (DiscordUtils.HasMentionPrefix(message, ref argPos, out var id))
@ -156,7 +156,7 @@ namespace PluralKit.Bot
}
// Catch any failed proxy checks so they get ignored in the global error handler
catch (ProxyService.ProxyChecksFailedException) {}
catch (ProxyService.ProxyChecksFailedException) { }
catch (PKError e)
{
@ -164,7 +164,7 @@ namespace PluralKit.Bot
if (botPermissions.HasFlag(PermissionSet.SendMessages))
{
await _rest.CreateMessage(evt.ChannelId,
new MessageRequest {Content = $"{Emojis.Error} {e.Message}"});
new MessageRequest { Content = $"{Emojis.Error} {e.Message}" });
}
}

View file

@ -14,7 +14,7 @@ namespace PluralKit.Bot
public class MessageDeleted: IEventHandler<MessageDeleteEvent>, IEventHandler<MessageDeleteBulkEvent>
{
private static readonly TimeSpan MessageDeleteDelay = TimeSpan.FromSeconds(15);
private readonly IDatabase _db;
private readonly ModelRepository _repo;
private readonly ILogger _logger;
@ -27,7 +27,7 @@ namespace PluralKit.Bot
_lastMessage = lastMessage;
_logger = logger.ForContext<MessageDeleted>();
}
public Task Handle(Shard shard, MessageDeleteEvent evt)
{
// Delete deleted webhook messages from the data store
@ -54,11 +54,11 @@ namespace PluralKit.Bot
{
await Task.Delay(MessageDeleteDelay);
_logger.Information("Bulk deleting {Count} messages in channel {Channel}",
_logger.Information("Bulk deleting {Count} messages in channel {Channel}",
evt.Ids.Length, evt.ChannelId);
await _db.Execute(c => _repo.DeleteMessagesBulk(c, evt.Ids));
}
_lastMessage.HandleMessageDeletion(evt.ChannelId, evt.Ids.ToList());
_ = Inner();
return Task.CompletedTask;

View file

@ -28,7 +28,7 @@ namespace PluralKit.Bot
private readonly Bot _bot;
private readonly DiscordApiClient _rest;
private readonly ILogger _logger;
public MessageEdited(LastMessageCacheService lastMessageCache, ProxyService proxy, IDatabase db, IMetrics metrics, ModelRepository repo, Cluster client, IDiscordCache cache, Bot bot, DiscordApiClient rest, ILogger logger)
{
_lastMessageCache = lastMessageCache;
@ -46,11 +46,11 @@ namespace PluralKit.Bot
public async Task Handle(Shard shard, MessageUpdateEvent evt)
{
if (evt.Author.Value?.Id == _client.User?.Id) return;
// Edit message events sometimes arrive with missing data; double-check it's all there
if (!evt.Content.HasValue || !evt.Author.HasValue || !evt.Member.HasValue)
if (!evt.Content.HasValue || !evt.Author.HasValue || !evt.Member.HasValue)
return;
var channel = _cache.GetChannel(evt.ChannelId);
if (!DiscordUtils.IsValidGuildChannel(channel))
return;
@ -60,7 +60,7 @@ namespace PluralKit.Bot
// Only react to the last message in the channel
if (lastMessage?.Id != evt.Id)
return;
// Just run the normal message handling code, with a flag to disable autoproxying
MessageContext ctx;
await using (var conn = await _db.Obtain())
@ -76,7 +76,7 @@ namespace PluralKit.Bot
channel: channel, botPermissions: botPermissions);
}
// Catch any failed proxy checks so they get ignored in the global error handler
catch (ProxyService.ProxyChecksFailedException) {}
catch (ProxyService.ProxyChecksFailedException) { }
}
private async Task<MessageCreateEvent> GetMessageCreateEvent(MessageUpdateEvent evt, CachedMessage lastMessage, Channel channel)
@ -86,11 +86,11 @@ namespace PluralKit.Bot
var messageReference = lastMessage.ReferencedMessage != null
? new Message.Reference(channel.GuildId, evt.ChannelId, lastMessage.ReferencedMessage.Value)
: null;
var messageType = lastMessage.ReferencedMessage != null
? Message.MessageType.Reply
var messageType = lastMessage.ReferencedMessage != null
? Message.MessageType.Reply
: Message.MessageType.Default;
// TODO: is this missing anything?
var equivalentEvt = new MessageCreateEvent
{
@ -112,7 +112,7 @@ namespace PluralKit.Bot
{
if (referencedMessageId == null)
return null;
var botPermissions = _bot.PermissionsIn(channelId);
if (!botPermissions.HasFlag(PermissionSet.ReadMessageHistory))
{

View file

@ -40,7 +40,7 @@ namespace PluralKit.Bot
}
public async Task Handle(Shard shard, MessageReactionAddEvent evt)
{
{
await TryHandleProxyMessageReactions(evt);
}
@ -71,42 +71,42 @@ namespace PluralKit.Bot
// Ignore reactions from bots (we can't DM them anyway)
if (user.Bot) return;
switch (evt.Emoji.Name)
{
// Message deletion
case "\u274C": // Red X
{
await using var conn = await _db.Obtain();
var msg = await _repo.GetMessage(conn, evt.MessageId);
if (msg != null)
await HandleProxyDeleteReaction(evt, msg);
break;
}
{
await using var conn = await _db.Obtain();
var msg = await _repo.GetMessage(conn, evt.MessageId);
if (msg != null)
await HandleProxyDeleteReaction(evt, msg);
break;
}
case "\u2753": // Red question mark
case "\u2754": // White question mark
{
await using var conn = await _db.Obtain();
var msg = await _repo.GetMessage(conn, evt.MessageId);
if (msg != null)
await HandleQueryReaction(evt, msg);
break;
}
{
await using var conn = await _db.Obtain();
var msg = await _repo.GetMessage(conn, evt.MessageId);
if (msg != null)
await HandleQueryReaction(evt, msg);
break;
}
case "\U0001F514": // Bell
case "\U0001F6CE": // Bellhop bell
case "\U0001F3D3": // Ping pong paddle (lol)
case "\u23F0": // Alarm clock
case "\u2757": // Exclamation mark
{
await using var conn = await _db.Obtain();
var msg = await _repo.GetMessage(conn, evt.MessageId);
if (msg != null)
await HandlePingReaction(evt, msg);
break;
}
{
await using var conn = await _db.Obtain();
var msg = await _repo.GetMessage(conn, evt.MessageId);
if (msg != null)
await HandlePingReaction(evt, msg);
break;
}
}
}
@ -114,7 +114,7 @@ namespace PluralKit.Bot
{
if (!_bot.PermissionsIn(evt.ChannelId).HasFlag(PermissionSet.ManageMessages))
return;
using var conn = await _db.Obtain();
var system = await _repo.GetSystemByAccount(conn, evt.UserId);
@ -136,7 +136,7 @@ namespace PluralKit.Bot
private async ValueTask HandleCommandDeleteReaction(MessageReactionAddEvent evt, CommandMessage msg)
{
// Can only delete your own message
if (msg.AuthorId != evt.UserId)
if (msg.AuthorId != evt.UserId)
return;
try
@ -154,7 +154,7 @@ namespace PluralKit.Bot
private async ValueTask HandleQueryReaction(MessageReactionAddEvent evt, FullMessage msg)
{
var guild = _cache.GetGuild(evt.GuildId!.Value);
// Try to DM the user info about the message
try
{
@ -163,14 +163,14 @@ namespace PluralKit.Bot
{
Embed = await _embeds.CreateMemberEmbed(msg.System, msg.Member, guild, LookupContext.ByNonOwner)
});
await _rest.CreateMessage(dm.Id, new MessageRequest
{
Embed = await _embeds.CreateMessageInfoEmbed(msg)
});
}
catch (ForbiddenException) { } // No permissions to DM, can't check for this :(
await TryRemoveOriginalReaction(evt);
}
@ -178,20 +178,20 @@ namespace PluralKit.Bot
{
if (!_bot.PermissionsIn(evt.ChannelId).HasFlag(PermissionSet.ManageMessages))
return;
// Check if the "pinger" has permission to send messages in this channel
// (if not, PK shouldn't send messages on their behalf)
var member = await _rest.GetGuildMember(evt.GuildId!.Value, evt.UserId);
var requiredPerms = PermissionSet.ViewChannel | PermissionSet.SendMessages;
if (member == null || !_cache.PermissionsFor(evt.ChannelId, member).HasFlag(requiredPerms)) return;
if (msg.System.PingsEnabled)
{
// If the system has pings enabled, go ahead
await _rest.CreateMessage(evt.ChannelId, new()
{
Content = $"Psst, **{msg.Member.DisplayName()}** (<@{msg.Message.Sender}>), you have been pinged by <@{evt.UserId}>.",
Components = new []
Components = new[]
{
new MessageComponent
{
@ -208,7 +208,7 @@ namespace PluralKit.Bot
}
}
},
AllowedMentions = new AllowedMentions {Users = new[] {msg.Message.Sender}}
AllowedMentions = new AllowedMentions { Users = new[] { msg.Message.Sender } }
});
}
else
@ -221,7 +221,7 @@ namespace PluralKit.Bot
{
Content = $"{Emojis.Error} {msg.Member.DisplayName()}'s system has disabled reaction pings. If you want to mention them anyway, you can copy/paste the following message:"
});
await _rest.CreateMessage(dm.Id, new MessageRequest {Content = $"<@{msg.Message.Sender}>".AsCode()});
await _rest.CreateMessage(dm.Id, new MessageRequest { Content = $"<@{msg.Message.Sender}>".AsCode() });
}
catch (ForbiddenException) { }
}

View file

@ -24,29 +24,29 @@ namespace PluralKit.Bot
// Load configuration and run global init stuff
var config = InitUtils.BuildConfiguration(args).Build();
InitUtils.InitStatic();
// Set up DI container and modules
var services = BuildContainer(config);
return RunWrapper(services, async ct =>
{
// init version service
await BuildInfoService.LoadVersion();
var logger = services.Resolve<ILogger>().ForContext<Init>();
// Initialize Sentry SDK, and make sure it gets dropped at the end
using var _ = Sentry.SentrySdk.Init(services.Resolve<CoreConfig>().SentryUrl);
// "Connect to the database" (ie. set off database migrations and ensure state)
logger.Information("Connecting to database");
await services.Resolve<IDatabase>().ApplyMigrations();
// Init the bot instance itself, register handlers and such to the client before beginning to connect
logger.Information("Initializing bot");
var bot = services.Resolve<Bot>();
bot.Init();
// Install observer for request/responses
DiscordRequestObserver.Install(services);
@ -80,14 +80,14 @@ namespace PluralKit.Bot
var shutdown = new TaskCompletionSource<object>();
var gracefulShutdownCts = new CancellationTokenSource();
Console.CancelKeyPress += delegate
{
// ReSharper disable once AccessToDisposedClosure (will only be hit before the below disposal)
logger.Information("Received SIGINT/Ctrl-C, attempting graceful shutdown...");
gracefulShutdownCts.Cancel();
};
AppDomain.CurrentDomain.ProcessExit += (_, __) =>
{
// This callback is fired on a SIGKILL is sent.
@ -109,9 +109,9 @@ namespace PluralKit.Bot
{
logger.Fatal(e, "Error while running bot");
}
// Allow the log buffer to flush properly before exiting
((Logger) logger).Dispose();
((Logger)logger).Dispose();
await Task.Delay(500);
shutdown.SetResult(null);
}
@ -138,18 +138,18 @@ namespace PluralKit.Bot
var cluster = services.Resolve<Cluster>();
var config = services.Resolve<BotConfig>();
if (config.Cluster != null)
{
// For multi-instance deployments, calculate the "span" of shards this node is responsible for
var totalNodes = config.Cluster.TotalNodes;
var totalShards = config.Cluster.TotalShards;
var nodeIndex = ExtractNodeIndex(config.Cluster.NodeName);
// Should evenly distribute shards even with an uneven amount of nodes
var shardMin = (int) Math.Round(totalShards * (float) nodeIndex / totalNodes);
var shardMax = (int) Math.Round(totalShards * (float) (nodeIndex + 1) / totalNodes) - 1;
var shardMin = (int)Math.Round(totalShards * (float)nodeIndex / totalNodes);
var shardMax = (int)Math.Round(totalShards * (float)(nodeIndex + 1) / totalNodes) - 1;
await cluster.Start(info.Url, shardMin, shardMax, totalShards, info.SessionStartLimit.MaxConcurrency);
}
else

View file

@ -21,7 +21,7 @@ namespace PluralKit.Bot.Interactive
protected readonly TaskCompletionSource _tcs = new();
protected Message _message { get; private set; }
protected bool _running;
protected BaseInteractive(Context ctx)
{
_ctx = ctx;
@ -33,7 +33,7 @@ namespace PluralKit.Bot.Interactive
{
var dispatch = _ctx.Services.Resolve<InteractionDispatchService>();
var customId = dispatch.Register(handler, Timeout);
var button = new Button
{
Label = label,
@ -56,16 +56,17 @@ namespace PluralKit.Bot.Interactive
protected async Task Finish(InteractionContext? ctx = null)
{
foreach (var button in _buttons)
foreach (var button in _buttons)
button.Disabled = true;
if (ctx != null)
await Update(ctx);
else
await _ctx.Rest.EditMessage(_message.ChannelId, _message.Id, new MessageEditRequest {
else
await _ctx.Rest.EditMessage(_message.ChannelId, _message.Id, new MessageEditRequest
{
Components = GetComponents()
});
_tcs.TrySetResult();
}
@ -95,12 +96,12 @@ namespace PluralKit.Bot.Interactive
public void Setup(Context ctx)
{
var dispatch = ctx.Services.Resolve<InteractionDispatchService>();
foreach (var button in _buttons)
foreach (var button in _buttons)
button.CustomId = dispatch.Register(button.Handler, Timeout);
}
public abstract Task Start();
public async Task Run()
{
if (_running)
@ -108,7 +109,7 @@ namespace PluralKit.Bot.Interactive
_running = true;
await Start();
var cts = new CancellationTokenSource(Timeout.ToTimeSpan());
cts.Token.Register(() => _tcs.TrySetException(new TimeoutException("Action timed out")));
@ -125,7 +126,7 @@ namespace PluralKit.Bot.Interactive
protected void Cleanup()
{
var dispatch = _ctx.Services.Resolve<InteractionDispatchService>();
foreach (var button in _buttons)
foreach (var button in _buttons)
dispatch.Unregister(button.CustomId!);
}
}

View file

@ -15,9 +15,9 @@ namespace PluralKit.Bot.Interactive
public MessageComponent ToMessageComponent() => new()
{
Type = ComponentType.Button,
Label = Label,
Style = Style,
Type = ComponentType.Button,
Label = Label,
Style = Style,
CustomId = CustomId,
Disabled = Disabled
};

View file

@ -23,7 +23,7 @@ namespace PluralKit.Bot.Interactive
public string CancelLabel { get; set; } = "Cancel";
public ButtonStyle CancelStyle { get; set; } = ButtonStyle.Secondary;
public override async Task Start()
{
AddButton(ctx => OnButtonClick(ctx, true), AcceptLabel, AcceptStyle);
@ -32,7 +32,7 @@ namespace PluralKit.Bot.Interactive
AllowedMentions mentions = null;
if (User != _ctx.Author.Id)
mentions = new AllowedMentions {Users = new[] {User!.Value}};
mentions = new AllowedMentions { Users = new[] { User!.Value } };
await Send(Message, mentions: mentions);
}
@ -44,7 +44,7 @@ namespace PluralKit.Bot.Interactive
await Update(ctx);
return;
}
Result = result;
await Finish(ctx);
}
@ -82,11 +82,11 @@ namespace PluralKit.Bot.Interactive
_running = true;
var queue = _ctx.Services.Resolve<HandlerQueue<MessageCreateEvent>>();
var messageDispatch = queue.WaitFor(MessagePredicate, Timeout, cts.Token);
await Start();
cts.Token.Register(() => _tcs.TrySetException(new TimeoutException("Action timed out")));
try
@ -101,7 +101,7 @@ namespace PluralKit.Bot.Interactive
}
}
public YesNoPrompt(Context ctx): base(ctx)
public YesNoPrompt(Context ctx) : base(ctx)
{
User = ctx.Author.Id;
}

View file

@ -69,7 +69,7 @@ namespace PluralKit.Bot
builder.RegisterType<SystemLink>().AsSelf();
builder.RegisterType<SystemList>().AsSelf();
builder.RegisterType<Token>().AsSelf();
// Bot core
builder.RegisterType<Bot>().AsSelf().SingleInstance();
builder.RegisterType<MessageCreated>().As<IEventHandler<MessageCreateEvent>>();
@ -77,11 +77,11 @@ namespace PluralKit.Bot
builder.RegisterType<MessageEdited>().As<IEventHandler<MessageUpdateEvent>>();
builder.RegisterType<ReactionAdded>().As<IEventHandler<MessageReactionAddEvent>>();
builder.RegisterType<InteractionCreated>().As<IEventHandler<InteractionCreateEvent>>();
// Event handler queue
builder.RegisterType<HandlerQueue<MessageCreateEvent>>().AsSelf().SingleInstance();
builder.RegisterType<HandlerQueue<MessageReactionAddEvent>>().AsSelf().SingleInstance();
// Bot services
builder.RegisterType<EmbedService>().AsSelf().SingleInstance();
builder.RegisterType<ProxyService>().AsSelf().SingleInstance();
@ -97,7 +97,7 @@ namespace PluralKit.Bot
builder.RegisterType<ErrorMessageService>().AsSelf().SingleInstance();
builder.RegisterType<CommandMessageService>().AsSelf().SingleInstance();
builder.RegisterType<InteractionDispatchService>().AsSelf().SingleInstance();
// Sentry stuff
builder.Register(_ => new Scope(null)).AsSelf().InstancePerLifetimeScope();
builder.RegisterType<SentryEnricher>()
@ -107,7 +107,7 @@ namespace PluralKit.Bot
.As<ISentryEnricher<MessageDeleteBulkEvent>>()
.As<ISentryEnricher<MessageReactionAddEvent>>()
.SingleInstance();
// Proxy stuff
builder.RegisterType<ProxyMatcher>().AsSelf().SingleInstance();
builder.RegisterType<ProxyTagParser>().AsSelf().SingleInstance();
@ -116,7 +116,7 @@ namespace PluralKit.Bot
builder.Register(c => new HttpClient
{
Timeout = TimeSpan.FromSeconds(5),
DefaultRequestHeaders = {{"User-Agent", DiscordApiClient.UserAgent}}
DefaultRequestHeaders = { { "User-Agent", DiscordApiClient.UserAgent } }
}).AsSelf().SingleInstance();
builder.RegisterInstance(SystemClock.Instance).As<IClock>();
builder.RegisterType<SerilogGatewayEnricherFactory>().AsSelf().SingleInstance();

View file

@ -1,4 +1,4 @@
#nullable enable
#nullable enable
using PluralKit.Core;
namespace PluralKit.Bot
@ -8,7 +8,7 @@ namespace PluralKit.Bot
public ProxyMember Member;
public string? Content;
public ProxyTag? ProxyTags;
public string? ProxyContent
{
get

View file

@ -1,4 +1,4 @@
using System.Collections.Generic;
using System.Collections.Generic;
using System.Linq;
using NodaTime;
@ -32,7 +32,7 @@ namespace PluralKit.Bot
private bool TryMatchTags(IReadOnlyCollection<ProxyMember> members, string messageContent, bool hasAttachments, out ProxyMatch match)
{
if (!_parser.TryMatch(members, messageContent, out match)) return false;
// Edge case: If we got a match with blank inner text, we'd normally just send w/ attachments
// However, if there are no attachments, the user probably intended something else, so we "un-match" and proceed to autoproxy
return hasAttachments || match.Content.Trim().Length > 0;
@ -50,19 +50,19 @@ namespace PluralKit.Bot
// Find the member we should autoproxy (null if none)
var member = ctx.AutoproxyMode switch
{
AutoproxyMode.Member when ctx.AutoproxyMember != null =>
AutoproxyMode.Member when ctx.AutoproxyMember != null =>
members.FirstOrDefault(m => m.Id == ctx.AutoproxyMember),
AutoproxyMode.Front when ctx.LastSwitchMembers.Length > 0 =>
AutoproxyMode.Front when ctx.LastSwitchMembers.Length > 0 =>
members.FirstOrDefault(m => m.Id == ctx.LastSwitchMembers[0]),
AutoproxyMode.Latch when ctx.LastMessageMember != null =>
members.FirstOrDefault(m => m.Id == ctx.LastMessageMember.Value),
_ => null
};
// Throw an error if the member is null, message varies depending on autoproxy mode
if (member == null)
if (member == null)
{
if (ctx.AutoproxyMode == AutoproxyMode.Front)
throw new ProxyService.ProxyChecksFailedException("You are using autoproxy front, but no members are currently registered as fronting. Please use `pk;switch <member>` to log a new switch.");
@ -78,13 +78,13 @@ namespace PluralKit.Bot
// Moved the IsLatchExpired() check to here, so that an expired latch and a latch without any previous messages throw different errors
if (ctx.AutoproxyMode == AutoproxyMode.Latch && IsLatchExpired(ctx))
throw new ProxyService.ProxyChecksFailedException("Latch-mode autoproxy has timed out. Please send a new message using proxy tags.");
throw new ProxyService.ProxyChecksFailedException("Latch-mode autoproxy has timed out. Please send a new message using proxy tags.");
match = new ProxyMatch
{
Content = messageContent,
Member = member,
// We're autoproxying, so not using any proxy tags here
// we just find the first pair of tags (if any), otherwise null
ProxyTags = member.ProxyTags.FirstOrDefault()
@ -98,7 +98,7 @@ namespace PluralKit.Bot
if (ctx.LatchTimeout == 0) return false;
var timeout = ctx.LatchTimeout.HasValue
? Duration.FromSeconds(ctx.LatchTimeout.Value)
? Duration.FromSeconds(ctx.LatchTimeout.Value)
: DefaultLatchExpiryTime;
var timestamp = DiscordUtils.SnowflakeToInstant(ctx.LastMessage.Value);

View file

@ -53,7 +53,7 @@ namespace PluralKit.Bot
public async Task<bool> HandleIncomingMessage(Shard shard, MessageCreateEvent message, MessageContext ctx, Guild guild, Channel channel, bool allowAutoproxy, PermissionSet botPermissions)
{
if (!ShouldProxy(channel, message, ctx))
if (!ShouldProxy(channel, message, ctx))
return false;
// Fetch members and try to match to a specific member
@ -64,7 +64,7 @@ namespace PluralKit.Bot
List<ProxyMember> members;
using (_metrics.Measure.Timer.Time(BotMetrics.ProxyMembersQueryTime))
members = (await _repo.GetProxyMembers(conn, message.Author.Id, message.GuildId!.Value)).ToList();
if (!_matcher.TryMatch(ctx, members, out var match, message.Content, message.Attachments.Length > 0,
allowAutoproxy)) return false;
@ -72,12 +72,12 @@ namespace PluralKit.Bot
if (message.Content != null && message.Content.Length > 2000) throw new PKError("PluralKit cannot proxy messages over 2000 characters in length.");
// Permission check after proxy match so we don't get spammed when not actually proxying
if (!await CheckBotPermissionsOrError(botPermissions, rootChannel.Id))
if (!await CheckBotPermissionsOrError(botPermissions, rootChannel.Id))
return false;
// this method throws, so no need to wrap it in an if statement
CheckProxyNameBoundsOrError(match.Member.ProxyName(ctx));
// Check if the sender account can mention everyone/here + embed links
// we need to "mirror" these permissions when proxying to prevent exploits
var senderPermissions = PermissionExtensions.PermissionsFor(guild, rootChannel, message);
@ -94,17 +94,17 @@ namespace PluralKit.Bot
// Make sure author has a system
if (ctx.SystemId == null)
throw new ProxyChecksFailedException(Errors.NoSystemError.Message);
// Make sure channel is a guild text channel and this is a normal message
if (!DiscordUtils.IsValidGuildChannel(channel))
throw new ProxyChecksFailedException("This channel is not a text channel.");
if (msg.Type != Message.MessageType.Default && msg.Type != Message.MessageType.Reply)
throw new ProxyChecksFailedException("This message is not a normal message.");
// Make sure author is a normal user
if (msg.Author.System == true || msg.Author.Bot || msg.WebhookId != null)
throw new ProxyChecksFailedException("This message was not sent by a normal user.");
// Make sure proxying is enabled here
if (ctx.InBlacklist)
throw new ProxyChecksFailedException($"Proxying was disabled in this channel by a server administrator (via the proxy blacklist).");
@ -112,12 +112,12 @@ namespace PluralKit.Bot
// Make sure the system has proxying enabled in the server
if (!ctx.ProxyEnabled)
throw new ProxyChecksFailedException("Your system has proxying disabled in this server. Type `pk;proxy on` to enable it.");
// Make sure we have either an attachment or message content
var isMessageBlank = msg.Content == null || msg.Content.Trim().Length == 0;
if (isMessageBlank && msg.Attachments.Length == 0)
throw new ProxyChecksFailedException("Message cannot be blank.");
// All good!
return true;
}
@ -137,17 +137,17 @@ namespace PluralKit.Bot
if (embed != null)
embeds.Add(embed);
}
// TODO: have a clean error for when message can't be fetched instead of just being silent
}
// Send the webhook
var content = match.ProxyContent;
if (!allowEmbeds) content = content.BreakLinkEmbeds();
var messageChannel = _cache.GetChannel(trigger.ChannelId);
var rootChannel = _cache.GetRootChannel(trigger.ChannelId);
var threadId = messageChannel.IsThread() ? messageChannel.Id : (ulong?)null;
var threadId = messageChannel.IsThread() ? messageChannel.Id : (ulong?)null;
var proxyMessage = await _webhookExecutor.ExecuteWebhook(new ProxyRequest
{
@ -186,7 +186,7 @@ namespace PluralKit.Bot
{
// repliedTo doesn't have a GuildId field :/
var jumpLink = $"https://discord.com/channels/{trigger.GuildId}/{repliedTo.ChannelId}/{repliedTo.Id}";
var content = new StringBuilder();
var hasContent = !string.IsNullOrWhiteSpace(repliedTo.Content);
@ -211,7 +211,7 @@ namespace PluralKit.Bot
var urlTail = repliedTo.Content.Substring(100).Split(" ")[0];
msg += urlTail + " ";
}
var spoilersInOriginalString = Regex.Matches(repliedTo.Content, @"\|\|").Count;
var spoilersInTruncatedString = Regex.Matches(msg, @"\|\|").Count;
if (spoilersInTruncatedString % 2 == 1 && spoilersInOriginalString % 2 == 0)
@ -219,7 +219,7 @@ namespace PluralKit.Bot
if (msg != repliedTo.Content)
msg += "…";
}
content.Append($"**[Reply to:]({jumpLink})** ");
content.Append(msg);
if (repliedTo.Attachments.Length > 0)
@ -229,7 +229,7 @@ namespace PluralKit.Bot
{
content.Append($"*[(click to see attachment)]({jumpLink})*");
}
var username = nickname ?? repliedTo.Author.Username;
var avatarUrl = avatar != null
? $"https://cdn.discordapp.com/guilds/{trigger.GuildId}/users/{repliedTo.Author.Id}/{avatar}.png"
@ -247,12 +247,12 @@ namespace PluralKit.Bot
private async Task<string> FixSameName(ulong channelId, MessageContext ctx, ProxyMember member)
{
var proxyName = member.ProxyName(ctx);
var lastMessage = _lastMessage.GetLastMessage(channelId)?.Previous;
if (lastMessage == null)
// cache is out of date or channel is empty.
return proxyName;
await using var conn = await _db.Obtain();
var pkMessage = await _repo.GetMessage(conn, lastMessage.Id);
@ -294,9 +294,9 @@ namespace PluralKit.Bot
};
Task SaveMessageInDatabase() => _repo.AddMessage(conn, sentMessage);
Task LogMessageToChannel() => _logChannel.LogMessage(ctx, sentMessage, triggerMessage, proxyMessage).AsTask();
async Task DeleteProxyTriggerMessage()
{
// Wait a second or so before deleting the original message
@ -307,13 +307,13 @@ namespace PluralKit.Bot
}
catch (NotFoundException)
{
_logger.Debug("Trigger message {TriggerMessageId} was already deleted when we attempted to; deleting proxy message {ProxyMessageId} also",
_logger.Debug("Trigger message {TriggerMessageId} was already deleted when we attempted to; deleting proxy message {ProxyMessageId} also",
triggerMessage.Id, proxyMessage.Id);
await HandleTriggerAlreadyDeleted(proxyMessage);
// Swallow the exception, we don't need it
}
}
// Run post-proxy actions (simultaneously; order doesn't matter)
// Note that only AddMessage is using our passed-in connection, careful not to pass it elsewhere and run into conflicts
await Task.WhenAll(
@ -341,7 +341,7 @@ namespace PluralKit.Bot
{
// If we can't send messages at all, just bail immediately.
// 2020-04-22: Manage Messages does *not* override a lack of Send Messages.
if (!permissions.HasFlag(PermissionSet.SendMessages))
if (!permissions.HasFlag(PermissionSet.SendMessages))
return false;
if (!permissions.HasFlag(PermissionSet.ManageWebhooks))
@ -370,9 +370,9 @@ namespace PluralKit.Bot
{
if (proxyName.Length > Limits.MaxProxyNameLength) throw Errors.ProxyNameTooLong(proxyName);
}
public class ProxyChecksFailedException : Exception
public class ProxyChecksFailedException: Exception
{
public ProxyChecksFailedException(string message) : base(message) {}
public ProxyChecksFailedException(string message) : base(message) { }
}
}
}
}

View file

@ -1,4 +1,4 @@
#nullable enable
#nullable enable
using System.Collections.Generic;
using System.Linq;
@ -11,10 +11,10 @@ namespace PluralKit.Bot
public bool TryMatch(IEnumerable<ProxyMember> members, string? input, out ProxyMatch result)
{
result = default;
// Null input is valid and is equivalent to empty string
if (input == null) return false;
// If the message starts with a @mention, and then proceeds to have proxy tags,
// extract the mention and place it inside the inner message
// eg. @Ske [text] => [@Ske text]
@ -26,13 +26,13 @@ namespace PluralKit.Bot
var tags = members
.SelectMany(member => member.ProxyTags.Select(tag => (tag, member)))
.OrderByDescending(p => p.tag.ProxyString.Length);
// Iterate now-ordered list of tags and try matching each one
foreach (var (tag, member) in tags)
{
result.ProxyTags = tag;
result.Member = member;
// Skip blank tags (shouldn't ever happen in practice)
if (tag.Prefix == null && tag.Suffix == null) continue;
@ -43,10 +43,10 @@ namespace PluralKit.Bot
if (leadingMention != null) result.Content = $"{leadingMention} {result.Content}";
return true;
}
// (if not, keep going)
}
// We couldn't match anything :(
return false;
}
@ -54,25 +54,25 @@ namespace PluralKit.Bot
private bool TryMatchTagsInner(string input, ProxyTag tag, out string inner)
{
inner = "";
// Normalize null tags to empty strings
var prefix = tag.Prefix ?? "";
var suffix = tag.Suffix ?? "";
// Check if our input starts/ends with the tags
var isMatch = input.Length >= prefix.Length + suffix.Length
var isMatch = input.Length >= prefix.Length + suffix.Length
&& input.StartsWith(prefix) && input.EndsWith(suffix);
// Special case: image-only proxies + proxy tags with spaces
// Trim everything, then see if we have a "contentless tag pair" (normally disallowed, but OK if we have an attachment)
// Note `input` is still "", even if there are spaces between
if (!isMatch && input.Trim() == prefix.TrimEnd() + suffix.TrimStart())
return true;
if (!isMatch) return false;
if (!isMatch) return false;
// We got a match, extract inner text
inner = input.Substring(prefix.Length, input.Length - prefix.Length - suffix.Length);
// (see https://github.com/xSke/PluralKit/pull/181)
return inner.Trim() != "\U0000fe0f";
}
@ -81,7 +81,7 @@ namespace PluralKit.Bot
{
var mentionPos = 0;
if (!DiscordUtils.HasMentionPrefix(input, ref mentionPos, out _)) return null;
var leadingMention = input.Substring(0, mentionPos);
input = input.Substring(mentionPos);
return leadingMention;

View file

@ -1,4 +1,4 @@
using System.Threading.Tasks;
using System.Threading.Tasks;
using NodaTime;
@ -11,12 +11,12 @@ namespace PluralKit.Bot
public class CommandMessageService
{
private static readonly Duration CommandMessageRetention = Duration.FromHours(2);
private readonly IDatabase _db;
private readonly ModelRepository _repo;
private readonly IClock _clock;
private readonly ILogger _logger;
public CommandMessageService(IDatabase db, ModelRepository repo, IClock clock, ILogger logger)
{
_db = db;
@ -42,7 +42,7 @@ namespace PluralKit.Bot
var deleteThresholdSnowflake = DiscordUtils.InstantToSnowflake(deleteThresholdInstant);
var deletedRows = await _db.Execute(conn => _repo.DeleteCommandMessagesBefore(conn, deleteThresholdSnowflake));
_logger.Information("Pruned {DeletedRows} command messages older than retention {Retention} (older than {DeleteThresholdInstant} / {DeleteThresholdSnowflake})",
deletedRows, CommandMessageRetention, deleteThresholdInstant, deleteThresholdSnowflake);
}

View file

@ -8,7 +8,7 @@ namespace PluralKit.Bot
public class CpuStatService
{
private readonly ILogger _logger;
public double LastCpuMeasure { get; private set; }
public CpuStatService(ILogger logger)
@ -23,15 +23,15 @@ namespace PluralKit.Bot
{
// We get the current processor time, wait 5 seconds, then compare
// https://medium.com/@jackwild/getting-cpu-usage-in-net-core-7ef825831b8b
_logger.Debug("Estimating CPU usage...");
var stopwatch = new Stopwatch();
stopwatch.Start();
var cpuTimeBefore = Process.GetCurrentProcess().TotalProcessorTime;
await Task.Delay(5000);
stopwatch.Stop();
var cpuTimeAfter = Process.GetCurrentProcess().TotalProcessorTime;

View file

@ -16,7 +16,8 @@ using NodaTime;
using PluralKit.Core;
namespace PluralKit.Bot {
namespace PluralKit.Bot
{
public class EmbedService
{
private readonly IDatabase _db;
@ -42,11 +43,11 @@ namespace PluralKit.Bot {
return Task.WhenAll(ids.Select(Inner));
}
public async Task<Embed> CreateSystemEmbed(Context cctx, PKSystem system, LookupContext ctx)
{
await using var conn = await _db.Obtain();
// Fetch/render info for all accounts simultaneously
var accounts = await _repo.GetSystemAccounts(conn, system.Id);
var users = (await GetUsers(accounts)).Select(x => x.User?.NameAndMention() ?? $"(deleted account {x.Id})");
@ -81,13 +82,13 @@ namespace PluralKit.Bot {
eb.Field(new("Fronter".ToQuantity(switchMembers.Count, ShowQuantityAs.None), string.Join(", ", switchMembers.Select(m => m.NameFor(ctx)))));
}
if (system.Tag != null)
if (system.Tag != null)
eb.Field(new("Tag", system.Tag.EscapeMarkdown(), true));
if (cctx.Guild != null)
{
var guildSettings = await _repo.GetSystemGuild(conn, cctx.Guild.Id, system.Id);
if (guildSettings.Tag != null && guildSettings.TagEnabled)
eb.Field(new($"Tag (in server '{cctx.Guild.Name}')", guildSettings.Tag
.EscapeMarkdown(), true));
@ -114,7 +115,8 @@ namespace PluralKit.Bot {
return eb.Build();
}
public Embed CreateLoggedMessageEmbed(Message triggerMessage, Message proxiedMessage, string systemHid, PKMember member, string channelName, string oldContent = null) {
public Embed CreateLoggedMessageEmbed(Message triggerMessage, Message proxiedMessage, string systemHid, PKMember member, string channelName, string oldContent = null)
{
// TODO: pronouns in ?-reacted response using this card
var timestamp = DiscordUtils.SnowflakeToInstant(proxiedMessage.Id);
var name = proxiedMessage.Author.Username;
@ -129,7 +131,7 @@ namespace PluralKit.Bot {
if (oldContent != null)
embed.Field(new("Old message", oldContent?.NormalizeLineEndSpacing().Truncate(1000)));
return embed.Build();
}
@ -155,7 +157,7 @@ namespace PluralKit.Bot {
}
await using var conn = await _db.Obtain();
var guildSettings = guild != null ? await _repo.GetMemberGuild(conn, guild.Id, member.Id) : null;
var guildDisplayName = guildSettings?.DisplayName;
var avatar = guildSettings?.AvatarUrl ?? member.AvatarFor(ctx);
@ -179,19 +181,19 @@ namespace PluralKit.Bot {
var description = "";
if (member.MemberVisibility == PrivacyLevel.Private) description += "*(this member is hidden)*\n";
if (guildSettings?.AvatarUrl != null)
if (member.AvatarFor(ctx) != null)
if (member.AvatarFor(ctx) != null)
description += $"*(this member has a server-specific avatar set; [click here]({member.AvatarUrl.TryGetCleanCdnUrl()}) to see the global avatar)*\n";
else
description += "*(this member has a server-specific avatar set)*\n";
if (description != "") eb.Description(description);
if (avatar != null) eb.Thumbnail(new(avatar.TryGetCleanCdnUrl()));
if (!member.DisplayName.EmptyOrNull() && member.NamePrivacy.CanAccess(ctx)) eb.Field(new("Display Name", member.DisplayName.Truncate(1024), true));
if (guild != null && guildDisplayName != null) eb.Field(new($"Server Nickname (for {guild.Name})", guildDisplayName.Truncate(1024), true));
if (member.BirthdayFor(ctx) != null) eb.Field(new("Birthdate", member.BirthdayString, true));
if (member.PronounsFor(ctx) is {} pronouns && !string.IsNullOrWhiteSpace(pronouns)) eb.Field(new("Pronouns", pronouns.Truncate(1024), true));
if (member.MessageCountFor(ctx) is {} count && count > 0) eb.Field(new("Message Count", member.MessageCount.ToString(), true));
if (member.PronounsFor(ctx) is { } pronouns && !string.IsNullOrWhiteSpace(pronouns)) eb.Field(new("Pronouns", pronouns.Truncate(1024), true));
if (member.MessageCountFor(ctx) is { } count && count > 0) eb.Field(new("Message Count", member.MessageCount.ToString(), true));
if (member.HasProxyTags) eb.Field(new("Proxy Tags", member.ProxyTagsString("\n").Truncate(1024), true));
// --- For when this gets added to the member object itself or however they get added
// if (member.LastMessage != null && member.MetadataPrivacy.CanAccess(ctx)) eb.AddField("Last message:" FormatTimestamp(DiscordUtils.SnowflakeToInstant(m.LastMessage.Value)));
@ -208,7 +210,7 @@ namespace PluralKit.Bot {
eb.Field(new($"Groups ({groups.Count})", content.Truncate(1000)));
}
if (member.DescriptionFor(ctx) is {} desc)
if (member.DescriptionFor(ctx) is { } desc)
eb.Field(new("Description", member.Description.NormalizeLineEndSpacing(), false));
return eb.Build();
@ -217,7 +219,7 @@ namespace PluralKit.Bot {
public async Task<Embed> CreateGroupEmbed(Context ctx, PKSystem system, PKGroup target)
{
await using var conn = await _db.Obtain();
var pctx = ctx.LookupContextFor(system);
var memberCount = ctx.MatchPrivateFlag(pctx) ? await _repo.GetGroupMemberCount(conn, target.Id, PrivacyLevel.Public) : await _repo.GetGroupMemberCount(conn, target.Id);
@ -246,7 +248,7 @@ namespace PluralKit.Bot {
if (target.DisplayName != null)
eb.Field(new("Display Name", target.DisplayName, true));
if (!target.Color.EmptyOrNull()) eb.Field(new("Color", $"#{target.Color}", true));
if (target.ListPrivacy.CanAccess(pctx))
@ -261,7 +263,7 @@ namespace PluralKit.Bot {
if (target.DescriptionFor(pctx) is { } desc)
eb.Field(new("Description", desc));
if (target.IconFor(pctx) is {} icon)
if (target.IconFor(pctx) is { } icon)
eb.Thumbnail(new(icon.TryGetCleanCdnUrl()));
return eb.Build();
@ -349,14 +351,14 @@ namespace PluralKit.Bot {
.Select(role => role.Name));
eb.Field(new($"Account roles ({roles.Count})", rolesString.Truncate(1024)));
}
return eb.Build();
}
public Task<Embed> CreateFrontPercentEmbed(FrontBreakdown breakdown, PKSystem system, PKGroup group, DateTimeZone tz, LookupContext ctx, string embedTitle, bool ignoreNoFronters, bool showFlat)
{
string color = system.Color;
if (group != null)
if (group != null)
{
color = group.Color;
}
@ -408,7 +410,7 @@ namespace PluralKit.Bot {
foreach (var pair in membersOrdered)
{
var frac = pair.Value / period;
eb.Field(new(pair.Key?.NameFor(ctx) ?? "*(no fronter)*", $"{frac*100:F0}% ({pair.Value.FormatDuration()})"));
eb.Field(new(pair.Key?.NameFor(ctx) ?? "*(no fronter)*", $"{frac * 100:F0}% ({pair.Value.FormatDuration()})"));
}
if (membersOrdered.Count > maxEntriesToDisplay)
@ -422,4 +424,4 @@ namespace PluralKit.Bot {
return Task.FromResult(eb.Build());
}
}
}
}

View file

@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Concurrent;
using System.Threading.Tasks;
@ -17,11 +17,11 @@ namespace PluralKit.Bot
{
private static readonly Duration MinErrorInterval = Duration.FromSeconds(10);
private readonly ConcurrentDictionary<ulong, Instant> _lastErrorInChannel = new ConcurrentDictionary<ulong, Instant>();
private readonly IMetrics _metrics;
private readonly ILogger _logger;
private readonly DiscordApiClient _rest;
public ErrorMessageService(IMetrics metrics, ILogger logger, DiscordApiClient rest)
{
_metrics = metrics;
@ -53,7 +53,7 @@ namespace PluralKit.Bot
Content = $"> **Error code:** `{errorId}`",
Embed = embed.Build()
});
_logger.Information("Sent error message to {ChannelId} with error code {ErrorId}", channelId, errorId);
_metrics.Measure.Meter.Mark(BotMetrics.ErrorMessagesSent, "sent");
}

View file

@ -12,14 +12,14 @@ namespace PluralKit.Bot
private class Response
{
private readonly Groups.AddRemoveOperation _op;
private readonly string _actionStr;
private readonly string _containStr;
private readonly string _emojiStr;
private readonly bool _memberPlural;
private readonly bool _groupPlural;
private readonly int _actionedOn;
private readonly int _notActionedOn;
@ -27,18 +27,18 @@ namespace PluralKit.Bot
int notActionedOn)
{
_op = action;
_actionStr = action == Groups.AddRemoveOperation.Add ? "added to" : "removed from";
_containStr = action == Groups.AddRemoveOperation.Add ? "in" : "not in";
_emojiStr = actionedOn > 0 ? Emojis.Success : Emojis.Error;
_memberPlural = memberCount > 1;
_groupPlural = groupCount > 1;
// sanity checking: we can't add multiple groups to multiple members (at least for now)
if (_memberPlural && _groupPlural)
throw new ArgumentOutOfRangeException();
// sanity checking: we can't act/not act on a different number of entities than we have
if (_memberPlural && (actionedOn + notActionedOn) != memberCount)
throw new ArgumentOutOfRangeException();
@ -48,7 +48,7 @@ namespace PluralKit.Bot
_actionedOn = actionedOn;
_notActionedOn = notActionedOn;
}
// name generators
private string MemberString(bool capitalize = false)
=> capitalize
@ -59,14 +59,14 @@ namespace PluralKit.Bot
=> capitalize
? (count == 1 ? "Member" : "Members")
: (count == 1 ? "member" : "members");
private string GroupString() => _groupPlural ? "groups" : "group";
private string GroupString(int count)
=> count == 1 ? "group" : "groups";
// string generators
private string ResponseString()
{
if (_actionedOn > 0 && _notActionedOn > 0 && _memberPlural)
@ -100,9 +100,9 @@ namespace PluralKit.Bot
return $" ({msg})";
}
public string ToString() => $"{_emojiStr} {ResponseString()}{InfoMessage()}.";
// |
}
}

View file

@ -18,7 +18,7 @@ namespace PluralKit.Bot
private readonly IClock _clock;
private readonly ILogger _logger;
private readonly Task _cleanupWorker;
public InteractionDispatchService(IClock clock, ILogger logger)
{
_clock = clock;
@ -31,7 +31,7 @@ namespace PluralKit.Bot
{
if (!Guid.TryParse(customId, out var customIdGuid))
return false;
if (!_handlers.TryGetValue(customIdGuid, out var handler))
return false;
@ -52,10 +52,10 @@ namespace PluralKit.Bot
var key = Guid.NewGuid();
var handler = new RegisteredInteraction
{
Callback = callback,
Callback = callback,
Expiry = _clock.GetCurrentInstant() + (expiry ?? DefaultExpiry)
};
_handlers[key] = handler;
return key.ToString();
}
@ -93,7 +93,7 @@ namespace PluralKit.Bot
public void Dispose()
{
_cts.Cancel();
_cts.Cancel();
_cts.Dispose();
}
}

View file

@ -1,4 +1,4 @@
#nullable enable
#nullable enable
using System.Collections.Concurrent;
using System.Collections.Generic;
@ -18,7 +18,7 @@ namespace PluralKit.Bot
_cache[msg.ChannelId] = new(current, previous?.Current);
}
private CachedMessage ToCachedMessage(Message msg) =>
private CachedMessage ToCachedMessage(Message msg) =>
new(msg.Id, msg.ReferencedMessage.Value?.Id, msg.Author.Username);
public CacheEntry? GetLastMessage(ulong channel)
@ -73,4 +73,4 @@ namespace PluralKit.Bot
public record CacheEntry(CachedMessage Current, CachedMessage? Previous);
public record CachedMessage(ulong Id, ulong? ReferencedMessage, string AuthorUsername);
}
}

View file

@ -12,8 +12,10 @@ using PluralKit.Core;
using Serilog;
namespace PluralKit.Bot {
public class LogChannelService {
namespace PluralKit.Bot
{
public class LogChannelService
{
private readonly EmbedService _embed;
private readonly IDatabase _db;
private readonly ModelRepository _repo;
@ -40,7 +42,7 @@ namespace PluralKit.Bot {
return;
var triggerChannel = _cache.GetChannel(proxiedMessage.Channel);
await using var conn = await _db.Obtain();
var system = await _repo.GetSystem(conn, ctx.SystemId.Value);
var member = await _repo.GetMember(conn, proxiedMessage.Member);
@ -48,7 +50,7 @@ namespace PluralKit.Bot {
// Send embed!
var embed = _embed.CreateLoggedMessageEmbed(trigger, hookMessage, system.Hid, member, triggerChannel.Name, oldContent);
var url = $"https://discord.com/channels/{proxiedMessage.Guild.Value}/{proxiedMessage.Channel}/{proxiedMessage.Mid}";
await _rest.CreateMessage(logChannel.Id, new() {Content = url, Embed = embed});
await _rest.CreateMessage(logChannel.Id, new() { Content = url, Embed = embed });
}
private async Task<Channel?> GetAndCheckLogChannel(MessageContext ctx, Message trigger, PKMessage proxiedMessage)
@ -71,17 +73,17 @@ namespace PluralKit.Bot {
}
if (ctx.SystemId == null || logChannelId == null || isBlacklisted) return null;
// Find log channel and check if valid
var logChannel = await FindLogChannel(guildId, logChannelId.Value);
if (logChannel == null || logChannel.Type != Channel.ChannelType.GuildText) return null;
// Check bot permissions
var perms = _bot.PermissionsIn(logChannel.Id);
if (!perms.HasFlag(PermissionSet.SendMessages | PermissionSet.EmbedLinks))
{
_logger.Information(
"Does not have permission to log proxy, ignoring (channel: {ChannelId}, guild: {GuildId}, bot permissions: {BotPermissions})",
"Does not have permission to log proxy, ignoring (channel: {ChannelId}, guild: {GuildId}, bot permissions: {BotPermissions})",
ctx.LogChannel.Value, trigger.GuildId!.Value, perms);
return null;
}
@ -94,12 +96,12 @@ namespace PluralKit.Bot {
// TODO: fetch it directly on cache miss?
if (_cache.TryGetChannel(channelId, out var channel))
return channel;
// Channel doesn't exist or we don't have permission to access it, let's remove it from the database too
_logger.Warning("Attempted to fetch missing log channel {LogChannel} for guild {Guild}, removing from database", channelId, guildId);
await using var conn = await _db.Obtain();
await conn.ExecuteAsync("update servers set log_channel = null where id = @Guild",
new {Guild = guildId});
new { Guild = guildId });
return null;
}

View file

@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
@ -48,15 +48,15 @@ namespace PluralKit.Bot
// There are two "Logger"s. They seem to be entirely unrelated. Don't ask.
new LoggerBot("Logger#6088", 298822483060981760 , ExtractLoggerA, webhookName: "Logger"),
new LoggerBot("Logger#6278", 327424261180620801, ExtractLoggerB),
new LoggerBot("Logger#6278", 327424261180620801, ExtractLoggerB),
new LoggerBot("Dyno", 155149108183695360, ExtractDyno, webhookName: "Dyno"),
new LoggerBot("Auttaja", 242730576195354624, ExtractAuttaja, webhookName: "Auttaja"),
new LoggerBot("GenericBot", 295329346590343168, ExtractGenericBot),
new LoggerBot("blargbot", 134133271750639616, ExtractBlargBot),
new LoggerBot("Mantaro", 213466096718708737, ExtractMantaro),
new LoggerBot("UnbelievaBoat", 292953664492929025, ExtractUnbelievaBoat, webhookName: "UnbelievaBoat"),
new LoggerBot("Vanessa", 310261055060443136, fuzzyExtractFunc: ExtractVanessa),
new LoggerBot("GenericBot", 295329346590343168, ExtractGenericBot),
new LoggerBot("blargbot", 134133271750639616, ExtractBlargBot),
new LoggerBot("Mantaro", 213466096718708737, ExtractMantaro),
new LoggerBot("UnbelievaBoat", 292953664492929025, ExtractUnbelievaBoat, webhookName: "UnbelievaBoat"),
new LoggerBot("Vanessa", 310261055060443136, fuzzyExtractFunc: ExtractVanessa),
new LoggerBot("SafetyAtLast", 401549924199694338, fuzzyExtractFunc: ExtractSAL),
new LoggerBot("GearBot", 349977940198555660, fuzzyExtractFunc: ExtractGearBot),
new LoggerBot("GiselleBot", 356831787445387285, ExtractGiselleBot),
@ -72,7 +72,7 @@ namespace PluralKit.Bot
private readonly IDiscordCache _cache;
private readonly Bot _bot; // todo: get rid of this nasty
private readonly ILogger _logger;
public LoggerCleanService(IDatabase db, DiscordApiClient client, IDiscordCache cache, Bot bot, ILogger logger)
{
_db = db;
@ -87,17 +87,17 @@ namespace PluralKit.Bot
public async ValueTask HandleLoggerBotCleanup(Message msg)
{
var channel = _cache.GetChannel(msg.ChannelId);
if (channel.Type != Channel.ChannelType.GuildText) return;
if (!_bot.PermissionsIn(channel.Id).HasFlag(PermissionSet.ManageMessages)) return;
// If this message is from a *webhook*, check if the name matches one of the bots we know
// TODO: do we need to do a deeper webhook origin check, or would that be too hard on the rate limit?
// If it's from a *bot*, check the bot ID to see if we know it.
LoggerBot bot = null;
if (msg.WebhookId != null) _botsByWebhookName.TryGetValue(msg.Author.Username, out bot);
else if (msg.Author.Bot) _bots.TryGetValue(msg.Author.Id, out bot);
// If we didn't find anything before, or what we found is an unsupported bot, bail
if (bot == null) return;
@ -113,10 +113,10 @@ namespace PluralKit.Bot
// either way but shouldn't be too much, given it's constrained by user ID and guild.
var fuzzy = bot.FuzzyExtractFunc(msg);
if (fuzzy == null) return;
_logger.Debug("Fuzzy logclean for {BotName} on {MessageId}: {@FuzzyExtractResult}",
bot.Name, msg.Id, fuzzy);
var mid = await _db.Execute(conn =>
conn.QuerySingleOrDefaultAsync<ulong?>(
"select mid from messages where sender = @User and mid > @ApproxID and guild = @Guild limit 1",
@ -127,11 +127,11 @@ namespace PluralKit.Bot
ApproxId = DiscordUtils.InstantToSnowflake(
fuzzy.Value.ApproxTimestamp - Duration.FromSeconds(3))
}));
// If we didn't find a corresponding message, bail
if (mid == null)
return;
if (mid == null)
return;
// Otherwise, we can *reasonably assume* that this is a logged deletion, so delete the log message.
await _client.DeleteMessage(msg.ChannelId, msg.Id);
}
@ -140,12 +140,12 @@ namespace PluralKit.Bot
// Other bots give us the message ID itself, and we can just extract that from the database directly.
var extractedId = bot.ExtractFunc(msg);
if (extractedId == null) return; // If we didn't find anything, bail.
_logger.Debug("Pure logclean for {BotName} on {MessageId}: {@FuzzyExtractResult}",
bot.Name, msg.Id, extractedId);
var mid = await _db.Execute(conn => conn.QuerySingleOrDefaultAsync<ulong?>(
"select mid from messages where original_mid = @Mid", new {Mid = extractedId.Value}));
"select mid from messages where original_mid = @Mid", new { Mid = extractedId.Value }));
if (mid == null) return;
// If we've gotten this far, we found a logged deletion of a trigger message. Just yeet it!
@ -167,9 +167,9 @@ namespace PluralKit.Bot
// Regex also checks that this is a deletion.
var stringWithId = msg.Embeds?.FirstOrDefault()?.Description ?? msg.Content;
if (stringWithId == null) return null;
var match = _auttajaRegex.Match(stringWithId);
return match.Success ? ulong.Parse(match.Groups[1].Value) : (ulong?) null;
return match.Success ? ulong.Parse(match.Groups[1].Value) : (ulong?)null;
}
private static ulong? ExtractDyno(Message msg)
@ -178,7 +178,7 @@ namespace PluralKit.Bot
var embed = msg.Embeds?.FirstOrDefault();
if (embed?.Footer == null || !(embed.Description?.Contains("deleted in") ?? false)) return null;
var match = _dynoRegex.Match(embed.Footer.Text ?? "");
return match.Success ? ulong.Parse(match.Groups[1].Value) : (ulong?) null;
return match.Success ? ulong.Parse(match.Groups[1].Value) : (ulong?)null;
}
private static ulong? ExtractLoggerA(Message msg)
@ -188,11 +188,11 @@ namespace PluralKit.Bot
var embed = msg.Embeds?.FirstOrDefault();
if (embed == null) return null;
if (!embed.Description.StartsWith("Message deleted in")) return null;
var idField = embed.Fields.FirstOrDefault(f => f.Name == "ID");
if (idField.Value == null) return null; // "OrDefault" = all-null object
var match = _loggerARegex.Match(idField.Value);
return match.Success ? ulong.Parse(match.Groups[1].Value) : (ulong?) null;
return match.Success ? ulong.Parse(match.Groups[1].Value) : (ulong?)null;
}
private static ulong? ExtractLoggerB(Message msg)
@ -202,7 +202,7 @@ namespace PluralKit.Bot
var embed = msg.Embeds?.FirstOrDefault();
if (embed?.Footer == null || !(embed.Title?.EndsWith("A Message Was Deleted!") ?? false)) return null;
var match = _loggerBRegex.Match(embed.Footer.Text ?? "");
return match.Success ? ulong.Parse(match.Groups[1].Value) : (ulong?) null;
return match.Success ? ulong.Parse(match.Groups[1].Value) : (ulong?)null;
}
private static ulong? ExtractGenericBot(Message msg)
@ -211,7 +211,7 @@ namespace PluralKit.Bot
var embed = msg.Embeds?.FirstOrDefault();
if (embed?.Footer == null || !(embed.Title?.Contains("Message Deleted") ?? false)) return null;
var match = _basicRegex.Match(embed.Footer.Text ?? "");
return match.Success ? ulong.Parse(match.Groups[1].Value) : (ulong?) null;
return match.Success ? ulong.Parse(match.Groups[1].Value) : (ulong?)null;
}
private static ulong? ExtractBlargBot(Message msg)
@ -221,7 +221,7 @@ namespace PluralKit.Bot
if (embed == null || !(embed.Title?.EndsWith("Message Deleted") ?? false)) return null;
var field = embed.Fields.FirstOrDefault(f => f.Name == "Message ID");
var match = _basicRegex.Match(field.Value ?? "");
return match.Success ? ulong.Parse(match.Groups[1].Value) : (ulong?) null;
return match.Success ? ulong.Parse(match.Groups[1].Value) : (ulong?)null;
}
private static ulong? ExtractMantaro(Message msg)
@ -229,7 +229,7 @@ namespace PluralKit.Bot
// Plain message, "Message (ID: [id]) created by [user] (ID: [id]) in channel [channel] was deleted.
if (!(msg.Content?.Contains("was deleted.") ?? false)) return null;
var match = _mantaroRegex.Match(msg.Content);
return match.Success ? ulong.Parse(match.Groups[1].Value) : (ulong?) null;
return match.Success ? ulong.Parse(match.Groups[1].Value) : (ulong?)null;
}
private static FuzzyExtractResult? ExtractCarlBot(Message msg)
@ -239,13 +239,13 @@ namespace PluralKit.Bot
var embed = msg.Embeds?.FirstOrDefault();
if (embed?.Footer == null || embed.Timestamp == null || !(embed.Title?.StartsWith("Message deleted in") ?? false)) return null;
var match = _carlRegex.Match(embed.Footer.Text ?? "");
return match.Success
return match.Success
? new FuzzyExtractResult
{
User = ulong.Parse(match.Groups[1].Value),
User = ulong.Parse(match.Groups[1].Value),
ApproxTimestamp = OffsetDateTimePattern.Rfc3339.Parse(embed.Timestamp).GetValueOrThrow().ToInstant()
}
: (FuzzyExtractResult?) null;
: (FuzzyExtractResult?)null;
}
private static FuzzyExtractResult? ExtractCircle(Message msg)
@ -261,16 +261,17 @@ namespace PluralKit.Bot
var field = embed.Fields.FirstOrDefault(f => f.Name == "Message Author");
if (field.Value == null) return null;
stringWithId = field.Value;
}
}
if (stringWithId == null) return null;
var match = _circleRegex.Match(stringWithId);
return match.Success
? new FuzzyExtractResult {
return match.Success
? new FuzzyExtractResult
{
User = ulong.Parse(match.Groups[1].Value),
ApproxTimestamp = msg.Timestamp().ToInstant()
}
: (FuzzyExtractResult?) null;
: (FuzzyExtractResult?)null;
}
private static FuzzyExtractResult? ExtractPancake(Message msg)
@ -286,18 +287,18 @@ namespace PluralKit.Bot
User = ulong.Parse(match.Groups[1].Value),
ApproxTimestamp = msg.Timestamp().ToInstant()
}
: (FuzzyExtractResult?) null;
: (FuzzyExtractResult?)null;
}
private static ulong? ExtractUnbelievaBoat(Message msg)
{
// Embed author is "Message Deleted", footer contains message ID per regex
var embed = msg.Embeds?.FirstOrDefault();
if (embed?.Footer == null || embed.Author?.Name != "Message Deleted") return null;
var match = _unbelievaboatRegex.Match(embed.Footer.Text ?? "");
return match.Success ? ulong.Parse(match.Groups[1].Value) : (ulong?) null;
return match.Success ? ulong.Parse(match.Groups[1].Value) : (ulong?)null;
}
private static FuzzyExtractResult? ExtractVanessa(Message msg)
{
// Title is "Message Deleted", embed description contains mention
@ -310,9 +311,9 @@ namespace PluralKit.Bot
User = ulong.Parse(match.Groups[1].Value),
ApproxTimestamp = msg.Timestamp().ToInstant()
}
: (FuzzyExtractResult?) null;
: (FuzzyExtractResult?)null;
}
private static FuzzyExtractResult? ExtractSAL(Message msg)
{
// Title is "Message Deleted!", field "Message Author" contains ID
@ -327,7 +328,7 @@ namespace PluralKit.Bot
User = ulong.Parse(match.Groups[1].Value),
ApproxTimestamp = msg.Timestamp().ToInstant()
}
: (FuzzyExtractResult?) null;
: (FuzzyExtractResult?)null;
}
private static FuzzyExtractResult? ExtractGearBot(Message msg)
@ -342,7 +343,7 @@ namespace PluralKit.Bot
User = ulong.Parse(match.Groups[1].Value),
ApproxTimestamp = msg.Timestamp().ToInstant()
}
: (FuzzyExtractResult?) null;
: (FuzzyExtractResult?)null;
}
private static ulong? ExtractGiselleBot(Message msg)
@ -350,7 +351,7 @@ namespace PluralKit.Bot
var embed = msg.Embeds?.FirstOrDefault();
if (embed?.Title == null || embed.Title != "🗑 Message Deleted") return null;
var match = _GiselleRegex.Match(embed?.Description);
return match.Success ? ulong.Parse(match.Groups[1].Value) : (ulong?) null;
return match.Success ? ulong.Parse(match.Groups[1].Value) : (ulong?)null;
}
private static FuzzyExtractResult? ExtractVortex(Message msg)

View file

@ -43,11 +43,11 @@ namespace PluralKit.Bot
{
var stopwatch = new Stopwatch();
stopwatch.Start();
// Aggregate guild/channel stats
var guildCount = 0;
var channelCount = 0;
// No LINQ today, sorry
await foreach (var guild in _cache.GetAllGuilds())
{
@ -58,10 +58,10 @@ namespace PluralKit.Bot
channelCount++;
}
}
_metrics.Measure.Gauge.SetValue(BotMetrics.Guilds, guildCount);
_metrics.Measure.Gauge.SetValue(BotMetrics.Channels, channelCount);
// Aggregate DB stats
var counts = await _db.Execute(c => c.QueryFirstAsync<Counts>("select (select count(*) from systems) as systems, (select count(*) from members) as members, (select count(*) from switches) as switches, (select count(*) from messages) as messages, (select count(*) from groups) as groups"));
_metrics.Measure.Gauge.SetValue(CoreMetrics.SystemCount, counts.Systems);
@ -69,7 +69,7 @@ namespace PluralKit.Bot
_metrics.Measure.Gauge.SetValue(CoreMetrics.SwitchCount, counts.Switches);
_metrics.Measure.Gauge.SetValue(CoreMetrics.MessageCount, counts.Messages);
_metrics.Measure.Gauge.SetValue(CoreMetrics.GroupCount, counts.Groups);
// Process info
var process = Process.GetCurrentProcess();
_metrics.Measure.Gauge.SetValue(CoreMetrics.ProcessPhysicalMemory, process.WorkingSet64);
@ -78,10 +78,10 @@ namespace PluralKit.Bot
_metrics.Measure.Gauge.SetValue(CoreMetrics.ProcessThreads, process.Threads.Count);
_metrics.Measure.Gauge.SetValue(CoreMetrics.ProcessHandles, process.HandleCount);
_metrics.Measure.Gauge.SetValue(CoreMetrics.CpuUsage, await _cpu.EstimateCpuUsage());
// Database info
_metrics.Measure.Gauge.SetValue(CoreMetrics.DatabaseConnections, _countHolder.ConnectionCount);
// Other shiz
_metrics.Measure.Gauge.SetValue(BotMetrics.WebhookCacheSize, _webhookCache.CacheSize);
@ -92,7 +92,7 @@ namespace PluralKit.Bot
public class Counts
{
public int Systems { get; }
public int Members { get; }
public int Members { get; }
public int Switches { get; }
public int Messages { get; }
public int Groups { get; }

View file

@ -38,7 +38,7 @@ namespace PluralKit.Bot
private readonly IDatabase _db;
private readonly ModelRepository _repo;
public ShardInfoService(ILogger logger, Cluster client, IMetrics metrics, IDatabase db, ModelRepository repo)
{
_client = client;
@ -68,18 +68,19 @@ namespace PluralKit.Bot
if (_shardInfo.TryGetValue(shard.ShardId, out var info))
{
// Skip adding listeners if we've seen this shard & already added listeners to it
if (info.HasAttachedListeners)
if (info.HasAttachedListeners)
return;
} else _shardInfo[shard.ShardId] = info = new ShardInfo();
}
else _shardInfo[shard.ShardId] = info = new ShardInfo();
// Call our own SocketOpened listener manually (and then attach the listener properly)
// Register listeners for new shards
shard.Resumed += () => ReadyOrResumed(shard);
shard.Ready += () => ReadyOrResumed(shard);
shard.SocketClosed += (closeStatus, message) => SocketClosed(shard, closeStatus, message);
shard.HeartbeatReceived += latency => Heartbeated(shard, latency);
// Register that we've seen it
info.HasAttachedListeners = true;
}
@ -100,7 +101,7 @@ namespace PluralKit.Bot
info.LastConnectionTime = SystemClock.Instance.GetCurrentInstant();
info.Connected = true;
ReportShardStatus();
_ = ExecuteWithDatabase(async c =>
{
await _repo.SetShardStatus(c, shard.ShardId, PKShardInfo.ShardStatus.Up);
@ -114,7 +115,7 @@ namespace PluralKit.Bot
info.DisconnectionCount++;
info.Connected = false;
ReportShardStatus();
_ = ExecuteWithDatabase(c =>
_repo.SetShardStatus(c, shard.ShardId, PKShardInfo.ShardStatus.Down));
}
@ -125,7 +126,7 @@ namespace PluralKit.Bot
info.LastHeartbeatTime = SystemClock.Instance.GetCurrentInstant();
info.Connected = true;
info.ShardLatency = latency.ToDuration();
_ = ExecuteWithDatabase(c =>
_repo.RegisterShardHeartbeat(c, shard.ShardId, latency.ToDuration()));
}

View file

@ -34,7 +34,7 @@ namespace PluralKit.Bot
_logger = logger.ForContext<WebhookCacheService>();
_webhooks = new ConcurrentDictionary<ulong, Lazy<Task<Webhook>>>();
}
public async Task<Webhook> GetWebhook(ulong channelId)
{
// We cache the webhook through a Lazy<Task<T>>, this way we make sure to only create one webhook per channel
@ -46,14 +46,14 @@ namespace PluralKit.Bot
return _webhooks.GetOrAdd(channelId, new Lazy<Task<Webhook>>(Factory));
}
var lazyWebhookValue = GetWebhookTaskInner();
// If we've cached a failed Task, we need to clear it and try again
// This is so errors don't become "sticky" and *they* in turn get cached (not good)
// although, keep in mind this block gets hit the call *after* the task failed (since we only await it below)
if (lazyWebhookValue.IsValueCreated && lazyWebhookValue.Value.IsFaulted)
{
_logger.Warning(lazyWebhookValue.Value.Exception, "Cached webhook task for {Channel} faulted with below exception", channelId);
// Specifically don't recurse here so we don't infinite-loop - if this one errors too, it'll "stick"
// until next time this function gets hit (which is okay, probably).
_webhooks.TryRemove(channelId, out _);
@ -72,7 +72,7 @@ namespace PluralKit.Bot
{
// note: webhook.ChannelId may not be the same as channelId >.>
_logger.Debug("Refreshing webhook for channel {Channel}", webhook.ChannelId);
_webhooks.TryRemove(webhook.ChannelId, out _);
return await GetWebhook(channelId);
}
@ -81,7 +81,7 @@ namespace PluralKit.Bot
{
_logger.Debug("Webhook for channel {Channel} not found in cache, trying to fetch", channelId);
_metrics.Measure.Meter.Mark(BotMetrics.WebhookCacheMisses);
_logger.Debug("Finding webhook for channel {Channel}", channelId);
var webhooks = await FetchChannelWebhooks(channelId);
@ -89,12 +89,12 @@ namespace PluralKit.Bot
var ourWebhook = webhooks.FirstOrDefault(IsWebhookMine);
if (ourWebhook != null)
return ourWebhook;
// We don't have one, so we gotta create a new one
// but first, make sure we haven't hit the webhook cap yet...
if (webhooks.Length >= 10)
throw new PKError("This channel has the maximum amount of possible webhooks (10) already created. A server admin must delete one or more webhooks so PluralKit can create one for proxying.");
return await DoCreateWebhook(channelId);
}
@ -113,7 +113,7 @@ namespace PluralKit.Bot
return new Webhook[0];
}
}
private async Task<Webhook> DoCreateWebhook(ulong channelId)
{
_logger.Information("Creating new webhook for channel {Channel}", channelId);

View file

@ -21,10 +21,12 @@ using Serilog;
namespace PluralKit.Bot
{
public class WebhookExecutionErrorOnDiscordsEnd: Exception {
public class WebhookExecutionErrorOnDiscordsEnd: Exception
{
}
public class WebhookRateLimited: WebhookExecutionErrorOnDiscordsEnd {
public class WebhookRateLimited: WebhookExecutionErrorOnDiscordsEnd
{
// Exceptions for control flow? don't mind if I do
// TODO: rewrite both of these as a normal exceptional return value (0?) in case of error to be discarded by caller
}
@ -41,7 +43,7 @@ namespace PluralKit.Bot
public Embed[] Embeds { get; init; }
public bool AllowEveryone { get; init; }
}
public class WebhookExecutorService
{
private readonly IDiscordCache _cache;
@ -64,31 +66,32 @@ namespace PluralKit.Bot
public async Task<Message> ExecuteWebhook(ProxyRequest req)
{
_logger.Verbose("Invoking webhook in channel {Channel}", req.ChannelId);
// Get a webhook, execute it
var webhook = await _webhookCache.GetWebhook(req.ChannelId);
var webhookMessage = await ExecuteWebhookInner(webhook, req);
// Log the relevant metrics
_metrics.Measure.Meter.Mark(BotMetrics.MessagesProxied);
_logger.Information("Invoked webhook {Webhook} in channel {Channel} (thread {ThreadId})", webhook.Id,
req.ChannelId, req.ThreadId);
return webhookMessage;
}
public async Task<Message> EditWebhookMessage(ulong channelId, ulong messageId, string newContent)
{
var webhook = await _webhookCache.GetWebhook(channelId);
var allowedMentions = newContent.ParseMentions() with {
var allowedMentions = newContent.ParseMentions() with
{
Roles = Array.Empty<ulong>(),
Parse = Array.Empty<AllowedMentions.ParseType>()
};
return await _rest.EditWebhookMessage(webhook.Id, webhook.Token, messageId,
new WebhookMessageEditRequest {Content = newContent, AllowedMentions = allowedMentions});
new WebhookMessageEditRequest { Content = newContent, AllowedMentions = allowedMentions });
}
private async Task<Message> ExecuteWebhookInner(Webhook webhook, ProxyRequest req, bool hasRetried = false)
{
var guild = _cache.GetGuild(req.GuildId);
@ -96,7 +99,8 @@ namespace PluralKit.Bot
var allowedMentions = content.ParseMentions();
if (!req.AllowEveryone)
allowedMentions = allowedMentions.RemoveUnmentionableRoles(guild) with {
allowedMentions = allowedMentions.RemoveUnmentionableRoles(guild) with
{
// also clear @everyones
Parse = Array.Empty<AllowedMentions.ParseType>()
};
@ -109,18 +113,19 @@ namespace PluralKit.Bot
AvatarUrl = !string.IsNullOrWhiteSpace(req.AvatarUrl) ? req.AvatarUrl : null,
Embeds = req.Embeds
};
MultipartFile[] files = null;
var attachmentChunks = ChunkAttachmentsOrThrow(req.Attachments, 8 * 1024 * 1024);
if (attachmentChunks.Count > 0)
{
_logger.Information("Invoking webhook with {AttachmentCount} attachments totalling {AttachmentSize} MiB in {AttachmentChunks} chunks",
_logger.Information("Invoking webhook with {AttachmentCount} attachments totalling {AttachmentSize} MiB in {AttachmentChunks} chunks",
req.Attachments.Length, req.Attachments.Select(a => a.Size).Sum() / 1024 / 1024, attachmentChunks.Count);
files = await GetAttachmentFiles(attachmentChunks[0]);
}
Message webhookMessage;
using (_metrics.Measure.Timer.Time(BotMetrics.WebhookResponseTime)) {
using (_metrics.Measure.Timer.Time(BotMetrics.WebhookResponseTime))
{
try
{
webhookMessage = await _rest.ExecuteWebhook(webhook.Id, webhook.Token, webhookReq, files, req.ThreadId);
@ -139,14 +144,14 @@ namespace PluralKit.Bot
// but is still in our cache. Invalidate, refresh, try again
_logger.Warning("Error invoking webhook {Webhook} in channel {Channel} (thread {ThreadId})",
webhook.Id, webhook.ChannelId, req.ThreadId);
var newWebhook = await _webhookCache.InvalidateAndRefreshWebhook(req.ChannelId, webhook);
return await ExecuteWebhookInner(newWebhook, req, hasRetried: true);
}
throw;
}
}
}
// We don't care about whether the sending succeeds, and we don't want to *wait* for it, so we just fork it off
var _ = TrySendRemainingAttachments(webhook, req.Name, req.AvatarUrl, attachmentChunks, req.ThreadId);
@ -161,11 +166,11 @@ namespace PluralKit.Bot
for (var i = 1; i < attachmentChunks.Count; i++)
{
var files = await GetAttachmentFiles(attachmentChunks[i]);
var req = new ExecuteWebhookRequest {Username = name, AvatarUrl = avatarUrl};
var req = new ExecuteWebhookRequest { Username = name, AvatarUrl = avatarUrl };
await _rest.ExecuteWebhook(webhook.Id, webhook.Token!, req, files, threadId);
}
}
private async Task<MultipartFile[]> GetAttachmentFiles(IReadOnlyCollection<Message.Attachment> attachments)
{
async Task<MultipartFile> GetStream(Message.Attachment attachment)
@ -184,7 +189,7 @@ namespace PluralKit.Bot
// If any individual attachment is larger than 8MB, will throw an error
var chunks = new List<List<Message.Attachment>>();
var list = new List<Message.Attachment>();
foreach (var attachment in attachments)
{
if (attachment.Size >= sizeThreshold) throw Errors.AttachmentTooLarge;
@ -194,7 +199,7 @@ namespace PluralKit.Bot
chunks.Add(list);
list = new List<Message.Attachment>();
}
list.Add(attachment);
}
@ -212,7 +217,7 @@ namespace PluralKit.Bot
// since Discord blocks webhooks containing the word "Clyde"... for some reason. /shrug
return Regex.Replace(name, "(c)(lyde)", Replacement, RegexOptions.IgnoreCase);
}
private string FixSingleCharacterName(string proxyName)
{
if (proxyName.Length == 1)

View file

@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
@ -30,7 +30,7 @@ namespace PluralKit.Bot
_metrics = metrics;
_logger = logger.ForContext<DiscordRequestObserver>();
}
public void OnCompleted() { }
public void OnError(Exception error) { }
@ -53,10 +53,10 @@ namespace PluralKit.Bot
url = Regex.Replace(url, @"/reactions/[^{/]+/\d+", "/reactions/{emoji}/{user_id}");
url = Regex.Replace(url, @"/reactions/[^{/]+", "/reactions/{emoji}");
url = Regex.Replace(url, @"/invites/[^{/]+", "/invites/{invite_code}");
// catch-all for missed IDs
url = Regex.Replace(url, @"\d{17,19}", "{snowflake}");
return url;
}
@ -66,7 +66,7 @@ namespace PluralKit.Bot
var routePath = NormalizeRoutePath(localPath);
return $"{req.Method} {routePath}";
}
private void HandleException(Exception exc, HttpRequestMessage req)
{
_logger
@ -109,8 +109,8 @@ namespace PluralKit.Bot
if (IsDiscordApiRequest(response))
{
var timer = _metrics.Provider.Timer.Instance(BotMetrics.DiscordApiRequests, new MetricTags(
new[] {"endpoint", "status_code"},
new[] {endpoint, ((int) response.StatusCode).ToString()}
new[] { "endpoint", "status_code" },
new[] { endpoint, ((int)response.StatusCode).ToString() }
));
timer.Record(activity.Duration.Ticks / 10, TimeUnit.Microseconds);
}
@ -131,19 +131,19 @@ namespace PluralKit.Bot
switch (value.Key)
{
case "System.Net.Http.HttpRequestOut.Stop":
{
var data = Unsafe.As<ActivityStopData>(value.Value);
if (data.Response != null)
HandleResponse(data.Response, Activity.Current);
{
var data = Unsafe.As<ActivityStopData>(value.Value);
if (data.Response != null)
HandleResponse(data.Response, Activity.Current);
break;
}
break;
}
case "System.Net.Http.Exception":
{
var data = Unsafe.As<ExceptionData>(value.Value);
HandleException(data.Exception, data.Request);
break;
}
{
var data = Unsafe.As<ExceptionData>(value.Value);
HandleException(data.Exception, data.Request);
break;
}
}
}
@ -151,7 +151,7 @@ namespace PluralKit.Bot
{
DiagnosticListener.AllListeners.Subscribe(new ListenerObserver(services));
}
#pragma warning disable 649
private class ActivityStopData
{
@ -160,7 +160,7 @@ namespace PluralKit.Bot
public HttpRequestMessage Request;
public TaskStatus RequestTaskStatus;
}
private class ExceptionData
{
// Field order here matters!

View file

@ -1,4 +1,4 @@
using System;
using System;
using System.Linq;
using System.Net.Http;
using System.Text.RegularExpressions;
@ -8,11 +8,13 @@ using PluralKit.Core;
using SixLabors.ImageSharp;
namespace PluralKit.Bot {
public static class AvatarUtils {
namespace PluralKit.Bot
{
public static class AvatarUtils
{
public static async Task VerifyAvatarOrThrow(HttpClient client, string url, bool isFullSizeImage = false)
{
if (url.Length > Limits.MaxUriLength)
if (url.Length > Limits.MaxUriLength)
throw Errors.UrlTooLong(url);
// List of MIME types we consider acceptable

View file

@ -17,8 +17,10 @@ using NodaTime;
using PluralKit.Bot.Interactive;
using PluralKit.Core;
namespace PluralKit.Bot {
public static class ContextUtils {
namespace PluralKit.Bot
{
public static class ContextUtils
{
public static async Task<bool> ConfirmClear(this Context ctx, string toClear)
{
if (!(await ctx.PromptYesNo($"{Emojis.Warn} Are you sure you want to clear {toClear}?", "Clear"))) throw Errors.GenericCancelled();
@ -50,7 +52,7 @@ namespace PluralKit.Bot {
if (predicate != null && !predicate.Invoke(evt)) return false; // Check predicate
return true;
}
return await ctx.Services.Resolve<HandlerQueue<MessageReactionAddEvent>>().WaitFor(ReactionPredicate, timeout);
}
@ -58,20 +60,21 @@ namespace PluralKit.Bot {
{
bool Predicate(MessageCreateEvent e) =>
e.Author.Id == ctx.Author.Id && e.ChannelId == ctx.Channel.Id;
var msg = await ctx.Services.Resolve<HandlerQueue<MessageCreateEvent>>()
.WaitFor(Predicate, Duration.FromMinutes(1));
return string.Equals(msg.Content, expectedReply, StringComparison.InvariantCultureIgnoreCase);
}
public static async Task Paginate<T>(this Context ctx, IAsyncEnumerable<T> items, int totalCount, int itemsPerPage, string title, string color, Func<EmbedBuilder, IEnumerable<T>, Task> renderer) {
public static async Task Paginate<T>(this Context ctx, IAsyncEnumerable<T> items, int totalCount, int itemsPerPage, string title, string color, Func<EmbedBuilder, IEnumerable<T>, Task> renderer)
{
// TODO: make this generic enough we can use it in Choose<T> below
var buffer = new List<T>();
await using var enumerator = items.GetAsyncEnumerator();
var pageCount = (int) Math.Ceiling(totalCount / (double) itemsPerPage);
var pageCount = (int)Math.Ceiling(totalCount / (double)itemsPerPage);
async Task<Embed> MakeEmbedForPage(int page)
{
var bufferedItemsNeeded = (page + 1) * itemsPerPage;
@ -79,10 +82,10 @@ namespace PluralKit.Bot {
buffer.Add(enumerator.Current);
var eb = new EmbedBuilder();
eb.Title(pageCount > 1 ? $"[{page+1}/{pageCount}] {title}" : title);
eb.Title(pageCount > 1 ? $"[{page + 1}/{pageCount}] {title}" : title);
if (color != null)
eb.Color(color.ToDiscordColor());
await renderer(eb, buffer.Skip(page*itemsPerPage).Take(itemsPerPage));
await renderer(eb, buffer.Skip(page * itemsPerPage).Take(itemsPerPage));
return eb.Build();
}
@ -94,9 +97,11 @@ namespace PluralKit.Bot {
var _ = ctx.Rest.CreateReactionsBulk(msg, botEmojis); // Again, "fork"
try {
try
{
var currentPage = 0;
while (true) {
while (true)
{
var reaction = await ctx.AwaitReaction(msg, ctx.Author, timeout: Duration.FromMinutes(5));
// Increment/decrement page counter based on which reaction was clicked
@ -105,19 +110,21 @@ namespace PluralKit.Bot {
if (reaction.Emoji.Name == "\u27A1") currentPage = (currentPage + 1) % pageCount; // >
if (reaction.Emoji.Name == "\u23E9") currentPage = pageCount - 1; // >>
if (reaction.Emoji.Name == Emojis.Error) break; // X
// C#'s % operator is dumb and wrong, so we fix negative numbers
if (currentPage < 0) currentPage += pageCount;
// If we can, remove the user's reaction (so they can press again quickly)
if (ctx.BotPermissions.HasFlag(PermissionSet.ManageMessages))
await ctx.Rest.DeleteUserReaction(msg.ChannelId, msg.Id, reaction.Emoji, reaction.UserId);
// Edit the embed with the new page
var embed = await MakeEmbedForPage(currentPage);
await ctx.Rest.EditMessage(msg.ChannelId, msg.Id, new MessageEditRequest {Embed = embed});
await ctx.Rest.EditMessage(msg.ChannelId, msg.Id, new MessageEditRequest { Embed = embed });
}
} catch (TimeoutException) {
}
catch (TimeoutException)
{
// "escape hatch", clean up as if we hit X
}
@ -132,7 +139,7 @@ namespace PluralKit.Bot {
// either way, nothing to do here
catch (ForbiddenException) { }
}
public static async Task<T> Choose<T>(this Context ctx, string description, IList<T> items, Func<T, string> display = null)
{
// Generate a list of :regional_indicator_?: emoji surrogate pairs (starting at codepoint 0x1F1E6)
@ -157,27 +164,27 @@ namespace PluralKit.Bot {
if (items.Count > pageSize)
{
var currPage = 0;
var pageCount = (items.Count-1) / pageSize + 1;
var pageCount = (items.Count - 1) / pageSize + 1;
// Send the original message
var msg = await ctx.Reply($"**[Page {currPage + 1}/{pageCount}]**\n{description}\n{MakeOptionList(currPage)}");
// Add back/forward reactions and the actual indicator emojis
async Task AddEmojis()
{
await ctx.Rest.CreateReaction(msg.ChannelId, msg.Id, new() { Name = "\u2B05" });
await ctx.Rest.CreateReaction(msg.ChannelId, msg.Id, new() { Name = "\u27A1" });
for (int i = 0; i < items.Count; i++)
for (int i = 0; i < items.Count; i++)
await ctx.Rest.CreateReaction(msg.ChannelId, msg.Id, new() { Name = indicators[i] });
}
var _ = AddEmojis(); // Not concerned about awaiting
while (true)
{
// Wait for a reaction
var reaction = await ctx.AwaitReaction(msg, ctx.Author);
// If it's a movement reaction, inc/dec the page index
if (reaction.Emoji.Name == "\u2B05") currPage -= 1; // <
if (reaction.Emoji.Name == "\u27A1") currPage += 1; // >
@ -210,17 +217,17 @@ namespace PluralKit.Bot {
async Task AddEmojis()
{
for (int i = 0; i < items.Count; i++)
await ctx.Rest.CreateReaction(msg.ChannelId, msg.Id, new() {Name = indicators[i]});
await ctx.Rest.CreateReaction(msg.ChannelId, msg.Id, new() { Name = indicators[i] });
}
var _ = AddEmojis();
// Then wait for a reaction and return whichever one we found
var reaction = await ctx.AwaitReaction(msg, ctx.Author,rx => indicators.Contains(rx.Emoji.Name));
var reaction = await ctx.AwaitReaction(msg, ctx.Author, rx => indicators.Contains(rx.Emoji.Name));
return items[Array.IndexOf(indicators, reaction.Emoji.Name)];
}
}
public static async Task BusyIndicator(this Context ctx, Func<Task> f, string emoji = "\u23f3" /* hourglass */)
{
await ctx.BusyIndicator<object>(async () =>
@ -239,13 +246,13 @@ namespace PluralKit.Bot {
try
{
await Task.WhenAll(ctx.Rest.CreateReaction(ctx.Message.ChannelId, ctx.Message.Id, new() {Name = emoji}), task);
await Task.WhenAll(ctx.Rest.CreateReaction(ctx.Message.ChannelId, ctx.Message.Id, new() { Name = emoji }), task);
return await task;
}
finally
{
var _ = ctx.Rest.DeleteOwnReaction(ctx.Message.ChannelId, ctx.Message.Id, new() { Name = emoji });
}
}
}
}
}

View file

@ -24,7 +24,7 @@ namespace PluralKit.Bot
public const uint Green = 0x00cc78;
public const uint Red = 0xef4b3d;
public const uint Gray = 0x979c9f;
private static readonly Regex USER_MENTION = new Regex("<@!?(\\d{17,19})>");
private static readonly Regex ROLE_MENTION = new Regex("<@&(\\d{17,19})>");
private static readonly Regex EVERYONE_HERE_MENTION = new Regex("@(everyone|here)");
@ -36,23 +36,23 @@ namespace PluralKit.Bot
// corresponding to: https://github.com/Khan/simple-markdown/blob/master/src/index.js#L1489
// I added <? and >? at the start/end; they need to be handled specially later...
private static readonly Regex UNBROKEN_LINK_REGEX = new Regex("<?(https?:\\/\\/[^\\s<]+[^<.,:;\"')\\]\\s])>?");
public static string NameAndMention(this User user)
{
return $"{user.Username}#{user.Discriminator} ({user.Mention()})";
}
public static Instant SnowflakeToInstant(ulong snowflake) =>
Instant.FromUtc(2015, 1, 1, 0, 0, 0) + Duration.FromMilliseconds(snowflake >> 22);
public static ulong InstantToSnowflake(Instant time) =>
(ulong) (time - Instant.FromUtc(2015, 1, 1, 0, 0, 0)).TotalMilliseconds << 22;
(ulong)(time - Instant.FromUtc(2015, 1, 1, 0, 0, 0)).TotalMilliseconds << 22;
public static async Task CreateReactionsBulk(this DiscordApiClient rest, Message msg, string[] reactions)
{
foreach (var reaction in reactions)
{
await rest.CreateReaction(msg.ChannelId, msg.Id, new() {Name = reaction});
await rest.CreateReaction(msg.ChannelId, msg.Id, new() { Name = reaction });
}
}
@ -97,22 +97,23 @@ namespace PluralKit.Bot
var users = USER_MENTION.Matches(input).Select(x => ulong.Parse(x.Groups[1].Value));
var roles = ROLE_MENTION.Matches(input).Select(x => ulong.Parse(x.Groups[1].Value));
var everyone = EVERYONE_HERE_MENTION.IsMatch(input);
return new AllowedMentions
{
Users = users.Distinct().ToArray(),
Roles = roles.Distinct().ToArray(),
Parse = everyone ? new[] {AllowedMentions.ParseType.Everyone} : null
Parse = everyone ? new[] { AllowedMentions.ParseType.Everyone } : null
};
}
public static AllowedMentions RemoveUnmentionableRoles(this AllowedMentions mentions, Guild guild)
{
return mentions with {
return mentions with
{
Roles = mentions.Roles
?.Where(id => guild.Roles.FirstOrDefault(r => r.Id == id)?.Mentionable == true)
.ToArray()
};
};
}
public static string EscapeMarkdown(this string input)
@ -146,7 +147,7 @@ namespace PluralKit.Bot
// So, surrounding with two backticks, then escaping all backtick pairs makes it impossible(!) to "break out"
return $"``{EscapeBacktickPair(input)}``";
}
public static EmbedBuilder WithSimpleLineContent(this EmbedBuilder eb, IEnumerable<string> lines)
{
static int CharacterLimit(int pageNumber) =>
@ -178,7 +179,7 @@ namespace PluralKit.Bot
return $"<{match.Value}>";
});
public static string EventType(this IGatewayEvent evt) =>
public static string EventType(this IGatewayEvent evt) =>
evt.GetType().Name.Replace("Event", "");
public static bool HasReactionPermissions(Context ctx)
@ -188,11 +189,11 @@ namespace PluralKit.Bot
}
public static bool IsValidGuildChannel(Channel channel) =>
channel.Type is
channel.Type is
Channel.ChannelType.GuildText or
Channel.ChannelType.GuildNews or
Channel.ChannelType.GuildNews or
Channel.ChannelType.GuildPublicThread or
Channel.ChannelType.GuildPrivateThread or
Channel.ChannelType.GuildNewsThread;
}
}
}

View file

@ -13,7 +13,7 @@ namespace PluralKit.Bot
{
private readonly InteractionCreateEvent _evt;
private readonly ILifetimeScope _services;
public InteractionContext(InteractionCreateEvent evt, ILifetimeScope services)
{
_evt = evt;
@ -55,9 +55,9 @@ namespace PluralKit.Bot
}
public async Task Respond(InteractionResponse.ResponseType type, InteractionApplicationCommandCallbackData? data)
{
{
var rest = _services.Resolve<DiscordApiClient>();
await rest.CreateInteractionResponse(_evt.Id, _evt.Token, new InteractionResponse {Type = type, Data = data});
await rest.CreateInteractionResponse(_evt.Id, _evt.Token, new InteractionResponse { Type = type, Data = data });
}
}
}

View file

@ -5,7 +5,7 @@ namespace PluralKit.Bot.Utils
{
// PK note: class is wholesale copied from Discord.NET (MIT-licensed)
// https://github.com/discord-net/Discord.Net/blob/ff0fea98a65d907fbce07856f1a9ef4aebb9108b/src/Discord.Net.Core/Utils/MentionUtils.cs
/// <summary>
/// Provides a series of helper methods for parsing mentions.
/// </summary>

View file

@ -16,10 +16,11 @@ using Polly.Timeout;
namespace PluralKit.Bot
{
public static class MiscUtils {
public static string ProxyTagsString(this PKMember member, string separator = ", ") =>
public static class MiscUtils
{
public static string ProxyTagsString(this PKMember member, string separator = ", ") =>
string.Join(separator, member.ProxyTags.Select(t => t.ProxyString.AsCode()));
public static bool IsOurProblem(this Exception e)
{
// This function filters out sporadic errors out of our control from being reported to Sentry
@ -40,24 +41,24 @@ namespace PluralKit.Bot
if (e is TimeoutRejectedException) return false;
// 5xxs? also not our problem :^)
if (e is UnknownDiscordRequestException udre && (int) udre.StatusCode >= 500) return false;
if (e is UnknownDiscordRequestException udre && (int)udre.StatusCode >= 500) return false;
// Webhook server errors are also *not our problem*
// (this includes rate limit errors, WebhookRateLimited is a subclass)
if (e is WebhookExecutionErrorOnDiscordsEnd) return false;
// Socket errors are *not our problem*
if (e.GetBaseException() is SocketException) return false;
// Tasks being cancelled for whatver reason are, you guessed it, also not our problem.
if (e is TaskCanceledException) return false;
// Sometimes Discord just times everything out.
if (e is TimeoutException) return false;
// Ignore "Database is shutting down" error
if (e is PostgresException pe && pe.SqlState == "57P03") return false;
// Ignore thread pool exhaustion errors
if (e is NpgsqlException npe && npe.Message.Contains("The connection pool has been exhausted")) return false;

View file

@ -1,4 +1,4 @@
using System.Linq;
using System.Linq;
using System.Text.RegularExpressions;
using PluralKit.Core;
@ -24,21 +24,21 @@ namespace PluralKit.Bot
bool IsSimple(string s) =>
// No spaces, no symbols, allow single quote but not at the start
Regex.IsMatch(s, "^[\\w\\d\\-_'?]+$") && !s.StartsWith("'");
// If it's very long (>25 chars), always use hid
if (name.Length >= 25)
return hid;
// If name is "simple" just use that
if (IsSimple(name))
if (IsSimple(name))
return name;
// If three or fewer "words" and they're all simple individually, quote them
var words = name.Split(' ');
if (words.Length <= 3 && words.All(w => w.Length > 0 && IsSimple(w)))
// Words with double quotes are never "simple" so we're safe to naive-quote here
return $"\"{name}\"";
// Otherwise, just use hid
return hid;
}

View file

@ -7,7 +7,7 @@ using Sentry;
namespace PluralKit.Bot
{
public interface ISentryEnricher<T> where T: IGatewayEvent
public interface ISentryEnricher<T> where T : IGatewayEvent
{
void Enrich(Scope scope, Shard shard, T evt);
}
@ -25,10 +25,10 @@ namespace PluralKit.Bot
{
_bot = bot;
}
// TODO: should this class take the Scope by dependency injection instead?
// Would allow us to create a centralized "chain of handlers" where this class could just be registered as an entry in
public void Enrich(Scope scope, Shard shard, MessageCreateEvent evt)
{
scope.AddBreadcrumb(evt.Content, "event.message", data: new Dictionary<string, string>

View file

@ -1,4 +1,4 @@
using System.Collections.Generic;
using System.Collections.Generic;
using Myriad.Cache;
using Myriad.Extensions;
@ -26,14 +26,14 @@ namespace PluralKit.Bot
{
new("ShardId", new ScalarValue(shard.ShardId)),
};
var (guild, channel) = GetGuildChannelId(evt);
var user = GetUserId(evt);
var message = GetMessageId(evt);
if (guild != null)
props.Add(new("GuildId", new ScalarValue(guild.Value)));
if (channel != null)
{
props.Add(new("ChannelId", new ScalarValue(channel.Value)));
@ -44,10 +44,10 @@ namespace PluralKit.Bot
props.Add(new("BotPermissions", new ScalarValue(botPermissions)));
}
}
if (message != null)
props.Add(new("MessageId", new ScalarValue(message.Value)));
if (user != null)
props.Add(new("UserId", new ScalarValue(user.Value)));
@ -56,7 +56,7 @@ namespace PluralKit.Bot
return new Inner(props);
}
private (ulong?, ulong?) GetGuildChannelId(IGatewayEvent evt) => evt switch
{
ChannelCreateEvent e => (e.GuildId, e.Id),
@ -101,7 +101,7 @@ namespace PluralKit.Bot
{
public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory)
{
foreach (var prop in Properties)
foreach (var prop in Properties)
logEvent.AddPropertyIfAbsent(prop);
}
}