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);
}