Files

81 lines
4.4 KiB
C#

using System.Net;
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
using System.Text.Json.Nodes;
using Volcanic.AgenticCore.Abstractions.Json;
namespace Volcanic.AgenticCore.Extensions.GoogleCalendar;
internal readonly record struct CalendarCall(string Action, string CalendarId, string? EventId = null);
internal sealed record PrimaryCalendar(string? Account, string? TimeZone);
/// <summary>Calendar v3 over raw HTTP for one chat: bearer token from the connection, one refresh-and-retry on 401.</summary>
internal sealed class GoogleCalendarApi(GoogleCalendarConnection connection)
{
public const string BaseUrl = "https://www.googleapis.com/calendar/v3/";
public static string Events(string calendarId) => $"calendars/{Uri.EscapeDataString(calendarId)}/events";
public static string Event(string calendarId, string eventId) => $"{Events(calendarId)}/{Uri.EscapeDataString(eventId)}";
public static string Query(IEnumerable<KeyValuePair<string, string>> pairs) =>
string.Join("&", pairs.Select(p => $"{Uri.EscapeDataString(p.Key)}={Uri.EscapeDataString(p.Value)}"));
public async Task<JsonElement> SendAsync(HttpMethod method, string pathAndQuery, JsonObject? body, CalendarCall call, CancellationToken cancellationToken)
{
var http = connection.Settings.Http;
var response = await SendOnceAsync(http, method, pathAndQuery, body,
await connection.GetAccessTokenAsync(cancellationToken).ConfigureAwait(false), cancellationToken).ConfigureAwait(false);
if (response.StatusCode == HttpStatusCode.Unauthorized)
{
response.Dispose();
connection.Invalidate();
response = await SendOnceAsync(http, method, pathAndQuery, body,
await connection.GetAccessTokenAsync(cancellationToken).ConfigureAwait(false), cancellationToken).ConfigureAwait(false);
}
using (response)
{
var text = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
if (response.IsSuccessStatusCode) return string.IsNullOrWhiteSpace(text) ? VolcanicJson.EmptyObject : VolcanicJson.Parse(text);
if (response.StatusCode is HttpStatusCode.NotFound or HttpStatusCode.Gone && call.EventId is not null)
throw new GoogleCalendarException($"Event {call.EventId} not found in {call.CalendarId}.");
throw new GoogleCalendarException($"Google Calendar {call.Action} failed: {(int)response.StatusCode} {ErrorMessage(text) ?? response.ReasonPhrase}");
}
}
/// <summary>The account (primary calendar id = email) and zone, with a token that isn't stored yet.</summary>
public static async Task<PrimaryCalendar> GetPrimaryAsync(HttpClient http, string accessToken, CancellationToken cancellationToken)
{
using var response = await SendOnceAsync(http, HttpMethod.Get, "calendars/primary", null, accessToken, cancellationToken).ConfigureAwait(false);
var text = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
if (!response.IsSuccessStatusCode)
throw new GoogleCalendarException($"calendars/primary failed: {(int)response.StatusCode} {ErrorMessage(text) ?? response.ReasonPhrase}");
var json = VolcanicJson.Parse(text);
return new PrimaryCalendar(
json.TryGetProperty("id", out var id) ? id.GetString() : null,
json.TryGetProperty("timeZone", out var zone) ? zone.GetString() : null);
}
private static async Task<HttpResponseMessage> SendOnceAsync(HttpClient http, HttpMethod method, string pathAndQuery, JsonObject? body, string accessToken, CancellationToken cancellationToken)
{
using var request = new HttpRequestMessage(method, BaseUrl + pathAndQuery);
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
if (body is not null) request.Content = new StringContent(body.ToJsonString(VolcanicJson.Options), Encoding.UTF8, "application/json");
return await http.SendAsync(request, cancellationToken).ConfigureAwait(false);
}
private static string? ErrorMessage(string text)
{
try
{
return JsonNode.Parse(text) is JsonObject o && o["error"] is JsonObject e && e["message"] is JsonValue m && m.TryGetValue<string>(out var s) ? s : null;
}
catch (JsonException)
{
return null;
}
}
}