Skip to content
Open
21 changes: 21 additions & 0 deletions NetCord/AutoModerationActionMetadataProperties.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,33 @@ namespace NetCord;
[GenerateMethodsForProperties]
public partial class AutoModerationActionMetadataProperties
{
/// <summary>
/// The ID of the channel to which user content should be logged.
/// </summary>
/// <remarks>
/// Required for an <see cref="AutoModerationActionType.SendAlertMessage"/> action.
/// This must be an existing channel.
/// </remarks>
[JsonPropertyName("channel_id")]
public ulong? ChannelId { get; set; }

/// <summary>
/// The timeout duration, in seconds.
/// </summary>
/// <remarks>
/// Required for an <see cref="AutoModerationActionType.Timeout"/> action.
/// The maximum duration is 2,419,200 seconds (4 weeks).
/// </remarks>
[JsonPropertyName("duration_seconds")]
public int? DurationSeconds { get; set; }

/// <summary>
/// An additional explanation that will be shown to members whenever their message is blocked.
/// </summary>
/// <remarks>
/// Only applies to an <see cref="AutoModerationActionType.BlockMessage"/> action.
/// The maximum length is 150 characters.
/// </remarks>
[JsonPropertyName("custom_message")]
public string? CustomMessage { get; set; }
}
4 changes: 4 additions & 0 deletions NetCord/Channels/TextChannels/Guild/ForumGuildChannel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,10 @@ public ForumGuildChannel(JsonChannel jsonModel, ulong guildId, RestClient client
/// <summary>
/// The set of tags available for use in the channel.
/// </summary>
/// <remarks>
/// Can be set when creating or updating a channel, which determines which tags can be set on individual threads within the thread’s <see cref="GuildChannelOptions.AppliedTags"/> field.
/// When updating a <see cref="ForumGuildChannel"/> or a <see cref="MediaForumGuildChannel"/> channel, tag objects only require the name field.
/// </remarks>
public IReadOnlyList<ForumTag> AvailableTags { get; }

/// <summary>
Expand Down
56 changes: 56 additions & 0 deletions NetCord/CodeBlock.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,30 @@

namespace NetCord;

/// <summary>
/// Represents a Discord Markdown fenced code block.
/// </summary>
/// <param name="code">The content of the code block.</param>
/// <param name="formatter">The optional formatter or language identifier following the opening backticks.</param>
public class CodeBlock(string code, string? formatter = null) : ISpanFormattable, ISpanParsable<CodeBlock>
{
/// <summary>
/// Gets the content of the code block.
/// </summary>
public string Code { get; } = code;

/// <summary>
/// Gets the formatter or language identifier of the code block, if present.
/// </summary>
public string? Formatter { get; } = formatter;

/// <inheritdoc/>
public override string ToString() => $"```{Formatter}\n{Code}```";

/// <inheritdoc/>
public string ToString(string? format, IFormatProvider? formatProvider) => ToString();

/// <inheritdoc/>
public bool TryFormat(Span<char> destination, out int charsWritten, ReadOnlySpan<char> format = default, IFormatProvider? provider = null)
{
var code = Code;
Expand Down Expand Up @@ -46,6 +61,20 @@ public bool TryFormat(Span<char> destination, out int charsWritten, ReadOnlySpan
return true;
}

/// <summary>
/// Attempts to parse a Discord Markdown fenced code block.
/// </summary>
/// <param name="s">The characters to parse.</param>
/// <param name="strictMode">
/// Whether an apparent formatter followed only by whitespace should instead be treated as code.
/// </param>
/// <param name="result">
/// When this method returns, contains the parsed code block if parsing succeeded; otherwise, <see langword="null"/>.
/// </param>
/// <returns>
/// <see langword="true"/> if <paramref name="s"/> starts and ends with triple backticks and contains at least
/// one character between them; otherwise, <see langword="false"/>.
/// </returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)] // Inline so that 'strictMode' branches can be eliminated if it is a constant
public static bool TryParse(ReadOnlySpan<char> s, bool strictMode, [MaybeNullWhen(false)] out CodeBlock result)
{
Expand Down Expand Up @@ -89,12 +118,31 @@ public static bool TryParse(ReadOnlySpan<char> s, bool strictMode, [MaybeNullWhe
return isCodeBlock;
}

/// <summary>
/// Attempts to parse a Discord Markdown fenced code block in strict mode.
/// </summary>
/// <param name="s">The characters to parse.</param>
/// <param name="result">
/// When this method returns, contains the parsed code block if parsing succeeded; otherwise, <see langword="null"/>.
/// </param>
/// <returns><see langword="true"/> if parsing succeeded; otherwise, <see langword="false"/>.</returns>
public static bool TryParse(ReadOnlySpan<char> s, [MaybeNullWhen(false)] out CodeBlock result) => TryParse(s, true, out result);

/// <inheritdoc/>
public static bool TryParse(ReadOnlySpan<char> s, IFormatProvider? provider, [MaybeNullWhen(false)] out CodeBlock result) => TryParse(s, true, out result);

/// <inheritdoc/>
public static bool TryParse([NotNullWhen(true)] string? s, IFormatProvider? provider, [MaybeNullWhen(false)] out CodeBlock result) => TryParse(s.AsSpan(), true, out result);

/// <summary>
/// Parses a Discord Markdown fenced code block.
/// </summary>
/// <param name="s">The characters to parse.</param>
/// <param name="strictMode">
/// Whether an apparent formatter followed only by whitespace should instead be treated as code.
/// </param>
/// <returns>The parsed code block.</returns>
/// <exception cref="FormatException"><paramref name="s"/> is not a valid fenced code block.</exception>
[MethodImpl(MethodImplOptions.AggressiveInlining)] // Inline so that 'strictMode' branches can be eliminated if it is a constant
public static CodeBlock Parse(ReadOnlySpan<char> s, bool strictMode)
{
Expand All @@ -104,9 +152,17 @@ public static CodeBlock Parse(ReadOnlySpan<char> s, bool strictMode)
throw new FormatException($"Cannot parse '{nameof(CodeBlock)}'.");
}

/// <summary>
/// Parses a Discord Markdown fenced code block in strict mode.
/// </summary>
/// <param name="s">The characters to parse.</param>
/// <returns>The parsed code block.</returns>
/// <exception cref="FormatException"><paramref name="s"/> is not a valid fenced code block.</exception>
public static CodeBlock Parse(ReadOnlySpan<char> s) => Parse(s, true);

/// <inheritdoc/>
public static CodeBlock Parse(ReadOnlySpan<char> s, IFormatProvider? provider) => Parse(s, true);

/// <inheritdoc/>
public static CodeBlock Parse(string s, IFormatProvider? provider) => Parse(s.AsSpan(), true);
}
11 changes: 11 additions & 0 deletions NetCord/GuildFromGuildTemplateProperties.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,18 @@

namespace NetCord.Rest;

/// <summary>
/// Represents properties used to create a guild from a guild template.
/// </summary>
/// <remarks>
/// Discord deprecated application-driven guild creation in April 2025
/// and removed the corresponding API endpoint in July 2025.
/// This type is retained for compatibility with
/// <see cref="RestClient.CreateGuildFromGuildTemplateAsync(string, GuildFromGuildTemplateProperties, RestRequestProperties?, CancellationToken)"/>.
/// </remarks>
/// <param name="name">The name of the guild.</param>
[GenerateMethodsForProperties]
[Obsolete("Discord deprecated application-driven guild creation in April 2025 and removed the corresponding API endpoint in July 2025.")]
public partial class GuildFromGuildTemplateProperties(string name)
{
[JsonPropertyName("name")]
Expand Down
60 changes: 60 additions & 0 deletions NetCord/IToken.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,18 @@

namespace NetCord;

/// <summary>
/// Represents a Discord bot token.
/// </summary>
public class BotToken : IEntityToken
{
/// <summary>
/// Initializes a new instance of the <see cref="BotToken"/> class.
/// </summary>
/// <param name="token">The raw bot token.</param>
/// <exception cref="ArgumentException">
/// <paramref name="token"/> is null or empty, or is not a valid bot token.
/// </exception>
public BotToken(string token)
{
if (string.IsNullOrEmpty(token))
Expand All @@ -16,17 +26,40 @@ public BotToken(string token)
RawToken = token;
}

/// <summary>
/// Gets the raw bot token.
/// </summary>
public string RawToken { get; }

/// <summary>
/// Gets the value to use for the HTTP <c>Authorization</c> header.
/// </summary>
/// <remarks>
/// The value uses the <c>Bot</c> authentication scheme.
/// </remarks>
public string HttpHeaderValue => $"Bot {RawToken}";

/// <summary>
/// Gets the entity ID encoded in the token.
/// </summary>
public ulong Id { get; }

/// <summary>
/// Gets the creation time derived from <see cref="Id"/>.
/// </summary>
public DateTimeOffset CreatedAt => Snowflake.Timestamp(Id);
}

/// <summary>
/// Represents an OAuth2 bearer token.
/// </summary>
public class BearerToken : IToken
{
/// <summary>
/// Initializes a new instance of the <see cref="BearerToken"/> class.
/// </summary>
/// <param name="token">The raw OAuth2 bearer token.</param>
/// <exception cref="ArgumentException"><paramref name="token"/> is null or empty.</exception>
public BearerToken(string token)
{
if (string.IsNullOrEmpty(token))
Expand All @@ -35,13 +68,31 @@ public BearerToken(string token)
RawToken = token;
}

/// <summary>
/// Gets the raw bearer token.
/// </summary>
public string RawToken { get; }

/// <summary>
/// Gets the value to use for the HTTP <c>Authorization</c> header.
/// </summary>
/// <remarks>
/// The value uses the <c>Bearer</c> authentication scheme.
/// </remarks>
public string HttpHeaderValue => $"Bearer {RawToken}";
}

/// <summary>
/// Represents an authentication token that identifies a Discord entity.
/// </summary>
public interface IEntityToken : IToken, IEntity
{
/// <summary>
/// Attempts to extract the entity ID encoded in a token.
/// </summary>
/// <param name="token">The token to inspect.</param>
/// <param name="id">When this method returns, contains the decoded entity ID if successful.</param>
/// <returns><see langword="true"/> if the entity ID was decoded; otherwise, <see langword="false"/>.</returns>
[SkipLocalsInit]
protected static bool TryGetTokenId(ReadOnlySpan<char> token, out ulong id)
{
Expand All @@ -68,9 +119,18 @@ protected static bool TryGetTokenId(ReadOnlySpan<char> token, out ulong id)
}
}

/// <summary>
/// Represents a Discord authentication token.
/// </summary>
public interface IToken
{
/// <summary>
/// Gets the raw token.
/// </summary>
public string RawToken { get; }

/// <summary>
/// Gets the value to use for the HTTP <c>Authorization</c> header.
/// </summary>
public string HttpHeaderValue { get; }
}
13 changes: 13 additions & 0 deletions NetCord/MessagePollMediaProperties.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,22 @@ namespace NetCord;
[GenerateMethodsForProperties]
public partial class MessagePollMediaProperties
{
/// <summary>
/// The text of the poll media.
/// </summary>
/// <remarks>
/// This value should currently be non-null for both poll questions and poll answers.
/// Discord may support other forms of poll media in the future, which may not require text.
/// </remarks>
[JsonPropertyName("text")]
public string? Text { get; set; }

/// <summary>
/// The emoji of the poll media.
/// </summary>
/// <remarks>
/// This may be specified for poll answers. Poll questions currently only support <see cref="Text"/>.
/// </remarks>
[JsonPropertyName("emoji")]
public EmojiProperties? Emoji { get; set; }
}
7 changes: 7 additions & 0 deletions NetCord/Rest/ComponentProperties/ComponentMediaProperties.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@ namespace NetCord.Rest;
[GenerateMethodsForProperties]
public partial class ComponentMediaProperties(string url)
{
/// <summary>
/// Source URL of the media item.
/// </summary>
/// <remarks>
/// Supports arbitrary urls and attachment://&lt;filename&gt; references.
/// For a file component, only supports using the attachment:// protocol.
/// </remarks>
[JsonPropertyName("url")]
public string Url { get; set; } = url;

Expand Down
11 changes: 11 additions & 0 deletions NetCord/Rest/ComponentProperties/FileDisplayProperties.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@

namespace NetCord.Rest;

/// <summary>
/// Represents a file component to be sent in a message.
/// </summary>
/// <param name="file">The file to be sent as a component. The file must be attached to the message for it to be displayed correctly, and the URL must use the attachment:// protocol.</param>
[GenerateMethodsForProperties]
public partial class FileDisplayProperties(ComponentMediaProperties file) : IMessageComponentProperties, IComponentContainerComponentProperties
{
Expand All @@ -13,6 +17,13 @@ public partial class FileDisplayProperties(ComponentMediaProperties file) : IMes
[JsonPropertyName("id")]
public int? Id { get; set; }

/// <summary>
/// The file to be sent as a component.
/// </summary>
/// <remarks>
/// The file must be attached to the message for it to be displayed correctly.
/// The URL must use the attachment:// protocol.
/// </remarks>
[JsonPropertyName("file")]
public ComponentMediaProperties File { get; set; } = file;

Expand Down
4 changes: 4 additions & 0 deletions NetCord/Rest/ComponentProperties/FileUploadProperties.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@

namespace NetCord.Rest;

/// <summary>
/// Represents a file upload component.
/// </summary>
/// <param name="customId"></param>
[GenerateMethodsForProperties]
public partial class FileUploadProperties(string customId) : IInteractiveComponentProperties, ILabelComponentProperties
{
Expand Down
11 changes: 11 additions & 0 deletions NetCord/Rest/ComponentProperties/MenuProperties.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ public abstract partial class MenuProperties(string customId) : IInteractiveComp
/// <summary>
/// Minimum number of items that must be chosen, default 1 (0-25).
/// </summary>
/// <remarks>
/// In a modal, this may be 0 when <see cref="Required"/> is <see langword="false"/>.
/// If the menu is required, this must be at least 1 if specified.
/// </remarks>
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonPropertyName("min_values")]
public int? MinValues { get; set; }
Expand All @@ -44,13 +48,20 @@ public abstract partial class MenuProperties(string customId) : IInteractiveComp
/// <summary>
/// Whether the menu is disabled.
/// </summary>
/// <remarks>
/// This only applies to menus in messages. Discord does not allow a disabled menu in a modal.
/// </remarks>
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
[JsonPropertyName("disabled")]
public bool Disabled { get; set; }

/// <summary>
/// Whether the menu is required to answer in a modal. Defaults to <see langword="true"/>.
/// </summary>
/// <remarks>
/// This only applies to menus in modals and is ignored for menus in messages.
/// When this is <see langword="true"/> or omitted, <see cref="MinValues"/> must be at least 1 if specified.
/// </remarks>
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonPropertyName("required")]
public bool? Required { get; set; }
Expand Down
Loading