diff --git a/DisCatSharp.CommandsNext/Attributes/CooldownAttribute.cs b/DisCatSharp.CommandsNext/Attributes/CooldownAttribute.cs
index a3d6965b6..a088a865f 100644
--- a/DisCatSharp.CommandsNext/Attributes/CooldownAttribute.cs
+++ b/DisCatSharp.CommandsNext/Attributes/CooldownAttribute.cs
@@ -1,342 +1,333 @@
// This file is part of the DisCatSharp project.
//
// Copyright (c) 2021 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.Concurrent;
using System.Globalization;
using System.Threading;
using System.Threading.Tasks;
namespace DisCatSharp.CommandsNext.Attributes
{
///
/// Defines a cooldown for this command. This allows you to define how many times can users execute a specific command
///
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = true, Inherited = false)]
public sealed class CooldownAttribute : CheckBaseAttribute
{
///
/// Gets the maximum number of uses before this command triggers a cooldown for its bucket.
///
public int MaxUses { get; }
///
/// Gets the time after which the cooldown is reset.
///
public TimeSpan Reset { get; }
///
/// Gets the type of the cooldown bucket. This determines how cooldowns are applied.
///
public CooldownBucketType BucketType { get; }
///
/// Gets the cooldown buckets for this command.
///
private ConcurrentDictionary Buckets { get; }
///
/// Defines a cooldown for this command. This means that users will be able to use the command a specific number of times before they have to wait to use it again.
///
/// Number of times the command can be used before triggering a cooldown.
/// Number of seconds after which the cooldown is reset.
/// Type of cooldown bucket. This allows controlling whether the bucket will be cooled down per user, guild, channel, or globally.
public CooldownAttribute(int maxUses, double resetAfter, CooldownBucketType bucketType)
{
this.MaxUses = maxUses;
this.Reset = TimeSpan.FromSeconds(resetAfter);
this.BucketType = bucketType;
this.Buckets = new ConcurrentDictionary();
}
///
/// Gets a cooldown bucket for given command context.
///
/// Command context to get cooldown bucket for.
/// Requested cooldown bucket, or null if one wasn't present.
public CommandCooldownBucket GetBucket(CommandContext ctx)
{
var bid = this.GetBucketId(ctx, out _, out _, out _);
this.Buckets.TryGetValue(bid, out var bucket);
return bucket;
}
///
/// Calculates the cooldown remaining for given command context.
///
/// Context for which to calculate the cooldown.
/// Remaining cooldown, or zero if no cooldown is active.
public TimeSpan GetRemainingCooldown(CommandContext ctx)
{
var bucket = this.GetBucket(ctx);
return bucket == null ? TimeSpan.Zero : bucket.RemainingUses > 0 ? TimeSpan.Zero : bucket.ResetsAt - DateTimeOffset.UtcNow;
}
///
/// Calculates bucket ID for given command context.
///
/// Context for which to calculate bucket ID for.
/// ID of the user with which this bucket is associated.
/// ID of the channel with which this bucket is associated.
/// ID of the guild with which this bucket is associated.
/// Calculated bucket ID.
private string GetBucketId(CommandContext ctx, out ulong userId, out ulong channelId, out ulong guildId)
{
userId = 0ul;
if ((this.BucketType & CooldownBucketType.User) != 0)
userId = ctx.User.Id;
channelId = 0ul;
if ((this.BucketType & CooldownBucketType.Channel) != 0)
channelId = ctx.Channel.Id;
if ((this.BucketType & CooldownBucketType.Guild) != 0 && ctx.Guild == null)
channelId = ctx.Channel.Id;
guildId = 0ul;
if (ctx.Guild != null && (this.BucketType & CooldownBucketType.Guild) != 0)
guildId = ctx.Guild.Id;
var bid = CommandCooldownBucket.MakeId(userId, channelId, guildId);
return bid;
}
///
/// Executes a check.
///
/// The command context.
/// If true, help - returns true.
public override async Task ExecuteCheckAsync(CommandContext ctx, bool help)
{
if (help)
return true;
var bid = this.GetBucketId(ctx, out var usr, out var chn, out var gld);
if (!this.Buckets.TryGetValue(bid, out var bucket))
{
bucket = new CommandCooldownBucket(this.MaxUses, this.Reset, usr, chn, gld);
this.Buckets.AddOrUpdate(bid, bucket, (k, v) => bucket);
}
return await bucket.DecrementUseAsync().ConfigureAwait(false);
}
}
///
/// Defines how are command cooldowns applied.
///
public enum CooldownBucketType : int
{
///
/// Denotes that the command will have its cooldown applied per-user.
///
User = 1,
///
/// Denotes that the command will have its cooldown applied per-channel.
///
Channel = 2,
///
/// Denotes that the command will have its cooldown applied per-guild. In DMs, this applies the cooldown per-channel.
///
Guild = 4,
///
/// Denotes that the command will have its cooldown applied globally.
///
Global = 0
}
///
/// Represents a cooldown bucket for commands.
///
public sealed class CommandCooldownBucket : IEquatable
{
///
/// Gets the ID of the user with whom this cooldown is associated.
///
public ulong UserId { get; }
///
/// Gets the ID of the channel with which this cooldown is associated.
///
public ulong ChannelId { get; }
///
/// Gets the ID of the guild with which this cooldown is associated.
///
public ulong GuildId { get; }
///
/// Gets the ID of the bucket. This is used to distinguish between cooldown buckets.
///
public string BucketId { get; }
///
/// Gets the remaining number of uses before the cooldown is triggered.
///
public int RemainingUses
=> Volatile.Read(ref this._remaining_uses);
private int _remaining_uses;
///
/// Gets the maximum number of times this command can be used in given timespan.
///
public int MaxUses { get; }
///
/// Gets the date and time at which the cooldown resets.
///
public DateTimeOffset ResetsAt { get; internal set; }
///
/// Gets the time after which this cooldown resets.
///
public TimeSpan Reset { get; internal set; }
///
/// Gets the semaphore used to lock the use value.
///
private SemaphoreSlim UsageSemaphore { get; }
///
/// Creates a new command cooldown bucket.
///
/// Maximum number of uses for this bucket.
/// Time after which this bucket resets.
/// ID of the user with which this cooldown is associated.
/// ID of the channel with which this cooldown is associated.
/// ID of the guild with which this cooldown is associated.
internal CommandCooldownBucket(int maxUses, TimeSpan resetAfter, ulong userId = 0, ulong channelId = 0, ulong guildId = 0)
{
this._remaining_uses = maxUses;
this.MaxUses = maxUses;
this.ResetsAt = DateTimeOffset.UtcNow + resetAfter;
this.Reset = resetAfter;
this.UserId = userId;
this.ChannelId = channelId;
this.GuildId = guildId;
this.BucketId = MakeId(userId, channelId, guildId);
this.UsageSemaphore = new SemaphoreSlim(1, 1);
}
///
/// Decrements the remaining use counter.
///
/// Whether decrement succeded or not.
internal async Task DecrementUseAsync()
{
await this.UsageSemaphore.WaitAsync().ConfigureAwait(false);
// if we're past reset time...
var now = DateTimeOffset.UtcNow;
if (now >= this.ResetsAt)
{
// ...do the reset and set a new reset time
Interlocked.Exchange(ref this._remaining_uses, this.MaxUses);
this.ResetsAt = now + this.Reset;
}
// check if we have any uses left, if we do...
var success = false;
if (this.RemainingUses > 0)
{
// ...decrement, and return success...
Interlocked.Decrement(ref this._remaining_uses);
success = true;
}
// ...otherwise just fail
this.UsageSemaphore.Release();
return success;
}
///
/// Returns a string representation of this command cooldown bucket.
///
/// String representation of this command cooldown bucket.
public override string ToString() => $"Command bucket {this.BucketId}";
///
/// 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 CommandCooldownBucket);
///
/// Checks whether this is equal to another .
///
/// to compare to.
/// Whether the is equal to this .
public bool Equals(CommandCooldownBucket other) => other is not null && (ReferenceEquals(this, other) || (this.UserId == other.UserId && this.ChannelId == other.ChannelId && this.GuildId == other.GuildId));
///
/// Gets the hash code for this .
///
/// The hash code for this .
- public override int GetHashCode()
- {
- var hash = 13;
-
- hash = (hash * 7) + this.UserId.GetHashCode();
- hash = (hash * 7) + this.ChannelId.GetHashCode();
- hash = (hash * 7) + this.GuildId.GetHashCode();
-
- return hash;
- }
+ public override int GetHashCode() => HashCode.Combine(this.UserId, this.ChannelId, this.GuildId);
///
/// Gets whether the two objects are equal.
///
/// First bucket to compare.
/// Second bucket to compare.
/// Whether the two buckets are equal.
public static bool operator ==(CommandCooldownBucket bucket1, CommandCooldownBucket bucket2)
{
var null1 = bucket1 is null;
var null2 = bucket2 is null;
return (null1 && null2) || (null1 == null2 && null1.Equals(null2));
}
///
/// Gets whether the two objects are not equal.
///
/// First bucket to compare.
/// Second bucket to compare.
/// Whether the two buckets are not equal.
public static bool operator !=(CommandCooldownBucket bucket1, CommandCooldownBucket bucket2)
=> !(bucket1 == bucket2);
///
/// Creates a bucket ID from given bucket parameters.
///
/// ID of the user with which this cooldown is associated.
/// ID of the channel with which this cooldown is associated.
/// ID of the guild with which this cooldown is associated.
/// Generated bucket ID.
public static string MakeId(ulong userId = 0, ulong channelId = 0, ulong guildId = 0)
=> $"{userId.ToString(CultureInfo.InvariantCulture)}:{channelId.ToString(CultureInfo.InvariantCulture)}:{guildId.ToString(CultureInfo.InvariantCulture)}";
}
}
diff --git a/DisCatSharp.Hosting.Tests/GlobalSuppressions.cs b/DisCatSharp.Hosting.Tests/GlobalSuppressions.cs
index b0f832543..25642d608 100644
--- a/DisCatSharp.Hosting.Tests/GlobalSuppressions.cs
+++ b/DisCatSharp.Hosting.Tests/GlobalSuppressions.cs
@@ -1,20 +1,26 @@
// This file is used by Code Analysis to maintain SuppressMessage
// attributes that are applied to this project.
// Project-level suppressions either have no target or are given
// a specific target and scoped to a namespace, type, member, etc.
using System.Diagnostics.CodeAnalysis;
[assembly: SuppressMessage("DocumentationHeader", "ClassDocumentationHeader:The class must have a documentation header.", Justification = "")]
[assembly: SuppressMessage("DocumentationHeader", "MethodDocumentationHeader:The method must have a documentation header.", Justification = "")]
[assembly: SuppressMessage("Style", "IDE0044:Add readonly modifier", Justification = "", Scope = "member", Target = "~F:DisCatSharp.Hosting.Tests.HostExtensionTests._discordConfig")]
[assembly: SuppressMessage("CodeQuality", "IDE0051:Remove unused private members", Justification = "", Scope = "member", Target = "~F:DisCatSharp.Hosting.Tests.HostExtensionTests._discordConfig")]
[assembly: SuppressMessage("Style", "IDE0044:Add readonly modifier", Justification = "", Scope = "member", Target = "~F:DisCatSharp.Hosting.Tests.HostExtensionTests._interactivityConfig")]
[assembly: SuppressMessage("CodeQuality", "IDE0051:Remove unused private members", Justification = "", Scope = "member", Target = "~F:DisCatSharp.Hosting.Tests.HostExtensionTests._interactivityConfig")]
[assembly: SuppressMessage("CodeQuality", "IDE0051:Remove unused private members", Justification = "", Scope = "member", Target = "~F:DisCatSharp.Hosting.Tests.HostExtensionTests._lavalinkConfig")]
[assembly: SuppressMessage("Style", "IDE0044:Add readonly modifier", Justification = "", Scope = "member", Target = "~F:DisCatSharp.Hosting.Tests.HostExtensionTests._lavalinkConfig")]
[assembly: SuppressMessage("DocumentationHeader", "ConstructorDocumentationHeader:The constructor must have a documentation header.", Justification = "", Scope = "member", Target = "~M:DisCatSharp.Hosting.Tests.Bot.#ctor(Microsoft.Extensions.Configuration.IConfiguration,Microsoft.Extensions.Logging.ILogger{DisCatSharp.Hosting.DiscordHostedService},System.IServiceProvider)")]
[assembly: SuppressMessage("Performance", "CA1822:Mark members as static", Justification = "", Scope = "member", Target = "~M:DisCatSharp.Hosting.Tests.HostExtensionTests.DefaultDiscord~System.Collections.Generic.Dictionary{System.String,System.String}")]
[assembly: SuppressMessage("Performance", "CA1822:Mark members as static", Justification = "", Scope = "member", Target = "~M:DisCatSharp.Hosting.Tests.HostExtensionTests.DiscordInteractivityAndLavaLinkConfiguration~Microsoft.Extensions.Configuration.IConfiguration")]
[assembly: SuppressMessage("Performance", "CA1822:Mark members as static", Justification = "", Scope = "member", Target = "~M:DisCatSharp.Hosting.Tests.HostTests.Create(System.Collections.Generic.Dictionary{System.String,System.String})~Microsoft.Extensions.Hosting.IHostBuilder")]
[assembly: SuppressMessage("Performance", "CA1822:Mark members as static", Justification = "", Scope = "member", Target = "~M:DisCatSharp.Hosting.Tests.HostTests.DefaultDiscord~System.Collections.Generic.Dictionary{System.String,System.String}")]
+[assembly: SuppressMessage("DocumentationHeader", "ConstructorDocumentationHeader:The constructor must have a documentation header.", Justification = "", Scope = "member", Target = "~M:DisCatSharp.Hosting.Tests.Bot.#ctor(Microsoft.Extensions.Configuration.IConfiguration,Microsoft.Extensions.Logging.ILogger{DisCatSharp.Hosting.Tests.Bot},System.IServiceProvider)")]
+[assembly: SuppressMessage("DocumentationHeader", "ConstructorDocumentationHeader:The constructor must have a documentation header.", Justification = "", Scope = "member", Target = "~M:DisCatSharp.Hosting.Tests.BotTwoService.#ctor(Microsoft.Extensions.Configuration.IConfiguration,Microsoft.Extensions.Logging.ILogger{DisCatSharp.Hosting.Tests.BotTwoService},System.IServiceProvider)")]
+[assembly: SuppressMessage("DocumentationHeader", "ConstructorDocumentationHeader:The constructor must have a documentation header.", Justification = "", Scope = "member", Target = "~M:DisCatSharp.Hosting.Tests.MyCustomBot.#ctor(Microsoft.Extensions.Configuration.IConfiguration,Microsoft.Extensions.Logging.ILogger{DisCatSharp.Hosting.Tests.MyCustomBot},System.IServiceProvider)")]
+[assembly: SuppressMessage("DocumentationHeader", "InterfaceDocumentationHeader:The interface must have a documentation header.", Justification = "", Scope = "type", Target = "~T:DisCatSharp.Hosting.Tests.IBotTwoService")]
+[assembly: SuppressMessage("Performance", "CA1822:Mark members as static", Justification = "", Scope = "member", Target = "~M:DisCatSharp.Hosting.Tests.HostTests.Create(System.String)~Microsoft.Extensions.Hosting.IHostBuilder")]
+[assembly: SuppressMessage("Performance", "CA1822:Mark members as static", Justification = "", Scope = "member", Target = "~M:DisCatSharp.Hosting.Tests.HostTests.Create``2(System.String)~Microsoft.Extensions.Hosting.IHostBuilder")]
diff --git a/DisCatSharp/Entities/Channel/DiscordChannel.cs b/DisCatSharp/Entities/Channel/DiscordChannel.cs
index ceb6bb3e7..94c29b996 100644
--- a/DisCatSharp/Entities/Channel/DiscordChannel.cs
+++ b/DisCatSharp/Entities/Channel/DiscordChannel.cs
@@ -1,1339 +1,1356 @@
// This file is part of the DisCatSharp project.
//
// Copyright (c) 2021 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.Collections.ObjectModel;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using DisCatSharp.Exceptions;
using DisCatSharp.Net.Abstractions;
using DisCatSharp.Net.Models;
using Newtonsoft.Json;
namespace DisCatSharp.Entities
{
///
/// Represents a discord channel.
///
public class DiscordChannel : SnowflakeObject, IEquatable
{
///
/// Gets ID of the guild to which this channel belongs.
///
[JsonProperty("guild_id", NullValueHandling = NullValueHandling.Ignore)]
public ulong? GuildId { get; internal set; }
///
/// Gets ID of the category that contains this channel.
///
[JsonProperty("parent_id", NullValueHandling = NullValueHandling.Include)]
public ulong? ParentId { get; internal set; }
///
/// Gets the category that contains this channel.
///
[JsonIgnore]
public DiscordChannel Parent
=> this.ParentId.HasValue ? this.Guild.GetChannel(this.ParentId.Value) : null;
///
/// Gets the name of this channel.
///
[JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)]
public string Name { get; internal set; }
///
/// Gets the type of this channel.
///
[JsonProperty("type", NullValueHandling = NullValueHandling.Ignore)]
public ChannelType Type { get; internal set; }
///
/// Gets the position of this channel.
///
[JsonProperty("position", NullValueHandling = NullValueHandling.Ignore)]
public int Position { get; internal set; }
///
/// Gets the maximum available position to move the channel to.
/// This can contain outdated informations.
///
public int GetMaxPosition()
{
var channels = this.Guild.Channels.Values;
return this.ParentId != null
? this.Type == ChannelType.Text || this.Type == ChannelType.News
? channels.Where(xc => xc.ParentId == this.ParentId && (xc.Type == ChannelType.Text || xc.Type == ChannelType.News)).OrderBy(xc => xc.Position).ToArray().Last().Position
: this.Type == ChannelType.Voice || this.Type == ChannelType.Stage
? channels.Where(xc => xc.ParentId == this.ParentId && (xc.Type == ChannelType.Voice || xc.Type == ChannelType.Stage)).OrderBy(xc => xc.Position).ToArray().Last().Position
: channels.Where(xc => xc.ParentId == this.ParentId && xc.Type == this.Type).OrderBy(xc => xc.Position).ToArray().Last().Position
: channels.Where(xc => xc.ParentId == null && xc.Type == this.Type).OrderBy(xc => xc.Position).ToArray().Last().Position;
}
///
/// Gets the minimum available position to move the channel to.
///
public int GetMinPosition()
{
var channels = this.Guild.Channels.Values;
return this.ParentId != null
? this.Type == ChannelType.Text || this.Type == ChannelType.News
? channels.Where(xc => xc.ParentId == this.ParentId && (xc.Type == ChannelType.Text || xc.Type == ChannelType.News)).OrderBy(xc => xc.Position).ToArray().First().Position
: this.Type == ChannelType.Voice || this.Type == ChannelType.Stage
? channels.Where(xc => xc.ParentId == this.ParentId && (xc.Type == ChannelType.Voice || xc.Type == ChannelType.Stage)).OrderBy(xc => xc.Position).ToArray().First().Position
: channels.Where(xc => xc.ParentId == this.ParentId && xc.Type == this.Type).OrderBy(xc => xc.Position).ToArray().First().Position
: channels.Where(xc => xc.ParentId == null && xc.Type == this.Type).OrderBy(xc => xc.Position).ToArray().First().Position;
}
///
/// Gets whether this channel is a DM channel.
///
[JsonIgnore]
public bool IsPrivate
=> this.Type == ChannelType.Private || this.Type == ChannelType.Group;
///
/// Gets whether this channel is a channel category.
///
[JsonIgnore]
public bool IsCategory
=> this.Type == ChannelType.Category;
///
/// Gets whether this channel is a stage channel.
///
[JsonIgnore]
public bool IsStage
=> this.Type == ChannelType.Stage;
///
/// Gets the guild to which this channel belongs.
///
[JsonIgnore]
public DiscordGuild Guild
=> this.GuildId.HasValue && this.Discord.Guilds.TryGetValue(this.GuildId.Value, out var guild) ? guild : null;
///
/// Gets a collection of permission overwrites for this channel.
///
[JsonIgnore]
public IReadOnlyList PermissionOverwrites
=> this._permissionOverwritesLazy.Value;
[JsonProperty("permission_overwrites", NullValueHandling = NullValueHandling.Ignore)]
internal List _permissionOverwrites = new();
[JsonIgnore]
private readonly Lazy> _permissionOverwritesLazy;
///
/// Gets the channel's topic. This is applicable to text channels only.
///
[JsonProperty("topic", NullValueHandling = NullValueHandling.Ignore)]
public string Topic { get; internal set; }
///
/// Gets the ID of the last message sent in this channel. This is applicable to text channels only.
///
[JsonProperty("last_message_id", NullValueHandling = NullValueHandling.Ignore)]
public ulong? LastMessageId { get; internal set; }
///
/// Gets this channel's bitrate. This is applicable to voice channels only.
///
[JsonProperty("bitrate", NullValueHandling = NullValueHandling.Ignore)]
public int? Bitrate { get; internal set; }
///
/// Gets this channel's user limit. This is applicable to voice channels only.
///
[JsonProperty("user_limit", NullValueHandling = NullValueHandling.Ignore)]
public int? UserLimit { get; internal set; }
///
/// Gets the slow mode delay configured for this channel.
/// All bots, as well as users with or permissions in the channel are exempt from slow mode.
///
[JsonProperty("rate_limit_per_user")]
public int? PerUserRateLimit { get; internal set; }
///
/// Gets this channel's video quality mode. This is applicable to voice channels only.
///
[JsonProperty("video_quality_mode", NullValueHandling = NullValueHandling.Ignore)]
public VideoQualityMode? QualityMode { get; internal set; }
///
/// Gets when the last pinned message was pinned.
///
[JsonIgnore]
public DateTimeOffset? LastPinTimestamp
=> !string.IsNullOrWhiteSpace(this.LastPinTimestampRaw) && DateTimeOffset.TryParse(this.LastPinTimestampRaw, CultureInfo.InvariantCulture, DateTimeStyles.None, out var dto) ?
dto : null;
///
/// Gets when the last pinned message was pinned as raw string.
///
[JsonProperty("last_pin_timestamp", NullValueHandling = NullValueHandling.Ignore)]
internal string LastPinTimestampRaw { get; set; }
///
/// Gets this channel's default duration for newly created threads, in minutes, to automatically archive the thread after recent activity.
///
[JsonProperty("default_auto_archive_duration", NullValueHandling = NullValueHandling.Ignore)]
public ThreadAutoArchiveDuration? DefaultAutoArchiveDuration { get; internal set; }
///
/// Gets this channel's mention string.
///
[JsonIgnore]
public string Mention
=> Formatter.Mention(this);
///
/// Gets this channel's children. This applies only to channel categories.
///
[JsonIgnore]
public IReadOnlyList Children
{
get
{
return !this.IsCategory
? throw new ArgumentException("Only channel categories contain children.")
: this.Guild._channels.Values.Where(e => e.ParentId == this.Id).ToList();
}
}
///
/// Gets the list of members currently in the channel (if voice channel), or members who can see the channel (otherwise).
///
[JsonIgnore]
public virtual IReadOnlyList Users
{
get
{
return this.Guild == null
? throw new InvalidOperationException("Cannot query users outside of guild channels.")
- : this.Type == ChannelType.Voice || this.Type == ChannelType.Stage
+ : this.IsVoiceJoinable()
? this.Guild.Members.Values.Where(x => x.VoiceState?.ChannelId == this.Id).ToList()
: this.Guild.Members.Values.Where(x => (this.PermissionsFor(x) & Permissions.AccessChannels) == Permissions.AccessChannels).ToList();
}
}
///
/// Gets whether this channel is an NSFW channel.
///
[JsonProperty("nsfw")]
public bool IsNSFW { get; internal set; }
///
/// Gets this channel's region id (if voice channel).
///
[JsonProperty("rtc_region", NullValueHandling = NullValueHandling.Ignore)]
internal string RtcRegionId { get; set; }
///
/// Gets this channel's region override (if voice channel).
///
[JsonIgnore]
public DiscordVoiceRegion RtcRegion
=> this.RtcRegionId != null ? this.Discord.VoiceRegions[this.RtcRegionId] : null;
///
/// Only sent on the resolved channels of interaction responses for application commands. Gets the permissions of the user in this channel who invoked the command.
///
[JsonProperty("permissions")]
public Permissions? UserPermissions { get; internal set; }
///
/// Initializes a new instance of the class.
///
internal DiscordChannel()
{
this._permissionOverwritesLazy = new Lazy>(() => new ReadOnlyCollection(this._permissionOverwrites));
}
#region Methods
///
/// Sends a message to this channel.
///
/// Content of the message to send.
/// The sent message.
/// Thrown when the client does not have the permission if TTS is true and if TTS is true.
/// Thrown when the channel does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public Task SendMessageAsync(string content)
{
- return this.Type != ChannelType.Text && this.Type != ChannelType.Private && this.Type != ChannelType.Group && this.Type != ChannelType.News
+ return !this.IsWriteable()
? throw new ArgumentException("Cannot send a text message to a non-text channel.")
: this.Discord.ApiClient.CreateMessageAsync(this.Id, content, null, sticker: null, replyMessageId: null, mentionReply: false, failOnInvalidReply: false);
}
///
/// Sends a message to this channel.
///
/// Embed to attach to the message.
/// The sent message.
/// Thrown when the client does not have the permission and if TTS is true.
/// Thrown when the channel does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public Task SendMessageAsync(DiscordEmbed embed)
{
- return this.Type != ChannelType.Text && this.Type != ChannelType.Private && this.Type != ChannelType.Group && this.Type != ChannelType.News
+ return !this.IsWriteable()
? throw new ArgumentException("Cannot send a text message to a non-text channel.")
: this.Discord.ApiClient.CreateMessageAsync(this.Id, null, embed != null ? new[] { embed } : null, sticker: null, replyMessageId: null, mentionReply: false, failOnInvalidReply: false);
}
///
/// Sends a message to this channel.
///
/// Embed to attach to the message.
/// Content of the message to send.
/// The sent message.
/// Thrown when the client does not have the permission if TTS is true and if TTS is true.
/// Thrown when the channel does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public Task SendMessageAsync(string content, DiscordEmbed embed)
{
- return this.Type != ChannelType.Text && this.Type != ChannelType.Private && this.Type != ChannelType.Group && this.Type != ChannelType.News
+ return !this.IsWriteable()
? throw new ArgumentException("Cannot send a text message to a non-text channel.")
: this.Discord.ApiClient.CreateMessageAsync(this.Id, content, embed != null ? new[] { embed } : null, sticker: null, replyMessageId: null, mentionReply: false, failOnInvalidReply: false);
}
///
/// Sends a message to this channel.
///
/// The builder with all the items to send.
/// The sent message.
/// Thrown when the client does not have the permission TTS is true and if TTS is true.
/// Thrown when the channel does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public Task SendMessageAsync(DiscordMessageBuilder builder)
=> this.Discord.ApiClient.CreateMessageAsync(this.Id, builder);
///
/// Sends a message to this channel.
///
/// The builder with all the items to send.
/// The sent message.
/// Thrown when the client does not have the permission TTS is true and if TTS is true.
/// Thrown when the channel does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public Task SendMessageAsync(Action action)
{
var builder = new DiscordMessageBuilder();
action(builder);
- return this.Discord.ApiClient.CreateMessageAsync(this.Id, builder);
+ return !this.IsWriteable()
+ ? throw new ArgumentException("Cannot send a text message to a non-text channel.")
+ : this.Discord.ApiClient.CreateMessageAsync(this.Id, builder);
}
///
/// Deletes a guild channel
///
/// Reason for audit logs.
/// Thrown when the client does not have the permission.
/// Thrown when the channel does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public Task DeleteAsync(string reason = null)
=> this.Discord.ApiClient.DeleteChannelAsync(this.Id, reason);
///
/// Clones this channel. This operation will create a channel with identical settings to this one. Note that this will not copy messages.
///
/// Reason for audit logs.
/// Newly-created channel.
/// Thrown when the client does not have the permission.
/// Thrown when the channel does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public async Task CloneAsync(string reason = null)
{
if (this.Guild == null)
throw new InvalidOperationException("Non-guild channels cannot be cloned.");
var ovrs = new List();
foreach (var ovr in this._permissionOverwrites)
#pragma warning disable CS0618 // Type or member is obsolete
ovrs.Add(await new DiscordOverwriteBuilder().FromAsync(ovr).ConfigureAwait(false));
#pragma warning restore CS0618 // Type or member is obsolete
var bitrate = this.Bitrate;
var userLimit = this.UserLimit;
Optional perUserRateLimit = this.PerUserRateLimit;
- if (this.Type != ChannelType.Voice)
+ if(!this.IsVoiceJoinable())
{
bitrate = null;
userLimit = null;
}
- if (this.Type != ChannelType.Text)
+
+ if (this.Type == ChannelType.Stage)
+ {
+ userLimit = null;
+ }
+ if (!this.IsWriteable())
{
perUserRateLimit = Optional.FromNoValue();
}
return await this.Guild.CreateChannelAsync(this.Name, this.Type, this.Parent, this.Topic, bitrate, userLimit, ovrs, this.IsNSFW, perUserRateLimit, this.QualityMode, reason).ConfigureAwait(false);
}
///
/// Returns a specific message
///
/// The id of the message
/// Thrown when the client does not have the permission.
/// Thrown when the channel does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public async Task GetMessageAsync(ulong id)
{
return this.Discord.Configuration.MessageCacheSize > 0
&& this.Discord is DiscordClient dc
&& dc.MessageCache != null
&& dc.MessageCache.TryGet(xm => xm.Id == id && xm.ChannelId == this.Id, out var msg)
? msg
: await this.Discord.ApiClient.GetMessageAsync(this.Id, id).ConfigureAwait(false);
}
///
/// Modifies the current channel.
///
/// Action to perform on this channel
/// Thrown when the client does not have the .
/// Thrown when the client does not have the correct for modifying the .
/// Thrown when the channel does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public Task ModifyAsync(Action action)
{
var mdl = new ChannelEditModel();
action(mdl);
if (mdl.DefaultAutoArchiveDuration.HasValue)
{
if (!Utilities.CheckThreadAutoArchiveDurationFeature(this.Guild, mdl.DefaultAutoArchiveDuration.Value))
throw new NotSupportedException($"Cannot modify DefaultAutoArchiveDuration. Guild needs boost tier {(mdl.DefaultAutoArchiveDuration.Value == ThreadAutoArchiveDuration.ThreeDays ? "one" : "two")}.");
}
return this.Discord.ApiClient.ModifyChannelAsync(this.Id, mdl.Name, mdl.Position, mdl.Topic, mdl.Nsfw,
mdl.Parent.HasValue ? mdl.Parent.Value?.Id : default(Optional), mdl.Bitrate, mdl.Userlimit, mdl.PerUserRateLimit, mdl.RtcRegion.IfPresent(r => r?.Id),
mdl.QualityMode, mdl.DefaultAutoArchiveDuration, mdl.Type, mdl.PermissionOverwrites, mdl.AuditLogReason);
}
///
/// Updates the channel position when it doesn't have a category.
///
/// Use for moving to other categories.
/// Use to move out of a category.
/// Use for moving within a category.
///
/// Position the channel should be moved to.
/// Reason for audit logs.
/// Thrown when the client does not have the permission.
/// Thrown when the channel does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
[Obsolete("This will be replaced by ModifyPositionInCategoryAsync. Use it instead.")]
public Task ModifyPositionAsync(int position, string reason = null)
{
if (this.Guild == null)
throw new ArgumentException("Cannot modify order of non-guild channels.");
+ if (!this.IsMovable())
+ throw new NotSupportedException("You can't move this type of channel in categories.");
if (this.ParentId != null)
throw new ArgumentException("Cannot modify order of channels within a category. Use ModifyPositionInCategoryAsync instead.");
var chns = this.Guild._channels.Values.Where(xc => xc.Type == this.Type).OrderBy(xc => xc.Position).ToArray();
var pmds = new RestGuildChannelReorderPayload[chns.Length];
for (var i = 0; i < chns.Length; i++)
{
pmds[i] = new RestGuildChannelReorderPayload
{
ChannelId = chns[i].Id,
};
pmds[i].Position = chns[i].Id == this.Id ? position : chns[i].Position >= position ? chns[i].Position + 1 : chns[i].Position;
}
return this.Discord.ApiClient.ModifyGuildChannelPositionAsync(this.Guild.Id, pmds, reason);
}
///
/// Updates the channel position within it's own category.
///
/// Use for moving to other categories.
/// Use to move out of a category.
/// Use to move channels outside a category.
///
/// The position.
/// The reason.
/// Thrown when the client does not have the permission.
/// Thrown when the channel does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
/// Thrown when is out of range.
/// Thrown when function is called on a channel without a parent channel.
public async Task ModifyPositionInCategoryAsync(int position, string reason = null)
{
//if (this.ParentId == null)
// throw new ArgumentException("You can call this function only on channels in categories.");
+ if (!this.IsMovableInParent())
+ throw new NotSupportedException("You can't move this type of channel in categories.");
+
var isUp = position > this.Position;
var channels = await this.InternalRefreshChannelsAsync();
var chns = this.ParentId != null
? this.Type == ChannelType.Text || this.Type == ChannelType.News
? channels.Where(xc => xc.ParentId == this.ParentId && (xc.Type == ChannelType.Text || xc.Type == ChannelType.News))
: this.Type == ChannelType.Voice || this.Type == ChannelType.Stage
? channels.Where(xc => xc.ParentId == this.ParentId && (xc.Type == ChannelType.Voice || xc.Type == ChannelType.Stage))
: channels.Where(xc => xc.ParentId == this.ParentId && xc.Type == this.Type)
: this.Type == ChannelType.Text || this.Type == ChannelType.News
? channels.Where(xc => xc.ParentId == null && (xc.Type == ChannelType.Text || xc.Type == ChannelType.News))
: this.Type == ChannelType.Voice || this.Type == ChannelType.Stage
? channels.Where(xc => xc.ParentId == null && (xc.Type == ChannelType.Voice || xc.Type == ChannelType.Stage))
: channels.Where(xc => xc.ParentId == null && xc.Type == this.Type);
var ochns = chns.OrderBy(xc => xc.Position).ToArray();
var min = ochns.First().Position;
var max = ochns.Last().Position;
if (position > max || position < min)
throw new IndexOutOfRangeException($"Position is not in range. {position} is {(position > max ? "greater then the maximal" : "lower then the minimal")} position.");
var pmds = new RestGuildChannelReorderPayload[ochns.Length];
for (var i = 0; i < ochns.Length; i++)
{
pmds[i] = new RestGuildChannelReorderPayload
{
ChannelId = ochns[i].Id,
};
if (ochns[i].Id == this.Id)
{
pmds[i].Position = position;
}
else
{
if (isUp)
{
if (ochns[i].Position <= position && ochns[i].Position > this.Position)
{
pmds[i].Position = ochns[i].Position - 1;
}
else if (ochns[i].Position < this.Position || ochns[i].Position > position)
{
pmds[i].Position = ochns[i].Position;
}
}
else
{
if (ochns[i].Position >= position && ochns[i].Position < this.Position)
{
pmds[i].Position = ochns[i].Position + 1;
}
else if (ochns[i].Position > this.Position || ochns[i].Position < position)
{
pmds[i].Position = ochns[i].Position;
}
}
}
}
await this.Discord.ApiClient.ModifyGuildChannelPositionAsync(this.Guild.Id, pmds, reason).ConfigureAwait(false);
}
///
/// Internaly refreshes the channel list.
///
private async Task> InternalRefreshChannelsAsync()
{
await this.RefreshPositionsAsync();
return this.Guild.Channels.Values.ToList().AsReadOnly();
}
///
/// Refreshes the positions.
///
public async Task RefreshPositionsAsync()
{
var channels = await this.Discord.ApiClient.GetGuildChannelsAsync(this.Guild.Id);
this.Guild._channels.Clear();
foreach (var channel in channels.ToList())
{
channel.Discord = this.Discord;
foreach (var xo in channel._permissionOverwrites)
{
xo.Discord = this.Discord;
xo._channel_id = channel.Id;
}
this.Guild._channels[channel.Id] = channel;
}
}
///
/// Updates the channel position within it's own category.
/// Valid modes: '+' or 'down' to move a channel down | '-' or 'up' to move a channel up.
///
/// Use for moving to other categories.
/// Use to move out of a category.
/// Use to move channels outside a category.
///
/// The mode. Valid: '+' or 'down' to move a channel down | '-' or 'up' to move a channel up
/// The position.
/// The reason.
/// Thrown when the client does not have the permission.
/// Thrown when the channel does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
/// Thrown when is out of range.
/// Thrown when function is called on a channel without a parent channel, a wrong mode is givven or given position is zero.
public Task ModifyPositionInCategorySmartAsync(string mode, int position, string reason = null)
{
+ if (!this.IsMovableInParent())
+ throw new NotSupportedException("You can't move this type of channel in categories.");
+
if (mode != "+" && mode != "-" && mode != "down" && mode != "up")
throw new ArgumentException("Error with the selected mode: Valid is '+' or 'down' to move a channel down and '-' or 'up' to move a channel up");
var positive = mode == "+" || mode == "positive" || mode == "down";
var negative = mode == "-" || mode == "negative" || mode == "up";
return positive
? position < this.GetMaxPosition()
? this.ModifyPositionInCategoryAsync(this.Position + position, reason)
: throw new IndexOutOfRangeException($"Position is not in range of category.")
: negative
? position > this.GetMinPosition()
? this.ModifyPositionInCategoryAsync(this.Position - position, reason)
: throw new IndexOutOfRangeException($"Position is not in range of category.")
: throw new ArgumentException("You can only modify with +X or -X. 0 is not valid.");
}
///
/// Updates the channel parent, moving the channel to the bottom of the new category.
///
/// New parent for channel. Will move out of parent if null.
/// Sync permissions with parent. Defaults to null.
/// Reason for audit logs.
/// Thrown when the client does not have the permission.
/// Thrown when the channel does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
#pragma warning disable CS8632 // The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.
public Task ModifyParentAsync(DiscordChannel? newParent = null, bool? lock_permissions = null, string reason = null)
#pragma warning restore CS8632 // The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.
{
if (this.Guild == null)
throw new ArgumentException("Cannot modify parent of non-guild channels.");
- if (this.IsCategory)
- throw new ArgumentException("Cannot modify parent of category channels.");
+ if (!this.IsMovableInParent())
+ throw new NotSupportedException("You can't move this type of channel in categories.");
if (newParent.Type is not ChannelType.Category)
throw new ArgumentException("Only category type channels can be parents.");
var position = this.Guild._channels.Values.Where(xc => xc.Type == this.Type && xc.ParentId == newParent.Id) // gets list same type channels in parent
.Select(xc => xc.Position).DefaultIfEmpty(-1).Max() + 1; // returns highest position of list +1, default val: 0
var chns = this.Guild._channels.Values.Where(xc => xc.Type == this.Type)
.OrderBy(xc => xc.Position).ToArray();
var pmds = new RestGuildChannelNewParentPayload[chns.Length];
for (var i = 0; i < chns.Length; i++)
{
pmds[i] = new RestGuildChannelNewParentPayload
{
ChannelId = chns[i].Id,
Position = chns[i].Position >= position ? chns[i].Position + 1 : chns[i].Position,
};
if (chns[i].Id == this.Id)
{
pmds[i].Position = position;
pmds[i].ParentId = newParent is not null ? newParent.Id : null;
pmds[i].LockPermissions = lock_permissions;
}
}
return this.Discord.ApiClient.ModifyGuildChannelParentAsync(this.Guild.Id, pmds, reason);
}
///
/// Moves the channel out of a category.
///
/// Reason for audit logs.
/// Thrown when the client does not have the permission.
/// Thrown when the channel does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public Task RemoveParentAsync(string reason = null)
{
if (this.Guild == null)
throw new ArgumentException("Cannot modify parent of non-guild channels.");
- if (this.IsCategory)
- throw new ArgumentException("Cannot modify parent of category channels.");
+ if (!this.IsMovableInParent())
+ throw new NotSupportedException("You can't move this type of channel in categories.");
var position = this.Guild._channels.Values.Where(xc => xc.Type == this.Type && xc.Parent is null) //gets list of same type channels with no parent
.Select(xc => xc.Position).DefaultIfEmpty(-1).Max() + 1; // returns highest position of list +1, default val: 0
var chns = this.Guild._channels.Values.Where(xc => xc.Type == this.Type)
.OrderBy(xc => xc.Position).ToArray();
var pmds = new RestGuildChannelNoParentPayload[chns.Length];
for (var i = 0; i < chns.Length; i++)
{
pmds[i] = new RestGuildChannelNoParentPayload
{
ChannelId = chns[i].Id,
};
if (chns[i].Id == this.Id)
{
pmds[i].Position = 1;
pmds[i].ParentId = null;
}
else
{
pmds[i].Position = chns[i].Position < this.Position ? chns[i].Position + 1 : chns[i].Position;
}
}
return this.Discord.ApiClient.DetachGuildChannelParentAsync(this.Guild.Id, pmds, reason);
}
///
/// Returns a list of messages before a certain message.
/// The amount of messages to fetch.
/// Message to fetch before from.
///
/// Thrown when the client does not have the permission.
/// Thrown when the channel does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public Task> GetMessagesBeforeAsync(ulong before, int limit = 100)
=> this.GetMessagesInternalAsync(limit, before, null, null);
///
/// Returns a list of messages after a certain message.
/// The amount of messages to fetch.
/// Message to fetch after from.
///
/// Thrown when the client does not have the permission.
/// Thrown when the channel does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public Task> GetMessagesAfterAsync(ulong after, int limit = 100)
=> this.GetMessagesInternalAsync(limit, null, after, null);
///
/// Returns a list of messages around a certain message.
/// The amount of messages to fetch.
/// Message to fetch around from.
///
/// Thrown when the client does not have the permission.
/// Thrown when the channel does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public Task> GetMessagesAroundAsync(ulong around, int limit = 100)
=> this.GetMessagesInternalAsync(limit, null, null, around);
///
/// Returns a list of messages from the last message in the channel.
/// The amount of messages to fetch.
///
/// Thrown when the client does not have the permission.
/// Thrown when the channel does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public Task> GetMessagesAsync(int limit = 100) =>
this.GetMessagesInternalAsync(limit, null, null, null);
///
/// Returns a list of messages
///
/// How many messages should be returned.
/// Get messages before snowflake.
/// Get messages after snowflake.
/// Get messages around snowflake.
private async Task> GetMessagesInternalAsync(int limit = 100, ulong? before = null, ulong? after = null, ulong? around = null)
{
- if (this.Type != ChannelType.Text && this.Type != ChannelType.Private && this.Type != ChannelType.Group && this.Type != ChannelType.News)
+ if (!this.IsWriteable())
throw new ArgumentException("Cannot get the messages of a non-text channel.");
if (limit < 0)
throw new ArgumentException("Cannot get a negative number of messages.");
if (limit == 0)
return Array.Empty();
//return this.Discord.ApiClient.GetChannelMessagesAsync(this.Id, limit, before, after, around);
if (limit > 100 && around != null)
throw new InvalidOperationException("Cannot get more than 100 messages around the specified ID.");
var msgs = new List(limit);
var remaining = limit;
ulong? last = null;
var isAfter = after != null;
int lastCount;
do
{
var fetchSize = remaining > 100 ? 100 : remaining;
var fetch = await this.Discord.ApiClient.GetChannelMessagesAsync(this.Id, fetchSize, !isAfter ? last ?? before : null, isAfter ? last ?? after : null, around).ConfigureAwait(false);
lastCount = fetch.Count;
remaining -= lastCount;
if (!isAfter)
{
msgs.AddRange(fetch);
last = fetch.LastOrDefault()?.Id;
}
else
{
msgs.InsertRange(0, fetch);
last = fetch.FirstOrDefault()?.Id;
}
}
while (remaining > 0 && lastCount > 0);
return new ReadOnlyCollection(msgs);
}
///
/// Deletes multiple messages if they are less than 14 days old. If they are older, none of the messages will be deleted and you will receive a error.
///
/// A collection of messages to delete.
/// Reason for audit logs.
/// Thrown when the client does not have the permission.
/// Thrown when the channel does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public async Task DeleteMessagesAsync(IEnumerable messages, string reason = null)
{
// don't enumerate more than once
var msgs = messages.Where(x => x.Channel.Id == this.Id).Select(x => x.Id).ToArray();
if (messages == null || !msgs.Any())
throw new ArgumentException("You need to specify at least one message to delete.");
if (msgs.Count() < 2)
{
await this.Discord.ApiClient.DeleteMessageAsync(this.Id, msgs.Single(), reason).ConfigureAwait(false);
return;
}
for (var i = 0; i < msgs.Count(); i += 100)
await this.Discord.ApiClient.DeleteMessagesAsync(this.Id, msgs.Skip(i).Take(100), reason).ConfigureAwait(false);
}
///
/// Deletes a message
///
/// The message to be deleted.
/// Reason for audit logs.
/// Thrown when the client does not have the permission.
/// Thrown when the channel does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public Task DeleteMessageAsync(DiscordMessage message, string reason = null)
=> this.Discord.ApiClient.DeleteMessageAsync(this.Id, message.Id, reason);
///
/// Returns a list of invite objects
///
/// Thrown when the client does not have the permission.
/// Thrown when the channel does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public Task> GetInvitesAsync()
{
return this.Guild == null
? throw new ArgumentException("Cannot get the invites of a channel that does not belong to a guild.")
: this.Discord.ApiClient.GetChannelInvitesAsync(this.Id);
}
///
/// Create a new invite object
///
/// Duration of invite in seconds before expiry, or 0 for never. Defaults to 86400.
/// Max number of uses or 0 for unlimited. Defaults to 0
/// Whether this invite only grants temporary membership. Defaults to false.
/// If true, don't try to reuse a similar invite (useful for creating many unique one time use invites)
/// Target type of invite for the channel. Defaults to Streaming
/// Target application of invite for the channel. Defaults to None
/// Reason for audit logs.
/// Thrown when the client does not have the permission.
/// Thrown when the channel does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public Task CreateInviteAsync(int max_age = 86400, int max_uses = 0, bool temporary = false, bool unique = false, TargetType? target_type = null, TargetActivity? target_application = null, string reason = null)
=> this.Discord.ApiClient.CreateChannelInviteAsync(this.Id, max_age, max_uses, target_type, target_application, temporary, unique, reason);
#region Stage
///
/// Opens a stage.
///
/// Topic of the stage.
/// Whether @everyone should be notified.
/// Privacy level of the stage (Defaults to .
/// Audit log reason.
/// Stage instance
/// Thrown when the client does not have the permission.
/// Thrown when the channel does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public async Task OpenStageAsync(string topic, bool send_start_notification = false, StagePrivacyLevel privacy_level = StagePrivacyLevel.GUILD_ONLY, string reason = null)
=> await this.Discord.ApiClient.CreateStageInstanceAsync(this.Id, topic, send_start_notification, privacy_level, reason);
///
/// Modifies a stage topic.
///
/// New topic of the stage.
/// New privacy level of the stage.
/// Audit log reason.
/// Thrown when the client does not have the permission.
/// Thrown when the channel does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public async Task ModifyStageAsync(Optional topic, Optional privacy_level, string reason = null)
=> await this.Discord.ApiClient.ModifyStageInstanceAsync(this.Id, topic, privacy_level, reason);
///
/// Closes a stage.
///
/// Audit log reason.
/// Thrown when the client does not have the permission.
/// Thrown when the channel does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public async Task CloseStageAsync(string reason = null)
=> await this.Discord.ApiClient.DeleteStageInstanceAsync(this.Id, reason);
///
/// Gets a stage.
///
/// The requested stage.
/// Thrown when the client does not have the or permission.
/// Thrown when the channel does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public async Task GetStageAsync()
=> await this.Discord.ApiClient.GetStageInstanceAsync(this.Id);
#endregion
#region Threads
///
/// Creates a thread.
/// Depending on whether it is created inside an or an it is either an or an .
/// Depending on whether the is set to it is either an or an (default).
///
/// The name of the thread.
/// till it gets archived. Defaults to .
/// Can be either an , or an .
/// Audit log reason.
/// The created thread.
/// Thrown when the client does not have the or or if creating a private thread the permission.
/// Thrown when the guild hasn't enabled threads atm.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
/// Thrown when the cannot be modified. This happens, when the guild hasn't reached a certain boost . Or if is not enabled for guild. This happens, if the guild does not have
public async Task CreateThreadAsync(string name, ThreadAutoArchiveDuration auto_archive_duration = ThreadAutoArchiveDuration.OneHour, ChannelType type = ChannelType.PublicThread, string reason = null)
{
return (type != ChannelType.NewsThread && type != ChannelType.PublicThread && type != ChannelType.PrivateThread)
? throw new NotSupportedException("Wrong thread type given.")
- : (this.Type != ChannelType.News && this.Type != ChannelType.Text)
- ? throw new NotSupportedException("Parent channel is no text or news channel")
+ : (!this.IsThreadHolder())
+ ? throw new NotSupportedException("Parent channel can't have threads")
: type == ChannelType.PrivateThread
? Utilities.CheckThreadPrivateFeature(this.Guild)
? Utilities.CheckThreadAutoArchiveDurationFeature(this.Guild, auto_archive_duration)
? await this.Discord.ApiClient.CreateThreadWithoutMessageAsync(this.Id, name, auto_archive_duration, type, reason)
: throw new NotSupportedException($"Cannot modify ThreadAutoArchiveDuration. Guild needs boost tier {(auto_archive_duration == ThreadAutoArchiveDuration.ThreeDays ? "one" : "two")}.")
: throw new NotSupportedException($"Cannot create a private thread. Guild needs to be boost tier two.")
: Utilities.CheckThreadAutoArchiveDurationFeature(this.Guild, auto_archive_duration)
? await this.Discord.ApiClient.CreateThreadWithoutMessageAsync(this.Id, name, auto_archive_duration, this.Type == ChannelType.News ? ChannelType.NewsThread : ChannelType.PublicThread, reason)
: throw new NotSupportedException($"Cannot modify ThreadAutoArchiveDuration. Guild needs boost tier {(auto_archive_duration == ThreadAutoArchiveDuration.ThreeDays ? "one" : "two")}.");
}
///
/// Gets joined archived private threads. Can contain more threads.
/// If the result's value 'HasMore' is true, you need to recall this function to get older threads.
///
/// Get threads created before this thread id.
/// Defines the limit of returned .
/// Thrown when the client does not have the permission.
/// Thrown when the channel does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public async Task GetJoinedPrivateArchivedThreadsAsync(ulong? before, int? limit)
=> await this.Discord.ApiClient.GetJoinedPrivateArchivedThreadsAsync(this.Id, before, limit);
///
/// Gets archived public threads. Can contain more threads.
/// If the result's value 'HasMore' is true, you need to recall this function to get older threads.
///
/// Get threads created before this thread id.
/// Defines the limit of returned .
/// Thrown when the client does not have the permission.
/// Thrown when the channel does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public async Task GetPublicArchivedThreadsAsync(ulong? before, int? limit)
=> await this.Discord.ApiClient.GetPublicArchivedThreadsAsync(this.Id, before, limit);
///
/// Gets archived private threads. Can contain more threads.
/// If the result's value 'HasMore' is true, you need to recall this function to get older threads.
///
/// Get threads created before this thread id.
/// Defines the limit of returned .
/// Thrown when the client does not have the or permission.
/// Thrown when the channel does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public async Task GetPrivateArchivedThreadsAsync(ulong? before, int? limit)
=> await this.Discord.ApiClient.GetPrivateArchivedThreadsAsync(this.Id, before, limit);
#endregion
///
/// Adds a channel permission overwrite for specified role.
///
/// The role to have the permission added.
/// The permissions to allow.
/// The permissions to deny.
/// Reason for audit logs.
/// Thrown when the client does not have the permission.
/// Thrown when the channel does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public Task AddOverwriteAsync(DiscordRole role, Permissions allow = Permissions.None, Permissions deny = Permissions.None, string reason = null)
=> this.Discord.ApiClient.EditChannelPermissionsAsync(this.Id, role.Id, allow, deny, "role", reason);
///
/// Adds a channel permission overwrite for specified member.
///
/// The member to have the permission added.
/// The permissions to allow.
/// The permissions to deny.
/// Reason for audit logs.
/// Thrown when the client does not have the permission.
/// Thrown when the channel does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public Task AddOverwriteAsync(DiscordMember member, Permissions allow = Permissions.None, Permissions deny = Permissions.None, string reason = null)
=> this.Discord.ApiClient.EditChannelPermissionsAsync(this.Id, member.Id, allow, deny, "member", reason);
///
/// Deletes a channel permission overwrite for specified member.
///
/// The member to have the permission deleted.
/// Reason for audit logs.
/// Thrown when the client does not have the permission.
/// Thrown when the channel does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public Task DeleteOverwriteAsync(DiscordMember member, string reason = null)
=> this.Discord.ApiClient.DeleteChannelPermissionAsync(this.Id, member.Id, reason);
///
/// Deletes a channel permission overwrite for specified role.
///
/// The role to have the permission deleted.
/// Reason for audit logs.
/// Thrown when the client does not have the permission.
/// Thrown when the channel does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public Task DeleteOverwriteAsync(DiscordRole role, string reason = null)
=> this.Discord.ApiClient.DeleteChannelPermissionAsync(this.Id, role.Id, reason);
///
/// Post a typing indicator
///
/// Thrown when the channel does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public Task TriggerTypingAsync()
{
- return this.Type != ChannelType.Text && this.Type != ChannelType.Private && this.Type != ChannelType.Group && this.Type != ChannelType.News
+ return !this.IsWriteable()
? throw new ArgumentException("Cannot start typing in a non-text channel.")
: this.Discord.ApiClient.TriggerTypingAsync(this.Id);
}
///
/// Returns all pinned messages
///
/// Thrown when the client does not have the permission.
/// Thrown when the channel does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public Task> GetPinnedMessagesAsync()
{
- return this.Type != ChannelType.Text && this.Type != ChannelType.Private && this.Type != ChannelType.Group && this.Type != ChannelType.News
+ return !this.IsWriteable()
? throw new ArgumentException("A non-text channel does not have pinned messages.")
: this.Discord.ApiClient.GetPinnedMessagesAsync(this.Id);
}
///
/// Create a new webhook
///
/// The name of the webhook.
/// The image for the default webhook avatar.
/// Reason for audit logs.
/// Thrown when the client does not have the permission.
/// Thrown when the channel does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public async Task CreateWebhookAsync(string name, Optional avatar = default, string reason = null)
{
var av64 = Optional.FromNoValue();
if (avatar.HasValue && avatar.Value != null)
using (var imgtool = new ImageTool(avatar.Value))
av64 = imgtool.GetBase64();
else if (avatar.HasValue)
av64 = null;
return await this.Discord.ApiClient.CreateWebhookAsync(this.Id, name, av64, reason).ConfigureAwait(false);
}
///
/// Returns a list of webhooks
///
/// Thrown when the client does not have the permission.
/// Thrown when the channel does not exist.
/// Thrown when Discord is unable to process the request.
public Task> GetWebhooksAsync()
=> this.Discord.ApiClient.GetChannelWebhooksAsync(this.Id);
///
/// Moves a member to this voice channel
///
/// The member to be moved.
/// Thrown when the client does not have the permission.
/// Thrown when the channel does not exists or if the Member does not exists.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public async Task PlaceMemberAsync(DiscordMember member)
{
- if (this.Type != ChannelType.Voice && this.Type != ChannelType.Stage)
- throw new ArgumentException("Cannot place a member in a non-voice channel!"); // be a little more angry, let em learn!!1
+ if (!this.IsVoiceJoinable())
+ throw new ArgumentException("Cannot place a member in a non-voice channel.");
await this.Discord.ApiClient.ModifyGuildMemberAsync(this.Guild.Id, member.Id, default, default, default,
default, this.Id, null).ConfigureAwait(false);
}
///
/// Follows a news channel
///
/// Channel to crosspost messages to
/// Thrown when trying to follow a non-news channel
/// Thrown when the current user doesn't have on the target channel
public Task FollowAsync(DiscordChannel targetChannel)
{
return this.Type != ChannelType.News
? throw new ArgumentException("Cannot follow a non-news channel.")
: this.Discord.ApiClient.FollowChannelAsync(this.Id, targetChannel.Id);
}
///
/// Publishes a message in a news channel to following channels
///
/// Message to publish
/// Thrown when the message has already been crossposted
///
/// Thrown when the current user doesn't have and/or
///
public Task CrosspostMessageAsync(DiscordMessage message)
{
return (message.Flags & MessageFlags.Crossposted) == MessageFlags.Crossposted
? throw new ArgumentException("Message is already crossposted.")
: this.Discord.ApiClient.CrosspostMessageAsync(this.Id, message.Id);
}
///
/// Updates the current user's suppress state in this channel, if stage channel.
///
/// Toggles the suppress state.
/// Sets the time the user requested to speak.
/// Thrown when the channel is not a stage channel.
public async Task UpdateCurrentUserVoiceStateAsync(bool? suppress, DateTimeOffset? requestToSpeakTimestamp = null)
{
if (this.Type != ChannelType.Stage)
throw new ArgumentException("Voice state can only be updated in a stage channel.");
await this.Discord.ApiClient.UpdateCurrentUserVoiceStateAsync(this.GuildId.Value, this.Id, suppress, requestToSpeakTimestamp).ConfigureAwait(false);
}
///
/// Calculates permissions for a given member.
///
/// Member to calculate permissions for.
/// Calculated permissions for a given member.
public Permissions PermissionsFor(DiscordMember mbr)
{
// future note: might be able to simplify @everyone role checks to just check any role ... but I'm not sure
// xoxo, ~uwx
//
// you should use a single tilde
// ~emzi
// user > role > everyone
// allow > deny > undefined
// =>
// user allow > user deny > role allow > role deny > everyone allow > everyone deny
// thanks to meew0
if (this.IsPrivate || this.Guild == null)
return Permissions.None;
if (this.Guild.OwnerId == mbr.Id)
return PermissionMethods.FULL_PERMS;
Permissions perms;
// assign @everyone permissions
var everyoneRole = this.Guild.EveryoneRole;
perms = everyoneRole.Permissions;
// roles that member is in
var mbRoles = mbr.Roles.Where(xr => xr.Id != everyoneRole.Id).ToArray();
// assign permissions from member's roles (in order)
perms |= mbRoles.Aggregate(Permissions.None, (c, role) => c | role.Permissions);
// Adminstrator grants all permissions and cannot be overridden
if ((perms & Permissions.Administrator) == Permissions.Administrator)
return PermissionMethods.FULL_PERMS;
// channel overrides for roles that member is in
var mbRoleOverrides = mbRoles
.Select(xr => this._permissionOverwrites.FirstOrDefault(xo => xo.Id == xr.Id))
.Where(xo => xo != null)
.ToList();
// assign channel permission overwrites for @everyone pseudo-role
var everyoneOverwrites = this._permissionOverwrites.FirstOrDefault(xo => xo.Id == everyoneRole.Id);
if (everyoneOverwrites != null)
{
perms &= ~everyoneOverwrites.Denied;
perms |= everyoneOverwrites.Allowed;
}
// assign channel permission overwrites for member's roles (explicit deny)
perms &= ~mbRoleOverrides.Aggregate(Permissions.None, (c, overs) => c | overs.Denied);
// assign channel permission overwrites for member's roles (explicit allow)
perms |= mbRoleOverrides.Aggregate(Permissions.None, (c, overs) => c | overs.Allowed);
// channel overrides for just this member
var mbOverrides = this._permissionOverwrites.FirstOrDefault(xo => xo.Id == mbr.Id);
if (mbOverrides == null) return perms;
// assign channel permission overwrites for just this member
perms &= ~mbOverrides.Denied;
perms |= mbOverrides.Allowed;
return perms;
}
///
/// Returns a string representation of this channel.
///
/// String representation of this channel.
public override string ToString()
{
return this.Type == ChannelType.Category
? $"Channel Category {this.Name} ({this.Id})"
- : this.Type == ChannelType.Text || this.Type == ChannelType.News
+ : this.Type == ChannelType.Text || this.Type == ChannelType.News || this.IsThread()
? $"Channel #{this.Name} ({this.Id})"
+ : this.IsVoiceJoinable()
+ ? $"Channel #!{this.Name} ({this.Id})"
: !string.IsNullOrWhiteSpace(this.Name) ? $"Channel {this.Name} ({this.Id})" : $"Channel {this.Id}";
}
#endregion
///
/// 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 DiscordChannel);
///
/// Checks whether this is equal to another .
///
/// to compare to.
/// Whether the is equal to this .
public bool Equals(DiscordChannel e) => e is not null && (ReferenceEquals(this, e) || this.Id == e.Id);
///
/// 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 channel to compare.
/// Second channel to compare.
/// Whether the two channels are equal.
public static bool operator ==(DiscordChannel e1, DiscordChannel 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 channel to compare.
/// Second channel to compare.
/// Whether the two channels are not equal.
public static bool operator !=(DiscordChannel e1, DiscordChannel e2)
=> !(e1 == e2);
}
}
diff --git a/DisCatSharp/Entities/Thread/DiscordThreadChannel.cs b/DisCatSharp/Entities/Thread/DiscordThreadChannel.cs
index 82ffa762b..bcb7a6a4d 100644
--- a/DisCatSharp/Entities/Thread/DiscordThreadChannel.cs
+++ b/DisCatSharp/Entities/Thread/DiscordThreadChannel.cs
@@ -1,659 +1,661 @@
// This file is part of the DisCatSharp project.
//
// Copyright (c) 2021 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.Concurrent;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Globalization;
using System.Linq;
using System.Threading.Tasks;
using DisCatSharp.Exceptions;
using DisCatSharp.Net.Models;
using DisCatSharp.Net.Serialization;
using Newtonsoft.Json;
namespace DisCatSharp.Entities
{
///
/// Represents a discord thread channel.
///
public class DiscordThreadChannel : DiscordChannel, IEquatable
{
///
/// Gets ID of the owner that started this thread.
///
[JsonProperty("owner_id", NullValueHandling = NullValueHandling.Ignore)]
public ulong OwnerId { get; internal set; }
///
/// Gets the name of this thread.
///
[JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)]
public new string Name { get; internal set; }
///
/// Gets the type of this thread.
///
[JsonProperty("type", NullValueHandling = NullValueHandling.Ignore)]
public new ChannelType Type { get; internal set; }
///
/// Gets whether this thread is private.
///
[JsonIgnore]
public new bool IsPrivate
=> this.Type == ChannelType.PrivateThread;
///
/// Gets the ID of the last message sent in this thread.
///
[JsonProperty("last_message_id", NullValueHandling = NullValueHandling.Ignore)]
public new ulong? LastMessageId { get; internal set; }
///
/// Gets the slowmode delay configured for this thread.
/// All bots, as well as users with or permissions in the channel are exempt from slowmode.
///
[JsonProperty("rate_limit_per_user", NullValueHandling = NullValueHandling.Ignore)]
public new int? PerUserRateLimit { get; internal set; }
///
/// Gets an approximate count of messages in a thread, stops counting at 50.
///
[JsonProperty("message_count", NullValueHandling = NullValueHandling.Ignore)]
public int? MessageCount { get; internal set; }
///
/// Gets an approximate count of users in a thread, stops counting at 50.
///
[JsonProperty("member_count", NullValueHandling = NullValueHandling.Ignore)]
public int? MemberCount { get; internal set; }
///
/// Represents the current member for this thread. This will have a value if the user has joined the thread.
///
[JsonProperty("member", NullValueHandling = NullValueHandling.Ignore)]
public DiscordThreadChannelMember CurrentMember { get; internal set; }
///
/// Gets when the last pinned message was pinned in this thread.
///
[JsonIgnore]
public new DateTimeOffset? LastPinTimestamp
=> !string.IsNullOrWhiteSpace(this.LastPinTimestampRaw) && DateTimeOffset.TryParse(this.LastPinTimestampRaw, CultureInfo.InvariantCulture, DateTimeStyles.None, out var dto) ?
dto : null;
///
/// Gets when the last pinned message was pinned in this thread as raw string.
///
[JsonProperty("last_pin_timestamp", NullValueHandling = NullValueHandling.Ignore)]
internal new string LastPinTimestampRaw { get; set; }
///
/// Gets the threads metadata.
///
[JsonProperty("thread_metadata", NullValueHandling = NullValueHandling.Ignore)]
public DiscordThreadChannelMetadata ThreadMetadata { get; internal set; }
///
/// Gets the thread members object.
///
[JsonIgnore]
public IReadOnlyDictionary ThreadMembers => new ReadOnlyConcurrentDictionary(this._threadMembers);
[JsonProperty("thread_member", NullValueHandling = NullValueHandling.Ignore)]
[JsonConverter(typeof(SnowflakeArrayAsDictionaryJsonConverter))]
internal ConcurrentDictionary _threadMembers;
///
/// Initializes a new instance of the class.
///
internal DiscordThreadChannel() { }
#region Methods
///
/// Deletes a thread.
///
/// Reason for audit logs.
/// Thrown when the client does not have the permission.
/// Thrown when the thread does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public new Task DeleteAsync(string reason = null)
=> this.Discord.ApiClient.DeleteThreadAsync(this.Id, reason);
///
/// Modifies the current thread.
///
/// Action to perform on this thread
/// Thrown when the client does not have the permission.
/// Thrown when the thread does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
/// Thrown when the cannot be modified. This happens, when the guild hasn't reached a certain boost .
public Task ModifyAsync(Action action)
{
var mdl = new ThreadEditModel();
action(mdl);
var can_continue = !mdl.AutoArchiveDuration.HasValue || !mdl.AutoArchiveDuration.Value.HasValue || Utilities.CheckThreadAutoArchiveDurationFeature(this.Guild, mdl.AutoArchiveDuration.Value.Value);
if (mdl.Invitable.HasValue)
{
can_continue = this.Guild.Features.CanCreatePrivateThreads;
}
return can_continue ? this.Discord.ApiClient.ModifyThreadAsync(this.Id, mdl.Name, mdl.Locked, mdl.Archived, mdl.AutoArchiveDuration, mdl.PerUserRateLimit, mdl.Invitable, mdl.AuditLogReason) : throw new NotSupportedException($"Cannot modify ThreadAutoArchiveDuration. Guild needs boost tier {(mdl.AutoArchiveDuration.Value.Value == ThreadAutoArchiveDuration.ThreeDays ? "one" : "two")}.");
}
///
/// Archives a thread.
///
/// Whether the thread should be locked.
/// Reason for audit logs.
/// Thrown when the client does not have the permission.
/// Thrown when the thread does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public Task ArchiveAsync(bool locked = true, string reason = null)
=> this.Discord.ApiClient.ModifyThreadAsync(this.Id, null, locked, true, null, null, null, reason: reason);
///
/// Unarchives a thread.
///
/// Reason for audit logs.
/// Thrown when the thread does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public Task UnarchiveAsync(string reason = null)
=> this.Discord.ApiClient.ModifyThreadAsync(this.Id, null, null, false, null, null, null, reason: reason);
///
/// Gets the members of a thread. Needs the intent.
///
/// Thrown when the thread does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public async Task> GetMembersAsync()
=> await this.Discord.ApiClient.GetThreadMembersAsync(this.Id);
///
/// Adds a member to this thread.
///
/// The member id to be added.
/// Thrown when the thread does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public Task AddMemberAsync(ulong member_id)
=> this.Discord.ApiClient.AddThreadMemberAsync(this.Id, member_id);
///
/// Adds a member to this thread.
///
/// The member to be added.
/// Thrown when the thread does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public Task AddMemberAsync(DiscordMember member)
=> this.AddMemberAsync(member.Id);
///
/// Gets a member in this thread.
///
/// The member to be added.
/// Thrown when the member is not part of the thread.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public Task GetMemberAsync(ulong member_id)
=> this.Discord.ApiClient.GetThreadMemberAsync(this.Id, member_id);
///
/// Gets a member in this thread.
///
/// The member to be added.
/// Thrown when the member is not part of the thread.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public Task GetMemberAsync(DiscordMember member)
=> this.Discord.ApiClient.GetThreadMemberAsync(this.Id, member.Id);
///
/// Removes a member from this thread.
///
/// The member id to be removed.
/// Thrown when the thread does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public Task RemoveMemberAsync(ulong member_id)
=> this.Discord.ApiClient.RemoveThreadMemberAsync(this.Id, member_id);
///
/// Removes a member from this thread. Only applicable to private threads.
///
/// The member to be removed.
/// Thrown when the thread does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public Task RemoveMemberAsync(DiscordMember member)
=> this.RemoveMemberAsync(member.Id);
///
/// Adds a role to this thread. Only applicable to private threads.
///
/// The role id to be added.
/// Thrown when the thread does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public async Task AddRoleAsync(ulong role_id)
{
var role = this.Guild.GetRole(role_id);
var members = await this.Guild.GetAllMembersAsync();
var roleMembers = members.Where(m => m.Roles.Contains(role));
foreach(var member in roleMembers)
{
await this.Discord.ApiClient.AddThreadMemberAsync(this.Id, member.Id);
}
}
///
/// Adds a role to this thread. Only applicable to private threads.
///
/// The role to be added.
/// Thrown when the thread does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public Task AddRoleAsync(DiscordRole role)
=> this.AddRoleAsync(role.Id);
///
/// Removes a role from this thread. Only applicable to private threads.
///
/// The role id to be removed.
/// Thrown when the thread does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public async Task RemoveRoleAsync(ulong role_id)
{
var role = this.Guild.GetRole(role_id);
var members = await this.Guild.GetAllMembersAsync();
var roleMembers = members.Where(m => m.Roles.Contains(role));
foreach (var member in roleMembers)
{
await this.Discord.ApiClient.RemoveThreadMemberAsync(this.Id, member.Id);
}
}
///
/// Removes a role to from thread. Only applicable to private threads.
///
/// The role to be removed.
/// Thrown when the thread does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public Task RemoveRoleAsync(DiscordRole role)
=> this.RemoveRoleAsync(role.Id);
///
/// Joins a thread.
///
/// Thrown when the client has no access to this thread.
/// Thrown when the thread does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public Task JoinAsync()
=> this.Discord.ApiClient.JoinThreadAsync(this.Id);
///
/// Leaves a thread.
///
/// Thrown when the client has no access to this thread.
/// Thrown when the thread does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public Task LeaveAsync()
=> this.Discord.ApiClient.LeaveThreadAsync(this.Id);
///
/// Sends a message to this thread.
///
/// Content of the message to send.
/// The sent message.
/// Thrown when the client does not have the permission and if TTS is true or the thread is locked.
/// Thrown when the thread does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public new Task SendMessageAsync(string content)
{
- return this.Type != ChannelType.PublicThread && this.Type != ChannelType.PrivateThread && this.Type != ChannelType.NewsThread
+ return !this.IsWriteable()
? throw new ArgumentException("Cannot send a text message to a non-thread channel.")
: this.Discord.ApiClient.CreateMessageAsync(this.Id, content, null, sticker: null, replyMessageId: null, mentionReply: false, failOnInvalidReply: false);
}
///
/// Sends a message to this thread.
///
/// Embed to attach to the message.
/// The sent message.
/// Thrown when the client does not have the permission and if TTS is true or the thread is locked.
/// Thrown when the thread does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public new Task SendMessageAsync(DiscordEmbed embed)
{
- return this.Type != ChannelType.PublicThread && this.Type != ChannelType.PrivateThread && this.Type != ChannelType.NewsThread
+ return !this.IsWriteable()
? throw new ArgumentException("Cannot send a text message to a non-thread channel.")
: this.Discord.ApiClient.CreateMessageAsync(this.Id, null, new[] {embed}, sticker: null, replyMessageId: null, mentionReply: false, failOnInvalidReply: false);
}
///
/// Sends a message to this thread.
///
/// Content of the message to send.
/// Embed to attach to the message.
/// The sent message.
/// Thrown when the client does not have the permission and if TTS is true or the thread is locked.
/// Thrown when the thread does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public new Task SendMessageAsync(string content, DiscordEmbed embed)
{
- return this.Type != ChannelType.PublicThread && this.Type != ChannelType.PrivateThread && this.Type != ChannelType.NewsThread
+ return !this.IsWriteable()
? throw new ArgumentException("Cannot send a text message to a non-thread channel.")
: this.Discord.ApiClient.CreateMessageAsync(this.Id, content, new[] {embed}, sticker: null, replyMessageId: null, mentionReply: false, failOnInvalidReply: false);
}
///
/// Sends a message to this thread.
///
/// The builder with all the items to thread.
/// The sent message.
/// Thrown when the client does not have the permission and if TTS is true or the thread is locked.
/// Thrown when the thread does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public new Task SendMessageAsync(DiscordMessageBuilder builder)
=> this.Discord.ApiClient.CreateMessageAsync(this.Id, builder);
///
/// Sends a message to this channel.
///
/// The builder with all the items to send.
/// The sent message.
/// Thrown when the client does not have the permission and if TTS is true or the thread is locked.
/// Thrown when the thread does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public new Task SendMessageAsync(Action action)
{
var builder = new DiscordMessageBuilder();
action(builder);
- return this.Discord.ApiClient.CreateMessageAsync(this.Id, builder);
+ return !this.IsWriteable()
+ ? throw new ArgumentException("Cannot send a text message to a non-text channel.")
+ : this.Discord.ApiClient.CreateMessageAsync(this.Id, builder);
}
///
/// Returns a specific message
///
/// The id of the message
/// Thrown when the client does not have the permission and if TTS is true or the thread is locked.
/// Thrown when the thread does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public new async Task GetMessageAsync(ulong id)
{
return this.Discord.Configuration.MessageCacheSize > 0
&& this.Discord is DiscordClient dc
&& dc.MessageCache != null
&& dc.MessageCache.TryGet(xm => xm.Id == id && xm.ChannelId == this.Id, out var msg)
? msg
: await this.Discord.ApiClient.GetMessageAsync(this.Id, id).ConfigureAwait(false);
}
///
/// Returns a list of messages before a certain message.
/// The amount of messages to fetch.
/// Message to fetch before from.
///
/// Thrown when the client does not have the or the permission.
/// Thrown when the thread does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public new Task> GetMessagesBeforeAsync(ulong before, int limit = 100)
=> this.GetMessagesInternalAsync(limit, before, null, null);
///
/// Returns a list of messages after a certain message.
/// The amount of messages to fetch.
/// Message to fetch after from.
///
/// Thrown when the client does not have the or the permission.
/// Thrown when the thread does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public new Task> GetMessagesAfterAsync(ulong after, int limit = 100)
=> this.GetMessagesInternalAsync(limit, null, after, null);
///
/// Returns a list of messages around a certain message.
/// The amount of messages to fetch.
/// Message to fetch around from.
///
/// Thrown when the client does not have the or the permission.
/// Thrown when the thread does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public new Task> GetMessagesAroundAsync(ulong around, int limit = 100)
=> this.GetMessagesInternalAsync(limit, null, null, around);
///
/// Returns a list of messages from the last message in the thread.
/// The amount of messages to fetch.
///
/// Thrown when the client does not have the or the permission.
/// Thrown when the thread does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public new Task> GetMessagesAsync(int limit = 100) =>
this.GetMessagesInternalAsync(limit, null, null, null);
///
/// Returns a list of messages
///
/// How many messages should be returned.
/// Get messages before snowflake.
/// Get messages after snowflake.
/// Get messages around snowflake.
private async Task> GetMessagesInternalAsync(int limit = 100, ulong? before = null, ulong? after = null, ulong? around = null)
{
if (this.Type != ChannelType.PublicThread && this.Type != ChannelType.PrivateThread && this.Type != ChannelType.NewsThread)
throw new ArgumentException("Cannot get the messages of a non-thread channel.");
if (limit < 0)
throw new ArgumentException("Cannot get a negative number of messages.");
if (limit == 0)
return Array.Empty();
//return this.Discord.ApiClient.GetChannelMessagesAsync(this.Id, limit, before, after, around);
if (limit > 100 && around != null)
throw new InvalidOperationException("Cannot get more than 100 messages around the specified ID.");
var msgs = new List(limit);
var remaining = limit;
ulong? last = null;
var isAfter = after != null;
int lastCount;
do
{
var fetchSize = remaining > 100 ? 100 : remaining;
var fetch = await this.Discord.ApiClient.GetChannelMessagesAsync(this.Id, fetchSize, !isAfter ? last ?? before : null, isAfter ? last ?? after : null, around).ConfigureAwait(false);
lastCount = fetch.Count;
remaining -= lastCount;
if (!isAfter)
{
msgs.AddRange(fetch);
last = fetch.LastOrDefault()?.Id;
}
else
{
msgs.InsertRange(0, fetch);
last = fetch.FirstOrDefault()?.Id;
}
}
while (remaining > 0 && lastCount > 0);
return new ReadOnlyCollection(msgs);
}
///
/// Deletes multiple messages if they are less than 14 days old. If they are older, none of the messages will be deleted and you will receive a error.
///
/// A collection of messages to delete.
/// Reason for audit logs.
/// Thrown when the client does not have the permission.
/// Thrown when the thread does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public new async Task DeleteMessagesAsync(IEnumerable messages, string reason = null)
{
// don't enumerate more than once
var msgs = messages.Where(x => x.Channel.Id == this.Id).Select(x => x.Id).ToArray();
if (messages == null || !msgs.Any())
throw new ArgumentException("You need to specify at least one message to delete.");
if (msgs.Count() < 2)
{
await this.Discord.ApiClient.DeleteMessageAsync(this.Id, msgs.Single(), reason).ConfigureAwait(false);
return;
}
for (var i = 0; i < msgs.Count(); i += 100)
await this.Discord.ApiClient.DeleteMessagesAsync(this.Id, msgs.Skip(i).Take(100), reason).ConfigureAwait(false);
}
///
/// Deletes a message
///
/// The message to be deleted.
/// Reason for audit logs.
/// Thrown when the client does not have the permission.
/// Thrown when the thread does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public new Task DeleteMessageAsync(DiscordMessage message, string reason = null)
=> this.Discord.ApiClient.DeleteMessageAsync(this.Id, message.Id, reason);
///
/// Post a typing indicator
///
/// Thrown when the thread does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public new Task TriggerTypingAsync()
{
return this.Type != ChannelType.PublicThread && this.Type != ChannelType.PrivateThread && this.Type != ChannelType.NewsThread
? throw new ArgumentException("Cannot start typing in a non-text channel.")
: this.Discord.ApiClient.TriggerTypingAsync(this.Id);
}
///
/// Returns all pinned messages
///
/// Thrown when the client does not have the permission or the client is missing .
/// Thrown when the thread does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public new Task> GetPinnedMessagesAsync()
{
return this.Type != ChannelType.PublicThread && this.Type != ChannelType.PrivateThread&& this.Type != ChannelType.News
? throw new ArgumentException("A non-thread channel does not have pinned messages.")
: this.Discord.ApiClient.GetPinnedMessagesAsync(this.Id);
}
///
/// Returns a string representation of this thread.
///
/// String representation of this thread.
public override string ToString()
{
var threadchannel = (object)this.Type switch
{
ChannelType.NewsThread => $"News thread {this.Name} ({this.Id})",
ChannelType.PublicThread => $"Thread {this.Name} ({this.Id})",
ChannelType.PrivateThread => $"Private thread {this.Name} ({this.Id})",
_ => $"Thread {this.Name} ({this.Id})",
};
return threadchannel;
}
#endregion
///
/// 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 DiscordThreadChannel);
///
/// Checks whether this is equal to another .
///
/// to compare to.
/// Whether the is equal to this .
public bool Equals(DiscordThreadChannel e) => e is not null && (ReferenceEquals(this, e) || this.Id == e.Id);
///
/// 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 channel to compare.
/// Second channel to compare.
/// Whether the two channels are equal.
public static bool operator ==(DiscordThreadChannel e1, DiscordThreadChannel 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 channel to compare.
/// Second channel to compare.
/// Whether the two channels are not equal.
public static bool operator !=(DiscordThreadChannel e1, DiscordThreadChannel e2)
=> !(e1 == e2);
}
}
diff --git a/DisCatSharp/Entities/User/DiscordTeam.cs b/DisCatSharp/Entities/User/DiscordTeam.cs
index 6cfa6ec83..7a51a33d8 100644
--- a/DisCatSharp/Entities/User/DiscordTeam.cs
+++ b/DisCatSharp/Entities/User/DiscordTeam.cs
@@ -1,203 +1,197 @@
// This file is part of the DisCatSharp project.
//
// Copyright (c) 2021 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 DisCatSharp.Enums;
using DisCatSharp.Net;
using DisCatSharp.Net.Abstractions;
namespace DisCatSharp.Entities
{
///
/// Represents a team consisting of users. A team can own an application.
///
public sealed class DiscordTeam : SnowflakeObject, IEquatable
{
///
/// Gets the team's name.
///
public string Name { get; internal set; }
///
/// Gets the team's icon hash.
///
public string IconHash { get; internal set; }
///
/// Gets the team's icon.
///
public string Icon
=> !string.IsNullOrWhiteSpace(this.IconHash) ? $"{DiscordDomain.GetDomain(CoreDomain.DiscordCdn).Url}{Endpoints.TEAM_ICONS}/{this.Id.ToString(CultureInfo.InvariantCulture)}/{this.IconHash}.png?size=1024" : null;
///
/// Gets the owner of the team.
///
public DiscordUser Owner { get; internal set; }
///
/// Gets the members of this team.
///
public IReadOnlyList Members { get; internal set; }
///
/// Initializes a new instance of the class.
///
/// The tt.
internal DiscordTeam(TransportTeam tt)
{
this.Id = tt.Id;
this.Name = tt.Name;
this.IconHash = tt.IconHash;
}
///
/// Compares this team to another object and returns whether they are equal.
///
/// Object to compare this team to.
/// Whether this team is equal to the given object.
public override bool Equals(object obj)
=> obj is DiscordTeam other && this == other;
///
/// Compares this team to another team and returns whether they are equal.
///
/// Team to compare to.
/// Whether the teams are equal.
public bool Equals(DiscordTeam other)
=> this == other;
///
/// Gets the hash code of this team.
///
/// Hash code of this team.
public override int GetHashCode()
=> this.Id.GetHashCode();
///
/// Converts this team to its string representation.
///
/// The string representation of this team.
public override string ToString()
=> $"Team: {this.Name} ({this.Id})";
public static bool operator ==(DiscordTeam left, DiscordTeam right)
=> left?.Id == right?.Id;
public static bool operator !=(DiscordTeam left, DiscordTeam right)
=> left?.Id != right?.Id;
}
///
/// Represents a member of .
///
public sealed class DiscordTeamMember : IEquatable
{
///
/// Gets the member's membership status.
///
public DiscordTeamMembershipStatus MembershipStatus { get; internal set; }
///
/// Gets the member's permissions within the team.
///
public IReadOnlyCollection Permissions { get; internal set; }
///
/// Gets the team this member belongs to.
///
public DiscordTeam Team { get; internal set; }
///
/// Gets the user who is the team member.
///
public DiscordUser User { get; internal set; }
///
/// Initializes a new instance of the class.
///
/// The ttm.
internal DiscordTeamMember(TransportTeamMember ttm)
{
this.MembershipStatus = (DiscordTeamMembershipStatus)ttm.MembershipState;
this.Permissions = new ReadOnlySet(new HashSet(ttm.Permissions));
}
///
/// Compares this team member to another object and returns whether they are equal.
///
/// Object to compare to.
/// Whether this team is equal to given object.
public override bool Equals(object obj)
=> obj is DiscordTeamMember other && this == other;
///
/// Compares this team member to another team member and returns whether they are equal.
///
/// Team member to compare to.
/// Whether this team member is equal to the given one.
public bool Equals(DiscordTeamMember other)
=> this == other;
///
/// Gets a hash code of this team member.
///
/// Hash code of this team member.
- public override int GetHashCode()
- {
- var hash = 13;
- hash = (hash * 7) + this.User.GetHashCode();
- hash = (hash * 7) + this.Team.GetHashCode();
- return hash;
- }
+ public override int GetHashCode() => HashCode.Combine(this.User, this.Team);
///
/// Converts this team member to their string representation.
///
/// String representation of this team member.
public override string ToString()
=> $"Team member: {this.User.Username}#{this.User.Discriminator} ({this.User.Id}), part of team {this.Team.Name} ({this.Team.Id})";
public static bool operator ==(DiscordTeamMember left, DiscordTeamMember right)
=> left?.Team?.Id == right?.Team?.Id && left?.User?.Id == right?.User?.Id;
public static bool operator !=(DiscordTeamMember left, DiscordTeamMember right)
=> left?.Team?.Id != right?.Team?.Id || left?.User?.Id != right?.User?.Id;
}
///
/// Signifies the status of user's team membership.
///
public enum DiscordTeamMembershipStatus : int
{
///
/// Indicates that this user is invited to the team, and is pending membership.
///
Invited = 1,
///
/// Indicates that this user is a member of the team.
///
Accepted = 2
}
}
diff --git a/DisCatSharp/GlobalSuppressions.cs b/DisCatSharp/GlobalSuppressions.cs
index d7a572b96..61cb29272 100644
--- a/DisCatSharp/GlobalSuppressions.cs
+++ b/DisCatSharp/GlobalSuppressions.cs
@@ -1,34 +1,36 @@
// This file is part of the DisCatSharp project.
//
// Copyright (c) 2021 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.Diagnostics.CodeAnalysis;
[assembly: SuppressMessage("Style", "IDE0046:Convert to conditional expression", Justification = "", Scope = "member", Target = "~P:DisCatSharp.Net.Abstractions.ClientProperties.OperatingSystem")]
[assembly: SuppressMessage("CodeQuality", "IDE0079:Remove unnecessary suppression", Justification = "")]
[assembly: SuppressMessage("Style", "IDE1006:Naming Styles", Justification = "", Scope = "member", Target = "~F:DisCatSharp.Entities.DiscordUnicodeEmoji._1SkinTone1")]
[assembly: SuppressMessage("Style", "IDE1006:Naming Styles", Justification = "", Scope = "member", Target = "~F:DisCatSharp.Entities.DiscordUnicodeEmoji._1SkinTone2")]
[assembly: SuppressMessage("Style", "IDE1006:Naming Styles", Justification = "", Scope = "member", Target = "~F:DisCatSharp.Entities.DiscordUnicodeEmoji._1SkinTone3")]
[assembly: SuppressMessage("Style", "IDE1006:Naming Styles", Justification = "", Scope = "member", Target = "~F:DisCatSharp.Entities.DiscordUnicodeEmoji._1SkinTone4")]
[assembly: SuppressMessage("Style", "IDE1006:Naming Styles", Justification = "", Scope = "member", Target = "~F:DisCatSharp.Entities.DiscordUnicodeEmoji._1SkinTone5")]
[assembly: SuppressMessage("Style", "IDE1006:Naming Styles", Justification = "", Scope = "member", Target = "~F:DisCatSharp.Entities.DiscordUnicodeEmoji._8ball")]
[assembly: SuppressMessage("Style", "IDE1006:Naming Styles", Justification = "", Scope = "member", Target = "~F:DisCatSharp.Entities.DiscordColor.HexAlphabet")]
[assembly: SuppressMessage("Style", "IDE0044:Add readonly modifier", Justification = "", Scope = "member", Target = "~F:DisCatSharp.Entities.DiscordColor.HexAlphabet")]
+[assembly: SuppressMessage("CodeQuality", "IDE0051:Remove unused private members", Justification = "", Scope = "member", Target = "~P:DisCatSharp.Internals.PermissionStrings")]
+[assembly: SuppressMessage("CodeQuality", "IDE0051:Remove unused private members", Justification = "", Scope = "member", Target = "~P:DisCatSharp.Internals.VersionHeader")]
diff --git a/DisCatSharp/Internals.cs b/DisCatSharp/Internals.cs
new file mode 100644
index 000000000..2be336bc0
--- /dev/null
+++ b/DisCatSharp/Internals.cs
@@ -0,0 +1,101 @@
+// This file is part of the DisCatSharp project, a fork of DSharpPlus.
+//
+// Copyright (c) 2021 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 Microsoft.Extensions.Logging;
+
+namespace DisCatSharp
+{
+ ///
+ /// Internal tools.
+ ///
+ public static class Internals
+ {
+ ///
+ /// Gets the version of the library
+ ///
+ private static string VersionHeader
+ => Utilities.VersionHeader;
+
+ ///
+ /// Gets the permission strings.
+ ///
+ private static Dictionary PermissionStrings
+ => Utilities.PermissionStrings;
+
+ ///
+ /// Gets the utf8 encoding
+ ///
+ internal static UTF8Encoding UTF8
+ => Utilities.UTF8;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ static Internals() { }
+
+ ///
+ /// Whether the is joinable via voice.
+ ///
+ /// The channel.
+ internal static bool IsVoiceJoinable(this DiscordChannel channel) => channel.Type == ChannelType.Voice || channel.Type == ChannelType.Stage;
+
+ ///
+ /// Whether the is related to threads.
+ ///
+ /// The channel.
+ internal static bool IsThreadHolder(this DiscordChannel channel) => channel.Type == ChannelType.Text || channel.Type == ChannelType.News || channel.Type == ChannelType.GuildForum;
+
+ ///
+ /// Whether the is related to threads.
+ ///
+ /// The channel.
+ internal static bool IsThread(this DiscordChannel channel) => channel.Type == ChannelType.PublicThread || channel.Type == ChannelType.PrivateThread || channel.Type == ChannelType.NewsThread;
+
+ ///
+ /// Whether users can write the .
+ ///
+ /// The channel.
+ internal static bool IsWriteable(this DiscordChannel channel) => channel.Type == ChannelType.PublicThread || channel.Type == ChannelType.PrivateThread || channel.Type == ChannelType.NewsThread || channel.Type == ChannelType.Text || channel.Type == ChannelType.News || channel.Type == ChannelType.Group || channel.Type == ChannelType.Private || channel.Type == ChannelType.Voice;
+
+ ///
+ /// Whether the is moveable in a parent.
+ ///
+ /// The channel.
+ internal static bool IsMovableInParent(this DiscordChannel channel) => channel.Type == ChannelType.Voice || channel.Type == ChannelType.Stage || channel.Type == ChannelType.Text || channel.Type == ChannelType.GuildForum || channel.Type == ChannelType.News || channel.Type == ChannelType.Store;
+
+ ///
+ /// Whether the is moveable in a parent.
+ ///
+ /// The channel.
+ internal static bool IsMovable (this DiscordChannel channel) => channel.Type == ChannelType.Voice || channel.Type == ChannelType.Stage || channel.Type == ChannelType.Text || channel.Type == ChannelType.Category || channel.Type == ChannelType.GuildForum || channel.Type == ChannelType.News || channel.Type == ChannelType.Store;
+ }
+}
diff --git a/DisCatSharp/Utilities.cs b/DisCatSharp/Utilities.cs
index b71af6e5f..a5891168c 100644
--- a/DisCatSharp/Utilities.cs
+++ b/DisCatSharp/Utilities.cs
@@ -1,465 +1,465 @@
// This file is part of the DisCatSharp project.
//
// Copyright (c) 2021 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.Net;
using Microsoft.Extensions.Logging;
namespace DisCatSharp
{
///
/// Various Discord-related utilities.
///
public static class Utilities
{
///
/// Gets the version of the library
///
- private static string VersionHeader { get; set; }
+ internal static string VersionHeader { get; set; }
///
/// Gets or sets the permission strings.
///
- private static Dictionary PermissionStrings { get; set; }
+ internal static Dictionary PermissionStrings { get; set; }
///
/// Gets the utf8 encoding
///
internal static UTF8Encoding UTF8 { get; } = new UTF8Encoding(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)
{
return config.TokenType switch
{
TokenType.Bearer => $"Bearer {config.Token}",
TokenType.Bot => $"Bot {config.Token}",
_ => throw new ArgumentException("Invalid token type specified.", nameof(config.Token)),
};
}
///
/// 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);
foreach (Match match in matches)
yield return 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);
foreach (Match match in matches)
yield return 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);
foreach (Match match in matches)
yield return 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);
foreach (Match match in matches)
yield return 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}$", RegexOptions.ECMAScript);
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)
{
return taad == ThreadAutoArchiveDuration.ThreeDays
? (guild.PremiumTier.HasFlag(PremiumTier.Tier_1) || guild.Features.CanSetThreadArchiveDurationThreeDays)
: taad != ThreadAutoArchiveDuration.OneWeek || guild.PremiumTier.HasFlag(PremiumTier.Tier_2) || guild.Features.CanSetThreadArchiveDurationSevenDays;
}
///
/// Checks the thread private feature.
///
/// The guild.
/// A bool.
internal static bool CheckThreadPrivateFeature(DiscordGuild guild) => guild.PremiumTier.HasFlag(PremiumTier.Tier_2) || guild.Features.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 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);
///
/// 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.FULL_PERMS;
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;
}
}
}