diff --git a/DisCatSharp/Entities/Guild/ThreadAndForum/ForumPostTag.cs b/DisCatSharp/Entities/Guild/ThreadAndForum/ForumPostTag.cs index 71a9ec84d..32cd7c1e8 100644 --- a/DisCatSharp/Entities/Guild/ThreadAndForum/ForumPostTag.cs +++ b/DisCatSharp/Entities/Guild/ThreadAndForum/ForumPostTag.cs @@ -1,181 +1,178 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021-2022 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using System; using System.Linq; using System.Threading.Tasks; using DisCatSharp.Net.Models; using Newtonsoft.Json; namespace DisCatSharp.Entities; /// /// Represents a discord forum post tag. /// -public class ForumPostTag : SnowflakeObject, IEquatable +public class ForumPostTag : NullableSnowflakeObject, IEquatable { - [JsonProperty("id", NullValueHandling = NullValueHandling.Ignore)] - public new ulong? Id; - /// /// Gets the channel id this tag belongs to. /// [JsonIgnore] internal ulong ChannelId; /// /// Gets the channel this tag belongs to. /// [JsonIgnore] internal DiscordChannel Channel; /// /// Gets the name of this forum post tag. /// [JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)] public string Name { get; internal set; } /// /// Gets the emoji id of the forum post tag. /// [JsonProperty("emoji_id", NullValueHandling = NullValueHandling.Ignore)] public ulong? EmojiId { get; internal set; } /// /// Gets the unicode emoji of the forum post tag. /// [JsonProperty("emoji_name", NullValueHandling = NullValueHandling.Include)] public string? UnicodeEmojiString { get; internal set; } /// /// Gets whether the tag can only be used by moderators. /// [JsonProperty("moderated", NullValueHandling = NullValueHandling.Ignore)] public bool? Moderated { get; internal set; } /// /// Gets the emoji. /// [JsonIgnore] public DiscordEmoji Emoji => this.UnicodeEmojiString != null ? DiscordEmoji.FromName(this.Discord, $":{this.UnicodeEmojiString}:", false) : DiscordEmoji.FromGuildEmote(this.Discord, this.EmojiId.Value); /// /// Initializes a new instance of the class. /// internal ForumPostTag() { } /// /// Initializes a new instance of the class. /// /// The tags name. /// The tags emoji id. Defaults to . /// The tags emoji name (unicode emoji). Defaults to . /// Whether this tag can only be applied by moderators. Defaults to . public ForumPostTag(string name, ulong? emojiId = null, string? emojiName = null, bool moderated = false) { this.Id = null; this.Name = name; this.EmojiId = emojiId; this.UnicodeEmojiString = emojiName; this.Moderated = moderated; } /// /// Modifies the tag. /// /// This method is currently not implemented. public async Task ModifyAsync(Action action) { var mdl = new ForumPostTagEditModel(); action(mdl); var res = await this.Discord.ApiClient.ModifyForumChannelAsync(this.ChannelId, null, null, null, null, null, null, this.Channel.InternalAvailableTags.Where(x => x.Id != this.Id).ToList().Append(new ForumPostTag() { Id = this.Id, Discord = this.Discord, ChannelId = this.ChannelId, Channel = this.Channel, EmojiId = mdl.Emoji.HasValue ? mdl.Emoji.Value.Id : this.EmojiId, Moderated = mdl.Moderated.HasValue ? mdl.Moderated.Value : this.Moderated, Name = mdl.Name.HasValue ? mdl.Name.Value : this.Name, UnicodeEmojiString = mdl.Emoji.HasValue ? mdl.Emoji.Value.Name : this.UnicodeEmojiString }).ToList(), null, null, null, null, null, null, null, mdl.AuditLogReason); return res.InternalAvailableTags.First(x => x.Id == this.Id); } /// /// Deletes the tag. /// /// This method is currently not implemented. public Task DeleteAsync(string reason = null) => this.Discord.ApiClient.ModifyForumChannelAsync(this.ChannelId, null, null, Optional.None, Optional.None, null, Optional.None, this.Channel.InternalAvailableTags.Where(x => x.Id != this.Id).ToList(), Optional.None, Optional.None, Optional.None, Optional.None, Optional.None, null, Optional.None, reason); /// /// Checks whether this is equal to another object. /// /// Object to compare to. /// Whether the object is equal to this . public override bool Equals(object obj) => this.Equals(obj as ForumPostTag); /// /// Checks whether this is equal to another . /// /// to compare to. /// Whether the is equal to this . public bool Equals(ForumPostTag e) => e is not null && (ReferenceEquals(this, e) || (this.Id == e.Id && this.Name == e.Name)); /// /// Gets the hash code for this . /// /// The hash code for this . public override int GetHashCode() => this.Id.GetHashCode(); /// /// Gets whether the two objects are equal. /// /// First forum post tag to compare. /// Second forum post tag to compare. /// Whether the two forum post tags are equal. public static bool operator ==(ForumPostTag e1, ForumPostTag e2) { var o1 = e1 as object; var o2 = e2 as object; return (o1 != null || o2 == null) && (o1 == null || o2 != null) && ((o1 == null && o2 == null) || e1.Id == e2.Id); } /// /// Gets whether the two objects are not equal. /// /// First forum post tag to compare. /// Second forum post tag to compare. /// Whether the two forum post tags are not equal. public static bool operator !=(ForumPostTag e1, ForumPostTag e2) => !(e1 == e2); } diff --git a/DisCatSharp/Entities/NullableSnowflakeObject.cs b/DisCatSharp/Entities/NullableSnowflakeObject.cs new file mode 100644 index 000000000..b06dfb006 --- /dev/null +++ b/DisCatSharp/Entities/NullableSnowflakeObject.cs @@ -0,0 +1,57 @@ +// This file is part of the DisCatSharp project, based off DSharpPlus. +// +// Copyright (c) 2021-2022 AITSYS +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +using System; + +using Newtonsoft.Json; + +namespace DisCatSharp.Entities; + +/// +/// Represents an object in Discord API. +/// +public abstract class NullableSnowflakeObject +{ + /// + /// Gets the ID of this object. + /// + [JsonProperty("id", NullValueHandling = NullValueHandling.Ignore)] + public ulong? Id { get; internal set; } + + /// + /// Gets the date and time this object was created. + /// + [JsonIgnore] + public DateTimeOffset? CreationTimestamp + => this.Id.GetSnowflakeTime(); + + /// + /// Gets the client instance this object is tied to. + /// + [JsonIgnore] + internal BaseDiscordClient Discord { get; set; } + + /// + /// Initializes a new instance of the class. + /// + internal NullableSnowflakeObject() { } +} diff --git a/DisCatSharp/Utilities.cs b/DisCatSharp/Utilities.cs index d4db0fcca..6f7859c22 100644 --- a/DisCatSharp/Utilities.cs +++ b/DisCatSharp/Utilities.cs @@ -1,470 +1,480 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021-2022 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using System; using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Text; using System.Text.RegularExpressions; using System.Threading.Tasks; using DisCatSharp.Entities; using DisCatSharp.Enums; using DisCatSharp.Net; using Microsoft.Extensions.Logging; namespace DisCatSharp; /// /// Various Discord-related utilities. /// public static class Utilities { /// /// Gets the version of the library /// internal static string VersionHeader { get; set; } /// /// Gets or sets the permission strings. /// internal static Dictionary PermissionStrings { get; set; } /// /// Gets the utf8 encoding /// // ReSharper disable once InconsistentNaming internal static UTF8Encoding UTF8 { get; } = new(false); /// /// Initializes a new instance of the class. /// static Utilities() { PermissionStrings = new Dictionary(); var t = typeof(Permissions); var ti = t.GetTypeInfo(); var vals = Enum.GetValues(t).Cast(); foreach (var xv in vals) { var xsv = xv.ToString(); var xmv = ti.DeclaredMembers.FirstOrDefault(xm => xm.Name == xsv); var xav = xmv.GetCustomAttribute(); PermissionStrings[xv] = xav.String; } var a = typeof(DiscordClient).GetTypeInfo().Assembly; var vs = ""; var iv = a.GetCustomAttribute(); if (iv != null) vs = iv.InformationalVersion; else { var v = a.GetName().Version; vs = v.ToString(3); } VersionHeader = $"DiscordBot (https://github.com/Aiko-IT-Systems/DisCatSharp, v{vs})"; } /// /// Gets the api base uri. /// /// The config /// A string. internal static string GetApiBaseUri(DiscordConfiguration config = null) => config == null ? Endpoints.BASE_URI + "9" : config.UseCanary ? Endpoints.CANARY_URI + config.ApiVersion : Endpoints.BASE_URI + config.ApiVersion; /// /// Gets the api uri for. /// /// The path. /// The config /// An Uri. internal static Uri GetApiUriFor(string path, DiscordConfiguration config) => new($"{GetApiBaseUri(config)}{path}"); /// /// Gets the api uri for. /// /// The path. /// The query string. /// The config /// An Uri. internal static Uri GetApiUriFor(string path, string queryString, DiscordConfiguration config) => new($"{GetApiBaseUri(config)}{path}{queryString}"); /// /// Gets the api uri builder for. /// /// The path. /// The config /// A QueryUriBuilder. internal static QueryUriBuilder GetApiUriBuilderFor(string path, DiscordConfiguration config) => new($"{GetApiBaseUri(config)}{path}"); /// /// Gets the formatted token. /// /// The client. /// A string. internal static string GetFormattedToken(BaseDiscordClient client) => GetFormattedToken(client.Configuration); /// /// Gets the formatted token. /// /// The config. /// A string. internal static string GetFormattedToken(DiscordConfiguration config) => config.TokenType switch { TokenType.Bearer => $"Bearer {config.Token}", TokenType.Bot => $"Bot {config.Token}", _ => throw new ArgumentException("Invalid token type specified.", nameof(config)), }; /// /// Gets the base headers. /// /// A Dictionary. internal static Dictionary GetBaseHeaders() => new(); /// /// Gets the user agent. /// /// A string. internal static string GetUserAgent() => VersionHeader; /// /// Contains the user mentions. /// /// The message. /// A bool. internal static bool ContainsUserMentions(string message) { var pattern = @"<@(\d+)>"; var regex = new Regex(pattern, RegexOptions.ECMAScript); return regex.IsMatch(message); } /// /// Contains the nickname mentions. /// /// The message. /// A bool. internal static bool ContainsNicknameMentions(string message) { var pattern = @"<@!(\d+)>"; var regex = new Regex(pattern, RegexOptions.ECMAScript); return regex.IsMatch(message); } /// /// Contains the channel mentions. /// /// The message. /// A bool. internal static bool ContainsChannelMentions(string message) { var pattern = @"<#(\d+)>"; var regex = new Regex(pattern, RegexOptions.ECMAScript); return regex.IsMatch(message); } /// /// Contains the role mentions. /// /// The message. /// A bool. internal static bool ContainsRoleMentions(string message) { var pattern = @"<@&(\d+)>"; var regex = new Regex(pattern, RegexOptions.ECMAScript); return regex.IsMatch(message); } /// /// Contains the emojis. /// /// The message. /// A bool. internal static bool ContainsEmojis(string message) { var pattern = @""; var regex = new Regex(pattern, RegexOptions.ECMAScript); return regex.IsMatch(message); } /// /// Gets the user mentions. /// /// The message. /// A list of ulong. internal static IEnumerable GetUserMentions(DiscordMessage message) { var regex = new Regex(@"<@!?(\d+)>", RegexOptions.ECMAScript); var matches = regex.Matches(message.Content); return from Match match in matches select ulong.Parse(match.Groups[1].Value, CultureInfo.InvariantCulture); } /// /// Gets the role mentions. /// /// The message. /// A list of ulong. internal static IEnumerable GetRoleMentions(DiscordMessage message) { var regex = new Regex(@"<@&(\d+)>", RegexOptions.ECMAScript); var matches = regex.Matches(message.Content); return from Match match in matches select ulong.Parse(match.Groups[1].Value, CultureInfo.InvariantCulture); } /// /// Gets the channel mentions. /// /// The message. /// A list of ulong. internal static IEnumerable GetChannelMentions(DiscordMessage message) { var regex = new Regex(@"<#(\d+)>", RegexOptions.ECMAScript); var matches = regex.Matches(message.Content); return from Match match in matches select ulong.Parse(match.Groups[1].Value, CultureInfo.InvariantCulture); } /// /// Gets the emojis. /// /// The message. /// A list of ulong. internal static IEnumerable GetEmojis(DiscordMessage message) { var regex = new Regex(@"", RegexOptions.ECMAScript); var matches = regex.Matches(message.Content); return from Match match in matches select ulong.Parse(match.Groups[2].Value, CultureInfo.InvariantCulture); } /// /// Are the valid slash command name. /// /// The name. /// A bool. internal static bool IsValidSlashCommandName(string name) { var regex = new Regex(@"^[\w-]{1,32}$"); return regex.IsMatch(name); } /// /// Checks the thread auto archive duration feature. /// /// The guild. /// The taad. /// A bool. internal static bool CheckThreadAutoArchiveDurationFeature(DiscordGuild guild, ThreadAutoArchiveDuration taad) => true; /// /// Checks the thread private feature. /// /// The guild. /// A bool. internal static bool CheckThreadPrivateFeature(DiscordGuild guild) => guild.PremiumTier.HasFlag(PremiumTier.TierTwo) || guild.Features.HasFeature(GuildFeaturesEnum.CanCreatePrivateThreads); /// /// Have the message intents. /// /// The intents. /// A bool. internal static bool HasMessageIntents(DiscordIntents intents) => intents.HasIntent(DiscordIntents.GuildMessages) || intents.HasIntent(DiscordIntents.DirectMessages); /// /// Have the message intents. /// /// The intents. /// A bool. internal static bool HasMessageContentIntents(DiscordIntents intents) => intents.HasIntent(DiscordIntents.MessageContent); /// /// Have the reaction intents. /// /// The intents. /// A bool. internal static bool HasReactionIntents(DiscordIntents intents) => intents.HasIntent(DiscordIntents.GuildMessageReactions) || intents.HasIntent(DiscordIntents.DirectMessageReactions); /// /// Have the typing intents. /// /// The intents. /// A bool. internal static bool HasTypingIntents(DiscordIntents intents) => intents.HasIntent(DiscordIntents.GuildMessageTyping) || intents.HasIntent(DiscordIntents.DirectMessageTyping); // https://discord.com/developers/docs/topics/gateway#sharding-sharding-formula /// /// Gets a shard id from a guild id and total shard count. /// /// The guild id the shard is on. /// The total amount of shards. /// The shard id. public static int GetShardId(ulong guildId, int shardCount) => (int)(guildId >> 22) % shardCount; /// /// Helper method to create a from Unix time seconds for targets that do not support this natively. /// /// Unix time seconds to convert. /// Whether the method should throw on failure. Defaults to true. /// Calculated . public static DateTimeOffset GetDateTimeOffset(long unixTime, bool shouldThrow = true) { try { return DateTimeOffset.FromUnixTimeSeconds(unixTime); } catch (Exception) { if (shouldThrow) throw; return DateTimeOffset.MinValue; } } /// /// Helper method to create a from Unix time milliseconds for targets that do not support this natively. /// /// Unix time milliseconds to convert. /// Whether the method should throw on failure. Defaults to true. /// Calculated . public static DateTimeOffset GetDateTimeOffsetFromMilliseconds(long unixTime, bool shouldThrow = true) { try { return DateTimeOffset.FromUnixTimeMilliseconds(unixTime); } catch (Exception) { if (shouldThrow) throw; return DateTimeOffset.MinValue; } } /// /// Helper method to calculate Unix time seconds from a for targets that do not support this natively. /// /// to calculate Unix time for. /// Calculated Unix time. public static long GetUnixTime(DateTimeOffset dto) => dto.ToUnixTimeMilliseconds(); /// /// Computes a timestamp from a given snowflake. /// /// Snowflake to compute a timestamp from. /// Computed timestamp. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static DateTimeOffset GetSnowflakeTime(this ulong snowflake) => DiscordClient.DiscordEpoch.AddMilliseconds(snowflake >> 22); + /// + /// Computes a timestamp from a given snowflake. + /// + /// Snowflake to compute a timestamp from. + /// Computed timestamp. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static DateTimeOffset? GetSnowflakeTime(this ulong? snowflake) + => snowflake != null && snowflake.HasValue ? DiscordClient.DiscordEpoch.AddMilliseconds(snowflake.Value >> 22) : null; + + /// /// Converts this into human-readable format. /// /// Permissions enumeration to convert. /// Human-readable permissions. public static string ToPermissionString(this Permissions perm) { if (perm == Permissions.None) return PermissionStrings[perm]; perm &= PermissionMethods.FullPerms; var strs = PermissionStrings .Where(xkvp => xkvp.Key != Permissions.None && (perm & xkvp.Key) == xkvp.Key) .Select(xkvp => xkvp.Value); return string.Join(", ", strs.OrderBy(xs => xs)); } /// /// Checks whether this string contains given characters. /// /// String to check. /// Characters to check for. /// Whether the string contained these characters. public static bool Contains(this string str, params char[] characters) { foreach (var xc in str) if (characters.Contains(xc)) return true; return false; } /// /// Logs the task fault. /// /// The task. /// The logger. /// The level. /// The event id. /// The message. internal static void LogTaskFault(this Task task, ILogger logger, LogLevel level, EventId eventId, string message) { if (task == null) throw new ArgumentNullException(nameof(task)); if (logger == null) return; task.ContinueWith(t => logger.Log(level, eventId, t.Exception, message), TaskContinuationOptions.OnlyOnFaulted); } /// /// Deconstructs the. /// /// The kvp. /// The key. /// The value. internal static void Deconstruct(this KeyValuePair kvp, out TKey key, out TValue value) { key = kvp.Key; value = kvp.Value; } }