using Nuuru.Server.Auth; namespace Nuuru.Server.Auth { /// /// Defines user badges based on permissions /// public static class UserBadges { public static readonly IReadOnlyList Definitions = new List { // Blocked badges new SimpleBadgeDefinition("blocked_global", "Global Blocked", "#ef4444"), new SimpleBadgeDefinition("blocked_forum", "Forum Blocked", "#ef4444"), new SimpleBadgeDefinition("blocked_booru", "Booru Blocked", "#ef4444"), // Rank badges - ordered by priority (first = highest) new CompositeBadgeDefinition("administrator_global", "Global Administrator", "#ef4444", ["administrator_forum", "administrator_booru"]), new PermissionBadgeDefinition("administrator_forum", "Forum Administrator", "#ef4444", Permissions.Admin.ManageCategories), new PermissionBadgeDefinition("administrator_booru", "Booru Administrator", "#ef4444", Permissions.Admin.DeletePost), new CompositeBadgeDefinition("moderator_global", "Global Moderator", "#3b82f6", ["moderator_forum", "moderator_booru"]), new PermissionBadgeDefinition("moderator_forum", "Forum Moderator", "#3b82f6", Permissions.Forum.LockThread), new PermissionBadgeDefinition("moderator_booru", "Booru Moderator", "#3b82f6", Permissions.Moderation.LockComments), new CompositeBadgeDefinition("janitor_global", "Global Janitor", "#6366f1", ["janitor_forum", "janitor_booru"]), new PermissionBadgeDefinition("janitor_forum", "Forum Janitor", "#6366f1", Permissions.Forum.DeletePost), new PermissionBadgeDefinition("janitor_booru", "Booru Janitor", "#6366f1", Permissions.Moderation.TrashPost), new PermissionBadgeDefinition("approver_booru", "Booru Approver", "#8b5cf6", Permissions.Moderation.ApprovePost), new PermissionBadgeDefinition("trusted_booru", "Trusted", "#f59e0b", Permissions.User.AutoApprove), // Other badges new SimpleBadgeDefinition("babymode", "Baby Mode", "#fbbf24"), new SimpleBadgeDefinition("legacy", "Legacy User", "#94a3b8"), }; private static readonly Dictionary _byId = Definitions.ToDictionary(b => b.Id); public static BadgeDefinition? GetById(string id) => _byId.TryGetValue(id, out var badge) ? badge : null; /// /// Gets all applicable badge IDs for a user based on their permissions and active bans /// public static IReadOnlyList GetBadgeIds( IEnumerable permissions, IEnumerable<(Models.BanZone Zone, DateTime EndTime)>? activeBans = null) { var earnedIds = new HashSet(); // 1. Get the single highest-priority rank badge var rankBadgeId = GetRankBadgeFromPermissions(permissions); if (rankBadgeId != null) earnedIds.Add(rankBadgeId); // 2. Get all ban badges foreach (var banBadgeId in GetBadgeIdsFromBans(activeBans)) { earnedIds.Add(banBadgeId); } // 3. Collect other earned badges (like legacy) // (In this specific case, legacy has no automated trigger yet, but we check if user has it) // Note: If we had other automated non-rank badges from permissions, we'd add them here. // 4. Return in the priority order defined in Definitions. // Ban badges have expiry appended (e.g. "blocked_global:1713139200"), // so match by prefix against definition IDs. var ordered = new List(); foreach (var def in Definitions) { var match = earnedIds.FirstOrDefault(id => id == def.Id || id.StartsWith(def.Id + ":")); if (match != null) { ordered.Add(match); earnedIds.Remove(match); } } return ordered; } /// /// Returns the single highest-priority rank badge earned via permissions /// public static string? GetRankBadgeFromPermissions(IEnumerable permissions) { var permissionSet = permissions.ToHashSet(); var earnedIds = new HashSet(); // Identify direct permission badges foreach (var def in Definitions.OfType()) { if (permissionSet.Contains(def.RequiredPermission)) { earnedIds.Add(def.Id); } } // Combine into composite badges where all prerequisites are met bool changed; do { changed = false; foreach (var def in Definitions.OfType()) { if (!earnedIds.Contains(def.Id) && def.PrerequisiteBadgeIds.All(p => earnedIds.Contains(p))) { earnedIds.Add(def.Id); foreach (var prereqId in def.PrerequisiteBadgeIds) { earnedIds.Remove(prereqId); } changed = true; break; } } } while (changed); // Return the highest priority badge (first match in Definitions order) foreach (var def in Definitions) { if (earnedIds.Contains(def.Id)) return def.Id; } return null; } /// /// Identifies badges earned via active ban status. /// Badge IDs encode the expiry as "blocked_zone:unixs" so the frontend can display duration. /// public static IEnumerable GetBadgeIdsFromBans(IEnumerable<(Models.BanZone Zone, DateTime EndTime)>? activeBans) { if (activeBans == null) yield break; foreach (var (zone, endTime) in activeBans) { var expiry = new DateTimeOffset(endTime, TimeSpan.Zero).ToUnixTimeSeconds(); switch (zone) { case Models.BanZone.Sitewide: yield return $"blocked_global:{expiry}"; break; case Models.BanZone.Forumwide: yield return $"blocked_forum:{expiry}"; break; case Models.BanZone.Booruwide: yield return $"blocked_booru:{expiry}"; break; } } } } public abstract record BadgeDefinition( string Id, string DisplayName, string Color ); public record SimpleBadgeDefinition( string Id, string DisplayName, string Color ) : BadgeDefinition(Id, DisplayName, Color); public record PermissionBadgeDefinition( string Id, string DisplayName, string Color, string RequiredPermission ) : BadgeDefinition(Id, DisplayName, Color); public record CompositeBadgeDefinition( string Id, string DisplayName, string Color, IReadOnlyList PrerequisiteBadgeIds ) : BadgeDefinition(Id, DisplayName, Color); }