Skip to content

Commit 0c9a50f

Browse files
committed
Calender sync integration
1 parent ddba6cb commit 0c9a50f

18 files changed

+607
-131
lines changed

Daemons/vmpserverd/meson.build

+1-2
Original file line numberDiff line numberDiff line change
@@ -73,8 +73,7 @@ source = [
7373
'src/VMPRecordingManager.m',
7474
'src/VMPErrors.m',
7575
'src/VMPJournal.m',
76-
'src/VMPCalEvent.m',
77-
'src/VMPCalSync.m',
76+
'src/VMPCalendarSync.m',
7877
'src/NSString+substituteVariables.m',
7978
'src/NSRunLoop+blockExecution.m',
8079
# Models

Daemons/vmpserverd/src/VMPCalEvent.h

-18
This file was deleted.

Daemons/vmpserverd/src/VMPCalEvent.m

-10
This file was deleted.

Daemons/vmpserverd/src/VMPCalSync.h

-24
This file was deleted.

Daemons/vmpserverd/src/VMPCalSync.m

-25
This file was deleted.
+53
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
/* vmpserverd - A virtual multimedia processor
2+
* Copyright (C) 2024 Hugo Melder
3+
*
4+
* SPDX-License-Identifier: MIT
5+
*/
6+
7+
#import <CalendarKit/CalendarKit.h>
8+
#import <Foundation/Foundation.h>
9+
10+
typedef void (^VMPCalendarNotificationBlock)(ICALComponent *);
11+
typedef BOOL (^VMPCalendarFilterBlock)(ICALComponent *);
12+
13+
/**
14+
* @brief Synchronises events from an ICAL server in a
15+
* user-specified time interval based on a list of lecture halls.
16+
*
17+
* All VEVENT's in the iCalendar feed have a LOCATION property which
18+
* features a lecture hall identifier. A configuration dictionary, passed
19+
* during initialisation of a VMPCalendarSync instance, is used to
20+
* filter-out unrelated VEVENTs.
21+
*/
22+
@interface VMPCalendarSync : NSObject
23+
24+
/**
25+
* @brief Synchronisation interval
26+
*/
27+
@property (readonly) NSTimeInterval interval;
28+
29+
@property (readonly) NSTimeInterval notifyBeforeStartThreshold;
30+
31+
/**
32+
* @brief iCalendar feed URL
33+
*/
34+
@property (readonly) NSURL *url;
35+
36+
/**
37+
* @brief Called when "notifyBeforeStart" threshold is reached or
38+
* exceeded. Note that the accuracy currently depends on the sync
39+
* interval.
40+
*/
41+
@property VMPCalendarNotificationBlock notificationBlock;
42+
43+
/**
44+
* @brief Called when new events are found in the feed. Callee
45+
* decides whether the events are added or ignored.
46+
*/
47+
@property VMPCalendarFilterBlock filterBlock;
48+
49+
- (instancetype)initWithURL:(NSURL *)url
50+
interval:(NSTimeInterval)interval
51+
notifyBeforeStart:(NSTimeInterval)threshold;
52+
53+
@end
+180
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
#include "CalendarKit/ICALComponent.h"
2+
#include "Foundation/NSArray.h"
3+
#include "Foundation/NSObjCRuntime.h"
4+
#import <dispatch/dispatch.h>
5+
6+
#import "VMPCalendarSync.h"
7+
#import "VMPJournal.h"
8+
9+
@implementation VMPCalendarSync {
10+
_Atomic(BOOL) _isActive;
11+
NSURLRequest *_request;
12+
NSLock *_lock;
13+
dispatch_queue_t _queue;
14+
dispatch_source_t _timer;
15+
short _retryAttempts;
16+
NSMutableArray<ICALComponent *> *_events;
17+
}
18+
19+
- (instancetype)initWithURL:(NSURL *)url
20+
interval:(NSTimeInterval)interval
21+
notifyBeforeStart:(NSTimeInterval)threshold {
22+
self = [super init];
23+
24+
if (self) {
25+
_url = url;
26+
_interval = interval;
27+
_notifyBeforeStartThreshold = threshold;
28+
_queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
29+
_timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, _queue);
30+
_lock = [NSLock new];
31+
_request = [NSURLRequest requestWithURL:url];
32+
_events = [NSMutableArray arrayWithCapacity:32];
33+
_retryAttempts = 5;
34+
35+
uint64_t dispatchInterval = (uint64_t) (interval * NSEC_PER_SEC);
36+
uint64_t leeway = (uint64_t) (dispatchInterval * 0.05); // 5% leeway
37+
dispatch_source_set_timer(_timer, DISPATCH_TIME_NOW, dispatchInterval, leeway);
38+
dispatch_source_set_event_handler(_timer, ^{
39+
if (_isActive) {
40+
[self _sync];
41+
}
42+
});
43+
dispatch_resume(_timer);
44+
_isActive = YES;
45+
}
46+
47+
return self;
48+
}
49+
50+
- (void)_sync {
51+
short retriesLeft = _retryAttempts;
52+
NSError *error = nil;
53+
NSURLResponse *response = nil;
54+
NSData *data = nil;
55+
56+
VMPInfo(@"Synchronising calendar with remote (%@)", _url);
57+
58+
retry:
59+
if (retriesLeft == 0) {
60+
VMPError(@"Calendar Sync: Number of retries exhausted");
61+
return;
62+
}
63+
retriesLeft--;
64+
error = nil;
65+
data = [NSURLConnection sendSynchronousRequest:_request
66+
returningResponse:&response
67+
error:&error];
68+
if (!data) {
69+
VMPError(@"Calendar Sync: Failed to fetch calendar feed with error: %@", error,
70+
retriesLeft);
71+
VMPError(@"Calendar Sync: %d retries left", retriesLeft);
72+
goto retry;
73+
}
74+
75+
// Check if the mimetype is correct
76+
if (![@"text/calendar" isEqualToString:[response MIMEType]]) {
77+
VMPError(@"Calendar Sync: Got response '%@' but mimetype is not text/calendar", response);
78+
VMPError(@"Calendar Sync: %d retries left", retriesLeft);
79+
goto retry;
80+
}
81+
82+
// We can now try to parse the calendar feed using CalendarKit
83+
VMPDebug(@"Calendar Sync: Parsing returned data from server");
84+
error = nil;
85+
ICALComponent *cal = [ICALComponent componentWithData:data error:&error];
86+
if (!cal) {
87+
VMPError(@"Calendar Sync: Failed to parse calendar feed: %@", error);
88+
return;
89+
}
90+
91+
NSMutableArray *updatedEvents = [NSMutableArray arrayWithCapacity:[_events count]];
92+
NSDate *current = [NSDate date];
93+
94+
[cal enumerateComponentsUsingBlock:^(ICALComponent *comp, BOOL *stop) {
95+
if ([comp kind] == ICALComponentKindVEVENT) {
96+
NSDate *endDate = [comp endDate];
97+
if (!endDate) {
98+
VMPError(@"Calendar Sync: Failed to retrieve end date from %@", comp);
99+
return; // skip
100+
}
101+
102+
if ([current compare:endDate] != NSOrderedAscending) {
103+
return; // skip if date is same or in the past
104+
}
105+
106+
// Check if we are interested in this event
107+
if (_filterBlock && !_filterBlock(comp)) {
108+
return; // skip over this element
109+
}
110+
111+
// TODO(hugo): We might want to keep the existing array and only update
112+
// it (by using a hashset or a min heap), but this might create some edge cases, that I
113+
// just don't want to bother with right now.
114+
[updatedEvents addObject:[comp copy]];
115+
}
116+
}];
117+
118+
VMPInfo(@"Calendar Sync: Found %ld events of interest", [updatedEvents count]);
119+
120+
// Replace existing set with updated events
121+
[_lock lock];
122+
_events = updatedEvents;
123+
[_lock unlock];
124+
125+
// Check if we have events that are passed the notification threshold
126+
// Note that we have an error of up to 'interval' so we just notify earlier :P
127+
NSDate *threshold =
128+
[[NSDate alloc] initWithTimeIntervalSinceNow:_notifyBeforeStartThreshold + _interval];
129+
NSMutableArray<ICALComponent *> *eventsToRemove = [NSMutableArray new];
130+
for (ICALComponent *comp in _events) {
131+
NSDate *start = [comp startDate];
132+
if (!start) {
133+
continue;
134+
}
135+
136+
// if threshold is not later in time than start
137+
if ([threshold compare:start] != NSOrderedDescending) {
138+
VMPInfo(@"Calendar Sync: Event %@ is passed threshold. Notifying...", comp);
139+
if (_notificationBlock) {
140+
_notificationBlock(comp);
141+
}
142+
[eventsToRemove addObject:comp];
143+
}
144+
}
145+
146+
VMPDebug(@"Calendar Sync: Removing %ld events after notification", [eventsToRemove count]);
147+
[_lock lock];
148+
[_events removeObjectsInArray:eventsToRemove];
149+
[_lock unlock];
150+
VMPDebug(@"Calendar Sync: events removed");
151+
}
152+
153+
- (void)start {
154+
if (NO == _isActive) {
155+
[_lock lock];
156+
if (NO == _isActive) {
157+
dispatch_resume(_timer);
158+
_isActive = YES;
159+
}
160+
[_lock unlock];
161+
}
162+
}
163+
164+
- (void)stop {
165+
if (YES == _isActive) {
166+
[_lock lock];
167+
if (YES == _isActive) {
168+
dispatch_suspend(_timer);
169+
_isActive = NO;
170+
}
171+
[_lock unlock];
172+
}
173+
}
174+
175+
- (void)dealloc {
176+
dispatch_source_cancel(_timer);
177+
dispatch_release(_timer);
178+
}
179+
180+
@end

Daemons/vmpserverd/src/VMPRecordingManager.h

+7
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,13 @@
2525
*/
2626
@property (nonatomic, readonly) NSDictionary *options;
2727

28+
/**
29+
* If this recording was scheduled by our calendar
30+
* scheduling system, then this is the UID from the
31+
* VEVENT.
32+
*/
33+
@property (nullable) NSString *associatedUID;
34+
2835
@property (atomic, assign) BOOL eosReceived;
2936

3037
+ (instancetype)recorderWithLaunchArgs:(NSString *)launchArgs

0 commit comments

Comments
 (0)