Skip to content

Commit

Permalink
feat: scheduled event recurrency rules
Browse files Browse the repository at this point in the history
  • Loading branch information
Lulalaby committed Aug 25, 2024
1 parent 57d35bd commit 253cc6d
Show file tree
Hide file tree
Showing 11 changed files with 372 additions and 21 deletions.
8 changes: 5 additions & 3 deletions DisCatSharp/Entities/Channel/DiscordChannel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1049,19 +1049,21 @@ public async Task<DiscordStageInstance> GetStageAsync()
/// <param name="scheduledStartTime">The scheduled start time.</param>
/// <param name="description">The description.</param>
/// <param name="coverImage">The cover image.</param>
/// <param name="recurrenceRule">The recurrence rule.</param>
/// <param name="reason">The reason.</param>
/// <returns>A scheduled event.</returns>
/// <exception cref="ValidationException">Thrown if the user gave an invalid input.</exception>
/// <exception cref="NotFoundException">Thrown when the resource does not exist.</exception>
/// <exception cref="BadRequestException">Thrown when an invalid parameter was provided.</exception>
/// <exception cref="ServerErrorException">Thrown when Discord is unable to process the request.</exception>
public async Task<DiscordScheduledEvent> CreateScheduledEventAsync(string name, DateTimeOffset scheduledStartTime, string description = null, Optional<Stream> coverImage = default, string reason = null)
public async Task<DiscordScheduledEvent> CreateScheduledEventAsync(string name, DateTimeOffset scheduledStartTime, string description = null, Optional<Stream> coverImage = default, DiscordScheduledEventRecurrenceRule? recurrenceRule = null, 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;
var type = this.Type is ChannelType.Voice ? ScheduledEventEntityType.Voice : ScheduledEventEntityType.StageInstance;

return await this.Guild.CreateScheduledEventAsync(name, scheduledStartTime, null, this, null, description, type, coverImage, reason).ConfigureAwait(false);
return await this.Guild.CreateScheduledEventAsync(name, scheduledStartTime, null, this, null, description, type, coverImage, recurrenceRule, reason).ConfigureAwait(false);
}

#endregion
Expand Down
14 changes: 9 additions & 5 deletions DisCatSharp/Entities/Guild/DiscordGuild.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1260,7 +1260,7 @@ public async Task<AutomodRule> CreateAutomodRuleAsync(
)
=> await this.Discord.ApiClient.CreateAutomodRuleAsync(this.Id, name, eventType, triggerType, actions, triggerMetadata, enabled, exemptRoles, exemptChannels, reason).ConfigureAwait(false);

#region Scheduled Events
#region Scheduled Events

/// <summary>
/// Creates a scheduled event.
Expand All @@ -1273,15 +1273,17 @@ public async Task<AutomodRule> CreateAutomodRuleAsync(
/// <param name="description">The description.</param>
/// <param name="type">The type.</param>
/// <param name="coverImage">The cover image.</param>
/// <param name="recurrenceRule">The recurrence rule.</param>
/// <param name="reason">The reason.</param>
/// <returns>A scheduled event.</returns>
/// <exception cref="ValidationException">Thrown if the user gave an invalid input.</exception>
/// <exception cref="NotFoundException">Thrown when the guild does not exist.</exception>
/// <exception cref="BadRequestException">Thrown when an invalid parameter was provided.</exception>
/// <exception cref="ServerErrorException">Thrown when Discord is unable to process the request.</exception>
public async Task<DiscordScheduledEvent> CreateScheduledEventAsync(string name, DateTimeOffset scheduledStartTime, DateTimeOffset? scheduledEndTime = null, DiscordChannel channel = null, DiscordScheduledEventEntityMetadata metadata = null, string description = null, ScheduledEventEntityType type = ScheduledEventEntityType.StageInstance, Optional<Stream> coverImage = default, string reason = null)
public async Task<DiscordScheduledEvent> CreateScheduledEventAsync(string name, DateTimeOffset scheduledStartTime, DateTimeOffset? scheduledEndTime = null, DiscordChannel channel = null, DiscordScheduledEventEntityMetadata metadata = null, string description = null, ScheduledEventEntityType type = ScheduledEventEntityType.StageInstance, Optional<Stream> coverImage = default, DiscordScheduledEventRecurrenceRule? recurrenceRule = null, string reason = null)
{
var coverb64 = ImageTool.Base64FromStream(coverImage);
return await this.Discord.ApiClient.CreateGuildScheduledEventAsync(this.Id, type == ScheduledEventEntityType.External ? null : channel?.Id, type == ScheduledEventEntityType.External ? metadata : null, name, scheduledStartTime, scheduledEndTime.HasValue && type == ScheduledEventEntityType.External ? scheduledEndTime.Value : null, description, type, coverb64, reason).ConfigureAwait(false);
return await this.Discord.ApiClient.CreateGuildScheduledEventAsync(this.Id, type is ScheduledEventEntityType.External ? null : channel?.Id, type is ScheduledEventEntityType.External ? metadata : null, name, scheduledStartTime, scheduledEndTime.HasValue && type is ScheduledEventEntityType.External ? scheduledEndTime.Value : null, description, type, coverb64, recurrenceRule, reason).ConfigureAwait(false);
}

/// <summary>
Expand All @@ -1293,15 +1295,17 @@ public async Task<DiscordScheduledEvent> CreateScheduledEventAsync(string name,
/// <param name="location">The location of the external event.</param>
/// <param name="description">The description.</param>
/// <param name="coverImage">The cover image.</param>
/// <param name="recurrenceRule">The recurrence rule.</param>
/// <param name="reason">The reason.</param>
/// <returns>A scheduled event.</returns>
/// <exception cref="ValidationException">Thrown if the user gave an invalid input.</exception>
/// <exception cref="NotFoundException">Thrown when the guild does not exist.</exception>
/// <exception cref="BadRequestException">Thrown when an invalid parameter was provided.</exception>
/// <exception cref="ServerErrorException">Thrown when Discord is unable to process the request.</exception>
public async Task<DiscordScheduledEvent> CreateExternalScheduledEventAsync(string name, DateTimeOffset scheduledStartTime, DateTimeOffset scheduledEndTime, string location, string description = null, Optional<Stream> coverImage = default, string reason = null)
public async Task<DiscordScheduledEvent> CreateExternalScheduledEventAsync(string name, DateTimeOffset scheduledStartTime, DateTimeOffset scheduledEndTime, string location, string description = null, Optional<Stream> coverImage = default, DiscordScheduledEventRecurrenceRule? recurrenceRule = null, string reason = null)
{
var coverb64 = ImageTool.Base64FromStream(coverImage);
return await this.Discord.ApiClient.CreateGuildScheduledEventAsync(this.Id, null, new(location), name, scheduledStartTime, scheduledEndTime, description, ScheduledEventEntityType.External, coverb64, reason).ConfigureAwait(false);
return await this.Discord.ApiClient.CreateGuildScheduledEventAsync(this.Id, null, new(location), name, scheduledStartTime, scheduledEndTime, description, ScheduledEventEntityType.External, coverb64, recurrenceRule, reason).ConfigureAwait(false);
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
using DisCatSharp.Enums;

using Newtonsoft.Json;

namespace DisCatSharp.Entities;

/// <summary>
/// Represents a specific day within a specific week to recur on.
/// </summary>
public sealed class DiscordRecurrenceRuleNWeekday
{
/// <summary>
/// Gets or sets the week number (1-5) for recurrence.
/// </summary>
[JsonProperty("n")]
public int WeekNumber { get; set; }

/// <summary>
/// Gets or sets the day of the week for recurrence.
/// </summary>
[JsonProperty("day")]
public RecurrenceRuleWeekday Day { get; set; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,12 @@ This isn't used.
public IReadOnlyList<ulong> SkuIds { get; internal set; }
*/

/// <summary>
/// Gets the recurrence rule for this scheduled event, if any.
/// </summary>
[JsonProperty("recurrence_rule", NullValueHandling = NullValueHandling.Ignore)]
public DiscordScheduledEventRecurrenceRule? RecurrenceRule { get; internal set; }

/// <summary>
/// Gets the total number of users subscribed to the scheduled event.
/// </summary>
Expand All @@ -169,6 +175,7 @@ internal DiscordScheduledEvent()
/// Modifies the current scheduled event.
/// </summary>
/// <param name="action">Action to perform on this thread</param>
/// <exception cref="ValidationException">Thrown if the user gave an invalid input.</exception>
/// <exception cref="UnauthorizedException">Thrown when the client does not have the <see cref="Permissions.ManageEvents"/> permission.</exception>
/// <exception cref="NotFoundException">Thrown when the event does not exist.</exception>
/// <exception cref="BadRequestException">Thrown when an invalid parameter was provided.</exception>
Expand All @@ -190,8 +197,8 @@ public async Task ModifyAsync(Action<ScheduledEventEditModel> action)
var scheduledEndTime = Optional<DateTimeOffset>.None;
if (mdl.ScheduledEndTime.HasValue && mdl.EntityType.HasValue ? mdl.EntityType == ScheduledEventEntityType.External : this.EntityType == ScheduledEventEntityType.External)
scheduledEndTime = mdl.ScheduledEndTime.Value;

await this.Discord.ApiClient.ModifyGuildScheduledEventAsync(this.GuildId, this.Id, channelId, this.EntityType == ScheduledEventEntityType.External ? new DiscordScheduledEventEntityMetadata(mdl.Location.Value) : null, mdl.Name, mdl.ScheduledStartTime, scheduledEndTime, mdl.Description, mdl.EntityType, mdl.Status, coverb64, mdl.AuditLogReason).ConfigureAwait(false);
await this.Discord.ApiClient.ModifyGuildScheduledEventAsync(this.GuildId, this.Id, channelId, this.EntityType == ScheduledEventEntityType.External ? new DiscordScheduledEventEntityMetadata(mdl.Location.Value) : null, mdl.Name, mdl.ScheduledStartTime, scheduledEndTime, mdl.Description, mdl.EntityType, mdl.Status, coverb64, mdl.RecurrenceRule, mdl.AuditLogReason).ConfigureAwait(false);
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
using System;
using System.Collections.Generic;

using DisCatSharp.Enums;

using Newtonsoft.Json;

namespace DisCatSharp.Entities;

/// <summary>
/// Represents the recurrence rule for a scheduled event.
/// </summary>
public sealed class DiscordScheduledEventRecurrenceRule
{
/// <summary>
/// Gets or sets the start time of the recurrence interval.
/// </summary>
[JsonProperty("start")]
public DateTimeOffset Start { get; set; }

/// <summary>
/// Gets the end time of the recurrence interval.
/// </summary>
[JsonProperty("end")]
public DateTimeOffset? End { get; internal set; }

/// <summary>
/// Gets or sets the frequency of the recurrence.
/// </summary>
[JsonProperty("frequency")]
public RecurrenceRuleFrequency Frequency { get; set; }

/// <summary>
/// Gets or sets the interval between events.
/// </summary>
[JsonProperty("interval")]
public int Interval { get; set; }

/// <summary>
/// Gets or sets specific days within a week for the event to recur on.
/// </summary>
[JsonProperty("by_weekday", NullValueHandling = NullValueHandling.Include)]
public List<RecurrenceRuleWeekday>? ByWeekday { get; set; }

/// <summary>
/// Gets or sets specific days within a specific week (1-5) to recur on.
/// </summary>
[JsonProperty("by_n_weekday", NullValueHandling = NullValueHandling.Include)]
public List<DiscordRecurrenceRuleNWeekday>? ByNWeekday { get; set; }

/// <summary>
/// Gets or sets specific months to recur on.
/// </summary>
[JsonProperty("by_month", NullValueHandling = NullValueHandling.Include)]
public List<int>? ByMonth { get; set; }

/// <summary>
/// Gets or sets specific dates within a month to recur on.
/// </summary>
[JsonProperty("by_month_day", NullValueHandling = NullValueHandling.Include)]
public List<int>? ByMonthDay { get; set; }

/// <summary>
/// Gets specific dates within a year to recur on.
/// </summary>
[JsonProperty("by_year_day", NullValueHandling = NullValueHandling.Include)]
public List<int>? ByYearDay { get; internal set; }

/// <summary>
/// Gets the total amount of times that the event is allowed to recur before stopping.
/// </summary>
[JsonProperty("count", NullValueHandling = NullValueHandling.Include)]
public int? Count { get; internal set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
using System.Collections.Generic;
using System.Linq;

using DisCatSharp.Enums;

namespace DisCatSharp.Entities;

/// <summary>
/// Validator for <see cref="DiscordScheduledEventRecurrenceRule" /> instances.
/// </summary>
public static class DiscordScheduledEventRecurrenceRuleValidator
{
/// <summary>
/// A collection of valid weekday sets for events with a daily recurrence frequency.
/// </summary>
private static readonly List<List<RecurrenceRuleWeekday>> s_validDailyWeekdaySets = [CreateWeekdaySet(RecurrenceRuleWeekday.Monday, RecurrenceRuleWeekday.Tuesday, RecurrenceRuleWeekday.Wednesday, RecurrenceRuleWeekday.Thursday, RecurrenceRuleWeekday.Friday), CreateWeekdaySet(RecurrenceRuleWeekday.Tuesday, RecurrenceRuleWeekday.Wednesday, RecurrenceRuleWeekday.Thursday, RecurrenceRuleWeekday.Friday, RecurrenceRuleWeekday.Saturday), CreateWeekdaySet(RecurrenceRuleWeekday.Sunday, RecurrenceRuleWeekday.Monday, RecurrenceRuleWeekday.Tuesday, RecurrenceRuleWeekday.Wednesday, RecurrenceRuleWeekday.Thursday), CreateWeekdaySet(RecurrenceRuleWeekday.Friday, RecurrenceRuleWeekday.Saturday), CreateWeekdaySet(RecurrenceRuleWeekday.Saturday, RecurrenceRuleWeekday.Sunday), CreateWeekdaySet(RecurrenceRuleWeekday.Sunday, RecurrenceRuleWeekday.Monday)];

/// <summary>
/// Creates a set of weekdays for easier readability.
/// </summary>
/// <param name="weekdays">The weekdays to include in the set.</param>
/// <returns>A list of <see cref="RecurrenceRuleWeekday" /> objects.</returns>
private static List<RecurrenceRuleWeekday> CreateWeekdaySet(params RecurrenceRuleWeekday[] weekdays)
=> [..weekdays];

/// <summary>
/// Validates the recurrence rule.
/// </summary>
/// <param name="rule">The recurrence rule to validate.</param>
/// <returns>A tuple containing a boolean indicating validity and an optional error message.</returns>
public static (bool IsValid, string? ErrorMessage) Validate(this DiscordScheduledEventRecurrenceRule rule)
{
return rule.ByWeekday is not null && rule.ByNWeekday is not null
? ((bool IsValid, string? ErrorMessage))(false, "by_weekday and by_n_weekday cannot both be set.")
: rule.ByMonth is not null && rule.ByMonthDay is not null
? ((bool IsValid, string? ErrorMessage))(false, "by_month and by_month_day cannot both be set.")
: rule.Frequency switch
{
RecurrenceRuleFrequency.Daily => rule.ValidateDailyFrequency(),
RecurrenceRuleFrequency.Weekly => rule.ValidateWeeklyFrequency(),
RecurrenceRuleFrequency.Monthly => rule.ValidateMonthlyFrequency(),
RecurrenceRuleFrequency.Yearly => rule.ValidateYearlyFrequency(),
_ => (false, "Unknown frequency type.")
};
}

/// <summary>
/// Validates recurrence rules with a daily frequency.
/// </summary>
/// <param name="rule">The recurrence rule to validate.</param>
/// <returns>A tuple containing a boolean indicating validity and an optional error message.</returns>
private static (bool IsValid, string? ErrorMessage) ValidateDailyFrequency(this DiscordScheduledEventRecurrenceRule rule)
=> rule.ByWeekday is not null && !rule.ByWeekday.IsValidDailyWeekdaySet()
? ((bool IsValid, string? ErrorMessage))(false, "Invalid by_weekday set for daily frequency.")
: (true, null);

/// <summary>
/// Validates recurrence rules with a weekly frequency.
/// </summary>
/// <param name="rule">The recurrence rule to validate.</param>
/// <returns>A tuple containing a boolean indicating validity and an optional error message.</returns>
private static (bool IsValid, string? ErrorMessage) ValidateWeeklyFrequency(this DiscordScheduledEventRecurrenceRule rule)
=> rule.ByWeekday?.Count is not 1
? ((bool IsValid, string? ErrorMessage))(false, "Weekly events must have a single day set in by_weekday.")
: rule.Interval is not 1 and not 2
? ((bool IsValid, string? ErrorMessage))(false, "Weekly events can only have an interval of 1 or 2.")
: (true, null);

/// <summary>
/// Validates recurrence rules with a monthly frequency.
/// </summary>
/// <param name="rule">The recurrence rule to validate.</param>
/// <returns>A tuple containing a boolean indicating validity and an optional error message.</returns>
private static (bool IsValid, string? ErrorMessage) ValidateMonthlyFrequency(this DiscordScheduledEventRecurrenceRule rule)
=> rule.ByNWeekday?.Count is not 1
? ((bool IsValid, string? ErrorMessage))(false, "Monthly events must have a single day set in by_n_weekday.")
: (true, null);

/// <summary>
/// Validates recurrence rules with a yearly frequency.
/// </summary>
/// <param name="rule">The recurrence rule to validate.</param>
/// <returns>A tuple containing a boolean indicating validity and an optional error message.</returns>
private static (bool IsValid, string? ErrorMessage) ValidateYearlyFrequency(this DiscordScheduledEventRecurrenceRule rule)
=> rule.ByMonth?.Count is not 1 || rule.ByMonthDay?.Count is not 1
? ((bool IsValid, string? ErrorMessage))(false, "Yearly events must have both by_month and by_month_day set to a single value.")
: (true, null);

/// <summary>
/// Validates if the weekday set is valid for a daily frequency.
/// </summary>
/// <param name="byWeekday">The weekday set to validate.</param>
/// <returns><see langword="true" /> if the set is valid; otherwise, <see langword="false" />.</returns>
private static bool IsValidDailyWeekdaySet(this IReadOnlyCollection<RecurrenceRuleWeekday> byWeekday)
=> s_validDailyWeekdaySets.Any(set => set.SequenceEqual(byWeekday));
}
27 changes: 27 additions & 0 deletions DisCatSharp/Enums/Guild/ScheduledEvent/RecurrenceRuleFrequency.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
namespace DisCatSharp.Enums;

/// <summary>
/// Represents the frequency of a scheduled event's recurrence.
/// </summary>
public enum RecurrenceRuleFrequency
{
/// <summary>
/// The scheduled event repeats yearly.
/// </summary>
Yearly = 0,

/// <summary>
/// The scheduled event repeats monthly.
/// </summary>
Monthly = 1,

/// <summary>
/// The scheduled event repeats weekly.
/// </summary>
Weekly = 2,

/// <summary>
/// The scheduled event repeats daily.
/// </summary>
Daily = 3
}
42 changes: 42 additions & 0 deletions DisCatSharp/Enums/Guild/ScheduledEvent/RecurrenceRuleWeekday.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
namespace DisCatSharp.Enums;

/// <summary>
/// Represents the days of the week for scheduling recurrent events.
/// </summary>
public enum RecurrenceRuleWeekday
{
/// <summary>
/// The scheduled event repeats on mondays.
/// </summary>
Monday = 0,

/// <summary>
/// The scheduled event repeats tuesdays.
/// </summary>
Tuesday = 1,

/// <summary>
/// The scheduled event repeats wednesdays.
/// </summary>
Wednesday = 2,

/// <summary>
/// The scheduled event repeats thursdays.
/// </summary>
Thursday = 3,

/// <summary>
/// The scheduled event repeats fridays.
/// </summary>
Friday = 4,

/// <summary>
/// The scheduled event repeats saturdays.
/// </summary>
Saturday = 5,

/// <summary>
/// The scheduled event repeats sundays.
/// </summary>
Sunday = 6
}
Loading

0 comments on commit 253cc6d

Please sign in to comment.