using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using System.Text.Json; using DorkNet.Server.Auth; using DorkNet.Server.Data; using DorkNet.Server.Data.Entities; namespace DorkNet.Server.Controllers.PlatformNotifications; /// /// platformnotifications.rec.net — the 2023 client's platform push- /// notification preference service (RecNet.Runtime LELAJKMOMIA; the /// service host enum value 15 resolves to the PlatformNotifications /// entry of the service map, relative paths "preferences", /// "config/categories", "badge", "device"). /// /// Wire contracts verified against the 2023.03.21 ISIL decompile: /// /// GET preferences → RecNet.PlatformNotificationPreferencesDTO — an /// OBJECT whose only key is "MutedCategories" (a list of category /// enum values). Generated reader JLHCGPANJCB.txt:151-185 registers /// exactly that key. LELAJKMOMIA.txt:1102 (DCGHGFDPAMM) issues the /// GET; an array/empty body throws the strict-reader error seen in /// Player.log:1409-1711 ("expected:'{', actual:'['" / /// "Malformed Response: '[]'" → "Failed to get notification /// preferences"). /// /// PUT preferences → LELAJKMOMIA.txt:1480 (KKPAKGOCPEA) sends the /// same DTO ({"MutedCategories":[...]}) with HTTP verb 3 = PUT /// (verb table HNLCIDLIIBO.txt:878-903). Fire-and-forget /// (LDGADANDBIO) — response body ignored. /// /// GET config/categories → parsed as envelope MKIFDCBPEAD /// {"Results":[...],"TotalResults":N} (reader JBGKGCLPCHP.txt:203-238) /// then converted via Func<MKIFDCBPEAD, List<PlatformNotification- /// CategoryConfigDTO>> (LELAJKMOMIA.txt:1930 region). Each item is /// {"CategoryId","Importance","Name","Description","IsMuteable"} /// (reader JNFDPPCIPHG.txt:371-462). /// [ApiController] [Authorize] public class PlatformNotificationsController(DorkNetDbContext db) : ControllerBase { private long Me => this.RequireCurrentPlayerId(); /// The category catalogue we expose. CategoryId values are /// round-tripped between config/categories and MutedCategories, so /// they only need to be internally consistent. private static readonly (int CategoryId, string Name, string Description)[] CategoryCatalog = { (1, "Messages", "Direct messages from other players"), (2, "Friend Requests", "Incoming friend requests"), (3, "Event Invites", "Invitations to events"), (4, "Announcements", "News and announcements from Rec Room"), }; [HttpGet("/preferences")] [HttpGet("/platformnotifications/preferences")] public async Task Preferences() { var row = await db.NotificationPrefs.FirstOrDefaultAsync(p => p.PlayerId == Me); // Exact DTO shape: {"MutedCategories":[...]} — see class doc. return Ok(new { MutedCategories = MutedCategoriesFor(row) }); } /// PUT preferences — body is the same DTO /// ({"MutedCategories":[1,4]}). Parsed manually so we tolerate any /// key casing and missing/null lists. Response body is ignored by /// the client (LDGADANDBIO), but we echo the stored DTO anyway. [HttpPut("/preferences")] [HttpPost("/preferences")] [HttpPut("/platformnotifications/preferences")] [HttpPost("/platformnotifications/preferences")] public async Task SetPreferences() { List? muted = null; string raw; using (var reader = new StreamReader(Request.Body)) raw = await reader.ReadToEndAsync(); if (!string.IsNullOrWhiteSpace(raw)) { try { using var doc = JsonDocument.Parse(raw); if (doc.RootElement.ValueKind == JsonValueKind.Object) { foreach (var prop in doc.RootElement.EnumerateObject()) { if (!prop.Name.Equals("MutedCategories", StringComparison.OrdinalIgnoreCase)) continue; if (prop.Value.ValueKind != JsonValueKind.Array) continue; muted = new List(); foreach (var el in prop.Value.EnumerateArray()) { if (el.ValueKind == JsonValueKind.Number && el.TryGetInt32(out var v)) muted.Add(v); else if (el.ValueKind == JsonValueKind.String && int.TryParse(el.GetString(), out var s)) muted.Add(s); } } } } catch (JsonException) { // Non-JSON body — treat as "no change". } } var row = await db.NotificationPrefs.FirstOrDefaultAsync(p => p.PlayerId == Me); if (row is null) { row = new NotificationPrefsEntity { PlayerId = Me, Platform = "platform", }; db.NotificationPrefs.Add(row); } if (muted is not null) { row.AllowMessage = !muted.Contains(1); row.AllowFriendRequest = !muted.Contains(2); row.AllowEventInvite = !muted.Contains(3); row.AllowAnnouncements = !muted.Contains(4); row.UpdatedAt = DateTime.UtcNow; } await db.SaveChangesAsync(); return Ok(new { MutedCategories = MutedCategoriesFor(row) }); } /// GET config/categories — {"Results":[...],"TotalResults":N} /// envelope of PlatformNotificationCategoryConfigDTO items. [HttpGet("/config/categories")] [HttpGet("/platformnotifications/config/categories")] [AllowAnonymous] public IActionResult Categories() { var categories = CategoryCatalog .Select(c => new { c.CategoryId, Importance = 0, c.Name, c.Description, IsMuteable = true, }) .ToArray(); return Ok(new { Results = categories, TotalResults = categories.Length, }); } private static int[] MutedCategoriesFor(NotificationPrefsEntity? row) { if (row is null) return Array.Empty(); var muted = new List(4); if (!row.AllowMessage) muted.Add(1); if (!row.AllowFriendRequest) muted.Add(2); if (!row.AllowEventInvite) muted.Add(3); if (!row.AllowAnnouncements) muted.Add(4); return muted.ToArray(); } }