Skip to content

Commit 362a18f

Browse files
authored
Merge pull request #363 from immichFrame/dev/jw/calendarauth
Feat: basic auth support for calendar (optional)
2 parents 04b5daf + 3a41d2a commit 362a18f

3 files changed

Lines changed: 59 additions & 19 deletions

File tree

Lines changed: 50 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,23 @@
1+
using System.Net.Http.Headers;
2+
using System.Text;
13
using Ical.Net;
24
using ImmichFrame.Core.Helpers;
35
using ImmichFrame.Core.Interfaces;
46
using ImmichFrame.WebApi.Helpers;
7+
using Microsoft.Extensions.Logging;
58

69
public class IcalCalendarService : ICalendarService
710
{
811
private readonly IGeneralSettings _serverSettings;
9-
private readonly IApiCache _appointmentCache = new ApiCache(TimeSpan.FromMinutes(15));
12+
private readonly ILogger<IcalCalendarService> _logger;
13+
private readonly IHttpClientFactory _httpClientFactory;
14+
private readonly ApiCache _appointmentCache = new(TimeSpan.FromMinutes(15));
1015

11-
public IcalCalendarService(IGeneralSettings serverSettings)
16+
public IcalCalendarService(IGeneralSettings serverSettings, ILogger<IcalCalendarService> logger, IHttpClientFactory httpClientFactory)
1217
{
18+
_logger = logger;
1319
_serverSettings = serverSettings;
20+
_httpClientFactory = httpClientFactory;
1421
}
1522

1623
public async Task<List<IAppointment>> GetAppointments()
@@ -19,7 +26,26 @@ public async Task<List<IAppointment>> GetAppointments()
1926
{
2027
var appointments = new List<IAppointment>();
2128

22-
var icals = await GetCalendars(_serverSettings.Webcalendars);
29+
List<(string? auth, string url)> cals = _serverSettings.Webcalendars.Select<string, (string? auth, string url)?>(x =>
30+
{
31+
try
32+
{
33+
var uri = new Uri(x.Replace("webcal://", "https://"));
34+
if (!string.IsNullOrEmpty(uri.UserInfo))
35+
{
36+
var url = uri.GetComponents(UriComponents.AbsoluteUri & ~UriComponents.UserInfo, UriFormat.UriEscaped);
37+
return (Uri.UnescapeDataString(uri.UserInfo), url);
38+
}
39+
return (null, x);
40+
}
41+
catch (UriFormatException)
42+
{
43+
_logger.LogError($"Invalid calendar URL: '{x}'");
44+
return null;
45+
}
46+
}).Where(x => x != null).Select(x => x!.Value).ToList();
47+
48+
var icals = await GetCalendars(cals);
2349

2450
foreach (var ical in icals)
2551
{
@@ -32,29 +58,36 @@ public async Task<List<IAppointment>> GetAppointments()
3258
});
3359
}
3460

35-
public async Task<List<string>> GetCalendars(IEnumerable<string> calendars)
61+
public async Task<List<string>> GetCalendars(IEnumerable<(string? auth, string url)> calendars)
3662
{
3763
var icals = new List<string>();
64+
var client = _httpClientFactory.CreateClient();
3865

39-
foreach (var webcal in calendars)
66+
foreach (var calendar in calendars)
4067
{
41-
string httpUrl = webcal.Replace("webcal://", "https://");
68+
_logger.LogDebug($"Loading calendar: {(calendar.auth != null ? "[authenticated]" : "no auth")} - {calendar.url}");
69+
70+
string httpUrl = calendar.url.Replace("webcal://", "https://");
4271

43-
using (HttpClient client = new HttpClient())
72+
using var request = new HttpRequestMessage(HttpMethod.Get, httpUrl);
73+
74+
if (!string.IsNullOrEmpty(calendar.auth))
4475
{
45-
HttpResponseMessage response = await client.GetAsync(httpUrl);
46-
if (response.IsSuccessStatusCode)
47-
{
48-
icals.Add(await response.Content.ReadAsStringAsync());
49-
}
50-
else
51-
{
52-
throw new Exception("Failed to load calendar data");
53-
}
76+
var byteArray = Encoding.UTF8.GetBytes(calendar.auth);
77+
request.Headers.Authorization = new AuthenticationHeaderValue("Basic", Convert.ToBase64String(byteArray));
78+
}
79+
80+
using var response = await client.SendAsync(request);
81+
if (response.IsSuccessStatusCode)
82+
{
83+
icals.Add(await response.Content.ReadAsStringAsync());
84+
}
85+
else
86+
{
87+
_logger.LogError($"Failed to load calendar data from '{httpUrl}' (Status: {response.StatusCode})");
5488
}
5589
}
5690

5791
return icals;
5892
}
59-
6093
}

docker/example.env

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ ApiKey=KEY
2222
# Albums=ALBUM1,ALBUM2
2323
# ExcludedAlbums=ALBUM3,ALBUM4
2424
# People=PERSON1,PERSON2
25-
# Webcalendars=https://calendar.mycalendar.com/basic.ics,webcal://calendar.mycalendar.com/basic.ics
25+
# Webcalendars=https://calendar.google.com/calendar/ical/XXXXXX/public/basic.ics,https://user:pass@calendar.immichframe.dev/dav/calendars/basic.ics
2626
# RefreshAlbumPeopleInterval=12
2727
# ShowClock=true
2828
# ClockFormat=hh:mm

docs/docs/getting-started/configuration.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,9 @@ General:
3838
DownloadImages: false # boolean
3939
# if images are downloaded, re-download if age (in days) is more than this
4040
RenewImagesDuration: 30 # int
41-
# A list of webcalendar URIs in the .ics format. e.g. https://calendar.google.com/calendar/ical/XXXXXX/public/basic.ics
41+
# A list of webcalendar URIs in the .ics format. Supports basic auth via standard URL format.
42+
# e.g. https://calendar.google.com/calendar/ical/XXXXXX/public/basic.ics
43+
# e.g. https://user:pass@calendar.immichframe.dev/dav/calendars/basic.ics
4244
Webcalendars: # string[]
4345
- UUID
4446
# Interval in hours. Determines how often images are pulled from a person in immich.
@@ -159,6 +161,11 @@ Weather is enabled by entering an API key. Get yours free from [OpenWeatherMap][
159161
### Calendar
160162
If you are using Google Calendar, more information can be found [here](https://support.google.com/calendar/answer/37648?hl=en#zippy=%2Cget-your-calendar-view-only).
161163

164+
Calendar supports basic authentication using the standard URL userinfo format:
165+
Example:
166+
No Auth: `https://calendar.google.com/calendar/ical/XXXXXX/public/basic.ics`
167+
With Auth: `https://username:password@calendar.immichframe.dev/dav/calendars/basic.ics`
168+
162169
### Misc
163170
#### Webhook
164171
A webhook to notify an external service is available. This is only enabled when the `Webhook`-Setting is set in your configuration. Your configured Webhook will be notified via `HTTP POST`-request.

0 commit comments

Comments
 (0)