forked from admin/VolcanicAgenticCore
81 lines
4.4 KiB
C#
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;
|
|
}
|
|
}
|
|
}
|