diff --git a/DisCatSharp/Entities/Channel/DiscordChannel.cs b/DisCatSharp/Entities/Channel/DiscordChannel.cs
index ac2921048..d816f4ace 100644
--- a/DisCatSharp/Entities/Channel/DiscordChannel.cs
+++ b/DisCatSharp/Entities/Channel/DiscordChannel.cs
@@ -1,1405 +1,1405 @@
// 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.Enums;
using DisCatSharp.Exceptions;
using DisCatSharp.Net;
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 this channels's banner hash, when applicable.
///
[JsonProperty("banner")]
public string BannerHash { get; internal set; }
///
/// Gets this channels's banner in url form.
///
[JsonIgnore]
public string BannerUrl
=> !string.IsNullOrWhiteSpace(this.BannerHash) ? $"{DiscordDomain.GetDomain(CoreDomain.DiscordCdn).Uri}{Endpoints.CHANNELS}/{this.Id.ToString(CultureInfo.InvariantCulture)}{Endpoints.BANNERS}/{this.BannerHash}.{(this.BannerHash.StartsWith("a_") ? "gif" : "png")}" : null;
///
/// 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.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.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.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.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.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.IsVoiceJoinable())
{
bitrate = null;
userLimit = null;
}
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")}.");
}
if (mdl.Banner.HasValue)
{
if (!this.Guild.Features.CanSetChannelBanner)
throw new NotSupportedException($"Cannot modify Banner. Guild needs boost tier three.");
}
var bannerb64 = Optional.FromNoValue();
if (mdl.Banner.HasValue && mdl.Banner.Value != null)
using (var imgtool = new ImageTool(mdl.Banner.Value))
bannerb64 = imgtool.GetBase64();
else if (mdl.Banner.HasValue)
bannerb64 = null;
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, bannerb64, 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.
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.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.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.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 should be temporary. Defaults to false.
/// Whether this invite should be unique. Defaults to false.
/// The target type. Defaults to null.
/// The target activity. Defaults to null.
/// The target user id. Defaults to null.
/// The 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 Task CreateInviteAsync(int max_age = 86400, int max_uses = 0, bool temporary = false, bool unique = false, TargetType? target_type = null, TargetActivity? target_application = null, ulong? target_user = null, string reason = null)
=> this.Discord.ApiClient.CreateChannelInviteAsync(this.Id, max_age, max_uses, target_type, target_application, target_user, 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.GuildOnly, 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 .
/// The per user ratelimit, aka slowdown.
/// 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, int? rate_limit_per_user = null, string reason = null)
{
return (type != ChannelType.NewsThread && type != ChannelType.PublicThread && type != ChannelType.PrivateThread)
? throw new NotSupportedException("Wrong thread type given.")
: (!this.IsThreadHolder())
- ? throw new NotSupportedException("Parent channel can't have threads")
+ ? 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, rate_limit_per_user, 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, rate_limit_per_user, reason)
: throw new NotSupportedException($"Cannot modify ThreadAutoArchiveDuration. Guild needs boost tier {(auto_archive_duration == ThreadAutoArchiveDuration.ThreeDays ? "one" : "two")}.");
}
///
/// Creates a scheduled event based on the channel type.
///
/// The name.
/// The privacy level.
/// The scheduled start time.
/// The description.
/// The metadata.
/// The reason.
/// A scheduled event.
/// Thrown when the ressource does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public async Task CreateScheduledEventAsync(string name, ScheduledEventPrivacyLevel privacy_level, DateTimeOffset scheduled_start_time, string description = null, DiscordScheduledEventEntityMetadata metadata = null, string reason = null)
{
- if (this.Type != ChannelType.Voice && this.Type != ChannelType.Stage)
+ if (!this.IsVoiceJoinable())
throw new NotSupportedException("Cannot create a scheduled event for this type of channel. Channel type must be either voice or stage.");
var type = this.Type == ChannelType.Voice ? ScheduledEventEntityType.Voice : ScheduledEventEntityType.StageInstance;
return await this.Guild.CreateScheduledEventAsync(name, privacy_level, scheduled_start_time, null, this, metadata, description, type, reason);
}
///
/// 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.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.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.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.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/Guild/ScheduledEvent/DiscordScheduledEvent.cs b/DisCatSharp/Entities/Guild/ScheduledEvent/DiscordScheduledEvent.cs
index fa7136676..4ac92b314 100644
--- a/DisCatSharp/Entities/Guild/ScheduledEvent/DiscordScheduledEvent.cs
+++ b/DisCatSharp/Entities/Guild/ScheduledEvent/DiscordScheduledEvent.cs
@@ -1,322 +1,320 @@
// 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.Threading.Tasks;
using DisCatSharp.Net.Models;
using Newtonsoft.Json;
namespace DisCatSharp.Entities
{
///
/// Represents an scheduled event.
///
public class DiscordScheduledEvent : SnowflakeObject, IEquatable
{
///
/// Gets the guild id of the associated scheduled event.
///
[JsonProperty("guild_id", NullValueHandling = NullValueHandling.Ignore)]
public ulong GuildId { get; internal set; }
///
/// Gets the guild to which this scheduled event belongs.
///
[JsonIgnore]
public DiscordGuild Guild
=> this.Discord.Guilds.TryGetValue(this.GuildId, out var guild) ? guild : null;
///
/// Gets the associated channel.
///
[JsonIgnore]
public Task Channel
=> this.ChannelId.HasValue ? this.Discord.ApiClient.GetChannelAsync(this.ChannelId.Value) : null;
///
/// Gets id of the associated channel id.
///
[JsonProperty("channel_id", NullValueHandling = NullValueHandling.Ignore)]
public ulong? ChannelId { get; internal set; }
///
/// Gets the ID of the user that created the scheduled event.
///
[JsonProperty("creator_id", NullValueHandling = NullValueHandling.Ignore)]
public ulong CreatorId { get; internal set; }
///
/// Gets the member that created the scheduled event.
///
[JsonIgnore]
public DiscordMember CreatorMember
=> this.Guild._members.TryGetValue(this.CreatorId, out var owner)
? owner
: this.Discord.ApiClient.GetGuildMemberAsync(this.GuildId, this.CreatorId).Result;
///
/// Gets the name of the scheduled event.
///
[JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)]
public string Name { get; internal set; }
///
/// Gets the description of the scheduled event.
///
[JsonProperty("description", NullValueHandling = NullValueHandling.Ignore)]
public string Description { get; internal set; }
/* TODO|INFO: Is not available yet to users / clients / bots.
///
/// Gets this event's cover hash, when applicable.
///
[JsonProperty("image", NullValueHandling = NullValueHandling.Ignore)]
public string ImageHash { get; internal set; }
///
/// Gets this event's cover in url form.
///
[JsonIgnore]
public string ImageUrl
=> !string.IsNullOrWhiteSpace(this.ImageHash) ? $"{DiscordDomain.GetDomain(CoreDomain.DiscordCdn).Uri}{Endpoints.EVENTS}/{this.GuildId.ToString(CultureInfo.InvariantCulture)}/images/{this.ImageHash}.{(this.ImageHash.StartsWith("a_") ? "gif" : "png")}" : null;
*/
///
/// Gets the scheduled start time of the scheduled event.
///
[JsonIgnore]
public DateTimeOffset? ScheduledStartTime
=> !string.IsNullOrWhiteSpace(this.ScheduledStartTimeRaw) && DateTimeOffset.TryParse(this.ScheduledStartTimeRaw, CultureInfo.InvariantCulture, DateTimeStyles.None, out var dto) ?
dto : null;
///
/// Gets the scheduled start time of the scheduled event as raw string.
///
[JsonProperty("scheduled_start_time", NullValueHandling = NullValueHandling.Ignore)]
internal string ScheduledStartTimeRaw { get; set; }
///
/// Gets the scheduled end time of the scheduled event.
///
[JsonIgnore]
public DateTimeOffset? ScheduledEndTime
=> !string.IsNullOrWhiteSpace(this.ScheduledEndTimeRaw) && DateTimeOffset.TryParse(this.ScheduledEndTimeRaw, CultureInfo.InvariantCulture, DateTimeStyles.None, out var dto) ?
dto : null;
///
/// Gets the scheduled end time of the scheduled event as raw string.
///
[JsonProperty("scheduled_end_time", NullValueHandling = NullValueHandling.Ignore)]
internal string ScheduledEndTimeRaw { get; set; }
///
/// Gets the privacy level of the scheduled event.
///
[JsonProperty("privacy_level", NullValueHandling = NullValueHandling.Ignore)]
public ScheduledEventPrivacyLevel PrivacyLevel { get; internal set; }
///
/// Gets the status of the scheduled event.
///
[JsonProperty("status", NullValueHandling = NullValueHandling.Ignore)]
public ScheduledEventStatus Status { get; internal set; }
///
/// Gets the entity type.
///
[JsonProperty("entity_type", NullValueHandling = NullValueHandling.Ignore)]
public ScheduledEventEntityType EntityType { get; internal set; }
///
/// Gets id of the entity.
///
[JsonProperty("entity_id", NullValueHandling = NullValueHandling.Ignore)]
public ulong? EntityId { get; internal set; }
///
/// Gets metadata of the entity.
///
[JsonProperty("entity_metadata", NullValueHandling = NullValueHandling.Ignore)]
public DiscordScheduledEventEntityMetadata EntityMetadata { get; internal set; }
/* This isn't used.
* See https://github.com/discord/discord-api-docs/pull/3586#issuecomment-969066061.
* Was originally for paid stages.
///
/// Gets the sku ids of the scheduled event.
///
[JsonProperty("sku_ids", NullValueHandling = NullValueHandling.Ignore)]
public IReadOnlyList SkuIds { get; internal set; }*/
///
/// Gets the user that created the scheduled event.
///
[JsonProperty("creator", NullValueHandling = NullValueHandling.Ignore)]
public DiscordUser Creator { get; internal set; }
- // TODO: Skus
-
///
/// Gets the total number of users subscribed to the scheduled event.
///
[JsonProperty("user_count", NullValueHandling = NullValueHandling.Ignore)]
public int UserCount { get; internal set; }
///
/// Initializes a new instance of the class.
///
internal DiscordScheduledEvent() { }
#region Methods
///
/// Modifies the current scheduled event.
///
/// Action to perform on this thread
/// Thrown when the client does not have the permission.
/// Thrown when the event does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public async Task ModifyAsync(Action action)
{
var mdl = new ScheduledEventEditModel();
action(mdl);
var channelId = Optional.FromNoValue();
if (mdl.Channel.HasValue && (mdl.Channel.Value.Type != ChannelType.Voice || mdl.Channel.Value.Type != ChannelType.Stage) && mdl.Channel.Value != null)
throw new ArgumentException("Channel needs to be a voice or stage channel.");
else if (mdl.Channel.HasValue && mdl.Channel.Value != null)
channelId = mdl.Channel.Value.Id;
var description = Optional.FromNoValue();
if (mdl.Description.HasValue && mdl.Description.Value != null)
description = mdl.Description;
else if (mdl.Description.HasValue)
description = null;
var scheduledEndTime = Optional.FromNoValue();
if (mdl.ScheduledEndTime.HasValue && mdl.ScheduledEndTime.Value != null && mdl.EntityType.HasValue ? mdl.EntityType == ScheduledEventEntityType.External : this.EntityType == ScheduledEventEntityType.External)
scheduledEndTime = mdl.ScheduledEndTime.Value;
await this.Discord.ApiClient.ModifyGuildScheduledEventAsync(this.GuildId, this.Id, channelId, scheduledEndTime.HasValue ? new DiscordScheduledEventEntityMetadata(null, mdl.Location.Value) : null, mdl.Name, mdl.PrivacyLevel, mdl.ScheduledStartTime, scheduledEndTime, description, mdl.EntityType, mdl.Status, mdl.AuditLogReason);
}
///
/// Starts the current scheduled event.
///
/// Thrown when the client does not have the permission.
/// Thrown when the event does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public async Task StartAsync(string reason = null)
=> await this.Discord.ApiClient.ModifyGuildScheduledEventStatusAsync(this.GuildId, this.Id, ScheduledEventStatus.Active, reason);
///
/// Cancels the current scheduled event.
///
/// Thrown when the client does not have the permission.
/// Thrown when the event does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public async Task CancelAsync(string reason = null)
=> await this.Discord.ApiClient.ModifyGuildScheduledEventStatusAsync(this.GuildId, this.Id, ScheduledEventStatus.Canceled, reason);
///
/// Ends the current scheduled event.
///
/// Thrown when the client does not have the permission.
/// Thrown when the event does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public async Task EndAsync(string reason = null)
=> await this.Discord.ApiClient.ModifyGuildScheduledEventStatusAsync(this.GuildId, this.Id, ScheduledEventStatus.Completed, reason);
///
/// Gets a list of users RSVP'd to the scheduled event.
///
/// The limit how many users to receive from the event.
/// Wether to include guild member data.
/// Thrown when the client does not have the correct permissions.
/// Thrown when the event does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public async Task> GetUsersAsync(int? limit = null, bool? with_member = null)
=> await this.Discord.ApiClient.GetGuildScheduledEventRSPVUsersAsync(this.GuildId, this.Id, limit, with_member);
///
/// Deletes a scheduled event.
///
/// Audit log reason
/// Thrown when the client does not have the permission.
/// Thrown when the event does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public async Task DeleteAsync(string reason = null)
=> await this.Discord.ApiClient.DeleteGuildScheduledEventAsync(this.GuildId ,this.Id, reason);
#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 DiscordScheduledEvent);
///
/// Checks whether this is equal to another .
///
/// to compare to.
/// Whether the is equal to this .
public bool Equals(DiscordScheduledEvent 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 event to compare.
/// Second ecent to compare.
/// Whether the two events are equal.
public static bool operator ==(DiscordScheduledEvent e1, DiscordScheduledEvent 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 event to compare.
/// Second event to compare.
/// Whether the two events are not equal.
public static bool operator !=(DiscordScheduledEvent e1, DiscordScheduledEvent e2)
=> !(e1 == e2);
}
}