Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feature] Soundboard support #2667

Draft
wants to merge 12 commits into
base: dev
Choose a base branch
from
10 changes: 10 additions & 0 deletions src/Discord.Net.Core/CDN.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,16 @@ public static string GetTeamIconUrl(ulong teamId, string iconId)
public static string GetApplicationIconUrl(ulong appId, string iconId)
=> iconId != null ? $"{DiscordConfig.CDNUrl}app-icons/{appId}/{iconId}.jpg" : null;

/// <summary>
/// Returns a soundboard sound URL.
/// </summary>
/// <param name="soundId">The sound identifier.</param>
/// <returns>
/// A URL pointing to the soundboard sound.
/// </returns>
public static string GetSoundboardSoundUrl(ulong soundId)
=> $"{DiscordConfig.CDNUrl}soundboard-sounds/{soundId}.mp3";

/// <summary>
/// Returns a user avatar URL.
/// </summary>
Expand Down
14 changes: 14 additions & 0 deletions src/Discord.Net.Core/Entities/AnimationType.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
namespace Discord;

public enum AnimationType
{
/// <summary>
/// A fun animation, sent by a Nitro subscriber.
/// </summary>
Premium = 0,

/// <summary>
/// The standard animation.
/// </summary>
Basic = 1,
}
92 changes: 92 additions & 0 deletions src/Discord.Net.Core/Entities/Guilds/SoundboardSound.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
using System;
using System.Diagnostics;

namespace Discord;

/// <summary>
/// Represents a soundboard sound.
/// </summary>
[DebuggerDisplay(@"{DebuggerDisplay,nq}")]
public class SoundboardSound : ISnowflakeEntity
{
/// <inheritdoc />
public ulong Id => SoundId;

/// <summary>
/// Gets the Id of the sound.
/// </summary>
public ulong SoundId { get; internal set; }

/// <inheritdoc />
/// <remarks>
/// May be inaccurate for default sounds.
/// </remarks>
public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id);

/// <summary>
/// Gets the name of the sound.
/// </summary>
public string Name { get; internal set; }

/// <summary>
/// Gets the Id of the author of the sound.
/// </summary>
public ulong AuthorId { get; internal set; }

/// <summary>
/// Gets the author of the sound.
/// </summary>
public IUser Author { get; internal set; }

/// <summary>
/// Gets the icon of the sound.
/// </summary>
/// <remarks>
/// Custom emojis will only have Id property filled due to limited data returned by discord.
/// </remarks>
public IEmote Emoji { get; internal set; }

/// <summary>
/// Gets the volume of the sound.
/// </summary>
public double Volume { get; internal set; }

/// <summary>
/// Gets whether the sound is available or not.
/// </summary>
public bool? IsAvailable { get; internal set; }

/// <summary>
/// Gets the Id of the guild this sound belongs to. <see langword="null"/> if not available.
/// </summary>
public ulong? GuildId { get; internal set; }

internal SoundboardSound(ulong soundId, string name, ulong authorId, double volume, ulong? guildId = null,
string emojiName = null, ulong? emojiId = null, IUser author = null, bool? isAvailable = null)
{
GuildId = guildId;
SoundId = soundId;
Name = name;
AuthorId = authorId;
Author = author;
IsAvailable = isAvailable;
Volume = volume;

if (emojiId is not null)
Emoji = new Emote(emojiId.Value, emojiName, false);
else if (!string.IsNullOrWhiteSpace(emojiName))
Emoji = new Emoji(emojiName);
else
Emoji = null;
}

private string DebuggerDisplay => $"{Name} ({SoundId})";

internal SoundboardSound Clone() => (SoundboardSound)MemberwiseClone();

/// <summary>
/// Gets the url for the sound.
/// </summary>
public string GetUrl()
=> CDN.GetSoundboardSoundUrl(SoundId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,11 @@ public enum ChannelPermission : ulong
/// Allows members to edit and cancel events in this channel.
/// </summary>
CreateEvents = 1L << 44,

/// <summary>
/// Allows members to use sounds from other servers.
/// </summary>
UseExternalSounds = 1L << 45,

/// <summary>
/// Allows sending voice messages.
Expand Down
19 changes: 13 additions & 6 deletions src/Discord.Net.Core/Entities/Permissions/ChannelPermissions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ public struct ChannelPermissions
/// <summary>
/// Gets a <see cref="ChannelPermissions"/> that grants all permissions for voice channels.
/// </summary>
public static readonly ChannelPermissions Voice = new(0b1_110001_001010_001010_110011_111101_111111_111101_010001);
public static readonly ChannelPermissions Voice = new(0b1_111001_001010_001010_110011_111101_111111_111101_010001);

/// <summary>
/// Gets a <see cref="ChannelPermissions"/> that grants all permissions for stage channels.
Expand Down Expand Up @@ -152,6 +152,8 @@ public static ChannelPermissions All(IChannel channel)
public bool UseClydeAI => Permissions.GetValue(RawValue, ChannelPermission.UseClydeAI);
/// <summary> If <see langword="true"/>, a user can set the status of a voice channel.</summary>
public bool SetVoiceChannelStatus => Permissions.GetValue(RawValue, GuildPermission.SetVoiceChannelStatus);
/// <summary> If <see langword="true"/>, a user can use sounds from other servers.</summary>
public bool UseExternalSounds => Permissions.GetValue(RawValue, ChannelPermission.UseExternalSounds);

/// <summary> Creates a new <see cref="ChannelPermissions"/> with the provided packed value.</summary>
public ChannelPermissions(ulong rawValue) { RawValue = rawValue; }
Expand Down Expand Up @@ -191,7 +193,8 @@ private ChannelPermissions(ulong initialValue,
bool? createEvents = null,
bool? sendVoiceMessages = null,
bool? useClydeAI = null,
bool? setVoiceChannelStatus = null)
bool? setVoiceChannelStatus = null,
bool? useExternalSounds = null)
{
ulong value = initialValue;

Expand Down Expand Up @@ -230,6 +233,7 @@ private ChannelPermissions(ulong initialValue,
Permissions.SetValue(ref value, sendVoiceMessages, ChannelPermission.SendVoiceMessages);
Permissions.SetValue(ref value, useClydeAI, ChannelPermission.UseClydeAI);
Permissions.SetValue(ref value, setVoiceChannelStatus, ChannelPermission.SetVoiceChannelStatus);
Permissions.SetValue(ref value, useExternalSounds, ChannelPermission.UseExternalSounds);

RawValue = value;
}
Expand Down Expand Up @@ -270,12 +274,13 @@ public ChannelPermissions(
bool createEvents = false,
bool sendVoiceMessages = false,
bool useClydeAI = false,
bool setVoiceChannelStatus = false)
bool setVoiceChannelStatus = false,
bool useExternalSounds = false)
: this(0, createInstantInvite, manageChannel, addReactions, viewChannel, sendMessages, sendTTSMessages, manageMessages,
embedLinks, attachFiles, readMessageHistory, mentionEveryone, useExternalEmojis, connect,
speak, muteMembers, deafenMembers, moveMembers, useVoiceActivation, prioritySpeaker, stream, manageRoles, manageWebhooks,
useApplicationCommands, requestToSpeak, manageThreads, createPublicThreads, createPrivateThreads, useExternalStickers, sendMessagesInThreads,
startEmbeddedActivities, useSoundboard, createEvents, sendVoiceMessages, useClydeAI, setVoiceChannelStatus)
startEmbeddedActivities, useSoundboard, createEvents, sendVoiceMessages, useClydeAI, setVoiceChannelStatus, useExternalSounds)
{ }

/// <summary> Creates a new <see cref="ChannelPermissions"/> from this one, changing the provided non-null permissions.</summary>
Expand Down Expand Up @@ -314,7 +319,8 @@ public ChannelPermissions Modify(
bool? createEvents = null,
bool? sendVoiceMessages = null,
bool? useClydeAI = null,
bool? setVoiceChannelStatus = null)
bool? setVoiceChannelStatus = null,
bool? useExternalSounds = null)
=> new ChannelPermissions(RawValue,
createInstantInvite,
manageChannel,
Expand Down Expand Up @@ -350,7 +356,8 @@ public ChannelPermissions Modify(
createEvents,
sendVoiceMessages,
useClydeAI,
setVoiceChannelStatus);
setVoiceChannelStatus,
useExternalSounds);

public bool Has(ChannelPermission permission) => Permissions.GetValue(RawValue, permission);

Expand Down
5 changes: 5 additions & 0 deletions src/Discord.Net.Core/Entities/Permissions/GuildPermission.cs
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,11 @@ public enum GuildPermission : ulong
/// </summary>
UseSoundboard = 1L << 42,

/// <summary>
/// Allows members to use sounds from other servers.
/// </summary>
UseExternalSounds = 1L << 45,

/// <summary>
/// Allows for creating emojis, stickers, and soundboard sounds, and editing and deleting those created by the current user.
/// </summary>
Expand Down
18 changes: 13 additions & 5 deletions src/Discord.Net.Core/Entities/Permissions/GuildPermissions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,8 @@ public struct GuildPermissions
public bool CreateGuildExpressions => Permissions.GetValue(RawValue, GuildPermission.CreateGuildExpressions);
/// <summary> If <see langword="true"/>, a user can set the status of a voice channel.</summary>
public bool SetVoiceChannelStatus => Permissions.GetValue(RawValue, GuildPermission.SetVoiceChannelStatus);
/// <summary> If <see langword="true"/>, a user can use sounds from other servers.</summary>
public bool UseExternalSounds => Permissions.GetValue(RawValue, GuildPermission.UseExternalSounds);

/// <summary> Creates a new <see cref="GuildPermissions"/> with the provided packed value. </summary>
public GuildPermissions(ulong rawValue) { RawValue = rawValue; }
Expand Down Expand Up @@ -170,7 +172,8 @@ private GuildPermissions(ulong initialValue,
bool? sendVoiceMessages = null,
bool? useClydeAI = null,
bool? createGuildExpressions = null,
bool? setVoiceChannelStatus = null)
bool? setVoiceChannelStatus = null,
bool? useExternalSounds = null)
{
ulong value = initialValue;

Expand Down Expand Up @@ -221,6 +224,7 @@ private GuildPermissions(ulong initialValue,
Permissions.SetValue(ref value, useClydeAI, GuildPermission.UseClydeAI);
Permissions.SetValue(ref value, createGuildExpressions, GuildPermission.CreateGuildExpressions);
Permissions.SetValue(ref value, setVoiceChannelStatus, GuildPermission.SetVoiceChannelStatus);
Permissions.SetValue(ref value, useExternalSounds, GuildPermission.UseExternalSounds);

RawValue = value;
}
Expand Down Expand Up @@ -273,7 +277,8 @@ public GuildPermissions(
bool sendVoiceMessages = false,
bool useClydeAI = false,
bool createGuildExpressions = false,
bool setVoiceChannelStatus = false)
bool setVoiceChannelStatus = false,
bool useExternalSounds = false)
: this(0,
createInstantInvite: createInstantInvite,
manageRoles: manageRoles,
Expand Down Expand Up @@ -321,7 +326,8 @@ public GuildPermissions(
sendVoiceMessages: sendVoiceMessages,
useClydeAI: useClydeAI,
createGuildExpressions: createGuildExpressions,
setVoiceChannelStatus: setVoiceChannelStatus)
setVoiceChannelStatus: setVoiceChannelStatus,
useExternalSounds: useExternalSounds)
{ }

/// <summary> Creates a new <see cref="GuildPermissions"/> from this one, changing the provided non-null permissions. </summary>
Expand Down Expand Up @@ -372,13 +378,15 @@ public GuildPermissions Modify(
bool? sendVoiceMessages = null,
bool? useClydeAI = null,
bool? createGuildExpressions = null,
bool? setVoiceChannelStatus = null)
bool? setVoiceChannelStatus = null,
bool? useExternalSounds = null)
=> new GuildPermissions(RawValue, createInstantInvite, kickMembers, banMembers, administrator, manageChannels, manageGuild, addReactions,
viewAuditLog, viewGuildInsights, viewChannel, sendMessages, sendTTSMessages, manageMessages, embedLinks, attachFiles,
readMessageHistory, mentionEveryone, useExternalEmojis, connect, speak, muteMembers, deafenMembers, moveMembers,
useVoiceActivation, prioritySpeaker, stream, changeNickname, manageNicknames, manageRoles, manageWebhooks, manageEmojisAndStickers,
useApplicationCommands, requestToSpeak, manageEvents, manageThreads, createPublicThreads, createPrivateThreads, useExternalStickers, sendMessagesInThreads,
startEmbeddedActivities, moderateMembers, useSoundboard, viewMonetizationAnalytics, sendVoiceMessages, useClydeAI, createGuildExpressions, setVoiceChannelStatus);
startEmbeddedActivities, moderateMembers, useSoundboard, viewMonetizationAnalytics, sendVoiceMessages, useClydeAI, createGuildExpressions, setVoiceChannelStatus,
useExternalSounds);

/// <summary>
/// Returns a value that indicates if a specific <see cref="GuildPermission"/> is enabled
Expand Down
79 changes: 79 additions & 0 deletions src/Discord.Net.Core/Entities/Sound.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
using System;
using System.IO;

namespace Discord
{
/// <summary>
/// An sound that will be uploaded to Discord.
/// </summary>
public struct Sound : IDisposable
{
private bool _isDisposed;

/// <summary>
/// Gets the stream to be uploaded to Discord.
/// </summary>
#pragma warning disable IDISP008
public Stream Stream { get; }
#pragma warning restore IDISP008
/// <summary>
/// Create the sound with a <see cref="System.IO.Stream"/>.
/// </summary>
/// <param name="stream">
/// The <see cref="System.IO.Stream" /> to create the sound with. Note that this must be some type of stream
/// with the contents of a file in it.
/// </param>
public Sound(Stream stream)
{
_isDisposed = false;
Stream = stream;
}

/// <summary>
/// Create the sound from a file path.
/// </summary>
/// <remarks>
/// This file path is NOT validated and is passed directly into a
/// <see cref="File.OpenRead"/>.
/// </remarks>
/// <param name="path">The path to the file.</param>
/// <exception cref="System.ArgumentException">
/// <paramref name="path" /> is a zero-length string, contains only white space, or contains one or more invalid
/// characters as defined by <see cref="Path.GetInvalidPathChars"/>.
/// </exception>
/// <exception cref="System.ArgumentNullException"><paramref name="path" /> is <c>null</c>.</exception>
/// <exception cref="PathTooLongException">
/// The specified path, file name, or both exceed the system-defined maximum length. For example, on
/// Windows-based platforms, paths must be less than 248 characters, and file names must be less than 260
/// characters.
/// </exception>
/// <exception cref="System.NotSupportedException"><paramref name="path" /> is in an invalid format.</exception>
/// <exception cref="DirectoryNotFoundException">
/// The specified <paramref name="path"/> is invalid, (for example, it is on an unmapped drive).
/// </exception>
/// <exception cref="System.UnauthorizedAccessException">
/// <paramref name="path" /> specified a directory.-or- The caller does not have the required permission.
/// </exception>
/// <exception cref="FileNotFoundException">The file specified in <paramref name="path" /> was not found.
/// </exception>
/// <exception cref="IOException">An I/O error occurred while opening the file. </exception>
public Sound(string path)
{
_isDisposed = false;
Stream = File.OpenRead(path);
}

/// <inheritdoc/>
public void Dispose()
{
if (!_isDisposed)
{
#pragma warning disable IDISP007
Stream?.Dispose();
#pragma warning restore IDISP007

_isDisposed = true;
}
}
}
}
5 changes: 5 additions & 0 deletions src/Discord.Net.Core/IDiscordClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -349,5 +349,10 @@ IAsyncEnumerable<IReadOnlyCollection<IEntitlement>> GetEntitlementsAsync(int? li
/// Returns all SKUs for a given application.
/// </summary>
Task<IReadOnlyCollection<SKU>> GetSKUsAsync(RequestOptions options = null);

/// <summary>
/// Returns all default soundboard sounds.
/// </summary>
Task<IReadOnlyCollection<SoundboardSound>> GetDefaultSoundboardSoundsAsync(RequestOptions options = null);
}
}
Loading