fix: confirm email changes and enforce clean backend build
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
using System.Text;
|
||||
using System.Globalization;
|
||||
|
||||
namespace Socialize.Api.Modules.CalendarIntegrations.Services;
|
||||
|
||||
@@ -11,9 +12,9 @@ internal sealed record CalendarExportFeedEvent(
|
||||
string? Description,
|
||||
string? Url);
|
||||
|
||||
internal class CalendarExportFeedBuilder
|
||||
internal static class CalendarExportFeedBuilder
|
||||
{
|
||||
public string Build(string calendarName, IReadOnlyCollection<CalendarExportFeedEvent> events)
|
||||
public static string Build(string calendarName, IReadOnlyCollection<CalendarExportFeedEvent> events)
|
||||
{
|
||||
StringBuilder builder = new();
|
||||
builder.AppendLine("BEGIN:VCALENDAR");
|
||||
@@ -21,34 +22,34 @@ internal class CalendarExportFeedBuilder
|
||||
builder.AppendLine("PRODID:-//Socialize//User Work Calendar//EN");
|
||||
builder.AppendLine("CALSCALE:GREGORIAN");
|
||||
builder.AppendLine("METHOD:PUBLISH");
|
||||
builder.AppendLine($"X-WR-CALNAME:{EscapeText(calendarName)}");
|
||||
AppendLineInvariant(builder, $"X-WR-CALNAME:{EscapeText(calendarName)}");
|
||||
|
||||
foreach (CalendarExportFeedEvent feedEvent in events.OrderBy(calendarEvent => calendarEvent.StartsAt))
|
||||
{
|
||||
builder.AppendLine("BEGIN:VEVENT");
|
||||
builder.AppendLine($"UID:{EscapeText(feedEvent.Uid)}");
|
||||
builder.AppendLine($"DTSTAMP:{FormatUtc(DateTimeOffset.UtcNow)}");
|
||||
builder.AppendLine($"SUMMARY:{EscapeText(feedEvent.Title)}");
|
||||
AppendLineInvariant(builder, $"UID:{EscapeText(feedEvent.Uid)}");
|
||||
AppendLineInvariant(builder, $"DTSTAMP:{FormatUtc(DateTimeOffset.UtcNow)}");
|
||||
AppendLineInvariant(builder, $"SUMMARY:{EscapeText(feedEvent.Title)}");
|
||||
|
||||
if (feedEvent.IsAllDay)
|
||||
{
|
||||
builder.AppendLine($"DTSTART;VALUE=DATE:{FormatDate(feedEvent.StartsAt)}");
|
||||
builder.AppendLine($"DTEND;VALUE=DATE:{FormatDate(feedEvent.EndsAt)}");
|
||||
AppendLineInvariant(builder, $"DTSTART;VALUE=DATE:{FormatDate(feedEvent.StartsAt)}");
|
||||
AppendLineInvariant(builder, $"DTEND;VALUE=DATE:{FormatDate(feedEvent.EndsAt)}");
|
||||
}
|
||||
else
|
||||
{
|
||||
builder.AppendLine($"DTSTART:{FormatUtc(feedEvent.StartsAt)}");
|
||||
builder.AppendLine($"DTEND:{FormatUtc(feedEvent.EndsAt)}");
|
||||
AppendLineInvariant(builder, $"DTSTART:{FormatUtc(feedEvent.StartsAt)}");
|
||||
AppendLineInvariant(builder, $"DTEND:{FormatUtc(feedEvent.EndsAt)}");
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(feedEvent.Description))
|
||||
{
|
||||
builder.AppendLine($"DESCRIPTION:{EscapeText(feedEvent.Description)}");
|
||||
AppendLineInvariant(builder, $"DESCRIPTION:{EscapeText(feedEvent.Description)}");
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(feedEvent.Url))
|
||||
{
|
||||
builder.AppendLine($"URL:{EscapeText(feedEvent.Url)}");
|
||||
AppendLineInvariant(builder, $"URL:{EscapeText(feedEvent.Url)}");
|
||||
}
|
||||
|
||||
builder.AppendLine("END:VEVENT");
|
||||
@@ -71,10 +72,15 @@ internal class CalendarExportFeedBuilder
|
||||
private static string EscapeText(string value)
|
||||
{
|
||||
return value
|
||||
.Replace("\\", "\\\\")
|
||||
.Replace("\r\n", "\\n")
|
||||
.Replace("\n", "\\n")
|
||||
.Replace(";", "\\;")
|
||||
.Replace(",", "\\,");
|
||||
.Replace("\\", "\\\\", StringComparison.Ordinal)
|
||||
.Replace("\r\n", "\\n", StringComparison.Ordinal)
|
||||
.Replace("\n", "\\n", StringComparison.Ordinal)
|
||||
.Replace(";", "\\;", StringComparison.Ordinal)
|
||||
.Replace(",", "\\,", StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
private static void AppendLineInvariant(StringBuilder builder, FormattableString value)
|
||||
{
|
||||
builder.AppendLine(value.ToString(CultureInfo.InvariantCulture));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,11 +3,11 @@ using Socialize.Api.Data;
|
||||
|
||||
namespace Socialize.Api.Modules.CalendarIntegrations.Services;
|
||||
|
||||
internal class CalendarExportFeedService(AppDbContext dbContext, CalendarExportFeedBuilder feedBuilder)
|
||||
internal class CalendarExportFeedService(AppDbContext dbContext)
|
||||
{
|
||||
public async Task<string> BuildUserFeedAsync(Guid userId, string? userEmail, string appBaseUrl, CancellationToken ct)
|
||||
{
|
||||
string normalizedEmail = userEmail?.Trim().ToUpperInvariant() ?? string.Empty;
|
||||
string normalizedEmail = userEmail?.Trim() ?? string.Empty;
|
||||
Guid[] workspaceIds = await dbContext.Workspaces
|
||||
.Where(workspace =>
|
||||
workspace.OwnerUserId == userId ||
|
||||
@@ -51,7 +51,7 @@ internal class CalendarExportFeedService(AppDbContext dbContext, CalendarExportF
|
||||
.Where(approval =>
|
||||
approval.DueAt.HasValue &&
|
||||
(approval.RequestedByUserId == userId ||
|
||||
(!string.IsNullOrEmpty(normalizedEmail) && approval.ReviewerEmail.ToUpper() == normalizedEmail)))
|
||||
(!string.IsNullOrEmpty(normalizedEmail) && EF.Functions.ILike(approval.ReviewerEmail, normalizedEmail))))
|
||||
.Join(
|
||||
dbContext.ContentItems,
|
||||
approval => approval.ContentItemId,
|
||||
@@ -91,7 +91,7 @@ internal class CalendarExportFeedService(AppDbContext dbContext, CalendarExportF
|
||||
appBaseUrl))
|
||||
.ToListAsync(ct));
|
||||
|
||||
return feedBuilder.Build("Socialize my work", events);
|
||||
return CalendarExportFeedBuilder.Build("Socialize my work", events);
|
||||
}
|
||||
|
||||
private static CalendarExportFeedEvent ToContentFeedEvent(
|
||||
|
||||
@@ -23,12 +23,15 @@ internal sealed class CalendarImportBackgroundService(
|
||||
CalendarImportSyncService syncService = scope.ServiceProvider.GetRequiredService<CalendarImportSyncService>();
|
||||
await syncService.RefreshDueSourcesAsync(stoppingToken);
|
||||
}
|
||||
catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
|
||||
catch (OperationCanceledException ex) when (stoppingToken.IsCancellationRequested)
|
||||
{
|
||||
logger.LogDebug(ex, "Calendar import background sync stopped.");
|
||||
}
|
||||
#pragma warning disable CA1031 // Background service should log and continue after unexpected sync failures.
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Calendar import background sync failed.");
|
||||
}
|
||||
#pragma warning restore CA1031
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Socialize.Api.Data;
|
||||
using Socialize.Api.Modules.CalendarIntegrations.Data;
|
||||
using System.Globalization;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Socialize.Api.Modules.CalendarIntegrations.Services;
|
||||
|
||||
#pragma warning disable S1075 // Supplemental observance identifiers intentionally use stable URI-like values.
|
||||
|
||||
internal sealed class CalendarImportSyncService(
|
||||
AppDbContext dbContext,
|
||||
IHttpClientFactory httpClientFactory,
|
||||
IcsCalendarParser parser)
|
||||
IHttpClientFactory httpClientFactory)
|
||||
{
|
||||
private static readonly JsonSerializerOptions JsonSerializerOptions = new(JsonSerializerDefaults.Web);
|
||||
|
||||
public async Task RefreshSourceAsync(Guid sourceId, CancellationToken ct)
|
||||
{
|
||||
CalendarSource? source = await dbContext.CalendarSources
|
||||
@@ -115,7 +119,7 @@ internal sealed class CalendarImportSyncService(
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<IReadOnlyCollection<ParsedCalendarEvent>> GetParsedEventsAsync(
|
||||
private static async Task<IReadOnlyCollection<ParsedCalendarEvent>> GetParsedEventsAsync(
|
||||
HttpClient httpClient,
|
||||
string sourceUrl,
|
||||
DateOnly rangeStart,
|
||||
@@ -127,8 +131,8 @@ internal sealed class CalendarImportSyncService(
|
||||
return await GetNagerEventsAsync(httpClient, sourceUrl, countryCode!, rangeStart, rangeEnd, ct);
|
||||
}
|
||||
|
||||
string content = await httpClient.GetStringAsync(sourceUrl, ct);
|
||||
return parser.Parse(content, rangeStart, rangeEnd);
|
||||
string content = await httpClient.GetStringAsync(new Uri(sourceUrl), ct);
|
||||
return IcsCalendarParser.Parse(content, rangeStart, rangeEnd);
|
||||
}
|
||||
|
||||
private static async Task<IReadOnlyCollection<ParsedCalendarEvent>> GetNagerEventsAsync(
|
||||
@@ -143,14 +147,12 @@ internal sealed class CalendarImportSyncService(
|
||||
for (int year = rangeStart.Year; year <= rangeEnd.Year; year++)
|
||||
{
|
||||
string yearUrl = BuildNagerYearUrl(sourceUrl, countryCode, year);
|
||||
string json = await httpClient.GetStringAsync(yearUrl, ct);
|
||||
NagerHoliday[] holidays = JsonSerializer.Deserialize<NagerHoliday[]>(
|
||||
json,
|
||||
new JsonSerializerOptions(JsonSerializerDefaults.Web)) ?? [];
|
||||
string json = await httpClient.GetStringAsync(new Uri(yearUrl), ct);
|
||||
NagerHoliday[] holidays = JsonSerializer.Deserialize<NagerHoliday[]>(json, JsonSerializerOptions) ?? [];
|
||||
|
||||
foreach (NagerHoliday holiday in holidays)
|
||||
{
|
||||
if (!DateOnly.TryParse(holiday.Date, out DateOnly date) ||
|
||||
if (!DateOnly.TryParse(holiday.Date, CultureInfo.InvariantCulture, out DateOnly date) ||
|
||||
date < rangeStart ||
|
||||
date > rangeEnd)
|
||||
{
|
||||
@@ -283,7 +285,7 @@ internal sealed class CalendarImportSyncService(
|
||||
private static string NormalizeUidPart(string? value)
|
||||
{
|
||||
return new string((value ?? "holiday")
|
||||
.ToLowerInvariant()
|
||||
.ToUpperInvariant()
|
||||
.Select(character => char.IsLetterOrDigit(character) ? character : '-')
|
||||
.ToArray())
|
||||
.Trim('-');
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
|
||||
namespace Socialize.Api.Modules.CalendarIntegrations.Services;
|
||||
|
||||
@@ -39,9 +40,9 @@ internal sealed record IcsRawEvent(
|
||||
string? SourceUrl,
|
||||
DateTimeOffset? LastModifiedAt);
|
||||
|
||||
internal sealed class IcsCalendarParser
|
||||
internal static class IcsCalendarParser
|
||||
{
|
||||
public IReadOnlyCollection<ParsedCalendarEvent> Parse(
|
||||
public static IReadOnlyCollection<ParsedCalendarEvent> Parse(
|
||||
string content,
|
||||
DateOnly rangeStart,
|
||||
DateOnly rangeEnd)
|
||||
@@ -63,10 +64,12 @@ internal sealed class IcsCalendarParser
|
||||
private static IEnumerable<IcsRawEvent> ReadRawEvents(string content)
|
||||
{
|
||||
List<string> lines = UnfoldLines(content).ToList();
|
||||
for (int index = 0; index < lines.Count; index++)
|
||||
int index = 0;
|
||||
while (index < lines.Count)
|
||||
{
|
||||
if (!lines[index].Equals("BEGIN:VEVENT", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
index++;
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -74,9 +77,10 @@ internal sealed class IcsCalendarParser
|
||||
new(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
index++;
|
||||
for (; index < lines.Count && !lines[index].Equals("END:VEVENT", StringComparison.OrdinalIgnoreCase); index++)
|
||||
while (index < lines.Count && !lines[index].Equals("END:VEVENT", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
ParseProperty(lines[index], properties);
|
||||
index++;
|
||||
}
|
||||
|
||||
if (!TryGetFirst(properties, "DTSTART", out var startProperty))
|
||||
@@ -105,32 +109,34 @@ internal sealed class IcsCalendarParser
|
||||
TryGetFirst(properties, "LAST-MODIFIED", out var lastModified)
|
||||
? ParseDateTimeValue(lastModified.Value, lastModified.Parameters).UtcDateTime
|
||||
: null);
|
||||
|
||||
index++;
|
||||
}
|
||||
}
|
||||
|
||||
private static IEnumerable<string> UnfoldLines(string content)
|
||||
{
|
||||
string? current = null;
|
||||
StringBuilder? current = null;
|
||||
using StringReader reader = new(content.Replace("\r\n", "\n", StringComparison.Ordinal).Replace('\r', '\n'));
|
||||
while (reader.ReadLine() is { } line)
|
||||
{
|
||||
if ((line.StartsWith(' ') || line.StartsWith('\t')) && current is not null)
|
||||
{
|
||||
current += line[1..];
|
||||
current.Append(line[1..]);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (current is not null)
|
||||
{
|
||||
yield return current;
|
||||
yield return current.ToString();
|
||||
}
|
||||
|
||||
current = line;
|
||||
current = new StringBuilder(line);
|
||||
}
|
||||
|
||||
if (current is not null)
|
||||
{
|
||||
yield return current;
|
||||
yield return current.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -309,7 +315,7 @@ internal sealed class IcsCalendarParser
|
||||
return TimeSpan.Zero;
|
||||
}
|
||||
|
||||
private static IReadOnlyCollection<DateOnly> ExpandStartDates(
|
||||
private static List<DateOnly> ExpandStartDates(
|
||||
IcsRawEvent rawEvent,
|
||||
DateOnly rangeStart,
|
||||
DateOnly rangeEnd)
|
||||
|
||||
Reference in New Issue
Block a user