diff --git a/Numerous.sln.DotSettings b/Numerous.sln.DotSettings
index 3822a76..b3537fb 100644
--- a/Numerous.sln.DotSettings
+++ b/Numerous.sln.DotSettings
@@ -47,7 +47,7 @@
ERROR
WARNING
WARNING
- WARNING
+ DO_NOT_SHOW
WARNING
WARNING
WARNING
@@ -66,6 +66,8 @@
True
ShowAndRun
1
+ True
+ 70
ALWAYS
NEVER
ALWAYS
diff --git a/Numerous/ApiClients/Osu/Extensions.cs b/Numerous/ApiClients/Osu/Extensions.cs
new file mode 100644
index 0000000..0b8e1dc
--- /dev/null
+++ b/Numerous/ApiClients/Osu/Extensions.cs
@@ -0,0 +1,29 @@
+// Copyright (C) Pasi4K5
+// This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
+// This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+// You should have received a copy of the GNU General Public License along with this program. If not, see .
+
+using Numerous.ApiClients.Osu.Models;
+
+namespace Numerous.ApiClients.Osu;
+
+public static class Extensions
+{
+ public static bool IsRankedMapper(this OsuUser user)
+ {
+ return user.RankedBeatmapsetCount > 0
+ || user.GuestBeatmapsetCount > 0;
+ }
+
+ public static bool IsLovedMapper(this OsuUser user)
+ {
+ return user.LovedBeatmapsetCount > 0;
+ }
+
+ public static bool IsUnrankedMapper(this OsuUser user)
+ {
+ return
+ (user.GraveyardBeatmapsetCount > 0 || user.PendingBeatmapsetCount > 0)
+ && !user.IsRankedMapper();
+ }
+}
diff --git a/Numerous/ApiClients/Osu/Models/OsuUser.cs b/Numerous/ApiClients/Osu/Models/OsuUser.cs
index 7a4f818..9a0fa31 100644
--- a/Numerous/ApiClients/Osu/Models/OsuUser.cs
+++ b/Numerous/ApiClients/Osu/Models/OsuUser.cs
@@ -7,7 +7,7 @@
namespace Numerous.ApiClients.Osu.Models;
-public record struct OsuUser
+public record OsuUser
{
[JsonProperty("id")]
public uint Id { get; init; }
@@ -16,5 +16,34 @@ public record struct OsuUser
public bool IsBot { get; init; }
[JsonProperty("username")]
- public string Username { get; init; }
+ public string Username { get; init; } = null!;
+
+ [JsonProperty("groups")]
+ public OsuUserGroup[]? Groups { get; set; } = Array.Empty();
+
+ [JsonProperty("graveyard_beatmapset_count")]
+ public uint GraveyardBeatmapsetCount { get; init; }
+
+ [JsonProperty("guest_beatmapset_count")]
+ public uint GuestBeatmapsetCount { get; init; }
+
+ [JsonProperty("loved_beatmapset_count")]
+ public uint LovedBeatmapsetCount { get; init; }
+
+ [JsonProperty("pending_beatmapset_count")]
+ public uint PendingBeatmapsetCount { get; init; }
+
+ [JsonProperty("ranked_beatmapset_count")]
+ public uint RankedBeatmapsetCount { get; init; }
+
+ public OsuUserGroup[] GetGroups()
+ {
+ return Groups ?? Array.Empty();
+ }
+}
+
+public sealed record OsuUserExtended : OsuUser
+{
+ [JsonProperty("discord")]
+ public string DiscordUsername { get; init; } = null!;
}
diff --git a/Numerous/ApiClients/Osu/OsuApi.Repository.cs b/Numerous/ApiClients/Osu/OsuApi.Repository.cs
index bb91d86..dcbc29e 100644
--- a/Numerous/ApiClients/Osu/OsuApi.Repository.cs
+++ b/Numerous/ApiClients/Osu/OsuApi.Repository.cs
@@ -25,6 +25,14 @@ public partial class OsuApi
return await RequestRefAsync($"users/{user}", ("key", "id"));
}
+ ///
+ /// Prioritizes the user ID over the username.
+ ///
+ public async Task GetUserAsync(string user)
+ {
+ return await RequestRefAsync($"users/{user}");
+ }
+
public async Task GetBeatmapsetAsync(uint id)
{
return await RequestValAsync($"beatmapsets/{id}");
diff --git a/Numerous/ApiClients/Osu/OsuUserGroup.cs b/Numerous/ApiClients/Osu/OsuUserGroup.cs
new file mode 100644
index 0000000..4c6a2c3
--- /dev/null
+++ b/Numerous/ApiClients/Osu/OsuUserGroup.cs
@@ -0,0 +1,26 @@
+// Copyright (C) Pasi4K5
+// This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
+// This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+// You should have received a copy of the GNU General Public License along with this program. If not, see .
+
+using Discord.Interactions;
+
+namespace Numerous.ApiClients.Osu;
+
+public enum OsuUserGroup
+{
+ [ChoiceDisplay("Unranked Mapper")] UnrankedMapper = -1,
+ [ChoiceDisplay("Ranked Mapper")] RankedMapper = -2,
+ [ChoiceDisplay("Loved Mapper")] LovedMapper = -3,
+ [ChoiceDisplay("GMT")] GlobalModerationTeam = 4,
+ [ChoiceDisplay("NAT")] NominationAssessmentTeam = 7,
+ [ChoiceDisplay("Developers")] Developers = 11,
+ [ChoiceDisplay("Community Contributors")] OsuAlumin = 16,
+ [ChoiceDisplay("Technical Support Team")] TechnicalSupportTeam = 17,
+ [ChoiceDisplay("Beatmap Nominators")] BeatmapNominators = 28,
+ [ChoiceDisplay("Project Loved")] ProjectLoved = 31,
+ [ChoiceDisplay("Beatmap Nominators (Probationary)")] BeatmapNominatorsProbationary = 32,
+ [ChoiceDisplay("ppy")] Ppy = 33,
+ [ChoiceDisplay("Featured Artists")] FeaturedArtists = 35,
+ [ChoiceDisplay("Beatmap Spotlight Curators")] BeatmapSpotlightCurators = 48,
+}
diff --git a/Numerous/Database/DbManager.cs b/Numerous/Database/DbManager.cs
index 983e70d..61f9d44 100644
--- a/Numerous/Database/DbManager.cs
+++ b/Numerous/Database/DbManager.cs
@@ -43,20 +43,27 @@ public DbManager(ConfigManager configManager)
}
}
+ public async Task EnsureUserExistsAsync(ulong id)
+ {
+ await GetUserAsync(id);
+ }
+
public async Task GetUserAsync(ulong id)
{
var user = await Users.Find(x => x.Id == id).FirstOrDefaultAsync();
- if (user is null)
+ if (user is not null)
{
- user = new DbUser
- {
- Id = id,
- };
-
- await Users.InsertOneAsync(user);
+ return user;
}
+ user = new DbUser
+ {
+ Id = id,
+ };
+
+ await Users.InsertOneAsync(user);
+
return user;
}
}
diff --git a/Numerous/Database/Entities/DbUser.cs b/Numerous/Database/Entities/DbUser.cs
index 992d63d..ac2638b 100644
--- a/Numerous/Database/Entities/DbUser.cs
+++ b/Numerous/Database/Entities/DbUser.cs
@@ -10,5 +10,6 @@ public sealed record DbUser : DbEntity
// For future osu!-related features
// public DbOsuUser? OsuUser { get; init; }
+ public uint? OsuId { get; set; }
public string? TimeZone { get; set; }
}
diff --git a/Numerous/Database/Entities/GuildOptions.cs b/Numerous/Database/Entities/GuildOptions.cs
index c956b4b..717299b 100644
--- a/Numerous/Database/Entities/GuildOptions.cs
+++ b/Numerous/Database/Entities/GuildOptions.cs
@@ -3,6 +3,8 @@
// This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
// You should have received a copy of the GNU General Public License along with this program. If not, see .
+using Numerous.ApiClients.Osu;
+
namespace Numerous.Database.Entities;
// TODO: Remove comment
@@ -11,6 +13,7 @@ public sealed record GuildOptions : DbEntity
{
public bool TrackMessages { get; init; }
public bool TrackMemberCount { get; init; }
+ public ICollection OsuRoles { get; init; } = [];
public TrackingOptions[] PlayerTrackingOptions { get; init; } = Array.Empty();
@@ -19,4 +22,10 @@ public record struct TrackingOptions
public ulong DiscordId { get; init; }
public bool TrackPlayer { get; init; }
}
+
+ public record struct OsuRole
+ {
+ public OsuUserGroup Group { get; init; }
+ public ulong RoleId { get; init; }
+ }
}
diff --git a/Numerous/Discord/Commands/CommandModule.cs b/Numerous/Discord/Commands/CommandModule.cs
index 41010f4..5501e15 100644
--- a/Numerous/Discord/Commands/CommandModule.cs
+++ b/Numerous/Discord/Commands/CommandModule.cs
@@ -30,9 +30,10 @@ protected async Task RespondWithEmbedAsync(string message, ResponseType type = R
);
}
- protected async Task FollowupWithEmbedAsync(string message, ResponseType type = ResponseType.Info)
+ protected async Task FollowupWithEmbedAsync(string title = "", string message = "", ResponseType type = ResponseType.Info)
{
await FollowupAsync(embed: new EmbedBuilder()
+ .WithTitle(title)
.WithColor(GetTypeColor(type))
.WithDescription(message)
.Build()
diff --git a/Numerous/Discord/Commands/ConfigCommandModule.cs b/Numerous/Discord/Commands/ConfigCommandModule.cs
new file mode 100644
index 0000000..448e25b
--- /dev/null
+++ b/Numerous/Discord/Commands/ConfigCommandModule.cs
@@ -0,0 +1,49 @@
+// Copyright (C) Pasi4K5
+// This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
+// This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+// You should have received a copy of the GNU General Public License along with this program. If not, see .
+
+using Discord;
+using Discord.Interactions;
+using JetBrains.Annotations;
+using Numerous.ApiClients.Osu;
+
+namespace Numerous.Discord.Commands;
+
+[Group("config", "Configuration commands")]
+[DefaultMemberPermissions(GuildPermission.Administrator)]
+public sealed class ConfigCommandModule : CommandModule
+{
+ [Group("role", "Role configuration commands")]
+ public sealed class RoleCommandModule(OsuVerifier verifier) : CommandModule
+ {
+ [UsedImplicitly]
+ [SlashCommand("set", "Configures which role to assign to which users.")]
+ public async Task SetRole(
+ [Summary("group", "The group to set the role for.")] OsuUserGroup group,
+ [Summary("role", "The role to assign to users in the group.")] IRole role
+ )
+ {
+ await verifier.SetRoleAsync(Context.Guild, group, role);
+
+ await RespondWithEmbedAsync(
+ $"Set role for group {group} to {role.Mention}.",
+ ResponseType.Success
+ );
+ }
+
+ [UsedImplicitly]
+ [SlashCommand("remove", "Stops automatically assigning the given role.")]
+ public async Task RemoveRole(
+ [Summary("group", "The group to remove the role for.")] OsuUserGroup group
+ )
+ {
+ await verifier.RemoveRoleAsync(Context.Guild, group);
+
+ await RespondWithEmbedAsync(
+ $"Removed role for group {group}.",
+ ResponseType.Success
+ );
+ }
+ }
+}
diff --git a/Numerous/Discord/Commands/InteractionHandler.cs b/Numerous/Discord/Commands/InteractionHandler.cs
index 958bc0f..c939ec0 100644
--- a/Numerous/Discord/Commands/InteractionHandler.cs
+++ b/Numerous/Discord/Commands/InteractionHandler.cs
@@ -22,7 +22,7 @@ public sealed class InteractionHandler(
ConfigManager configManager
) : IHostedService
{
- public async Task StartAsync(CancellationToken cancellationToken)
+ public Task StartAsync(CancellationToken cancellationToken)
{
var cfg = configManager.Get();
@@ -40,8 +40,11 @@ public async Task StartAsync(CancellationToken cancellationToken)
return Task.CompletedTask;
};
- client.Ready += cfg.GuildMode
- ? async () =>
+ client.Ready += async () =>
+ {
+ await interactions.AddModulesAsync(Assembly.GetEntryAssembly(), services);
+
+ if (cfg.GuildMode)
{
foreach (var cmd in await client.GetGlobalApplicationCommandsAsync())
{
@@ -53,7 +56,7 @@ public async Task StartAsync(CancellationToken cancellationToken)
await interactions.RegisterCommandsToGuildAsync(guildId);
}
}
- : async () =>
+ else
{
foreach (var guild in await client.Rest.GetGuildsAsync())
{
@@ -61,10 +64,11 @@ public async Task StartAsync(CancellationToken cancellationToken)
}
await interactions.RegisterCommandsGloballyAsync();
- };
+ }
+ };
client.InteractionCreated += OnInteractionCreatedAsync;
- await interactions.AddModulesAsync(Assembly.GetEntryAssembly(), services);
+ return Task.CompletedTask;
}
public Task StopAsync(CancellationToken cancellationToken)
diff --git a/Numerous/Discord/Commands/ReminderCommandModule.cs b/Numerous/Discord/Commands/ReminderCommandModule.cs
index 6553dc4..2bcb773 100644
--- a/Numerous/Discord/Commands/ReminderCommandModule.cs
+++ b/Numerous/Discord/Commands/ReminderCommandModule.cs
@@ -103,8 +103,8 @@ public async Task AtCommand(
if (timeZoneId is null)
{
await FollowupWithEmbedAsync(
- "To use this command, please set your time zone with `/settimezone` first.",
- ResponseType.Error
+ message: "To use this command, please set your time zone with `/settimezone` first.",
+ type: ResponseType.Error
);
return;
@@ -120,8 +120,8 @@ await FollowupWithEmbedAsync(
if (timestamp is null)
{
await FollowupWithEmbedAsync(
- "The specified time must be at least 10 seconds in the future.",
- ResponseType.Error
+ message: "The specified time must be at least 10 seconds in the future.",
+ type: ResponseType.Error
);
return;
@@ -143,7 +143,7 @@ await FollowupAsync(embed:
}
catch (ArgumentOutOfRangeException)
{
- await FollowupWithEmbedAsync("The specified time is invalid.", ResponseType.Error);
+ await FollowupWithEmbedAsync("The specified time is invalid.", type: ResponseType.Error);
}
}
@@ -281,7 +281,7 @@ int index
if (index < 1 || index > reminders.Count)
{
- await FollowupWithEmbedAsync("There is no reminder with that index.", ResponseType.Error);
+ await FollowupWithEmbedAsync("There is no reminder with that index.", type: ResponseType.Error);
return;
}
diff --git a/Numerous/Discord/Commands/VerifyCommandModule.cs b/Numerous/Discord/Commands/VerifyCommandModule.cs
new file mode 100644
index 0000000..802110f
--- /dev/null
+++ b/Numerous/Discord/Commands/VerifyCommandModule.cs
@@ -0,0 +1,82 @@
+// Copyright (C) Pasi4K5
+// This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
+// This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+// You should have received a copy of the GNU General Public License along with this program. If not, see .
+
+using Discord.Interactions;
+using JetBrains.Annotations;
+using Numerous.ApiClients.Osu;
+
+namespace Numerous.Discord.Commands;
+
+public sealed class VerifyCommandModule(OsuApi osu, OsuVerifier verifier) : CommandModule
+{
+ [UsedImplicitly]
+ [SlashCommand("verify", "Verifies your osu! account.")]
+ public async Task Verify(
+ [Summary("user", "Your osu! username or user ID")]
+ string username
+ )
+ {
+ await DeferAsync();
+
+ var guildUser = Context.Guild.GetUser(Context.User.Id);
+
+ if (await verifier.UserIsVerifiedAsync(guildUser))
+ {
+ await FollowupWithEmbedAsync(
+ "You are already verified.",
+ type: ResponseType.Info
+ );
+
+ return;
+ }
+
+ var osuUser = await osu.GetUserAsync(username);
+
+ if (osuUser is null)
+ {
+ await FollowupWithEmbedAsync(
+ "User not found.",
+ type: ResponseType.Error
+ );
+
+ return;
+ }
+
+ if (!string.Equals(osuUser.DiscordUsername, Context.User.Username, StringComparison.OrdinalIgnoreCase))
+ {
+ await FollowupWithEmbedAsync(
+ "Verification failed",
+ "Please make sure that the provided osu! username or ID is correct and that "
+ + $"the \"Discord\" field on your osu! profile matches your Discord username (\"{Context.User.Username}\").\n"
+ + "You can change that in your [osu! account settings](https://osu.ppy.sh/home/account/edit) under \"Profile\"\u2192\"discord\".\n"
+ + "After the verification, you can remove it again if you want to.\n\n"
+ + "**Important:**\n"
+ + "* Do not enter another person's Discord username in your osu! profile. Otherwise they will be able to verify as you.\n"
+ + "* You can only perform this verification once.",
+ ResponseType.Error
+ );
+
+ return;
+ }
+
+ if (await verifier.OsuUserIsVerifiedAsync(osuUser))
+ {
+ await FollowupWithEmbedAsync(
+ message: "This osu! account is already verified. "
+ + "If you believe this is a mistake or you have lost access to your Discord account, please contact a server administrator.",
+ type: ResponseType.Error
+ );
+
+ return;
+ }
+
+ await verifier.VerifyUserAsync(Context.Guild.GetUser(Context.User.Id), osuUser);
+
+ await FollowupWithEmbedAsync(
+ "Verification successful",
+ type: ResponseType.Success
+ );
+ }
+}
diff --git a/Numerous/Discord/OsuVerifier.cs b/Numerous/Discord/OsuVerifier.cs
new file mode 100644
index 0000000..cf839bf
--- /dev/null
+++ b/Numerous/Discord/OsuVerifier.cs
@@ -0,0 +1,181 @@
+// Copyright (C) Pasi4K5
+// This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
+// This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+// You should have received a copy of the GNU General Public License along with this program. If not, see .
+
+using Coravel;
+using Discord;
+using Discord.WebSocket;
+using Microsoft.Extensions.Hosting;
+using MongoDB.Driver;
+using Numerous.ApiClients.Osu;
+using Numerous.ApiClients.Osu.Models;
+using Numerous.Database;
+using Numerous.Database.Entities;
+using Numerous.DependencyInjection;
+
+namespace Numerous.Discord;
+
+[SingletonService]
+public sealed class OsuVerifier(IHost host, DiscordSocketClient discord, DbManager db, OsuApi osu)
+{
+ public Task StartAsync()
+ {
+ host.Services.UseScheduler(scheduler => scheduler.ScheduleAsync(async () =>
+ {
+ foreach (var guild in discord.Guilds)
+ {
+ var dbUsers = await db.Users.FindAsync(x => x.OsuId != null);
+
+ await dbUsers.ForEachAsync(async dbUser =>
+ {
+ var guildUser = guild.GetUser(dbUser.Id);
+
+ if (guildUser is null)
+ {
+ return;
+ }
+
+ await AssignRolesAsync(guildUser);
+ });
+ }
+ }).EveryFiveSeconds());
+
+ return Task.CompletedTask;
+ }
+
+ public async Task UserIsVerifiedAsync(IGuildUser guildUser)
+ {
+ var user = await db.GetUserAsync(guildUser.Id);
+
+ return user.OsuId is not null;
+ }
+
+ public async Task OsuUserIsVerifiedAsync(OsuUser osuUser)
+ {
+ return await db.Users.Find(x => x.OsuId == osuUser.Id).AnyAsync();
+ }
+
+ public async Task VerifyUserAsync(IGuildUser guildUser, OsuUser osuUser)
+ {
+ await db.EnsureUserExistsAsync(guildUser.Id);
+
+ await db.Users.UpdateOneAsync(
+ Builders.Filter.Eq(x => x.Id, guildUser.Id),
+ Builders.Update.Set(x => x.OsuId, osuUser.Id)
+ );
+
+ await AssignRolesAsync(guildUser);
+ }
+
+ public async Task SetRoleAsync(IGuild guild, OsuUserGroup group, IRole role)
+ {
+ var guildConfig = await (await db.GuildOptions.FindAsync(x => x.Id == guild.Id)).SingleAsync();
+
+ guildConfig.OsuRoles.Remove(guildConfig.OsuRoles.FirstOrDefault(x => x.Group == group));
+ guildConfig.OsuRoles.Add(new GuildOptions.OsuRole
+ {
+ Group = group,
+ RoleId = role.Id,
+ });
+
+ await db.GuildOptions.UpdateOneAsync(
+ Builders.Filter.Eq(x => x.Id, guild.Id),
+ Builders.Update.Set(x => x.OsuRoles, guildConfig.OsuRoles)
+ );
+ }
+
+ public async Task RemoveRoleAsync(IGuild guild, OsuUserGroup group)
+ {
+ var guildConfig = await (await db.GuildOptions.FindAsync(x => x.Id == guild.Id)).SingleAsync();
+
+ guildConfig.OsuRoles.Remove(guildConfig.OsuRoles.FirstOrDefault(x => x.Group == group));
+
+ await db.GuildOptions.UpdateOneAsync(
+ Builders.Filter.Eq(x => x.Id, guild.Id),
+ Builders.Update.Set(x => x.OsuRoles, guildConfig.OsuRoles)
+ );
+ }
+
+ private async Task AssignRolesAsync(IGuildUser guildUser)
+ {
+ var osuUser = await GetOsuUserAsync(guildUser);
+
+ if (osuUser is null)
+ {
+ return;
+ }
+
+ var guildConfig = await (await db.GuildOptions.FindAsync(x => x.Id == guildUser.GuildId)).SingleAsync();
+
+ foreach (var osuRole in guildConfig.OsuRoles.Where(osuRole => osuRole.Group > 0))
+ {
+ var role = guildUser.Guild.GetRole(osuRole.RoleId);
+
+ if (osuUser.GetGroups().Contains(osuRole.Group))
+ {
+ if (role is not null && !guildUser.RoleIds.Contains(osuRole.RoleId))
+ {
+ await guildUser.AddRoleAsync(role);
+ }
+ }
+ else if (role is not null && guildUser.RoleIds.Contains(osuRole.RoleId))
+ {
+ await guildUser.RemoveRoleAsync(role);
+ }
+ }
+
+ await Task.WhenAll(
+ AssignRoleAsync(OsuUserGroup.UnrankedMapper, osuUser.IsUnrankedMapper()),
+ AssignRoleAsync(OsuUserGroup.RankedMapper, osuUser.IsRankedMapper()),
+ AssignRoleAsync(OsuUserGroup.ProjectLoved, osuUser.IsLovedMapper())
+ );
+
+ return;
+
+ async Task AssignRoleAsync(OsuUserGroup group, bool add)
+ {
+ var roleId = guildConfig?.OsuRoles.FirstOrDefault(osuRole => osuRole.Group == group).RoleId;
+
+ if (roleId is null)
+ {
+ return;
+ }
+
+ var role = guildUser.Guild.GetRole(roleId.Value);
+
+ if (role is not null)
+ {
+ await (add ? AddRoleAsync(role) : RemoveRoleAsync(role));
+ }
+ }
+
+ async Task AddRoleAsync(IRole role)
+ {
+ if (!guildUser.RoleIds.Contains(role.Id))
+ {
+ await guildUser.AddRoleAsync(role);
+ }
+ }
+
+ async Task RemoveRoleAsync(IRole role)
+ {
+ if (guildUser.RoleIds.Contains(role.Id))
+ {
+ await guildUser.RemoveRoleAsync(role);
+ }
+ }
+ }
+
+ private async Task GetOsuUserAsync(IUser discordUser)
+ {
+ var user = await db.GetUserAsync(discordUser.Id);
+
+ if (user.OsuId is null)
+ {
+ return null;
+ }
+
+ return await osu.GetUserAsync(user.OsuId.Value.ToString());
+ }
+}
diff --git a/Numerous/Services/Startup.cs b/Numerous/Services/Startup.cs
index 6c02e1b..09af50d 100644
--- a/Numerous/Services/Startup.cs
+++ b/Numerous/Services/Startup.cs
@@ -20,7 +20,8 @@ public sealed class Startup(
DiscordSocketClient discordClient,
ConfigManager cfgManager,
DbManager dbManager,
- ReminderService reminderService
+ ReminderService reminderService,
+ OsuVerifier verifier
) : IHostedService
{
private Config Cfg => cfgManager.Get();
@@ -46,6 +47,7 @@ await dbManager.GuildOptions.InsertOneAsync(new GuildOptions
}
await reminderService.StartAsync(cancellationToken);
+ await verifier.StartAsync();
}
public async Task StopAsync(CancellationToken cancellationToken)