Port more things!

This commit is contained in:
Ske 2020-12-24 14:52:44 +01:00
parent f6fb8204bb
commit 47b16dc51b
26 changed files with 332 additions and 186 deletions

View File

@ -1,6 +1,8 @@
using System.Threading.Tasks;
using Myriad.Gateway;
using Myriad.Rest;
using Myriad.Types;
namespace Myriad.Cache
{
@ -29,7 +31,7 @@ namespace Myriad.Cache
case GuildRoleDeleteEvent grd:
return cache.RemoveRole(grd.GuildId, grd.RoleId);
case MessageCreateEvent mc:
return cache.SaveUser(mc.Author);
return cache.SaveMessageCreate(mc);
}
return default;
@ -46,5 +48,23 @@ namespace Myriad.Cache
foreach (var member in guildCreate.Members)
await cache.SaveUser(member.User);
}
private static async ValueTask SaveMessageCreate(this IDiscordCache cache, MessageCreateEvent evt)
{
await cache.SaveUser(evt.Author);
foreach (var mention in evt.Mentions)
await cache.SaveUser(mention);
}
public static async ValueTask<User?> GetOrFetchUser(this IDiscordCache cache, DiscordApiClient rest, ulong userId)
{
if (cache.TryGetUser(userId, out var cacheUser))
return cacheUser;
var restUser = await rest.GetUser(userId);
if (restUser != null)
await cache.SaveUser(restUser);
return restUser;
}
}
}

View File

@ -6,7 +6,7 @@ namespace Myriad.Extensions
{
public static string Mention(this User user) => $"<@{user.Id}>";
public static string AvatarUrl(this User user) =>
$"https://cdn.discordapp.com/avatars/{user.Id}/{user.Avatar}.png";
public static string AvatarUrl(this User user, string? format = "png", int? size = 128) =>
$"https://cdn.discordapp.com/avatars/{user.Id}/{user.Avatar}.{format}?size={size}";
}
}

View File

@ -27,6 +27,7 @@ namespace Myriad.Gateway
public IReadOnlyDictionary<int, Shard> Shards => _shards;
public ClusterSessionState SessionState => GetClusterState();
public User? User => _shards.Values.Select(s => s.User).FirstOrDefault(s => s != null);
public ApplicationPartial? Application => _shards.Values.Select(s => s.Application).FirstOrDefault(s => s != null);
private ClusterSessionState GetClusterState()
{

View File

@ -32,6 +32,7 @@ namespace Myriad.Gateway
public ShardState State { get; private set; }
public TimeSpan? Latency { get; private set; }
public User? User { get; private set; }
public ApplicationPartial? Application { get; private set; }
public Func<IGatewayEvent, Task>? OnEventReceived { get; set; }
@ -258,6 +259,7 @@ namespace Myriad.Gateway
ShardInfo = ready.Shard;
SessionInfo = SessionInfo with { Session = ready.SessionId };
User = ready.User;
Application = ready.Application;
State = ShardState.Open;
return Task.CompletedTask;

View File

@ -33,8 +33,11 @@ namespace Myriad.Rest
public Task<Message?> GetMessage(ulong channelId, ulong messageId) =>
_client.Get<Message>($"/channels/{channelId}/messages/{messageId}", ("GetMessage", channelId));
public Task<Channel?> GetGuild(ulong id) =>
_client.Get<Channel>($"/guilds/{id}", ("GetGuild", id));
public Task<Guild?> GetGuild(ulong id) =>
_client.Get<Guild>($"/guilds/{id}", ("GetGuild", id));
public Task<Channel[]> GetGuildChannels(ulong id) =>
_client.Get<Channel[]>($"/guilds/{id}/channels", ("GetGuildChannels", id))!;
public Task<User?> GetUser(ulong id) =>
_client.Get<User>($"/users/{id}", ("GetUser", default));

View File

@ -77,8 +77,8 @@ namespace Myriad.Rest.Ratelimit
var headerNextReset = DateTimeOffset.UtcNow + headers.ResetAfter.Value; // todo: server time
if (headerNextReset > _nextReset)
{
_logger.Debug("{BucketKey}/{BucketMajor}: Received reset time {NextReset} from server",
Key, Major, _nextReset);
_logger.Debug("{BucketKey}/{BucketMajor}: Received reset time {NextReset} from server (after: {NextResetAfter})",
Key, Major, headerNextReset, headers.ResetAfter.Value);
_nextReset = headerNextReset;
_resetTimeValid = true;
@ -101,7 +101,7 @@ namespace Myriad.Rest.Ratelimit
_semaphore.Wait();
// If we're past the reset time *and* we haven't reset already, do that
var timeSinceReset = _nextReset - now;
var timeSinceReset = now - _nextReset;
var shouldReset = _resetTimeValid && timeSinceReset > TimeSpan.Zero;
if (shouldReset)
{

View File

@ -1,10 +1,22 @@
using Myriad.Types;
using System.Text.Json.Serialization;
using Myriad.Types;
using Myriad.Utils;
namespace Myriad.Rest.Types.Requests
{
public record MessageEditRequest
{
public string? Content { get; set; }
public Embed? Embed { get; set; }
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
public Optional<string?> Content { get; init; }
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
public Optional<Embed?> Embed { get; init; }
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
public Optional<Message.MessageFlags> Flags { get; init; }
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
public Optional<AllowedMentions> AllowedMentions { get; init; }
}
}

View File

@ -7,7 +7,7 @@ namespace Myriad.Rest.Types.Requests
public string? Content { get; set; }
public object? Nonce { get; set; }
public bool Tts { get; set; }
public AllowedMentions AllowedMentions { get; set; }
public AllowedMentions? AllowedMentions { get; set; }
public Embed? Embed { get; set; }
}
}

View File

@ -0,0 +1,43 @@
using System;
using System.Reflection;
using System.Text.Json;
using System.Text.Json.Serialization;
using Myriad.Utils;
namespace Myriad.Serialization
{
public class OptionalConverter: JsonConverter<IOptional>
{
public override IOptional? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
var innerType = typeToConvert.GetGenericArguments()[0];
var inner = JsonSerializer.Deserialize(ref reader, innerType, options);
// TODO: rewrite to JsonConverterFactory to cut down on reflection
return (IOptional?) Activator.CreateInstance(
typeof(Optional<>).MakeGenericType(innerType),
BindingFlags.Instance | BindingFlags.Public,
null,
new[] {inner},
null);
}
public override void Write(Utf8JsonWriter writer, IOptional value, JsonSerializerOptions options)
{
var innerType = value.GetType().GetGenericArguments()[0];
JsonSerializer.Serialize(writer, value.GetValue(), innerType, options);
}
public override bool CanConvert(Type typeToConvert)
{
if (!typeToConvert.IsGenericType)
return false;
if (typeToConvert.GetGenericTypeDefinition() != typeof(Optional<>))
return false;
return true;
}
}
}

32
Myriad/Utils/Optional.cs Normal file
View File

@ -0,0 +1,32 @@
using System.Text.Json.Serialization;
using Myriad.Serialization;
namespace Myriad.Utils
{
public interface IOptional
{
bool HasValue { get; }
object? GetValue();
}
[JsonConverter(typeof(OptionalConverter))]
public readonly struct Optional<T>: IOptional
{
public Optional(T value)
{
HasValue = true;
Value = value;
}
public bool HasValue { get; }
public object? GetValue() => Value;
public T Value { get; }
public static implicit operator Optional<T>(T value) => new(value);
public static Optional<T> Some(T value) => new(value);
public static Optional<T> None() => default;
}
}

View File

@ -8,11 +8,11 @@ using Autofac;
using DSharpPlus;
using DSharpPlus.Entities;
using DSharpPlus.Net;
using Myriad.Cache;
using Myriad.Extensions;
using Myriad.Gateway;
using Myriad.Rest.Types;
using Myriad.Rest.Types.Requests;
using Myriad.Types;
@ -34,7 +34,7 @@ namespace PluralKit.Bot
private readonly Guild? _guild;
private readonly Channel _channel;
private readonly DiscordMessage _message = null;
private readonly Message _messageNew;
private readonly MessageCreateEvent _messageNew;
private readonly Parameters _parameters;
private readonly MessageContext _messageContext;
private readonly PermissionSet _botPermissions;
@ -79,6 +79,7 @@ namespace PluralKit.Bot
public DiscordChannel Channel => _message.Channel;
public Channel ChannelNew => _channel;
public User AuthorNew => _messageNew.Author;
public GuildMemberPartial MemberNew => _messageNew.Member;
public DiscordMessage Message => _message;
public Message MessageNew => _messageNew;
public DiscordGuild Guild => _message.Channel.Guild;
@ -91,6 +92,7 @@ namespace PluralKit.Bot
public PermissionSet UserPermissions => _userPermissions;
public DiscordRestClient Rest => _rest;
public DiscordApiClient RestNew => _newRest;
public PKSystem System => _senderSystem;
@ -102,16 +104,16 @@ namespace PluralKit.Bot
public Task<DiscordMessage> Reply(string text, DiscordEmbed embed,
IEnumerable<IMention>? mentions = null)
{
return Reply(text, (DiscordEmbed) null, mentions);
throw new NotImplementedException();
}
public Task<DiscordMessage> Reply(DiscordEmbed embed,
IEnumerable<IMention>? mentions = null)
{
return Reply(null, (DiscordEmbed) null, mentions);
throw new NotImplementedException();
}
public async Task<DiscordMessage> Reply(string text = null, Embed embed = null, IEnumerable<IMention>? mentions = null)
public async Task<Message> Reply(string text = null, Embed embed = null, AllowedMentions? mentions = null)
{
if (!BotPermissions.HasFlag(PermissionSet.SendMessages))
// Will be "swallowed" during the error handler anyway, this message is never shown.
@ -123,10 +125,10 @@ namespace PluralKit.Bot
var msg = await _newRest.CreateMessage(_channel.Id, new MessageRequest
{
Content = text,
Embed = embed
Embed = embed,
AllowedMentions = mentions
});
// TODO: embeds/mentions
// var msg = await Channel.SendMessageFixedAsync(text, embed: embed, mentions: mentions);
// TODO: mentions should default to empty and not null?
if (embed != null)
{
@ -135,8 +137,7 @@ namespace PluralKit.Bot
await _commandMessageService.RegisterMessage(msg.Id, AuthorNew.Id);
}
// return msg;
return null;
return msg;
}
public async Task Execute<T>(Command commandDef, Func<T, Task> handler)

View File

@ -1,8 +1,6 @@
using System.Threading.Tasks;
using DSharpPlus;
using DSharpPlus.Entities;
using Myriad.Cache;
using Myriad.Types;
using PluralKit.Bot.Utils;
@ -12,11 +10,12 @@ namespace PluralKit.Bot
{
public static class ContextEntityArgumentsExt
{
public static async Task<DiscordUser> MatchUser(this Context ctx)
public static async Task<User> MatchUser(this Context ctx)
{
var text = ctx.PeekArgument();
if (text.TryParseMention(out var id))
return await ctx.Shard.GetUser(id);
return await ctx.Cache.GetOrFetchUser(ctx.RestNew, id);
return null;
}

View File

@ -1,10 +1,11 @@
using System;
using System.Threading.Tasks;
using DSharpPlus.Entities;
using Humanizer;
using Myriad.Builders;
using Myriad.Types;
using NodaTime;
using PluralKit.Core;
@ -84,10 +85,11 @@ namespace PluralKit.Bot
await ctx.Reply($"{Emojis.Success} Autoproxy set to **{member.NameFor(ctx)}** in this server.");
}
private async Task<DiscordEmbed> CreateAutoproxyStatusEmbed(Context ctx)
private async Task<Embed> CreateAutoproxyStatusEmbed(Context ctx)
{
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 DiscordEmbedBuilder().WithTitle($"Current autoproxy status (for {ctx.Guild.Name.EscapeMarkdown()})");
var eb = new EmbedBuilder()
.Title($"Current autoproxy status (for {ctx.GuildNew.Name.EscapeMarkdown()})");
var fronters = ctx.MessageContext.LastSwitchMembers;
var relevantMember = ctx.MessageContext.AutoproxyMode switch
@ -98,35 +100,36 @@ namespace PluralKit.Bot
};
switch (ctx.MessageContext.AutoproxyMode) {
case AutoproxyMode.Off: eb.WithDescription($"Autoproxy is currently **off** in this server. To enable it, use one of the following commands:\n{commandList}");
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.WithDescription("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.");
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.WithDescription($"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`.");
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;
}
// AutoproxyMember is never null if Mode is Member, this is just to make the compiler shut up
case AutoproxyMode.Member when relevantMember != null: {
eb.WithDescription($"Autoproxy is active for member **{relevantMember.NameFor(ctx)}** (`{relevantMember.Hid}`) in this server. To disable, type `pk;autoproxy off`.");
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.WithDescription("Autoproxy is currently set to **latch mode**, meaning the *last-proxied member* will be autoproxied. To disable, type `pk;autoproxy off`.");
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)
eb.AddField("\u200b", $"{Emojis.Note} Autoproxy is currently **disabled** for your account (<@{ctx.Author.Id}>). To enable it, use `pk;autoproxy account enable`.");
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();
}
@ -178,7 +181,7 @@ namespace PluralKit.Bot
else
{
var statusString = ctx.MessageContext.AllowAutoproxy ? "enabled" : "disabled";
await ctx.Reply($"Autoproxy is currently **{statusString}** for account <@{ctx.Author.Id}>.", mentions: new IMention[]{});
await ctx.Reply($"Autoproxy is currently **{statusString}** for account <@{ctx.Author.Id}>.");
}
}
@ -187,18 +190,18 @@ namespace PluralKit.Bot
var statusString = allow ? "enabled" : "disabled";
if (ctx.MessageContext.AllowAutoproxy == allow)
{
await ctx.Reply($"{Emojis.Note} Autoproxy is already {statusString} for account <@{ctx.Author.Id}>.", mentions: new IMention[]{});
await ctx.Reply($"{Emojis.Note} Autoproxy is already {statusString} for account <@{ctx.Author.Id}>.");
return;
}
var patch = new AccountPatch { AllowAutoproxy = allow };
await _db.Execute(conn => _repo.UpdateAccount(conn, ctx.Author.Id, patch));
await ctx.Reply($"{Emojis.Success} Autoproxy {statusString} for account <@{ctx.Author.Id}>.", mentions: new IMention[]{});
await ctx.Reply($"{Emojis.Success} Autoproxy {statusString} for account <@{ctx.Author.Id}>.");
}
private Task UpdateAutoproxy(Context ctx, AutoproxyMode autoproxyMode, MemberId? autoproxyMember)
{
var patch = new SystemGuildPatch {AutoproxyMode = autoproxyMode, AutoproxyMember = autoproxyMember};
return _db.Execute(conn => _repo.UpsertSystemGuild(conn, ctx.System.Id, ctx.Guild.Id, patch));
return _db.Execute(conn => _repo.UpsertSystemGuild(conn, ctx.System.Id, ctx.GuildNew.Id, patch));
}
}
}

View File

@ -4,8 +4,8 @@ using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using DSharpPlus;
using DSharpPlus.Entities;
using Myriad.Extensions;
using Myriad.Types;
namespace PluralKit.Bot
{
@ -22,7 +22,7 @@ namespace PluralKit.Bot
// If we have a user @mention/ID, use their avatar
if (await ctx.MatchUser() is { } user)
{
var url = user.GetAvatarUrl(ImageFormat.Png, 256);
var url = user.AvatarUrl("png", 256);
return new ParsedImage {Url = url, Source = AvatarSource.User, SourceUser = user};
}
@ -64,7 +64,7 @@ namespace PluralKit.Bot
{
public string Url;
public AvatarSource Source;
public DiscordUser? SourceUser;
public User? SourceUser;
}
public enum AvatarSource

View File

@ -10,6 +10,8 @@ using DSharpPlus.Entities;
using Humanizer;
using Myriad.Builders;
using PluralKit.Core;
namespace PluralKit.Bot
@ -194,7 +196,7 @@ 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 DiscordEmbedBuilder().WithImageUrl(img.Url).Build())
? ctx.Reply(msg, embed: new EmbedBuilder().Image(new(img.Url)).Build())
: ctx.Reply(msg));
}
@ -265,7 +267,7 @@ namespace PluralKit.Bot
var title = system.Name != null ? $"Groups of {system.Name} (`{system.Hid}`)" : $"Groups of `{system.Hid}`";
await ctx.Paginate(groups.ToAsyncEnumerable(), groups.Count, 25, title, Renderer);
Task Renderer(DiscordEmbedBuilder eb, IEnumerable<ListedGroup> page)
Task Renderer(EmbedBuilder eb, IEnumerable<ListedGroup> page)
{
eb.WithSimpleLineContent(page.Select(g =>
{
@ -274,7 +276,7 @@ namespace PluralKit.Bot
else
return $"[`{g.Hid}`] **{g.Name.EscapeMarkdown()}** ({"member".ToQuantity(g.MemberCount)})";
}));
eb.WithFooter($"{groups.Count} total.");
eb.Footer(new($"{groups.Count} total."));
return Task.CompletedTask;
}
}

View File

@ -3,10 +3,10 @@ using System.Linq;
using System.Text;
using System.Threading.Tasks;
using DSharpPlus.Entities;
using Humanizer;
using Myriad.Builders;
using NodaTime;
using PluralKit.Core;
@ -90,10 +90,10 @@ namespace PluralKit.Bot
await ctx.Paginate(members.ToAsyncEnumerable(), members.Count, itemsPerPage, embedTitle, Renderer);
// Base renderer, dispatches based on type
Task Renderer(DiscordEmbedBuilder eb, IEnumerable<ListedMember> page)
Task Renderer(EmbedBuilder eb, IEnumerable<ListedMember> page)
{
// Add a global footer with the filter/sort string + result count
eb.WithFooter($"{opts.CreateFilterString()}. {"result".ToQuantity(members.Count)}.");
eb.Footer(new($"{opts.CreateFilterString()}. {"result".ToQuantity(members.Count)}."));
// Then call the specific renderers
if (opts.Type == ListType.Short)
@ -104,7 +104,7 @@ namespace PluralKit.Bot
return Task.CompletedTask;
}
void ShortRenderer(DiscordEmbedBuilder eb, IEnumerable<ListedMember> page)
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" :)
@ -122,7 +122,7 @@ namespace PluralKit.Bot
}));
}
void LongRenderer(DiscordEmbedBuilder eb, IEnumerable<ListedMember> page)
void LongRenderer(EmbedBuilder eb, IEnumerable<ListedMember> page)
{
var zone = ctx.System?.Zone ?? DateTimeZone.Utc;
foreach (var m in page)
@ -162,7 +162,7 @@ namespace PluralKit.Bot
if (m.MemberVisibility == PrivacyLevel.Private)
profile.Append("\n*(this member is hidden)*");
eb.AddField(m.NameFor(ctx), profile.ToString().Truncate(1024));
eb.Field(new(m.NameFor(ctx), profile.ToString().Truncate(1024)));
}
}
}

View File

@ -4,6 +4,8 @@ using System.Threading.Tasks;
using DSharpPlus.Entities;
using Myriad.Builders;
using PluralKit.Core;
namespace PluralKit.Bot
@ -135,7 +137,7 @@ 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 DiscordEmbedBuilder().WithImageUrl(avatar.Url).Build())
? ctx.Reply(msg, embed: new EmbedBuilder().Image(new(avatar.Url)).Build())
: ctx.Reply(msg);
}

View File

@ -13,7 +13,16 @@ using Humanizer;
using NodaTime;
using PluralKit.Core;
using DSharpPlus.Entities;
using Myriad.Builders;
using Myriad.Cache;
using Myriad.Extensions;
using Myriad.Gateway;
using Myriad.Rest;
using Myriad.Rest.Types.Requests;
using Myriad.Types;
using Permissions = DSharpPlus.Permissions;
namespace PluralKit.Bot {
public class Misc
@ -25,8 +34,12 @@ namespace PluralKit.Bot {
private readonly EmbedService _embeds;
private readonly IDatabase _db;
private readonly ModelRepository _repo;
private readonly IDiscordCache _cache;
private readonly DiscordApiClient _rest;
private readonly Cluster _cluster;
private readonly Bot _bot;
public Misc(BotConfig botConfig, IMetrics metrics, CpuStatService cpu, ShardInfoService shards, EmbedService embeds, ModelRepository repo, IDatabase db)
public Misc(BotConfig botConfig, IMetrics metrics, CpuStatService cpu, ShardInfoService shards, EmbedService embeds, ModelRepository repo, IDatabase db, IDiscordCache cache, DiscordApiClient rest, Bot bot, Cluster cluster)
{
_botConfig = botConfig;
_metrics = metrics;
@ -35,20 +48,26 @@ namespace PluralKit.Bot {
_embeds = embeds;
_repo = repo;
_db = db;
_cache = cache;
_rest = rest;
_bot = bot;
_cluster = cluster;
}
public async Task Invite(Context ctx)
{
var clientId = _botConfig.ClientId ?? ctx.Client.CurrentApplication.Id;
var permissions = new Permissions()
.Grant(Permissions.AddReactions)
.Grant(Permissions.AttachFiles)
.Grant(Permissions.EmbedLinks)
.Grant(Permissions.ManageMessages)
.Grant(Permissions.ManageWebhooks)
.Grant(Permissions.ReadMessageHistory)
.Grant(Permissions.SendMessages);
var invite = $"https://discord.com/oauth2/authorize?client_id={clientId}&scope=bot%20applications.commands&permissions={(long)permissions}";
var clientId = _botConfig.ClientId ?? _cluster.Application?.Id;
var permissions =
PermissionSet.AddReactions |
PermissionSet.AttachFiles |
PermissionSet.EmbedLinks |
PermissionSet.ManageMessages |
PermissionSet.ManageWebhooks |
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}>");
}
@ -69,6 +88,7 @@ namespace PluralKit.Bot {
var totalSwitches = _metrics.Snapshot.GetForContext("Application").Gauges.FirstOrDefault(m => m.MultidimensionalName == CoreMetrics.SwitchCount.Name)?.Value ?? 0;
var totalMessages = _metrics.Snapshot.GetForContext("Application").Gauges.FirstOrDefault(m => m.MultidimensionalName == CoreMetrics.MessageCount.Name)?.Value ?? 0;
// TODO: shard stuff
var shardId = ctx.Shard.ShardId;
var shardTotal = ctx.Client.ShardClients.Count;
var shardUpTotal = _shards.Shards.Where(x => x.Connected).Count();
@ -79,30 +99,31 @@ namespace PluralKit.Bot {
var shardUptime = SystemClock.Instance.GetCurrentInstant() - shardInfo.LastConnectionTime;
var embed = new DiscordEmbedBuilder();
if (messagesReceived != null) embed.AddField("Messages processed",$"{messagesReceived.OneMinuteRate * 60:F1}/m ({messagesReceived.FifteenMinuteRate * 60:F1}/m over 15m)", true);
if (messagesProxied != null) embed.AddField("Messages proxied", $"{messagesProxied.OneMinuteRate * 60:F1}/m ({messagesProxied.FifteenMinuteRate * 60:F1}/m over 15m)", true);
if (commandsRun != null) embed.AddField("Commands executed", $"{commandsRun.OneMinuteRate * 60:F1}/m ({commandsRun.FifteenMinuteRate * 60:F1}/m over 15m)", true);
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 (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));
embed
.AddField("Current shard", $"Shard #{shardId} (of {shardTotal} total, {shardUpTotal} are up)", true)
.AddField("Shard uptime", $"{shardUptime.FormatDuration()} ({shardInfo.DisconnectionCount} disconnections)", true)
.AddField("CPU usage", $"{_cpu.LastCpuMeasure:P1}", true)
.AddField("Memory usage", $"{memoryUsage / 1024 / 1024} MiB", true)
.AddField("Latency", $"API: {apiLatency.TotalMilliseconds:F0} ms, shard: {shardInfo.ShardLatency.Milliseconds} ms", true)
.AddField("Total numbers", $"{totalSystems:N0} systems, {totalMembers:N0} members, {totalGroups:N0} groups, {totalSwitches:N0} switches, {totalMessages:N0} messages");
await msg.ModifyAsync("", embed.Build());
.Field(new("Current shard", $"Shard #{shardId} (of {shardTotal} total, {shardUpTotal} are up)", true))
.Field(new("Shard uptime", $"{shardUptime.FormatDuration()} ({shardInfo.DisconnectionCount} disconnections)", true))
.Field(new("CPU usage", $"{_cpu.LastCpuMeasure:P1}", true))
.Field(new("Memory usage", $"{memoryUsage / 1024 / 1024} MiB", true))
.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"));
await ctx.RestNew.EditMessage(msg.ChannelId, msg.Id,
new MessageEditRequest {Content = "", Embed = embed.Build()});
}
public async Task PermCheckGuild(Context ctx)
{
DiscordGuild guild;
DiscordMember senderGuildUser = null;
Guild guild;
GuildMemberPartial senderGuildUser = null;
if (ctx.Guild != null && !ctx.HasNext())
if (ctx.GuildNew != null && !ctx.HasNext())
{
guild = ctx.Guild;
senderGuildUser = (DiscordMember)ctx.Author;
guild = ctx.GuildNew;
senderGuildUser = ctx.MemberNew;
}
else
{
@ -110,31 +131,33 @@ namespace PluralKit.Bot {
if (!ulong.TryParse(guildIdStr, out var guildId))
throw new PKSyntaxError($"Could not parse {guildIdStr.AsCode()} as an ID.");
guild = ctx.Client.GetGuild(guildId);
if (guild != null) senderGuildUser = await guild.GetMember(ctx.Author.Id);
if (guild == null || senderGuildUser == null) throw Errors.GuildNotFound(guildId);
guild = await _rest.GetGuild(guildId);
if (guild != null)
senderGuildUser = await _rest.GetGuildMember(guildId, ctx.AuthorNew.Id);
if (guild == null || senderGuildUser == null)
throw Errors.GuildNotFound(guildId);
}
var requiredPermissions = new []
{
Permissions.AccessChannels,
Permissions.SendMessages,
Permissions.AddReactions,
Permissions.AttachFiles,
Permissions.EmbedLinks,
Permissions.ManageMessages,
Permissions.ManageWebhooks
PermissionSet.ViewChannel,
PermissionSet.SendMessages,
PermissionSet.AddReactions,
PermissionSet.AttachFiles,
PermissionSet.EmbedLinks,
PermissionSet.ManageMessages,
PermissionSet.ManageWebhooks
};
// Loop through every channel and group them by sets of permissions missing
var permissionsMissing = new Dictionary<ulong, List<DiscordChannel>>();
var permissionsMissing = new Dictionary<ulong, List<Channel>>();
var hiddenChannels = 0;
foreach (var channel in await guild.GetChannelsAsync())
foreach (var channel in await _rest.GetGuildChannels(guild.Id))
{
var botPermissions = channel.BotPermissions();
var botPermissions = _bot.PermissionsIn(channel.Id);
var userPermissions = PermissionExtensions.PermissionsFor(guild, channel, ctx.AuthorNew.Id, senderGuildUser.Roles);
var userPermissions = senderGuildUser.PermissionsIn(channel);
if ((userPermissions & Permissions.AccessChannels) == 0)
if ((userPermissions & PermissionSet.ViewChannel) == 0)
{
// If the user can't see this channel, don't calculate permissions for it
// (to prevent info-leaking, mostly)
@ -154,18 +177,18 @@ namespace PluralKit.Bot {
// This means we can check if the dict is empty to see if all channels are proxyable
if (missingPermissionField != 0)
{
permissionsMissing.TryAdd(missingPermissionField, new List<DiscordChannel>());
permissionsMissing.TryAdd(missingPermissionField, new List<Channel>());
permissionsMissing[missingPermissionField].Add(channel);
}
}
// Generate the output embed
var eb = new DiscordEmbedBuilder()
.WithTitle($"Permission check for **{guild.Name}**");
var eb = new EmbedBuilder()
.Title($"Permission check for **{guild.Name}**");
if (permissionsMissing.Count == 0)
{
eb.WithDescription($"No errors found, all channels proxyable :)").WithColor(DiscordUtils.Green);
eb.Description($"No errors found, all channels proxyable :)").Color((uint?) DiscordUtils.Green.Value);
}
else
{
@ -173,18 +196,19 @@ namespace PluralKit.Bot {
{
// Each missing permission field can have multiple missing channels
// so we extract them all and generate a comma-separated list
// TODO: port ToPermissionString?
var missingPermissionNames = ((Permissions)missingPermissionField).ToPermissionString();
var channelsList = string.Join("\n", channels
.OrderBy(c => c.Position)
.Select(c => $"#{c.Name}"));
eb.AddField($"Missing *{missingPermissionNames}*", channelsList.Truncate(1000));
eb.WithColor(DiscordUtils.Red);
eb.Field(new($"Missing *{missingPermissionNames}*", channelsList.Truncate(1000)));
eb.Color((uint?) DiscordUtils.Red.Value);
}
}
if (hiddenChannels > 0)
eb.WithFooter($"{"channel".ToQuantity(hiddenChannels)} were ignored as you do not have view access to them.");
eb.Footer(new($"{"channel".ToQuantity(hiddenChannels)} were ignored as you do not have view access to them."));
// Send! :)
await ctx.Reply(embed: eb.Build());

View File

@ -123,7 +123,7 @@ namespace PluralKit.Bot
{
if (lastCategory != channel!.ParentId && fieldValue.Length > 0)
{
eb.AddField(CategoryName(lastCategory), fieldValue.ToString());
eb.Field(new(CategoryName(lastCategory), fieldValue.ToString()));
fieldValue.Clear();
}
else fieldValue.Append("\n");
@ -132,7 +132,7 @@ namespace PluralKit.Bot
lastCategory = channel.ParentId;
}
eb.AddField(CategoryName(lastCategory), fieldValue.ToString());
eb.Field(new(CategoryName(lastCategory), fieldValue.ToString()));
return Task.CompletedTask;
});

View File

@ -4,6 +4,8 @@ using System.Threading.Tasks;
using DSharpPlus.Entities;
using Myriad.Builders;
using NodaTime;
using NodaTime.Text;
using NodaTime.TimeZones;
@ -150,7 +152,7 @@ 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 DiscordEmbedBuilder().WithImageUrl(img.Url).Build())
? ctx.Reply(msg, embed: new EmbedBuilder().Image(new(img.Url)).Build())
: ctx.Reply(msg));
}

View File

@ -98,9 +98,11 @@ namespace PluralKit.Bot
stringToAdd =
$"**{membersStr}** ({sw.Timestamp.FormatZoned(system.Zone)}, {switchSince.FormatDuration()} ago)\n";
}
try // Unfortunately the only way to test DiscordEmbedBuilder.Description max length is this
{
builder.Description += stringToAdd;
// TODO: what is this??
// builder.Description += stringToAdd;
}
catch (ArgumentException)
{

View File

@ -1,7 +1,8 @@
using System.Linq;
using System.Threading.Tasks;
using DSharpPlus.Entities;
using Myriad.Extensions;
using Myriad.Rest.Types;
using PluralKit.Core;
@ -33,8 +34,8 @@ namespace PluralKit.Bot
if (existingAccount != null)
throw Errors.AccountInOtherSystem(existingAccount);
var msg = $"{account.Mention}, please confirm the link by clicking the {Emojis.Success} reaction on this message.";
var mentions = new IMention[] { new UserMention(account) };
var msg = $"{account.Mention()}, please confirm the link by clicking the {Emojis.Success} reaction on this message.";
var mentions = new AllowedMentions {Users = new[] {account.Id}};
if (!await ctx.PromptYesNo(msg, user: account, mentions: mentions, matchFlag: false)) throw Errors.MemberLinkCancelled;
await _repo.AddAccount(conn, ctx.System.Id, account.Id);
await ctx.Reply($"{Emojis.Success} Account linked to system.");

View File

@ -83,7 +83,9 @@ namespace PluralKit.Bot
// Event handler queue
builder.RegisterType<HandlerQueue<MessageCreateEventArgs>>().AsSelf().SingleInstance();
builder.RegisterType<HandlerQueue<MessageCreateEvent>>().AsSelf().SingleInstance();
builder.RegisterType<HandlerQueue<MessageReactionAddEventArgs>>().AsSelf().SingleInstance();
builder.RegisterType<HandlerQueue<MessageReactionAddEvent>>().AsSelf().SingleInstance();
// Bot services
builder.RegisterType<EmbedService>().AsSelf().SingleInstance();

View File

@ -6,19 +6,17 @@ using System.Threading.Tasks;
using Autofac;
using DSharpPlus;
using DSharpPlus.Entities;
using DSharpPlus.EventArgs;
using DSharpPlus.Exceptions;
using Myriad.Builders;
using Myriad.Gateway;
using Myriad.Rest.Exceptions;
using Myriad.Rest.Types;
using Myriad.Rest.Types.Requests;
using Myriad.Types;
using NodaTime;
using PluralKit.Core;
using Permissions = DSharpPlus.Permissions;
namespace PluralKit.Bot {
public static class ContextUtils {
public static async Task<bool> ConfirmClear(this Context ctx, string toClear)
@ -27,52 +25,45 @@ namespace PluralKit.Bot {
else return true;
}
public static async Task<bool> PromptYesNo(this Context ctx, String msgString, DiscordUser user = null, Duration? timeout = null, IEnumerable<IMention> mentions = null, bool matchFlag = true)
public static async Task<bool> PromptYesNo(this Context ctx, string msgString, User user = null, Duration? timeout = null, AllowedMentions mentions = null, bool matchFlag = true)
{
DiscordMessage message;
Message message;
if (matchFlag && ctx.MatchFlag("y", "yes")) return true;
else message = await ctx.Reply(msgString, mentions: mentions);
var cts = new CancellationTokenSource();
if (user == null) user = ctx.Author;
if (user == null) user = ctx.AuthorNew;
if (timeout == null) timeout = Duration.FromMinutes(5);
// "Fork" the task adding the reactions off so we don't have to wait for them to be finished to start listening for presses
var _ = message.CreateReactionsBulk(new[] {Emojis.Success, Emojis.Error});
await ctx.RestNew.CreateReactionsBulk(message, new[] {Emojis.Success, Emojis.Error});
bool ReactionPredicate(MessageReactionAddEventArgs e)
bool ReactionPredicate(MessageReactionAddEvent e)
{
if (e.Channel.Id != message.ChannelId || e.Message.Id != message.Id) return false;
if (e.User.Id != user.Id) return false;
if (e.ChannelId != message.ChannelId || e.MessageId != message.Id) return false;
if (e.UserId != user.Id) return false;
return true;
}
bool MessagePredicate(MessageCreateEventArgs e)
bool MessagePredicate(MessageCreateEvent e)
{
if (e.Channel.Id != message.ChannelId) return false;
if (e.ChannelId != message.ChannelId) return false;
if (e.Author.Id != user.Id) return false;
var strings = new [] {"y", "yes", "n", "no"};
foreach (var str in strings)
if (e.Message.Content.Equals(str, StringComparison.InvariantCultureIgnoreCase))
return true;
return false;
return strings.Any(str => string.Equals(e.Content, str, StringComparison.InvariantCultureIgnoreCase));
}
var messageTask = ctx.Services.Resolve<HandlerQueue<MessageCreateEventArgs>>().WaitFor(MessagePredicate, timeout, cts.Token);
var reactionTask = ctx.Services.Resolve<HandlerQueue<MessageReactionAddEventArgs>>().WaitFor(ReactionPredicate, timeout, cts.Token);
var messageTask = ctx.Services.Resolve<HandlerQueue<MessageCreateEvent>>().WaitFor(MessagePredicate, timeout, cts.Token);
var reactionTask = ctx.Services.Resolve<HandlerQueue<MessageReactionAddEvent>>().WaitFor(ReactionPredicate, timeout, cts.Token);
var theTask = await Task.WhenAny(messageTask, reactionTask);
cts.Cancel();
if (theTask == messageTask)
{
var responseMsg = (await messageTask).Message;
var responseMsg = (await messageTask);
var positives = new[] {"y", "yes"};
foreach (var p in positives)
if (responseMsg.Content.Equals(p, StringComparison.InvariantCultureIgnoreCase))
return true;
return false;
return positives.Any(p => string.Equals(responseMsg.Content, p, StringComparison.InvariantCultureIgnoreCase));
}
if (theTask == reactionTask)
@ -81,50 +72,45 @@ namespace PluralKit.Bot {
return false;
}
public static async Task<MessageReactionAddEventArgs> AwaitReaction(this Context ctx, DiscordMessage message, DiscordUser user = null, Func<MessageReactionAddEventArgs, bool> predicate = null, TimeSpan? timeout = null) {
var tcs = new TaskCompletionSource<MessageReactionAddEventArgs>();
Task Inner(DiscordClient _, MessageReactionAddEventArgs args) {
if (message.Id != args.Message.Id) return Task.CompletedTask; // Ignore reactions for different messages
if (user != null && user.Id != args.User.Id) return Task.CompletedTask; // Ignore messages from other users if a user was defined
if (predicate != null && !predicate.Invoke(args)) return Task.CompletedTask; // Check predicate
tcs.SetResult(args);
return Task.CompletedTask;
public static async Task<MessageReactionAddEvent> AwaitReaction(this Context ctx, Message message, User user = null, Func<MessageReactionAddEvent, bool> predicate = null, Duration? timeout = null)
{
bool ReactionPredicate(MessageReactionAddEvent evt)
{
if (message.Id != evt.MessageId) return false; // Ignore reactions for different messages
if (user != null && user.Id != evt.UserId) return false; // Ignore messages from other users if a user was defined
if (predicate != null && !predicate.Invoke(evt)) return false; // Check predicate
return true;
}
ctx.Shard.MessageReactionAdded += Inner;
try {
return await tcs.Task.TimeoutAfter(timeout);
} finally {
ctx.Shard.MessageReactionAdded -= Inner;
}
return await ctx.Services.Resolve<HandlerQueue<MessageReactionAddEvent>>().WaitFor(ReactionPredicate, timeout);
}
public static async Task<bool> ConfirmWithReply(this Context ctx, string expectedReply)
{
bool Predicate(MessageCreateEventArgs e) =>
e.Author == ctx.Author && e.Channel.Id == ctx.Channel.Id;
bool Predicate(MessageCreateEvent e) =>
e.Author.Id == ctx.AuthorNew.Id && e.ChannelId == ctx.Channel.Id;
var msg = await ctx.Services.Resolve<HandlerQueue<MessageCreateEventArgs>>()
var msg = await ctx.Services.Resolve<HandlerQueue<MessageCreateEvent>>()
.WaitFor(Predicate, Duration.FromMinutes(1));
return string.Equals(msg.Message.Content, expectedReply, StringComparison.InvariantCultureIgnoreCase);
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, Func<DiscordEmbedBuilder, IEnumerable<T>, Task> renderer) {
public static async Task Paginate<T>(this Context ctx, IAsyncEnumerable<T> items, int totalCount, int itemsPerPage, string title, 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);
async Task<DiscordEmbed> MakeEmbedForPage(int page)
async Task<Embed> MakeEmbedForPage(int page)
{
var bufferedItemsNeeded = (page + 1) * itemsPerPage;
while (buffer.Count < bufferedItemsNeeded && await enumerator.MoveNextAsync())
buffer.Add(enumerator.Current);
var eb = new DiscordEmbedBuilder();
eb.Title = pageCount > 1 ? $"[{page+1}/{pageCount}] {title}" : title;
var eb = new EmbedBuilder();
eb.Title(pageCount > 1 ? $"[{page+1}/{pageCount}] {title}" : title);
await renderer(eb, buffer.Skip(page*itemsPerPage).Take(itemsPerPage));
return eb.Build();
}
@ -135,12 +121,12 @@ namespace PluralKit.Bot {
if (pageCount <= 1) return; // If we only have one (or no) page, don't bother with the reaction/pagination logic, lol
string[] botEmojis = { "\u23EA", "\u2B05", "\u27A1", "\u23E9", Emojis.Error };
var _ = msg.CreateReactionsBulk(botEmojis); // Again, "fork"
var _ = ctx.RestNew.CreateReactionsBulk(msg, botEmojis); // Again, "fork"
try {
var currentPage = 0;
while (true) {
var reaction = await ctx.AwaitReaction(msg, ctx.Author, timeout: TimeSpan.FromMinutes(5));
var reaction = await ctx.AwaitReaction(msg, ctx.AuthorNew, timeout: Duration.FromMinutes(5));
// Increment/decrement page counter based on which reaction was clicked
if (reaction.Emoji.Name == "\u23EA") currentPage = 0; // <<
@ -154,18 +140,18 @@ namespace PluralKit.Bot {
// If we can, remove the user's reaction (so they can press again quickly)
if (ctx.BotPermissions.HasFlag(PermissionSet.ManageMessages))
await msg.DeleteReactionAsync(reaction.Emoji, reaction.User);
await ctx.RestNew.DeleteUserReaction(msg.ChannelId, msg.Id, reaction.Emoji, reaction.UserId);
// Edit the embed with the new page
var embed = await MakeEmbedForPage(currentPage);
await msg.ModifyAsync(embed: embed);
await ctx.RestNew.EditMessage(msg.ChannelId, msg.Id, new MessageEditRequest {Embed = embed});
}
} catch (TimeoutException) {
// "escape hatch", clean up as if we hit X
}
if (ctx.BotPermissions.HasFlag(PermissionSet.ManageMessages))
await msg.DeleteAllReactionsAsync();
await ctx.RestNew.DeleteAllReactions(msg.ChannelId, msg.Id);
}
// If we get a "NotFound" error, the message has been deleted and thus not our problem
catch (NotFoundException) { }
@ -203,9 +189,10 @@ namespace PluralKit.Bot {
// Add back/forward reactions and the actual indicator emojis
async Task AddEmojis()
{
await msg.CreateReactionAsync(DiscordEmoji.FromUnicode("\u2B05"));
await msg.CreateReactionAsync(DiscordEmoji.FromUnicode("\u27A1"));
for (int i = 0; i < items.Count; i++) await msg.CreateReactionAsync(DiscordEmoji.FromUnicode(indicators[i]));
await ctx.RestNew.CreateReaction(msg.ChannelId, msg.Id, new() { Name = "\u2B05" });
await ctx.RestNew.CreateReaction(msg.ChannelId, msg.Id, new() { Name = "\u27A1" });
for (int i = 0; i < items.Count; i++)
await ctx.RestNew.CreateReaction(msg.ChannelId, msg.Id, new() { Name = indicators[i] });
}
var _ = AddEmojis(); // Not concerned about awaiting
@ -213,7 +200,7 @@ namespace PluralKit.Bot {
while (true)
{
// Wait for a reaction
var reaction = await ctx.AwaitReaction(msg, ctx.Author);
var reaction = await ctx.AwaitReaction(msg, ctx.AuthorNew);
// If it's a movement reaction, inc/dec the page index
if (reaction.Emoji.Name == "\u2B05") currPage -= 1; // <
@ -230,8 +217,13 @@ namespace PluralKit.Bot {
if (idx < items.Count) return items[idx];
}
var __ = msg.DeleteReactionAsync(reaction.Emoji, ctx.Author); // don't care about awaiting
await msg.ModifyAsync($"**[Page {currPage + 1}/{pageCount}]**\n{description}\n{MakeOptionList(currPage)}");
var __ = ctx.RestNew.DeleteUserReaction(msg.ChannelId, msg.Id, reaction.Emoji, ctx.Author.Id);
await ctx.RestNew.EditMessage(msg.ChannelId, msg.Id,
new()
{
Content =
$"**[Page {currPage + 1}/{pageCount}]**\n{description}\n{MakeOptionList(currPage)}"
});
}
}
else
@ -241,13 +233,14 @@ namespace PluralKit.Bot {
// Add the relevant reactions (we don't care too much about awaiting)
async Task AddEmojis()
{
for (int i = 0; i < items.Count; i++) await msg.CreateReactionAsync(DiscordEmoji.FromUnicode(indicators[i]));
for (int i = 0; i < items.Count; i++)
await ctx.RestNew.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.AuthorNew,rx => indicators.Contains(rx.Emoji.Name));
return items[Array.IndexOf(indicators, reaction.Emoji.Name)];
}
}
@ -271,12 +264,12 @@ namespace PluralKit.Bot {
try
{
await Task.WhenAll(ctx.Message.CreateReactionAsync(DiscordEmoji.FromUnicode(emoji)), task);
await Task.WhenAll(ctx.RestNew.CreateReaction(ctx.MessageNew.ChannelId, ctx.MessageNew.Id, new() {Name = emoji}), task);
return await task;
}
finally
{
var _ = ctx.Message.DeleteReactionAsync(DiscordEmoji.FromUnicode(emoji), ctx.Shard.CurrentUser);
var _ = ctx.RestNew.DeleteOwnReaction(ctx.MessageNew.ChannelId, ctx.MessageNew.Id, new() { Name = emoji });
}
}
}

View File

@ -12,7 +12,9 @@ using DSharpPlus.Entities;
using DSharpPlus.EventArgs;
using DSharpPlus.Exceptions;
using Myriad.Builders;
using Myriad.Extensions;
using Myriad.Rest;
using Myriad.Rest.Types;
using Myriad.Types;
@ -116,11 +118,11 @@ namespace PluralKit.Bot
public static ulong InstantToSnowflake(DateTimeOffset time) =>
(ulong) (time - new DateTimeOffset(2015, 1, 1, 0, 0, 0, TimeSpan.Zero)).TotalMilliseconds << 22;
public static async Task CreateReactionsBulk(this DiscordMessage msg, string[] reactions)
public static async Task CreateReactionsBulk(this DiscordApiClient rest, Message msg, string[] reactions)
{
foreach (var reaction in reactions)
{
await msg.CreateReactionAsync(DiscordEmoji.FromUnicode(reaction));
await rest.CreateReaction(msg.ChannelId, msg.Id, new() {Name = reaction});
}
}
@ -329,7 +331,7 @@ namespace PluralKit.Bot
}
}
public static DiscordEmbedBuilder WithSimpleLineContent(this DiscordEmbedBuilder eb, IEnumerable<string> lines)
public static EmbedBuilder WithSimpleLineContent(this EmbedBuilder eb, IEnumerable<string> lines)
{
static int CharacterLimit(int pageNumber) =>
// First chunk goes in description (2048 chars), rest go in embed values (1000 chars)
@ -340,11 +342,11 @@ namespace PluralKit.Bot
// Add the first page to the embed description
if (pages.Count > 0)
eb.WithDescription(pages[0]);
eb.Description(pages[0]);
// Add the rest to blank-named (\u200B) fields
for (var i = 1; i < pages.Count; i++)
eb.AddField("\u200B", pages[i]);
eb.Field(new("\u200B", pages[i]));
return eb;
}

View File

@ -10,7 +10,7 @@ namespace PluralKit.Core
public class HandlerQueue<T>
{
private long _seq;
private readonly ConcurrentDictionary<long, HandlerEntry> _handlers = new ConcurrentDictionary<long, HandlerEntry>();
private readonly ConcurrentDictionary<long, HandlerEntry> _handlers = new();
public async Task<T> WaitFor(Func<T, bool> predicate, Duration? timeout = null, CancellationToken ct = default)
{