From 1996db8cfa743cd5517cdf5e3c63ce68d90dc131 Mon Sep 17 00:00:00 2001 From: Yuze Fu Date: Thu, 19 Sep 2024 21:35:27 +0900 Subject: [PATCH] feat: online status for homepage --- .../Controllers/Discord/MetarModule.cs | 2 - .../Controllers/UtilController.cs | 97 ++++++ Net.Vatprc.Uniapi/Program.cs | 2 + Net.Vatprc.Uniapi/Services/DiscordWorker.cs | 4 +- .../Services/RudiMetarService.cs | 2 + Net.Vatprc.Uniapi/Services/VatsimData.cs | 307 ++++++++++++++++++ Net.Vatprc.Uniapi/Services/VatsimService.cs | 41 +++ Net.Vatprc.Uniapi/Utils/Utils.cs | 8 + 8 files changed, 459 insertions(+), 4 deletions(-) create mode 100644 Net.Vatprc.Uniapi/Controllers/UtilController.cs create mode 100644 Net.Vatprc.Uniapi/Services/VatsimData.cs create mode 100644 Net.Vatprc.Uniapi/Services/VatsimService.cs create mode 100644 Net.Vatprc.Uniapi/Utils/Utils.cs diff --git a/Net.Vatprc.Uniapi/Controllers/Discord/MetarModule.cs b/Net.Vatprc.Uniapi/Controllers/Discord/MetarModule.cs index 662ca71..42d0851 100644 --- a/Net.Vatprc.Uniapi/Controllers/Discord/MetarModule.cs +++ b/Net.Vatprc.Uniapi/Controllers/Discord/MetarModule.cs @@ -9,8 +9,6 @@ namespace Net.Vatprc.Uniapi.Controllers.Discord; public class MetarModule(RudiMetarService MetarService, ILogger Logger) : InteractionModuleBase { - - [SlashCommand("metar", "Get METAR for an airport")] public async Task WhoAmIAsync(string icao) { diff --git a/Net.Vatprc.Uniapi/Controllers/UtilController.cs b/Net.Vatprc.Uniapi/Controllers/UtilController.cs new file mode 100644 index 0000000..effff2a --- /dev/null +++ b/Net.Vatprc.Uniapi/Controllers/UtilController.cs @@ -0,0 +1,97 @@ +using Net.Vatprc.Uniapi.Services; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Authorization; +using Net.Vatprc.Uniapi.Models; +using System.Collections; +using System.Globalization; +using System.Text.RegularExpressions; + +namespace Net.Vatprc.Uniapi.Controllers; + +/// +/// Operate users. +/// +[ApiController, Route("api/util")] +public partial class UtilController(VatsimService VatsimService) : ControllerBase +{ + public class ControllerDto + { + public required int Cid { get; set; } + public required string Name { get; set; } + public required string Callsign { get; set; } + public required string Frequency { get; set; } + } + + public class FutureControllerDto + { + public required string Callsign { get; set; } + public required string Name { get; set; } + public required string Start { get; set; } + public required string End { get; set; } + } + + public class PilotDto + { + public required int Cid { get; set; } + public required string Name { get; set; } + public required string Callsign { get; set; } + public required string? Departure { get; set; } + public required string? Arrival { get; set; } + public required string? Aircraft { get; set; } + } + + public class VatprcStatusDto + { + public required DateTimeOffset LastUpdated { get; set; } + public required IEnumerable Pilots { get; set; } + public required IEnumerable Controllers { get; set; } + public required IEnumerable FutureControllers { get; set; } + } + + + [GeneratedRegex("^(Z[BSGUHWJPLYM][A-Z0-9]{2}(_[A-Z0-9]*)?_(DEL|GND|TWR|APP|DEP|CTR))|(PRC_FSS)$")] + protected static partial Regex vatprcControllerRegexp(); + [GeneratedRegex("Z[BMSPGJYWLH][A-Z]{2}")] + protected static partial Regex vatprcAirportRegexp(); + + [HttpGet("online-status")] + [AllowAnonymous] + public async Task Status() + { + var vatsimData = await VatsimService.GetOnlineData(); + var atcSchedule = await VatsimService.GetAtcSchedule(); + return new VatprcStatusDto + { + LastUpdated = vatsimData.General.UpdateTimestamp, + Pilots = vatsimData.Pilots + .Where(x => + (x.FlightPlan?.Departure != null && vatprcAirportRegexp().IsMatch(x.FlightPlan?.Departure!)) || + (x.FlightPlan?.Arrival != null && vatprcAirportRegexp().IsMatch(x.FlightPlan?.Arrival!))) + .Select(x => new PilotDto + { + Cid = Convert.ToInt32(x.Cid), + Name = x.Name, + Callsign = x.Callsign, + Departure = x.FlightPlan?.Departure, + Arrival = x.FlightPlan?.Arrival, + Aircraft = x.FlightPlan?.AircraftShort, + }), + Controllers = vatsimData.Controllers + .Where(x => vatprcAirportRegexp().IsMatch(x.Callsign)) + .Select(x => new ControllerDto + { + Cid = Convert.ToInt32(x.Cid), + Name = x.Name, + Callsign = x.Callsign, + Frequency = x.Frequency, + }), + FutureControllers = atcSchedule.Select(x => new FutureControllerDto + { + Name = $"{x.User.FirstName} {x.User.LastName}", + Callsign = x.Callsign, + Start = x.Start.ToUniversalTime().ToString("dd HH:mm"), + End = x.Finish.ToUniversalTime().ToString("dd HH:mm"), + }), + }; + } +} diff --git a/Net.Vatprc.Uniapi/Program.cs b/Net.Vatprc.Uniapi/Program.cs index 2edf75d..2ce53fa 100644 --- a/Net.Vatprc.Uniapi/Program.cs +++ b/Net.Vatprc.Uniapi/Program.cs @@ -11,6 +11,7 @@ global using f64 = double; global using Net.Vatprc.Uniapi; global using UniApi = Net.Vatprc.Uniapi; +global using static Net.Vatprc.Uniapi.Utils.Utils; using System.CommandLine; using System.Text.Json.Serialization; @@ -214,6 +215,7 @@ error message example. }); RudiMetarService.ConfigureOn(builder); +VatsimService.ConfigureOn(builder); var app = builder.Build(); diff --git a/Net.Vatprc.Uniapi/Services/DiscordWorker.cs b/Net.Vatprc.Uniapi/Services/DiscordWorker.cs index 81a9479..959e43a 100644 --- a/Net.Vatprc.Uniapi/Services/DiscordWorker.cs +++ b/Net.Vatprc.Uniapi/Services/DiscordWorker.cs @@ -18,8 +18,8 @@ IServiceProvider ServiceProvider ) : IHostedService { protected InteractionService Interaction { get; init; } = new(Client.Rest); - protected static ActivitySource ActivitySource = - new ActivitySource(typeof(DiscordWorker).FullName ?? throw new ArgumentNullException()); + protected readonly static ActivitySource ActivitySource = + new(typeof(DiscordWorker).FullName ?? throw new ArgumentNullException()); public async Task StartAsync(CancellationToken ct) { diff --git a/Net.Vatprc.Uniapi/Services/RudiMetarService.cs b/Net.Vatprc.Uniapi/Services/RudiMetarService.cs index 9528362..d3cafb3 100644 --- a/Net.Vatprc.Uniapi/Services/RudiMetarService.cs +++ b/Net.Vatprc.Uniapi/Services/RudiMetarService.cs @@ -1,3 +1,4 @@ +using System.Diagnostics; using Flurl; using Flurl.Http; using Microsoft.Extensions.Options; @@ -17,6 +18,7 @@ public async Task GetMetar(string icao) { var response = await Options.Value.Endpoint .SetQueryParam("id", icao) + .WithHeader("User-Agent", UniapiUserAgent) .GetAsync(); return await response.GetStringAsync(); } diff --git a/Net.Vatprc.Uniapi/Services/VatsimData.cs b/Net.Vatprc.Uniapi/Services/VatsimData.cs new file mode 100644 index 0000000..e86c399 --- /dev/null +++ b/Net.Vatprc.Uniapi/Services/VatsimData.cs @@ -0,0 +1,307 @@ +#pragma warning disable CS8618 + +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Globalization; + +namespace Net.Vatprc.Uniapi.Services.VatsimData; + +public partial class VatsimData +{ + [JsonPropertyName("general")] + public General General { get; set; } + + [JsonPropertyName("pilots")] + public Pilot[] Pilots { get; set; } + + [JsonPropertyName("controllers")] + public Atc[] Controllers { get; set; } + + [JsonPropertyName("atis")] + public Atc[] Atis { get; set; } + + [JsonPropertyName("servers")] + public ServerElement[] Servers { get; set; } + + [JsonPropertyName("prefiles")] + public Prefile[] Prefiles { get; set; } + + [JsonPropertyName("facilities")] + public Facility[] Facilities { get; set; } + + [JsonPropertyName("ratings")] + public Facility[] Ratings { get; set; } + + [JsonPropertyName("pilot_ratings")] + public Rating[] PilotRatings { get; set; } + + [JsonPropertyName("military_ratings")] + public Rating[] MilitaryRatings { get; set; } +} + +public partial class Atc +{ + [JsonPropertyName("cid")] + public long Cid { get; set; } + + [JsonPropertyName("name")] + public string Name { get; set; } + + [JsonPropertyName("callsign")] + public string Callsign { get; set; } + + [JsonPropertyName("frequency")] + public string Frequency { get; set; } + + [JsonPropertyName("facility")] + public long Facility { get; set; } + + [JsonPropertyName("rating")] + public long Rating { get; set; } + + [JsonPropertyName("server")] + public string Server { get; set; } + + [JsonPropertyName("visual_range")] + public long VisualRange { get; set; } + + [JsonPropertyName("atis_code")] + public string AtisCode { get; set; } + + [JsonPropertyName("text_atis")] + public string[] TextAtis { get; set; } + + [JsonPropertyName("last_updated")] + public DateTimeOffset LastUpdated { get; set; } + + [JsonPropertyName("logon_time")] + public DateTimeOffset LogonTime { get; set; } +} + +public partial class Facility +{ + [JsonPropertyName("id")] + public long Id { get; set; } + + [JsonPropertyName("short")] + public string Short { get; set; } + + [JsonPropertyName("long")] + public string Long { get; set; } +} + +public partial class General +{ + [JsonPropertyName("version")] + public long Version { get; set; } + + [JsonPropertyName("reload")] + public long Reload { get; set; } + + [JsonPropertyName("update")] + public string Update { get; set; } + + [JsonPropertyName("update_timestamp")] + public DateTimeOffset UpdateTimestamp { get; set; } + + [JsonPropertyName("connected_clients")] + public long ConnectedClients { get; set; } + + [JsonPropertyName("unique_users")] + public long UniqueUsers { get; set; } +} + +public partial class Rating +{ + [JsonPropertyName("id")] + public long Id { get; set; } + + [JsonPropertyName("short_name")] + public string ShortName { get; set; } + + [JsonPropertyName("long_name")] + public string LongName { get; set; } +} + +public partial class Pilot +{ + [JsonPropertyName("cid")] + public long Cid { get; set; } + + [JsonPropertyName("name")] + public string Name { get; set; } + + [JsonPropertyName("callsign")] + public string Callsign { get; set; } + + [JsonPropertyName("server")] + public string Server { get; set; } + + [JsonPropertyName("pilot_rating")] + public long PilotRating { get; set; } + + [JsonPropertyName("military_rating")] + public long MilitaryRating { get; set; } + + [JsonPropertyName("latitude")] + public double Latitude { get; set; } + + [JsonPropertyName("longitude")] + public double Longitude { get; set; } + + [JsonPropertyName("altitude")] + public long Altitude { get; set; } + + [JsonPropertyName("groundspeed")] + public long Groundspeed { get; set; } + + [JsonPropertyName("transponder")] + public string Transponder { get; set; } + + [JsonPropertyName("heading")] + public long Heading { get; set; } + + [JsonPropertyName("qnh_i_hg")] + public double QnhIHg { get; set; } + + [JsonPropertyName("qnh_mb")] + public long QnhMb { get; set; } + + [JsonPropertyName("flight_plan")] + public FlightPlan? FlightPlan { get; set; } + + [JsonPropertyName("logon_time")] + public DateTimeOffset LogonTime { get; set; } + + [JsonPropertyName("last_updated")] + public DateTimeOffset LastUpdated { get; set; } +} + +public partial class FlightPlan +{ + [JsonPropertyName("flight_rules")] + public string FlightRules { get; set; } + + [JsonPropertyName("aircraft")] + public string Aircraft { get; set; } + + [JsonPropertyName("aircraft_faa")] + public string AircraftFaa { get; set; } + + [JsonPropertyName("aircraft_short")] + public string AircraftShort { get; set; } + + [JsonPropertyName("departure")] + public string Departure { get; set; } + + [JsonPropertyName("arrival")] + public string Arrival { get; set; } + + [JsonPropertyName("alternate")] + public string Alternate { get; set; } + + [JsonPropertyName("cruise_tas")] + public string CruiseTas { get; set; } + + [JsonPropertyName("altitude")] + public string Altitude { get; set; } + + [JsonPropertyName("deptime")] + public string Deptime { get; set; } + + [JsonPropertyName("enroute_time")] + public string EnrouteTime { get; set; } + + [JsonPropertyName("fuel_time")] + public string FuelTime { get; set; } + + [JsonPropertyName("remarks")] + public string Remarks { get; set; } + + [JsonPropertyName("route")] + public string Route { get; set; } + + [JsonPropertyName("revision_id")] + public long RevisionId { get; set; } + + [JsonPropertyName("assigned_transponder")] + public string AssignedTransponder { get; set; } +} + +public partial class Prefile +{ + [JsonPropertyName("cid")] + public long Cid { get; set; } + + [JsonPropertyName("name")] + public string Name { get; set; } + + [JsonPropertyName("callsign")] + public string Callsign { get; set; } + + [JsonPropertyName("flight_plan")] + public FlightPlan FlightPlan { get; set; } + + [JsonPropertyName("last_updated")] + public DateTimeOffset LastUpdated { get; set; } +} + +public partial class ServerElement +{ + [JsonPropertyName("ident")] + public string Ident { get; set; } + + [JsonPropertyName("hostname_or_ip")] + public string HostnameOrIp { get; set; } + + [JsonPropertyName("location")] + public string Location { get; set; } + + [JsonPropertyName("name")] + public string Name { get; set; } + + [JsonPropertyName("clients_connection_allowed")] + public long ClientsConnectionAllowed { get; set; } + + [JsonPropertyName("client_connections_allowed")] + public bool ClientConnectionsAllowed { get; set; } + + [JsonPropertyName("is_sweatbox")] + public bool IsSweatbox { get; set; } +} + +public partial class AtcSchedule +{ + [JsonPropertyName("callsign")] + public string Callsign { get; set; } + + [JsonPropertyName("start")] + public DateTimeOffset Start { get; set; } + + [JsonPropertyName("finish")] + public DateTimeOffset Finish { get; set; } + + [JsonPropertyName("remark")] + public string Remark { get; set; } + + [JsonPropertyName("created_at")] + public DateTimeOffset CreatedAt { get; set; } + + [JsonPropertyName("updated_at")] + public DateTimeOffset UpdatedAt { get; set; } + + [JsonPropertyName("user")] + public User User { get; set; } +} + +public partial class User +{ + [JsonPropertyName("id")] + public long Id { get; set; } + + [JsonPropertyName("first_name")] + public string FirstName { get; set; } + + [JsonPropertyName("last_name")] + public string LastName { get; set; } +} diff --git a/Net.Vatprc.Uniapi/Services/VatsimService.cs b/Net.Vatprc.Uniapi/Services/VatsimService.cs new file mode 100644 index 0000000..3ee840b --- /dev/null +++ b/Net.Vatprc.Uniapi/Services/VatsimService.cs @@ -0,0 +1,41 @@ +using System.Diagnostics; +using Flurl; +using Flurl.Http; +using Microsoft.Extensions.Caching.Memory; + +namespace Net.Vatprc.Uniapi.Services; + +public class VatsimService +{ + MemoryCache Cache { get; init; } = new(new MemoryCacheOptions()); + + public static WebApplicationBuilder ConfigureOn(WebApplicationBuilder builder) + { + builder.Services.AddSingleton(); + return builder; + } + + public async Task GetOnlineData() + { + var result = await Cache.GetOrCreateAsync("vatsim-data", async entry => + { + entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(1); + return await "https://data.vatsim.net/v3/vatsim-data.json" + .WithHeader("User-Agent", UniapiUserAgent) + .GetJsonAsync(); + }) ?? throw new Exception("Unexpected null on fetch vatsim data"); + return result; + } + + public async Task> GetAtcSchedule() + { + var result = await Cache.GetOrCreateAsync("schedule", async entry => + { + entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(1); + return await "https://atcapi.vatprc.net/v1/public/schedules" + .WithHeader("User-Agent", UniapiUserAgent) + .GetJsonAsync>(); + }) ?? throw new Exception("Unexpected null on fetch vatprc data"); + return result; + } +} diff --git a/Net.Vatprc.Uniapi/Utils/Utils.cs b/Net.Vatprc.Uniapi/Utils/Utils.cs new file mode 100644 index 0000000..78c4cac --- /dev/null +++ b/Net.Vatprc.Uniapi/Utils/Utils.cs @@ -0,0 +1,8 @@ +using Flurl.Http; + +namespace Net.Vatprc.Uniapi.Utils; + +public static class Utils +{ + public static readonly string UniapiUserAgent = $"Flurl.Http/{typeof(FlurlClient).Assembly.GetName().Version} {System.Reflection.Assembly.GetExecutingAssembly().GetName().Name}/{System.Reflection.Assembly.GetExecutingAssembly().GetName().Version}"; +}