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;
}
}