feat(calendar): add application date integrations

This commit is contained in:
cesnimda
2026-08-31 22:24:38 +02:00
parent caf486dceb
commit 1152e05687
10 changed files with 547 additions and 2 deletions
+44 -1
View File
@@ -25,6 +25,7 @@ public interface IGmailOAuthService
Task<IReadOnlyList<GmailMessageSummary>> ListThreadMessagesAsync(string ownerUserId, string threadId, CancellationToken cancellationToken);
Task<GmailMessageDetail> GetMessageAsync(string ownerUserId, string messageId, CancellationToken cancellationToken);
Task<GmailSendResult> SendAsync(string ownerUserId, GmailSendRequest request, CancellationToken cancellationToken);
Task<ExternalCalendarEventResult> CreateCalendarEventAsync(string ownerUserId, ExternalCalendarEventRequest request, CancellationToken cancellationToken);
}
public sealed record GmailOAuthExchangeResult(string GmailAddress);
@@ -47,7 +48,8 @@ internal sealed class GmailTokenResponse
public sealed class GmailOAuthService : IGmailOAuthService
{
public const string SendScope = "https://www.googleapis.com/auth/gmail.send";
private const string Scope = $"openid email profile https://www.googleapis.com/auth/gmail.readonly {SendScope}";
public const string CalendarScope = "https://www.googleapis.com/auth/calendar.events";
private const string Scope = $"openid email profile https://www.googleapis.com/auth/gmail.readonly {SendScope} {CalendarScope}";
private readonly IConfiguration _cfg;
private readonly JobTrackerContext _db;
private readonly IDataProtector _protector;
@@ -419,6 +421,47 @@ public sealed class GmailOAuthService : IGmailOAuthService
public static bool HasSendScope(string? scope) =>
!string.IsNullOrWhiteSpace(scope) && scope.Split(' ', StringSplitOptions.RemoveEmptyEntries).Contains(SendScope, StringComparer.OrdinalIgnoreCase);
public async Task<ExternalCalendarEventResult> CreateCalendarEventAsync(string ownerUserId, ExternalCalendarEventRequest request, CancellationToken cancellationToken)
{
var connection = await _db.GmailConnections.AsNoTracking().FirstOrDefaultAsync(item => item.OwnerUserId == ownerUserId, cancellationToken);
if (connection is null || !HasCalendarScope(connection.Scope))
throw new InvalidOperationException("Reconnect Google and approve calendar access before adding events.");
var payload = request.AllDay
? JsonSerializer.Serialize(new
{
summary = request.Summary,
description = request.Description,
location = request.Location,
start = new { date = request.StartsAtUtc.UtcDateTime.ToString("yyyy-MM-dd") },
end = new { date = request.EndsAtUtc.UtcDateTime.ToString("yyyy-MM-dd") }
})
: JsonSerializer.Serialize(new
{
summary = request.Summary,
description = request.Description,
location = request.Location,
start = new { dateTime = request.StartsAtUtc.ToString("O") },
end = new { dateTime = request.EndsAtUtc.ToString("O") }
});
var accessToken = await GetValidAccessTokenAsync(ownerUserId, cancellationToken);
var client = _httpClientFactory.CreateClient();
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
using var response = await client.PutAsync(
$"https://www.googleapis.com/calendar/v3/calendars/primary/events/{Uri.EscapeDataString(request.StableId)}",
new StringContent(payload, Encoding.UTF8, "application/json"),
cancellationToken);
response.EnsureSuccessStatusCode();
using var document = await JsonDocument.ParseAsync(await response.Content.ReadAsStreamAsync(cancellationToken), cancellationToken: cancellationToken);
return new ExternalCalendarEventResult(
document.RootElement.TryGetProperty("id", out var id) ? id.GetString() : null,
document.RootElement.TryGetProperty("htmlLink", out var link) ? link.GetString() : null);
}
public static bool HasCalendarScope(string? scope) =>
!string.IsNullOrWhiteSpace(scope) && scope.Split(' ', StringSplitOptions.RemoveEmptyEntries).Contains(CalendarScope, StringComparer.OrdinalIgnoreCase);
private static void ValidateSendRequest(string to, string subject, string bodyText)
{
if (string.IsNullOrWhiteSpace(to)) throw new ArgumentException("Recipient is required.", nameof(to));