diff --git a/PluralKit.Bot/CommandSystem/Context.cs b/PluralKit.Bot/CommandSystem/Context.cs index 1a710c84..17e18637 100644 --- a/PluralKit.Bot/CommandSystem/Context.cs +++ b/PluralKit.Bot/CommandSystem/Context.cs @@ -6,12 +6,11 @@ using App.Metrics; using Autofac; -using Discord; -using Discord.WebSocket; using DSharpPlus; using DSharpPlus.Entities; +using PluralKit.Bot.Utils; using PluralKit.Core; namespace PluralKit.Bot @@ -20,6 +19,7 @@ namespace PluralKit.Bot { private ILifetimeScope _provider; + private readonly DiscordRestClient _rest; private readonly DiscordShardedClient _client; private readonly DiscordClient _shard; private readonly DiscordMessage _message; @@ -34,6 +34,7 @@ namespace PluralKit.Bot public Context(ILifetimeScope provider, DiscordClient shard, DiscordMessage message, int commandParseOffset, PKSystem senderSystem) { + _rest = provider.Resolve(); _client = provider.Resolve(); _message = message; _shard = shard; @@ -50,6 +51,9 @@ namespace PluralKit.Bot public DiscordGuild Guild => _message.Channel.Guild; public DiscordClient Shard => _shard; public DiscordShardedClient Client => _client; + + public DiscordRestClient Rest => _rest; + public PKSystem System => _senderSystem; public string PopArgument() => _parameters.Pop(); @@ -280,10 +284,11 @@ namespace PluralKit.Bot public DiscordChannel MatchChannel() { if (!MentionUtils.TryParseChannel(PeekArgument(), out var channel)) return null; - if (!(_client.GetChannelAsync(channel) is ITextChannel textChannel)) return null; + var discordChannel = _rest.GetChannelAsync(channel).GetAwaiter().GetResult(); + if (discordChannel.Type != ChannelType.Text) return null; PopArgument(); - return textChannel; + return null;// return textChannel; } } } \ No newline at end of file diff --git a/PluralKit.Bot/Commands/Autoproxy.cs b/PluralKit.Bot/Commands/Autoproxy.cs index a84d9a7c..714478f4 100644 --- a/PluralKit.Bot/Commands/Autoproxy.cs +++ b/PluralKit.Bot/Commands/Autoproxy.cs @@ -1,8 +1,8 @@ -using System; +using System; using System.Linq; using System.Threading.Tasks; -using Discord; +using DSharpPlus.Entities; using PluralKit.Core; @@ -96,12 +96,12 @@ namespace PluralKit.Bot await ctx.Reply($"{Emojis.Success} Autoproxy set to **{member.Name}** in this server."); } - private async Task CreateAutoproxyStatusEmbed(Context ctx) + private async Task CreateAutoproxyStatusEmbed(Context ctx) { var settings = await _data.GetSystemGuildSettings(ctx.System, ctx.Guild.Id); var commandList = "**pk;autoproxy latch** - Autoproxies as last-proxied member\n**pk;autoproxy front** - Autoproxies as current (first) fronter\n**pk;autoproxy ** - Autoproxies as a specific member"; - var eb = new EmbedBuilder().WithTitle($"Current autoproxy status (for {ctx.Guild.Name.EscapeMarkdown()})"); + var eb = new DiscordEmbedBuilder().WithTitle($"Current autoproxy status (for {ctx.Guild.Name.EscapeMarkdown()})"); switch (settings.AutoproxyMode) { case AutoproxyMode.Off: eb.WithDescription($"Autoproxy is currently **off** in this server. To enable it, use one of the following commands:\n{commandList}"); diff --git a/PluralKit.Bot/Commands/CommandTree.cs b/PluralKit.Bot/Commands/CommandTree.cs index 3ec50e65..900811aa 100644 --- a/PluralKit.Bot/Commands/CommandTree.cs +++ b/PluralKit.Bot/Commands/CommandTree.cs @@ -1,7 +1,7 @@ using System.Linq; using System.Threading.Tasks; -using Discord.WebSocket; +using DSharpPlus; using PluralKit.Core; @@ -81,6 +81,7 @@ namespace PluralKit.Bot public CommandTree(DiscordShardedClient client) { + _client = client; } @@ -345,7 +346,7 @@ namespace PluralKit.Bot { // Try to resolve the user ID to find the associated account, // so we can print their username. - var user = await _client.Rest.GetUserAsync(id); + var user = await ctx.Rest.GetUserAsync(id); // Print descriptive errors based on whether we found the user or not. if (user == null) diff --git a/PluralKit.Bot/Commands/Help.cs b/PluralKit.Bot/Commands/Help.cs index 07ea6b8a..de4a6d12 100644 --- a/PluralKit.Bot/Commands/Help.cs +++ b/PluralKit.Bot/Commands/Help.cs @@ -1,6 +1,6 @@ using System.Threading.Tasks; -using Discord; +using DSharpPlus.Entities; using PluralKit.Core; @@ -10,7 +10,7 @@ namespace PluralKit.Bot { public async Task HelpRoot(Context ctx) { - await ctx.Reply(embed: new EmbedBuilder() + await ctx.Reply(embed: new DiscordEmbedBuilder() .WithTitle("PluralKit") .WithDescription("PluralKit is a bot designed for plural communities on Discord. It allows you to register systems, maintain system information, set up message proxying, log switches, and more.") .AddField("What is this for? What are systems?", "This bot detects messages with certain tags associated with a profile, then replaces that message under a \"pseudo-account\" of that profile using webhooks. This is useful for multiple people sharing one body (aka \"systems\"), people who wish to roleplay as different characters without having several accounts, or anyone else who may want to post messages as a different person from the same account.") @@ -20,7 +20,7 @@ namespace PluralKit.Bot .AddField("More information", "For a full list of commands, see [the command list](https://pluralkit.me/commands).\nFor a more in-depth explanation of message proxying, see [the documentation](https://pluralkit.me/guide#proxying).\nIf you're an existing user of Tupperbox, type `pk;import` and attach a Tupperbox export file (from `tul!export`) to import your data from there.") .AddField("Support server", "We also have a Discord server for support, discussion, suggestions, announcements, etc: https://discord.gg/PczBt78") .WithFooter("By @Ske#6201 | GitHub: https://github.com/xSke/PluralKit/ | Website: https://pluralkit.me/") - .WithColor(Color.Blue) + .WithColor(DiscordColor.Blue) .Build()); } } diff --git a/PluralKit.Bot/Commands/ImportExport.cs b/PluralKit.Bot/Commands/ImportExport.cs index 2a9a2d43..946d6b23 100644 --- a/PluralKit.Bot/Commands/ImportExport.cs +++ b/PluralKit.Bot/Commands/ImportExport.cs @@ -5,10 +5,9 @@ using System.Net.Http; using System.Text; using System.Threading.Tasks; -using Discord; -using Discord.Net; - using Newtonsoft.Json; +using DSharpPlus.Exceptions; +using DSharpPlus.Entities; using PluralKit.Core; @@ -134,13 +133,14 @@ namespace PluralKit.Bot try { - await ctx.Author.SendFileAsync(stream, "system.json", $"{Emojis.Success} Here you go!"); - + var dm = await ctx.Rest.CreateDmAsync(ctx.Author.Id); + await dm.SendFileAsync("system.json", stream, $"{Emojis.Success} Here you go!"); + // If the original message wasn't posted in DMs, send a public reminder - if (!(ctx.Channel is IDMChannel)) + if (!(ctx.Channel is DiscordDmChannel)) await ctx.Reply($"{Emojis.Success} Check your DMs!"); } - catch (HttpException) + catch (UnauthorizedException) { // If user has DMs closed, tell 'em to open them await ctx.Reply( diff --git a/PluralKit.Bot/Commands/MemberAvatar.cs b/PluralKit.Bot/Commands/MemberAvatar.cs index ac84d964..0e0b8916 100644 --- a/PluralKit.Bot/Commands/MemberAvatar.cs +++ b/PluralKit.Bot/Commands/MemberAvatar.cs @@ -1,7 +1,8 @@ -using System.Linq; +using System.Linq; using System.Threading.Tasks; -using Discord; +using DSharpPlus; +using DSharpPlus.Entities; using PluralKit.Core; @@ -39,7 +40,7 @@ namespace PluralKit.Bot { if ((target.AvatarUrl?.Trim() ?? "").Length > 0) { - var eb = new EmbedBuilder() + var eb = new DiscordEmbedBuilder() .WithTitle($"{target.Name.SanitizeMentions()}'s avatar") .WithImageUrl(target.AvatarUrl); if (target.System == ctx.System?.Id) @@ -55,18 +56,17 @@ namespace PluralKit.Bot return; } - + var user = await ctx.MatchUser(); if (ctx.System == null) throw Errors.NoSystemError; if (target.System != ctx.System.Id) throw Errors.NotOwnMemberError; - - else if (await ctx.MatchUser() is IUser user) + else if (user != null) { - if (user.AvatarId == null) throw Errors.UserHasNoAvatar; + if (user.AvatarUrl == user.DefaultAvatarUrl) throw Errors.UserHasNoAvatar; //TODO: is this necessary? target.AvatarUrl = user.GetAvatarUrl(ImageFormat.Png, size: 256); await _data.SaveMember(target); - var embed = new EmbedBuilder().WithImageUrl(target.AvatarUrl).Build(); + var embed = new DiscordEmbedBuilder().WithImageUrl(target.AvatarUrl).Build(); await ctx.Reply( $"{Emojis.Success} Member avatar changed to {user.Username}'s avatar! {Emojis.Warn} Please note that if {user.Username} changes their avatar, the member's avatar will need to be re-set.", embed: embed); } @@ -76,10 +76,10 @@ namespace PluralKit.Bot target.AvatarUrl = url; await _data.SaveMember(target); - var embed = new EmbedBuilder().WithImageUrl(url).Build(); + var embed = new DiscordEmbedBuilder().WithImageUrl(url).Build(); await ctx.Reply($"{Emojis.Success} Member avatar changed.", embed: embed); } - else if (ctx.Message.Attachments.FirstOrDefault() is Attachment attachment) + else if (ctx.Message.Attachments.FirstOrDefault() is DiscordAttachment attachment) { await AvatarUtils.VerifyAvatarOrThrow(attachment.Url); target.AvatarUrl = attachment.Url; @@ -113,7 +113,7 @@ namespace PluralKit.Bot { if ((guildData.AvatarUrl?.Trim() ?? "").Length > 0) { - var eb = new EmbedBuilder() + var eb = new DiscordEmbedBuilder() .WithTitle($"{target.Name.SanitizeMentions()}'s server avatar (for {ctx.Guild.Name})") .WithImageUrl(guildData.AvatarUrl); if (target.System == ctx.System?.Id) @@ -125,17 +125,17 @@ namespace PluralKit.Bot return; } - + var user = await ctx.MatchUser(); if (ctx.System == null) throw Errors.NoSystemError; if (target.System != ctx.System.Id) throw Errors.NotOwnMemberError; - if (await ctx.MatchUser() is IUser user) + if (user != null) { - if (user.AvatarId == null) throw Errors.UserHasNoAvatar; + if (user.AvatarUrl == user.DefaultAvatarUrl) throw Errors.UserHasNoAvatar; guildData.AvatarUrl = user.GetAvatarUrl(ImageFormat.Png, size: 256); await _data.SetMemberGuildSettings(target, ctx.Guild.Id, guildData); - var embed = new EmbedBuilder().WithImageUrl(guildData.AvatarUrl).Build(); + var embed = new DiscordEmbedBuilder().WithImageUrl(guildData.AvatarUrl).Build(); await ctx.Reply( $"{Emojis.Success} Member server avatar changed to {user.Username}'s avatar! This avatar will now be used when proxying in this server (**{ctx.Guild.Name}**). {Emojis.Warn} Please note that if {user.Username} changes their avatar, the member's server avatar will need to be re-set.", embed: embed); } @@ -145,10 +145,10 @@ namespace PluralKit.Bot guildData.AvatarUrl = url; await _data.SetMemberGuildSettings(target, ctx.Guild.Id, guildData); - var embed = new EmbedBuilder().WithImageUrl(url).Build(); + var embed = new DiscordEmbedBuilder().WithImageUrl(url).Build(); await ctx.Reply($"{Emojis.Success} Member server avatar changed. This avatar will now be used when proxying in this server (**{ctx.Guild.Name}**).", embed: embed); } - else if (ctx.Message.Attachments.FirstOrDefault() is Attachment attachment) + else if (ctx.Message.Attachments.FirstOrDefault() is DiscordAttachment attachment) { await AvatarUtils.VerifyAvatarOrThrow(attachment.Url); guildData.AvatarUrl = attachment.Url; diff --git a/PluralKit.Bot/Commands/MemberEdit.cs b/PluralKit.Bot/Commands/MemberEdit.cs index a6fff5e7..e471a2f9 100644 --- a/PluralKit.Bot/Commands/MemberEdit.cs +++ b/PluralKit.Bot/Commands/MemberEdit.cs @@ -1,8 +1,8 @@ -using System; +using System; using System.Text.RegularExpressions; using System.Threading.Tasks; -using Discord; +using DSharpPlus.Entities; using NodaTime; @@ -86,7 +86,7 @@ namespace PluralKit.Bot else if (ctx.MatchFlag("r", "raw")) await ctx.Reply($"```\n{target.Description.SanitizeMentions()}\n```"); else - await ctx.Reply(embed: new EmbedBuilder() + await ctx.Reply(embed: new DiscordEmbedBuilder() .WithTitle("Member description") .WithDescription(target.Description) .AddField("\u200B", $"To print the description with formatting, type `pk;member {target.Hid} description -raw`." @@ -163,7 +163,7 @@ namespace PluralKit.Bot else await ctx.Reply("This member does not have a color set."); else - await ctx.Reply(embed: new EmbedBuilder() + await ctx.Reply(embed: new DiscordEmbedBuilder() .WithTitle("Member color") .WithColor(target.Color.ToDiscordColor().Value) .WithThumbnailUrl($"https://fakeimg.pl/256x256/{target.Color}/?text=%20") @@ -180,7 +180,7 @@ namespace PluralKit.Bot target.Color = color.ToLower(); await _data.SaveMember(target); - await ctx.Reply(embed: new EmbedBuilder() + await ctx.Reply(embed: new DiscordEmbedBuilder() .WithTitle($"{Emojis.Success} Member color changed.") .WithColor(target.Color.ToDiscordColor().Value) .WithThumbnailUrl($"https://fakeimg.pl/256x256/{target.Color}/?text=%20") @@ -220,13 +220,13 @@ namespace PluralKit.Bot } } - private async Task CreateMemberNameInfoEmbed(Context ctx, PKMember target) + private async Task CreateMemberNameInfoEmbed(Context ctx, PKMember target) { MemberGuildSettings memberGuildConfig = null; if (ctx.Guild != null) memberGuildConfig = await _data.GetMemberGuildSettings(target, ctx.Guild.Id); - var eb = new EmbedBuilder().WithTitle($"Member names") + var eb = new DiscordEmbedBuilder().WithTitle($"Member names") .WithFooter($"Member ID: {target.Hid} | Active name in bold. Server name overrides display name, which overrides base name."); if (target.DisplayName == null && memberGuildConfig?.DisplayName == null) diff --git a/PluralKit.Bot/Commands/Misc.cs b/PluralKit.Bot/Commands/Misc.cs index 6df8a8ae..98dcf3de 100644 --- a/PluralKit.Bot/Commands/Misc.cs +++ b/PluralKit.Bot/Commands/Misc.cs @@ -3,16 +3,18 @@ using System.Diagnostics; using System.Linq; using System.Text.RegularExpressions; using System.Threading.Tasks; +using System.Net.WebSockets; using App.Metrics; -using Discord; +using DSharpPlus; using Humanizer; using NodaTime; using PluralKit.Core; +using DSharpPlus.Entities; namespace PluralKit.Bot { public class Misc @@ -36,18 +38,16 @@ namespace PluralKit.Bot { public async Task Invite(Context ctx) { - var clientId = _botConfig.ClientId ?? (await ctx.Client.GetApplicationInfoAsync()).Id; - var permissions = new GuildPermissions( - addReactions: true, - attachFiles: true, - embedLinks: true, - manageMessages: true, - manageWebhooks: true, - readMessageHistory: true, - sendMessages: true - ); - - var invite = $"https://discordapp.com/oauth2/authorize?client_id={clientId}&scope=bot&permissions={permissions.RawValue}"; + 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://discordapp.com/oauth2/authorize?client_id={clientId}&scope=bot&permissions={(long)permissions}"; await ctx.Reply($"{Emojis.Success} Use this link to add PluralKit to your server:\n<{invite}>"); } @@ -65,8 +65,8 @@ namespace PluralKit.Bot { var totalMessages = _metrics.Snapshot.GetForContext("Application").Gauges.First(m => m.MultidimensionalName == CoreMetrics.MessageCount.Name).Value; var shardId = ctx.Shard.ShardId; - var shardTotal = ctx.Client.Shards.Count; - var shardUpTotal = ctx.Client.Shards.Select(s => s.ConnectionState == ConnectionState.Connected).Count(); + var shardTotal = ctx.Client.ShardClients.Count; + var shardUpTotal = ctx.Client.ShardClients.Where(x => x.Value.IsConnected()).Count(); var shardInfo = _shards.GetShardInfo(ctx.Shard); var process = Process.GetCurrentProcess(); @@ -74,7 +74,7 @@ namespace PluralKit.Bot { var shardUptime = SystemClock.Instance.GetCurrentInstant() - shardInfo.LastConnectionTime; - var embed = new EmbedBuilder() + var embed = new DiscordEmbedBuilder() .AddField("Messages processed", $"{messagesReceived.OneMinuteRate * 60:F1}/m ({messagesReceived.FifteenMinuteRate * 60:F1}/m over 15m)", true) .AddField("Messages proxied", $"{messagesProxied.OneMinuteRate * 60:F1}/m ({messagesProxied.FifteenMinuteRate * 60:F1}/m over 15m)", true) .AddField("Commands executed", $"{commandsRun.OneMinuteRate * 60:F1}/m ({commandsRun.FifteenMinuteRate * 60:F1}/m over 15m)", true) @@ -84,17 +84,12 @@ namespace PluralKit.Bot { .AddField("Memory usage", $"{memoryUsage / 1024 / 1024} MiB", true) .AddField("Latency", $"API: {(msg.Timestamp - ctx.Message.Timestamp).TotalMilliseconds:F0} ms, shard: {shardInfo.ShardLatency} ms", true) .AddField("Total numbers", $"{totalSystems:N0} systems, {totalMembers:N0} members, {totalSwitches:N0} switches, {totalMessages:N0} messages"); - - await msg.ModifyAsync(f => - { - f.Content = ""; - f.Embed = embed.Build(); - }); + await msg.ModifyAsync("", embed.Build()); } - + public async Task PermCheckGuild(Context ctx) { - IGuild guild; + DiscordGuild guild; if (ctx.Guild != null && !ctx.HasNext()) { @@ -107,51 +102,52 @@ namespace PluralKit.Bot { throw new PKSyntaxError($"Could not parse `{guildIdStr.SanitizeMentions()}` as an ID."); // TODO: will this call break for sharding if you try to request a guild on a different bot instance? - guild = ctx.Client.GetGuild(guildId); + guild = await ctx.Rest.GetGuildAsync(guildId); if (guild == null) throw Errors.GuildNotFound(guildId); } - + var requiredPermissions = new [] { - ChannelPermission.ViewChannel, - ChannelPermission.SendMessages, - ChannelPermission.AddReactions, - ChannelPermission.AttachFiles, - ChannelPermission.EmbedLinks, - ChannelPermission.ManageMessages, - ChannelPermission.ManageWebhooks + Permissions.AccessChannels, + Permissions.SendMessages, + Permissions.AddReactions, + Permissions.AttachFiles, + Permissions.EmbedLinks, + Permissions.ManageMessages, + Permissions.ManageWebhooks }; // Loop through every channel and group them by sets of permissions missing - var permissionsMissing = new Dictionary>(); - foreach (var channel in await guild.GetTextChannelsAsync()) + var permissionsMissing = new Dictionary>(); + var guildTextChannels = (await guild.GetChannelsAsync()).Where(x => x.Type == ChannelType.Text); + foreach (var channel in guildTextChannels) { // TODO: do we need to hide channels here to prevent info-leaking? - var perms = channel.PermissionsIn(); + var perms = await channel.PermissionsIn(ctx.Client.CurrentUser); // We use a bitfield so we can set individual permission bits in the loop ulong missingPermissionField = 0; foreach (var requiredPermission in requiredPermissions) - if (!perms.Has(requiredPermission)) + if (!perms.HasPermission(requiredPermission)) missingPermissionField |= (ulong) requiredPermission; // If we're not missing any permissions, don't bother adding it to the dict // 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()); + permissionsMissing.TryAdd(missingPermissionField, new List()); permissionsMissing[missingPermissionField].Add(channel); } } // Generate the output embed - var eb = new EmbedBuilder() + var eb = new DiscordEmbedBuilder() .WithTitle($"Permission check for **{guild.Name.SanitizeMentions()}**"); if (permissionsMissing.Count == 0) { - eb.WithDescription($"No errors found, all channels proxyable :)").WithColor(Color.Green); + eb.WithDescription($"No errors found, all channels proxyable :)").WithColor(DiscordColor.Green); } else { @@ -159,15 +155,13 @@ namespace PluralKit.Bot { { // Each missing permission field can have multiple missing channels // so we extract them all and generate a comma-separated list - var missingPermissionNames = string.Join(", ", new ChannelPermissions(missingPermissionField) - .ToList() - .Select(perm => perm.Humanize().Transform(To.TitleCase))); + 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(Color.Red); + eb.WithColor(DiscordColor.Red); } } @@ -189,7 +183,7 @@ namespace PluralKit.Bot { var message = await _data.GetMessage(messageId); if (message == null) throw Errors.MessageNotFound(messageId); - await ctx.Reply(embed: await _embeds.CreateMessageInfoEmbed(message)); + await ctx.Reply(embed: await _embeds.CreateMessageInfoEmbed(ctx.Shard, message)); //TODO: test this } } } \ No newline at end of file diff --git a/PluralKit.Bot/Commands/ServerConfig.cs b/PluralKit.Bot/Commands/ServerConfig.cs index 8f758067..ad7658ff 100644 --- a/PluralKit.Bot/Commands/ServerConfig.cs +++ b/PluralKit.Bot/Commands/ServerConfig.cs @@ -2,7 +2,8 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using Discord; +using DSharpPlus; +using DSharpPlus.Entities; using PluralKit.Core; @@ -20,12 +21,13 @@ namespace PluralKit.Bot public async Task SetLogChannel(Context ctx) { - ctx.CheckGuildContext().CheckAuthorPermission(GuildPermission.ManageGuild, "Manage Server"); + ctx.CheckGuildContext().CheckAuthorPermission(Permissions.ManageGuild, "Manage Server"); - ITextChannel channel = null; + DiscordChannel channel = null; if (ctx.HasNext()) channel = ctx.MatchChannel() ?? throw new PKSyntaxError("You must pass a #channel to set."); - if (channel != null && channel.GuildId != ctx.Guild.Id) throw new PKError("That channel is not in this server!"); + if (channel != null && channel.GuildId != ctx.Guild.Id) throw new PKError("That channel is not in this server!"); + if (channel.Type != ChannelType.Text) throw new PKError("The logging channel must be a text channel."); //TODO: test this var cfg = await _data.GetOrCreateGuildConfig(ctx.Guild.Id); cfg.LogChannel = channel?.Id; @@ -39,15 +41,16 @@ namespace PluralKit.Bot public async Task SetLogEnabled(Context ctx, bool enable) { - ctx.CheckGuildContext().CheckAuthorPermission(GuildPermission.ManageGuild, "Manage Server"); + ctx.CheckGuildContext().CheckAuthorPermission(Permissions.ManageGuild, "Manage Server"); - var affectedChannels = new List(); + var affectedChannels = new List(); if (ctx.Match("all")) - affectedChannels = (await ctx.Guild.GetChannelsAsync()).OfType().ToList(); + affectedChannels = (await ctx.Guild.GetChannelsAsync()).Where(x => x.Type == ChannelType.Text).ToList(); else if (!ctx.HasNext()) throw new PKSyntaxError("You must pass one or more #channels."); else while (ctx.HasNext()) { - if (!(ctx.MatchChannel() is ITextChannel channel)) + var channel = ctx.MatchChannel(); //TODO: test this + if (channel.Type != ChannelType.Text) throw new PKSyntaxError($"Channel \"{ctx.PopArgument().SanitizeMentions()}\" not found."); if (channel.GuildId != ctx.Guild.Id) throw new PKError($"Channel {ctx.Guild.Id} is not in this server."); affectedChannels.Add(channel); @@ -65,15 +68,16 @@ namespace PluralKit.Bot public async Task SetBlacklisted(Context ctx, bool onBlacklist) { - ctx.CheckGuildContext().CheckAuthorPermission(GuildPermission.ManageGuild, "Manage Server"); + ctx.CheckGuildContext().CheckAuthorPermission(Permissions.ManageGuild, "Manage Server"); - var affectedChannels = new List(); + var affectedChannels = new List(); if (ctx.Match("all")) - affectedChannels = (await ctx.Guild.GetChannelsAsync()).OfType().ToList(); + affectedChannels = (await ctx.Guild.GetChannelsAsync()).Where(x => x.Type == ChannelType.Text).ToList(); else if (!ctx.HasNext()) throw new PKSyntaxError("You must pass one or more #channels."); else while (ctx.HasNext()) { - if (!(ctx.MatchChannel() is ITextChannel channel)) + var channel = ctx.MatchChannel(); //TODO: test this + if (channel.Type != ChannelType.Text) throw new PKSyntaxError($"Channel \"{ctx.PopArgument().SanitizeMentions()}\" not found."); if (channel.GuildId != ctx.Guild.Id) throw new PKError($"Channel {ctx.Guild.Id} is not in this server."); affectedChannels.Add(channel); @@ -89,7 +93,7 @@ namespace PluralKit.Bot public async Task SetLogCleanup(Context ctx) { - ctx.CheckGuildContext().CheckAuthorPermission(GuildPermission.ManageGuild, "Manage Server"); + ctx.CheckGuildContext().CheckAuthorPermission(Permissions.ManageGuild, "Manage Server"); var guildCfg = await _data.GetOrCreateGuildConfig(ctx.Guild.Id); var botList = string.Join(", ", _cleanService.Bots.Select(b => b.Name).OrderBy(x => x.ToLowerInvariant())); @@ -108,7 +112,7 @@ namespace PluralKit.Bot } else { - var eb = new EmbedBuilder() + var eb = new DiscordEmbedBuilder() .WithTitle("Log cleanup settings") .AddField("Supported bots", botList); diff --git a/PluralKit.Bot/Commands/Switch.cs b/PluralKit.Bot/Commands/Switch.cs index be5a6675..e650db19 100644 --- a/PluralKit.Bot/Commands/Switch.cs +++ b/PluralKit.Bot/Commands/Switch.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using Discord; +using DSharpPlus.Entities; using NodaTime; using NodaTime.TimeZones; @@ -140,7 +140,7 @@ namespace PluralKit.Bot var lastSwitchMemberStr = string.Join(", ", await lastSwitchMembers.Select(m => m.Name).ToListAsync()); var lastSwitchDeltaStr = DateTimeFormats.DurationFormat.Format(SystemClock.Instance.GetCurrentInstant() - lastTwoSwitches[0].Timestamp); - IUserMessage msg; + DiscordMessage msg; if (lastTwoSwitches.Count == 1) { msg = await ctx.Reply( diff --git a/PluralKit.Bot/Commands/System.cs b/PluralKit.Bot/Commands/System.cs index 3094c3b6..2fde9b66 100644 --- a/PluralKit.Bot/Commands/System.cs +++ b/PluralKit.Bot/Commands/System.cs @@ -18,9 +18,9 @@ namespace PluralKit.Bot public async Task Query(Context ctx, PKSystem system) { if (system == null) throw Errors.NoSystemError; - await ctx.Reply(embed: await _embeds.CreateSystemEmbed(system, ctx.LookupContextFor(system))); + await ctx.Reply(embed: await _embeds.CreateSystemEmbed(ctx.Shard, system, ctx.LookupContextFor(system))); } - + public async Task New(Context ctx) { ctx.CheckNoSystem(); diff --git a/PluralKit.Bot/Commands/SystemEdit.cs b/PluralKit.Bot/Commands/SystemEdit.cs index ef0970b6..454614a4 100644 --- a/PluralKit.Bot/Commands/SystemEdit.cs +++ b/PluralKit.Bot/Commands/SystemEdit.cs @@ -1,8 +1,9 @@ -using System; +using System; using System.Linq; using System.Threading.Tasks; -using Discord; +using DSharpPlus; +using DSharpPlus.Entities; using NodaTime; using NodaTime.Text; @@ -70,7 +71,7 @@ namespace PluralKit.Bot else if (ctx.MatchFlag("r", "raw")) await ctx.Reply($"```\n{ctx.System.Description.SanitizeMentions()}\n```"); else - await ctx.Reply(embed: new EmbedBuilder() + await ctx.Reply(embed: new DiscordEmbedBuilder() .WithTitle("System description") .WithDescription(ctx.System.Description) .WithFooter("To print the description with formatting, type `pk;s description -raw`. To clear it, type `pk;s description -clear`. To change it, type `pk;s description `.") @@ -128,7 +129,7 @@ namespace PluralKit.Bot { if ((ctx.System.AvatarUrl?.Trim() ?? "").Length > 0) { - var eb = new EmbedBuilder() + var eb = new DiscordEmbedBuilder() .WithTitle($"System avatar") .WithImageUrl(ctx.System.AvatarUrl) .WithDescription($"To clear, use `pk;system avatar clear`."); @@ -143,11 +144,11 @@ namespace PluralKit.Bot var member = await ctx.MatchUser(); if (member != null) { - if (member.AvatarId == null) throw Errors.UserHasNoAvatar; + if (member.AvatarUrl == member.DefaultAvatarUrl) throw Errors.UserHasNoAvatar; ctx.System.AvatarUrl = member.GetAvatarUrl(ImageFormat.Png, size: 256); await _data.SaveSystem(ctx.System); - var embed = new EmbedBuilder().WithImageUrl(ctx.System.AvatarUrl).Build(); + var embed = new DiscordEmbedBuilder().WithImageUrl(ctx.System.AvatarUrl).Build(); await ctx.Reply( $"{Emojis.Success} System avatar changed to {member.Username}'s avatar! {Emojis.Warn} Please note that if {member.Username} changes their avatar, the system's avatar will need to be re-set.", embed: embed); } @@ -160,7 +161,7 @@ namespace PluralKit.Bot ctx.System.AvatarUrl = url; await _data.SaveSystem(ctx.System); - var embed = url != null ? new EmbedBuilder().WithImageUrl(url).Build() : null; + var embed = url != null ? new DiscordEmbedBuilder().WithImageUrl(url).Build() : null; await ctx.Reply($"{Emojis.Success} System avatar changed.", embed: embed); } } @@ -249,7 +250,7 @@ namespace PluralKit.Bot _ => throw new ArgumentOutOfRangeException(nameof(level), level, null) }; - var eb = new EmbedBuilder() + var eb = new DiscordEmbedBuilder() .WithTitle("Current privacy settings for your system") .AddField("Description", PrivacyLevelString(ctx.System.DescriptionPrivacy)) .AddField("Member list", PrivacyLevelString(ctx.System.MemberListPrivacy)) diff --git a/PluralKit.Bot/Commands/SystemFront.cs b/PluralKit.Bot/Commands/SystemFront.cs index 726987d9..1abe995b 100644 --- a/PluralKit.Bot/Commands/SystemFront.cs +++ b/PluralKit.Bot/Commands/SystemFront.cs @@ -1,7 +1,8 @@ -using System.Linq; +using ArgumentException = System.ArgumentException; +using System.Linq; using System.Threading.Tasks; -using Discord; +using DSharpPlus.Entities; using NodaTime; @@ -88,9 +89,14 @@ namespace PluralKit.Bot stringToAdd = $"**{membersStr}** ({DateTimeFormats.ZonedDateTimeFormat.Format(sw.Timestamp.InZone(system.Zone))}, {DateTimeFormats.DurationFormat.Format(switchSince)} ago)\n"; } - - if (outputStr.Length + stringToAdd.Length > EmbedBuilder.MaxDescriptionLength) break; - outputStr += stringToAdd; + try // Unfortunately the only way to test DiscordEmbedBuilder.Description max length is this + { + builder.Description += stringToAdd; + } + catch (ArgumentException) + { + break; + }// TODO: Make sure this works } builder.Description = outputStr; diff --git a/PluralKit.Bot/Commands/SystemList.cs b/PluralKit.Bot/Commands/SystemList.cs index 39ea4074..34e50f2d 100644 --- a/PluralKit.Bot/Commands/SystemList.cs +++ b/PluralKit.Bot/Commands/SystemList.cs @@ -1,9 +1,9 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using Discord; +using DSharpPlus.Entities; using Humanizer; @@ -21,7 +21,7 @@ namespace PluralKit.Bot } private async Task RenderMemberList(Context ctx, PKSystem system, bool canShowPrivate, int membersPerPage, string embedTitle, Func filter, - Func, Task> + Func, Task> renderer) { var authCtx = ctx.LookupContextFor(system); @@ -54,7 +54,7 @@ namespace PluralKit.Bot }); } - private Task ShortRenderer(EmbedBuilder eb, IEnumerable members) + private Task ShortRenderer(DiscordEmbedBuilder eb, IEnumerable members) { eb.Description = string.Join("\n", members.Select((m) => { @@ -73,7 +73,7 @@ namespace PluralKit.Bot return Task.CompletedTask; } - private Task LongRenderer(EmbedBuilder eb, IEnumerable members) + private Task LongRenderer(DiscordEmbedBuilder eb, IEnumerable members) { foreach (var m in members) { diff --git a/PluralKit.Bot/Commands/Token.cs b/PluralKit.Bot/Commands/Token.cs index dbdfa057..d502b96e 100644 --- a/PluralKit.Bot/Commands/Token.cs +++ b/PluralKit.Bot/Commands/Token.cs @@ -1,6 +1,7 @@ using System.Threading.Tasks; -using Discord; +using DSharpPlus; +using DSharpPlus.Entities; using PluralKit.Core; @@ -22,14 +23,15 @@ namespace PluralKit.Bot var token = ctx.System.Token ?? await MakeAndSetNewToken(ctx.System); // If we're not already in a DM, reply with a reminder to check - if (!(ctx.Channel is IDMChannel)) + if (!(ctx.Channel is DiscordDmChannel)) { await ctx.Reply($"{Emojis.Success} Check your DMs!"); } // DM the user a security disclaimer, and then the token in a separate message (for easy copying on mobile) - await ctx.Author.SendMessageAsync($"{Emojis.Warn} Please note that this grants access to modify (and delete!) all your system data, so keep it safe and secure. If it leaks or you need a new one, you can invalidate this one with `pk;token refresh`.\n\nYour token is below:"); - await ctx.Author.SendMessageAsync(token); + var dm = await ctx.Rest.CreateDmAsync(ctx.Author.Id); + await dm.SendMessageAsync($"{Emojis.Warn} Please note that this grants access to modify (and delete!) all your system data, so keep it safe and secure. If it leaks or you need a new one, you can invalidate this one with `pk;token refresh`.\n\nYour token is below:"); + await dm.SendMessageAsync(token); } private async Task MakeAndSetNewToken(PKSystem system) @@ -55,14 +57,15 @@ namespace PluralKit.Bot var token = await MakeAndSetNewToken(ctx.System); // If we're not already in a DM, reply with a reminder to check - if (!(ctx.Channel is IDMChannel)) + if (!(ctx.Channel is DiscordDmChannel)) { await ctx.Reply($"{Emojis.Success} Check your DMs!"); } - + // DM the user an invalidation disclaimer, and then the token in a separate message (for easy copying on mobile) - await ctx.Author.SendMessageAsync($"{Emojis.Warn} Your previous API token has been invalidated. You will need to change it anywhere it's currently used.\n\nYour token is below:"); - await ctx.Author.SendMessageAsync(token); + var dm = await ctx.Rest.CreateDmAsync(ctx.Author.Id); + await dm.SendMessageAsync($"{Emojis.Warn} Your previous API token has been invalidated. You will need to change it anywhere it's currently used.\n\nYour token is below:"); + await dm.SendMessageAsync(token); } } } \ No newline at end of file diff --git a/PluralKit.Bot/Extensions.cs b/PluralKit.Bot/Extensions.cs new file mode 100644 index 00000000..b5434e5c --- /dev/null +++ b/PluralKit.Bot/Extensions.cs @@ -0,0 +1,23 @@ +using DSharpPlus; + +using System.Net.WebSockets; + +namespace PluralKit.Bot +{ + static class Extensions + { + //Unfortunately D#+ doesn't expose the connection state of the client, so we have to test for it instead + public static bool IsConnected(this DiscordClient client) + { + try + { + client.GetConnectionsAsync().GetAwaiter().GetResult(); + } + catch(WebSocketException) + { + return false; + } + return true; + } + } +} diff --git a/PluralKit.Bot/Modules.cs b/PluralKit.Bot/Modules.cs index 2581a7d0..051c2d78 100644 --- a/PluralKit.Bot/Modules.cs +++ b/PluralKit.Bot/Modules.cs @@ -15,14 +15,20 @@ namespace PluralKit.Bot { protected override void Load(ContainerBuilder builder) { - // Client + // Clients builder.Register(c => new DiscordShardedClient(new DiscordConfiguration { Token = c.Resolve().Token, TokenType = TokenType.Bot, MessageCacheSize = 0, })).AsSelf().SingleInstance(); - + builder.Register(c => new DiscordRestClient(new DiscordConfiguration + { + Token = c.Resolve().Token, + TokenType = TokenType.Bot, + MessageCacheSize = 0, + })).AsSelf().SingleInstance(); + // Commands builder.RegisterType().AsSelf(); builder.RegisterType().AsSelf(); diff --git a/PluralKit.Bot/PluralKit.Bot.csproj b/PluralKit.Bot/PluralKit.Bot.csproj index f56c7d05..cc6b2b1c 100644 --- a/PluralKit.Bot/PluralKit.Bot.csproj +++ b/PluralKit.Bot/PluralKit.Bot.csproj @@ -10,7 +10,8 @@ - + + diff --git a/PluralKit.Bot/Utils/MentionUtils.cs b/PluralKit.Bot/Utils/MentionUtils.cs new file mode 100644 index 00000000..cd6e9034 --- /dev/null +++ b/PluralKit.Bot/Utils/MentionUtils.cs @@ -0,0 +1,121 @@ +using System; +using System.Globalization; +using System.Text; + +namespace PluralKit.Bot.Utils +{ + /// + /// Provides a series of helper methods for parsing mentions. + /// + public static class MentionUtils + { + private const char SanitizeChar = '\x200b'; + + //If the system can't be positive a user doesn't have a nickname, assume useNickname = true (source: Jake) + internal static string MentionUser(string id, bool useNickname = true) => useNickname ? $"<@!{id}>" : $"<@{id}>"; + /// + /// Returns a mention string based on the user ID. + /// + /// + /// A user mention string (e.g. <@80351110224678912>). + /// + public static string MentionUser(ulong id) => MentionUser(id.ToString(), true); + internal static string MentionChannel(string id) => $"<#{id}>"; + /// + /// Returns a mention string based on the channel ID. + /// + /// + /// A channel mention string (e.g. <#103735883630395392>). + /// + public static string MentionChannel(ulong id) => MentionChannel(id.ToString()); + internal static string MentionRole(string id) => $"<@&{id}>"; + /// + /// Returns a mention string based on the role ID. + /// + /// + /// A role mention string (e.g. <@&165511591545143296>). + /// + public static string MentionRole(ulong id) => MentionRole(id.ToString()); + + /// + /// Parses a provided user mention string. + /// + /// Invalid mention format. + public static ulong ParseUser(string text) + { + if (TryParseUser(text, out ulong id)) + return id; + throw new ArgumentException(message: "Invalid mention format.", paramName: nameof(text)); + } + /// + /// Tries to parse a provided user mention string. + /// + public static bool TryParseUser(string text, out ulong userId) + { + if (text.Length >= 3 && text[0] == '<' && text[1] == '@' && text[text.Length - 1] == '>') + { + if (text.Length >= 4 && text[2] == '!') + text = text.Substring(3, text.Length - 4); //<@!123> + else + text = text.Substring(2, text.Length - 3); //<@123> + + if (ulong.TryParse(text, NumberStyles.None, CultureInfo.InvariantCulture, out userId)) + return true; + } + userId = 0; + return false; + } + + /// + /// Parses a provided channel mention string. + /// + /// Invalid mention format. + public static ulong ParseChannel(string text) + { + if (TryParseChannel(text, out ulong id)) + return id; + throw new ArgumentException(message: "Invalid mention format.", paramName: nameof(text)); + } + /// + /// Tries to parse a provided channel mention string. + /// + public static bool TryParseChannel(string text, out ulong channelId) + { + if (text.Length >= 3 && text[0] == '<' && text[1] == '#' && text[text.Length - 1] == '>') + { + text = text.Substring(2, text.Length - 3); //<#123> + + if (ulong.TryParse(text, NumberStyles.None, CultureInfo.InvariantCulture, out channelId)) + return true; + } + channelId = 0; + return false; + } + + /// + /// Parses a provided role mention string. + /// + /// Invalid mention format. + public static ulong ParseRole(string text) + { + if (TryParseRole(text, out ulong id)) + return id; + throw new ArgumentException(message: "Invalid mention format.", paramName: nameof(text)); + } + /// + /// Tries to parse a provided role mention string. + /// + public static bool TryParseRole(string text, out ulong roleId) + { + if (text.Length >= 4 && text[0] == '<' && text[1] == '@' && text[2] == '&' && text[text.Length - 1] == '>') + { + text = text.Substring(3, text.Length - 4); //<@&123> + + if (ulong.TryParse(text, NumberStyles.None, CultureInfo.InvariantCulture, out roleId)) + return true; + } + roleId = 0; + return false; + } + } +} \ No newline at end of file