From 645035fcc71ee51a47a69f5e994e18471dfa3990 Mon Sep 17 00:00:00 2001 From: Fenris Wolf Date: Tue, 9 Sep 2025 21:17:19 +0000 Subject: [PATCH] [mod] --- conf/example.json | 4 +- source/conf.php | 19 ++- source/helpers/cache.php | 1 - source/helpers/ics.php | 4 +- source/model.php | 119 +++++++++++++++++++ source/overwrites/caldav_backend.php | 54 +++++---- source/sources/_factory.php | 20 +++- source/sources/_interface.php | 15 +-- source/sources/ics_feed.php | 171 +++++++++++++++++---------- 9 files changed, 297 insertions(+), 110 deletions(-) create mode 100644 source/model.php diff --git a/conf/example.json b/conf/example.json index 3a1d83c..7721c6c 100644 --- a/conf/example.json +++ b/conf/example.json @@ -6,8 +6,8 @@ "source": { "kind": "ics_feed", "data": { - "url": "https://export.kalender.digital/ics/0/3e10dae66950379d4cc8/gesamterkalender.ics?past_months=1&future_months=2", - "condense": false + "url": "https://www.phpclasses.org/browse/download/1/file/63438/name/example.ics", + "combine": true } }, "settings": { diff --git a/source/conf.php b/source/conf.php index 4cac9cd..4847a33 100644 --- a/source/conf.php +++ b/source/conf.php @@ -12,6 +12,16 @@ class struct_auth } +/** + */ +class struct_source_data_ics_feed +{ + public string $url; + public bool $combined; + public int $lifetime; +} + + /** */ class struct_source @@ -91,7 +101,14 @@ function load( function get( ) : struct_root { - return _state::$data; + if (_state::$data === null) + { + throw (new \Exception('not yet loaded')); + } + else + { + return _state::$data; + } } ?> diff --git a/source/helpers/cache.php b/source/helpers/cache.php index f1698b8..440c9d6 100644 --- a/source/helpers/cache.php +++ b/source/helpers/cache.php @@ -170,7 +170,6 @@ class class_cache_memory } - /** */ class class_cache_file diff --git a/source/helpers/ics.php b/source/helpers/ics.php index d64a5ac..f4aeb75 100644 --- a/source/helpers/ics.php +++ b/source/helpers/ics.php @@ -296,7 +296,7 @@ function time_decode( \intval(\substr($time_encoded, 0, 2)), \intval(\substr($time_encoded, 2, 2)), \intval(\substr($time_encoded, 4, 2)), - ((\strlen($time_encoded >= 7) && ($time_encoded[6] === 'Z'))) + ((\strlen($time_encoded) >= 7) && ($time_encoded[6] === 'Z')) )); } @@ -496,7 +496,7 @@ function vcalendar_decode( fn($x) => \explode(',', $x), \array_values( \array_filter( - $event_raw->additionalProperties['categories_array'], + ($event_raw->additionalProperties['categories_array'] ?? []), fn($x) => \is_string($x) ) ) diff --git a/source/model.php b/source/model.php new file mode 100644 index 0000000..fde033f --- /dev/null +++ b/source/model.php @@ -0,0 +1,119 @@ +id = $id; + $this->title = $title; + $this->begin = $begin; + $this->end = $end; + $this->location = $location; + $this->description = $description; + $this->tags = $tags; + } +} + + +/** + */ +function event_to_raw( + struct_event $event +) +{ + return [ + 'id' => $event->id, + 'title' => $event->title, + 'begin' => $event->begin, + 'end' => $event->end, + 'location' => $event->location, + 'description' => $event->description, + 'tags' => $event->tags, + ]; +} + + +/** + */ +function event_from_raw( + $event_raw +) : struct_event +{ + return (new struct_event( + $event_raw['id'], + $event_raw['title'], + $event_raw['begin'], + $event_raw['end'], + $event_raw['location'], + $event_raw['description'], + $event_raw['tags'], + )); +} + + +/** + */ +class struct_calendar +{ + public array $events; + + public function __construct( + array $events + ) + { + $this->events = $events; + } +} + + +/** + */ +function calendar_to_raw( + struct_calendar $calendar +) +{ + return [ + 'events' => \array_map( + fn($event) => event_to_raw($event), + $calendar->events + ), + ]; +} + + +/** + */ +function calendar_from_raw( + $calendar_raw +) : struct_calendar +{ + return (new struct_calendar( + \array_map( + fn($event_raw) => event_from_raw($event_raw), + $calendar_raw['events'] + ) + )); +} + + ?> diff --git a/source/overwrites/caldav_backend.php b/source/overwrites/caldav_backend.php index 459772b..c005243 100644 --- a/source/overwrites/caldav_backend.php +++ b/source/overwrites/caldav_backend.php @@ -5,6 +5,7 @@ namespace davigil\overwrites; require_once('vendor/autoload.php'); require_once('helpers/call.php'); require_once('helpers/ics.php'); +require_once('model.php'); require_once('sources/_interface.php'); require_once('conf.php'); @@ -69,32 +70,32 @@ class class_caldav_backend * @todo outsource */ private function event_to_vevent( - array $event + \davigil\model\struct_event $event ) : \davigil\helpers\ics\struct_vevent { $vevent = new \davigil\helpers\ics\struct_vevent(); { - $vevent->uid = $event['id']; - $vevent->dtstamp = \davigil\helpers\ics\datetime_from_unix_timestamp($event['begin']); + $vevent->uid = $event->id; + $vevent->dtstamp = \davigil\helpers\ics\datetime_from_unix_timestamp($event->begin); $vevent->dtstart = new \davigil\helpers\ics\struct_dt( '', - \davigil\helpers\ics\datetime_from_unix_timestamp($event['begin']) + \davigil\helpers\ics\datetime_from_unix_timestamp($event->begin) ); $vevent->dtend = ( - ($event['end'] === null) + ($event->end === null) ? null : new \davigil\helpers\ics\struct_dt( '', - \davigil\helpers\ics\datetime_from_unix_timestamp($event['end']) + \davigil\helpers\ics\datetime_from_unix_timestamp($event->end) ) ); - $vevent->summary = $event['title']; - $vevent->location = $event['location']; - $vevent->description = $event['description']; + $vevent->summary = $event->title; + $vevent->location = $event->location; + $vevent->description = $event->description; $vevent->class = \davigil\helpers\ics\enum_class::public_; - $vevent->categories = $event['tags']; + $vevent->categories = $event->tags; } return $vevent; } @@ -131,11 +132,11 @@ class class_caldav_backend $principalUri ) { - $data = $this->source->get([]); + $calendar = $this->source->get([]); $tags = []; - foreach ($data as $entry) + foreach ($calendar->events as $event) { - foreach ($entry['tags'] as $tag) + foreach ($event->tags as $tag) { $tags[$tag] = null; } @@ -187,27 +188,27 @@ class class_caldav_backend ) { $tag = $calendarId; - $data = $this->source->get([]); + $calendar = $this->source->get([]); $result = \array_map( - fn($entry) => [ + fn($event) => [ 'calendarid' => $calendarId, - 'id' => $entry['id'], + 'id' => $event->id, // 'uri' => \sprintf('%s.ics', $entry['id']), - 'uri' => $entry['id'], + 'uri' => $event->id, 'lastmodified' => \time(), // 'etag' => null, // 'size' => null, 'component' => 'vevent', - '{DAV:}displayname' => $entry['title'], + '{DAV:}displayname' => $event->title, ], \array_values( \array_filter( - $data, - fn($entry) => \in_array( + $calendar->events, + fn($event) => \in_array( $tag, \array_map( fn($x) => $this->hash_tag($x), - $entry['tags'] + $event->tags ) ) ) @@ -230,18 +231,21 @@ class class_caldav_backend $entries = \array_values( \array_filter( $data, - fn($entry) => ($entry['id'] === $id) + fn($entry) => ($entry->id === $id) ) ); - if (\count($entries) !== 1) + if (\count($entries) < 1) { - throw (new \Exception(\sprintf('not found or ambiguous'))); + throw (new \Exception(\sprintf('not found: %s', $objectUri))); + } + else if (\count($entries) > 1) + { + throw (new \Exception(\sprintf('ambiguous: %s', $objectUri))); } else { $vcalendar = $this->events_to_vcalendar($entries); $ics = \davigil\helpers\ics\vcalendar_encode($vcalendar); -\error_log($ics); return [ 'calendardata' => $ics, 'uri' => $objectUri, diff --git a/source/sources/_factory.php b/source/sources/_factory.php index cd7610c..3e1943c 100644 --- a/source/sources/_factory.php +++ b/source/sources/_factory.php @@ -21,14 +21,24 @@ function make( { case 'ics_feed': { - return (new class_source_ics_feed( - $descriptor['data']['url'], - ($descriptor['data']['condense'] ?? false) - )); + return ( + new class_source_ics_feed( + $descriptor['data']['url'], + ($descriptor['data']['lifetime'] ?? (60 * 15)), + ($descriptor['data']['combine'] ?? false) + ) + ); } default: { - throw (new \Exception(\sprintf('unhandled source kind: %s', $descriptor['kind']))); + throw ( + new \Exception( + \sprintf( + 'unhandled source kind: %s', + $descriptor['kind'] + ) + ) + ); break; } } diff --git a/source/sources/_interface.php b/source/sources/_interface.php index ff89e9e..1ad1215 100644 --- a/source/sources/_interface.php +++ b/source/sources/_interface.php @@ -15,23 +15,10 @@ interface interface_source * password:(null|string), * > * } - * @return array { - * list< - * record< - * id:string, - * title:string, - * begin:type_unix_timestamp, - * end:(null|type_unix_timestamp), - * location:(null|string), - * description:(null|string), - * tags:list, - * > - * > - * } */ public function get( array $parameters - ) : array + ) : \davigil\model\struct_calendar ; } diff --git a/source/sources/ics_feed.php b/source/sources/ics_feed.php index d3f610b..8fa0343 100644 --- a/source/sources/ics_feed.php +++ b/source/sources/ics_feed.php @@ -6,6 +6,7 @@ require_once('helpers/string.php'); require_once('helpers/cache.php'); require_once('helpers/pit.php'); require_once('helpers/ics.php'); +require_once('model.php'); /** @@ -21,31 +22,107 @@ class class_source_ics_feed /** */ - private bool $condense; + private ?int $lifetime; + + + /** + */ + private bool $combine; /** */ public function __construct( string $url, - bool $condense + ?int $lifetime, + bool $combine ) { $this->url = $url; - $this->condense = $condense; - $this->cache_file = new \davigil\helpers\cache\class_cache_encoded( + $this->lifetime = $lifetime; + $this->combine = $combine; + $this->cache_file = \davigil\helpers\call\convey( new \davigil\helpers\cache\class_cache_file('data'), - fn($value) => \json_encode($value), - fn($value_encoded) => \json_decode($value_encoded, true) + [ + fn($x) => new \davigil\helpers\cache\class_cache_encoded( + $x, + fn($value) => \json_encode($value), + fn($value_encoded) => \json_decode($value_encoded, true) + ), + ] + ); + $this->cache_memory = \davigil\helpers\call\convey( + (new \davigil\helpers\cache\class_cache_memory()), + [ + ] + ); + } + + + /** + */ + private function vcalendar_to_calendar( + \davigil\helpers\ics\struct_vcalendar $vcalendar + ) : \davigil\model\struct_calendar + { + return ( + new \davigil\model\struct_calendar( + \array_map( + fn($vevent) => (new \davigil\model\struct_event( + // id + $vevent->uid, + // title + ( + $this->combine + ? + \sprintf( + '%s%s', + $vevent->summary, + \implode( + '', + \array_map( + fn($category) => \sprintf(' (%s)', $category), + $vevent->categories + ) + ) + ) + : + $vevent->summary + ), + // begin + \davigil\helpers\ics\datetime_to_unix_timestamp($vevent->dtstart->value), + // end + ( + ($vevent->dtend === null) + ? + null + : + \davigil\helpers\ics\datetime_to_unix_timestamp($vevent->dtend->value) + ), + // location + $vevent->location, + // description + $vevent->description, + // 'tags + ( + $this->combine + ? + ['combined'] + : + $vevent->categories + ) + )), + $vcalendar->events + ) + ) ); - $this->cache_memory = new \davigil\helpers\cache\class_cache_memory(); } /** */ private function retrieve( - ) : array + ) { $client = new \Sabre\HTTP\Client(); $request = new \Sabre\HTTP\Request( @@ -62,52 +139,21 @@ class class_source_ics_feed { $ics = $response->getBody(); $vcalendar = \davigil\helpers\ics\vcalendar_decode($ics); - $data = \array_map( - fn($vevent) => [ - 'id' => $vevent->uid, - 'title' => ( - $this->condense - ? - \sprintf( - '%s%s', - $vevent->summary, - \implode( - '', - \array_map( - fn($category) => \sprintf(' (%s)', $category), - $vevent->categories - ) - ) - ) - : - $vevent->summary - ), - 'begin' => \davigil\helpers\ics\datetime_to_unix_timestamp($vevent->dtstart->value), - 'end' => ( - ($vevent->dtend === null) - ? - null - : - \davigil\helpers\ics\datetime_to_unix_timestamp($vevent->dtend->value) - ), - 'location' => $vevent->location, - 'description' => $vevent->description, - 'tags' => ( - $this->condense - ? - ['dummy'] - : - $vevent->categories - ), - ], - $vcalendar->events - ); - return $data; + $calendar = $this->vcalendar_to_calendar($vcalendar); + $calendar_raw = \davigil\model\calendar_to_raw($calendar); + return $calendar_raw; break; } default: { - throw (new \Exception(\sprintf('unhandled response status code: %u', $status_code))); + throw ( + new \Exception( + \sprintf( + 'unhandled response status code: %u', + $status_code + ) + ) + ); break; } } @@ -119,24 +165,29 @@ class class_source_ics_feed */ public function get( array $parameters - ) : array + ) : \davigil\model\struct_calendar { $key = $this->url; - return \davigil\helpers\cache\get( + $f1 = fn() => $this->retrieve(); + $f2 = fn() => \davigil\helpers\cache\get( + $this->cache_file, + $key, + $f1, + [ + 'ttl' => $this->lifetime, + ] + ); + $f3 = fn() => \davigil\helpers\cache\get( $this->cache_memory, $key, - fn() => \davigil\helpers\cache\get( - $this->cache_file, - $key, - fn() => $this->retrieve(), - [ - 'ttl' => (60 * 5), - ] - ), + $f2, [ 'ttl' => null, ] ); + return \davigil\model\calendar_from_raw( + ($f3)() + ); } }