using System.Net; using MaxMind.Db; namespace PlaytimeHistory; // Country and city for an IP, from DB-IP's City Lite database (MaxMind DB format, CC BY 4.0: // "IP Geolocation by DB-IP", https://db-ip.com). Only the result is kept; the IP never is. public sealed class Geo : IDisposable { public sealed record Location(string? CountryIso, string? CountryName, string? City); private readonly Reader _reader; private Geo(Reader reader) => _reader = reader; public string DatabaseType => _reader.Metadata.DatabaseType; public DateTime BuildDate => _reader.Metadata.BuildDate; // Null when the file isn't there. public static Geo? Open(string path) => File.Exists(path) ? new Geo(new Reader(path, FileAccessMode.MemoryMapped)) : null; // `address` is "ip" or "ip:port", as CCSPlayerController.IpAddress gives it. public Location? Lookup(string? address) { if (string.IsNullOrEmpty(address)) return null; var colon = address.LastIndexOf(':'); var host = colon > 0 && address.IndexOf(':') == colon ? address[..colon] : address; if (!IPAddress.TryParse(host, out var ip)) return null; var data = _reader.Find>(ip); if (data == null) return null; string? iso = null, country = null, city = null; if (data.TryGetValue("country", out var c) && c is Dictionary countryData) { iso = countryData.TryGetValue("iso_code", out var code) ? code as string : null; country = EnglishName(countryData); } if (data.TryGetValue("city", out var ct) && ct is Dictionary cityData) city = EnglishName(cityData); return iso == null && city == null ? null : new Location(iso, country, city); } private static string? EnglishName(Dictionary data) => data.TryGetValue("names", out var n) && n is Dictionary names && names.TryGetValue("en", out var en) ? en as string : null; public void Dispose() => _reader.Dispose(); }