1
0
Fork 1
mirror of https://github.com/TeamOctolings/Octobot.git synced 2025-05-05 13:36:30 +03:00

The Milestone Commit (#48)

mctaylors:
- updated readme 7 times (and only adding new logo from /about)
-
[removed](aeeb3d4399)
bot footer from created event embed on the second try
-
[changed](4b9b91d9e4)
cdn from discord to upload.systems

Octol1ttle:
- Guild settings code has been overhauled. Instead of instances of a
`GuildConfiguration` class being (de-)serialized when used with listing
and setting options provided by reflection, there are now multiple
`Option` classes responsible for the type of option they are storing.
The classes support getting a value, validating and setting values with
Results, and getting a user-friendly representation of these values.
This makes use of polymorphism, providing clean and easier to use and
refactor code.
- Gateway event responders have been split into their own separate
files, which should make it easier to find and modify responders when
needed.
- Warning suppressions regarding unused and never instantiated classes
have been replaced by `[ImplicitUse]` annotations provided by
`JetBrains.Annotations`. This avoids hiding real issues and provides a
better way to suppress false warnings while being explicit.
- It is no longer possible to execute some slash commands if they are
run without the correct permissions
- Dependencies are now more explicitly defined

neroduckale:
 - Made easter eggs case-insensitive

---------

Signed-off-by: Macintosh II <95250141+mctaylors@users.noreply.github.com>
Signed-off-by: Octol1ttle <l1ttleofficial@outlook.com>
Co-authored-by: Octol1ttle <l1ttleofficial@outlook.com>
Co-authored-by: nrdk <neroduck@vk.com>
This commit is contained in:
Macintxsh 2023-07-18 15:25:02 +03:00 committed by GitHub
parent 3eb17b96c5
commit c6dd3727c3
Signed by: GitHub
GPG key ID: 4AEE18F83AFDEB23
39 changed files with 912 additions and 658 deletions

View file

@ -1,5 +1,6 @@
using System.Collections.Concurrent;
using System.Text.Json;
using System.Text.Json.Nodes;
using Boyfriend.Data;
using Microsoft.Extensions.Hosting;
using Remora.Discord.API.Abstractions.Rest;
@ -36,8 +37,8 @@ public class GuildDataService : IHostedService {
private async Task SaveAsync(CancellationToken ct) {
var tasks = new List<Task>();
foreach (var data in _datas.Values) {
await using var configStream = File.OpenWrite(data.ConfigurationPath);
tasks.Add(JsonSerializer.SerializeAsync(configStream, data.Configuration, cancellationToken: ct));
await using var settingsStream = File.OpenWrite(data.SettingsPath);
tasks.Add(JsonSerializer.SerializeAsync(settingsStream, data.Settings, cancellationToken: ct));
await using var eventsStream = File.OpenWrite(data.ScheduledEventsPath);
tasks.Add(JsonSerializer.SerializeAsync(eventsStream, data.ScheduledEvents, cancellationToken: ct));
@ -58,17 +59,16 @@ public class GuildDataService : IHostedService {
private async Task<GuildData> InitializeData(Snowflake guildId, CancellationToken ct = default) {
var idString = $"{guildId}";
var memberDataPath = $"{guildId}/MemberData";
var configurationPath = $"{guildId}/Configuration.json";
var settingsPath = $"{guildId}/Settings.json";
var scheduledEventsPath = $"{guildId}/ScheduledEvents.json";
if (!Directory.Exists(idString)) Directory.CreateDirectory(idString);
if (!Directory.Exists(memberDataPath)) Directory.CreateDirectory(memberDataPath);
if (!File.Exists(configurationPath)) await File.WriteAllTextAsync(configurationPath, "{}", ct);
if (!File.Exists(settingsPath)) await File.WriteAllTextAsync(settingsPath, "{}", ct);
if (!File.Exists(scheduledEventsPath)) await File.WriteAllTextAsync(scheduledEventsPath, "{}", ct);
await using var configurationStream = File.OpenRead(configurationPath);
var configuration
= JsonSerializer.DeserializeAsync<GuildConfiguration>(
configurationStream, cancellationToken: ct);
await using var settingsStream = File.OpenRead(settingsPath);
var jsonSettings
= JsonNode.Parse(settingsStream);
await using var eventsStream = File.OpenRead(scheduledEventsPath);
var events
@ -80,23 +80,23 @@ public class GuildDataService : IHostedService {
await using var dataStream = File.OpenRead(dataPath);
var data = await JsonSerializer.DeserializeAsync<MemberData>(dataStream, cancellationToken: ct);
if (data is null) continue;
var memberResult = await _guildApi.GetGuildMemberAsync(guildId, data.Id.ToDiscordSnowflake(), ct);
var memberResult = await _guildApi.GetGuildMemberAsync(guildId, data.Id.ToSnowflake(), ct);
if (memberResult.IsSuccess)
data.Roles = memberResult.Entity.Roles.ToList();
data.Roles = memberResult.Entity.Roles.ToList().ConvertAll(r => r.Value);
memberData.Add(data.Id, data);
}
var finalData = new GuildData(
await configuration ?? new GuildConfiguration(), configurationPath,
jsonSettings ?? new JsonObject(), settingsPath,
await events ?? new Dictionary<ulong, ScheduledEventData>(), scheduledEventsPath,
memberData, memberDataPath);
while (!_datas.ContainsKey(guildId)) _datas.TryAdd(guildId, finalData);
return finalData;
}
public async Task<GuildConfiguration> GetConfiguration(Snowflake guildId, CancellationToken ct = default) {
return (await GetData(guildId, ct)).Configuration;
public async Task<JsonNode> GetSettings(Snowflake guildId, CancellationToken ct = default) {
return (await GetData(guildId, ct)).Settings;
}
public async Task<MemberData> GetMemberData(Snowflake guildId, Snowflake userId, CancellationToken ct = default) {

View file

@ -1,3 +1,4 @@
using System.Text.Json.Nodes;
using Boyfriend.Data;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
@ -94,9 +95,9 @@ public class GuildUpdateService : BackgroundService {
/// This method does the following:
/// <list type="bullet">
/// <item>Automatically unbans users once their ban period has expired.</item>
/// <item>Automatically grants members the guild's <see cref="GuildConfiguration.DefaultRole"/> if one is set.</item>
/// <item>Automatically grants members the guild's <see cref="GuildSettings.DefaultRole"/> if one is set.</item>
/// <item>Sends reminders about an upcoming scheduled event.</item>
/// <item>Automatically starts scheduled events if <see cref="GuildConfiguration.AutoStartEvents"/> is enabled.</item>
/// <item>Automatically starts scheduled events if <see cref="GuildSettings.AutoStartEvents"/> is enabled.</item>
/// <item>Sends scheduled event start notifications.</item>
/// <item>Sends scheduled event completion notifications.</item>
/// <item>Sends reminders to members.</item>
@ -114,15 +115,15 @@ public class GuildUpdateService : BackgroundService {
/// <param name="ct">The cancellation token for this operation.</param>
private async Task TickGuildAsync(Snowflake guildId, CancellationToken ct = default) {
var data = await _dataService.GetData(guildId, ct);
Messages.Culture = data.Culture;
var defaultRoleSnowflake = data.Configuration.DefaultRole.ToDiscordSnowflake();
Messages.Culture = GuildSettings.Language.Get(data.Settings);
var defaultRole = GuildSettings.DefaultRole.Get(data.Settings);
foreach (var memberData in data.MemberData.Values) {
var userId = memberData.Id.ToDiscordSnowflake();
var userId = memberData.Id.ToSnowflake();
if (defaultRoleSnowflake.Value is not 0 && !memberData.Roles.Contains(defaultRoleSnowflake))
if (defaultRole.Value is not 0 && !memberData.Roles.Contains(defaultRole.Value))
_ = _guildApi.AddGuildMemberRoleAsync(
guildId, userId, defaultRoleSnowflake, ct: ct);
guildId, userId, defaultRole, ct: ct);
if (DateTimeOffset.UtcNow > memberData.BannedUntil) {
var unbanResult = await _guildApi.RemoveGuildBanAsync(
@ -139,7 +140,7 @@ public class GuildUpdateService : BackgroundService {
for (var i = memberData.Reminders.Count - 1; i >= 0; i--) {
var reminder = memberData.Reminders[i];
if (DateTimeOffset.UtcNow < reminder.RemindAt) continue;
if (DateTimeOffset.UtcNow < reminder.At) continue;
var embed = new EmbedBuilder().WithSmallTitle(
string.Format(Messages.Reminder, user.GetTag()), user)
@ -151,7 +152,7 @@ public class GuildUpdateService : BackgroundService {
if (!embed.IsDefined(out var built)) continue;
var messageResult = await _channelApi.CreateMessageAsync(
reminder.Channel, Mention.User(user), embeds: new[] { built }, ct: ct);
reminder.Channel.ToSnowflake(), Mention.User(user), embeds: new[] { built }, ct: ct);
if (!messageResult.IsSuccess)
_logger.LogWarning(
"Error in reminder send.\n{ErrorMessage}", messageResult.Error.Message);
@ -163,7 +164,7 @@ public class GuildUpdateService : BackgroundService {
var eventsResult = await _eventApi.ListScheduledEventsForGuildAsync(guildId, ct: ct);
if (!eventsResult.IsDefined(out var events)) return;
if (data.Configuration.EventNotificationChannel is 0) return;
if (GuildSettings.EventNotificationChannel.Get(data.Settings).Empty()) return;
foreach (var scheduledEvent in events) {
if (!data.ScheduledEvents.ContainsKey(scheduledEvent.ID.Value)) {
@ -172,7 +173,7 @@ public class GuildUpdateService : BackgroundService {
var storedEvent = data.ScheduledEvents[scheduledEvent.ID.Value];
if (storedEvent.Status == scheduledEvent.Status) {
if (DateTimeOffset.UtcNow >= scheduledEvent.ScheduledStartTime) {
if (data.Configuration.AutoStartEvents
if (GuildSettings.AutoStartEvents.Get(data.Settings)
&& scheduledEvent.Status is not GuildScheduledEventStatus.Active) {
var startResult = await _eventApi.ModifyGuildScheduledEventAsync(
guildId, scheduledEvent.ID,
@ -182,10 +183,11 @@ public class GuildUpdateService : BackgroundService {
"Error in automatic scheduled event start request.\n{ErrorMessage}",
startResult.Error.Message);
}
} else if (data.Configuration.EventEarlyNotificationOffset != TimeSpan.Zero
} else if (GuildSettings.EventEarlyNotificationOffset.Get(data.Settings) != TimeSpan.Zero
&& !storedEvent.EarlyNotificationSent
&& DateTimeOffset.UtcNow
>= scheduledEvent.ScheduledStartTime - data.Configuration.EventEarlyNotificationOffset) {
>= scheduledEvent.ScheduledStartTime
- GuildSettings.EventEarlyNotificationOffset.Get(data.Settings)) {
var earlyResult = await SendScheduledEventUpdatedMessage(scheduledEvent, data, true, ct);
if (earlyResult.IsSuccess)
storedEvent.EarlyNotificationSent = true;
@ -203,7 +205,7 @@ public class GuildUpdateService : BackgroundService {
var result = scheduledEvent.Status switch {
GuildScheduledEventStatus.Scheduled =>
await SendScheduledEventCreatedMessage(scheduledEvent, data.Configuration, ct),
await SendScheduledEventCreatedMessage(scheduledEvent, data.Settings, ct),
GuildScheduledEventStatus.Active or GuildScheduledEventStatus.Completed =>
await SendScheduledEventUpdatedMessage(scheduledEvent, data, false, ct),
_ => Result.FromError(new ArgumentOutOfRangeError(nameof(scheduledEvent.Status)))
@ -215,19 +217,17 @@ public class GuildUpdateService : BackgroundService {
}
/// <summary>
/// Handles sending a notification, mentioning the <see cref="GuildConfiguration.EventNotificationRole" /> if one is
/// Handles sending a notification, mentioning the <see cref="GuildSettings.EventNotificationRole" /> if one is
/// set,
/// when a scheduled event is created
/// in a guild's <see cref="GuildConfiguration.EventNotificationChannel" /> if one is set.
/// in a guild's <see cref="GuildSettings.EventNotificationChannel" /> if one is set.
/// </summary>
/// <param name="scheduledEvent">The scheduled event that has just been created.</param>
/// <param name="config">The configuration of the guild containing the scheduled event.</param>
/// <param name="settings">The settings of the guild containing the scheduled event.</param>
/// <param name="ct">The cancellation token for this operation.</param>
/// <returns>A notification sending result which may or may not have succeeded.</returns>
private async Task<Result> SendScheduledEventCreatedMessage(
IGuildScheduledEvent scheduledEvent, GuildConfiguration config, CancellationToken ct = default) {
var currentUserResult = await _userApi.GetCurrentUserAsync(ct);
if (!currentUserResult.IsDefined(out var currentUser)) return Result.FromError(currentUserResult);
IGuildScheduledEvent scheduledEvent, JsonNode settings, CancellationToken ct = default) {
if (!scheduledEvent.CreatorID.IsDefined(out var creatorId))
return Result.FromError(new ArgumentNullError(nameof(scheduledEvent.CreatorID)));
@ -275,14 +275,13 @@ public class GuildUpdateService : BackgroundService {
.WithTitle(scheduledEvent.Name)
.WithDescription(embedDescription)
.WithEventCover(scheduledEvent.ID, scheduledEvent.Image)
.WithUserFooter(currentUser)
.WithCurrentTimestamp()
.WithColour(ColorsList.White)
.Build();
if (!embed.IsDefined(out var built)) return Result.FromError(embed);
var roleMention = config.EventNotificationRole is not 0
? Mention.Role(config.EventNotificationRole.ToDiscordSnowflake())
var roleMention = !GuildSettings.EventNotificationRole.Get(settings).Empty()
? Mention.Role(GuildSettings.EventNotificationRole.Get(settings))
: string.Empty;
var button = new ButtonComponent(
@ -294,14 +293,14 @@ public class GuildUpdateService : BackgroundService {
);
return (Result)await _channelApi.CreateMessageAsync(
config.EventNotificationChannel.ToDiscordSnowflake(), roleMention, embeds: new[] { built },
GuildSettings.EventNotificationChannel.Get(settings), roleMention, embeds: new[] { built },
components: new[] { new ActionRowComponent(new[] { button }) }, ct: ct);
}
/// <summary>
/// Handles sending a notification, mentioning the <see cref="GuildConfiguration.EventStartedReceivers" />s,
/// Handles sending a notification, mentioning the <see cref="GuildSettings.EventNotificationRole"/> and event subscribers,
/// when a scheduled event is about to start, has started or completed
/// in a guild's <see cref="GuildConfiguration.EventNotificationChannel" /> if one is set.
/// in a guild's <see cref="GuildSettings.EventNotificationChannel" /> if one is set.
/// </summary>
/// <param name="scheduledEvent">The scheduled event that is about to start, has started or completed.</param>
/// <param name="data">The data for the guild containing the scheduled event.</param>
@ -353,7 +352,7 @@ public class GuildUpdateService : BackgroundService {
}
var contentResult = await _utility.GetEventNotificationMentions(
scheduledEvent, data.Configuration, ct);
scheduledEvent, data.Settings, ct);
if (!contentResult.IsDefined(out content))
return Result.FromError(contentResult);
@ -383,7 +382,7 @@ public class GuildUpdateService : BackgroundService {
if (!result.IsDefined(out var built)) return Result.FromError(result);
return (Result)await _channelApi.CreateMessageAsync(
data.Configuration.EventNotificationChannel.ToDiscordSnowflake(),
GuildSettings.EventNotificationChannel.Get(data.Settings),
content ?? default(Optional<string>), embeds: new[] { built }, ct: ct);
}
}

View file

@ -1,4 +1,5 @@
using System.Text;
using System.Text.Json.Nodes;
using Boyfriend.Data;
using Microsoft.Extensions.Hosting;
using Remora.Discord.API.Abstractions.Objects;
@ -103,38 +104,32 @@ public class UtilityService : IHostedService {
}
/// <summary>
/// Gets the string mentioning all <see cref="GuildConfiguration.NotificationReceiver" />s related to a scheduled
/// Gets the string mentioning the <see cref="GuildSettings.EventNotificationRole"/> and event subscribers related to a scheduled
/// event.
/// </summary>
/// <remarks>
/// If the guild configuration enables <see cref="GuildConfiguration.NotificationReceiver.Role" />, then the
/// <see cref="GuildConfiguration.EventNotificationRole" /> will also be mentioned.
/// </remarks>
/// <param name="scheduledEvent">
/// The scheduled event whose subscribers will be mentioned if the guild configuration enables
/// <see cref="GuildConfiguration.NotificationReceiver.Interested" />.
/// The scheduled event whose subscribers will be mentioned.
/// </param>
/// <param name="config">The configuration of the guild containing the scheduled event</param>
/// <param name="settings">The settings of the guild containing the scheduled event</param>
/// <param name="ct">The cancellation token for this operation.</param>
/// <returns>A result containing the string which may or may not have succeeded.</returns>
public async Task<Result<string>> GetEventNotificationMentions(
IGuildScheduledEvent scheduledEvent, GuildConfiguration config, CancellationToken ct = default) {
IGuildScheduledEvent scheduledEvent, JsonNode settings, CancellationToken ct = default) {
var builder = new StringBuilder();
var receivers = config.EventStartedReceivers;
var role = config.EventNotificationRole.ToDiscordSnowflake();
var role = GuildSettings.EventNotificationRole.Get(settings);
var usersResult = await _eventApi.GetGuildScheduledEventUsersAsync(
scheduledEvent.GuildID, scheduledEvent.ID, withMember: true, ct: ct);
if (!usersResult.IsDefined(out var users)) return Result<string>.FromError(usersResult);
if (receivers.Contains(GuildConfiguration.NotificationReceiver.Role) && role.Value is not 0)
if (role.Value is not 0)
builder.Append($"{Mention.Role(role)} ");
if (receivers.Contains(GuildConfiguration.NotificationReceiver.Interested))
builder = users.Where(
user => {
if (!user.GuildMember.IsDefined(out var member)) return true;
return !member.Roles.Contains(role);
})
.Aggregate(builder, (current, user) => current.Append($"{Mention.User(user.User)} "));
builder = users.Where(
user => {
if (!user.GuildMember.IsDefined(out var member)) return true;
return !member.Roles.Contains(role);
})
.Aggregate(builder, (current, user) => current.Append($"{Mention.User(user.User)} "));
return builder.ToString();
}
}