From 29c2c0221ac900307f8e95a9d9e4b8db5b0056f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 23 Dec 2024 15:17:49 +0100 Subject: [PATCH 1/3] Implement required interactions with beatmap mirrors --- .../BeatmapSubmissionController.cs | 10 +++- .../DatabaseOperationExtensions.cs | 19 +++++++ .../Models/Database/osu_mirror.cs | 16 ++++++ osu.Server.BeatmapSubmission/Program.cs | 2 + .../Services/IMirrorService.cs | 12 ++++ .../Services/MirrorService.cs | 57 +++++++++++++++++++ 6 files changed, 115 insertions(+), 1 deletion(-) create mode 100644 osu.Server.BeatmapSubmission/Models/Database/osu_mirror.cs create mode 100644 osu.Server.BeatmapSubmission/Services/IMirrorService.cs create mode 100644 osu.Server.BeatmapSubmission/Services/MirrorService.cs diff --git a/osu.Server.BeatmapSubmission/BeatmapSubmissionController.cs b/osu.Server.BeatmapSubmission/BeatmapSubmissionController.cs index df7c850..8795058 100644 --- a/osu.Server.BeatmapSubmission/BeatmapSubmissionController.cs +++ b/osu.Server.BeatmapSubmission/BeatmapSubmissionController.cs @@ -23,12 +23,18 @@ public class BeatmapSubmissionController : Controller private readonly IBeatmapStorage beatmapStorage; private readonly BeatmapPackagePatcher patcher; private readonly ILegacyIO legacyIO; + private readonly IMirrorService mirrorService; - public BeatmapSubmissionController(IBeatmapStorage beatmapStorage, BeatmapPackagePatcher patcher, ILegacyIO legacyIO) + public BeatmapSubmissionController( + IBeatmapStorage beatmapStorage, + BeatmapPackagePatcher patcher, + ILegacyIO legacyIO, + IMirrorService mirrorService) { this.beatmapStorage = beatmapStorage; this.patcher = patcher; this.legacyIO = legacyIO; + this.mirrorService = mirrorService; } /// @@ -403,6 +409,8 @@ private async Task updateBeatmapSetFromArchiveAsync(uint beatmapSetId, Str if (await db.IsBeatmapSetNominatedAsync(beatmapSetId)) await legacyIO.DisqualifyBeatmapSetAsync(beatmapSetId, "This beatmap set was updated by the mapper after a nomination. Please ensure to re-check the beatmaps for new issues. If you are the mapper, please comment in this thread on what you changed."); + await mirrorService.PurgeBeatmapSetAsync(db, beatmapSetId); + if (!await db.IsBeatmapSetInProcessingQueueAsync(beatmapSetId)) { await db.AddBeatmapSetToProcessingQueueAsync(beatmapSetId); diff --git a/osu.Server.BeatmapSubmission/DatabaseOperationExtensions.cs b/osu.Server.BeatmapSubmission/DatabaseOperationExtensions.cs index 6b5dfd0..d9c0446 100644 --- a/osu.Server.BeatmapSubmission/DatabaseOperationExtensions.cs +++ b/osu.Server.BeatmapSubmission/DatabaseOperationExtensions.cs @@ -397,5 +397,24 @@ public static async Task> GetBeatmapOwnersAsync(this MySqlConn }, transaction); } + + public static async Task> GetMirrorsRequiringUpdateAsync(this MySqlConnection db, MySqlTransaction? transaction = null) + { + return await db.QueryAsync( + "SELECT * FROM `osu_mirrors` WHERE `version` > 1 AND `perform_updates` > 0", + transaction: transaction); + } + + public static async Task MarkPendingPurgeAsync(this MySqlConnection db, osu_mirror mirror, uint beatmapSetId, MySqlTransaction? transaction = null) + { + await db.ExecuteAsync( + "UPDATE `osu_mirrors` SET `pending_purge` = CONCAT(IFNULL(`pending_purge`, ''), @beatmapset_id) WHERE `mirror_id` = @mirror_id", + new + { + beatmapset_id = beatmapSetId, + mirror_id = mirror.mirror_id + }, + transaction); + } } } diff --git a/osu.Server.BeatmapSubmission/Models/Database/osu_mirror.cs b/osu.Server.BeatmapSubmission/Models/Database/osu_mirror.cs new file mode 100644 index 0000000..43cdbcd --- /dev/null +++ b/osu.Server.BeatmapSubmission/Models/Database/osu_mirror.cs @@ -0,0 +1,16 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +// ReSharper disable InconsistentNaming + +namespace osu.Server.BeatmapSubmission.Models.Database +{ + public class osu_mirror + { + public ushort mirror_id { get; set; } + public string base_url { get; set; } = string.Empty; + public decimal version { get; set; } + public bool perform_updates { get; set; } + public string secret_key { get; set; } = string.Empty; + } +} diff --git a/osu.Server.BeatmapSubmission/Program.cs b/osu.Server.BeatmapSubmission/Program.cs index 09ac90d..cb48220 100644 --- a/osu.Server.BeatmapSubmission/Program.cs +++ b/osu.Server.BeatmapSubmission/Program.cs @@ -43,6 +43,7 @@ public static void Main(string[] args) builder.Services.AddTransient(); builder.Services.AddHttpClient(); builder.Services.AddTransient(); + builder.Services.AddTransient(); builder.Services.AddSwaggerGen(c => { c.IncludeXmlComments(Path.Combine(AppContext.BaseDirectory, Assembly.GetExecutingAssembly().GetName().Name + ".xml")); @@ -57,6 +58,7 @@ public static void Main(string[] args) builder.Services.AddTransient(); builder.Services.AddHttpClient(); builder.Services.AddTransient(); + builder.Services.AddTransient(); break; } } diff --git a/osu.Server.BeatmapSubmission/Services/IMirrorService.cs b/osu.Server.BeatmapSubmission/Services/IMirrorService.cs new file mode 100644 index 0000000..9b882c4 --- /dev/null +++ b/osu.Server.BeatmapSubmission/Services/IMirrorService.cs @@ -0,0 +1,12 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using MySqlConnector; + +namespace osu.Server.BeatmapSubmission.Services +{ + public interface IMirrorService + { + Task PurgeBeatmapSetAsync(MySqlConnection db, uint beatmapSetId); + } +} diff --git a/osu.Server.BeatmapSubmission/Services/MirrorService.cs b/osu.Server.BeatmapSubmission/Services/MirrorService.cs new file mode 100644 index 0000000..445d51f --- /dev/null +++ b/osu.Server.BeatmapSubmission/Services/MirrorService.cs @@ -0,0 +1,57 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using MySqlConnector; +using osu.Framework.Extensions; +using osu.Server.BeatmapSubmission.Models.Database; + +namespace osu.Server.BeatmapSubmission.Services +{ + public class MirrorService : IMirrorService + { + private readonly HttpClient client; + + public MirrorService(HttpClient client) + { + this.client = client; + } + + public async Task PurgeBeatmapSetAsync(MySqlConnection db, uint beatmapSetId) + { + osu_mirror[] mirrors = (await db.GetMirrorsRequiringUpdateAsync()).ToArray(); + + foreach (var mirror in mirrors) + { + if (await performMirrorAction(mirror, "purge", new Dictionary { ["s"] = beatmapSetId.ToString() }) != "1") + await db.MarkPendingPurgeAsync(mirror, beatmapSetId); + } + } + + private async Task performMirrorAction(osu_mirror mirror, string action, Dictionary data) + { + data["ts"] = DateTimeOffset.Now.ToUnixTimeSeconds().ToString(); + data["action"] = action; + data["cs"] = ($"{data.GetValueOrDefault("s")}{data.GetValueOrDefault("fd")}{data.GetValueOrDefault("fs")}{data.GetValueOrDefault("ts")}" + + $"{data.GetValueOrDefault("nv")}{data.GetValueOrDefault("action")}{mirror.secret_key}").ComputeMD5Hash(); + + var request = new HttpRequestMessage(HttpMethod.Post, mirror.base_url); + request.Content = new FormUrlEncodedContent(data); + + try + { + var response = await client.SendAsync(request); + return await response.Content.ReadAsStringAsync(); + } + catch (Exception) + { + // TODO: log error + return null; + } + } + } + + public class NoOpMirrorService : IMirrorService + { + public Task PurgeBeatmapSetAsync(MySqlConnection db, uint beatmapSetId) => Task.CompletedTask; + } +} From 4701354177e3e751df12e7edbc89c063e2c2ef14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 24 Dec 2024 11:44:22 +0100 Subject: [PATCH 2/3] Use no-op mirror service in tests --- .../BeatmapSubmissionControllerTest.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Server.BeatmapSubmission.Tests/BeatmapSubmissionControllerTest.cs b/osu.Server.BeatmapSubmission.Tests/BeatmapSubmissionControllerTest.cs index cb6234d..6002c1a 100644 --- a/osu.Server.BeatmapSubmission.Tests/BeatmapSubmissionControllerTest.cs +++ b/osu.Server.BeatmapSubmission.Tests/BeatmapSubmissionControllerTest.cs @@ -46,6 +46,7 @@ public BeatmapSubmissionControllerTest(IntegrationTestWebApplicationFactory(_ => beatmapStorage); services.AddTransient(); services.AddTransient(_ => mockLegacyIO.Object); + services.AddTransient(); }); }).CreateClient(); } From 45137e95bec40551a6c7aa84ec4a004949d33d5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 30 Dec 2024 09:11:08 +0100 Subject: [PATCH 3/3] Fix incorrect update of `pending_purge` on `osu_mirrors` --- osu.Server.BeatmapSubmission/DatabaseOperationExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Server.BeatmapSubmission/DatabaseOperationExtensions.cs b/osu.Server.BeatmapSubmission/DatabaseOperationExtensions.cs index d9c0446..3ca37cc 100644 --- a/osu.Server.BeatmapSubmission/DatabaseOperationExtensions.cs +++ b/osu.Server.BeatmapSubmission/DatabaseOperationExtensions.cs @@ -408,7 +408,7 @@ public static async Task> GetMirrorsRequiringUpdateAsync public static async Task MarkPendingPurgeAsync(this MySqlConnection db, osu_mirror mirror, uint beatmapSetId, MySqlTransaction? transaction = null) { await db.ExecuteAsync( - "UPDATE `osu_mirrors` SET `pending_purge` = CONCAT(IFNULL(`pending_purge`, ''), @beatmapset_id) WHERE `mirror_id` = @mirror_id", + "UPDATE `osu_mirrors` SET `pending_purge` = CONCAT(`pending_purge`, ',', @beatmapset_id) WHERE `mirror_id` = @mirror_id", new { beatmapset_id = beatmapSetId,