namespace GroupVerify; // The plugin's settings, from gv_settings. Immutable: a reload swaps in a new one. internal sealed record Settings( bool Enabled, ulong GroupId64, int MinForcedRefreshSeconds, int DbReloadSeconds, TimeZoneInfo TimeZone, string VerifyMinRank, string[] VerifyExtraRanks, string ClanTag) { // Written to gv_settings when missing. The rank defaults are our real SimpleAdmin groups (see // the fork's Helper.StaffRanks and the web panel's site config). Only the names are set here: // "Admin or higher" is decided by immunity read from sa_groups. public static readonly (string Name, string Value)[] Defaults = [ ("enabled", "1"), // 0 turns verification off (css_gv_enable/css_gv_disable) ("group_id64", "0"), // the Steam group's 64-bit ID (103582791...); 0 turns verification off (css_gv_group) ("min_forced_refresh_seconds", "60"), // a player is rechecked with Steam (css_verify, rejoining) at most this often ("db_reload_seconds", "60"), // how often gv_verified and gv_settings are reread ("timezone", "UTC"), // must match CS2-SimpleAdmin's Timezone ("verify_min_rank", "#rank/admin"), // css_verify: this group's immunity or higher ("verify_extra_ranks", "#rank/guardian"), // css_verify: these groups too, comma-separated ("clan_tag", "") // scoreboard tag for verified players without a rank (css_gv_clantag) ]; public static readonly Settings Default = Parse(new Dictionary(), _ => { }); public static Settings Parse(IReadOnlyDictionary values, Action warn) { string Get(string name) => values.TryGetValue(name, out var value) ? value.Trim() : Defaults.First(d => d.Name == name).Value; int GetInt(string name, int min) { if (int.TryParse(Get(name), out var value) && value >= min) return value; var fallback = int.Parse(Defaults.First(d => d.Name == name).Value); warn($"gv_settings {name} must be a number of at least {min}; using {fallback}"); return fallback; } ulong groupId = 0; var groupText = Get("group_id64"); if (groupText.Length > 0 && !ulong.TryParse(groupText, out groupId)) warn($"gv_settings group_id64 \"{groupText}\" isn't a 64-bit group ID; verification is off"); var timeZone = TimeZoneInfo.Utc; var zoneName = Get("timezone"); try { timeZone = TimeZoneInfo.FindSystemTimeZoneById(zoneName); } catch (Exception ex) when (ex is TimeZoneNotFoundException or InvalidTimeZoneException) { warn($"gv_settings timezone \"{zoneName}\" isn't a known time zone; using UTC"); } return new Settings( Get("enabled") != "0", groupId, GetInt("min_forced_refresh_seconds", 0), GetInt("db_reload_seconds", 10), timeZone, Get("verify_min_rank"), Get("verify_extra_ranks").Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries), ParseClanTag(Get("clan_tag"), warn)); } // Steam's own group tags are at most 12 characters. Quotes and control characters are refused, // since the tag travels in console commands. public const int ClanTagMaxLength = 12; public static bool ValidClanTag(string tag) => tag.Length <= ClanTagMaxLength && !tag.Any(c => char.IsControl(c) || c == '"' || c == ';'); private static string ParseClanTag(string tag, Action warn) { if (ValidClanTag(tag)) return tag; warn($"gv_settings clan_tag \"{tag}\" must be at most {ClanTagMaxLength} characters, without quotes, ; or control characters; ignoring it"); return ""; } // The tag other plugins see: none while verification is off. public string EffectiveClanTag => Active ? ClanTag : ""; // Verification only runs when enabled with a group set; otherwise nobody is verified. public bool Active => Enabled && GroupId64 != 0; // Now as a wall-clock time in the database's time zone, to the second (MySQL rounds fractions). public DateTime Now() { var now = TimeZoneInfo.ConvertTimeFromUtc(DateTime.UtcNow, TimeZone); return DateTime.SpecifyKind(now.AddTicks(-(now.Ticks % TimeSpan.TicksPerSecond)), DateTimeKind.Unspecified); } }