using System; using System.Globalization; using System.Linq; using System.Net.Http; using System.Net.Sockets; using System.Text.RegularExpressions; using System.Threading.Tasks; using Discord; using Discord.Net; using PluralKit.Core; using Image = SixLabors.ImageSharp.Image; namespace PluralKit.Bot { public static class Utils { public static string NameAndMention(this IUser user) { return $"{user.Username}#{user.Discriminator} ({user.Mention})"; } public static Color? ToDiscordColor(this string color) { if (uint.TryParse(color, NumberStyles.HexNumber, null, out var colorInt)) return new Color(colorInt); throw new ArgumentException($"Invalid color string '{color}'."); } public static async Task VerifyAvatarOrThrow(string url) { // List of MIME types we consider acceptable var acceptableMimeTypes = new[] { "image/jpeg", "image/gif", "image/png" // TODO: add image/webp once ImageSharp supports this }; using (var client = new HttpClient()) { Uri uri; try { uri = new Uri(url); if (!uri.IsAbsoluteUri) throw Errors.InvalidUrl(url); } catch (UriFormatException) { throw Errors.InvalidUrl(url); } var response = await client.GetAsync(uri); if (!response.IsSuccessStatusCode) // Check status code throw Errors.AvatarServerError(response.StatusCode); if (response.Content.Headers.ContentLength == null) // Check presence of content length throw Errors.AvatarNotAnImage(null); if (response.Content.Headers.ContentLength > Limits.AvatarFileSizeLimit) // Check content length throw Errors.AvatarFileSizeLimit(response.Content.Headers.ContentLength.Value); if (!acceptableMimeTypes.Contains(response.Content.Headers.ContentType.MediaType)) // Check MIME type throw Errors.AvatarNotAnImage(response.Content.Headers.ContentType.MediaType); // Parse the image header in a worker var stream = await response.Content.ReadAsStreamAsync(); var image = await Task.Run(() => Image.Identify(stream)); if (image.Width > Limits.AvatarDimensionLimit || image.Height > Limits.AvatarDimensionLimit) // Check image size throw Errors.AvatarDimensionsTooLarge(image.Width, image.Height); } } public static bool HasMentionPrefix(string content, ref int argPos, out ulong mentionId) { mentionId = 0; // Roughly ported from Discord.Commands.MessageExtensions.HasMentionPrefix if (string.IsNullOrEmpty(content) || content.Length <= 3 || (content[0] != '<' || content[1] != '@')) return false; int num = content.IndexOf('>'); if (num == -1 || content.Length < num + 2 || content[num + 1] != ' ' || !MentionUtils.TryParseUser(content.Substring(0, num + 1), out mentionId)) return false; argPos = num + 2; return true; } public static bool TryParseMention(this string potentialMention, out ulong id) { if (ulong.TryParse(potentialMention, out id)) return true; if (MentionUtils.TryParseUser(potentialMention, out id)) return true; return false; } public static string SanitizeMentions(this string input) => Regex.Replace(Regex.Replace(input, "<@[!&]?(\\d{17,19})>", "<\u200B@$1>"), "@(everyone|here)", "@\u200B$1"); public static string SanitizeEveryone(this string input) => Regex.Replace(input, "@(everyone|here)", "@\u200B$1"); public static string EscapeMarkdown(this string input) { Regex pattern = new Regex(@"[*_~>`(||)\\]", RegexOptions.Multiline); if (input != null) return pattern.Replace(input, @"\$&"); else return input; } public static string ProxyTagsString(this PKMember member) => string.Join(", ", member.ProxyTags.Select(t => $"`{t.ProxyString.EscapeMarkdown()}`")); public static async Task PermissionsIn(this IChannel channel) { switch (channel) { case IDMChannel _: return ChannelPermissions.DM; case IGroupChannel _: return ChannelPermissions.Group; case IGuildChannel gc: var currentUser = await gc.Guild.GetCurrentUserAsync(); return currentUser.GetPermissions(gc); default: return ChannelPermissions.None; } } public static async Task HasPermission(this IChannel channel, ChannelPermission permission) => (await PermissionsIn(channel)).Has(permission); public static bool IsOurProblem(this Exception e) { // This function filters out sporadic errors out of our control from being reported to Sentry // otherwise we'd blow out our error reporting budget as soon as Discord takes a dump, or something. // Discord server errors are *not our problem* if (e is HttpException he && ((int) he.HttpCode) >= 500) return false; // Socket errors are *not our problem* if (e is SocketException) return false; // Tasks being cancelled for whatver reason are, you guessed it, also not our problem. if (e is TaskCanceledException) return false; // This may expanded at some point. return true; } } /// /// An exception class representing user-facing errors caused when parsing and executing commands. /// public class PKError : Exception { public PKError(string message) : base(message) { } } /// /// A subclass of that represent command syntax errors, meaning they'll have their command /// usages printed in the message. /// public class PKSyntaxError : PKError { public PKSyntaxError(string message) : base(message) { } } }