2021-07-03 12:19:53 +00:00
|
|
|
|
using System;
|
|
|
|
|
using System.Collections.Generic;
|
|
|
|
|
using System.ComponentModel.DataAnnotations;
|
|
|
|
|
using System.Globalization;
|
|
|
|
|
using System.Net.Http;
|
|
|
|
|
using System.Text;
|
|
|
|
|
using System.Threading.Tasks;
|
|
|
|
|
using System.Xml;
|
|
|
|
|
|
|
|
|
|
namespace BlazorApp.Data
|
|
|
|
|
{
|
|
|
|
|
public class Event
|
|
|
|
|
{
|
|
|
|
|
public string Uid { get; set; }
|
|
|
|
|
public string Summary { get; set; }
|
|
|
|
|
public DateTime? DtStart { get; set; }
|
|
|
|
|
public DateTime? DtEnd { get; set; }
|
|
|
|
|
public string Description { get; set; }
|
2021-07-04 12:36:21 +00:00
|
|
|
|
public TimeSpan? Duration { get; set; }
|
|
|
|
|
|
2021-07-04 12:37:57 +00:00
|
|
|
|
public DateTime? CalculatedEnd => DtEnd ?? DtStart + Duration;
|
2021-07-03 12:19:53 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public class Calendar
|
|
|
|
|
{
|
|
|
|
|
public int Id { get; set; }
|
|
|
|
|
|
|
|
|
|
[Required] public string Url { get; set; }
|
|
|
|
|
|
|
|
|
|
public string Username { get; set; }
|
|
|
|
|
|
|
|
|
|
public string Password { get; set; }
|
|
|
|
|
|
2021-07-04 10:28:21 +00:00
|
|
|
|
[Required] public string Name { get; set; }
|
2021-07-03 12:19:53 +00:00
|
|
|
|
|
2021-07-04 10:28:21 +00:00
|
|
|
|
private string AuthorizationHeader => Username == null || Password == null
|
|
|
|
|
? null
|
|
|
|
|
: $"Basic {Convert.ToBase64String(Encoding.UTF8.GetBytes($"{Username}:{Password}"))}";
|
2021-07-03 12:19:53 +00:00
|
|
|
|
|
|
|
|
|
private static DateTime _parseDateTime(string s)
|
|
|
|
|
{
|
|
|
|
|
var format = s.Contains('T') ? "yyyyMMddTHHmmssK" : "yyyyMMdd";
|
|
|
|
|
return DateTime.ParseExact(s, format, CultureInfo.InvariantCulture);
|
|
|
|
|
}
|
|
|
|
|
|
2021-07-04 12:36:21 +00:00
|
|
|
|
/// <summary>
|
|
|
|
|
/// Very non-strict parser of durations as specified in
|
|
|
|
|
/// https://datatracker.ietf.org/doc/html/rfc5545#section-3.3.6
|
|
|
|
|
/// </summary>
|
|
|
|
|
private static TimeSpan? _parseDuration(string s)
|
|
|
|
|
{
|
|
|
|
|
var timeSpan = new TimeSpan();
|
|
|
|
|
var positive = s[0] != '-';
|
|
|
|
|
var value = 0;
|
|
|
|
|
foreach (var c in s)
|
|
|
|
|
{
|
|
|
|
|
if (char.IsDigit(c))
|
|
|
|
|
{
|
|
|
|
|
value = value * 10 + (c - '0');
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
switch (c)
|
|
|
|
|
{
|
|
|
|
|
case 'W':
|
|
|
|
|
timeSpan += TimeSpan.FromDays(value * 7);
|
|
|
|
|
break;
|
|
|
|
|
case 'D':
|
|
|
|
|
timeSpan += TimeSpan.FromDays(value);
|
|
|
|
|
break;
|
|
|
|
|
case 'H':
|
|
|
|
|
timeSpan += TimeSpan.FromHours(value);
|
|
|
|
|
break;
|
|
|
|
|
case 'M':
|
|
|
|
|
timeSpan += TimeSpan.FromMinutes(value);
|
|
|
|
|
break;
|
|
|
|
|
case 'S':
|
|
|
|
|
timeSpan += TimeSpan.FromSeconds(value);
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
value = 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return positive ? timeSpan : -timeSpan;
|
|
|
|
|
}
|
|
|
|
|
|
2021-07-03 12:19:53 +00:00
|
|
|
|
private static List<Event> ParseICal(string iCal)
|
|
|
|
|
{
|
|
|
|
|
Event @event = null;
|
|
|
|
|
var events = new List<Event>();
|
|
|
|
|
|
|
|
|
|
// Un-wrap all lines from the input
|
|
|
|
|
var body = iCal
|
|
|
|
|
.Replace("\n ", "")
|
|
|
|
|
.Replace("\n\t", "")
|
|
|
|
|
.Replace("\r", "");
|
2021-07-04 10:28:21 +00:00
|
|
|
|
|
2021-07-03 12:19:53 +00:00
|
|
|
|
foreach (var line in body.Split("\n"))
|
|
|
|
|
{
|
|
|
|
|
var trimmed = line.Trim();
|
|
|
|
|
var split = trimmed.Split(':', 2);
|
|
|
|
|
if (split.Length != 2)
|
|
|
|
|
continue;
|
2021-07-04 10:28:21 +00:00
|
|
|
|
|
2021-07-03 12:19:53 +00:00
|
|
|
|
var left = split[0].Trim().Split(';');
|
|
|
|
|
var name = left[0];
|
|
|
|
|
|
|
|
|
|
// TODO Turn this into a regex
|
|
|
|
|
var value = split[1]
|
|
|
|
|
.Replace(@"\n", "\n")
|
|
|
|
|
.Replace(@"\N", "\n")
|
|
|
|
|
.Replace(@"\,", ",")
|
|
|
|
|
.Replace(@"\;", ";")
|
|
|
|
|
.Replace(@"\\", @"\");
|
|
|
|
|
|
|
|
|
|
if (name == "BEGIN" && value == "VEVENT")
|
|
|
|
|
{
|
|
|
|
|
// Start of a new event
|
|
|
|
|
@event = new Event();
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (@event == null)
|
|
|
|
|
{
|
|
|
|
|
// Event hasn't started yet, ignore the property.
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
switch (name)
|
|
|
|
|
{
|
|
|
|
|
case "END":
|
|
|
|
|
if (value == "VEVENT")
|
|
|
|
|
{
|
|
|
|
|
// End of the current event, add it to the list
|
|
|
|
|
events.Add(@event);
|
|
|
|
|
@event = null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
break;
|
|
|
|
|
case "UID":
|
|
|
|
|
@event.Uid = value;
|
|
|
|
|
break;
|
|
|
|
|
case "SUMMARY":
|
|
|
|
|
@event.Summary = value;
|
|
|
|
|
break;
|
|
|
|
|
case "DESCRIPTION":
|
|
|
|
|
@event.Description = value;
|
|
|
|
|
break;
|
|
|
|
|
case "DTSTART":
|
|
|
|
|
@event.DtStart = _parseDateTime(value);
|
|
|
|
|
break;
|
|
|
|
|
case "DTEND":
|
|
|
|
|
@event.DtEnd = _parseDateTime(value);
|
|
|
|
|
break;
|
2021-07-04 12:36:21 +00:00
|
|
|
|
case "DURATION":
|
|
|
|
|
@event.Duration = _parseDuration(value);
|
|
|
|
|
break;
|
2021-07-03 12:19:53 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return events;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* <summary>Retrieves and parses all events from the CalDav server.</summary>
|
|
|
|
|
*/
|
|
|
|
|
public async Task<List<Event>> GetEvents()
|
|
|
|
|
{
|
|
|
|
|
var client = new HttpClient();
|
2021-07-04 10:28:21 +00:00
|
|
|
|
if (AuthorizationHeader != null)
|
|
|
|
|
client.DefaultRequestHeaders.Add("Authorization", AuthorizationHeader);
|
2021-07-04 12:36:21 +00:00
|
|
|
|
|
2021-07-03 12:19:53 +00:00
|
|
|
|
var body = await client.GetStringAsync(Url);
|
|
|
|
|
var events = ParseICal(body);
|
2021-07-04 10:28:21 +00:00
|
|
|
|
|
2021-07-03 12:44:54 +00:00
|
|
|
|
var now = DateTime.Now;
|
|
|
|
|
events = events.FindAll(@event => @event.DtStart > now || @event.DtEnd > now);
|
2021-07-03 12:19:53 +00:00
|
|
|
|
|
|
|
|
|
events.Sort((x, y) =>
|
|
|
|
|
{
|
|
|
|
|
if (x.DtStart == null)
|
2021-07-03 12:44:54 +00:00
|
|
|
|
return -1;
|
|
|
|
|
if (y.DtStart == null)
|
2021-07-03 12:19:53 +00:00
|
|
|
|
return 1;
|
2021-07-04 10:28:21 +00:00
|
|
|
|
|
2021-07-03 12:44:54 +00:00
|
|
|
|
return DateTime.Compare(x.DtStart.Value, y.DtStart.Value);
|
2021-07-03 12:19:53 +00:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return events;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* <summary>Retrieves and parses an event with UID <code>uid</code> from the CalDav server.</summary>
|
|
|
|
|
*/
|
|
|
|
|
public async Task<Event> GetEventByUid(string uid)
|
|
|
|
|
{
|
|
|
|
|
var client = new HttpClient();
|
2021-07-04 10:28:21 +00:00
|
|
|
|
if (AuthorizationHeader != null)
|
|
|
|
|
client.DefaultRequestHeaders.Add("Authorization", AuthorizationHeader);
|
2021-07-03 12:19:53 +00:00
|
|
|
|
|
|
|
|
|
var request = new HttpRequestMessage(new HttpMethod("REPORT"), Url);
|
|
|
|
|
request.Headers.Add("Depth", "1");
|
2021-07-04 10:28:21 +00:00
|
|
|
|
|
2021-07-03 12:19:53 +00:00
|
|
|
|
// TODO Use XML generator to safely add UID - this is sensitive to scary injection stuff
|
|
|
|
|
request.Content = new StringContent(@$"<?xml version=""1.0"" encoding=""utf-8"" ?>
|
|
|
|
|
<C:calendar-query xmlns:C=""urn:ietf:params:xml:ns:caldav"">
|
|
|
|
|
<D:prop xmlns:D=""DAV:"">
|
|
|
|
|
<D:getetag/>
|
|
|
|
|
<C:calendar-data/>
|
|
|
|
|
</D:prop>
|
|
|
|
|
<C:filter>
|
|
|
|
|
<C:comp-filter name=""VCALENDAR"">
|
|
|
|
|
<C:comp-filter name=""VEVENT"">
|
|
|
|
|
<C:prop-filter name=""UID"">
|
|
|
|
|
<C:text-match collation=""i;octet""
|
|
|
|
|
>{uid}</C:text-match>
|
|
|
|
|
</C:prop-filter>
|
|
|
|
|
</C:comp-filter>
|
|
|
|
|
</C:comp-filter>
|
|
|
|
|
</C:filter>
|
|
|
|
|
</C:calendar-query>
|
|
|
|
|
", Encoding.UTF8, "application/xml");
|
|
|
|
|
|
|
|
|
|
var result = await client.SendAsync(request);
|
|
|
|
|
var body = await result.Content.ReadAsStreamAsync();
|
|
|
|
|
|
|
|
|
|
var reader = new XmlTextReader(body);
|
|
|
|
|
var document = new XmlDocument();
|
|
|
|
|
document.Load(reader);
|
|
|
|
|
|
|
|
|
|
var node = document.GetElementsByTagName("C:calendar-data")[0]?.InnerText;
|
|
|
|
|
var events = ParseICal(node);
|
|
|
|
|
return events.Count > 0 ? events[0] : null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|