-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
18 changed files
with
607 additions
and
131 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file was deleted.
Oops, something went wrong.
This file was deleted.
Oops, something went wrong.
This file was deleted.
Oops, something went wrong.
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
/* vmpserverd - A virtual multimedia processor | ||
* Copyright (C) 2024 Hugo Melder | ||
* | ||
* SPDX-License-Identifier: MIT | ||
*/ | ||
|
||
#import <CalendarKit/CalendarKit.h> | ||
#import <Foundation/Foundation.h> | ||
|
||
typedef void (^VMPCalendarNotificationBlock)(ICALComponent *); | ||
typedef BOOL (^VMPCalendarFilterBlock)(ICALComponent *); | ||
|
||
/** | ||
* @brief Synchronises events from an ICAL server in a | ||
* user-specified time interval based on a list of lecture halls. | ||
* | ||
* All VEVENT's in the iCalendar feed have a LOCATION property which | ||
* features a lecture hall identifier. A configuration dictionary, passed | ||
* during initialisation of a VMPCalendarSync instance, is used to | ||
* filter-out unrelated VEVENTs. | ||
*/ | ||
@interface VMPCalendarSync : NSObject | ||
|
||
/** | ||
* @brief Synchronisation interval | ||
*/ | ||
@property (readonly) NSTimeInterval interval; | ||
|
||
@property (readonly) NSTimeInterval notifyBeforeStartThreshold; | ||
|
||
/** | ||
* @brief iCalendar feed URL | ||
*/ | ||
@property (readonly) NSURL *url; | ||
|
||
/** | ||
* @brief Called when "notifyBeforeStart" threshold is reached or | ||
* exceeded. Note that the accuracy currently depends on the sync | ||
* interval. | ||
*/ | ||
@property VMPCalendarNotificationBlock notificationBlock; | ||
|
||
/** | ||
* @brief Called when new events are found in the feed. Callee | ||
* decides whether the events are added or ignored. | ||
*/ | ||
@property VMPCalendarFilterBlock filterBlock; | ||
|
||
- (instancetype)initWithURL:(NSURL *)url | ||
interval:(NSTimeInterval)interval | ||
notifyBeforeStart:(NSTimeInterval)threshold; | ||
|
||
@end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,180 @@ | ||
#include "CalendarKit/ICALComponent.h" | ||
#include "Foundation/NSArray.h" | ||
#include "Foundation/NSObjCRuntime.h" | ||
#import <dispatch/dispatch.h> | ||
|
||
#import "VMPCalendarSync.h" | ||
#import "VMPJournal.h" | ||
|
||
@implementation VMPCalendarSync { | ||
_Atomic(BOOL) _isActive; | ||
NSURLRequest *_request; | ||
NSLock *_lock; | ||
dispatch_queue_t _queue; | ||
dispatch_source_t _timer; | ||
short _retryAttempts; | ||
NSMutableArray<ICALComponent *> *_events; | ||
} | ||
|
||
- (instancetype)initWithURL:(NSURL *)url | ||
interval:(NSTimeInterval)interval | ||
notifyBeforeStart:(NSTimeInterval)threshold { | ||
self = [super init]; | ||
|
||
if (self) { | ||
_url = url; | ||
_interval = interval; | ||
_notifyBeforeStartThreshold = threshold; | ||
_queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); | ||
_timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, _queue); | ||
_lock = [NSLock new]; | ||
_request = [NSURLRequest requestWithURL:url]; | ||
_events = [NSMutableArray arrayWithCapacity:32]; | ||
_retryAttempts = 5; | ||
|
||
uint64_t dispatchInterval = (uint64_t) (interval * NSEC_PER_SEC); | ||
uint64_t leeway = (uint64_t) (dispatchInterval * 0.05); // 5% leeway | ||
dispatch_source_set_timer(_timer, DISPATCH_TIME_NOW, dispatchInterval, leeway); | ||
dispatch_source_set_event_handler(_timer, ^{ | ||
if (_isActive) { | ||
[self _sync]; | ||
} | ||
}); | ||
dispatch_resume(_timer); | ||
_isActive = YES; | ||
} | ||
|
||
return self; | ||
} | ||
|
||
- (void)_sync { | ||
short retriesLeft = _retryAttempts; | ||
NSError *error = nil; | ||
NSURLResponse *response = nil; | ||
NSData *data = nil; | ||
|
||
VMPInfo(@"Synchronising calendar with remote (%@)", _url); | ||
|
||
retry: | ||
if (retriesLeft == 0) { | ||
VMPError(@"Calendar Sync: Number of retries exhausted"); | ||
return; | ||
} | ||
retriesLeft--; | ||
error = nil; | ||
data = [NSURLConnection sendSynchronousRequest:_request | ||
returningResponse:&response | ||
error:&error]; | ||
if (!data) { | ||
VMPError(@"Calendar Sync: Failed to fetch calendar feed with error: %@", error, | ||
retriesLeft); | ||
VMPError(@"Calendar Sync: %d retries left", retriesLeft); | ||
goto retry; | ||
} | ||
|
||
// Check if the mimetype is correct | ||
if (![@"text/calendar" isEqualToString:[response MIMEType]]) { | ||
VMPError(@"Calendar Sync: Got response '%@' but mimetype is not text/calendar", response); | ||
VMPError(@"Calendar Sync: %d retries left", retriesLeft); | ||
goto retry; | ||
} | ||
|
||
// We can now try to parse the calendar feed using CalendarKit | ||
VMPDebug(@"Calendar Sync: Parsing returned data from server"); | ||
error = nil; | ||
ICALComponent *cal = [ICALComponent componentWithData:data error:&error]; | ||
if (!cal) { | ||
VMPError(@"Calendar Sync: Failed to parse calendar feed: %@", error); | ||
return; | ||
} | ||
|
||
NSMutableArray *updatedEvents = [NSMutableArray arrayWithCapacity:[_events count]]; | ||
NSDate *current = [NSDate date]; | ||
|
||
[cal enumerateComponentsUsingBlock:^(ICALComponent *comp, BOOL *stop) { | ||
if ([comp kind] == ICALComponentKindVEVENT) { | ||
NSDate *endDate = [comp endDate]; | ||
if (!endDate) { | ||
VMPError(@"Calendar Sync: Failed to retrieve end date from %@", comp); | ||
return; // skip | ||
} | ||
|
||
if ([current compare:endDate] != NSOrderedAscending) { | ||
return; // skip if date is same or in the past | ||
} | ||
|
||
// Check if we are interested in this event | ||
if (_filterBlock && !_filterBlock(comp)) { | ||
return; // skip over this element | ||
} | ||
|
||
// TODO(hugo): We might want to keep the existing array and only update | ||
// it (by using a hashset or a min heap), but this might create some edge cases, that I | ||
// just don't want to bother with right now. | ||
[updatedEvents addObject:[comp copy]]; | ||
} | ||
}]; | ||
|
||
VMPInfo(@"Calendar Sync: Found %ld events of interest", [updatedEvents count]); | ||
|
||
// Replace existing set with updated events | ||
[_lock lock]; | ||
_events = updatedEvents; | ||
[_lock unlock]; | ||
|
||
// Check if we have events that are passed the notification threshold | ||
// Note that we have an error of up to 'interval' so we just notify earlier :P | ||
NSDate *threshold = | ||
[[NSDate alloc] initWithTimeIntervalSinceNow:_notifyBeforeStartThreshold + _interval]; | ||
NSMutableArray<ICALComponent *> *eventsToRemove = [NSMutableArray new]; | ||
for (ICALComponent *comp in _events) { | ||
NSDate *start = [comp startDate]; | ||
if (!start) { | ||
continue; | ||
} | ||
|
||
// if threshold is not later in time than start | ||
if ([threshold compare:start] != NSOrderedDescending) { | ||
VMPInfo(@"Calendar Sync: Event %@ is passed threshold. Notifying...", comp); | ||
if (_notificationBlock) { | ||
_notificationBlock(comp); | ||
} | ||
[eventsToRemove addObject:comp]; | ||
} | ||
} | ||
|
||
VMPDebug(@"Calendar Sync: Removing %ld events after notification", [eventsToRemove count]); | ||
[_lock lock]; | ||
[_events removeObjectsInArray:eventsToRemove]; | ||
[_lock unlock]; | ||
VMPDebug(@"Calendar Sync: events removed"); | ||
} | ||
|
||
- (void)start { | ||
if (NO == _isActive) { | ||
[_lock lock]; | ||
if (NO == _isActive) { | ||
dispatch_resume(_timer); | ||
_isActive = YES; | ||
} | ||
[_lock unlock]; | ||
} | ||
} | ||
|
||
- (void)stop { | ||
if (YES == _isActive) { | ||
[_lock lock]; | ||
if (YES == _isActive) { | ||
dispatch_suspend(_timer); | ||
_isActive = NO; | ||
} | ||
[_lock unlock]; | ||
} | ||
} | ||
|
||
- (void)dealloc { | ||
dispatch_source_cancel(_timer); | ||
dispatch_release(_timer); | ||
} | ||
|
||
@end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.