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 System.Threading.Tasks;
using Myriad.Gateway; using Myriad.Gateway;
using Myriad.Rest;
using Myriad.Types;
namespace Myriad.Cache namespace Myriad.Cache
{ {
@ -29,7 +31,7 @@ namespace Myriad.Cache
case GuildRoleDeleteEvent grd: case GuildRoleDeleteEvent grd:
return cache.RemoveRole(grd.GuildId, grd.RoleId); return cache.RemoveRole(grd.GuildId, grd.RoleId);
case MessageCreateEvent mc: case MessageCreateEvent mc:
return cache.SaveUser(mc.Author); return cache.SaveMessageCreate(mc);
} }
return default; return default;
@ -46,5 +48,23 @@ namespace Myriad.Cache
foreach (var member in guildCreate.Members) foreach (var member in guildCreate.Members)
await cache.SaveUser(member.User); 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 Mention(this User user) => $"<@{user.Id}>";
public static string AvatarUrl(this User user) => public static string AvatarUrl(this User user, string? format = "png", int? size = 128) =>
$"https://cdn.discordapp.com/avatars/{user.Id}/{user.Avatar}.png"; $"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 IReadOnlyDictionary<int, Shard> Shards => _shards;
public ClusterSessionState SessionState => GetClusterState(); public ClusterSessionState SessionState => GetClusterState();
public User? User => _shards.Values.Select(s => s.User).FirstOrDefault(s => s != null); 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() private ClusterSessionState GetClusterState()
{ {

View File

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

View File

@ -33,8 +33,11 @@ namespace Myriad.Rest
public Task<Message?> GetMessage(ulong channelId, ulong messageId) => public Task<Message?> GetMessage(ulong channelId, ulong messageId) =>
_client.Get<Message>($"/channels/{channelId}/messages/{messageId}", ("GetMessage", channelId)); _client.Get<Message>($"/channels/{channelId}/messages/{messageId}", ("GetMessage", channelId));
public Task<Channel?> GetGuild(ulong id) => public Task<Guild?> GetGuild(ulong id) =>
_client.Get<Channel>($"/guilds/{id}", ("GetGuild", 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) => public Task<User?> GetUser(ulong id) =>
_client.Get<User>($"/users/{id}", ("GetUser", default)); _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 var headerNextReset = DateTimeOffset.UtcNow + headers.ResetAfter.Value; // todo: server time
if (headerNextReset > _nextReset) if (headerNextReset > _nextReset)
{ {
_logger.Debug("{BucketKey}/{BucketMajor}: Received reset time {NextReset} from server", _logger.Debug("{BucketKey}/{BucketMajor}: Received reset time {NextReset} from server (after: {NextResetAfter})",
Key, Major, _nextReset); Key, Major, headerNextReset, headers.ResetAfter.Value);
_nextReset = headerNextReset; _nextReset = headerNextReset;
_resetTimeValid = true; _resetTimeValid = true;
@ -101,7 +101,7 @@ namespace Myriad.Rest.Ratelimit
_semaphore.Wait(); _semaphore.Wait();
// If we're past the reset time *and* we haven't reset already, do that // 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; var shouldReset = _resetTimeValid && timeSinceReset > TimeSpan.Zero;
if (shouldReset) 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 namespace Myriad.Rest.Types.Requests
{ {
public record MessageEditRequest public record MessageEditRequest
{ {
public string? Content { get; set; } [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
public Embed? Embed { get; set; } 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 string? Content { get; set; }
public object? Nonce { get; set; } public object? Nonce { get; set; }
public bool Tts { get; set; } public bool Tts { get; set; }
public AllowedMentions AllowedMentions { get; set; } public AllowedMentions? AllowedMentions { get; set; }
public Embed? Embed { 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;
using DSharpPlus.Entities; using DSharpPlus.Entities;
using DSharpPlus.Net;
using Myriad.Cache; using Myriad.Cache;
using Myriad.Extensions; using Myriad.Extensions;
using Myriad.Gateway; using Myriad.Gateway;
using Myriad.Rest.Types;
using Myriad.Rest.Types.Requests; using Myriad.Rest.Types.Requests;
using Myriad.Types; using Myriad.Types;
@ -34,7 +34,7 @@ namespace PluralKit.Bot
private readonly Guild? _guild; private readonly Guild? _guild;
private readonly Channel _channel; private readonly Channel _channel;
private readonly DiscordMessage _message = null; private readonly DiscordMessage _message = null;
private readonly Message _messageNew; private readonly MessageCreateEvent _messageNew;
private readonly Parameters _parameters; private readonly Parameters _parameters;
private readonly MessageContext _messageContext; private readonly MessageContext _messageContext;
private readonly PermissionSet _botPermissions; private readonly PermissionSet _botPermissions;
@ -79,6 +79,7 @@ namespace PluralKit.Bot
public DiscordChannel Channel => _message.Channel; public DiscordChannel Channel => _message.Channel;
public Channel ChannelNew => _channel; public Channel ChannelNew => _channel;
public User AuthorNew => _messageNew.Author; public User AuthorNew => _messageNew.Author;
public GuildMemberPartial MemberNew => _messageNew.Member;
public DiscordMessage Message => _message; public DiscordMessage Message => _message;
public Message MessageNew => _messageNew; public Message MessageNew => _messageNew;
public DiscordGuild Guild => _message.Channel.Guild; public DiscordGuild Guild => _message.Channel.Guild;
@ -91,6 +92,7 @@ namespace PluralKit.Bot
public PermissionSet UserPermissions => _userPermissions; public PermissionSet UserPermissions => _userPermissions;
public DiscordRestClient Rest => _rest; public DiscordRestClient Rest => _rest;
public DiscordApiClient RestNew => _newRest;
public PKSystem System => _senderSystem; public PKSystem System => _senderSystem;
@ -102,16 +104,16 @@ namespace PluralKit.Bot
public Task<DiscordMessage> Reply(string text, DiscordEmbed embed, public Task<DiscordMessage> Reply(string text, DiscordEmbed embed,
IEnumerable<IMention>? mentions = null) IEnumerable<IMention>? mentions = null)
{ {
return Reply(text, (DiscordEmbed) null, mentions); throw new NotImplementedException();
} }
public Task<DiscordMessage> Reply(DiscordEmbed embed, public Task<DiscordMessage> Reply(DiscordEmbed embed,
IEnumerable<IMention>? mentions = null) 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)) if (!BotPermissions.HasFlag(PermissionSet.SendMessages))
// Will be "swallowed" during the error handler anyway, this message is never shown. // 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 var msg = await _newRest.CreateMessage(_channel.Id, new MessageRequest
{ {
Content = text, Content = text,
Embed = embed Embed = embed,
AllowedMentions = mentions
}); });
// TODO: embeds/mentions // TODO: mentions should default to empty and not null?
// var msg = await Channel.SendMessageFixedAsync(text, embed: embed, mentions: mentions);
if (embed != null) if (embed != null)
{ {
@ -135,8 +137,7 @@ namespace PluralKit.Bot
await _commandMessageService.RegisterMessage(msg.Id, AuthorNew.Id); await _commandMessageService.RegisterMessage(msg.Id, AuthorNew.Id);
} }
// return msg; return msg;
return null;
} }
public async Task Execute<T>(Command commandDef, Func<T, Task> handler) public async Task Execute<T>(Command commandDef, Func<T, Task> handler)

View File

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

View File

@ -1,10 +1,11 @@
using System; using System;
using System.Threading.Tasks; using System.Threading.Tasks;
using DSharpPlus.Entities;
using Humanizer; using Humanizer;
using Myriad.Builders;
using Myriad.Types;
using NodaTime; using NodaTime;
using PluralKit.Core; using PluralKit.Core;
@ -84,10 +85,11 @@ namespace PluralKit.Bot
await ctx.Reply($"{Emojis.Success} Autoproxy set to **{member.NameFor(ctx)}** in this server."); 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 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 fronters = ctx.MessageContext.LastSwitchMembers;
var relevantMember = ctx.MessageContext.AutoproxyMode switch var relevantMember = ctx.MessageContext.AutoproxyMode switch
@ -98,35 +100,36 @@ namespace PluralKit.Bot
}; };
switch (ctx.MessageContext.AutoproxyMode) { 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; break;
case AutoproxyMode.Front: case AutoproxyMode.Front:
{ {
if (fronters.Length == 0) 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 else
{ {
if (relevantMember == null) 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."); 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; break;
} }
// AutoproxyMember is never null if Mode is Member, this is just to make the compiler shut up // AutoproxyMember is never null if Mode is Member, this is just to make the compiler shut up
case AutoproxyMode.Member when relevantMember != null: { 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; break;
} }
case AutoproxyMode.Latch: 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; break;
default: throw new ArgumentOutOfRangeException(); default: throw new ArgumentOutOfRangeException();
} }
if (!ctx.MessageContext.AllowAutoproxy) 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(); return eb.Build();
} }
@ -178,7 +181,7 @@ namespace PluralKit.Bot
else else
{ {
var statusString = ctx.MessageContext.AllowAutoproxy ? "enabled" : "disabled"; 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"; var statusString = allow ? "enabled" : "disabled";
if (ctx.MessageContext.AllowAutoproxy == allow) 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; return;
} }
var patch = new AccountPatch { AllowAutoproxy = allow }; var patch = new AccountPatch { AllowAutoproxy = allow };
await _db.Execute(conn => _repo.UpdateAccount(conn, ctx.Author.Id, patch)); 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) 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)); 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.Text.RegularExpressions;
using System.Threading.Tasks; using System.Threading.Tasks;
using DSharpPlus; using Myriad.Extensions;
using DSharpPlus.Entities; using Myriad.Types;
namespace PluralKit.Bot namespace PluralKit.Bot
{ {
@ -22,7 +22,7 @@ namespace PluralKit.Bot
// If we have a user @mention/ID, use their avatar // If we have a user @mention/ID, use their avatar
if (await ctx.MatchUser() is { } user) 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}; return new ParsedImage {Url = url, Source = AvatarSource.User, SourceUser = user};
} }
@ -64,7 +64,7 @@ namespace PluralKit.Bot
{ {
public string Url; public string Url;
public AvatarSource Source; public AvatarSource Source;
public DiscordUser? SourceUser; public User? SourceUser;
} }
public enum AvatarSource public enum AvatarSource

View File

@ -10,6 +10,8 @@ using DSharpPlus.Entities;
using Humanizer; using Humanizer;
using Myriad.Builders;
using PluralKit.Core; using PluralKit.Core;
namespace PluralKit.Bot namespace PluralKit.Bot
@ -194,7 +196,7 @@ namespace PluralKit.Bot
// The attachment's already right there, no need to preview it. // The attachment's already right there, no need to preview it.
var hasEmbed = img.Source != AvatarSource.Attachment; var hasEmbed = img.Source != AvatarSource.Attachment;
await (hasEmbed 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)); : 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}`"; 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); 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 => eb.WithSimpleLineContent(page.Select(g =>
{ {
@ -274,7 +276,7 @@ namespace PluralKit.Bot
else else
return $"[`{g.Hid}`] **{g.Name.EscapeMarkdown()}** ({"member".ToQuantity(g.MemberCount)})"; return $"[`{g.Hid}`] **{g.Name.EscapeMarkdown()}** ({"member".ToQuantity(g.MemberCount)})";
})); }));
eb.WithFooter($"{groups.Count} total."); eb.Footer(new($"{groups.Count} total."));
return Task.CompletedTask; return Task.CompletedTask;
} }
} }

View File

@ -3,10 +3,10 @@ using System.Linq;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using DSharpPlus.Entities;
using Humanizer; using Humanizer;
using Myriad.Builders;
using NodaTime; using NodaTime;
using PluralKit.Core; using PluralKit.Core;
@ -90,10 +90,10 @@ namespace PluralKit.Bot
await ctx.Paginate(members.ToAsyncEnumerable(), members.Count, itemsPerPage, embedTitle, Renderer); await ctx.Paginate(members.ToAsyncEnumerable(), members.Count, itemsPerPage, embedTitle, Renderer);
// Base renderer, dispatches based on type // 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 // 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 // Then call the specific renderers
if (opts.Type == ListType.Short) if (opts.Type == ListType.Short)
@ -104,7 +104,7 @@ namespace PluralKit.Bot
return Task.CompletedTask; 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 // We may end up over the description character limit
// so run it through a helper that "makes it work" :) // 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; var zone = ctx.System?.Zone ?? DateTimeZone.Utc;
foreach (var m in page) foreach (var m in page)
@ -162,7 +162,7 @@ namespace PluralKit.Bot
if (m.MemberVisibility == PrivacyLevel.Private) if (m.MemberVisibility == PrivacyLevel.Private)
profile.Append("\n*(this member is hidden)*"); 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 DSharpPlus.Entities;
using Myriad.Builders;
using PluralKit.Core; using PluralKit.Core;
namespace PluralKit.Bot namespace PluralKit.Bot
@ -135,7 +137,7 @@ namespace PluralKit.Bot
// The attachment's already right there, no need to preview it. // The attachment's already right there, no need to preview it.
var hasEmbed = avatar.Source != AvatarSource.Attachment; var hasEmbed = avatar.Source != AvatarSource.Attachment;
return hasEmbed 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); : ctx.Reply(msg);
} }

View File

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

View File

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

View File

@ -4,6 +4,8 @@ using System.Threading.Tasks;
using DSharpPlus.Entities; using DSharpPlus.Entities;
using Myriad.Builders;
using NodaTime; using NodaTime;
using NodaTime.Text; using NodaTime.Text;
using NodaTime.TimeZones; using NodaTime.TimeZones;
@ -150,7 +152,7 @@ namespace PluralKit.Bot
// The attachment's already right there, no need to preview it. // The attachment's already right there, no need to preview it.
var hasEmbed = img.Source != AvatarSource.Attachment; var hasEmbed = img.Source != AvatarSource.Attachment;
await (hasEmbed 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)); : ctx.Reply(msg));
} }

View File

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

View File

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

View File

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

View File

@ -6,19 +6,17 @@ using System.Threading.Tasks;
using Autofac; using Autofac;
using DSharpPlus; using Myriad.Builders;
using DSharpPlus.Entities; using Myriad.Gateway;
using DSharpPlus.EventArgs; using Myriad.Rest.Exceptions;
using DSharpPlus.Exceptions; using Myriad.Rest.Types;
using Myriad.Rest.Types.Requests;
using Myriad.Types; using Myriad.Types;
using NodaTime; using NodaTime;
using PluralKit.Core; using PluralKit.Core;
using Permissions = DSharpPlus.Permissions;
namespace PluralKit.Bot { namespace PluralKit.Bot {
public static class ContextUtils { public static class ContextUtils {
public static async Task<bool> ConfirmClear(this Context ctx, string toClear) public static async Task<bool> ConfirmClear(this Context ctx, string toClear)
@ -27,52 +25,45 @@ namespace PluralKit.Bot {
else return true; 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; if (matchFlag && ctx.MatchFlag("y", "yes")) return true;
else message = await ctx.Reply(msgString, mentions: mentions); else message = await ctx.Reply(msgString, mentions: mentions);
var cts = new CancellationTokenSource(); var cts = new CancellationTokenSource();
if (user == null) user = ctx.Author; if (user == null) user = ctx.AuthorNew;
if (timeout == null) timeout = Duration.FromMinutes(5); 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 // "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.ChannelId != message.ChannelId || e.MessageId != message.Id) return false;
if (e.User.Id != user.Id) return false; if (e.UserId != user.Id) return false;
return true; 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; if (e.Author.Id != user.Id) return false;
var strings = new [] {"y", "yes", "n", "no"}; var strings = new [] {"y", "yes", "n", "no"};
foreach (var str in strings) return strings.Any(str => string.Equals(e.Content, str, StringComparison.InvariantCultureIgnoreCase));
if (e.Message.Content.Equals(str, StringComparison.InvariantCultureIgnoreCase))
return true;
return false;
} }
var messageTask = ctx.Services.Resolve<HandlerQueue<MessageCreateEventArgs>>().WaitFor(MessagePredicate, timeout, cts.Token); var messageTask = ctx.Services.Resolve<HandlerQueue<MessageCreateEvent>>().WaitFor(MessagePredicate, timeout, cts.Token);
var reactionTask = ctx.Services.Resolve<HandlerQueue<MessageReactionAddEventArgs>>().WaitFor(ReactionPredicate, timeout, cts.Token); var reactionTask = ctx.Services.Resolve<HandlerQueue<MessageReactionAddEvent>>().WaitFor(ReactionPredicate, timeout, cts.Token);
var theTask = await Task.WhenAny(messageTask, reactionTask); var theTask = await Task.WhenAny(messageTask, reactionTask);
cts.Cancel(); cts.Cancel();
if (theTask == messageTask) if (theTask == messageTask)
{ {
var responseMsg = (await messageTask).Message; var responseMsg = (await messageTask);
var positives = new[] {"y", "yes"}; var positives = new[] {"y", "yes"};
foreach (var p in positives) return positives.Any(p => string.Equals(responseMsg.Content, p, StringComparison.InvariantCultureIgnoreCase));
if (responseMsg.Content.Equals(p, StringComparison.InvariantCultureIgnoreCase))
return true;
return false;
} }
if (theTask == reactionTask) if (theTask == reactionTask)
@ -81,50 +72,45 @@ namespace PluralKit.Bot {
return false; return false;
} }
public static async Task<MessageReactionAddEventArgs> AwaitReaction(this Context ctx, DiscordMessage message, DiscordUser user = null, Func<MessageReactionAddEventArgs, bool> predicate = null, TimeSpan? timeout = null) { public static async Task<MessageReactionAddEvent> AwaitReaction(this Context ctx, Message message, User user = null, Func<MessageReactionAddEvent, bool> predicate = null, Duration? timeout = null)
var tcs = new TaskCompletionSource<MessageReactionAddEventArgs>(); {
Task Inner(DiscordClient _, MessageReactionAddEventArgs args) { bool ReactionPredicate(MessageReactionAddEvent evt)
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 (message.Id != evt.MessageId) return false; // Ignore reactions for different messages
if (predicate != null && !predicate.Invoke(args)) return Task.CompletedTask; // Check predicate if (user != null && user.Id != evt.UserId) return false; // Ignore messages from other users if a user was defined
tcs.SetResult(args); if (predicate != null && !predicate.Invoke(evt)) return false; // Check predicate
return Task.CompletedTask; 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) public static async Task<bool> ConfirmWithReply(this Context ctx, string expectedReply)
{ {
bool Predicate(MessageCreateEventArgs e) => bool Predicate(MessageCreateEvent e) =>
e.Author == ctx.Author && e.Channel.Id == ctx.Channel.Id; 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)); .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 // TODO: make this generic enough we can use it in Choose<T> below
var buffer = new List<T>(); var buffer = new List<T>();
await using var enumerator = items.GetAsyncEnumerator(); await using var enumerator = items.GetAsyncEnumerator();
var pageCount = (int) Math.Ceiling(totalCount / (double) itemsPerPage); 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; var bufferedItemsNeeded = (page + 1) * itemsPerPage;
while (buffer.Count < bufferedItemsNeeded && await enumerator.MoveNextAsync()) while (buffer.Count < bufferedItemsNeeded && await enumerator.MoveNextAsync())
buffer.Add(enumerator.Current); buffer.Add(enumerator.Current);
var eb = new DiscordEmbedBuilder(); var eb = new EmbedBuilder();
eb.Title = pageCount > 1 ? $"[{page+1}/{pageCount}] {title}" : title; eb.Title(pageCount > 1 ? $"[{page+1}/{pageCount}] {title}" : title);
await renderer(eb, buffer.Skip(page*itemsPerPage).Take(itemsPerPage)); await renderer(eb, buffer.Skip(page*itemsPerPage).Take(itemsPerPage));
return eb.Build(); return eb.Build();
} }
@ -134,13 +120,13 @@ namespace PluralKit.Bot {
var msg = await ctx.Reply(embed: await MakeEmbedForPage(0)); var msg = await ctx.Reply(embed: await MakeEmbedForPage(0));
if (pageCount <= 1) return; // If we only have one (or no) page, don't bother with the reaction/pagination logic, lol 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 }; string[] botEmojis = { "\u23EA", "\u2B05", "\u27A1", "\u23E9", Emojis.Error };
var _ = msg.CreateReactionsBulk(botEmojis); // Again, "fork" var _ = ctx.RestNew.CreateReactionsBulk(msg, botEmojis); // Again, "fork"
try { try {
var currentPage = 0; var currentPage = 0;
while (true) { 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 // Increment/decrement page counter based on which reaction was clicked
if (reaction.Emoji.Name == "\u23EA") currentPage = 0; // << 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 we can, remove the user's reaction (so they can press again quickly)
if (ctx.BotPermissions.HasFlag(PermissionSet.ManageMessages)) 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 // Edit the embed with the new page
var embed = await MakeEmbedForPage(currentPage); var embed = await MakeEmbedForPage(currentPage);
await msg.ModifyAsync(embed: embed); await ctx.RestNew.EditMessage(msg.ChannelId, msg.Id, new MessageEditRequest {Embed = embed});
} }
} catch (TimeoutException) { } catch (TimeoutException) {
// "escape hatch", clean up as if we hit X // "escape hatch", clean up as if we hit X
} }
if (ctx.BotPermissions.HasFlag(PermissionSet.ManageMessages)) 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 // If we get a "NotFound" error, the message has been deleted and thus not our problem
catch (NotFoundException) { } catch (NotFoundException) { }
@ -203,9 +189,10 @@ namespace PluralKit.Bot {
// Add back/forward reactions and the actual indicator emojis // Add back/forward reactions and the actual indicator emojis
async Task AddEmojis() async Task AddEmojis()
{ {
await msg.CreateReactionAsync(DiscordEmoji.FromUnicode("\u2B05")); await ctx.RestNew.CreateReaction(msg.ChannelId, msg.Id, new() { Name = "\u2B05" });
await msg.CreateReactionAsync(DiscordEmoji.FromUnicode("\u27A1")); await ctx.RestNew.CreateReaction(msg.ChannelId, msg.Id, new() { Name = "\u27A1" });
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(); // Not concerned about awaiting var _ = AddEmojis(); // Not concerned about awaiting
@ -213,7 +200,7 @@ namespace PluralKit.Bot {
while (true) while (true)
{ {
// Wait for a reaction // 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 it's a movement reaction, inc/dec the page index
if (reaction.Emoji.Name == "\u2B05") currPage -= 1; // < if (reaction.Emoji.Name == "\u2B05") currPage -= 1; // <
@ -230,8 +217,13 @@ namespace PluralKit.Bot {
if (idx < items.Count) return items[idx]; if (idx < items.Count) return items[idx];
} }
var __ = msg.DeleteReactionAsync(reaction.Emoji, ctx.Author); // don't care about awaiting var __ = ctx.RestNew.DeleteUserReaction(msg.ChannelId, msg.Id, reaction.Emoji, ctx.Author.Id);
await msg.ModifyAsync($"**[Page {currPage + 1}/{pageCount}]**\n{description}\n{MakeOptionList(currPage)}"); await ctx.RestNew.EditMessage(msg.ChannelId, msg.Id,
new()
{
Content =
$"**[Page {currPage + 1}/{pageCount}]**\n{description}\n{MakeOptionList(currPage)}"
});
} }
} }
else else
@ -241,13 +233,14 @@ namespace PluralKit.Bot {
// Add the relevant reactions (we don't care too much about awaiting) // Add the relevant reactions (we don't care too much about awaiting)
async Task AddEmojis() 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(); var _ = AddEmojis();
// Then wait for a reaction and return whichever one we found // 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)]; return items[Array.IndexOf(indicators, reaction.Emoji.Name)];
} }
} }
@ -271,12 +264,12 @@ namespace PluralKit.Bot {
try 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; return await task;
} }
finally 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.EventArgs;
using DSharpPlus.Exceptions; using DSharpPlus.Exceptions;
using Myriad.Builders;
using Myriad.Extensions; using Myriad.Extensions;
using Myriad.Rest;
using Myriad.Rest.Types; using Myriad.Rest.Types;
using Myriad.Types; using Myriad.Types;
@ -116,11 +118,11 @@ namespace PluralKit.Bot
public static ulong InstantToSnowflake(DateTimeOffset time) => public static ulong InstantToSnowflake(DateTimeOffset time) =>
(ulong) (time - new DateTimeOffset(2015, 1, 1, 0, 0, 0, TimeSpan.Zero)).TotalMilliseconds << 22; (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) 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) => static int CharacterLimit(int pageNumber) =>
// First chunk goes in description (2048 chars), rest go in embed values (1000 chars) // 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 // Add the first page to the embed description
if (pages.Count > 0) if (pages.Count > 0)
eb.WithDescription(pages[0]); eb.Description(pages[0]);
// Add the rest to blank-named (\u200B) fields // Add the rest to blank-named (\u200B) fields
for (var i = 1; i < pages.Count; i++) for (var i = 1; i < pages.Count; i++)
eb.AddField("\u200B", pages[i]); eb.Field(new("\u200B", pages[i]));
return eb; return eb;
} }

View File

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