From 83db945d4bd66c9384c47f9518dc25d966631a25 Mon Sep 17 00:00:00 2001 From: Pascal Pomper Date: Fri, 6 Dec 2024 22:09:52 +0100 Subject: [PATCH 1/4] Use .env file for `DesignTimeNumerousDbContextFactory` --- .gitignore | 2 -- ...plate => DesignTimeNumerousDbContextFactory.cs} | 14 ++++++++++---- 2 files changed, 10 insertions(+), 6 deletions(-) rename src/Numerous.Database/Context/{DesignTimeNumerousDbContextFactory.cs.template => DesignTimeNumerousDbContextFactory.cs} (77%) diff --git a/.gitignore b/.gitignore index aa6c539..ea16193 100644 --- a/.gitignore +++ b/.gitignore @@ -10,5 +10,3 @@ logs/ *.conf *.json .env - -DesignTimeNumerousDbContextFactory.cs diff --git a/src/Numerous.Database/Context/DesignTimeNumerousDbContextFactory.cs.template b/src/Numerous.Database/Context/DesignTimeNumerousDbContextFactory.cs similarity index 77% rename from src/Numerous.Database/Context/DesignTimeNumerousDbContextFactory.cs.template rename to src/Numerous.Database/Context/DesignTimeNumerousDbContextFactory.cs index a0ed22a..dd9a894 100644 --- a/src/Numerous.Database/Context/DesignTimeNumerousDbContextFactory.cs.template +++ b/src/Numerous.Database/Context/DesignTimeNumerousDbContextFactory.cs @@ -3,6 +3,7 @@ // 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 DotNetEnv; using JetBrains.Annotations; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Design; @@ -12,13 +13,18 @@ namespace Numerous.Database.Context; [UsedImplicitly] public sealed class DesignTimeNumerousDbContextFactory : IDesignTimeDbContextFactory { - private const string ConnectionString = - "Host=YOUR_HOST;Username=YOUR_USERNAME;Password=YOUR_PASSWORD;Database=numerous"; - public NumerousDbContext CreateDbContext(string[] args) { + Env.TraversePath().Load(".env"); + + var connectionString = + $"Host=localhost;" + + $"Username={Env.GetString("POSTGRES_USER")};" + + $"Password={Env.GetString("POSTGRES_PASSWORD")};" + + $"Database={Env.GetString("POSTGRES_DB")}"; + var optionsBuilder = new DbContextOptionsBuilder() - .UseNpgsql(ConnectionString); + .UseNpgsql(connectionString); return new NumerousDbContext(optionsBuilder.Options); } From f09d278afeb3e3c2ec118736ed181c3eaf7ef520 Mon Sep 17 00:00:00 2001 From: Pascal Pomper Date: Fri, 6 Dec 2024 23:45:42 +0100 Subject: [PATCH 2/4] Record guild member count --- src/Numerous.Bot/BotServiceConfiguration.cs | 4 +- .../Discord/Events/DiscordEventHandler.cs | 2 + .../Discord/Events/EventHandler.Logger.cs | 1 + .../Events/EventHandler.MessageCommands.cs | 1 + .../Discord/Events/MessageResponder.Ban.cs | 1 + .../Discord/Events/MessageResponder.cs | 2 +- .../Discord/Events/MudaeMessageHandler.cs | 2 + .../AdminCommandModule.Competition.End.cs | 1 + .../Config/ConfigCommandModule.Mapfeed.cs | 1 + .../Config/ConfigCommandModule.Role.cs | 1 + .../Commands/ReminderCommandModule.cs | 2 + .../Commands/SuperDeleteCommandModule.cs | 1 + .../Commands/UnDeleteCommandModule.cs | 1 + .../Commands/UnEditCommandModule.cs | 1 + .../Commands/VerifyCommandModule.cs | 1 + .../Interactions/InteractionHandler.cs | 2 + .../Attachments}/AttachmentService.cs | 2 +- .../Attachments}/FileAttachmentInfo.cs | 2 +- .../Discord/Services/GuildStatsService.cs | 38 + .../Discord/{ => Services}/OsuVerifier.cs | 6 +- .../Discord/{ => Services}/ReminderService.cs | 4 +- .../Discord/{ => Util}/Constants.cs | 2 +- .../Discord/{ => Util}/DiscordExtensions.cs | 2 +- .../Exclusive/StarReactPreventionService.cs | 1 + src/Numerous.Bot/Osu/MapFeedService.cs | 1 + src/Numerous.Bot/Services/Startup.cs | 12 +- .../{ => Services}/HostedService.cs | 2 +- src/Numerous.Database/Context/UnitOfWork.cs | 2 + src/Numerous.Database/DbMapperProfile.cs | 3 + .../Dtos/GuildStatsEntryDto.cs | 16 + src/Numerous.Database/Entities/DbGuild.cs | 1 + .../Entities/DbGuildStatsEntry.cs | 22 + .../20241206220252_AddGuildStats.Designer.cs | 910 ++++++++++++++++++ .../20241206220252_AddGuildStats.cs | 46 + .../NumerousDbContextModelSnapshot.cs | 37 + .../Repositories/GuildStatsEntryRepository.cs | 27 + src/Numerous.Tests/AttachmentServiceTests.cs | 1 + .../Controllers/AuthController.cs | 1 + 38 files changed, 1144 insertions(+), 18 deletions(-) rename src/Numerous.Bot/Discord/{ => Services/Attachments}/AttachmentService.cs (97%) rename src/Numerous.Bot/Discord/{ => Services/Attachments}/FileAttachmentInfo.cs (94%) create mode 100644 src/Numerous.Bot/Discord/Services/GuildStatsService.cs rename src/Numerous.Bot/Discord/{ => Services}/OsuVerifier.cs (98%) rename src/Numerous.Bot/Discord/{ => Services}/ReminderService.cs (98%) rename src/Numerous.Bot/Discord/{ => Util}/Constants.cs (95%) rename src/Numerous.Bot/Discord/{ => Util}/DiscordExtensions.cs (98%) rename src/Numerous.Common/{ => Services}/HostedService.cs (96%) create mode 100644 src/Numerous.Database/Dtos/GuildStatsEntryDto.cs create mode 100644 src/Numerous.Database/Entities/DbGuildStatsEntry.cs create mode 100644 src/Numerous.Database/Migrations/20241206220252_AddGuildStats.Designer.cs create mode 100644 src/Numerous.Database/Migrations/20241206220252_AddGuildStats.cs create mode 100644 src/Numerous.Database/Repositories/GuildStatsEntryRepository.cs diff --git a/src/Numerous.Bot/BotServiceConfiguration.cs b/src/Numerous.Bot/BotServiceConfiguration.cs index 4de8dcf..b8c432c 100644 --- a/src/Numerous.Bot/BotServiceConfiguration.cs +++ b/src/Numerous.Bot/BotServiceConfiguration.cs @@ -4,9 +4,10 @@ // You should have received a copy of the GNU General Public License along with this program. If not, see . using Microsoft.Extensions.DependencyInjection; -using Numerous.Bot.Discord; using Numerous.Bot.Discord.Events; using Numerous.Bot.Discord.Interactions; +using Numerous.Bot.Discord.Services; +using Numerous.Bot.Discord.Services.Attachments; using Numerous.Bot.Discord.Util; using Numerous.Bot.Exclusive; using Numerous.Bot.Osu; @@ -28,6 +29,7 @@ public static void Configure(IServiceCollection services) services.AddSingleton(); services.AddTransient(); services.AddSingleton(); + services.AddTransient(); services.AddHostedService(); services.AddHostedService(); services.AddHostedService(); diff --git a/src/Numerous.Bot/Discord/Events/DiscordEventHandler.cs b/src/Numerous.Bot/Discord/Events/DiscordEventHandler.cs index debc33d..4abcf03 100644 --- a/src/Numerous.Bot/Discord/Events/DiscordEventHandler.cs +++ b/src/Numerous.Bot/Discord/Events/DiscordEventHandler.cs @@ -4,6 +4,8 @@ // You should have received a copy of the GNU General Public License along with this program. If not, see . using Discord.WebSocket; +using Numerous.Bot.Discord.Services; +using Numerous.Bot.Discord.Services.Attachments; using Numerous.Bot.Util; using Numerous.Bot.Web.Osu; using Numerous.Common.Config; diff --git a/src/Numerous.Bot/Discord/Events/EventHandler.Logger.cs b/src/Numerous.Bot/Discord/Events/EventHandler.Logger.cs index ae2324e..7242f94 100644 --- a/src/Numerous.Bot/Discord/Events/EventHandler.Logger.cs +++ b/src/Numerous.Bot/Discord/Events/EventHandler.Logger.cs @@ -4,6 +4,7 @@ // You should have received a copy of the GNU General Public License along with this program. If not, see . using Discord.WebSocket; +using Numerous.Bot.Discord.Util; using Numerous.Bot.Util; using Serilog; diff --git a/src/Numerous.Bot/Discord/Events/EventHandler.MessageCommands.cs b/src/Numerous.Bot/Discord/Events/EventHandler.MessageCommands.cs index d53d293..a264623 100644 --- a/src/Numerous.Bot/Discord/Events/EventHandler.MessageCommands.cs +++ b/src/Numerous.Bot/Discord/Events/EventHandler.MessageCommands.cs @@ -5,6 +5,7 @@ using System.Net; using Discord; +using Numerous.Bot.Discord.Util; using Numerous.Bot.Util; using Refit; diff --git a/src/Numerous.Bot/Discord/Events/MessageResponder.Ban.cs b/src/Numerous.Bot/Discord/Events/MessageResponder.Ban.cs index 3bf2674..375cc71 100644 --- a/src/Numerous.Bot/Discord/Events/MessageResponder.Ban.cs +++ b/src/Numerous.Bot/Discord/Events/MessageResponder.Ban.cs @@ -7,6 +7,7 @@ using Discord; using Discord.Net; using Discord.WebSocket; +using Numerous.Bot.Discord.Util; using Numerous.Common.Util; namespace Numerous.Bot.Discord.Events; diff --git a/src/Numerous.Bot/Discord/Events/MessageResponder.cs b/src/Numerous.Bot/Discord/Events/MessageResponder.cs index e8042f8..092b77b 100644 --- a/src/Numerous.Bot/Discord/Events/MessageResponder.cs +++ b/src/Numerous.Bot/Discord/Events/MessageResponder.cs @@ -4,7 +4,7 @@ // You should have received a copy of the GNU General Public License along with this program. If not, see . using Discord.WebSocket; -using Numerous.Common; +using Numerous.Common.Services; namespace Numerous.Bot.Discord.Events; diff --git a/src/Numerous.Bot/Discord/Events/MudaeMessageHandler.cs b/src/Numerous.Bot/Discord/Events/MudaeMessageHandler.cs index f29a818..f2beed4 100644 --- a/src/Numerous.Bot/Discord/Events/MudaeMessageHandler.cs +++ b/src/Numerous.Bot/Discord/Events/MudaeMessageHandler.cs @@ -5,7 +5,9 @@ using Discord; using Discord.WebSocket; +using Numerous.Bot.Discord.Util; using Numerous.Common; +using Numerous.Common.Services; namespace Numerous.Bot.Discord.Events; diff --git a/src/Numerous.Bot/Discord/Interactions/Commands/Admin/AdminCommandModule.Competition.End.cs b/src/Numerous.Bot/Discord/Interactions/Commands/Admin/AdminCommandModule.Competition.End.cs index 6577369..6edf82c 100644 --- a/src/Numerous.Bot/Discord/Interactions/Commands/Admin/AdminCommandModule.Competition.End.cs +++ b/src/Numerous.Bot/Discord/Interactions/Commands/Admin/AdminCommandModule.Competition.End.cs @@ -6,6 +6,7 @@ using Discord; using Discord.Interactions; using JetBrains.Annotations; +using Numerous.Bot.Discord.Util; using Numerous.Common.Util; namespace Numerous.Bot.Discord.Interactions.Commands.Admin; diff --git a/src/Numerous.Bot/Discord/Interactions/Commands/Config/ConfigCommandModule.Mapfeed.cs b/src/Numerous.Bot/Discord/Interactions/Commands/Config/ConfigCommandModule.Mapfeed.cs index 0c9751e..4217791 100644 --- a/src/Numerous.Bot/Discord/Interactions/Commands/Config/ConfigCommandModule.Mapfeed.cs +++ b/src/Numerous.Bot/Discord/Interactions/Commands/Config/ConfigCommandModule.Mapfeed.cs @@ -6,6 +6,7 @@ using Discord; using Discord.Interactions; using JetBrains.Annotations; +using Numerous.Bot.Discord.Util; using Numerous.Database.Context; namespace Numerous.Bot.Discord.Interactions.Commands.Config; diff --git a/src/Numerous.Bot/Discord/Interactions/Commands/Config/ConfigCommandModule.Role.cs b/src/Numerous.Bot/Discord/Interactions/Commands/Config/ConfigCommandModule.Role.cs index b733d91..01c4092 100644 --- a/src/Numerous.Bot/Discord/Interactions/Commands/Config/ConfigCommandModule.Role.cs +++ b/src/Numerous.Bot/Discord/Interactions/Commands/Config/ConfigCommandModule.Role.cs @@ -6,6 +6,7 @@ using Discord; using Discord.Interactions; using JetBrains.Annotations; +using Numerous.Bot.Discord.Services; using Numerous.Common.Enums; namespace Numerous.Bot.Discord.Interactions.Commands.Config; diff --git a/src/Numerous.Bot/Discord/Interactions/Commands/ReminderCommandModule.cs b/src/Numerous.Bot/Discord/Interactions/Commands/ReminderCommandModule.cs index 01508a9..cc9b319 100644 --- a/src/Numerous.Bot/Discord/Interactions/Commands/ReminderCommandModule.cs +++ b/src/Numerous.Bot/Discord/Interactions/Commands/ReminderCommandModule.cs @@ -6,6 +6,8 @@ using Discord; using Discord.Interactions; using JetBrains.Annotations; +using Numerous.Bot.Discord.Services; +using Numerous.Bot.Discord.Util; using Numerous.Bot.Util; using Numerous.Database.Context; using Numerous.Database.Dtos; diff --git a/src/Numerous.Bot/Discord/Interactions/Commands/SuperDeleteCommandModule.cs b/src/Numerous.Bot/Discord/Interactions/Commands/SuperDeleteCommandModule.cs index 99aa54f..3eae5a1 100644 --- a/src/Numerous.Bot/Discord/Interactions/Commands/SuperDeleteCommandModule.cs +++ b/src/Numerous.Bot/Discord/Interactions/Commands/SuperDeleteCommandModule.cs @@ -6,6 +6,7 @@ using Discord; using Discord.Interactions; using JetBrains.Annotations; +using Numerous.Bot.Discord.Util; using Numerous.Database.Context; namespace Numerous.Bot.Discord.Interactions.Commands; diff --git a/src/Numerous.Bot/Discord/Interactions/Commands/UnDeleteCommandModule.cs b/src/Numerous.Bot/Discord/Interactions/Commands/UnDeleteCommandModule.cs index 399ec09..5f86320 100644 --- a/src/Numerous.Bot/Discord/Interactions/Commands/UnDeleteCommandModule.cs +++ b/src/Numerous.Bot/Discord/Interactions/Commands/UnDeleteCommandModule.cs @@ -6,6 +6,7 @@ using Discord; using Discord.Interactions; using JetBrains.Annotations; +using Numerous.Bot.Discord.Services.Attachments; using Numerous.Bot.Util; using Numerous.Common.Util; using Numerous.Database.Context; diff --git a/src/Numerous.Bot/Discord/Interactions/Commands/UnEditCommandModule.cs b/src/Numerous.Bot/Discord/Interactions/Commands/UnEditCommandModule.cs index b293d21..147494d 100644 --- a/src/Numerous.Bot/Discord/Interactions/Commands/UnEditCommandModule.cs +++ b/src/Numerous.Bot/Discord/Interactions/Commands/UnEditCommandModule.cs @@ -7,6 +7,7 @@ using Discord.Interactions; using Discord.WebSocket; using JetBrains.Annotations; +using Numerous.Bot.Discord.Util; using Numerous.Bot.Util; using Numerous.Common.Util; using Numerous.Database.Context; diff --git a/src/Numerous.Bot/Discord/Interactions/Commands/VerifyCommandModule.cs b/src/Numerous.Bot/Discord/Interactions/Commands/VerifyCommandModule.cs index e80baa2..0670940 100644 --- a/src/Numerous.Bot/Discord/Interactions/Commands/VerifyCommandModule.cs +++ b/src/Numerous.Bot/Discord/Interactions/Commands/VerifyCommandModule.cs @@ -6,6 +6,7 @@ using Discord; using Discord.Interactions; using JetBrains.Annotations; +using Numerous.Bot.Discord.Services; using Numerous.Common.Config; using Numerous.Common.Enums; using Numerous.Database.Context; diff --git a/src/Numerous.Bot/Discord/Interactions/InteractionHandler.cs b/src/Numerous.Bot/Discord/Interactions/InteractionHandler.cs index e4b85a1..9a64f56 100644 --- a/src/Numerous.Bot/Discord/Interactions/InteractionHandler.cs +++ b/src/Numerous.Bot/Discord/Interactions/InteractionHandler.cs @@ -7,8 +7,10 @@ using Discord; using Discord.Interactions; using Discord.WebSocket; +using Numerous.Bot.Discord.Util; using Numerous.Common; using Numerous.Common.Config; +using Numerous.Common.Services; using Numerous.Common.Util; using Serilog; diff --git a/src/Numerous.Bot/Discord/AttachmentService.cs b/src/Numerous.Bot/Discord/Services/Attachments/AttachmentService.cs similarity index 97% rename from src/Numerous.Bot/Discord/AttachmentService.cs rename to src/Numerous.Bot/Discord/Services/Attachments/AttachmentService.cs index 5472e2e..b73d170 100644 --- a/src/Numerous.Bot/Discord/AttachmentService.cs +++ b/src/Numerous.Bot/Discord/Services/Attachments/AttachmentService.cs @@ -7,7 +7,7 @@ using Numerous.Bot.Services; using Numerous.Common.Config; -namespace Numerous.Bot.Discord; +namespace Numerous.Bot.Discord.Services.Attachments; public sealed class AttachmentService(IConfigProvider cfgProvider, IFileService files) { diff --git a/src/Numerous.Bot/Discord/FileAttachmentInfo.cs b/src/Numerous.Bot/Discord/Services/Attachments/FileAttachmentInfo.cs similarity index 94% rename from src/Numerous.Bot/Discord/FileAttachmentInfo.cs rename to src/Numerous.Bot/Discord/Services/Attachments/FileAttachmentInfo.cs index cfc0dde..7005df0 100644 --- a/src/Numerous.Bot/Discord/FileAttachmentInfo.cs +++ b/src/Numerous.Bot/Discord/Services/Attachments/FileAttachmentInfo.cs @@ -5,7 +5,7 @@ using Discord; -namespace Numerous.Bot.Discord; +namespace Numerous.Bot.Discord.Services.Attachments; public readonly record struct FileAttachmentInfo(string Path, string FileName) { diff --git a/src/Numerous.Bot/Discord/Services/GuildStatsService.cs b/src/Numerous.Bot/Discord/Services/GuildStatsService.cs new file mode 100644 index 0000000..7e88280 --- /dev/null +++ b/src/Numerous.Bot/Discord/Services/GuildStatsService.cs @@ -0,0 +1,38 @@ +// 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.WebSocket; +using Numerous.Database.Context; + +namespace Numerous.Bot.Discord.Services; + +public sealed class GuildStatsService( + DiscordSocketClient client, + IUnitOfWorkFactory uowFactory +) +{ + public void Start() + { + client.UserJoined += async gu => await UpdateStatsAsync(gu.Guild); + client.UserLeft += async (g, _) => await UpdateStatsAsync(g); + } + + private async Task UpdateStatsAsync(SocketGuild guild) + { + var now = DateTimeOffset.UtcNow; + await guild.DownloadUsersAsync(); + + await using var uow = uowFactory.Create(); + + await uow.GuildStats.InsertAsync(new() + { + GuildId = guild.Id, + Timestamp = now, + MemberCount = guild.DownloadedMemberCount, + }); + + await uow.CommitAsync(); + } +} diff --git a/src/Numerous.Bot/Discord/OsuVerifier.cs b/src/Numerous.Bot/Discord/Services/OsuVerifier.cs similarity index 98% rename from src/Numerous.Bot/Discord/OsuVerifier.cs rename to src/Numerous.Bot/Discord/Services/OsuVerifier.cs index b1e4b26..5e2e24d 100644 --- a/src/Numerous.Bot/Discord/OsuVerifier.cs +++ b/src/Numerous.Bot/Discord/Services/OsuVerifier.cs @@ -15,7 +15,7 @@ using osu.Game.Beatmaps; using Serilog; -namespace Numerous.Bot.Discord; +namespace Numerous.Bot.Discord.Services; public sealed class OsuVerifier( IHost host, @@ -24,7 +24,7 @@ public sealed class OsuVerifier( IOsuApiRepository osuApi ) { - public Task StartAsync(CancellationToken ct) + public void Start(CancellationToken ct) { host.Services.UseScheduler(scheduler => scheduler.ScheduleAsync(async () => await AssignAllRolesAsync(ct)) .EveryMinute() @@ -34,8 +34,6 @@ public Task StartAsync(CancellationToken ct) await GetOsuUsersAsync(user, ct), await GetGroupRoleMappingsAsync(user.Guild, ct) ); - - return Task.CompletedTask; } public async Task AssignAllRolesAsync(SocketGuild guild, CancellationToken ct = default) diff --git a/src/Numerous.Bot/Discord/ReminderService.cs b/src/Numerous.Bot/Discord/Services/ReminderService.cs similarity index 98% rename from src/Numerous.Bot/Discord/ReminderService.cs rename to src/Numerous.Bot/Discord/Services/ReminderService.cs index 435a329..14cc52e 100644 --- a/src/Numerous.Bot/Discord/ReminderService.cs +++ b/src/Numerous.Bot/Discord/Services/ReminderService.cs @@ -12,7 +12,7 @@ using Numerous.Database.Dtos; using Timer = System.Timers.Timer; -namespace Numerous.Bot.Discord; +namespace Numerous.Bot.Discord.Services; public sealed class ReminderService(IHost host, IUnitOfWorkFactory uowFactory, DiscordSocketClient client) { @@ -20,7 +20,7 @@ public sealed class ReminderService(IHost host, IUnitOfWorkFactory uowFactory, D private readonly Dictionary _timerCache = new(); - public void StartAsync(CancellationToken ct) + public void Start(CancellationToken ct) { host.Services.UseScheduler(s => s.ScheduleAsync(() => CacheRemindersAsync(ct)).Hourly().RunOnceAtStart().PreventOverlapping(nameof(ReminderService)) diff --git a/src/Numerous.Bot/Discord/Constants.cs b/src/Numerous.Bot/Discord/Util/Constants.cs similarity index 95% rename from src/Numerous.Bot/Discord/Constants.cs rename to src/Numerous.Bot/Discord/Util/Constants.cs index 964fa65..7edb288 100644 --- a/src/Numerous.Bot/Discord/Constants.cs +++ b/src/Numerous.Bot/Discord/Util/Constants.cs @@ -3,7 +3,7 @@ // 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 . -namespace Numerous.Bot.Discord; +namespace Numerous.Bot.Discord.Util; internal static class Constants { diff --git a/src/Numerous.Bot/Discord/DiscordExtensions.cs b/src/Numerous.Bot/Discord/Util/DiscordExtensions.cs similarity index 98% rename from src/Numerous.Bot/Discord/DiscordExtensions.cs rename to src/Numerous.Bot/Discord/Util/DiscordExtensions.cs index 01c1d63..3665725 100644 --- a/src/Numerous.Bot/Discord/DiscordExtensions.cs +++ b/src/Numerous.Bot/Discord/Util/DiscordExtensions.cs @@ -8,7 +8,7 @@ using Discord; using Discord.WebSocket; -namespace Numerous.Bot.Discord; +namespace Numerous.Bot.Discord.Util; public static partial class DiscordExtensions { diff --git a/src/Numerous.Bot/Exclusive/StarReactPreventionService.cs b/src/Numerous.Bot/Exclusive/StarReactPreventionService.cs index 106a6f9..9f303c8 100644 --- a/src/Numerous.Bot/Exclusive/StarReactPreventionService.cs +++ b/src/Numerous.Bot/Exclusive/StarReactPreventionService.cs @@ -7,6 +7,7 @@ using Discord.WebSocket; using Numerous.Common; using Numerous.Common.Config; +using Numerous.Common.Services; namespace Numerous.Bot.Exclusive; diff --git a/src/Numerous.Bot/Osu/MapFeedService.cs b/src/Numerous.Bot/Osu/MapFeedService.cs index 7755ada..9b54971 100644 --- a/src/Numerous.Bot/Osu/MapFeedService.cs +++ b/src/Numerous.Bot/Osu/MapFeedService.cs @@ -11,6 +11,7 @@ using Numerous.Bot.Util; using Numerous.Bot.Web.Osu; using Numerous.Common; +using Numerous.Common.Services; using Numerous.Common.Util; using Numerous.Database.Context; using osu.Game.Beatmaps; diff --git a/src/Numerous.Bot/Services/Startup.cs b/src/Numerous.Bot/Services/Startup.cs index 0bfd56b..4b9be62 100644 --- a/src/Numerous.Bot/Services/Startup.cs +++ b/src/Numerous.Bot/Services/Startup.cs @@ -5,10 +5,10 @@ using Discord; using Discord.WebSocket; -using Numerous.Bot.Discord; using Numerous.Bot.Discord.Events; -using Numerous.Common; +using Numerous.Bot.Discord.Services; using Numerous.Common.Config; +using Numerous.Common.Services; using Numerous.Database.Context; namespace Numerous.Bot.Services; @@ -19,7 +19,8 @@ public sealed class Startup( IUnitOfWorkFactory uowFactory, ReminderService reminderService, OsuVerifier verifier, - DiscordEventHandler eventHandler + DiscordEventHandler eventHandler, + GuildStatsService guildStatsService ) : HostedService { private Config Cfg => cfgProvider.Get(); @@ -46,9 +47,10 @@ await uow.Guilds.InsertAsync(new() await uow.CommitAsync(ct); - reminderService.StartAsync(ct); - await verifier.StartAsync(ct); + reminderService.Start(ct); + verifier.Start(ct); eventHandler.Start(); + guildStatsService.Start(); } public override async Task StopAsync(CancellationToken cancellationToken) diff --git a/src/Numerous.Common/HostedService.cs b/src/Numerous.Common/Services/HostedService.cs similarity index 96% rename from src/Numerous.Common/HostedService.cs rename to src/Numerous.Common/Services/HostedService.cs index ea13252..0fa30ad 100644 --- a/src/Numerous.Common/HostedService.cs +++ b/src/Numerous.Common/Services/HostedService.cs @@ -5,7 +5,7 @@ using Microsoft.Extensions.Hosting; -namespace Numerous.Common; +namespace Numerous.Common.Services; public abstract class HostedService : IHostedService { diff --git a/src/Numerous.Database/Context/UnitOfWork.cs b/src/Numerous.Database/Context/UnitOfWork.cs index 29ebcfe..a09adfc 100644 --- a/src/Numerous.Database/Context/UnitOfWork.cs +++ b/src/Numerous.Database/Context/UnitOfWork.cs @@ -21,6 +21,7 @@ public interface IUnitOfWork : IDisposable, IAsyncDisposable IDiscordUserRepository DiscordUsers { get; } IGroupRoleMappingRepository GroupRoleMappings { get; } IGuildRepository Guilds { get; } + IGuildStatsEntryRepository GuildStats { get; } IIdRepository LocalBeatmaps { get; } IIdRepository JoinMessages { get; } IOnlineBeatmapRepository OnlineBeatmaps { get; } @@ -45,6 +46,7 @@ public sealed class UnitOfWork(IDbContextFactory contextProvi public IDiscordUserRepository DiscordUsers => new DiscordUserRepository(_context, mapper); public IGroupRoleMappingRepository GroupRoleMappings => new GroupRoleMappingRepository(_context, mapper); public IGuildRepository Guilds => new GuildRepository(_context, mapper); + public IGuildStatsEntryRepository GuildStats => new GuildStatsEntryRepository(_context, mapper); public IIdRepository LocalBeatmaps => new IdRepository(_context, mapper); public IIdRepository JoinMessages => new IdRepository(_context, mapper); public IOnlineBeatmapRepository OnlineBeatmaps => new OnlineBeatmapRepository(_context, mapper); diff --git a/src/Numerous.Database/DbMapperProfile.cs b/src/Numerous.Database/DbMapperProfile.cs index d04c3b9..147dc4a 100644 --- a/src/Numerous.Database/DbMapperProfile.cs +++ b/src/Numerous.Database/DbMapperProfile.cs @@ -63,6 +63,9 @@ public DbMapperProfile() CreateMap(); CreateMap(); + CreateMap(); + CreateMap(); + CreateMap(); CreateMap(); diff --git a/src/Numerous.Database/Dtos/GuildStatsEntryDto.cs b/src/Numerous.Database/Dtos/GuildStatsEntryDto.cs new file mode 100644 index 0000000..9afab1a --- /dev/null +++ b/src/Numerous.Database/Dtos/GuildStatsEntryDto.cs @@ -0,0 +1,16 @@ +// 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 . + +namespace Numerous.Database.Dtos; + +public class GuildStatsEntryDto +{ + public GuildDto Guild { get; set; } = null!; + public required ulong GuildId { get; set; } + + public required DateTimeOffset Timestamp { get; set; } + + public required int MemberCount { get; set; } +} diff --git a/src/Numerous.Database/Entities/DbGuild.cs b/src/Numerous.Database/Entities/DbGuild.cs index d4be001..12023ca 100644 --- a/src/Numerous.Database/Entities/DbGuild.cs +++ b/src/Numerous.Database/Entities/DbGuild.cs @@ -31,4 +31,5 @@ public sealed class DbGuild : DbEntity public ICollection Channels { get; set; } = []; public ICollection GroupRoleMappings { get; set; } = []; public ICollection BeatmapCompetitions { get; set; } = []; + public ICollection Stats { get; set; } = []; } diff --git a/src/Numerous.Database/Entities/DbGuildStatsEntry.cs b/src/Numerous.Database/Entities/DbGuildStatsEntry.cs new file mode 100644 index 0000000..6fff9b6 --- /dev/null +++ b/src/Numerous.Database/Entities/DbGuildStatsEntry.cs @@ -0,0 +1,22 @@ +// 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 System.ComponentModel.DataAnnotations.Schema; +using Microsoft.EntityFrameworkCore; + +namespace Numerous.Database.Entities; + +[Table("guild_stats_entry")] +[PrimaryKey(nameof(GuildId), nameof(Timestamp))] +[Index(nameof(GuildId))] +public sealed class DbGuildStatsEntry +{ + public DbGuild Guild { get; set; } = null!; + public ulong GuildId { get; set; } + + public DateTimeOffset Timestamp { get; set; } + + public int MemberCount { get; set; } +} diff --git a/src/Numerous.Database/Migrations/20241206220252_AddGuildStats.Designer.cs b/src/Numerous.Database/Migrations/20241206220252_AddGuildStats.Designer.cs new file mode 100644 index 0000000..0c0bce5 --- /dev/null +++ b/src/Numerous.Database/Migrations/20241206220252_AddGuildStats.Designer.cs @@ -0,0 +1,910 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using Numerous.Database.Context; + +#nullable disable + +namespace Numerous.Database.Migrations +{ + [DbContext(typeof(NumerousDbContext))] + [Migration("20241206220252_AddGuildStats")] + partial class AddGuildStats + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.10") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseSerialColumns(modelBuilder); + + modelBuilder.Entity("Numerous.Database.Entities.DbAutoPingMapping", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseSerialColumn(b.Property("Id")); + + b.Property("ChannelId") + .HasColumnType("numeric(20,0)") + .HasColumnName("channel_id"); + + b.Property("RoleId") + .HasColumnType("numeric(20,0)") + .HasColumnName("role_id"); + + b.Property("TagId") + .HasColumnType("numeric(20,0)") + .HasColumnName("tag_id"); + + b.HasKey("Id") + .HasName("pk_auto_ping_mapping"); + + b.HasIndex("ChannelId", "TagId", "RoleId") + .IsUnique() + .HasDatabaseName("ix_auto_ping_mapping_channel_id_tag_id_role_id"); + + b.ToTable("auto_ping_mapping", (string)null); + }); + + modelBuilder.Entity("Numerous.Database.Entities.DbBeatmapCompetition", b => + { + b.Property("GuildId") + .HasColumnType("numeric(20,0)") + .HasColumnName("guild_id"); + + b.Property("StartTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("start_time"); + + b.Property("EndTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("end_time"); + + b.Property("LocalBeatmapId") + .HasColumnType("uuid") + .HasColumnName("local_beatmap_id"); + + b.HasKey("GuildId", "StartTime") + .HasName("pk_beatmap_competition"); + + b.HasIndex("LocalBeatmapId") + .HasDatabaseName("ix_beatmap_competition_local_beatmap_id"); + + b.ToTable("beatmap_competition", null, t => + { + t.HasCheckConstraint("CK_BeatmapCompetition_ValidTime", "\"StartTime\" < \"EndTime\""); + }); + }); + + modelBuilder.Entity("Numerous.Database.Entities.DbBeatmapCompetitionScore", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("Md5Hash"); + + b.Property("Accuracy") + .HasColumnType("double precision") + .HasColumnName("accuracy"); + + b.Property("DateTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("date_time"); + + b.Property("GreatCount") + .HasColumnType("bigint") + .HasColumnName("great_count"); + + b.Property("GuildId") + .HasColumnType("numeric(20,0)") + .HasColumnName("guild_id"); + + b.Property("MaxCombo") + .HasColumnType("bigint") + .HasColumnName("max_combo"); + + b.Property("MehCount") + .HasColumnType("bigint") + .HasColumnName("meh_count"); + + b.Property("MissCount") + .HasColumnType("bigint") + .HasColumnName("miss_count"); + + b.Property("Mods") + .IsRequired() + .HasColumnType("char(2)[]") + .HasColumnName("mods"); + + b.Property("OkCount") + .HasColumnType("bigint") + .HasColumnName("ok_count"); + + b.Property("OnlineId") + .HasColumnType("numeric(20,0)") + .HasColumnName("online_id"); + + b.Property("PlayerId") + .HasColumnType("bigint") + .HasColumnName("player_id"); + + b.Property("StartTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("start_time"); + + b.Property("TotalScore") + .HasColumnType("bigint") + .HasColumnName("total_score"); + + b.HasKey("Id") + .HasName("pk_beatmap_competition_score"); + + b.HasIndex("OnlineId") + .IsUnique() + .HasDatabaseName("ix_beatmap_competition_score_online_id"); + + b.HasIndex("PlayerId") + .HasDatabaseName("ix_beatmap_competition_score_player_id"); + + b.HasIndex("GuildId", "StartTime") + .HasDatabaseName("ix_beatmap_competition_score_guild_id_start_time"); + + b.ToTable("beatmap_competition_score", (string)null); + }); + + modelBuilder.Entity("Numerous.Database.Entities.DbChannel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("numeric(20,0)") + .HasColumnName("id"); + + b.Property("GuildId") + .HasColumnType("numeric(20,0)") + .HasColumnName("guild_id"); + + b.HasKey("Id"); + + b.HasIndex("GuildId") + .HasDatabaseName("ix_channel_guild_id"); + + b.ToTable("channel"); + + b.UseTptMappingStrategy(); + }); + + modelBuilder.Entity("Numerous.Database.Entities.DbDiscordMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("numeric(20,0)") + .HasColumnName("id"); + + b.Property("AuthorId") + .HasColumnType("numeric(20,0)") + .HasColumnName("author_id"); + + b.Property("ChannelId") + .HasColumnType("numeric(20,0)") + .HasColumnName("channel_id"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("IsHidden") + .HasColumnType("boolean") + .HasColumnName("is_hidden"); + + b.Property("ReferenceMessageId") + .HasColumnType("numeric(20,0)") + .HasColumnName("reference_message_id"); + + b.HasKey("Id") + .HasName("pk_discord_message"); + + b.HasIndex("AuthorId") + .HasDatabaseName("ix_discord_message_author_id"); + + b.HasIndex("ChannelId") + .HasDatabaseName("ix_discord_message_channel_id"); + + b.HasIndex("ReferenceMessageId") + .HasDatabaseName("ix_discord_message_reference_message_id"); + + b.ToTable("discord_message", (string)null); + }); + + modelBuilder.Entity("Numerous.Database.Entities.DbDiscordMessageVersion", b => + { + b.Property("MessageId") + .HasColumnType("numeric(20,0)") + .HasColumnName("message_id"); + + b.Property("Timestamp") + .HasColumnType("timestamp with time zone") + .HasColumnName("timestamp"); + + b.Property("CleanContent") + .HasMaxLength(4000) + .HasColumnType("character varying(4000)") + .HasColumnName("clean_content") + .HasComment("If NULL, the clean content is the same as the raw content."); + + b.Property("RawContent") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("character varying(4000)") + .HasColumnName("raw_content"); + + b.HasKey("MessageId", "Timestamp") + .HasName("pk_discord_message_version"); + + b.ToTable("discord_message_version", (string)null); + }); + + modelBuilder.Entity("Numerous.Database.Entities.DbDiscordUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("numeric(20,0)") + .HasColumnName("id"); + + b.Property("TimeZoneId") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("time_zone_id"); + + b.HasKey("Id") + .HasName("pk_discord_user"); + + b.ToTable("discord_user", (string)null); + }); + + modelBuilder.Entity("Numerous.Database.Entities.DbGroupRoleMapping", b => + { + b.Property("GuildId") + .HasColumnType("numeric(20,0)") + .HasColumnName("guild_id"); + + b.Property("RoleId") + .HasColumnType("numeric(20,0)") + .HasColumnName("role_id"); + + b.Property("Group") + .HasColumnType("smallint") + .HasColumnName("group_id"); + + b.HasKey("GuildId", "RoleId", "Group") + .HasName("pk_group_role_mapping"); + + b.ToTable("group_role_mapping", (string)null); + }); + + modelBuilder.Entity("Numerous.Database.Entities.DbGuild", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("numeric(20,0)") + .HasColumnName("id"); + + b.Property("GreetOnAdded") + .HasColumnType("boolean") + .HasColumnName("greet_on_added"); + + b.Property("MapfeedChannelId") + .HasColumnType("numeric(20,0)") + .HasColumnName("mapfeed_channel_id"); + + b.Property("TrackMessages") + .HasColumnType("boolean") + .HasColumnName("track_messages"); + + b.Property("UnverifiedRoleId") + .HasColumnType("numeric(20,0)") + .HasColumnName("unverified_role_id"); + + b.HasKey("Id") + .HasName("pk_guild"); + + b.HasIndex("MapfeedChannelId") + .IsUnique() + .HasDatabaseName("ix_guild_mapfeed_channel_id"); + + b.HasIndex("UnverifiedRoleId") + .IsUnique() + .HasDatabaseName("ix_guild_unverified_role_id"); + + b.ToTable("guild", (string)null); + }); + + modelBuilder.Entity("Numerous.Database.Entities.DbGuildStatsEntry", b => + { + b.Property("GuildId") + .HasColumnType("numeric(20,0)") + .HasColumnName("guild_id"); + + b.Property("Timestamp") + .HasColumnType("timestamp with time zone") + .HasColumnName("timestamp"); + + b.Property("MemberCount") + .HasColumnType("integer") + .HasColumnName("member_count"); + + b.HasKey("GuildId", "Timestamp") + .HasName("pk_guild_stats_entry"); + + b.HasIndex("GuildId") + .HasDatabaseName("ix_guild_stats_entry_guild_id"); + + b.ToTable("guild_stats_entry", (string)null); + }); + + modelBuilder.Entity("Numerous.Database.Entities.DbJoinMessage", b => + { + b.Property("GuildId") + .HasColumnType("numeric(20,0)") + .HasColumnName("guild_id"); + + b.Property("ChannelId") + .HasColumnType("numeric(20,0)") + .HasColumnName("channel_id"); + + b.Property("Description") + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("description"); + + b.Property("Title") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("title"); + + b.HasKey("GuildId") + .HasName("pk_join_message"); + + b.HasIndex("ChannelId") + .HasDatabaseName("ix_join_message_channel_id"); + + b.ToTable("join_message", null, t => + { + t.HasCheckConstraint("CK_JoinMessage_HasText", "\"Title\" IS NOT NULL OR \"Description\" IS NOT NULL"); + }); + }); + + modelBuilder.Entity("Numerous.Database.Entities.DbLocalBeatmap", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("md5_hash"); + + b.Property("MaxCombo") + .HasColumnType("bigint") + .HasColumnName("max_combo"); + + b.Property("OnlineBeatmapId") + .HasColumnType("bigint") + .HasColumnName("online_beatmap_id"); + + b.Property("OsuText") + .IsRequired() + .HasColumnType("text") + .HasColumnName("osu_text"); + + b.Property("OszHash") + .IsRequired() + .HasColumnType("bytea") + .HasColumnName("osz_hash"); + + b.HasKey("Id") + .HasName("pk_local_beatmap"); + + b.HasIndex("OnlineBeatmapId") + .HasDatabaseName("ix_local_beatmap_online_beatmap_id"); + + b.ToTable("local_beatmap", null, t => + { + t.HasCheckConstraint("CK_LocalBeatmap_ValidSha256", "length(\"OszHash\") = 256 / 8"); + }); + }); + + modelBuilder.Entity("Numerous.Database.Entities.DbOnlineBeatmap", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseSerialColumn(b.Property("Id")); + + b.Property("CreatorId") + .HasColumnType("bigint") + .HasColumnName("creator_id"); + + b.Property("OnlineBeatmapsetId") + .HasColumnType("bigint") + .HasColumnName("online_beatmapset_id"); + + b.HasKey("Id") + .HasName("pk_online_beatmap"); + + b.HasIndex("CreatorId") + .HasDatabaseName("ix_online_beatmap_creator_id"); + + b.HasIndex("OnlineBeatmapsetId") + .HasDatabaseName("ix_online_beatmap_online_beatmapset_id"); + + b.ToTable("online_beatmap", (string)null); + }); + + modelBuilder.Entity("Numerous.Database.Entities.DbOnlineBeatmapset", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseSerialColumn(b.Property("Id")); + + b.Property("CreatorId") + .HasColumnType("bigint") + .HasColumnName("creator_id"); + + b.HasKey("Id") + .HasName("pk_online_beatmapset"); + + b.HasIndex("CreatorId") + .HasDatabaseName("ix_online_beatmapset_creator_id"); + + b.ToTable("online_beatmapset", (string)null); + }); + + modelBuilder.Entity("Numerous.Database.Entities.DbOsuUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseSerialColumn(b.Property("Id")); + + b.Property("DiscordUserId") + .HasColumnType("numeric(20,0)") + .HasColumnName("discord_user_id"); + + b.HasKey("Id") + .HasName("pk_osu_user"); + + b.HasIndex("DiscordUserId") + .IsUnique() + .HasDatabaseName("ix_osu_user_discord_user_id"); + + b.ToTable("osu_user", (string)null); + }); + + modelBuilder.Entity("Numerous.Database.Entities.DbReminder", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseSerialColumn(b.Property("Id")); + + b.Property("ChannelId") + .HasColumnType("numeric(20,0)") + .HasColumnName("channel_id"); + + b.Property("Message") + .HasMaxLength(6000) + .HasColumnType("character varying(6000)") + .HasColumnName("message"); + + b.Property("Timestamp") + .HasColumnType("timestamp with time zone") + .HasColumnName("timestamp"); + + b.Property("UserId") + .HasColumnType("numeric(20,0)") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_reminder"); + + b.HasIndex("ChannelId") + .HasDatabaseName("ix_reminder_channel_id"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_reminder_user_id"); + + b.ToTable("reminder", (string)null); + }); + + modelBuilder.Entity("Numerous.Database.Entities.DbReplay", b => + { + b.Property("Id") + .HasColumnType("uuid") + .HasColumnName("md5_hash"); + + b.Property("Data") + .IsRequired() + .HasColumnType("bytea") + .HasColumnName("data"); + + b.HasKey("Id") + .HasName("pk_replay"); + + b.ToTable("replay", (string)null); + }); + + modelBuilder.Entity("Numerous.Database.Entities.DbForumChannel", b => + { + b.HasBaseType("Numerous.Database.Entities.DbChannel"); + + b.ToTable("forum_channel"); + }); + + modelBuilder.Entity("Numerous.Database.Entities.DbMessageChannel", b => + { + b.HasBaseType("Numerous.Database.Entities.DbChannel"); + + b.Property("IsReadOnly") + .HasColumnType("boolean") + .HasColumnName("is_read_only"); + + b.ToTable("message_channel"); + }); + + modelBuilder.Entity("Numerous.Database.Entities.DbAutoPingMapping", b => + { + b.HasOne("Numerous.Database.Entities.DbForumChannel", "Channel") + .WithMany("AutoPingMappings") + .HasForeignKey("ChannelId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_auto_ping_mapping_forum_channel_channel_id"); + + b.Navigation("Channel"); + }); + + modelBuilder.Entity("Numerous.Database.Entities.DbBeatmapCompetition", b => + { + b.HasOne("Numerous.Database.Entities.DbGuild", "Guild") + .WithMany("BeatmapCompetitions") + .HasForeignKey("GuildId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_beatmap_competition_guild_guild_id"); + + b.HasOne("Numerous.Database.Entities.DbLocalBeatmap", "LocalBeatmap") + .WithMany("BeatmapCompetitions") + .HasForeignKey("LocalBeatmapId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_beatmap_competition_local_beatmap_local_beatmap_id"); + + b.Navigation("Guild"); + + b.Navigation("LocalBeatmap"); + }); + + modelBuilder.Entity("Numerous.Database.Entities.DbBeatmapCompetitionScore", b => + { + b.HasOne("Numerous.Database.Entities.DbOsuUser", "Player") + .WithMany() + .HasForeignKey("PlayerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_beatmap_competition_score_osu_user_player_id"); + + b.HasOne("Numerous.Database.Entities.DbBeatmapCompetition", "Competition") + .WithMany("Scores") + .HasForeignKey("GuildId", "StartTime") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_beatmap_competition_score_beatmap_competition_guild_id_star"); + + b.Navigation("Competition"); + + b.Navigation("Player"); + }); + + modelBuilder.Entity("Numerous.Database.Entities.DbChannel", b => + { + b.HasOne("Numerous.Database.Entities.DbGuild", "Guild") + .WithMany("Channels") + .HasForeignKey("GuildId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_channel_guild_guild_id"); + + b.Navigation("Guild"); + }); + + modelBuilder.Entity("Numerous.Database.Entities.DbDiscordMessage", b => + { + b.HasOne("Numerous.Database.Entities.DbDiscordUser", "Author") + .WithMany("Messages") + .HasForeignKey("AuthorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_discord_message_discord_user_author_id"); + + b.HasOne("Numerous.Database.Entities.DbMessageChannel", "Channel") + .WithMany("Messages") + .HasForeignKey("ChannelId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_discord_message_message_channel_channel_id"); + + b.HasOne("Numerous.Database.Entities.DbDiscordMessage", "ReferenceMessage") + .WithMany("Replies") + .HasForeignKey("ReferenceMessageId") + .HasConstraintName("fk_discord_message_discord_message_reference_message_id"); + + b.Navigation("Author"); + + b.Navigation("Channel"); + + b.Navigation("ReferenceMessage"); + }); + + modelBuilder.Entity("Numerous.Database.Entities.DbDiscordMessageVersion", b => + { + b.HasOne("Numerous.Database.Entities.DbDiscordMessage", "Message") + .WithMany("Versions") + .HasForeignKey("MessageId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_discord_message_version_discord_message_message_id"); + + b.Navigation("Message"); + }); + + modelBuilder.Entity("Numerous.Database.Entities.DbGroupRoleMapping", b => + { + b.HasOne("Numerous.Database.Entities.DbGuild", "Guild") + .WithMany("GroupRoleMappings") + .HasForeignKey("GuildId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_role_mapping_guild_guild_id"); + + b.Navigation("Guild"); + }); + + modelBuilder.Entity("Numerous.Database.Entities.DbGuild", b => + { + b.HasOne("Numerous.Database.Entities.DbChannel", "MapfeedChannel") + .WithOne() + .HasForeignKey("Numerous.Database.Entities.DbGuild", "MapfeedChannelId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_guild_channel_mapfeed_channel_id"); + + b.Navigation("MapfeedChannel"); + }); + + modelBuilder.Entity("Numerous.Database.Entities.DbGuildStatsEntry", b => + { + b.HasOne("Numerous.Database.Entities.DbGuild", "Guild") + .WithMany("Stats") + .HasForeignKey("GuildId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_guild_stats_entry_guild_guild_id"); + + b.Navigation("Guild"); + }); + + modelBuilder.Entity("Numerous.Database.Entities.DbJoinMessage", b => + { + b.HasOne("Numerous.Database.Entities.DbMessageChannel", "Channel") + .WithMany() + .HasForeignKey("ChannelId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_join_message_message_channel_channel_id"); + + b.HasOne("Numerous.Database.Entities.DbGuild", "Guild") + .WithOne("JoinMessage") + .HasForeignKey("Numerous.Database.Entities.DbJoinMessage", "GuildId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_join_message_guild_guild_id"); + + b.Navigation("Channel"); + + b.Navigation("Guild"); + }); + + modelBuilder.Entity("Numerous.Database.Entities.DbLocalBeatmap", b => + { + b.HasOne("Numerous.Database.Entities.DbOnlineBeatmap", "OnlineBeatmap") + .WithMany("LocalBeatmaps") + .HasForeignKey("OnlineBeatmapId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_local_beatmap_online_beatmap_online_beatmap_id"); + + b.Navigation("OnlineBeatmap"); + }); + + modelBuilder.Entity("Numerous.Database.Entities.DbOnlineBeatmap", b => + { + b.HasOne("Numerous.Database.Entities.DbOsuUser", "Creator") + .WithMany("OnlineBeatmaps") + .HasForeignKey("CreatorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_online_beatmap_osu_user_creator_id"); + + b.HasOne("Numerous.Database.Entities.DbOnlineBeatmapset", "OnlineBeatmapset") + .WithMany("Beatmaps") + .HasForeignKey("OnlineBeatmapsetId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_online_beatmap_online_beatmapset_online_beatmapset_id"); + + b.Navigation("Creator"); + + b.Navigation("OnlineBeatmapset"); + }); + + modelBuilder.Entity("Numerous.Database.Entities.DbOnlineBeatmapset", b => + { + b.HasOne("Numerous.Database.Entities.DbOsuUser", "Creator") + .WithMany("OnlineBeatmapsets") + .HasForeignKey("CreatorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_online_beatmapset_osu_user_creator_id"); + + b.Navigation("Creator"); + }); + + modelBuilder.Entity("Numerous.Database.Entities.DbOsuUser", b => + { + b.HasOne("Numerous.Database.Entities.DbDiscordUser", "DiscordUser") + .WithOne("OsuUser") + .HasForeignKey("Numerous.Database.Entities.DbOsuUser", "DiscordUserId") + .HasConstraintName("fk_osu_user_discord_user_discord_user_id"); + + b.Navigation("DiscordUser"); + }); + + modelBuilder.Entity("Numerous.Database.Entities.DbReminder", b => + { + b.HasOne("Numerous.Database.Entities.DbMessageChannel", "Channel") + .WithMany() + .HasForeignKey("ChannelId") + .HasConstraintName("fk_reminder_message_channel_channel_id"); + + b.HasOne("Numerous.Database.Entities.DbDiscordUser", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_reminder_discord_user_user_id"); + + b.Navigation("Channel"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Numerous.Database.Entities.DbReplay", b => + { + b.HasOne("Numerous.Database.Entities.DbBeatmapCompetitionScore", "Score") + .WithOne("Replay") + .HasForeignKey("Numerous.Database.Entities.DbReplay", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_replay_beatmap_competition_score_md5_hash"); + + b.Navigation("Score"); + }); + + modelBuilder.Entity("Numerous.Database.Entities.DbForumChannel", b => + { + b.HasOne("Numerous.Database.Entities.DbChannel", null) + .WithOne() + .HasForeignKey("Numerous.Database.Entities.DbForumChannel", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_forum_channel_channel_id"); + }); + + modelBuilder.Entity("Numerous.Database.Entities.DbMessageChannel", b => + { + b.HasOne("Numerous.Database.Entities.DbChannel", null) + .WithOne() + .HasForeignKey("Numerous.Database.Entities.DbMessageChannel", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_message_channel_channel_id"); + }); + + modelBuilder.Entity("Numerous.Database.Entities.DbBeatmapCompetition", b => + { + b.Navigation("Scores"); + }); + + modelBuilder.Entity("Numerous.Database.Entities.DbBeatmapCompetitionScore", b => + { + b.Navigation("Replay"); + }); + + modelBuilder.Entity("Numerous.Database.Entities.DbDiscordMessage", b => + { + b.Navigation("Replies"); + + b.Navigation("Versions"); + }); + + modelBuilder.Entity("Numerous.Database.Entities.DbDiscordUser", b => + { + b.Navigation("Messages"); + + b.Navigation("OsuUser") + .IsRequired(); + }); + + modelBuilder.Entity("Numerous.Database.Entities.DbGuild", b => + { + b.Navigation("BeatmapCompetitions"); + + b.Navigation("Channels"); + + b.Navigation("GroupRoleMappings"); + + b.Navigation("JoinMessage"); + + b.Navigation("Stats"); + }); + + modelBuilder.Entity("Numerous.Database.Entities.DbLocalBeatmap", b => + { + b.Navigation("BeatmapCompetitions"); + }); + + modelBuilder.Entity("Numerous.Database.Entities.DbOnlineBeatmap", b => + { + b.Navigation("LocalBeatmaps"); + }); + + modelBuilder.Entity("Numerous.Database.Entities.DbOnlineBeatmapset", b => + { + b.Navigation("Beatmaps"); + }); + + modelBuilder.Entity("Numerous.Database.Entities.DbOsuUser", b => + { + b.Navigation("OnlineBeatmaps"); + + b.Navigation("OnlineBeatmapsets"); + }); + + modelBuilder.Entity("Numerous.Database.Entities.DbForumChannel", b => + { + b.Navigation("AutoPingMappings"); + }); + + modelBuilder.Entity("Numerous.Database.Entities.DbMessageChannel", b => + { + b.Navigation("Messages"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Numerous.Database/Migrations/20241206220252_AddGuildStats.cs b/src/Numerous.Database/Migrations/20241206220252_AddGuildStats.cs new file mode 100644 index 0000000..d51ebc8 --- /dev/null +++ b/src/Numerous.Database/Migrations/20241206220252_AddGuildStats.cs @@ -0,0 +1,46 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Numerous.Database.Migrations +{ + /// + public partial class AddGuildStats : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "guild_stats_entry", + columns: table => new + { + guild_id = table.Column(type: "numeric(20,0)", nullable: false), + timestamp = table.Column(type: "timestamp with time zone", nullable: false), + member_count = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_guild_stats_entry", x => new { x.guild_id, x.timestamp }); + table.ForeignKey( + name: "fk_guild_stats_entry_guild_guild_id", + column: x => x.guild_id, + principalTable: "guild", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "ix_guild_stats_entry_guild_id", + table: "guild_stats_entry", + column: "guild_id"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "guild_stats_entry"); + } + } +} diff --git a/src/Numerous.Database/Migrations/NumerousDbContextModelSnapshot.cs b/src/Numerous.Database/Migrations/NumerousDbContextModelSnapshot.cs index f1e4263..699af26 100644 --- a/src/Numerous.Database/Migrations/NumerousDbContextModelSnapshot.cs +++ b/src/Numerous.Database/Migrations/NumerousDbContextModelSnapshot.cs @@ -325,6 +325,29 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("guild", (string)null); }); + modelBuilder.Entity("Numerous.Database.Entities.DbGuildStatsEntry", b => + { + b.Property("GuildId") + .HasColumnType("numeric(20,0)") + .HasColumnName("guild_id"); + + b.Property("Timestamp") + .HasColumnType("timestamp with time zone") + .HasColumnName("timestamp"); + + b.Property("MemberCount") + .HasColumnType("integer") + .HasColumnName("member_count"); + + b.HasKey("GuildId", "Timestamp") + .HasName("pk_guild_stats_entry"); + + b.HasIndex("GuildId") + .HasDatabaseName("ix_guild_stats_entry_guild_id"); + + b.ToTable("guild_stats_entry", (string)null); + }); + modelBuilder.Entity("Numerous.Database.Entities.DbJoinMessage", b => { b.Property("GuildId") @@ -670,6 +693,18 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("MapfeedChannel"); }); + modelBuilder.Entity("Numerous.Database.Entities.DbGuildStatsEntry", b => + { + b.HasOne("Numerous.Database.Entities.DbGuild", "Guild") + .WithMany("Stats") + .HasForeignKey("GuildId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_guild_stats_entry_guild_guild_id"); + + b.Navigation("Guild"); + }); + modelBuilder.Entity("Numerous.Database.Entities.DbJoinMessage", b => { b.HasOne("Numerous.Database.Entities.DbMessageChannel", "Channel") @@ -831,6 +866,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("GroupRoleMappings"); b.Navigation("JoinMessage"); + + b.Navigation("Stats"); }); modelBuilder.Entity("Numerous.Database.Entities.DbLocalBeatmap", b => diff --git a/src/Numerous.Database/Repositories/GuildStatsEntryRepository.cs b/src/Numerous.Database/Repositories/GuildStatsEntryRepository.cs new file mode 100644 index 0000000..ea089b6 --- /dev/null +++ b/src/Numerous.Database/Repositories/GuildStatsEntryRepository.cs @@ -0,0 +1,27 @@ +// 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 AutoMapper; +using Microsoft.EntityFrameworkCore; +using Numerous.Database.Context; +using Numerous.Database.Dtos; +using Numerous.Database.Entities; + +namespace Numerous.Database.Repositories; + +public interface IGuildStatsEntryRepository : IRepository +{ + public Task> GetGuildStatsAsync(ulong guildId, CancellationToken ct = default); +} + +public sealed class GuildStatsEntryRepository(NumerousDbContext context, IMapper mapper) + : Repository(context, mapper), IGuildStatsEntryRepository +{ + public async Task> GetGuildStatsAsync(ulong guildId, CancellationToken ct = default) + { + return await Set.Where(x => x.GuildId == guildId) + .ToDictionaryAsync(x => x.Timestamp, x => x.MemberCount, ct); + } +} diff --git a/src/Numerous.Tests/AttachmentServiceTests.cs b/src/Numerous.Tests/AttachmentServiceTests.cs index 323a355..79e5f2b 100644 --- a/src/Numerous.Tests/AttachmentServiceTests.cs +++ b/src/Numerous.Tests/AttachmentServiceTests.cs @@ -1,5 +1,6 @@ using Discord; using Numerous.Bot.Discord; +using Numerous.Bot.Discord.Services.Attachments; using Numerous.Bot.Services; using Numerous.Common.Config; using Numerous.Tests.Stubs; diff --git a/src/Numerous.Web/Controllers/AuthController.cs b/src/Numerous.Web/Controllers/AuthController.cs index 323a6d6..c26fe15 100644 --- a/src/Numerous.Web/Controllers/AuthController.cs +++ b/src/Numerous.Web/Controllers/AuthController.cs @@ -13,6 +13,7 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Numerous.Bot.Discord; +using Numerous.Bot.Discord.Services; using Numerous.Common.Config; using Numerous.Web.Auth; From 4302e236a58f76faf1cf3ad87a9ec05356ed9d71 Mon Sep 17 00:00:00 2001 From: Pascal Pomper Date: Sat, 7 Dec 2024 00:12:56 +0100 Subject: [PATCH 3/4] Code cleanup --- src/Numerous.Database/Dtos/GuildStatsEntryDto.cs | 2 +- src/Numerous.Tests/AttachmentServiceTests.cs | 1 - src/Numerous.Web/Controllers/AuthController.cs | 1 - 3 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/Numerous.Database/Dtos/GuildStatsEntryDto.cs b/src/Numerous.Database/Dtos/GuildStatsEntryDto.cs index 9afab1a..0dc0c45 100644 --- a/src/Numerous.Database/Dtos/GuildStatsEntryDto.cs +++ b/src/Numerous.Database/Dtos/GuildStatsEntryDto.cs @@ -5,7 +5,7 @@ namespace Numerous.Database.Dtos; -public class GuildStatsEntryDto +public sealed class GuildStatsEntryDto { public GuildDto Guild { get; set; } = null!; public required ulong GuildId { get; set; } diff --git a/src/Numerous.Tests/AttachmentServiceTests.cs b/src/Numerous.Tests/AttachmentServiceTests.cs index 79e5f2b..809ffd5 100644 --- a/src/Numerous.Tests/AttachmentServiceTests.cs +++ b/src/Numerous.Tests/AttachmentServiceTests.cs @@ -1,5 +1,4 @@ using Discord; -using Numerous.Bot.Discord; using Numerous.Bot.Discord.Services.Attachments; using Numerous.Bot.Services; using Numerous.Common.Config; diff --git a/src/Numerous.Web/Controllers/AuthController.cs b/src/Numerous.Web/Controllers/AuthController.cs index c26fe15..3895f8e 100644 --- a/src/Numerous.Web/Controllers/AuthController.cs +++ b/src/Numerous.Web/Controllers/AuthController.cs @@ -12,7 +12,6 @@ using Microsoft.AspNetCore.Mvc; using Newtonsoft.Json; using Newtonsoft.Json.Linq; -using Numerous.Bot.Discord; using Numerous.Bot.Discord.Services; using Numerous.Common.Config; using Numerous.Web.Auth; From 6f6f12101eb5ad9fa1050de18b9f5d4302a4e303 Mon Sep 17 00:00:00 2001 From: Pascal Pomper Date: Sat, 7 Dec 2024 01:00:32 +0100 Subject: [PATCH 4/4] Remove unused usings --- src/Numerous.Bot/Discord/Events/MudaeMessageHandler.cs | 1 - src/Numerous.Bot/Discord/Interactions/InteractionHandler.cs | 1 - src/Numerous.Bot/Exclusive/StarReactPreventionService.cs | 1 - src/Numerous.Bot/Osu/MapFeedService.cs | 1 - 4 files changed, 4 deletions(-) diff --git a/src/Numerous.Bot/Discord/Events/MudaeMessageHandler.cs b/src/Numerous.Bot/Discord/Events/MudaeMessageHandler.cs index f2beed4..76e5943 100644 --- a/src/Numerous.Bot/Discord/Events/MudaeMessageHandler.cs +++ b/src/Numerous.Bot/Discord/Events/MudaeMessageHandler.cs @@ -6,7 +6,6 @@ using Discord; using Discord.WebSocket; using Numerous.Bot.Discord.Util; -using Numerous.Common; using Numerous.Common.Services; namespace Numerous.Bot.Discord.Events; diff --git a/src/Numerous.Bot/Discord/Interactions/InteractionHandler.cs b/src/Numerous.Bot/Discord/Interactions/InteractionHandler.cs index 9a64f56..ec5d9c7 100644 --- a/src/Numerous.Bot/Discord/Interactions/InteractionHandler.cs +++ b/src/Numerous.Bot/Discord/Interactions/InteractionHandler.cs @@ -8,7 +8,6 @@ using Discord.Interactions; using Discord.WebSocket; using Numerous.Bot.Discord.Util; -using Numerous.Common; using Numerous.Common.Config; using Numerous.Common.Services; using Numerous.Common.Util; diff --git a/src/Numerous.Bot/Exclusive/StarReactPreventionService.cs b/src/Numerous.Bot/Exclusive/StarReactPreventionService.cs index 9f303c8..c5fef96 100644 --- a/src/Numerous.Bot/Exclusive/StarReactPreventionService.cs +++ b/src/Numerous.Bot/Exclusive/StarReactPreventionService.cs @@ -5,7 +5,6 @@ using Discord; using Discord.WebSocket; -using Numerous.Common; using Numerous.Common.Config; using Numerous.Common.Services; diff --git a/src/Numerous.Bot/Osu/MapFeedService.cs b/src/Numerous.Bot/Osu/MapFeedService.cs index 9b54971..40f261d 100644 --- a/src/Numerous.Bot/Osu/MapFeedService.cs +++ b/src/Numerous.Bot/Osu/MapFeedService.cs @@ -10,7 +10,6 @@ using Numerous.Bot.Discord.Util; using Numerous.Bot.Util; using Numerous.Bot.Web.Osu; -using Numerous.Common; using Numerous.Common.Services; using Numerous.Common.Util; using Numerous.Database.Context;