diff --git a/DisCatSharp.ApplicationCommands/Attributes/SlashCommand/ChannelTypesAttribute.cs b/DisCatSharp.ApplicationCommands/Attributes/SlashCommand/ChannelTypesAttribute.cs
index 8060f26b4..4f0eb6780 100644
--- a/DisCatSharp.ApplicationCommands/Attributes/SlashCommand/ChannelTypesAttribute.cs
+++ b/DisCatSharp.ApplicationCommands/Attributes/SlashCommand/ChannelTypesAttribute.cs
@@ -1,47 +1,49 @@
// This file is part of the DisCatSharp project, based off DSharpPlus.
//
// Copyright (c) 2021-2022 AITSYS
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
using System;
using System.Collections.Generic;
+using DisCatSharp.Enums;
+
namespace DisCatSharp.ApplicationCommands.Attributes;
///
/// Defines allowed channel types for a channel parameter.
///
[AttributeUsage(AttributeTargets.Parameter)]
public class ChannelTypesAttribute : Attribute
{
///
/// Allowed channel types.
///
public IEnumerable ChannelTypes { get; }
///
/// Defines allowed channel types for a channel parameter.
///
/// The channel types to allow.
public ChannelTypesAttribute(params ChannelType[] channelTypes)
{
this.ChannelTypes = channelTypes;
}
}
diff --git a/DisCatSharp.Lavalink/DiscordClientExtensions.cs b/DisCatSharp.Lavalink/DiscordClientExtensions.cs
index c30f9038e..75a21d526 100644
--- a/DisCatSharp.Lavalink/DiscordClientExtensions.cs
+++ b/DisCatSharp.Lavalink/DiscordClientExtensions.cs
@@ -1,131 +1,132 @@
// This file is part of the DisCatSharp project, based off DSharpPlus.
//
// Copyright (c) 2021-2022 AITSYS
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Threading.Tasks;
using DisCatSharp.Entities;
+using DisCatSharp.Enums;
using Microsoft.Extensions.Logging;
namespace DisCatSharp.Lavalink;
///
/// The discord client extensions.
///
public static class DiscordClientExtensions
{
///
/// Creates a new Lavalink client with specified settings.
///
/// Discord client to create Lavalink instance for.
/// Lavalink client instance.
public static LavalinkExtension UseLavalink(this DiscordClient client)
{
if (client.GetExtension() != null)
throw new InvalidOperationException("Lavalink is already enabled for that client.");
if (!client.Configuration.Intents.HasIntent(DiscordIntents.GuildVoiceStates))
client.Logger.LogCritical(LavalinkEvents.Intents, "The Lavalink extension is registered but the guild voice states intent is not enabled. It is highly recommended to enable it.");
var lava = new LavalinkExtension();
client.AddExtension(lava);
return lava;
}
///
/// Creates new Lavalink clients on all shards in a given sharded client.
///
/// Discord sharded client to create Lavalink instances for.
/// A dictionary of created Lavalink clients.
public static async Task> UseLavalinkAsync(this DiscordShardedClient client)
{
var modules = new Dictionary();
await client.InitializeShardsAsync().ConfigureAwait(false);
foreach (var shard in client.ShardClients.Select(xkvp => xkvp.Value))
{
var lava = shard.GetExtension();
if (lava == null)
lava = shard.UseLavalink();
modules[shard.ShardId] = lava;
}
return new ReadOnlyDictionary(modules);
}
///
/// Gets the active instance of the Lavalink client for the DiscordClient.
///
/// Discord client to get Lavalink instance for.
/// Lavalink client instance.
public static LavalinkExtension GetLavalink(this DiscordClient client)
=> client.GetExtension();
///
/// Retrieves a instance for each shard.
///
/// The shard client to retrieve instances from.
/// A dictionary containing instances for each shard.
public static async Task> GetLavalinkAsync(this DiscordShardedClient client)
{
await client.InitializeShardsAsync().ConfigureAwait(false);
var extensions = new Dictionary();
foreach (var shard in client.ShardClients.Values)
{
extensions.Add(shard.ShardId, shard.GetExtension());
}
return new ReadOnlyDictionary(extensions);
}
///
/// Connects to this voice channel using Lavalink.
///
/// Channel to connect to.
/// Lavalink node to connect through.
/// If successful, the Lavalink client.
public static Task ConnectAsync(this DiscordChannel channel, LavalinkNodeConnection node)
{
if (channel == null)
throw new NullReferenceException();
if (channel.Guild == null)
throw new InvalidOperationException("Lavalink can only be used with guild channels.");
if (channel.Type != ChannelType.Voice && channel.Type != ChannelType.Stage)
throw new InvalidOperationException("You can only connect to voice and stage channels.");
if (channel.Discord is not DiscordClient discord || discord == null)
throw new NullReferenceException();
var lava = discord.GetLavalink();
return lava == null
? throw new InvalidOperationException("Lavalink is not initialized for this Discord client.")
: node.ConnectAsync(channel);
}
}
diff --git a/DisCatSharp.Lavalink/LavalinkNodeConnection.cs b/DisCatSharp.Lavalink/LavalinkNodeConnection.cs
index e0d0ff35c..8ac2d0180 100644
--- a/DisCatSharp.Lavalink/LavalinkNodeConnection.cs
+++ b/DisCatSharp.Lavalink/LavalinkNodeConnection.cs
@@ -1,625 +1,626 @@
// This file is part of the DisCatSharp project, based off DSharpPlus.
//
// Copyright (c) 2021-2022 AITSYS
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using DisCatSharp.Common.Utilities;
using DisCatSharp.Entities;
+using DisCatSharp.Enums;
using DisCatSharp.EventArgs;
using DisCatSharp.Lavalink.Entities;
using DisCatSharp.Lavalink.EventArgs;
using DisCatSharp.Net;
using DisCatSharp.Net.WebSocket;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace DisCatSharp.Lavalink;
internal delegate void NodeDisconnectedEventHandler(LavalinkNodeConnection node);
///
/// Represents a connection to a Lavalink node.
///
public sealed class LavalinkNodeConnection
{
///
/// Triggered whenever Lavalink WebSocket throws an exception.
///
public event AsyncEventHandler LavalinkSocketErrored
{
add => this._lavalinkSocketError.Register(value);
remove => this._lavalinkSocketError.Unregister(value);
}
private readonly AsyncEvent _lavalinkSocketError;
///
/// Triggered when this node disconnects.
///
public event AsyncEventHandler Disconnected
{
add => this._disconnected.Register(value);
remove => this._disconnected.Unregister(value);
}
private readonly AsyncEvent _disconnected;
///
/// Triggered when this node receives a statistics update.
///
public event AsyncEventHandler StatisticsReceived
{
add => this._statsReceived.Register(value);
remove => this._statsReceived.Unregister(value);
}
private readonly AsyncEvent _statsReceived;
///
/// Triggered whenever any of the players on this node is updated.
///
public event AsyncEventHandler PlayerUpdated
{
add => this._playerUpdated.Register(value);
remove => this._playerUpdated.Unregister(value);
}
private readonly AsyncEvent _playerUpdated;
///
/// Triggered whenever playback of a track starts.
/// This is only available for version 3.3.1 and greater.
///
public event AsyncEventHandler PlaybackStarted
{
add => this._playbackStarted.Register(value);
remove => this._playbackStarted.Unregister(value);
}
private readonly AsyncEvent _playbackStarted;
///
/// Triggered whenever playback of a track finishes.
///
public event AsyncEventHandler PlaybackFinished
{
add => this._playbackFinished.Register(value);
remove => this._playbackFinished.Unregister(value);
}
private readonly AsyncEvent _playbackFinished;
///
/// Triggered whenever playback of a track gets stuck.
///
public event AsyncEventHandler TrackStuck
{
add => this._trackStuck.Register(value);
remove => this._trackStuck.Unregister(value);
}
private readonly AsyncEvent _trackStuck;
///
/// Triggered whenever playback of a track encounters an error.
///
public event AsyncEventHandler TrackException
{
add => this._trackException.Register(value);
remove => this._trackException.Unregister(value);
}
private readonly AsyncEvent _trackException;
///
/// Gets the remote endpoint of this Lavalink node connection.
///
public ConnectionEndpoint NodeEndpoint => this.Configuration.SocketEndpoint;
///
/// Gets whether the client is connected to Lavalink.
///
public bool IsConnected => !Volatile.Read(ref this._isDisposed);
private bool _isDisposed;
private int _backoff;
///
/// The minimum backoff.
///
private const int MINIMUM_BACKOFF = 7500;
///
/// The maximum backoff.
///
private const int MAXIMUM_BACKOFF = 120000;
///
/// Gets the current resource usage statistics.
///
public LavalinkStatistics Statistics { get; }
///
/// Gets a dictionary of Lavalink guild connections for this node.
///
public IReadOnlyDictionary ConnectedGuilds { get; }
internal ConcurrentDictionary ConnectedGuildsInternal = new();
///
/// Gets the REST client for this Lavalink connection.
///
public LavalinkRestClient Rest { get; }
///
/// Gets the parent extension which this node connection belongs to.
///
public LavalinkExtension Parent { get; }
///
/// Gets the Discord client this node connection belongs to.
///
public DiscordClient Discord { get; }
///
/// Gets the configuration.
///
internal LavalinkConfiguration Configuration { get; }
///
/// Gets the region.
///
internal DiscordVoiceRegion Region { get; }
///
/// Gets or sets the web socket.
///
private IWebSocketClient _webSocket;
///
/// Gets the voice state updates.
///
private readonly ConcurrentDictionary> _voiceStateUpdates;
///
/// Gets the voice server updates.
///
private readonly ConcurrentDictionary> _voiceServerUpdates;
///
/// Initializes a new instance of the class.
///
/// The client.
/// the event.tension.
/// The config.
internal LavalinkNodeConnection(DiscordClient client, LavalinkExtension extension, LavalinkConfiguration config)
{
this.Discord = client;
this.Parent = extension;
this.Configuration = new LavalinkConfiguration(config);
if (config.Region != null && this.Discord.VoiceRegions.Values.Contains(config.Region))
this.Region = config.Region;
this.ConnectedGuilds = new ReadOnlyConcurrentDictionary(this.ConnectedGuildsInternal);
this.Statistics = new LavalinkStatistics();
this._lavalinkSocketError = new AsyncEvent("LAVALINK_SOCKET_ERROR", TimeSpan.Zero, this.Discord.EventErrorHandler);
this._disconnected = new AsyncEvent("LAVALINK_NODE_DISCONNECTED", TimeSpan.Zero, this.Discord.EventErrorHandler);
this._statsReceived = new AsyncEvent("LAVALINK_STATS_RECEIVED", TimeSpan.Zero, this.Discord.EventErrorHandler);
this._playerUpdated = new AsyncEvent("LAVALINK_PLAYER_UPDATED", TimeSpan.Zero, this.Discord.EventErrorHandler);
this._playbackStarted = new AsyncEvent("LAVALINK_PLAYBACK_STARTED", TimeSpan.Zero, this.Discord.EventErrorHandler);
this._playbackFinished = new AsyncEvent("LAVALINK_PLAYBACK_FINISHED", TimeSpan.Zero, this.Discord.EventErrorHandler);
this._trackStuck = new AsyncEvent("LAVALINK_TRACK_STUCK", TimeSpan.Zero, this.Discord.EventErrorHandler);
this._trackException = new AsyncEvent("LAVALINK_TRACK_EXCEPTION", TimeSpan.Zero, this.Discord.EventErrorHandler);
this._voiceServerUpdates = new ConcurrentDictionary>();
this._voiceStateUpdates = new ConcurrentDictionary>();
this.Discord.VoiceStateUpdated += this.Discord_VoiceStateUpdated;
this.Discord.VoiceServerUpdated += this.Discord_VoiceServerUpdated;
this.Rest = new LavalinkRestClient(this.Configuration, this.Discord);
Volatile.Write(ref this._isDisposed, false);
}
///
/// Establishes a connection to the Lavalink node.
///
///
internal async Task StartAsync()
{
if (this.Discord?.CurrentUser?.Id == null || this.Discord?.ShardCount == null)
throw new InvalidOperationException("This operation requires the Discord client to be fully initialized.");
this._webSocket = this.Discord.Configuration.WebSocketClientFactory(this.Discord.Configuration.Proxy, this.Discord.ServiceProvider);
this._webSocket.Connected += this.WebSocket_OnConnect;
this._webSocket.Disconnected += this.WebSocket_OnDisconnect;
this._webSocket.ExceptionThrown += this.WebSocket_OnException;
this._webSocket.MessageReceived += this.WebSocket_OnMessage;
this._webSocket.AddDefaultHeader("Authorization", this.Configuration.Password);
this._webSocket.AddDefaultHeader("Num-Shards", this.Discord.ShardCount.ToString(CultureInfo.InvariantCulture));
this._webSocket.AddDefaultHeader("User-Id", this.Discord.CurrentUser.Id.ToString(CultureInfo.InvariantCulture));
this._webSocket.AddDefaultHeader("Client-Name", $"DisCatSharp.Lavalink version {this.Discord.VersionString}");
if (this.Configuration.ResumeKey != null)
this._webSocket.AddDefaultHeader("Resume-Key", this.Configuration.ResumeKey);
do
{
try
{
if (this._backoff != 0)
{
await Task.Delay(this._backoff).ConfigureAwait(false);
this._backoff = Math.Min(this._backoff * 2, MAXIMUM_BACKOFF);
}
else
{
this._backoff = MINIMUM_BACKOFF;
}
await this._webSocket.ConnectAsync(new Uri(this.Configuration.SocketEndpoint.ToWebSocketString())).ConfigureAwait(false);
break;
}
catch (PlatformNotSupportedException)
{ throw; }
catch (NotImplementedException)
{ throw; }
catch (Exception ex)
{
if (!this.Configuration.SocketAutoReconnect || this._backoff == MAXIMUM_BACKOFF)
{
this.Discord.Logger.LogCritical(LavalinkEvents.LavalinkConnectionError, ex, "Failed to connect to Lavalink.");
throw;
}
else
{
this.Discord.Logger.LogCritical(LavalinkEvents.LavalinkConnectionError, ex, $"Failed to connect to Lavalink, retrying in {this._backoff} ms.");
}
}
}
while (this.Configuration.SocketAutoReconnect);
Volatile.Write(ref this._isDisposed, false);
}
///
/// Stops this Lavalink node connection and frees resources.
///
///
public async Task StopAsync()
{
foreach (var kvp in this.ConnectedGuildsInternal)
await kvp.Value.DisconnectAsync().ConfigureAwait(false);
this.NodeDisconnected?.Invoke(this);
Volatile.Write(ref this._isDisposed, true);
await this._webSocket.DisconnectAsync().ConfigureAwait(false);
// this should not be here, no?
//await this._disconnected.InvokeAsync(this, new NodeDisconnectedEventArgs(this)).ConfigureAwait(false);
}
///
/// Connects this Lavalink node to specified Discord channel.
///
/// Voice channel to connect to.
/// Channel connection, which allows for playback control.
public async Task ConnectAsync(DiscordChannel channel)
{
if (this.ConnectedGuildsInternal.ContainsKey(channel.Guild.Id))
return this.ConnectedGuildsInternal[channel.Guild.Id];
if (channel.Guild == null || (channel.Type != ChannelType.Voice && channel.Type != ChannelType.Stage))
throw new ArgumentException("Invalid channel specified.", nameof(channel));
var vstut = new TaskCompletionSource();
var vsrut = new TaskCompletionSource();
this._voiceStateUpdates[channel.Guild.Id] = vstut;
this._voiceServerUpdates[channel.Guild.Id] = vsrut;
var vsd = new VoiceDispatch
{
OpCode = 4,
Payload = new VoiceStateUpdatePayload
{
GuildId = channel.Guild.Id,
ChannelId = channel.Id,
Deafened = false,
Muted = false
}
};
var vsj = JsonConvert.SerializeObject(vsd, Formatting.None);
await (channel.Discord as DiscordClient).WsSendAsync(vsj).ConfigureAwait(false);
var vstu = await vstut.Task.ConfigureAwait(false);
var vsru = await vsrut.Task.ConfigureAwait(false);
await this.SendPayloadAsync(new LavalinkVoiceUpdate(vstu, vsru)).ConfigureAwait(false);
var con = new LavalinkGuildConnection(this, channel, vstu);
con.ChannelDisconnected += this.Con_ChannelDisconnected;
con.PlayerUpdated += (s, e) => this._playerUpdated.InvokeAsync(s, e);
con.PlaybackStarted += (s, e) => this._playbackStarted.InvokeAsync(s, e);
con.PlaybackFinished += (s, e) => this._playbackFinished.InvokeAsync(s, e);
con.TrackStuck += (s, e) => this._trackStuck.InvokeAsync(s, e);
con.TrackException += (s, e) => this._trackException.InvokeAsync(s, e);
this.ConnectedGuildsInternal[channel.Guild.Id] = con;
return con;
}
///
/// Gets a Lavalink connection to specified Discord channel.
///
/// Guild to get connection for.
/// Channel connection, which allows for playback control.
public LavalinkGuildConnection GetGuildConnection(DiscordGuild guild)
=> this.ConnectedGuildsInternal.TryGetValue(guild.Id, out var lgc) && lgc.IsConnected ? lgc : null;
///
/// Sends the payload async.
///
/// The payload.
internal async Task SendPayloadAsync(LavalinkPayload payload)
=> await this.WsSendAsync(JsonConvert.SerializeObject(payload, Formatting.None)).ConfigureAwait(false);
///
/// Webs the socket_ on message.
///
/// The client.
/// the event.ent.
private async Task WebSocket_OnMessage(IWebSocketClient client, SocketMessageEventArgs e)
{
if (e is not SocketTextMessageEventArgs et)
{
this.Discord.Logger.LogCritical(LavalinkEvents.LavalinkConnectionError, "Lavalink sent binary data - unable to process");
return;
}
this.Discord.Logger.LogTrace(LavalinkEvents.LavalinkWsRx, et.Message);
var json = et.Message;
var jsonData = JObject.Parse(json);
switch (jsonData["op"].ToString())
{
case "playerUpdate":
var gid = (ulong)jsonData["guildId"];
var state = jsonData["state"].ToObject();
if (this.ConnectedGuildsInternal.TryGetValue(gid, out var lvl))
await lvl.InternalUpdatePlayerStateAsync(state).ConfigureAwait(false);
break;
case "stats":
var statsRaw = jsonData.ToObject();
this.Statistics.Update(statsRaw);
await this._statsReceived.InvokeAsync(this, new StatisticsReceivedEventArgs(this.Discord.ServiceProvider, this.Statistics)).ConfigureAwait(false);
break;
case "event":
var evtype = jsonData["type"].ToObject();
var guildId = (ulong)jsonData["guildId"];
switch (evtype)
{
case EventType.TrackStartEvent:
if (this.ConnectedGuildsInternal.TryGetValue(guildId, out var lvlEvtst))
await lvlEvtst.InternalPlaybackStartedAsync(jsonData["track"].ToString()).ConfigureAwait(false);
break;
case EventType.TrackEndEvent:
var reason = TrackEndReason.Cleanup;
switch (jsonData["reason"].ToString())
{
case "FINISHED":
reason = TrackEndReason.Finished;
break;
case "LOAD_FAILED":
reason = TrackEndReason.LoadFailed;
break;
case "STOPPED":
reason = TrackEndReason.Stopped;
break;
case "REPLACED":
reason = TrackEndReason.Replaced;
break;
case "CLEANUP":
reason = TrackEndReason.Cleanup;
break;
}
if (this.ConnectedGuildsInternal.TryGetValue(guildId, out var lvlEvtf))
await lvlEvtf.InternalPlaybackFinishedAsync(new TrackFinishData { Track = jsonData["track"].ToString(), Reason = reason }).ConfigureAwait(false);
break;
case EventType.TrackStuckEvent:
if (this.ConnectedGuildsInternal.TryGetValue(guildId, out var lvlEvts))
await lvlEvts.InternalTrackStuckAsync(new TrackStuckData { Track = jsonData["track"].ToString(), Threshold = (long)jsonData["thresholdMs"] }).ConfigureAwait(false);
break;
case EventType.TrackExceptionEvent:
var severity = LoadFailedSeverity.Common;
switch (jsonData["severity"].ToString())
{
case "COMMON":
severity = LoadFailedSeverity.Common;
break;
case "SUSPICIOUS":
severity = LoadFailedSeverity.Suspicious;
break;
case "FAULT":
severity = LoadFailedSeverity.Fault;
break;
}
if (this.ConnectedGuildsInternal.TryGetValue(guildId, out var lvlEvte))
await lvlEvte.InternalTrackExceptionAsync(new LavalinkLoadFailedInfo { Message = jsonData["message"].ToString(), Severity = severity }, jsonData["track"].ToString()).ConfigureAwait(false);
break;
case EventType.WebSocketClosedEvent:
if (this.ConnectedGuildsInternal.TryGetValue(guildId, out var lvlEwsce))
{
lvlEwsce.VoiceWsDisconnectTcs.SetResult(true);
await lvlEwsce.InternalWebSocketClosedAsync(new WebSocketCloseEventArgs(jsonData["code"].ToObject(), jsonData["reason"].ToString(), jsonData["byRemote"].ToObject(), this.Discord.ServiceProvider)).ConfigureAwait(false);
}
break;
}
break;
}
}
///
/// Webs the socket_ on exception.
///
/// The client.
/// the event.
private Task WebSocket_OnException(IWebSocketClient client, SocketErrorEventArgs e)
=> this._lavalinkSocketError.InvokeAsync(this, new SocketErrorEventArgs(client.ServiceProvider) { Exception = e.Exception });
///
/// Webs the socket_ on disconnect.
///
/// The client.
/// the event.
private async Task WebSocket_OnDisconnect(IWebSocketClient client, SocketCloseEventArgs e)
{
if (this.IsConnected && e.CloseCode != 1001 && e.CloseCode != -1)
{
this.Discord.Logger.LogWarning(LavalinkEvents.LavalinkConnectionClosed, "Connection broken ({0}, '{1}'), reconnecting", e.CloseCode, e.CloseMessage);
await this._disconnected.InvokeAsync(this, new NodeDisconnectedEventArgs(this, false)).ConfigureAwait(false);
if (this.Configuration.SocketAutoReconnect)
await this.StartAsync().ConfigureAwait(false);
}
else if (e.CloseCode != 1001 && e.CloseCode != -1)
{
this.Discord.Logger.LogInformation(LavalinkEvents.LavalinkConnectionClosed, "Connection closed ({0}, '{1}')", e.CloseCode, e.CloseMessage);
this.NodeDisconnected?.Invoke(this);
await this._disconnected.InvokeAsync(this, new NodeDisconnectedEventArgs(this, true)).ConfigureAwait(false);
}
else
{
Volatile.Write(ref this._isDisposed, true);
this.Discord.Logger.LogWarning(LavalinkEvents.LavalinkConnectionClosed, "Lavalink died");
foreach (var kvp in this.ConnectedGuildsInternal)
{
await kvp.Value.SendVoiceUpdateAsync().ConfigureAwait(false);
_ = this.ConnectedGuildsInternal.TryRemove(kvp.Key, out _);
}
this.NodeDisconnected?.Invoke(this);
await this._disconnected.InvokeAsync(this, new NodeDisconnectedEventArgs(this, false)).ConfigureAwait(false);
if (this.Configuration.SocketAutoReconnect)
await this.StartAsync().ConfigureAwait(false);
}
}
///
/// Webs the socket_ on connect.
///
/// The client.
/// the event..
private async Task WebSocket_OnConnect(IWebSocketClient client, SocketEventArgs ea)
{
this.Discord.Logger.LogDebug(LavalinkEvents.LavalinkConnected, "Connection to Lavalink node established");
this._backoff = 0;
if (this.Configuration.ResumeKey != null)
await this.SendPayloadAsync(new LavalinkConfigureResume(this.Configuration.ResumeKey, this.Configuration.ResumeTimeout)).ConfigureAwait(false);
}
///
/// Con_S the channel disconnected.
///
/// The con.
private void Con_ChannelDisconnected(LavalinkGuildConnection con)
=> this.ConnectedGuildsInternal.TryRemove(con.GuildId, out _);
///
/// Discord voice state updated.
///
/// The client.
/// the event.
private Task Discord_VoiceStateUpdated(DiscordClient client, VoiceStateUpdateEventArgs e)
{
var gld = e.Guild;
if (gld == null)
return Task.CompletedTask;
if (e.User == null)
return Task.CompletedTask;
if (e.User.Id == this.Discord.CurrentUser.Id)
{
if (this.ConnectedGuildsInternal.TryGetValue(e.Guild.Id, out var lvlgc))
lvlgc.VoiceStateUpdate = e;
if (e.After.Channel == null && this.IsConnected && this.ConnectedGuildsInternal.ContainsKey(gld.Id))
{
_ = Task.Run(async () =>
{
var delayTask = Task.Delay(this.Configuration.WebSocketCloseTimeout);
var tcs = lvlgc.VoiceWsDisconnectTcs.Task;
_ = await Task.WhenAny(delayTask, tcs).ConfigureAwait(false);
await lvlgc.DisconnectInternalAsync(false, true).ConfigureAwait(false);
_ = this.ConnectedGuildsInternal.TryRemove(gld.Id, out _);
});
}
if (!string.IsNullOrWhiteSpace(e.SessionId) && e.Channel != null && this._voiceStateUpdates.TryRemove(gld.Id, out var xe))
xe.SetResult(e);
}
return Task.CompletedTask;
}
///
/// Discord voice server updated.
///
/// The client.
/// the event.
private Task Discord_VoiceServerUpdated(DiscordClient client, VoiceServerUpdateEventArgs e)
{
var gld = e.Guild;
if (gld == null)
return Task.CompletedTask;
if (this.ConnectedGuildsInternal.TryGetValue(e.Guild.Id, out var lvlgc))
{
var lvlp = new LavalinkVoiceUpdate(lvlgc.VoiceStateUpdate, e);
_ = Task.Run(() => this.WsSendAsync(JsonConvert.SerializeObject(lvlp)));
}
if (this._voiceServerUpdates.TryRemove(gld.Id, out var xe))
xe.SetResult(e);
return Task.CompletedTask;
}
///
/// Ws the send async.
///
/// The payload.
private async Task WsSendAsync(string payload)
{
this.Discord.Logger.LogTrace(LavalinkEvents.LavalinkWsTx, payload);
await this._webSocket.SendMessageAsync(payload).ConfigureAwait(false);
}
internal event NodeDisconnectedEventHandler NodeDisconnected;
}
diff --git a/DisCatSharp.VoiceNext/DiscordClientExtensions.cs b/DisCatSharp.VoiceNext/DiscordClientExtensions.cs
index 738aa4d5b..8fa1c6dbb 100644
--- a/DisCatSharp.VoiceNext/DiscordClientExtensions.cs
+++ b/DisCatSharp.VoiceNext/DiscordClientExtensions.cs
@@ -1,139 +1,140 @@
// This file is part of the DisCatSharp project, based off DSharpPlus.
//
// Copyright (c) 2021-2022 AITSYS
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Threading.Tasks;
using DisCatSharp.Entities;
+using DisCatSharp.Enums;
namespace DisCatSharp.VoiceNext;
///
/// The discord client extensions.
///
public static class DiscordClientExtensions
{
///
/// Creates a new VoiceNext client with default settings.
///
/// Discord client to create VoiceNext instance for.
/// VoiceNext client instance.
public static VoiceNextExtension UseVoiceNext(this DiscordClient client)
=> UseVoiceNext(client, new VoiceNextConfiguration());
///
/// Creates a new VoiceNext client with specified settings.
///
/// Discord client to create VoiceNext instance for.
/// Configuration for the VoiceNext client.
/// VoiceNext client instance.
public static VoiceNextExtension UseVoiceNext(this DiscordClient client, VoiceNextConfiguration config)
{
if (client.GetExtension() != null)
throw new InvalidOperationException("VoiceNext is already enabled for that client.");
var vnext = new VoiceNextExtension(config);
client.AddExtension(vnext);
return vnext;
}
///
/// Creates new VoiceNext clients on all shards in a given sharded client.
///
/// Discord sharded client to create VoiceNext instances for.
/// Configuration for the VoiceNext clients.
/// A dictionary of created VoiceNext clients.
public static async Task> UseVoiceNextAsync(this DiscordShardedClient client, VoiceNextConfiguration config)
{
var modules = new Dictionary();
await client.InitializeShardsAsync().ConfigureAwait(false);
foreach (var shard in client.ShardClients.Select(xkvp => xkvp.Value))
{
var vnext = shard.GetExtension();
if (vnext == null)
vnext = shard.UseVoiceNext(config);
modules[shard.ShardId] = vnext;
}
return new ReadOnlyDictionary(modules);
}
///
/// Gets the active instance of VoiceNext client for the DiscordClient.
///
/// Discord client to get VoiceNext instance for.
/// VoiceNext client instance.
public static VoiceNextExtension GetVoiceNext(this DiscordClient client)
=> client.GetExtension();
///
/// Retrieves a instance for each shard.
///
/// The shard client to retrieve instances from.
/// A dictionary containing instances for each shard.
public static async Task> GetVoiceNextAsync(this DiscordShardedClient client)
{
await client.InitializeShardsAsync().ConfigureAwait(false);
var extensions = new Dictionary();
foreach (var shard in client.ShardClients.Values)
{
extensions.Add(shard.ShardId, shard.GetExtension());
}
return new ReadOnlyDictionary(extensions);
}
///
/// Connects to this voice channel using VoiceNext.
///
/// Channel to connect to.
/// If successful, the VoiceNext connection.
public static Task ConnectAsync(this DiscordChannel channel)
{
if (channel == null)
throw new NullReferenceException();
if (channel.Guild == null)
throw new InvalidOperationException("VoiceNext can only be used with guild channels.");
if (channel.Type != ChannelType.Voice && channel.Type != ChannelType.Stage)
throw new InvalidOperationException("You can only connect to voice or stage channels.");
if (channel.Discord is not DiscordClient discord || discord == null)
throw new NullReferenceException();
var vnext = discord.GetVoiceNext();
if (vnext == null)
throw new InvalidOperationException("VoiceNext is not initialized for this Discord client.");
var vnc = vnext.GetConnection(channel.Guild);
return vnc != null
? throw new InvalidOperationException("VoiceNext is already connected in this guild.")
: vnext.ConnectAsync(channel);
}
}
diff --git a/DisCatSharp.VoiceNext/VoiceNextExtension.cs b/DisCatSharp.VoiceNext/VoiceNextExtension.cs
index f08a55329..fc7f47b3f 100644
--- a/DisCatSharp.VoiceNext/VoiceNextExtension.cs
+++ b/DisCatSharp.VoiceNext/VoiceNextExtension.cs
@@ -1,264 +1,265 @@
// This file is part of the DisCatSharp project, based off DSharpPlus.
//
// Copyright (c) 2021-2022 AITSYS
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
using System;
using System.Collections.Concurrent;
using System.Threading.Tasks;
using DisCatSharp.Entities;
+using DisCatSharp.Enums;
using DisCatSharp.EventArgs;
using DisCatSharp.Net;
using DisCatSharp.VoiceNext.Entities;
using Newtonsoft.Json;
namespace DisCatSharp.VoiceNext;
///
/// Represents VoiceNext extension, which acts as Discord voice client.
///
public sealed class VoiceNextExtension : BaseExtension
{
///
/// Gets or sets the configuration.
///
private readonly VoiceNextConfiguration _configuration;
///
/// Gets or sets the active connections.
///
private readonly ConcurrentDictionary _activeConnections;
///
/// Gets or sets the voice state updates.
///
private readonly ConcurrentDictionary> _voiceStateUpdates;
///
/// Gets or sets the voice server updates.
///
private readonly ConcurrentDictionary> _voiceServerUpdates;
///
/// Gets whether this connection has incoming voice enabled.
///
public bool IsIncomingEnabled { get; }
///
/// Initializes a new instance of the class.
///
/// The config.
internal VoiceNextExtension(VoiceNextConfiguration config)
{
this._configuration = new VoiceNextConfiguration(config);
this.IsIncomingEnabled = config.EnableIncoming;
this._activeConnections = new ConcurrentDictionary();
this._voiceStateUpdates = new ConcurrentDictionary>();
this._voiceServerUpdates = new ConcurrentDictionary>();
}
///
/// DO NOT USE THIS MANUALLY.
///
/// DO NOT USE THIS MANUALLY.
///
protected internal override void Setup(DiscordClient client)
{
if (this.Client != null)
throw new InvalidOperationException("What did I tell you?");
this.Client = client;
this.Client.VoiceStateUpdated += this.Client_VoiceStateUpdate;
this.Client.VoiceServerUpdated += this.Client_VoiceServerUpdate;
}
///
/// Create a VoiceNext connection for the specified channel.
///
/// Channel to connect to.
/// VoiceNext connection for this channel.
public async Task ConnectAsync(DiscordChannel channel)
{
if (channel.Type != ChannelType.Voice && channel.Type != ChannelType.Stage)
throw new ArgumentException("Invalid channel specified; needs to be voice or stage channel", nameof(channel));
if (channel.Guild == null)
throw new ArgumentException("Invalid channel specified; needs to be guild channel", nameof(channel));
if (!channel.PermissionsFor(channel.Guild.CurrentMember).HasPermission(Permissions.AccessChannels | Permissions.UseVoice))
throw new InvalidOperationException("You need AccessChannels and UseVoice permission to connect to this voice channel");
var gld = channel.Guild;
if (this._activeConnections.ContainsKey(gld.Id))
throw new InvalidOperationException("This guild already has a voice connection");
var vstut = new TaskCompletionSource();
var vsrut = new TaskCompletionSource();
this._voiceStateUpdates[gld.Id] = vstut;
this._voiceServerUpdates[gld.Id] = vsrut;
var vsd = new VoiceDispatch
{
OpCode = 4,
Payload = new VoiceStateUpdatePayload
{
GuildId = gld.Id,
ChannelId = channel.Id,
Deafened = false,
Muted = false
}
};
var vsj = JsonConvert.SerializeObject(vsd, Formatting.None);
await (channel.Discord as DiscordClient).WsSendAsync(vsj).ConfigureAwait(false);
var vstu = await vstut.Task.ConfigureAwait(false);
var vstup = new VoiceStateUpdatePayload
{
SessionId = vstu.SessionId,
UserId = vstu.User.Id
};
var vsru = await vsrut.Task.ConfigureAwait(false);
var vsrup = new VoiceServerUpdatePayload
{
Endpoint = vsru.Endpoint,
GuildId = vsru.Guild.Id,
Token = vsru.VoiceToken
};
var vnc = new VoiceNextConnection(this.Client, gld, channel, this._configuration, vsrup, vstup);
vnc.VoiceDisconnected += this.Vnc_VoiceDisconnected;
await vnc.ConnectAsync().ConfigureAwait(false);
await vnc.WaitForReadyAsync().ConfigureAwait(false);
this._activeConnections[gld.Id] = vnc;
return vnc;
}
///
/// Gets a VoiceNext connection for specified guild.
///
/// Guild to get VoiceNext connection for.
/// VoiceNext connection for the specified guild.
public VoiceNextConnection GetConnection(DiscordGuild guild) => this._activeConnections.ContainsKey(guild.Id) ? this._activeConnections[guild.Id] : null;
///
/// Vnc_S the voice disconnected.
///
/// The guild.
/// A Task.
private async Task Vnc_VoiceDisconnected(DiscordGuild guild)
{
VoiceNextConnection vnc = null;
if (this._activeConnections.ContainsKey(guild.Id))
this._activeConnections.TryRemove(guild.Id, out vnc);
var vsd = new VoiceDispatch
{
OpCode = 4,
Payload = new VoiceStateUpdatePayload
{
GuildId = guild.Id,
ChannelId = null
}
};
var vsj = JsonConvert.SerializeObject(vsd, Formatting.None);
await (guild.Discord as DiscordClient).WsSendAsync(vsj).ConfigureAwait(false);
}
///
/// Client_S the voice state update.
///
/// The client.
/// The e.
/// A Task.
private Task Client_VoiceStateUpdate(DiscordClient client, VoiceStateUpdateEventArgs e)
{
var gld = e.Guild;
if (gld == null)
return Task.CompletedTask;
if (e.User == null)
return Task.CompletedTask;
if (e.User.Id == this.Client.CurrentUser.Id)
{
if (e.After.Channel == null && this._activeConnections.TryRemove(gld.Id, out var ac))
ac.Disconnect();
if (this._activeConnections.TryGetValue(e.Guild.Id, out var vnc))
vnc.TargetChannel = e.Channel;
if (!string.IsNullOrWhiteSpace(e.SessionId) && e.Channel != null && this._voiceStateUpdates.TryRemove(gld.Id, out var xe))
xe.SetResult(e);
}
return Task.CompletedTask;
}
///
/// Client_S the voice server update.
///
/// The client.
/// The e.
/// A Task.
private async Task Client_VoiceServerUpdate(DiscordClient client, VoiceServerUpdateEventArgs e)
{
var gld = e.Guild;
if (gld == null)
return;
if (this._activeConnections.TryGetValue(e.Guild.Id, out var vnc))
{
vnc.ServerData = new VoiceServerUpdatePayload
{
Endpoint = e.Endpoint,
GuildId = e.Guild.Id,
Token = e.VoiceToken
};
var eps = e.Endpoint;
var epi = eps.LastIndexOf(':');
var eph = string.Empty;
var epp = 443;
if (epi != -1)
{
eph = eps[..epi];
epp = int.Parse(eps[(epi + 1)..]);
}
else
{
eph = eps;
}
vnc.WebSocketEndpoint = new ConnectionEndpoint { Hostname = eph, Port = epp };
vnc.Resume = false;
await vnc.ReconnectAsync().ConfigureAwait(false);
}
if (this._voiceServerUpdates.ContainsKey(gld.Id))
{
this._voiceServerUpdates.TryRemove(gld.Id, out var xe);
xe.SetResult(e);
}
}
}
diff --git a/DisCatSharp/Entities/Application/DiscordApplicationCommandOption.cs b/DisCatSharp/Entities/Application/DiscordApplicationCommandOption.cs
index 0263ac96a..0891efb24 100644
--- a/DisCatSharp/Entities/Application/DiscordApplicationCommandOption.cs
+++ b/DisCatSharp/Entities/Application/DiscordApplicationCommandOption.cs
@@ -1,163 +1,165 @@
// This file is part of the DisCatSharp project, based off DSharpPlus.
//
// Copyright (c) 2021-2022 AITSYS
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
+using DisCatSharp.Enums;
+
using Newtonsoft.Json;
namespace DisCatSharp.Entities;
///
/// Represents a parameter for a .
///
public sealed class DiscordApplicationCommandOption
{
///
/// Gets the type of this command parameter.
///
[JsonProperty("type")]
public ApplicationCommandOptionType Type { get; internal set; }
///
/// Gets the name of this command parameter.
///
[JsonProperty("name")]
public string Name { get; internal set; }
///
/// Sets the name localizations.
///
[JsonProperty("name_localizations", NullValueHandling = NullValueHandling.Ignore)]
internal Dictionary RawNameLocalizations { get; set; }
///
/// Gets the name localizations.
///
[JsonIgnore]
public DiscordApplicationCommandLocalization NameLocalizations
=> new(this.RawNameLocalizations);
///
/// Gets the description of this command parameter.
///
[JsonProperty("description")]
public string Description { get; internal set; }
///
/// Sets the description localizations.
///
[JsonProperty("description_localizations", NullValueHandling = NullValueHandling.Ignore)]
internal Dictionary RawDescriptionLocalizations { get; set; }
///
/// Gets the description localizations.
///
[JsonIgnore]
public DiscordApplicationCommandLocalization DescriptionLocalizations
=> new(this.RawDescriptionLocalizations);
///
/// Gets whether this command parameter is required.
///
[JsonProperty("required", NullValueHandling = NullValueHandling.Ignore)]
public bool? Required { get; internal set; }
///
/// Gets the optional choices for this command parameter.
/// Not applicable for auto-complete options.
///
[JsonProperty("choices", NullValueHandling = NullValueHandling.Ignore)]
public IReadOnlyCollection Choices { get; internal set; }
///
/// Gets the optional subcommand parameters for this parameter.
///
[JsonProperty("options", NullValueHandling = NullValueHandling.Ignore)]
public IReadOnlyCollection Options { get; internal set; }
///
/// Gets the optional allowed channel types.
///
[JsonProperty("channel_types", NullValueHandling = NullValueHandling.Ignore)]
public IReadOnlyCollection ChannelTypes { get; internal set; }
///
/// Gets whether this option provides autocompletion.
///
[JsonProperty("autocomplete", NullValueHandling = NullValueHandling.Ignore)]
public bool? AutoComplete { get; internal set; }
///
/// Gets the minimum value for this slash command parameter.
///
[JsonProperty("min_value", NullValueHandling = NullValueHandling.Ignore)]
public object MinimumValue { get; internal set; }
///
/// Gets the maximum value for this slash command parameter.
///
[JsonProperty("max_value", NullValueHandling = NullValueHandling.Ignore)]
public object MaximumValue { get; internal set; }
///
/// Creates a new instance of a .
///
/// The name of this parameter.
/// The description of the parameter.
/// The type of this parameter.
/// Whether the parameter is required.
/// The optional choice selection for this parameter.
/// The optional subcommands for this parameter.
/// If the option is a channel type, the channels shown will be restricted to these types.
/// Whether this option provides autocompletion.
/// The minimum value for this parameter. Only valid for types or .
/// The maximum value for this parameter. Only valid for types or .
/// The localizations of the parameter name.
/// The localizations of the parameter description.
public DiscordApplicationCommandOption(string name, string description, ApplicationCommandOptionType type, bool? required = null, IEnumerable choices = null, IEnumerable options = null, IEnumerable channelTypes = null, bool? autocomplete = null, object minimumValue = null, object maximumValue = null, DiscordApplicationCommandLocalization nameLocalizations = null, DiscordApplicationCommandLocalization descriptionLocalizations = null)
{
if (!Utilities.IsValidSlashCommandName(name))
throw new ArgumentException("Invalid application command option name specified. It must be below 32 characters and not contain any whitespace.", nameof(name));
if (name.Any(char.IsUpper))
throw new ArgumentException("Application command option name cannot have any upper case characters.", nameof(name));
if (description.Length > 100)
throw new ArgumentException("Application command option description cannot exceed 100 characters.", nameof(description));
if ((autocomplete ?? false) && (choices?.Any() ?? false))
throw new InvalidOperationException("Auto-complete slash command options cannot provide choices.");
this.Name = name;
this.Description = description;
this.Type = type;
this.Required = required;
this.Choices = choices != null ? new ReadOnlyCollection(choices.ToList()) : null;
this.Options = options != null ? new ReadOnlyCollection(options.ToList()) : null;
this.ChannelTypes = channelTypes != null ? new ReadOnlyCollection(channelTypes.ToList()) : null;
this.AutoComplete = autocomplete;
this.MinimumValue = minimumValue;
this.MaximumValue = maximumValue;
this.RawNameLocalizations = nameLocalizations?.GetKeyValuePairs();
this.RawDescriptionLocalizations = descriptionLocalizations?.GetKeyValuePairs();
}
}
diff --git a/DisCatSharp/Entities/Channel/DiscordChannel.cs b/DisCatSharp/Entities/Channel/DiscordChannel.cs
index 8c750ecda..37bcdee85 100644
--- a/DisCatSharp/Entities/Channel/DiscordChannel.cs
+++ b/DisCatSharp/Entities/Channel/DiscordChannel.cs
@@ -1,1317 +1,1324 @@
// This file is part of the DisCatSharp project, based off DSharpPlus.
//
// Copyright (c) 2021-2022 AITSYS
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using DisCatSharp.Enums;
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 the template for new posts in this channel.
+ /// Applicable if forum channel.
+ ///
+ [JsonProperty("template", NullValueHandling = NullValueHandling.Ignore)]
+ public string Template { get; internal set; }
+
///
/// Gets this channel's banner hash, when applicable.
///
[JsonProperty("banner")]
public string BannerHash { get; internal set; }
///
/// Gets this channel'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 flags of this channel.
///
[JsonProperty("flags", NullValueHandling = NullValueHandling.Ignore)]
public ChannelFlags Flags { get; internal set; }
///
/// Gets the maximum available position to move the channel to.
/// This can contain outdated information.
///
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).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).Last().Position
: channels.Where(xc => xc.ParentId == this.ParentId && xc.Type == this.Type).OrderBy(xc => xc.Position).Last().Position
: channels.Where(xc => xc.ParentId == null && xc.Type == this.Type).OrderBy(xc => xc.Position).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).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).First().Position
: channels.Where(xc => xc.ParentId == this.ParentId && xc.Type == this.Type).OrderBy(xc => xc.Position).First().Position
: channels.Where(xc => xc.ParentId == null && xc.Type == this.Type).OrderBy(xc => xc.Position).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 PermissionOverwritesInternal = 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; }
///
/// List of available tags for forum posts.
///
[JsonProperty("available_tags", NullValueHandling = NullValueHandling.Ignore)]
public List AvailableTags { get; internal set; }
///
/// Starter template for forum posts.
///
[JsonProperty("template", NullValueHandling = NullValueHandling.Ignore)]
public string Template { 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 =>
!this.IsCategory
? throw new ArgumentException("Only channel categories contain children.")
: this.Guild.ChannelsInternal.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 =>
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", NullValueHandling = NullValueHandling.Ignore)]
public Permissions? UserPermissions { get; internal set; }
///
/// Initializes a new instance of the class.
///
internal DiscordChannel()
{
this._permissionOverwritesLazy = new Lazy>(() => new ReadOnlyCollection(this.PermissionOverwritesInternal));
}
#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) =>
!this.IsWritable()
? 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) =>
!this.IsWritable()
? 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) =>
!this.IsWritable()
? 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.IsWritable()
? 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.PermissionOverwritesInternal)
ovrs.Add(await new DiscordOverwriteBuilder().FromAsync(ovr).ConfigureAwait(false));
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.IsWritable())
{
perUserRateLimit = Optional.None;
}
return await this.Guild.CreateChannelAsync(this.Name, this.Type, this.Parent, this.Topic, bitrate, userLimit, ovrs, this.IsNsfw, perUserRateLimit, this.QualityMode, this.DefaultAutoArchiveDuration, 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) =>
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 = ImageTool.Base64FromStream(mdl.Banner);
return this.Discord.ApiClient.ModifyChannelAsync(this.Id, mdl.Name, mdl.Position, mdl.Topic, mdl.Nsfw,
mdl.Parent.Map(p => p?.Id), mdl.Bitrate, mdl.UserLimit, mdl.PerUserRateLimit, mdl.RtcRegion.Map(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 pmds = this.Guild.ChannelsInternal.Values.Where(xc => xc.Type == this.Type).OrderBy(xc => xc.Position)
.Select(x => new RestGuildChannelReorderPayload
{
ChannelId = x.Id,
Position = x.Id == this.Id ? position : x.Position >= position ? x.Position + 1 : x.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.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 = ochns.Select(x =>
new RestGuildChannelReorderPayload
{
ChannelId = x.Id,
Position = x.Id == this.Id
? position
: isUp
? x.Position <= position && x.Position > this.Position ? x.Position - 1 : x.Position
: x.Position >= position && x.Position < this.Position ? x.Position + 1 : x.Position
}
);
await this.Discord.ApiClient.ModifyGuildChannelPositionAsync(this.Guild.Id, pmds, reason).ConfigureAwait(false);
}
///
/// Internally 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.ChannelsInternal.Clear();
foreach (var channel in channels.ToList())
{
channel.Discord = this.Discord;
foreach (var xo in channel.PermissionOverwritesInternal)
{
xo.Discord = this.Discord;
xo.ChannelId = channel.Id;
}
this.Guild.ChannelsInternal[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 given 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. Use to remove from parent.
/// 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.
public Task ModifyParentAsync(DiscordChannel newParent, bool? lockPermissions = null, 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.");
if (newParent.Type is not ChannelType.Category)
throw new ArgumentException("Only category type channels can be parents.");
var position = this.Guild.ChannelsInternal.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 pmds = this.Guild.ChannelsInternal.Values.Where(xc => xc.Type == this.Type)
.OrderBy(xc => xc.Position)
.Select(x =>
{
var pmd = new RestGuildChannelNewParentPayload
{
ChannelId = x.Id,
Position = x.Position >= position ? x.Position + 1 : x.Position,
};
if (x.Id == this.Id)
{
pmd.Position = position;
pmd.ParentId = newParent is not null ? newParent.Id : null;
pmd.LockPermissions = lockPermissions;
}
return pmd;
});
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 pmds = this.Guild.ChannelsInternal.Values.Where(xc => xc.Type == this.Type)
.OrderBy(xc => xc.Position)
.Select(x =>
{
var pmd = new RestGuildChannelNoParentPayload { ChannelId = x.Id };
if (x.Id == this.Id)
{
pmd.Position = 1;
pmd.ParentId = null;
}
else
{
pmd.Position = x.Position < this.Position ? x.Position + 1 : x.Position;
}
return pmd;
});
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.IsWritable())
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.Length < 2)
{
await this.Discord.ApiClient.DeleteMessageAsync(this.Id, msgs.Single(), reason).ConfigureAwait(false);
return;
}
for (var i = 0; i < msgs.Length; 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() =>
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 maxAge = 86400, int maxUses = 0, bool temporary = false, bool unique = false, TargetType? targetType = null, TargetActivity? targetApplication = null, ulong? targetUser = null, string reason = null)
=> this.Discord.ApiClient.CreateChannelInviteAsync(this.Id, maxAge, maxUses, targetType, targetApplication, targetUser, 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 sendStartNotification = false, StagePrivacyLevel privacyLevel = StagePrivacyLevel.GuildOnly, string reason = null)
=> await this.Discord.ApiClient.CreateStageInstanceAsync(this.Id, topic, sendStartNotification, privacyLevel, 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 privacyLevel, string reason = null)
=> await this.Discord.ApiClient.ModifyStageInstanceAsync(this.Id, topic, privacyLevel, 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 Scheduled Events
///
/// Creates a scheduled event based on the channel type.
///
/// The name.
/// The scheduled start time.
/// The description.
/// The cover image.
/// The reason.
/// A scheduled event.
/// Thrown when the resource 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, DateTimeOffset scheduledStartTime, string description = null, Optional coverImage = default, string reason = null)
{
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, scheduledStartTime, null, this, null, description, type, coverImage, reason);
}
#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 autoArchiveDuration = ThreadAutoArchiveDuration.OneHour, ChannelType type = ChannelType.PublicThread, int? rateLimitPerUser = null, string reason = null) =>
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.")
: type == ChannelType.PrivateThread
? Utilities.CheckThreadPrivateFeature(this.Guild)
? Utilities.CheckThreadAutoArchiveDurationFeature(this.Guild, autoArchiveDuration)
? await this.Discord.ApiClient.CreateThreadAsync(this.Id, null, name, autoArchiveDuration, type, rateLimitPerUser, reason)
: throw new NotSupportedException($"Cannot modify ThreadAutoArchiveDuration. Guild needs boost tier {(autoArchiveDuration == ThreadAutoArchiveDuration.ThreeDays ? "one" : "two")}.")
: throw new NotSupportedException($"Cannot create a private thread. Guild needs to be boost tier two.")
: Utilities.CheckThreadAutoArchiveDurationFeature(this.Guild, autoArchiveDuration)
? await this.Discord.ApiClient.CreateThreadAsync(this.Id, null, name, autoArchiveDuration, this.Type == ChannelType.News ? ChannelType.NewsThread : ChannelType.PublicThread, rateLimitPerUser, reason)
: throw new NotSupportedException($"Cannot modify ThreadAutoArchiveDuration. Guild needs boost tier {(autoArchiveDuration == 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() =>
!this.IsWritable()
? 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() =>
!this.IsWritable()
? 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 = ImageTool.Base64FromStream(avatar);
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) =>
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) =>
(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)
{
// user > role > everyone
// allow > deny > undefined
// =>
// user allow > user deny > role allow > role deny > everyone allow > everyone deny
if (this.IsPrivate || this.Guild == null)
return Permissions.None;
if (this.Guild.OwnerId == mbr.Id)
return PermissionMethods.FullPerms;
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);
// assign permissions from member's roles (in order)
perms |= mbRoles.Aggregate(Permissions.None, (c, role) => c | role.Permissions);
// Administrator grants all permissions and cannot be overridden
if ((perms & Permissions.Administrator) == Permissions.Administrator)
return PermissionMethods.FullPerms;
// channel overrides for roles that member is in
var mbRoleOverrides = mbRoles
.Select(xr => this.PermissionOverwritesInternal.FirstOrDefault(xo => xo.Id == xr.Id))
.Where(xo => xo != null)
.ToList();
// assign channel permission overwrites for @everyone pseudo-role
var everyoneOverwrites = this.PermissionOverwritesInternal.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.PermissionOverwritesInternal.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() =>
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/Channel/Overwrite/DiscordOverwrite.cs b/DisCatSharp/Entities/Channel/Overwrite/DiscordOverwrite.cs
index d42d9a1ba..71c61516d 100644
--- a/DisCatSharp/Entities/Channel/Overwrite/DiscordOverwrite.cs
+++ b/DisCatSharp/Entities/Channel/Overwrite/DiscordOverwrite.cs
@@ -1,121 +1,123 @@
// This file is part of the DisCatSharp project, based off DSharpPlus.
//
// Copyright (c) 2021-2022 AITSYS
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
using System;
using System.Threading.Tasks;
+using DisCatSharp.Enums;
+
using Newtonsoft.Json;
namespace DisCatSharp.Entities;
///
/// Represents a permission overwrite for a channel.
///
public class DiscordOverwrite : SnowflakeObject
{
///
/// Gets the type of the overwrite. Either "role" or "member".
///
[JsonProperty("type", NullValueHandling = NullValueHandling.Ignore)]
public OverwriteType Type { get; internal set; }
///
/// Gets the allowed permission set.
///
[JsonProperty("allow", NullValueHandling = NullValueHandling.Ignore)]
public Permissions Allowed { get; internal set; }
///
/// Gets the denied permission set.
///
[JsonProperty("deny", NullValueHandling = NullValueHandling.Ignore)]
public Permissions Denied { get; internal set; }
[JsonIgnore]
internal ulong ChannelId;
#region Methods
///
/// Deletes this channel overwrite.
///
/// Reason as to why this overwrite gets deleted.
/// Thrown when the client does not have the permission.
/// Thrown when the overwrite 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.DeleteChannelPermissionAsync(this.ChannelId, this.Id, reason);
///
/// Updates this channel overwrite.
///
/// Permissions that are allowed.
/// Permissions that are denied.
/// Reason as to why you made this change.
/// Thrown when the client does not have the permission.
/// Thrown when the overwrite does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public Task UpdateAsync(Permissions? allow = null, Permissions? deny = null, string reason = null)
=> this.Discord.ApiClient.EditChannelPermissionsAsync(this.ChannelId, this.Id, allow ?? this.Allowed, deny ?? this.Denied, this.Type.ToString().ToLowerInvariant(), reason);
///
/// Gets the DiscordMember that is affected by this overwrite.
///
/// The DiscordMember that is affected by this overwrite
/// Thrown when the client does not have the permission.
/// Thrown when the overwrite does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public async Task GetMemberAsync() =>
this.Type != OverwriteType.Member
? throw new ArgumentException(nameof(this.Type), "This overwrite is for a role, not a member.")
: await (await this.Discord.ApiClient.GetChannelAsync(this.ChannelId).ConfigureAwait(false)).Guild.GetMemberAsync(this.Id).ConfigureAwait(false);
///
/// Gets the DiscordRole that is affected by this overwrite.
///
/// The DiscordRole that is affected by this overwrite
/// Thrown when the role does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public async Task GetRoleAsync() =>
this.Type != OverwriteType.Role
? throw new ArgumentException(nameof(this.Type), "This overwrite is for a member, not a role.")
: (await this.Discord.ApiClient.GetChannelAsync(this.ChannelId).ConfigureAwait(false)).Guild.GetRole(this.Id);
#endregion
///
/// Initializes a new instance of the class.
///
internal DiscordOverwrite()
{ }
///
/// Checks whether given permissions are allowed, denied, or not set.
///
/// Permissions to check.
/// Whether given permissions are allowed, denied, or not set.
public PermissionLevel CheckPermission(Permissions permission) =>
(this.Allowed & permission) != 0
? PermissionLevel.Allowed
: (this.Denied & permission) != 0 ? PermissionLevel.Denied : PermissionLevel.Unset;
}
diff --git a/DisCatSharp/Entities/Channel/Overwrite/DiscordOverwriteBuilder.cs b/DisCatSharp/Entities/Channel/Overwrite/DiscordOverwriteBuilder.cs
index 163954b38..f3ab04ca2 100644
--- a/DisCatSharp/Entities/Channel/Overwrite/DiscordOverwriteBuilder.cs
+++ b/DisCatSharp/Entities/Channel/Overwrite/DiscordOverwriteBuilder.cs
@@ -1,178 +1,180 @@
// This file is part of the DisCatSharp project, based off DSharpPlus.
//
// Copyright (c) 2021-2022 AITSYS
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
using System.Threading.Tasks;
+using DisCatSharp.Enums;
+
using Newtonsoft.Json;
namespace DisCatSharp.Entities;
///
/// Represents a Discord permission overwrite builder.
///
public sealed class DiscordOverwriteBuilder
{
///
/// Gets or sets the allowed permissions for this overwrite.
///
public Permissions Allowed { get; set; }
///
/// Gets or sets the denied permissions for this overwrite.
///
public Permissions Denied { get; set; }
///
/// Gets the type of this overwrite's target.
///
public OverwriteType Type { get; private set; }
///
/// Gets the target for this overwrite.
///
public SnowflakeObject Target { get; private set; }
///
/// Creates a new Discord permission overwrite builder for a member. This class can be used to construct permission overwrites for guild channels, used when creating channels.
///
public DiscordOverwriteBuilder(DiscordMember member)
{
this.Target = member;
this.Type = OverwriteType.Member;
}
///
/// Creates a new Discord permission overwrite builder for a role. This class can be used to construct permission overwrites for guild channels, used when creating channels.
///
public DiscordOverwriteBuilder(DiscordRole role)
{
this.Target = role;
this.Type = OverwriteType.Role;
}
///
/// Creates a new Discord permission overwrite builder. This class can be used to construct permission overwrites for guild channels, used when creating channels.
///
public DiscordOverwriteBuilder()
{ }
///
/// Allows a permission for this overwrite.
///
/// Permission or permission set to allow for this overwrite.
/// This builder.
public DiscordOverwriteBuilder Allow(Permissions permission)
{
this.Allowed |= permission;
return this;
}
///
/// Denies a permission for this overwrite.
///
/// Permission or permission set to deny for this overwrite.
/// This builder.
public DiscordOverwriteBuilder Deny(Permissions permission)
{
this.Denied |= permission;
return this;
}
///
/// Sets the member to which this overwrite applies.
///
/// Member to which apply this overwrite's permissions.
/// This builder.
public DiscordOverwriteBuilder For(DiscordMember member)
{
this.Target = member;
this.Type = OverwriteType.Member;
return this;
}
///
/// Sets the role to which this overwrite applies.
///
/// Role to which apply this overwrite's permissions.
/// This builder.
public DiscordOverwriteBuilder For(DiscordRole role)
{
this.Target = role;
this.Type = OverwriteType.Role;
return this;
}
///
/// Populates this builder with data from another overwrite object.
///
/// Overwrite from which data will be used.
/// This builder.
public async Task FromAsync(DiscordOverwrite other)
{
this.Allowed = other.Allowed;
this.Denied = other.Denied;
this.Type = other.Type;
this.Target = this.Type == OverwriteType.Member ? await other.GetMemberAsync().ConfigureAwait(false) as SnowflakeObject : await other.GetRoleAsync().ConfigureAwait(false) as SnowflakeObject;
return this;
}
///
/// Builds this DiscordOverwrite.
///
/// Use this object for creation of new overwrites.
internal DiscordRestOverwrite Build() =>
new()
{
Allow = this.Allowed,
Deny = this.Denied,
Id = this.Target.Id,
Type = this.Type,
};
}
internal struct DiscordRestOverwrite
{
///
/// Determines what is allowed.
///
[JsonProperty("allow", NullValueHandling = NullValueHandling.Ignore)]
internal Permissions Allow { get; set; }
///
/// Determines what is denied.
///
[JsonProperty("deny", NullValueHandling = NullValueHandling.Ignore)]
internal Permissions Deny { get; set; }
///
/// Gets or sets the id.
///
[JsonProperty("id", NullValueHandling = NullValueHandling.Ignore)]
internal ulong Id { get; set; }
///
/// Gets or sets the overwrite type.
///
[JsonProperty("type", NullValueHandling = NullValueHandling.Ignore)]
internal OverwriteType Type { get; set; }
}
diff --git a/DisCatSharp/Entities/Interaction/DiscordInteraction.cs b/DisCatSharp/Entities/Interaction/DiscordInteraction.cs
index f4227a536..99356b0be 100644
--- a/DisCatSharp/Entities/Interaction/DiscordInteraction.cs
+++ b/DisCatSharp/Entities/Interaction/DiscordInteraction.cs
@@ -1,219 +1,221 @@
// This file is part of the DisCatSharp project, based off DSharpPlus.
//
// Copyright (c) 2021-2022 AITSYS
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
using System;
using System.Threading.Tasks;
+using DisCatSharp.Enums;
+
using Newtonsoft.Json;
namespace DisCatSharp.Entities;
///
/// Represents an interaction that was invoked.
///
public sealed class DiscordInteraction : SnowflakeObject
{
///
/// Gets the type of interaction invoked.
///
[JsonProperty("type")]
public InteractionType Type { get; internal set; }
///
/// Gets the command data for this interaction.
///
[JsonProperty("data", NullValueHandling = NullValueHandling.Ignore)]
public DiscordInteractionData Data { get; internal set; }
///
/// Gets the Id of the guild that invoked this interaction.
///
[JsonIgnore]
public ulong? GuildId { get; internal set; }
///
/// Gets the guild that invoked this interaction.
///
[JsonIgnore]
public DiscordGuild Guild
=> (this.Discord as DiscordClient).InternalGetCachedGuild(this.GuildId);
///
/// Gets the Id of the channel that invoked this interaction.
///
[JsonIgnore]
public ulong ChannelId { get; internal set; }
///
/// Gets the channel that invoked this interaction.
///
[JsonIgnore]
public DiscordChannel Channel
=> (this.Discord as DiscordClient).InternalGetCachedChannel(this.ChannelId) ?? (DiscordChannel)(this.Discord as DiscordClient).InternalGetCachedThread(this.ChannelId) ?? (this.Guild == null ? new DiscordDmChannel { Id = this.ChannelId, Type = ChannelType.Private, Discord = this.Discord } : new DiscordChannel() { Id = this.ChannelId, Discord = this.Discord });
///
/// Gets the user that invoked this interaction.
/// This can be cast to a if created in a guild.
///
[JsonIgnore]
public DiscordUser User { get; internal set; }
///
/// Gets the continuation token for responding to this interaction.
///
[JsonProperty("token")]
public string Token { get; internal set; }
///
/// Gets the version number for this interaction type.
///
[JsonProperty("version")]
public int Version { get; internal set; }
///
/// Gets the ID of the application that created this interaction.
///
[JsonProperty("application_id")]
public ulong ApplicationId { get; internal set; }
///
/// The message this interaction was created with, if any.
///
[JsonProperty("message")]
internal DiscordMessage Message { get; set; }
///
/// Gets the invoking user locale.
///
[JsonProperty("locale", NullValueHandling = NullValueHandling.Ignore)]
public string Locale { get; internal set; }
///
/// Gets the guild locale if applicable.
///
[JsonProperty("guild_locale", NullValueHandling = NullValueHandling.Ignore)]
public string GuildLocale { get; internal set; }
///
/// Creates a response to this interaction.
///
/// The type of the response.
/// The data, if any, to send.
public Task CreateResponseAsync(InteractionResponseType type, DiscordInteractionResponseBuilder builder = null)
=> this.Discord.ApiClient.CreateInteractionResponseAsync(this.Id, this.Token, type, builder);
///
/// Creates a modal response to this interaction.
///
/// The data to send.
public Task CreateInteractionModalResponseAsync(DiscordInteractionModalBuilder builder)
=> this.Type != InteractionType.Ping && this.Type != InteractionType.ModalSubmit ? this.Discord.ApiClient.CreateInteractionModalResponseAsync(this.Id, this.Token, InteractionResponseType.Modal, builder) : throw new NotSupportedException("You can't respond to an PING with a modal.");
///
/// Gets the original interaction response.
///
/// The original message that was sent. This does not work on ephemeral messages.
public Task GetOriginalResponseAsync()
=> this.Discord.ApiClient.GetOriginalInteractionResponseAsync(this.Discord.CurrentApplication.Id, this.Token);
///
/// Edits the original interaction response.
///
/// The webhook builder.
/// The edited .
public async Task EditOriginalResponseAsync(DiscordWebhookBuilder builder)
{
builder.Validate(isInteractionResponse: true);
if (builder.KeepAttachmentsInternal.HasValue && builder.KeepAttachmentsInternal.Value)
{
var attachments = this.Discord.ApiClient.GetOriginalInteractionResponseAsync(this.Discord.CurrentApplication.Id, this.Token).Result.Attachments;
if (attachments?.Count > 0)
{
builder.AttachmentsInternal.AddRange(attachments);
}
}
else if (builder.KeepAttachmentsInternal.HasValue)
{
builder.AttachmentsInternal.Clear();
}
return await this.Discord.ApiClient.EditOriginalInteractionResponseAsync(this.Discord.CurrentApplication.Id, this.Token, builder).ConfigureAwait(false);
}
///
/// Deletes the original interaction response.
/// >
public Task DeleteOriginalResponseAsync()
=> this.Discord.ApiClient.DeleteOriginalInteractionResponseAsync(this.Discord.CurrentApplication.Id, this.Token);
///
/// Creates a follow up message to this interaction.
///
/// The webhook builder.
/// The created .
public async Task CreateFollowupMessageAsync(DiscordFollowupMessageBuilder builder)
{
builder.Validate();
return await this.Discord.ApiClient.CreateFollowupMessageAsync(this.Discord.CurrentApplication.Id, this.Token, builder).ConfigureAwait(false);
}
///
/// Gets a follow up message.
///
/// The id of the follow up message.
public Task GetFollowupMessageAsync(ulong messageId)
=> this.Discord.ApiClient.GetFollowupMessageAsync(this.Discord.CurrentApplication.Id, this.Token, messageId);
///
/// Edits a follow up message.
///
/// The id of the follow up message.
/// The webhook builder.
/// The edited .
public async Task EditFollowupMessageAsync(ulong messageId, DiscordWebhookBuilder builder)
{
builder.Validate(isFollowup: true);
if (builder.KeepAttachmentsInternal.HasValue && builder.KeepAttachmentsInternal.Value)
{
var attachments = this.Discord.ApiClient.GetFollowupMessageAsync(this.Discord.CurrentApplication.Id, this.Token, messageId).Result.Attachments;
if (attachments?.Count > 0)
{
builder.AttachmentsInternal.AddRange(attachments);
}
}
else if (builder.KeepAttachmentsInternal.HasValue)
{
builder.AttachmentsInternal.Clear();
}
return await this.Discord.ApiClient.EditFollowupMessageAsync(this.Discord.CurrentApplication.Id, this.Token, messageId, builder).ConfigureAwait(false);
}
///
/// Deletes a follow up message.
///
/// The id of the follow up message.
public Task DeleteFollowupMessageAsync(ulong messageId)
=> this.Discord.ApiClient.DeleteFollowupMessageAsync(this.Discord.CurrentApplication.Id, this.Token, messageId);
}
diff --git a/DisCatSharp/Entities/Invite/DiscordInviteChannel.cs b/DisCatSharp/Entities/Invite/DiscordInviteChannel.cs
index 4c1a51fa4..9a348c30f 100644
--- a/DisCatSharp/Entities/Invite/DiscordInviteChannel.cs
+++ b/DisCatSharp/Entities/Invite/DiscordInviteChannel.cs
@@ -1,49 +1,51 @@
// This file is part of the DisCatSharp project, based off DSharpPlus.
//
// Copyright (c) 2021-2022 AITSYS
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
+using DisCatSharp.Enums;
+
using Newtonsoft.Json;
namespace DisCatSharp.Entities;
///
/// Represents the channel to which an invite is linked.
///
public class DiscordInviteChannel : SnowflakeObject
{
///
/// Gets the name of the channel.
///
[JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)]
public string Name { get; internal set; }
///
/// Gets the type of the channel.
///
[JsonProperty("type", NullValueHandling = NullValueHandling.Ignore)]
public ChannelType Type { get; internal set; }
///
/// Initializes a new instance of the class.
///
internal DiscordInviteChannel()
{ }
}
diff --git a/DisCatSharp/Entities/Message/DiscordMessage.cs b/DisCatSharp/Entities/Message/DiscordMessage.cs
index c6f6cc30c..3369051e0 100644
--- a/DisCatSharp/Entities/Message/DiscordMessage.cs
+++ b/DisCatSharp/Entities/Message/DiscordMessage.cs
@@ -1,876 +1,878 @@
// This file is part of the DisCatSharp project, based off DSharpPlus.
//
// Copyright (c) 2021-2022 AITSYS
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Globalization;
using System.Linq;
using System.Threading.Tasks;
+using DisCatSharp.Enums;
+
using Newtonsoft.Json;
namespace DisCatSharp.Entities;
///
/// Represents a Discord text message.
///
public class DiscordMessage : SnowflakeObject, IEquatable
{
///
/// Initializes a new instance of the class.
///
internal DiscordMessage()
{
this._attachmentsLazy = new Lazy>(() => new ReadOnlyCollection(this.AttachmentsInternal));
this._embedsLazy = new Lazy>(() => new ReadOnlyCollection(this.EmbedsInternal));
this._mentionedChannelsLazy = new Lazy>(() => this.MentionedChannelsInternal != null
? new ReadOnlyCollection(this.MentionedChannelsInternal)
: Array.Empty());
this._mentionedRolesLazy = new Lazy>(() => this.MentionedRolesInternal != null ? new ReadOnlyCollection(this.MentionedRolesInternal) : Array.Empty());
this.MentionedUsersLazy = new Lazy>(() => new ReadOnlyCollection(this.MentionedUsersInternal));
this._reactionsLazy = new Lazy>(() => new ReadOnlyCollection(this.ReactionsInternal));
this._stickersLazy = new Lazy>(() => new ReadOnlyCollection(this.StickersInternal));
this._jumpLink = new Lazy(() =>
{
string gid = null;
if (this.Channel != null)
gid = this.Channel is DiscordDmChannel
? "@me"
: this.Channel is DiscordThreadChannel
? this.INTERNAL_THREAD.GuildId.Value.ToString(CultureInfo.InvariantCulture)
: this.Channel.GuildId.Value.ToString(CultureInfo.InvariantCulture);
var cid = this.ChannelId.ToString(CultureInfo.InvariantCulture);
var mid = this.Id.ToString(CultureInfo.InvariantCulture);
return new Uri($"https://{(this.Discord.Configuration.UseCanary ? "canary.discord.com" : this.Discord.Configuration.UsePtb ? "ptb.discord.com" : "discord.com")}/channels/{gid}/{cid}/{mid}");
});
}
///
/// Initializes a new instance of the class.
///
/// The other message.
internal DiscordMessage(DiscordMessage other)
: this()
{
this.Discord = other.Discord;
this.AttachmentsInternal = other.AttachmentsInternal; // the attachments cannot change, thus no need to copy and reallocate.
this.EmbedsInternal = new List(other.EmbedsInternal);
if (other.MentionedChannelsInternal != null)
this.MentionedChannelsInternal = new List(other.MentionedChannelsInternal);
if (other.MentionedRolesInternal != null)
this.MentionedRolesInternal = new List(other.MentionedRolesInternal);
if (other.MentionedRoleIds != null)
this.MentionedRoleIds = new List(other.MentionedRoleIds);
this.MentionedUsersInternal = new List(other.MentionedUsersInternal);
this.ReactionsInternal = new List(other.ReactionsInternal);
this.StickersInternal = new List(other.StickersInternal);
this.Author = other.Author;
this.ChannelId = other.ChannelId;
this.Content = other.Content;
this.EditedTimestampRaw = other.EditedTimestampRaw;
this.Id = other.Id;
this.IsTts = other.IsTts;
this.MessageType = other.MessageType;
this.Pinned = other.Pinned;
this.TimestampRaw = other.TimestampRaw;
this.WebhookId = other.WebhookId;
}
///
/// Gets the channel in which the message was sent.
///
[JsonIgnore]
public DiscordChannel Channel
{
get => (this.Discord as DiscordClient)?.InternalGetCachedChannel(this.ChannelId) ?? this._channel;
internal set => this._channel = value;
}
private DiscordChannel _channel;
///
/// Gets the thread in which the message was sent.
///
[JsonIgnore]
private DiscordThreadChannel INTERNAL_THREAD
{
get => (this.Discord as DiscordClient)?.InternalGetCachedThread(this.ChannelId) ?? this._thread;
set => this._thread = value;
}
private DiscordThreadChannel _thread;
///
/// Gets the ID of the channel in which the message was sent.
///
[JsonProperty("channel_id", NullValueHandling = NullValueHandling.Ignore)]
public ulong ChannelId { get; internal set; }
///
/// Gets the components this message was sent with.
///
[JsonProperty("components", NullValueHandling = NullValueHandling.Ignore)]
public IReadOnlyCollection Components { get; internal set; }
///
/// Gets the user or member that sent the message.
///
[JsonProperty("author", NullValueHandling = NullValueHandling.Ignore)]
public DiscordUser Author { get; internal set; }
///
/// Gets the message's content.
///
[JsonProperty("content", NullValueHandling = NullValueHandling.Ignore)]
public string Content { get; internal set; }
///
/// Gets the message's creation timestamp.
///
[JsonIgnore]
public DateTimeOffset Timestamp
=> !string.IsNullOrWhiteSpace(this.TimestampRaw) && DateTimeOffset.TryParse(this.TimestampRaw, CultureInfo.InvariantCulture, DateTimeStyles.None, out var dto) ?
dto : this.CreationTimestamp;
///
/// Gets the message's creation timestamp as raw string.
///
[JsonProperty("timestamp", NullValueHandling = NullValueHandling.Ignore)]
internal string TimestampRaw { get; set; }
///
/// Gets the message's edit timestamp. Will be null if the message was not edited.
///
[JsonIgnore]
public DateTimeOffset? EditedTimestamp
=> !string.IsNullOrWhiteSpace(this.EditedTimestampRaw) && DateTimeOffset.TryParse(this.EditedTimestampRaw, CultureInfo.InvariantCulture, DateTimeStyles.None, out var dto) ?
(DateTimeOffset?)dto : null;
///
/// Gets the message's edit timestamp as raw string. Will be null if the message was not edited.
///
[JsonProperty("edited_timestamp", NullValueHandling = NullValueHandling.Ignore)]
internal string EditedTimestampRaw { get; set; }
///
/// Gets whether this message was edited.
///
[JsonIgnore]
public bool IsEdited
=> !string.IsNullOrWhiteSpace(this.EditedTimestampRaw);
///
/// Gets whether the message is a text-to-speech message.
///
[JsonProperty("tts", NullValueHandling = NullValueHandling.Ignore)]
public bool IsTts { get; internal set; }
///
/// Gets whether the message mentions everyone.
///
[JsonProperty("mention_everyone", NullValueHandling = NullValueHandling.Ignore)]
public bool MentionEveryone { get; internal set; }
///
/// Gets users or members mentioned by this message.
///
[JsonIgnore]
public IReadOnlyList MentionedUsers
=> this.MentionedUsersLazy.Value;
[JsonProperty("mentions", NullValueHandling = NullValueHandling.Ignore)]
internal List MentionedUsersInternal;
[JsonIgnore]
internal readonly Lazy> MentionedUsersLazy;
// TODO: this will probably throw an exception in DMs since it tries to wrap around a null List...
// this is probably low priority but need to find out a clean way to solve it...
///
/// Gets roles mentioned by this message.
///
[JsonIgnore]
public IReadOnlyList MentionedRoles
=> this._mentionedRolesLazy.Value;
[JsonIgnore]
internal List MentionedRolesInternal;
[JsonProperty("mention_roles")]
internal List MentionedRoleIds;
[JsonIgnore]
private readonly Lazy> _mentionedRolesLazy;
///
/// Gets channels mentioned by this message.
///
[JsonIgnore]
public IReadOnlyList MentionedChannels
=> this._mentionedChannelsLazy.Value;
[JsonIgnore]
internal List MentionedChannelsInternal;
[JsonIgnore]
private readonly Lazy> _mentionedChannelsLazy;
///
/// Gets files attached to this message.
///
[JsonIgnore]
public IReadOnlyList Attachments
=> this._attachmentsLazy.Value;
[JsonProperty("attachments", NullValueHandling = NullValueHandling.Ignore)]
internal List AttachmentsInternal = new();
[JsonIgnore]
private readonly Lazy> _attachmentsLazy;
///
/// Gets embeds attached to this message.
///
[JsonIgnore]
public IReadOnlyList Embeds
=> this._embedsLazy.Value;
[JsonProperty("embeds", NullValueHandling = NullValueHandling.Ignore)]
internal List EmbedsInternal = new();
[JsonIgnore]
private readonly Lazy> _embedsLazy;
///
/// Gets reactions used on this message.
///
[JsonIgnore]
public IReadOnlyList Reactions
=> this._reactionsLazy.Value;
[JsonProperty("reactions", NullValueHandling = NullValueHandling.Ignore)]
internal List ReactionsInternal = new();
[JsonIgnore]
private readonly Lazy> _reactionsLazy;
///
/// Gets the nonce sent with the message, if the message was sent by the client.
///
[JsonProperty("nonce", NullValueHandling = NullValueHandling.Ignore)]
public string Nonce { get; internal set; }
///
/// Gets whether the message is pinned.
///
[JsonProperty("pinned", NullValueHandling = NullValueHandling.Ignore)]
public bool Pinned { get; internal set; }
///
/// Gets the id of the webhook that generated this message.
///
[JsonProperty("webhook_id", NullValueHandling = NullValueHandling.Ignore)]
public ulong? WebhookId { get; internal set; }
///
/// Gets the type of the message.
///
[JsonProperty("type", NullValueHandling = NullValueHandling.Ignore)]
public MessageType? MessageType { get; internal set; }
///
/// Gets the message activity in the Rich Presence embed.
///
[JsonProperty("activity", NullValueHandling = NullValueHandling.Ignore)]
public DiscordMessageActivity Activity { get; internal set; }
///
/// Gets the message application in the Rich Presence embed.
///
[JsonProperty("application", NullValueHandling = NullValueHandling.Ignore)]
public DiscordMessageApplication Application { get; internal set; }
///
/// Gets the message application id in the Rich Presence embed.
///
[JsonProperty("application_id", NullValueHandling = NullValueHandling.Ignore)]
public ulong ApplicationId { get; internal set; }
///
/// Gets the internal reference.
///
[JsonProperty("message_reference", NullValueHandling = NullValueHandling.Ignore)]
internal InternalDiscordMessageReference? InternalReference { get; set; }
///
/// Gets the original message reference from the crossposted message.
///
[JsonIgnore]
public DiscordMessageReference Reference
=> this.InternalReference.HasValue ? this?.InternalBuildMessageReference() : null;
///
/// Gets the bitwise flags for this message.
///
[JsonProperty("flags", NullValueHandling = NullValueHandling.Ignore)]
public MessageFlags? Flags { get; internal set; }
///
/// Gets whether the message originated from a webhook.
///
[JsonIgnore]
public bool WebhookMessage
=> this.WebhookId != null;
///
/// Gets the jump link to this message.
///
[JsonIgnore]
public Uri JumpLink => this._jumpLink.Value;
private readonly Lazy _jumpLink;
///
/// Gets stickers for this message.
///
[JsonIgnore]
public IReadOnlyList Stickers
=> this._stickersLazy.Value;
[JsonProperty("sticker_items", NullValueHandling = NullValueHandling.Ignore)]
internal List StickersInternal = new();
[JsonIgnore]
private readonly Lazy> _stickersLazy;
///
/// Gets the guild id.
///
[JsonProperty("guild_id", NullValueHandling = NullValueHandling.Ignore)]
internal ulong? GuildId { get; set; }
///
/// Gets the message object for the referenced message
///
[JsonProperty("referenced_message", NullValueHandling = NullValueHandling.Ignore)]
public DiscordMessage ReferencedMessage { get; internal set; }
///
/// Gets whether the message is a response to an interaction.
///
[JsonProperty("interaction", NullValueHandling = NullValueHandling.Ignore)]
public DiscordMessageInteraction Interaction { get; internal set; }
///
/// Gets the thread that was started from this message.
///
[JsonProperty("thread", NullValueHandling = NullValueHandling.Ignore)]
public DiscordThreadChannel Thread { get; internal set; }
///
/// Build the message reference.
///
internal DiscordMessageReference InternalBuildMessageReference()
{
var client = this.Discord as DiscordClient;
var guildId = this.InternalReference.Value.GuildId;
var channelId = this.InternalReference.Value.ChannelId;
var messageId = this.InternalReference.Value.MessageId;
var reference = new DiscordMessageReference();
if (guildId.HasValue)
reference.Guild = client.GuildsInternal.TryGetValue(guildId.Value, out var g)
? g
: new DiscordGuild
{
Id = guildId.Value,
Discord = client
};
var channel = client.InternalGetCachedChannel(channelId.Value);
if (channel == null)
{
reference.Channel = new DiscordChannel
{
Id = channelId.Value,
Discord = client
};
if (guildId.HasValue)
reference.Channel.GuildId = guildId.Value;
}
else reference.Channel = channel;
if (client.MessageCache != null && client.MessageCache.TryGet(m => m.Id == messageId.Value && m.ChannelId == channelId, out var msg))
reference.Message = msg;
else
{
reference.Message = new DiscordMessage
{
ChannelId = this.ChannelId,
Discord = client
};
if (messageId.HasValue)
reference.Message.Id = messageId.Value;
}
return reference;
}
///
/// Gets the mentions.
///
/// An array of IMentions.
private List GetMentions()
{
var mentions = new List();
if (this.ReferencedMessage != null && this.MentionedUsersInternal.Contains(this.ReferencedMessage.Author))
mentions.Add(new RepliedUserMention());
if (this.MentionedUsersInternal.Any())
mentions.AddRange(this.MentionedUsersInternal.Select(m => (IMention)new UserMention(m)));
if (this.MentionedRoleIds.Any())
mentions.AddRange(this.MentionedRoleIds.Select(r => (IMention)new RoleMention(r)));
return mentions;
}
///
/// Populates the mentions.
///
internal void PopulateMentions()
{
var guild = this.Channel?.Guild;
this.MentionedUsersInternal ??= new List();
this.MentionedRolesInternal ??= new List();
this.MentionedChannelsInternal ??= new List();
var mentionedUsers = new HashSet(new DiscordUserComparer());
if (guild != null)
{
foreach (var usr in this.MentionedUsersInternal)
{
usr.Discord = this.Discord;
this.Discord.UserCache.AddOrUpdate(usr.Id, usr, (id, old) =>
{
old.Username = usr.Username;
old.Discriminator = usr.Discriminator;
old.AvatarHash = usr.AvatarHash;
return old;
});
mentionedUsers.Add(guild.MembersInternal.TryGetValue(usr.Id, out var member) ? member : usr);
}
}
if (!string.IsNullOrWhiteSpace(this.Content))
{
//mentionedUsers.UnionWith(Utilities.GetUserMentions(this).Select(this.Discord.GetCachedOrEmptyUserInternal));
if (guild != null)
{
//this._mentionedRoles = this._mentionedRoles.Union(Utilities.GetRoleMentions(this).Select(xid => guild.GetRole(xid))).ToList();
this.MentionedRolesInternal = this.MentionedRolesInternal.Union(this.MentionedRoleIds.Select(xid => guild.GetRole(xid))).ToList();
this.MentionedChannelsInternal = this.MentionedChannelsInternal.Union(Utilities.GetChannelMentions(this).Select(xid => guild.GetChannel(xid))).ToList();
}
}
this.MentionedUsersInternal = mentionedUsers.ToList();
}
///
/// Edits the message.
///
/// New content.
/// Thrown when the client tried to modify a message not sent by them.
/// Thrown when the member does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public Task ModifyAsync(Optional content)
=> this.Discord.ApiClient.EditMessageAsync(this.ChannelId, this.Id, content, default, this.GetMentions(), default, default, Array.Empty(), default);
///
/// Edits the message.
///
/// New embed.
/// Thrown when the client tried to modify a message not sent by them.
/// Thrown when the member does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public Task ModifyAsync(Optional embed = default)
=> this.Discord.ApiClient.EditMessageAsync(this.ChannelId, this.Id, default, embed.Map(v => new[] { v }).ValueOr(Array.Empty()), this.GetMentions(), default, default, Array.Empty(), default);
///
/// Edits the message.
///
/// New content.
/// New embed.
/// Thrown when the client tried to modify a message not sent by them.
/// Thrown when the member does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public Task ModifyAsync(Optional content, Optional embed = default)
=> this.Discord.ApiClient.EditMessageAsync(this.ChannelId, this.Id, content, embed.Map(v => new[] { v }).ValueOr(Array.Empty()), this.GetMentions(), default, default, Array.Empty(), default);
///
/// Edits the message.
///
/// New content.
/// New embeds.
/// Thrown when the client tried to modify a message not sent by them.
/// Thrown when the member does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public Task ModifyAsync(Optional content, Optional> embeds = default)
=> this.Discord.ApiClient.EditMessageAsync(this.ChannelId, this.Id, content, embeds, this.GetMentions(), default, default, Array.Empty(), default);
///
/// Edits the message.
///
/// The builder of the message to edit.
/// Thrown when the client tried to modify a message not sent by them.
/// Thrown when the member does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public async Task ModifyAsync(DiscordMessageBuilder builder)
{
builder.Validate(true);
return await this.Discord.ApiClient.EditMessageAsync(this.ChannelId, this.Id, builder.Content, Optional.Some(builder.Embeds.AsEnumerable()), builder.Mentions, builder.Components, builder.Suppressed, builder.Files, builder.Attachments.Count > 0 ? Optional.Some(builder.Attachments.AsEnumerable()) : builder.KeepAttachmentsInternal.HasValue ? builder.KeepAttachmentsInternal.Value ? Optional.Some(this.Attachments.AsEnumerable()) : Array.Empty() : null);
}
///
/// Edits the message embed suppression.
///
/// Suppress embeds.
/// Thrown when the client tried to modify a message not sent by them.
/// Thrown when the member does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public Task ModifySuppressionAsync(bool suppress = false)
=> this.Discord.ApiClient.EditMessageAsync(this.ChannelId, this.Id, default, default, default, default, suppress, default, default);
///
/// Clears all attachments from the message.
///
///
public Task ClearAttachmentsAsync()
=> this.Discord.ApiClient.EditMessageAsync(this.ChannelId, this.Id, default, default, this.GetMentions(), default, default, default, Array.Empty());
///
/// Edits the message.
///
/// The builder of the message to edit.
/// Thrown when the client tried to modify a message not sent by them.
/// Thrown when the member 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 builder = new DiscordMessageBuilder();
action(builder);
builder.Validate(true);
return await this.Discord.ApiClient.EditMessageAsync(this.ChannelId, this.Id, builder.Content, Optional.Some(builder.Embeds.AsEnumerable()), builder.Mentions, builder.Components, builder.Suppressed, builder.Files, builder.Attachments.Count > 0 ? Optional.Some(builder.Attachments.AsEnumerable()) : builder.KeepAttachmentsInternal.HasValue ? builder.KeepAttachmentsInternal.Value ? Optional.Some(this.Attachments.AsEnumerable()) : Array.Empty() : null);
}
///
/// Deletes the message.
///
/// Thrown when the client does not have the permission.
/// Thrown when the member 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.DeleteMessageAsync(this.ChannelId, this.Id, reason);
///
/// Creates a thread.
/// Depending on the of the parent channel it's either a or a .
///
/// The name of the thread.
/// till it gets archived. Defaults to
/// The per user ratelimit, aka slowdown.
/// The reason.
/// 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.
/// Thrown when the cannot be modified.
public async Task CreateThreadAsync(string name, ThreadAutoArchiveDuration autoArchiveDuration = ThreadAutoArchiveDuration.OneHour, int? rateLimitPerUser = null, string reason = null) =>
Utilities.CheckThreadAutoArchiveDurationFeature(this.Channel.Guild, autoArchiveDuration)
? await this.Discord.ApiClient.CreateThreadAsync(this.ChannelId, this.Id, name, autoArchiveDuration, this.Channel.Type == ChannelType.News ? ChannelType.NewsThread : ChannelType.PublicThread, rateLimitPerUser, reason)
: throw new NotSupportedException($"Cannot modify ThreadAutoArchiveDuration. Guild needs boost tier {(autoArchiveDuration == ThreadAutoArchiveDuration.ThreeDays ? "one" : "two")}.");
///
/// Pins the message in its channel.
///
/// Thrown when the client does not have the permission.
/// Thrown when the member does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public Task PinAsync()
=> this.Discord.ApiClient.PinMessageAsync(this.ChannelId, this.Id);
///
/// Unpins the message in its channel.
///
/// Thrown when the client does not have the permission.
/// Thrown when the member does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public Task UnpinAsync()
=> this.Discord.ApiClient.UnpinMessageAsync(this.ChannelId, this.Id);
///
/// Responds to the message. This produces a reply.
///
/// Message content to respond with.
/// The sent message.
/// Thrown when the client does not have the permission.
/// Thrown when the member does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public Task RespondAsync(string content)
=> this.Discord.ApiClient.CreateMessageAsync(this.ChannelId, content, null, sticker: null, replyMessageId: this.Id, mentionReply: false, failOnInvalidReply: false);
///
/// Responds to the message. This produces a reply.
///
/// Embed to attach to the message.
/// The sent message.
/// Thrown when the client does not have the permission.
/// Thrown when the member does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public Task RespondAsync(DiscordEmbed embed)
=> this.Discord.ApiClient.CreateMessageAsync(this.ChannelId, null, embed != null ? new[] { embed } : null, sticker: null, replyMessageId: this.Id, mentionReply: false, failOnInvalidReply: false);
///
/// Responds to the message. This produces a reply.
///
/// Message content to respond with.
/// Embed to attach to the message.
/// The sent message.
/// Thrown when the client does not have the permission.
/// Thrown when the member does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public Task RespondAsync(string content, DiscordEmbed embed)
=> this.Discord.ApiClient.CreateMessageAsync(this.ChannelId, content, embed != null ? new[] { embed } : null, sticker: null, replyMessageId: this.Id, mentionReply: false, failOnInvalidReply: false);
///
/// Responds to the message. This produces a reply.
///
/// The Discord message builder.
/// The sent message.
/// Thrown when the client does not have the permission.
/// Thrown when the member does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public Task RespondAsync(DiscordMessageBuilder builder)
=> this.Discord.ApiClient.CreateMessageAsync(this.ChannelId, builder.WithReply(this.Id, mention: false, failOnInvalidReply: false));
///
/// Responds to the message. This produces a reply.
///
/// The Discord message builder.
/// The sent message.
/// Thrown when the client does not have the permission.
/// Thrown when the member does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public Task RespondAsync(Action action)
{
var builder = new DiscordMessageBuilder();
action(builder);
return this.Discord.ApiClient.CreateMessageAsync(this.ChannelId, builder.WithReply(this.Id, mention: false, failOnInvalidReply: false));
}
///
/// Creates a reaction to this message.
///
/// The emoji you want to react with, either an emoji or name:id
/// Thrown when the client does not have the permission.
/// Thrown when the emoji does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public Task CreateReactionAsync(DiscordEmoji emoji)
=> this.Discord.ApiClient.CreateReactionAsync(this.ChannelId, this.Id, emoji.ToReactionString());
///
/// Deletes your own reaction
///
/// Emoji for the reaction you want to remove, either an emoji or name:id
/// Thrown when the emoji does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public Task DeleteOwnReactionAsync(DiscordEmoji emoji)
=> this.Discord.ApiClient.DeleteOwnReactionAsync(this.ChannelId, this.Id, emoji.ToReactionString());
///
/// Deletes another user's reaction.
///
/// Emoji for the reaction you want to remove, either an emoji or name:id.
/// Member you want to remove the reaction for
/// Reason for audit logs.
/// Thrown when the client does not have the permission.
/// Thrown when the emoji does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public Task DeleteReactionAsync(DiscordEmoji emoji, DiscordUser user, string reason = null)
=> this.Discord.ApiClient.DeleteUserReactionAsync(this.ChannelId, this.Id, user.Id, emoji.ToReactionString(), reason);
///
/// Gets users that reacted with this emoji.
///
/// Emoji to react with.
/// Limit of users to fetch.
/// Fetch users after this user's id.
/// Thrown when the emoji does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public Task> GetReactionsAsync(DiscordEmoji emoji, int limit = 25, ulong? after = null)
=> this.GetReactionsInternalAsync(emoji, limit, after);
///
/// Deletes all reactions for this message.
///
/// Reason for audit logs.
/// Thrown when the client does not have the permission.
/// Thrown when the emoji does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public Task DeleteAllReactionsAsync(string reason = null)
=> this.Discord.ApiClient.DeleteAllReactionsAsync(this.ChannelId, this.Id, reason);
///
/// Deletes all reactions of a specific reaction for this message.
///
/// The emoji to clear, either an emoji or name:id.
/// Thrown when the client does not have the permission.
/// Thrown when the emoji does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public Task DeleteReactionsEmojiAsync(DiscordEmoji emoji)
=> this.Discord.ApiClient.DeleteReactionsEmojiAsync(this.ChannelId, this.Id, emoji.ToReactionString());
///
/// Gets the reactions.
///
/// The emoji to search for.
/// The limit of results.
/// Get the reasctions after snowflake.
private async Task> GetReactionsInternalAsync(DiscordEmoji emoji, int limit = 25, ulong? after = null)
{
if (limit < 0)
throw new ArgumentException("Cannot get a negative number of reactions' users.");
if (limit == 0)
return Array.Empty();
var users = new List(limit);
var remaining = limit;
var last = after;
int lastCount;
do
{
var fetchSize = remaining > 100 ? 100 : remaining;
var fetch = await this.Discord.ApiClient.GetReactionsAsync(this.Channel.Id, this.Id, emoji.ToReactionString(), last, fetchSize).ConfigureAwait(false);
lastCount = fetch.Count;
remaining -= lastCount;
users.AddRange(fetch);
last = fetch.LastOrDefault()?.Id;
} while (remaining > 0 && lastCount > 0);
return new ReadOnlyCollection(users);
}
///
/// Returns a string representation of this message.
///
/// String representation of this message.
public override string ToString()
=> $"Message {this.Id}; Attachment count: {this.AttachmentsInternal.Count}; Embed count: {this.EmbedsInternal.Count}; Contents: {this.Content}";
///
/// 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 DiscordMessage);
///
/// Checks whether this is equal to another .
///
/// to compare to.
/// Whether the is equal to this .
public bool Equals(DiscordMessage e)
=> e is not null && (ReferenceEquals(this, e) || (this.Id == e.Id && this.ChannelId == e.ChannelId));
///
/// Gets the hash code for this .
///
/// The hash code for this .
public override int GetHashCode()
{
var hash = 13;
hash = (hash * 7) + this.Id.GetHashCode();
hash = (hash * 7) + this.ChannelId.GetHashCode();
return hash;
}
///
/// Gets whether the two objects are equal.
///
/// First message to compare.
/// Second message to compare.
/// Whether the two messages are equal.
public static bool operator ==(DiscordMessage e1, DiscordMessage 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 && e1.ChannelId == e2.ChannelId));
}
///
/// Gets whether the two objects are not equal.
///
/// First message to compare.
/// Second message to compare.
/// Whether the two messages are not equal.
public static bool operator !=(DiscordMessage e1, DiscordMessage e2)
=> !(e1 == e2);
}
diff --git a/DisCatSharp/Entities/Channel/ForumPostTag.cs b/DisCatSharp/Entities/Thread/ForumPostTag.cs
similarity index 95%
rename from DisCatSharp/Entities/Channel/ForumPostTag.cs
rename to DisCatSharp/Entities/Thread/ForumPostTag.cs
index bf23a06f3..a957103ab 100644
--- a/DisCatSharp/Entities/Channel/ForumPostTag.cs
+++ b/DisCatSharp/Entities/Thread/ForumPostTag.cs
@@ -1,103 +1,110 @@
// This file is part of the DisCatSharp project, based off DSharpPlus.
//
// Copyright (c) 2021-2022 AITSYS
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
using System;
using Newtonsoft.Json;
namespace DisCatSharp.Entities;
///
/// Represents a discord forum post tag.
///
public class ForumPostTag : SnowflakeObject, IEquatable
{
///
/// Gets the name of this forum post tag.
///
[JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)]
public string Name { get; internal set; }
///
/// Gets the emoji id of the forum post tag.
///
[JsonProperty("emoji_id", NullValueHandling = NullValueHandling.Ignore)]
public ulong? EmojiId { get; internal set; }
///
/// Gets the unicode emoji of the forum post tag.
///
[JsonProperty("emoji_name", NullValueHandling = NullValueHandling.Ignore)]
internal string UnicodeEmojiString;
+
+ ///
+ /// Gets whether the tag can only be used by moderators.
+ ///
+ [JsonProperty("moderated", NullValueHandling = NullValueHandling.Ignore)]
+ public bool Moderated { get; internal set; }
+
///
/// Gets the unicode emoji.
///
public DiscordEmoji UnicodeEmoji
=> this.UnicodeEmojiString != null ? DiscordEmoji.FromName(this.Discord, $":{this.UnicodeEmojiString}:", false) : null;
///
/// Checks whether this is equal to another object.
///
/// Object to compare to.
/// Whether the object is equal to this .
public override bool Equals(object obj)
=> this.Equals(obj as ForumPostTag);
///
/// Checks whether this is equal to another .
///
/// to compare to.
/// Whether the is equal to this .
public bool Equals(ForumPostTag e)
=> e is not null && (ReferenceEquals(this, e) || (this.Id == e.Id && this.Name == e.Name));
///
/// Gets the hash code for this .
///
/// The hash code for this .
public override int GetHashCode()
=> this.Id.GetHashCode();
///
/// Gets whether the two objects are equal.
///
/// First forum post tag to compare.
/// Second forum post tag to compare.
/// Whether the two forum post tags are equal.
public static bool operator ==(ForumPostTag e1, ForumPostTag e2)
{
var o1 = e1 as object;
var o2 = e2 as object;
return (o1 != null || o2 == null) && (o1 == null || o2 != null) && ((o1 == null && o2 == null) || e1.Id == e2.Id);
}
///
/// Gets whether the two objects are not equal.
///
/// First forum post tag to compare.
/// Second forum post tag to compare.
/// Whether the two forum post tags are not equal.
public static bool operator !=(ForumPostTag e1, ForumPostTag e2)
=> !(e1 == e2);
}
diff --git a/DisCatSharp/Enums/Channel/ChannelFlags.cs b/DisCatSharp/Enums/Channel/ChannelFlags.cs
index 34e10bf2b..923a7e883 100644
--- a/DisCatSharp/Enums/Channel/ChannelFlags.cs
+++ b/DisCatSharp/Enums/Channel/ChannelFlags.cs
@@ -1,40 +1,56 @@
// This file is part of the DisCatSharp project, based off DSharpPlus.
//
// Copyright (c) 2021-2022 AITSYS
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
-namespace DisCatSharp;
+using DisCatSharp.Entities;
+
+namespace DisCatSharp.Enums;
///
/// Represents a channel's flags.
///
public enum ChannelFlags : int
{
///
- /// Indicates that this channel is removed from the guilds home feed.
+ /// Indicates that this channel is removed from the guilds home feed / from highlights.
+ /// Applicable for Text, Forum and News.
///
RemovedFromHome = 1 << 0,
+ RemovedFromHighlights = RemovedFromHome,
///
/// Indicates that this thread is pinned to the top of its parent forum channel.
/// Forum channel thread only.
///
- Pinned = 1 << 1
+ Pinned = 1 << 1,
+
+ ///
+ /// Indicates that this channel is removed from the active now within the guilds home feed.
+ /// Applicable for Text, News, Thread, Forum, Stage and Voice.
+ ///
+ RemovedFromActiveNow = 1 << 2,
+
+ ///
+ /// Indicates that the channel requires users to select at least one .
+ /// Only applicable for .
+ ///
+ RequireTags = 1<<4
}
diff --git a/DisCatSharp/Enums/Channel/ChannelType.cs b/DisCatSharp/Enums/Channel/ChannelType.cs
index 8fbd96e43..df8de5367 100644
--- a/DisCatSharp/Enums/Channel/ChannelType.cs
+++ b/DisCatSharp/Enums/Channel/ChannelType.cs
@@ -1,101 +1,101 @@
// This file is part of the DisCatSharp project, based off DSharpPlus.
//
// Copyright (c) 2021-2022 AITSYS
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
-namespace DisCatSharp;
+namespace DisCatSharp.Enums;
///
/// Represents a channel's type.
///
public enum ChannelType : int
{
///
/// Indicates that this is a text channel.
///
Text = 0,
///
/// Indicates that this is a private channel.
///
Private = 1,
///
/// Indicates that this is a voice channel.
///
Voice = 2,
///
/// Indicates that this is a group direct message channel.
///
Group = 3,
///
/// Indicates that this is a channel category.
///
Category = 4,
///
/// Indicates that this is a news channel.
///
News = 5,
///
/// Indicates that this is a store channel.
///
Store = 6,
///
/// Indicates that this is a temporary sub-channel within a news channel.
///
NewsThread = 10,
///
/// Indicates that this is a temporary sub-channel within a text channel.
///
PublicThread = 11,
///
/// Indicates that this is a temporary sub-channel within a text channel that is only viewable
/// by those invited and those with the MANAGE_THREADS permission.
///
PrivateThread = 12,
///
/// Indicates that this is a stage channel.
///
Stage = 13,
///
/// Indicates that this is a guild directory channel.
/// This is used for hub guilds (feature for schools).
///
GuildDirectory = 14,
///
/// Indicates that this is a guild forum channel (Threads only channel).
///
Forum = 15,
///
/// Indicates unknown channel type.
///
Unknown = int.MaxValue
}
diff --git a/DisCatSharp/Enums/Channel/OverwriteType.cs b/DisCatSharp/Enums/Channel/OverwriteType.cs
index bb336b64b..e60487e06 100644
--- a/DisCatSharp/Enums/Channel/OverwriteType.cs
+++ b/DisCatSharp/Enums/Channel/OverwriteType.cs
@@ -1,39 +1,39 @@
// This file is part of the DisCatSharp project, based off DSharpPlus.
//
// Copyright (c) 2021-2022 AITSYS
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
-namespace DisCatSharp;
+namespace DisCatSharp.Enums;
///
/// Represents a channel permission overwrite's type.
///
public enum OverwriteType : int
{
///
/// Specifies that this overwrite applies to a role.
///
Role = 0,
///
/// Specifies that this overwrite applies to a member.
///
Member = 1,
}
diff --git a/DisCatSharp/EventArgs/Thread/ThreadDeleteEventArgs.cs b/DisCatSharp/EventArgs/Thread/ThreadDeleteEventArgs.cs
index 8d2a39c6b..fb364627d 100644
--- a/DisCatSharp/EventArgs/Thread/ThreadDeleteEventArgs.cs
+++ b/DisCatSharp/EventArgs/Thread/ThreadDeleteEventArgs.cs
@@ -1,58 +1,59 @@
// This file is part of the DisCatSharp project, based off DSharpPlus.
//
// Copyright (c) 2021-2022 AITSYS
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
using System;
using DisCatSharp.Entities;
+using DisCatSharp.Enums;
namespace DisCatSharp.EventArgs;
///
/// Represents arguments for event.
///
public class ThreadDeleteEventArgs : DiscordEventArgs
{
///
/// Gets the thread that was deleted.
///
public DiscordThreadChannel Thread { get; internal set; }
///
/// Gets the threads parent channel.
///
public DiscordChannel Parent { get; internal set; }
///
/// Gets the guild this thread belonged to.
///
public DiscordGuild Guild { get; internal set; }
///
/// Gets the threads type.
///
public ChannelType Type { get; internal set; }
///
/// Initializes a new instance of the class.
///
internal ThreadDeleteEventArgs(IServiceProvider provider) : base(provider) { }
}
diff --git a/DisCatSharp/Internals.cs b/DisCatSharp/Internals.cs
index 68939f900..aba2cd938 100644
--- a/DisCatSharp/Internals.cs
+++ b/DisCatSharp/Internals.cs
@@ -1,93 +1,94 @@
// This file is part of the DisCatSharp project, based off DSharpPlus.
//
// Copyright (c) 2021-2022 AITSYS
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
using System.Collections.Generic;
using System.Text;
using DisCatSharp.Entities;
+using DisCatSharp.Enums;
namespace DisCatSharp;
///
/// Internal tools.
///
public static class Internals
{
///
/// Gets the version of the library
///
private static string s_versionHeader
=> Utilities.VersionHeader;
///
/// Gets the permission strings.
///
private static Dictionary s_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 can have threads.
///
/// The channel.
internal static bool IsThreadHolder(this DiscordChannel channel) => channel.Type == ChannelType.Text || channel.Type == ChannelType.News || channel.Type == ChannelType.Forum;
///
/// 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 IsWritable(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.Forum || channel.Type == ChannelType.News;
///
/// Whether the is moveable.
///
/// 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.Forum || channel.Type == ChannelType.News;
}
diff --git a/DisCatSharp/Net/Abstractions/Rest/RestChannelPayloads.cs b/DisCatSharp/Net/Abstractions/Rest/RestChannelPayloads.cs
index 4f81783b1..359df4bd0 100644
--- a/DisCatSharp/Net/Abstractions/Rest/RestChannelPayloads.cs
+++ b/DisCatSharp/Net/Abstractions/Rest/RestChannelPayloads.cs
@@ -1,504 +1,505 @@
// This file is part of the DisCatSharp project, based off DSharpPlus.
//
// Copyright (c) 2021-2022 AITSYS
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
using System.Collections.Generic;
using DisCatSharp.Entities;
+using DisCatSharp.Enums;
using Newtonsoft.Json;
namespace DisCatSharp.Net.Abstractions;
///
/// Represents a channel create payload.
///
internal sealed class RestChannelCreatePayload
{
///
/// Gets or sets the name.
///
[JsonProperty("name")]
public string Name { get; set; }
///
/// Gets or sets the type.
///
[JsonProperty("type")]
public ChannelType Type { get; set; }
///
/// Gets or sets the parent.
///
[JsonProperty("parent_id", NullValueHandling = NullValueHandling.Ignore)]
public ulong? Parent { get; set; }
///
/// Gets or sets the topic.
///
[JsonProperty("topic")]
public Optional Topic { get; set; }
///
/// Gets or sets the bitrate.
///
[JsonProperty("bitrate", NullValueHandling = NullValueHandling.Ignore)]
public int? Bitrate { get; set; }
///
/// Gets or sets the user limit.
///
[JsonProperty("user_limit", NullValueHandling = NullValueHandling.Ignore)]
public int? UserLimit { get; set; }
///
/// Gets or sets the permission overwrites.
///
[JsonProperty("permission_overwrites", NullValueHandling = NullValueHandling.Ignore)]
public IEnumerable PermissionOverwrites { get; set; }
///
/// Gets or sets a value indicating whether nsfw.
///
[JsonProperty("nsfw", NullValueHandling = NullValueHandling.Ignore)]
public bool? Nsfw { get; set; }
///
/// Gets or sets the per user rate limit.
///
[JsonProperty("rate_limit_per_user")]
public Optional PerUserRateLimit { get; set; }
///
/// Gets or sets the quality mode.
///
[JsonProperty("video_quality_mode", NullValueHandling = NullValueHandling.Ignore)]
public VideoQualityMode? QualityMode { get; set; }
///
/// Gets or sets the default auto archive duration.
///
[JsonProperty("default_auto_archive_duration", NullValueHandling = NullValueHandling.Ignore)]
public ThreadAutoArchiveDuration? DefaultAutoArchiveDuration { get; set; }
}
///
/// Represents a channel modify payload.
///
internal sealed class RestChannelModifyPayload
{
///
/// Gets or sets the name.
///
[JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)]
public string Name { get; set; }
///
/// Gets or sets the type.
///
[JsonProperty("type")]
public Optional Type { get; set; }
///
/// Gets or sets the position.
///
[JsonProperty("position", NullValueHandling = NullValueHandling.Ignore)]
public int? Position { get; set; }
///
/// Gets or sets the topic.
///
[JsonProperty("topic")]
public Optional Topic { get; set; }
///
/// Gets or sets a value indicating whether nsfw.
///
[JsonProperty("nsfw", NullValueHandling = NullValueHandling.Ignore)]
public bool? Nsfw { get; set; }
///
/// Gets or sets the parent.
///
[JsonProperty("parent_id")]
public Optional Parent { get; set; }
///
/// Gets or sets the bitrate.
///
[JsonProperty("bitrate", NullValueHandling = NullValueHandling.Ignore)]
public int? Bitrate { get; set; }
///
/// Gets or sets the user limit.
///
[JsonProperty("user_limit", NullValueHandling = NullValueHandling.Ignore)]
public int? UserLimit { get; set; }
///
/// Gets or sets the per user rate limit.
///
[JsonProperty("rate_limit_per_user")]
public Optional PerUserRateLimit { get; set; }
///
/// Gets or sets the rtc region.
///
[JsonProperty("rtc_region")]
public Optional RtcRegion { get; set; }
///
/// Gets or sets the quality mode.
///
[JsonProperty("video_quality_mode", NullValueHandling = NullValueHandling.Ignore)]
public VideoQualityMode? QualityMode { get; set; }
///
/// Gets or sets the default auto archive duration.
///
[JsonProperty("default_auto_archive_duration", NullValueHandling = NullValueHandling.Ignore)]
public ThreadAutoArchiveDuration? DefaultAutoArchiveDuration { get; set; }
///
/// Gets or sets the permission overwrites.
///
[JsonProperty("permission_overwrites", NullValueHandling = NullValueHandling.Ignore)]
public IEnumerable PermissionOverwrites { get; set; }
///
/// Gets or sets the banner base64.
///
[JsonProperty("banner")]
public Optional BannerBase64 { get; set; }
}
///
/// Represents a channel message edit payload.
///
internal class RestChannelMessageEditPayload
{
///
/// Gets or sets the content.
///
[JsonProperty("content", NullValueHandling = NullValueHandling.Include)]
public string Content { get; set; }
///
/// Gets or sets a value indicating whether has content.
///
[JsonIgnore]
public bool HasContent { get; set; }
///
/// Gets or sets the embeds.
///
[JsonProperty("embeds", NullValueHandling = NullValueHandling.Ignore)]
public IEnumerable Embeds { get; set; }
///
/// Gets or sets the mentions.
///
[JsonProperty("allowed_mentions", NullValueHandling = NullValueHandling.Ignore)]
public DiscordMentions Mentions { get; set; }
///
/// Gets or sets the attachments.
///
[JsonProperty("attachments", NullValueHandling = NullValueHandling.Ignore)]
public IEnumerable Attachments { get; set; }
///
/// Gets or sets the flags.
///
[JsonProperty("flags", NullValueHandling = NullValueHandling.Ignore)]
public MessageFlags? Flags { get; set; }
///
/// Gets or sets the components.
///
[JsonProperty("components", NullValueHandling = NullValueHandling.Ignore)]
public IReadOnlyCollection Components { get; set; }
///
/// Gets or sets a value indicating whether has embed.
///
[JsonIgnore]
public bool HasEmbed { get; set; }
///
/// Should serialize the content.
///
public bool ShouldSerializeContent()
=> this.HasContent;
///
/// Should serialize the embed.
///
public bool ShouldSerializeEmbed()
=> this.HasEmbed;
}
///
/// Represents a channel message create payload.
///
internal sealed class RestChannelMessageCreatePayload : RestChannelMessageEditPayload
{
///
/// Gets or sets a value indicating whether t t is s.
///
[JsonProperty("tts", NullValueHandling = NullValueHandling.Ignore)]
public bool? IsTts { get; set; }
///
/// Gets or sets the stickers ids.
///
[JsonProperty("sticker_ids", NullValueHandling = NullValueHandling.Ignore)]
public IEnumerable StickersIds { get; set; }
///
/// Gets or sets the message reference.
///
[JsonProperty("message_reference", NullValueHandling = NullValueHandling.Ignore)]
public InternalDiscordMessageReference? MessageReference { get; set; }
}
///
/// Represents a channel message create multipart payload.
///
internal sealed class RestChannelMessageCreateMultipartPayload
{
///
/// Gets or sets the content.
///
[JsonProperty("content", NullValueHandling = NullValueHandling.Ignore)]
public string Content { get; set; }
///
/// Gets or sets a value indicating whether t t is s.
///
[JsonProperty("tts", NullValueHandling = NullValueHandling.Ignore)]
public bool? IsTts { get; set; }
///
/// Gets or sets the embeds.
///
[JsonProperty("embeds", NullValueHandling = NullValueHandling.Ignore)]
public IEnumerable Embeds { get; set; }
///
/// Gets or sets the mentions.
///
[JsonProperty("allowed_mentions", NullValueHandling = NullValueHandling.Ignore)]
public DiscordMentions Mentions { get; set; }
///
/// Gets or sets the message reference.
///
[JsonProperty("message_reference", NullValueHandling = NullValueHandling.Ignore)]
public InternalDiscordMessageReference? MessageReference { get; set; }
}
///
/// Represents a channel message bulk delete payload.
///
internal sealed class RestChannelMessageBulkDeletePayload
{
///
/// Gets or sets the messages.
///
[JsonProperty("messages", NullValueHandling = NullValueHandling.Ignore)]
public IEnumerable Messages { get; set; }
}
///
/// Represents a channel invite create payload.
///
internal sealed class RestChannelInviteCreatePayload
{
///
/// Gets or sets the max age.
///
[JsonProperty("max_age", NullValueHandling = NullValueHandling.Ignore)]
public int MaxAge { get; set; }
///
/// Gets or sets the max uses.
///
[JsonProperty("max_uses", NullValueHandling = NullValueHandling.Ignore)]
public int MaxUses { get; set; }
///
/// Gets or sets the target type.
///
[JsonProperty("target_type", NullValueHandling = NullValueHandling.Ignore)]
public TargetType? TargetType { get; set; }
///
/// Gets or sets the target application.
///
[JsonProperty("target_application_id", NullValueHandling = NullValueHandling.Ignore)]
public TargetActivity? TargetApplication { get; set; }
///
/// Gets or sets the target user id.
///
[JsonProperty("target_user_id", NullValueHandling = NullValueHandling.Ignore)]
public ulong? TargetUserId { get; set; }
///
/// Gets or sets a value indicating whether temporary.
///
[JsonProperty("temporary", NullValueHandling = NullValueHandling.Ignore)]
public bool Temporary { get; set; }
///
/// Gets or sets a value indicating whether unique.
///
[JsonProperty("unique", NullValueHandling = NullValueHandling.Ignore)]
public bool Unique { get; set; }
}
///
/// Represents a channel permission edit payload.
///
internal sealed class RestChannelPermissionEditPayload
{
///
/// Gets or sets the allow.
///
[JsonProperty("allow", NullValueHandling = NullValueHandling.Ignore)]
public Permissions Allow { get; set; }
///
/// Gets or sets the deny.
///
[JsonProperty("deny", NullValueHandling = NullValueHandling.Ignore)]
public Permissions Deny { get; set; }
///
/// Gets or sets the type.
///
[JsonProperty("type", NullValueHandling = NullValueHandling.Ignore)]
public string Type { get; set; }
}
///
/// Represents a channel group dm recipient add payload.
///
internal sealed class RestChannelGroupDmRecipientAddPayload : IOAuth2Payload
{
///
/// Gets or sets the access token.
///
[JsonProperty("access_token")]
public string AccessToken { get; set; }
///
/// Gets or sets the nickname.
///
[JsonProperty("nick", NullValueHandling = NullValueHandling.Ignore)]
public string Nickname { get; set; }
}
///
/// The acknowledge payload.
///
internal sealed class AcknowledgePayload
{
///
/// Gets or sets the token.
///
[JsonProperty("token", NullValueHandling = NullValueHandling.Include)]
public string Token { get; set; }
}
///
/// Represents a thread channel create payload.
///
internal sealed class RestThreadChannelCreatePayload
{
///
/// Gets or sets the name.
///
[JsonProperty("name")]
public string Name { get; set; }
///
/// Gets or sets the auto archive duration.
///
[JsonProperty("auto_archive_duration")]
public ThreadAutoArchiveDuration AutoArchiveDuration { get; set; }
///
/// Gets or sets the rate limit per user.
///
[JsonProperty("rate_limit_per_user")]
public int? PerUserRateLimit { get; set; }
///
/// Gets or sets the thread type.
///
[JsonProperty("type")]
public ChannelType Type { get; set; }
}
///
/// Represents a thread channel modify payload.
///
internal sealed class RestThreadChannelModifyPayload
{
///
/// Gets or sets the name.
///
[JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)]
public string Name { get; set; }
///
/// Gets or sets the archived.
///
[JsonProperty("archived", NullValueHandling = NullValueHandling.Ignore)]
public Optional Archived { get; set; }
///
/// Gets or sets the auto archive duration.
///
[JsonProperty("auto_archive_duration", NullValueHandling = NullValueHandling.Ignore)]
public Optional AutoArchiveDuration { get; set; }
///
/// Gets or sets the locked.
///
[JsonProperty("locked", NullValueHandling = NullValueHandling.Ignore)]
public Optional Locked { get; set; }
///
/// Gets or sets the per user rate limit.
///
[JsonProperty("rate_limit_per_user", NullValueHandling = NullValueHandling.Ignore)]
public Optional PerUserRateLimit { get; set; }
///
/// Gets or sets the thread's invitable state.
///
[JsonProperty("invitable", NullValueHandling = NullValueHandling.Ignore)]
public Optional Invitable { internal get; set; }
}
diff --git a/DisCatSharp/Net/Models/ChannelEditModel.cs b/DisCatSharp/Net/Models/ChannelEditModel.cs
index ac3941657..b1a80cb40 100644
--- a/DisCatSharp/Net/Models/ChannelEditModel.cs
+++ b/DisCatSharp/Net/Models/ChannelEditModel.cs
@@ -1,119 +1,120 @@
// This file is part of the DisCatSharp project, based off DSharpPlus.
//
// Copyright (c) 2021-2022 AITSYS
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
using System;
using System.Collections.Generic;
using System.IO;
using DisCatSharp.Entities;
+using DisCatSharp.Enums;
namespace DisCatSharp.Net.Models;
///
/// Represents a channel edit model.
///
public class ChannelEditModel : BaseEditModel
{
///
/// Sets the channel's new name.
///
public string Name { internal get; set; }
///
/// Sets the channel's type.
/// This can only be used to convert between text and news channels.
///
public Optional Type { internal get; set; }
///
/// Sets the channel's new position.
///
public int? Position { internal get; set; }
///
/// Sets the channel's new topic.
///
public Optional Topic { internal get; set; }
///
/// Sets whether the channel is to be marked as NSFW.
///
public bool? Nsfw { internal get; set; }
///
///