From 8bd989e30e371881c14a1e1b821a50a3b334b1d1 Mon Sep 17 00:00:00 2001 From: turegjorup Date: Tue, 26 Nov 2024 10:11:45 +0100 Subject: [PATCH 1/9] 2533: Refactored namespacing for feed types and models --- config/services.yaml | 2 +- docs/{ => feed}/calender-api-feed.md | 0 ...edFeedOutputs.php => FeedOutputModels.php} | 2 +- .../OutputModel/Calendar/Location.php} | 4 +- .../OutputModel/Calendar/Resource.php} | 4 +- src/Feed/OutputModel/Rss/Readme.md | 7 + .../Calendar}/CalendarApiFeedType.php | 31 +-- src/Feed/SourceType/Colibo/ApiClient.php | 248 ++++++++++++++++++ .../EventDatabaseApiFeedType.php | 6 +- .../{ => SourceType/Koba}/KobaFeedType.php | 6 +- .../Notified}/NotifiedFeedType.php | 6 +- src/Feed/{ => SourceType/Rss}/RssFeedType.php | 6 +- .../SparkleIO}/SparkleIOFeedType.php | 6 +- src/Model/CalendarEvent.php | 17 -- tests/Api/FeedSourceTest.php | 10 +- tests/Feed/NotifiedFeedTypeTest.php | 2 +- tests/Service/FeedServiceTest.php | 12 +- 17 files changed, 309 insertions(+), 60 deletions(-) rename docs/{ => feed}/calender-api-feed.md (100%) rename src/Feed/{SupportedFeedOutputs.php => FeedOutputModels.php} (98%) rename src/{Model/CalendarLocation.php => Feed/OutputModel/Calendar/Location.php} (71%) rename src/{Model/CalendarResource.php => Feed/OutputModel/Calendar/Resource.php} (76%) create mode 100644 src/Feed/OutputModel/Rss/Readme.md rename src/Feed/{ => SourceType/Calendar}/CalendarApiFeedType.php (95%) create mode 100644 src/Feed/SourceType/Colibo/ApiClient.php rename src/Feed/{ => SourceType/EventDatabase}/EventDatabaseApiFeedType.php (98%) rename src/Feed/{ => SourceType/Koba}/KobaFeedType.php (98%) rename src/Feed/{ => SourceType/Notified}/NotifiedFeedType.php (97%) rename src/Feed/{ => SourceType/Rss}/RssFeedType.php (95%) rename src/Feed/{ => SourceType/SparkleIO}/SparkleIOFeedType.php (98%) delete mode 100644 src/Model/CalendarEvent.php diff --git a/config/services.yaml b/config/services.yaml index 6f1a0b8c..2cf4cf64 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -51,7 +51,7 @@ services: Symfony\Component\Security\Http\Authentication\AuthenticationFailureHandlerInterface: '@Lexik\Bundle\JWTAuthenticationBundle\Security\Http\Authentication\AuthenticationFailureHandler' Symfony\Component\Security\Http\Authentication\AuthenticationSuccessHandlerInterface: '@Lexik\Bundle\JWTAuthenticationBundle\Security\Http\Authentication\AuthenticationSuccessHandler' - App\Feed\CalendarApiFeedType: + App\Feed\SourceType\Calendar\CalendarApiFeedType: arguments: $locationEndpoint: '%env(string:CALENDAR_API_FEED_SOURCE_LOCATION_ENDPOINT)%' $resourceEndpoint: '%env(string:CALENDAR_API_FEED_SOURCE_RESOURCE_ENDPOINT)%' diff --git a/docs/calender-api-feed.md b/docs/feed/calender-api-feed.md similarity index 100% rename from docs/calender-api-feed.md rename to docs/feed/calender-api-feed.md diff --git a/src/Feed/SupportedFeedOutputs.php b/src/Feed/FeedOutputModels.php similarity index 98% rename from src/Feed/SupportedFeedOutputs.php rename to src/Feed/FeedOutputModels.php index d080b97a..0458c723 100644 --- a/src/Feed/SupportedFeedOutputs.php +++ b/src/Feed/FeedOutputModels.php @@ -4,7 +4,7 @@ namespace App\Feed; -class SupportedFeedOutputs +class FeedOutputModels { /** * Data example: diff --git a/src/Model/CalendarLocation.php b/src/Feed/OutputModel/Calendar/Location.php similarity index 71% rename from src/Model/CalendarLocation.php rename to src/Feed/OutputModel/Calendar/Location.php index ddc08f81..057d7e86 100644 --- a/src/Model/CalendarLocation.php +++ b/src/Feed/OutputModel/Calendar/Location.php @@ -2,9 +2,9 @@ declare(strict_types=1); -namespace App\Model; +namespace App\Feed\OutputModel\Calendar; -class CalendarLocation +class Location { public function __construct( public string $id, diff --git a/src/Model/CalendarResource.php b/src/Feed/OutputModel/Calendar/Resource.php similarity index 76% rename from src/Model/CalendarResource.php rename to src/Feed/OutputModel/Calendar/Resource.php index 48cb9b16..740f454a 100644 --- a/src/Model/CalendarResource.php +++ b/src/Feed/OutputModel/Calendar/Resource.php @@ -2,9 +2,9 @@ declare(strict_types=1); -namespace App\Model; +namespace App\Feed\OutputModel\Calendar; -class CalendarResource +class Resource { public function __construct( public string $id, diff --git a/src/Feed/OutputModel/Rss/Readme.md b/src/Feed/OutputModel/Rss/Readme.md new file mode 100644 index 00000000..eb23bf1e --- /dev/null +++ b/src/Feed/OutputModel/Rss/Readme.md @@ -0,0 +1,7 @@ +# RRS / FeedIO + +The output model for RSS is defined as + +`FeedIo\Reader\Result` and `FeedIo\Feed\ItemInterface` + +@see https://alexdebril.github.io/feed-io/ diff --git a/src/Feed/CalendarApiFeedType.php b/src/Feed/SourceType/Calendar/CalendarApiFeedType.php similarity index 95% rename from src/Feed/CalendarApiFeedType.php rename to src/Feed/SourceType/Calendar/CalendarApiFeedType.php index 37210cf5..b3ebd57c 100644 --- a/src/Feed/CalendarApiFeedType.php +++ b/src/Feed/SourceType/Calendar/CalendarApiFeedType.php @@ -2,15 +2,16 @@ declare(strict_types=1); -namespace App\Feed; +namespace App\Feed\SourceType\Calendar; use App\Entity\Tenant\Feed; use App\Entity\Tenant\FeedSource; -use App\Model\CalendarEvent; -use App\Model\CalendarLocation; -use App\Model\CalendarResource; +use App\Feed\FeedOutputModels; +use App\Feed\FeedTypeInterface; +use App\Feed\OutputModel\Calendar\Event; +use App\Feed\OutputModel\Calendar\Location; +use App\Feed\OutputModel\Calendar\Resource; use App\Service\FeedService; -use Faker\Core\DateTime; use Psr\Cache\CacheItemPoolInterface; use Psr\Log\LoggerInterface; use Symfony\Component\HttpFoundation\Request; @@ -29,7 +30,7 @@ */ class CalendarApiFeedType implements FeedTypeInterface { - final public const string SUPPORTED_FEED_TYPE = SupportedFeedOutputs::CALENDAR_OUTPUT; + final public const string SUPPORTED_FEED_TYPE = FeedOutputModels::CALENDAR_OUTPUT; final public const string EXCLUDE_IF_TITLE_NOT_CONTAINS = 'EXCLUDE_IF_TITLE_NOT_CONTAINS'; final public const string REPLACE_TITLE_IF_CONTAINS = 'REPLACE_TITLE_IF_CONTAINS'; @@ -79,7 +80,7 @@ public function getData(Feed $feed): array foreach ($resources as $resource) { $events = $this->getResourceEvents($resource); - /** @var CalendarEvent $event */ + /** @var Event $event */ foreach ($events as $event) { $title = $event->title; @@ -204,7 +205,7 @@ public function getConfigOptions(Request $request, FeedSource $feedSource, strin $resources = array_merge($resources, $locationResources); } - $resourceOptions = array_map(fn (CalendarResource $resource) => [ + $resourceOptions = array_map(fn (Resource $resource) => [ 'id' => Ulid::generate(), 'title' => $resource->displayName, 'value' => $resource->id, @@ -215,7 +216,7 @@ public function getConfigOptions(Request $request, FeedSource $feedSource, strin return $resourceOptions; } elseif ('locations' === $name) { - $locationOptions = array_map(fn (CalendarLocation $location) => [ + $locationOptions = array_map(fn (Location $location) => [ 'id' => Ulid::generate(), 'title' => $location->displayName, 'value' => $location->id, @@ -260,7 +261,7 @@ private function getLocationOptions(): array { $locations = $this->loadLocations(); - return array_reduce($locations, function (array $carry, CalendarLocation $location) { + return array_reduce($locations, function (array $carry, Location $location) { $carry[] = $location->id; return $carry; @@ -274,7 +275,7 @@ private function getResourceEvents(string $resourceId): array if (!$cacheItem->isHit()) { $allEvents = $this->loadEvents(); - $items = array_filter($allEvents, fn (CalendarEvent $item) => $item->resourceId === $resourceId); + $items = array_filter($allEvents, fn (Event $item) => $item->resourceId === $resourceId); $cacheItem->set($items); $cacheItem->expiresAfter($this->cacheExpireSeconds); @@ -291,7 +292,7 @@ private function getLocationResources(string $locationId): array if (!$cacheItem->isHit()) { $allResources = $this->loadResources(); - $items = array_filter($allResources, fn (CalendarResource $item) => $item->locationId === $locationId); + $items = array_filter($allResources, fn (Resource $item) => $item->locationId === $locationId); $cacheItem->set($items); $cacheItem->expiresAfter($this->cacheExpireSeconds); @@ -311,7 +312,7 @@ private function loadLocations(): array $LocationEntries = $response->toArray(); - $locations = array_map(fn (array $entry) => new CalendarLocation( + $locations = array_map(fn (array $entry) => new Location( $entry[$this->getMapping('locationId')], $entry[$this->getMapping('locationDisplayName')], ), $LocationEntries); @@ -346,7 +347,7 @@ private function loadResources(): array // Only include resources that are included in events endpoint. if ($includeValue) { - $resource = new CalendarResource( + $resource = new Resource( $resourceEntry[$this->getMapping('resourceId')], $resourceEntry[$this->getMapping('resourceLocationId')], $resourceEntry[$this->getMapping('resourceDisplayName')], @@ -377,7 +378,7 @@ private function loadEvents(): array $eventEntries = $response->toArray(); $events = array_reduce($eventEntries, function (array $carry, array $entry) { - $newEntry = new CalendarEvent( + $newEntry = new Event( Ulid::generate(), $entry[$this->getMapping('eventTitle')], $this->stringToUnixTimestamp($entry[$this->getMapping('eventStartTime')]), diff --git a/src/Feed/SourceType/Colibo/ApiClient.php b/src/Feed/SourceType/Colibo/ApiClient.php new file mode 100644 index 00000000..23f51314 --- /dev/null +++ b/src/Feed/SourceType/Colibo/ApiClient.php @@ -0,0 +1,248 @@ + */ + private array $apiClients = []; + + public function __construct( + private readonly CacheInterface $feedsCache, + private readonly LoggerInterface $logger, + ) {} + + public function getFeedEntriesNews(FeedSource $feedSource, array $recipients, array $publishers): mixed + { + $results = []; + + try { + $client = $this->getApiClient($feedSource); + + $response = $client->request('GET', '/api/feedentries/news', [ + 'headers' => [ + 'Content-Type' => 'application/json', + ], + 'query' => [ + 'getQuery.recipients' => $recipients, + ], + ]); + + $results = json_decode($response->getContent(), false, 512, JSON_THROW_ON_ERROR); + } catch (\Throwable $throwable) { + $this->logger->error('{code}: {message}', [ + 'code' => $throwable->getCode(), + 'message' => $throwable->getMessage(), + ]); + + throw $throwable; + } + + return $results; + } + + public function getSearchGroups(FeedSource $feedSource, string $type = 'WorkGroup'): array + { + $responseData = $this->getSearchGroupsPage($feedSource, $type)->toArray(); + + $groups = $responseData['results']; + + $total = $responseData['total']; + $pages = (int) ceil($total / self::BATCH_SIZE); + + /** @var ResponseInterface[] $responses */ + $responses = []; + for ($page = 1; $page < $pages; ++$page) { + $responses[] = $this->getSearchGroupsPage($feedSource, $type, $page); + } + + foreach ($responses as $response) { + $responseData = $response->toArray(); + $groups = array_merge($groups, $responseData['results']); + } + + return $groups; + } + + /** + * @throws TransportExceptionInterface + * @throws ServerExceptionInterface + * @throws RedirectionExceptionInterface + * @throws DecodingExceptionInterface + * @throws ClientExceptionInterface + */ + public function getFeedEntryPublishersGroups(FeedSource $feedSource): array + { + $client = $this->getApiClient($feedSource); + + $response = $client->request('GET', '/api/feedentries/publishers/groups', [ + 'query' => ['groupType' => 'Department'], + ]); + + $groups = []; + $childGroupIds = []; + foreach ($response->toArray() as $group) { + $groups[] = $group; + + if (isset($group['hasChildren']) && $group['hasChildren']) { + $childGroupIds[] = $group['id']; + } + } + + $this->getFeedEntryPublishersGroupsChildren($feedSource, $childGroupIds, $groups); + + return $groups; + } + + /** + * @throws TransportExceptionInterface + * @throws ServerExceptionInterface + * @throws RedirectionExceptionInterface + * @throws ClientExceptionInterface + */ + private function getSearchGroupsPage(FeedSource $feedSource, string $type, int $pageIndex = 0, int $pageSize = self::BATCH_SIZE): ResponseInterface + { + $client = $this->getApiClient($feedSource); + + return $client->request('GET', '/api/search/groups', [ + 'query' => [ + 'groupSearchQuery.groupTypes' => $type, + 'groupSearchQuery.pageIndex' => $pageIndex, + 'groupSearchQuery.pageSize' => $pageSize, + ], + ]); + } + + /** + * @throws TransportExceptionInterface + * @throws ServerExceptionInterface + * @throws RedirectionExceptionInterface + * @throws DecodingExceptionInterface + * @throws ClientExceptionInterface + */ + private function getFeedEntryPublishersGroupsChildren(FeedSource $feedSource, array $childGroupIds, array &$groups): void + { + $client = $this->getApiClient($feedSource); + + $batches = array_chunk($childGroupIds, self::BATCH_SIZE); + + foreach ($batches as $batch) { + // @see https://symfony.com/doc/current/http_client.html#concurrent-requests + $responses = []; + foreach ($batch as $childGroupId) { + $uri = sprintf('/api/feedentries/publishers/groups/%d/children', $childGroupId); + $responses[] = $client->request('GET', $uri, []); + } + + $childGroupIds = []; + foreach ($responses as $response) { + foreach ($response->toArray() as $group) { + $groups[] = $group; + + if (isset($group['hasChildren']) && $group['hasChildren']) { + $childGroupIds[] = $group['id']; + } + } + } + } + + if (!empty($childGroupIds)) { + $this->getFeedEntryPublishersGroupsChildren($feedSource, $childGroupIds, $groups); + } + } + + private function getApiClient(FeedSource $feedSource): HttpClientInterface + { + $id = ColiboFeedType::getIdKey($feedSource); + + if (array_key_exists($id, $this->apiClients)) { + return $this->apiClients[$id]; + } + + $secrets = new SecretsDTO($feedSource); + $this->apiClients[$id] = HttpClient::createForBaseUri($secrets->apiBaseUri)->withOptions([ + 'headers' => [ + 'Authorization' => 'Bearer '.$this->fetchColiboToken($feedSource), + 'Accept' => 'application/json', + ], + ]); + + return $this->apiClients[$id]; + } + + /** + * @throws TransportExceptionInterface + * @throws \Throwable + * @throws ServerExceptionInterface + * @throws RedirectionExceptionInterface + * @throws ClientExceptionInterface + * @throws \JsonException + */ + private function fetchColiboToken(FeedSource $feedSource): string + { + $id = ColiboFeedType::getIdKey($feedSource); + + /** @var CacheItemInterface $cacheItem */ + $cacheItem = $this->feedsCache->getItem('colibo_token_'.$id); + + if (false && $cacheItem->isHit()) { + /** @var string $token */ + $token = $cacheItem->get(); + } else { + try { + $secrets = new SecretsDTO($feedSource); + $client = HttpClient::createForBaseUri($secrets->apiBaseUri); + + $response = $client->request('POST', '/auth/oauth2/connect/token', [ + 'headers' => [ + 'Content-Type' => 'application/x-www-form-urlencoded', + ], + 'body' => [ + 'grant_type' => self::GRANT_TYPE, + 'scope' => self::SCOPE, + 'client_id' => $secrets->clientId, + 'client_secret' => $secrets->clientSecret, + ], + ]); + + $content = $response->getContent(); + $contentDecoded = json_decode($content, false, 512, JSON_THROW_ON_ERROR); + + $token = $contentDecoded->access_token; + + // Expire cache 5 min before token expire + $expireSeconds = intval($contentDecoded->expires_in - 300); + + $cacheItem->set($token); + $cacheItem->expiresAfter($expireSeconds); + $this->feedsCache->save($cacheItem); + } catch (\Throwable $throwable) { + $this->logger->error('{code}: {message}', [ + 'code' => $throwable->getCode(), + 'message' => $throwable->getMessage(), + ]); + + throw $throwable; + } + } + + return $token; + } +} \ No newline at end of file diff --git a/src/Feed/EventDatabaseApiFeedType.php b/src/Feed/SourceType/EventDatabase/EventDatabaseApiFeedType.php similarity index 98% rename from src/Feed/EventDatabaseApiFeedType.php rename to src/Feed/SourceType/EventDatabase/EventDatabaseApiFeedType.php index fb660ef7..358548b9 100644 --- a/src/Feed/EventDatabaseApiFeedType.php +++ b/src/Feed/SourceType/EventDatabase/EventDatabaseApiFeedType.php @@ -2,10 +2,12 @@ declare(strict_types=1); -namespace App\Feed; +namespace App\Feed\SourceType\EventDatabase; use App\Entity\Tenant\Feed; use App\Entity\Tenant\FeedSource; +use App\Feed\FeedTypeInterface; +use App\Feed\FeedOutputModels; use App\Service\FeedService; use Doctrine\ORM\EntityManagerInterface; use Psr\Log\LoggerInterface; @@ -19,7 +21,7 @@ */ class EventDatabaseApiFeedType implements FeedTypeInterface { - final public const string SUPPORTED_FEED_TYPE = SupportedFeedOutputs::POSTER_OUTPUT; + final public const string SUPPORTED_FEED_TYPE = FeedOutputModels::POSTER_OUTPUT; final public const int REQUEST_TIMEOUT = 10; public function __construct( diff --git a/src/Feed/KobaFeedType.php b/src/Feed/SourceType/Koba/KobaFeedType.php similarity index 98% rename from src/Feed/KobaFeedType.php rename to src/Feed/SourceType/Koba/KobaFeedType.php index 2bf319b6..37091092 100644 --- a/src/Feed/KobaFeedType.php +++ b/src/Feed/SourceType/Koba/KobaFeedType.php @@ -2,10 +2,12 @@ declare(strict_types=1); -namespace App\Feed; +namespace App\Feed\SourceType\Koba; use App\Entity\Tenant\Feed; use App\Entity\Tenant\FeedSource; +use App\Feed\FeedTypeInterface; +use App\Feed\FeedOutputModels; use App\Service\FeedService; use Psr\Log\LoggerInterface; use Symfony\Component\HttpFoundation\Request; @@ -15,7 +17,7 @@ /** @deprecated */ class KobaFeedType implements FeedTypeInterface { - final public const string SUPPORTED_FEED_TYPE = SupportedFeedOutputs::CALENDAR_OUTPUT; + final public const string SUPPORTED_FEED_TYPE = FeedOutputModels::CALENDAR_OUTPUT; public function __construct( private readonly FeedService $feedService, diff --git a/src/Feed/NotifiedFeedType.php b/src/Feed/SourceType/Notified/NotifiedFeedType.php similarity index 97% rename from src/Feed/NotifiedFeedType.php rename to src/Feed/SourceType/Notified/NotifiedFeedType.php index 75ac8b9c..5995920e 100644 --- a/src/Feed/NotifiedFeedType.php +++ b/src/Feed/SourceType/Notified/NotifiedFeedType.php @@ -2,10 +2,12 @@ declare(strict_types=1); -namespace App\Feed; +namespace App\Feed\SourceType\Notified; use App\Entity\Tenant\Feed; use App\Entity\Tenant\FeedSource; +use App\Feed\FeedTypeInterface; +use App\Feed\FeedOutputModels; use App\Service\FeedService; use Psr\Log\LoggerInterface; use Symfony\Component\HttpFoundation\Request; @@ -17,7 +19,7 @@ */ class NotifiedFeedType implements FeedTypeInterface { - final public const string SUPPORTED_FEED_TYPE = SupportedFeedOutputs::INSTAGRAM_OUTPUT; + final public const string SUPPORTED_FEED_TYPE = FeedOutputModels::INSTAGRAM_OUTPUT; final public const int REQUEST_TIMEOUT = 10; private const string BASE_URL = 'https://api.listen.notified.com'; diff --git a/src/Feed/RssFeedType.php b/src/Feed/SourceType/Rss/RssFeedType.php similarity index 95% rename from src/Feed/RssFeedType.php rename to src/Feed/SourceType/Rss/RssFeedType.php index 4e395da7..078f3127 100644 --- a/src/Feed/RssFeedType.php +++ b/src/Feed/SourceType/Rss/RssFeedType.php @@ -2,10 +2,12 @@ declare(strict_types=1); -namespace App\Feed; +namespace App\Feed\SourceType\Rss; use App\Entity\Tenant\Feed; use App\Entity\Tenant\FeedSource; +use App\Feed\FeedOutputModels; +use App\Feed\FeedTypeInterface; use FeedIo\Adapter\Http\Client; use FeedIo\Feed\Item; use FeedIo\FeedIo; @@ -15,7 +17,7 @@ class RssFeedType implements FeedTypeInterface { - final public const string SUPPORTED_FEED_TYPE = SupportedFeedOutputs::RSS_OUTPUT; + final public const string SUPPORTED_FEED_TYPE = FeedOutputModels::RSS_OUTPUT; private readonly FeedIo $feedIo; diff --git a/src/Feed/SparkleIOFeedType.php b/src/Feed/SourceType/SparkleIO/SparkleIOFeedType.php similarity index 98% rename from src/Feed/SparkleIOFeedType.php rename to src/Feed/SourceType/SparkleIO/SparkleIOFeedType.php index 64daed7b..692b5947 100644 --- a/src/Feed/SparkleIOFeedType.php +++ b/src/Feed/SourceType/SparkleIO/SparkleIOFeedType.php @@ -2,10 +2,12 @@ declare(strict_types=1); -namespace App\Feed; +namespace App\Feed\SourceType\SparkleIO; use App\Entity\Tenant\Feed; use App\Entity\Tenant\FeedSource; +use App\Feed\FeedTypeInterface; +use App\Feed\FeedOutputModels; use App\Service\FeedService; use Psr\Cache\CacheItemInterface; use Psr\Cache\InvalidArgumentException; @@ -22,7 +24,7 @@ /** @deprecated The SparkleIO service is discontinued. */ class SparkleIOFeedType implements FeedTypeInterface { - final public const string SUPPORTED_FEED_TYPE = SupportedFeedOutputs::INSTAGRAM_OUTPUT; + final public const string SUPPORTED_FEED_TYPE = FeedOutputModels::INSTAGRAM_OUTPUT; final public const int REQUEST_TIMEOUT = 10; diff --git a/src/Model/CalendarEvent.php b/src/Model/CalendarEvent.php deleted file mode 100644 index d8aeea9c..00000000 --- a/src/Model/CalendarEvent.php +++ /dev/null @@ -1,17 +0,0 @@ - [ 'title' => 'Test feed source', 'description' => 'This is a test feed source', - 'feedType' => \App\Feed\EventDatabaseApiFeedType::class, + 'feedType' => \App\Feed\SourceType\EventDatabase\EventDatabaseApiFeedType::class, 'secrets' => [ 'host' => 'https://www.test.dk', ], @@ -77,7 +77,7 @@ public function testCreateFeedSource(): void '@type' => 'FeedSource', 'title' => 'Test feed source', 'description' => 'This is a test feed source', - 'feedType' => \App\Feed\EventDatabaseApiFeedType::class, + 'feedType' => \App\Feed\SourceType\EventDatabase\EventDatabaseApiFeedType::class, 'secrets' => [ 'host' => 'https://www.test.dk', ], @@ -146,7 +146,7 @@ public function testCreateFeedSourceWithEventDatabaseFeedTypeWithoutRequiredSecr 'json' => [ 'title' => 'Test feed source', 'outputType' => 'This is a test output type', - 'feedType' => \App\Feed\EventDatabaseApiFeedType::class, + 'feedType' => \App\Feed\SourceType\EventDatabase\EventDatabaseApiFeedType::class, 'secrets' => [ 'test secret', ], @@ -171,7 +171,7 @@ public function testUpdateFeedSource(): void 'title' => 'Updated title', 'description' => 'Updated description', 'outputType' => 'This is a test output type', - 'feedType' => \App\Feed\EventDatabaseApiFeedType::class, + 'feedType' => \App\Feed\SourceType\EventDatabase\EventDatabaseApiFeedType::class, 'secrets' => [ ], ], @@ -198,7 +198,7 @@ public function testDeleteFeedSource(): void 'title' => 'Test feed source', 'description' => 'This is a test feed source', 'outputType' => 'This is a test output type', - 'feedType' => \App\Feed\EventDatabaseApiFeedType::class, + 'feedType' => \App\Feed\SourceType\EventDatabase\EventDatabaseApiFeedType::class, 'secrets' => [ 'host' => 'https://www.test.dk', ], diff --git a/tests/Feed/NotifiedFeedTypeTest.php b/tests/Feed/NotifiedFeedTypeTest.php index 5a7ef76c..ec8203bb 100644 --- a/tests/Feed/NotifiedFeedTypeTest.php +++ b/tests/Feed/NotifiedFeedTypeTest.php @@ -4,7 +4,7 @@ namespace App\Tests\Feed; -use App\Feed\NotifiedFeedType; +use App\Feed\SourceType\Notified\NotifiedFeedType; use App\Repository\FeedSourceRepository; use App\Repository\SlideRepository; use App\Service\FeedService; diff --git a/tests/Service/FeedServiceTest.php b/tests/Service/FeedServiceTest.php index 9cfd24b2..c28c9ca2 100644 --- a/tests/Service/FeedServiceTest.php +++ b/tests/Service/FeedServiceTest.php @@ -6,13 +6,13 @@ use App\Entity\Tenant\Feed; use App\Entity\Tenant\FeedSource; -use App\Feed\CalendarApiFeedType; -use App\Feed\EventDatabaseApiFeedType; use App\Feed\FeedTypeInterface; -use App\Feed\KobaFeedType; -use App\Feed\NotifiedFeedType; -use App\Feed\RssFeedType; -use App\Feed\SparkleIOFeedType; +use App\Feed\SourceType\Calendar\CalendarApiFeedType; +use App\Feed\SourceType\EventDatabase\EventDatabaseApiFeedType; +use App\Feed\SourceType\Koba\KobaFeedType; +use App\Feed\SourceType\Notified\NotifiedFeedType; +use App\Feed\Type\Rss\RssFeedType; +use App\Feed\SourceType\SparkleIO\SparkleIOFeedType; use App\Service\FeedService; use Doctrine\ORM\EntityManagerInterface; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; From aed4bfc300a0f5dd95ed48a3f375b55f2fa456aa Mon Sep 17 00:00:00 2001 From: turegjorup Date: Thu, 5 Dec 2024 10:33:47 +0100 Subject: [PATCH 2/9] 2533: Add Colibo Feed --- composer.json | 1 + composer.lock | 270 +++++++++--------- config/packages/nelmio_cors.yaml | 2 +- docs/feed/feed-overview.md | 33 +++ migrations/Version20241125085559.php | 36 +++ src/Feed/FeedException.php | 8 + src/Feed/OutputModel/Calendar/Event.php | 17 ++ src/Feed/SourceType/Colibo/ApiClient.php | 254 ++++++++++------ .../SourceType/Colibo/ColiboException.php | 10 + src/Feed/SourceType/Colibo/ColiboFeedType.php | 201 +++++++++++++ src/Feed/SourceType/Colibo/SecretsDTO.php | 35 +++ src/Service/FeedService.php | 2 +- 12 files changed, 642 insertions(+), 227 deletions(-) create mode 100644 docs/feed/feed-overview.md create mode 100644 migrations/Version20241125085559.php create mode 100644 src/Feed/FeedException.php create mode 100644 src/Feed/OutputModel/Calendar/Event.php create mode 100644 src/Feed/SourceType/Colibo/ColiboException.php create mode 100644 src/Feed/SourceType/Colibo/ColiboFeedType.php create mode 100644 src/Feed/SourceType/Colibo/SecretsDTO.php diff --git a/composer.json b/composer.json index f2425bda..e4ff3420 100644 --- a/composer.json +++ b/composer.json @@ -27,6 +27,7 @@ "rlanvin/php-rrule": "^2.2", "symfony/asset": "~6.4.0", "symfony/console": "~6.4.0", + "symfony/dom-crawler": "~6.4.0", "symfony/dotenv": "~6.4.0", "symfony/expression-language": "~6.4.0", "symfony/flex": "^2.0", diff --git a/composer.lock b/composer.lock index fa7f11b5..ac9344e1 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "bed3fa646c40854e6982154f552fcae6", + "content-hash": "67233a09d452101515a4003cb8eee218", "packages": [ { "name": "api-platform/core", @@ -3611,6 +3611,73 @@ }, "time": "2024-09-04T12:55:26+00:00" }, + { + "name": "masterminds/html5", + "version": "2.9.0", + "source": { + "type": "git", + "url": "https://github.com/Masterminds/html5-php.git", + "reference": "f5ac2c0b0a2eefca70b2ce32a5809992227e75a6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Masterminds/html5-php/zipball/f5ac2c0b0a2eefca70b2ce32a5809992227e75a6", + "reference": "f5ac2c0b0a2eefca70b2ce32a5809992227e75a6", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "php": ">=5.3.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.35 || ^5.7.21 || ^6 || ^7 || ^8 || ^9" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.7-dev" + } + }, + "autoload": { + "psr-4": { + "Masterminds\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Matt Butcher", + "email": "technosophos@gmail.com" + }, + { + "name": "Matt Farina", + "email": "matt@mattfarina.com" + }, + { + "name": "Asmir Mustafic", + "email": "goetas@gmail.com" + } + ], + "description": "An HTML5 parser and serializer.", + "homepage": "http://masterminds.github.io/html5-php", + "keywords": [ + "HTML5", + "dom", + "html", + "parser", + "querypath", + "serializer", + "xml" + ], + "support": { + "issues": "https://github.com/Masterminds/html5-php/issues", + "source": "https://github.com/Masterminds/html5-php/tree/2.9.0" + }, + "time": "2024-03-31T07:05:07+00:00" + }, { "name": "monolog/monolog", "version": "3.7.0", @@ -6316,6 +6383,73 @@ ], "time": "2024-09-08T12:31:10+00:00" }, + { + "name": "symfony/dom-crawler", + "version": "v6.4.13", + "source": { + "type": "git", + "url": "https://github.com/symfony/dom-crawler.git", + "reference": "ae074dffb018c37a57071990d16e6152728dd972" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/ae074dffb018c37a57071990d16e6152728dd972", + "reference": "ae074dffb018c37a57071990d16e6152728dd972", + "shasum": "" + }, + "require": { + "masterminds/html5": "^2.6", + "php": ">=8.1", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-mbstring": "~1.0" + }, + "require-dev": { + "symfony/css-selector": "^5.4|^6.0|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\DomCrawler\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Eases DOM navigation for HTML and XML documents", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/dom-crawler/tree/v6.4.13" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-10-25T15:07:50+00:00" + }, { "name": "symfony/dotenv", "version": "v6.4.12", @@ -11666,73 +11800,6 @@ ], "time": "2020-07-06T04:49:32+00:00" }, - { - "name": "masterminds/html5", - "version": "2.9.0", - "source": { - "type": "git", - "url": "https://github.com/Masterminds/html5-php.git", - "reference": "f5ac2c0b0a2eefca70b2ce32a5809992227e75a6" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/Masterminds/html5-php/zipball/f5ac2c0b0a2eefca70b2ce32a5809992227e75a6", - "reference": "f5ac2c0b0a2eefca70b2ce32a5809992227e75a6", - "shasum": "" - }, - "require": { - "ext-dom": "*", - "php": ">=5.3.0" - }, - "require-dev": { - "phpunit/phpunit": "^4.8.35 || ^5.7.21 || ^6 || ^7 || ^8 || ^9" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.7-dev" - } - }, - "autoload": { - "psr-4": { - "Masterminds\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Matt Butcher", - "email": "technosophos@gmail.com" - }, - { - "name": "Matt Farina", - "email": "matt@mattfarina.com" - }, - { - "name": "Asmir Mustafic", - "email": "goetas@gmail.com" - } - ], - "description": "An HTML5 parser and serializer.", - "homepage": "http://masterminds.github.io/html5-php", - "keywords": [ - "HTML5", - "dom", - "html", - "parser", - "querypath", - "serializer", - "xml" - ], - "support": { - "issues": "https://github.com/Masterminds/html5-php/issues", - "source": "https://github.com/Masterminds/html5-php/tree/2.9.0" - }, - "time": "2024-03-31T07:05:07+00:00" - }, { "name": "myclabs/deep-copy", "version": "1.12.0", @@ -13889,73 +13956,6 @@ ], "time": "2024-05-31T14:49:08+00:00" }, - { - "name": "symfony/dom-crawler", - "version": "v6.4.12", - "source": { - "type": "git", - "url": "https://github.com/symfony/dom-crawler.git", - "reference": "9d307ecbcb917001692be333cdc58f474fdb37f0" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/9d307ecbcb917001692be333cdc58f474fdb37f0", - "reference": "9d307ecbcb917001692be333cdc58f474fdb37f0", - "shasum": "" - }, - "require": { - "masterminds/html5": "^2.6", - "php": ">=8.1", - "symfony/polyfill-ctype": "~1.8", - "symfony/polyfill-mbstring": "~1.0" - }, - "require-dev": { - "symfony/css-selector": "^5.4|^6.0|^7.0" - }, - "type": "library", - "autoload": { - "psr-4": { - "Symfony\\Component\\DomCrawler\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Eases DOM navigation for HTML and XML documents", - "homepage": "https://symfony.com", - "support": { - "source": "https://github.com/symfony/dom-crawler/tree/v6.4.12" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2024-09-15T06:35:36+00:00" - }, { "name": "symfony/maker-bundle", "version": "v1.61.0", diff --git a/config/packages/nelmio_cors.yaml b/config/packages/nelmio_cors.yaml index f815f4e3..7b39d06d 100644 --- a/config/packages/nelmio_cors.yaml +++ b/config/packages/nelmio_cors.yaml @@ -1,7 +1,7 @@ nelmio_cors: defaults: origin_regex: true - allow_credentials: false + allow_credentials: true allow_origin: ['%env(CORS_ALLOW_ORIGIN)%'] allow_methods: ['GET', 'OPTIONS', 'POST', 'PUT', 'PATCH', 'DELETE'] allow_headers: ['Content-Type', 'Authorization', 'Authorization-Tenant-Key'] diff --git a/docs/feed/feed-overview.md b/docs/feed/feed-overview.md new file mode 100644 index 00000000..d0f0322d --- /dev/null +++ b/docs/feed/feed-overview.md @@ -0,0 +1,33 @@ +# Feed Overview + +"Feeds" in OS2display are external data sources that can provide up-to-data to slides. The idea is that if you can set +up slide based on a feed and publish it. The Screen Client will then fetch new data from the feed whenever the Slide is +shown on screen. + +The simplest example is a classic RSS news feed. You can set up a slide based on the RSS slide template, configure the +RSS source URL, and whenever the slide is on screen it will show the latest entries from the RSS feed. + +This means that administrators can set up slides and playlists that stays up to date automatically. + +## Architecture + +The "Feed" architecture is designed to enable both generic and custom feed types. To enable this all feed based screen +templates are designed to support a given "feed output model". These are normalized data sets from a given feed type. + +Each feed implementation defines which output model it supports. Thereby multiple feed implementations can support the +same output model. This is done to enable decoupling of the screen templates from the feed implementation. + +For example: + +* If you have a news source that is not a RSS feed you can implement a "FeedSource" that fetches data from your source + then normalizes the data and outputs it as the RSS output model. When setting up RSS slides this feed source can then + be selected as the source for the slide. +* OS2display has calendar templates that can show bookings or meetings. To show data from your specific calendar or + booking system you can implement a "FeedSource" that fetches booking data from your source and normalizes it to match + the calendar output model. + +@todo + +Slide -> Feed -> FeedSource +Auth +Caching \ No newline at end of file diff --git a/migrations/Version20241125085559.php b/migrations/Version20241125085559.php new file mode 100644 index 00000000..3affb967 --- /dev/null +++ b/migrations/Version20241125085559.php @@ -0,0 +1,36 @@ +addSql('UPDATE feed_source SET feed_type = \'App\\Feed\\SourceType\\Calendar\\CalendarApiFeedType\' WHERE feed_type = \'App\\Feed\\CalendarApiFeedType\''); + $this->addSql('UPDATE feed_source SET feed_type = \'App\\Feed\\SourceType\\EventDatabase\\EventDatabaseApiFeedType\' WHERE feed_type = \'App\\Feed\\EventDatabaseApiFeedType\''); + $this->addSql('UPDATE feed_source SET feed_type = \'App\\Feed\\SourceType\\Koba\\KobaFeedType\' WHERE feed_type = \'App\\Feed\\KobaFeedType\''); + $this->addSql('UPDATE feed_source SET feed_type = \'App\\Feed\\SourceType\\Notified\\NotifiedFeedType\' WHERE feed_type = \'App\\Feed\\NotifiedFeedType\''); + $this->addSql('UPDATE feed_source SET feed_type = \'App\\Feed\\SourceType\\Rss\\RssFeedType\' WHERE feed_type = \'App\\Feed\\RssFeedType\''); + $this->addSql('UPDATE feed_source SET feed_type = \'App\\Feed\\SourceType\\SparkleIO\\SparkleIOFeedType\' WHERE feed_type = \'App\\Feed\\SparkleIOFeedType\''); + + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + + } +} diff --git a/src/Feed/FeedException.php b/src/Feed/FeedException.php new file mode 100644 index 00000000..448794a1 --- /dev/null +++ b/src/Feed/FeedException.php @@ -0,0 +1,8 @@ +getApiClient($feedSource); @@ -45,129 +51,196 @@ public function getFeedEntriesNews(FeedSource $feedSource, array $recipients, ar ], ]); - $results = json_decode($response->getContent(), false, 512, JSON_THROW_ON_ERROR); + return json_decode($response->getContent(), false, 512, JSON_THROW_ON_ERROR); + } catch (ColiboException $exception) { + return []; } catch (\Throwable $throwable) { $this->logger->error('{code}: {message}', [ 'code' => $throwable->getCode(), 'message' => $throwable->getMessage(), ]); - throw $throwable; + return []; } - - return $results; } + /** + * Retrieve search groups based on the given feed source and type. + * + * @param FeedSource $feedSource + * @param string $type + * + * @return array + */ public function getSearchGroups(FeedSource $feedSource, string $type = 'WorkGroup'): array { - $responseData = $this->getSearchGroupsPage($feedSource, $type)->toArray(); + try { + $responseData = $this->getSearchGroupsPage($feedSource, $type)->toArray(); - $groups = $responseData['results']; + $groups = $responseData['results']; - $total = $responseData['total']; - $pages = (int) ceil($total / self::BATCH_SIZE); + $total = $responseData['total']; + $pages = (int)ceil($total / self::BATCH_SIZE); - /** @var ResponseInterface[] $responses */ - $responses = []; - for ($page = 1; $page < $pages; ++$page) { - $responses[] = $this->getSearchGroupsPage($feedSource, $type, $page); - } + /** @var ResponseInterface[] $responses */ + $responses = []; + for ($page = 1; $page < $pages; ++$page) { + $responses[] = $this->getSearchGroupsPage($feedSource, $type, $page); + } - foreach ($responses as $response) { - $responseData = $response->toArray(); - $groups = array_merge($groups, $responseData['results']); - } + foreach ($responses as $response) { + $responseData = $response->toArray(); + $groups = array_merge($groups, $responseData['results']); + } + + return $groups; + } catch (ColiboException $exception) { + return []; + } catch (\Throwable $throwable) { + $this->logger->error('{code}: {message}', [ + 'code' => $throwable->getCode(), + 'message' => $throwable->getMessage(), + ]); - return $groups; + return []; + } } /** - * @throws TransportExceptionInterface - * @throws ServerExceptionInterface - * @throws RedirectionExceptionInterface - * @throws DecodingExceptionInterface - * @throws ClientExceptionInterface + * @param FeedSource $feedSource + * + * @return array */ public function getFeedEntryPublishersGroups(FeedSource $feedSource): array { - $client = $this->getApiClient($feedSource); + try { + $client = $this->getApiClient($feedSource); - $response = $client->request('GET', '/api/feedentries/publishers/groups', [ - 'query' => ['groupType' => 'Department'], - ]); + $response = $client->request('GET', '/api/feedentries/publishers/groups', [ + 'query' => ['groupType' => 'Department'], + ]); - $groups = []; - $childGroupIds = []; - foreach ($response->toArray() as $group) { - $groups[] = $group; + $groups = []; + $childGroupIds = []; + foreach ($response->toArray() as $group) { + $groups[] = $group; - if (isset($group['hasChildren']) && $group['hasChildren']) { - $childGroupIds[] = $group['id']; + if (isset($group['hasChildren']) && $group['hasChildren']) { + $childGroupIds[] = $group['id']; + } } - } - $this->getFeedEntryPublishersGroupsChildren($feedSource, $childGroupIds, $groups); + $this->getFeedEntryPublishersGroupsChildren($feedSource, $childGroupIds, $groups); + + return $groups; + } catch (ColiboException $exception) { + return []; + } catch (\Throwable $throwable) { + $this->logger->error('{code}: {message}', [ + 'code' => $throwable->getCode(), + 'message' => $throwable->getMessage(), + ]); - return $groups; + return []; + } } /** - * @throws TransportExceptionInterface - * @throws ServerExceptionInterface - * @throws RedirectionExceptionInterface - * @throws ClientExceptionInterface + * @param FeedSource $feedSource + * @param string $type + * @param int $pageIndex + * @param int $pageSize + * + * @return ResponseInterface + * + * @throws ColiboException */ private function getSearchGroupsPage(FeedSource $feedSource, string $type, int $pageIndex = 0, int $pageSize = self::BATCH_SIZE): ResponseInterface { - $client = $this->getApiClient($feedSource); + try { + $client = $this->getApiClient($feedSource); - return $client->request('GET', '/api/search/groups', [ - 'query' => [ - 'groupSearchQuery.groupTypes' => $type, - 'groupSearchQuery.pageIndex' => $pageIndex, - 'groupSearchQuery.pageSize' => $pageSize, - ], - ]); + return $client->request('GET', '/api/search/groups', [ + 'query' => [ + 'groupSearchQuery.groupTypes' => $type, + 'groupSearchQuery.pageIndex' => $pageIndex, + 'groupSearchQuery.pageSize' => $pageSize, + ], + ]); + } catch (ColiboException $exception) { + throw $exception; + } catch (\Throwable $throwable) { + $this->logger->error('{code}: {message}', [ + 'code' => $throwable->getCode(), + 'message' => $throwable->getMessage(), + ]); + + throw new ColiboException($throwable->getMessage(), $throwable->getCode(), $throwable); + } } /** - * @throws TransportExceptionInterface - * @throws ServerExceptionInterface - * @throws RedirectionExceptionInterface - * @throws DecodingExceptionInterface - * @throws ClientExceptionInterface + * Get + * + * @param FeedSource $feedSource + * @param array $childGroupIds + * @param array $groups + * + * @return void + * + * @throws ColiboException */ private function getFeedEntryPublishersGroupsChildren(FeedSource $feedSource, array $childGroupIds, array &$groups): void { - $client = $this->getApiClient($feedSource); + try { + $client = $this->getApiClient($feedSource); - $batches = array_chunk($childGroupIds, self::BATCH_SIZE); + $batches = array_chunk($childGroupIds, self::BATCH_SIZE); - foreach ($batches as $batch) { - // @see https://symfony.com/doc/current/http_client.html#concurrent-requests - $responses = []; - foreach ($batch as $childGroupId) { - $uri = sprintf('/api/feedentries/publishers/groups/%d/children', $childGroupId); - $responses[] = $client->request('GET', $uri, []); - } + foreach ($batches as $batch) { + // @see https://symfony.com/doc/current/http_client.html#concurrent-requests + $responses = []; + foreach ($batch as $childGroupId) { + $uri = sprintf('/api/feedentries/publishers/groups/%d/children', $childGroupId); + $responses[] = $client->request('GET', $uri, []); + } - $childGroupIds = []; - foreach ($responses as $response) { - foreach ($response->toArray() as $group) { - $groups[] = $group; + $childGroupIds = []; + foreach ($responses as $response) { + foreach ($response->toArray() as $group) { + $groups[] = $group; - if (isset($group['hasChildren']) && $group['hasChildren']) { - $childGroupIds[] = $group['id']; + if (isset($group['hasChildren']) && $group['hasChildren']) { + $childGroupIds[] = $group['id']; + } } } } - } - if (!empty($childGroupIds)) { - $this->getFeedEntryPublishersGroupsChildren($feedSource, $childGroupIds, $groups); + if (!empty($childGroupIds)) { + $this->getFeedEntryPublishersGroupsChildren($feedSource, $childGroupIds, $groups); + } + } catch (ColiboException $exception) { + throw $exception; + } catch (\Throwable $throwable) { + $this->logger->error('{code}: {message}', [ + 'code' => $throwable->getCode(), + 'message' => $throwable->getMessage(), + ]); + + throw new ColiboException($throwable->getMessage(), $throwable->getCode(), $throwable); } } + /** + * Get an authenticated scoped API client for the given FeedSource + * + * @param FeedSource $feedSource + * + * @return HttpClientInterface + * + * @throws ColiboException + */ private function getApiClient(FeedSource $feedSource): HttpClientInterface { $id = ColiboFeedType::getIdKey($feedSource); @@ -179,7 +252,7 @@ private function getApiClient(FeedSource $feedSource): HttpClientInterface $secrets = new SecretsDTO($feedSource); $this->apiClients[$id] = HttpClient::createForBaseUri($secrets->apiBaseUri)->withOptions([ 'headers' => [ - 'Authorization' => 'Bearer '.$this->fetchColiboToken($feedSource), + 'Authorization' => 'Bearer ' . $this->fetchColiboToken($feedSource), 'Accept' => 'application/json', ], ]); @@ -188,21 +261,22 @@ private function getApiClient(FeedSource $feedSource): HttpClientInterface } /** - * @throws TransportExceptionInterface - * @throws \Throwable - * @throws ServerExceptionInterface - * @throws RedirectionExceptionInterface - * @throws ClientExceptionInterface - * @throws \JsonException + * Get Colibo auth token for the given FeedSource + * + * @param FeedSource $feedSource + * + * @return string + * + * @throws ColiboException */ private function fetchColiboToken(FeedSource $feedSource): string { $id = ColiboFeedType::getIdKey($feedSource); /** @var CacheItemInterface $cacheItem */ - $cacheItem = $this->feedsCache->getItem('colibo_token_'.$id); + $cacheItem = $this->feedsCache->getItem('colibo_token_' . $id); - if (false && $cacheItem->isHit()) { + if ($cacheItem->isHit()) { /** @var string $token */ $token = $cacheItem->get(); } else { @@ -239,7 +313,7 @@ private function fetchColiboToken(FeedSource $feedSource): string 'message' => $throwable->getMessage(), ]); - throw $throwable; + throw new ColiboException($throwable->getMessage(), $throwable->getCode(), $throwable); } } diff --git a/src/Feed/SourceType/Colibo/ColiboException.php b/src/Feed/SourceType/Colibo/ColiboException.php new file mode 100644 index 00000000..c78fe300 --- /dev/null +++ b/src/Feed/SourceType/Colibo/ColiboException.php @@ -0,0 +1,10 @@ +feedService->getFeedSourceConfigUrl($feedSource, 'FeedEntryPublishers'); + $feedEntryRecipients = $this->feedService->getFeedSourceConfigUrl($feedSource, 'FeedEntryRecipients'); + + // @TODO: Translation. + return [ + [ + 'key' => 'colibo-feed-entry-publishers-selector', + 'input' => 'multiselect-from-endpoint', + 'endpoint' => $feedEntryPublishers, + 'name' => 'colibo-feed-entry-publishers', + 'label' => 'Vælg afsender grupper for de nyheder du ønsker at vise', + 'helpText' => 'Her vælger du hvilke afsender grupper der skal hentes nyheder fra.', + 'formGroupClasses' => 'col-md-6 mb-3', + ], + [ + 'key' => 'colibo-feed-entry-recipients-selector', + 'input' => 'multiselect-from-endpoint', + 'endpoint' => $feedEntryRecipients, + 'name' => 'colibo-feed-entry-recipients', + 'label' => 'Vælg modtager grupper for de nyheder du ønsker at vise', + 'helpText' => 'Her vælger du hvilke afsender grupper der skal hentes nyheder fra.', + 'formGroupClasses' => 'col-md-6 mb-3', + ], + ]; + } + + public function getData(Feed $feed): array + { + $configuration = $feed->getConfiguration(); + $baseUri = $feed->getFeedSource()->getSecrets()['api_base_uri']; + + $entries = $this->apiClient->getFeedEntriesNews($feed->getFeedSource(), $configuration['colibo-feed-entry-recipients'], $configuration['colibo-feed-entry-publishers']); + + $result = [ + 'title' => 'Colibo Feed', + 'entries' => [], + ]; + + foreach ($entries as $entry) { + $item = new Item(); + $item->setTitle($entry->fields->title); + + $crawler = new Crawler($entry->fields->description); + $summary = ''; + foreach ($crawler as $domElement) { + $summary .= $domElement->textContent; + } + $item->setSummary($summary); + + $item->setPublicId((string) $entry->id); + + $link = sprintf('%s/feedentry/%s', $baseUri, $entry->id); + $item->setLink($link); + + if (null !== $entry->fields->body) { + $crawler = new Crawler($entry->fields->body); + $content = ''; + foreach ($crawler as $domElement) { + $content .= $domElement->textContent; + } + } else { + $content = $item->getSummary(); + } + $item->setContent($content); + + $updated = null === $entry->updated ? $entry->publishDate : $entry->updated; + $item->setLastModified(new \DateTime($updated)); + + $author = new Item\Author(); + $author->setName($entry->publisher->name); + $item->setAuthor($author); + + if ($entry->fields->galleryItems !== null) { + $galleryItems = json_decode($entry->fields->galleryItems, true, 512, JSON_THROW_ON_ERROR); + foreach ($galleryItems as $galleryItem) { + $media = new Item\Media(); + + $large = sprintf('%s/api/files/%s/thumbnail/large', $baseUri, $galleryItem['id']); + $media->setUrl($large); + + $small = sprintf('%s/api/files/%s/thumbnail/small', $baseUri, $galleryItem['id']); + $media->setThumbnail($small); + + $item->addMedia($media); + } + } + + foreach ($entry->recipients as $recipient) { + $category = new Category(); + $category->setLabel($recipient->name); + + $item->addCategory($category); + } + + $result['entries'][] = $item->toArray(); + } + + return $result; + } + + public function getConfigOptions(Request $request, FeedSource $feedSource, string $name): ?array + { + switch ($name) { + case 'FeedEntryPublishers': + case 'FeedEntryRecipients': + $id = self::getIdKey($feedSource); + + /** @var CacheItemInterface $cacheItem */ + $cacheItem = $this->feedsCache->getItem('colibo_feed_entry_publishers_groups_'.$id); + + if ($cacheItem->isHit()) { + $groups = $cacheItem->get(); + } else { + $groups = $this->apiClient->getSearchGroups($feedSource); + + $groups = array_map(fn (array $item) => [ + 'id' => Ulid::generate(), + 'title' => sprintf('%s (%d)', $item['model']['title'], $item['model']['id']), + 'value' => (string) $item['model']['id'], + ], $groups); + + usort($groups, fn ($a, $b) => strcmp($a['title'], $b['title'])); + + $cacheItem->set($groups); + $cacheItem->expiresAfter(self::CACHE_TTL); + $this->feedsCache->save($cacheItem->set($groups)); + } + + return $groups; + + default: + return null; + } + } + + public function getRequiredSecrets(): array + { + return ['client_id', 'client_secret']; + } + + public function getRequiredConfiguration(): array + { + return ['api_base_uri']; + } + + public function getSupportedFeedOutputType(): string + { + return self::SUPPORTED_FEED_TYPE; + } + + public function getSchema(): array + { + return []; + } + + public static function getIdKey(FeedSource $feedSource): string + { + $ulid = $feedSource->getId(); + assert(null !== $ulid); + + return $ulid->toBase32(); + } +} diff --git a/src/Feed/SourceType/Colibo/SecretsDTO.php b/src/Feed/SourceType/Colibo/SecretsDTO.php new file mode 100644 index 00000000..101ebcb8 --- /dev/null +++ b/src/Feed/SourceType/Colibo/SecretsDTO.php @@ -0,0 +1,35 @@ +getSecrets(); + + if (null === $secrets) { + throw new \RuntimeException('No secrets found for feed source.'); + } + + if (!isset($secrets['api_base_uri'], $secrets['client_id'], $secrets['client_secret'])) { + throw new \RuntimeException('Missing required secrets for feed source.'); + } + + if (false === filter_var($secrets['api_base_uri'], FILTER_VALIDATE_URL)) { + throw new \RuntimeException('Invalid api_endpoint.'); + } + + $this->apiBaseUri = rtrim($secrets['api_base_uri'], '/'); + $this->clientId = $secrets['client_id']; + $this->clientSecret = $secrets['client_secret']; + } +} diff --git a/src/Service/FeedService.php b/src/Service/FeedService.php index fdd17039..71eff7e8 100644 --- a/src/Service/FeedService.php +++ b/src/Service/FeedService.php @@ -100,7 +100,7 @@ public function getData(Feed $feed): ?array /** @var CacheItemInterface $cacheItem */ $cacheItem = $this->feedsCache->getItem($feedId); - if ($cacheItem->isHit()) { + if (false && $cacheItem->isHit()) { /** @var array $data */ $data = $cacheItem->get(); } else { From c59f5bd583be3bf2b7f2f50071e375571d924f70 Mon Sep 17 00:00:00 2001 From: Troels Ugilt Jensen <6103205+tuj@users.noreply.github.com> Date: Mon, 9 Dec 2024 13:07:14 +0100 Subject: [PATCH 3/9] 3208: Fixed migration --- migrations/Version20241125085559.php | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/migrations/Version20241125085559.php b/migrations/Version20241125085559.php index 3affb967..56b233be 100644 --- a/migrations/Version20241125085559.php +++ b/migrations/Version20241125085559.php @@ -19,18 +19,21 @@ public function getDescription(): string public function up(Schema $schema): void { - $this->addSql('UPDATE feed_source SET feed_type = \'App\\Feed\\SourceType\\Calendar\\CalendarApiFeedType\' WHERE feed_type = \'App\\Feed\\CalendarApiFeedType\''); - $this->addSql('UPDATE feed_source SET feed_type = \'App\\Feed\\SourceType\\EventDatabase\\EventDatabaseApiFeedType\' WHERE feed_type = \'App\\Feed\\EventDatabaseApiFeedType\''); - $this->addSql('UPDATE feed_source SET feed_type = \'App\\Feed\\SourceType\\Koba\\KobaFeedType\' WHERE feed_type = \'App\\Feed\\KobaFeedType\''); - $this->addSql('UPDATE feed_source SET feed_type = \'App\\Feed\\SourceType\\Notified\\NotifiedFeedType\' WHERE feed_type = \'App\\Feed\\NotifiedFeedType\''); - $this->addSql('UPDATE feed_source SET feed_type = \'App\\Feed\\SourceType\\Rss\\RssFeedType\' WHERE feed_type = \'App\\Feed\\RssFeedType\''); - $this->addSql('UPDATE feed_source SET feed_type = \'App\\Feed\\SourceType\\SparkleIO\\SparkleIOFeedType\' WHERE feed_type = \'App\\Feed\\SparkleIOFeedType\''); - + $this->addSql('UPDATE feed_source SET feed_type = "App\\\\Feed\\\\SourceType\\\\Calendar\\\\CalendarApiFeedType" WHERE feed_type = "App\\\\Feed\\\\CalendarApiFeedType"'); + $this->addSql('UPDATE feed_source SET feed_type = "App\\\\Feed\\\\SourceType\\\\EventDatabase\\\\EventDatabaseApiFeedType" WHERE feed_type = "App\\\\Feed\\\\EventDatabaseApiFeedType"'); + $this->addSql('UPDATE feed_source SET feed_type = "App\\\\Feed\\\\SourceType\\\\Koba\\\\KobaFeedType" WHERE feed_type = "App\\\\Feed\\\\KobaFeedType"'); + $this->addSql('UPDATE feed_source SET feed_type = "App\\\\Feed\\\\SourceType\\\\Notified\\\\NotifiedFeedType" WHERE feed_type = "App\\\\Feed\\\\NotifiedFeedType"'); + $this->addSql('UPDATE feed_source SET feed_type = "App\\\\Feed\\\\SourceType\\\\Rss\\\\RssFeedType" WHERE feed_type = "App\\\\Feed\\\\RssFeedType"'); + $this->addSql('UPDATE feed_source SET feed_type = "App\\\\Feed\\\\SourceType\\\\SparkleIO\\\\SparkleIOFeedType" WHERE feed_type = "App\\\\Feed\\\\SparkleIOFeedType"'); } public function down(Schema $schema): void { - // this down() migration is auto-generated, please modify it to your needs - + $this->addSql('UPDATE feed_source SET feed_type = "App\\\\Feed\\\\CalendarApiFeedType" WHERE feed_type LIKE "App\\\\Feed\\\\SourceType\\\\Calendar\\\\CalendarApiFeedType"'); + $this->addSql('UPDATE feed_source SET feed_type = "App\\\\Feed\\\\EventDatabaseApiFeedType" WHERE feed_type LIKE "App\\\\Feed\\\\SourceType\\\\EventDatabase\\\\EventDatabaseApiFeedType"'); + $this->addSql('UPDATE feed_source SET feed_type = "App\\\\Feed\\\\KobaFeedType" WHERE feed_type LIKE "App\\\\Feed\\\\SourceType\\\\Koba\\\\KobaFeedType"'); + $this->addSql('UPDATE feed_source SET feed_type = "App\\\\Feed\\\\NotifiedFeedType" WHERE feed_type LIKE "App\\\\Feed\\\\SourceType\\\\Notified\\\\NotifiedFeedType"'); + $this->addSql('UPDATE feed_source SET feed_type = "App\\\\Feed\\\\RssFeedType" WHERE feed_type LIKE "App\\\\Feed\\\\SourceType\\\\Rss\\\\RssFeedType"'); + $this->addSql('UPDATE feed_source SET feed_type = "App\\\\Feed\\\\SparkleIOFeedType" WHERE feed_type LIKE "App\\\\Feed\\\\SourceType\\\\SparkleIO\\\\SparkleIOFeedType"'); } } From b6c052b91f49a07b6f37669b76ab40bfe8e83c8d Mon Sep 17 00:00:00 2001 From: Troels Ugilt Jensen <6103205+tuj@users.noreply.github.com> Date: Wed, 18 Dec 2024 10:00:47 +0100 Subject: [PATCH 4/9] 3208: Cleaned up implementation --- .env | 2 +- src/Feed/OutputModel/ConfigOption.php | 11 ++ src/Feed/SourceType/Colibo/ApiClient.php | 132 +++--------------- src/Feed/SourceType/Colibo/ColiboFeedType.php | 99 +++++++++---- src/Feed/SourceType/Rss/RssFeedType.php | 10 +- 5 files changed, 107 insertions(+), 147 deletions(-) create mode 100644 src/Feed/OutputModel/ConfigOption.php diff --git a/.env b/.env index 114aa336..697751b0 100644 --- a/.env +++ b/.env @@ -93,7 +93,7 @@ REDIS_CACHE_DSN=redis://redis:6379/0 ###< redis ### ###> Calendar Api Feed Source ### -# See docs/calendar-api-feed.md for variable explainations. +# See docs/feed/calendar-api-feed.md for variable explainations. CALENDAR_API_FEED_SOURCE_LOCATION_ENDPOINT= CALENDAR_API_FEED_SOURCE_RESOURCE_ENDPOINT= CALENDAR_API_FEED_SOURCE_EVENT_ENDPOINT= diff --git a/src/Feed/OutputModel/ConfigOption.php b/src/Feed/OutputModel/ConfigOption.php new file mode 100644 index 00000000..8fd9cd7c --- /dev/null +++ b/src/Feed/OutputModel/ConfigOption.php @@ -0,0 +1,11 @@ +getApiClient($feedSource); - $response = $client->request('GET', '/api/feedentries/news', [ + $options = [ 'headers' => [ 'Content-Type' => 'application/json', ], 'query' => [ - 'getQuery.recipients' => $recipients, + 'recipients' => array_map(fn($recipient) => (object) [ + 'Id' => $recipient, + 'Type' => 'Group' + ], $recipients), + 'publishers' => array_map(fn($publisher) => (object) [ + 'Id' => $publisher, + 'Type' => 'Group' + ], $publishers), ], - ]); + ]; + + $response = $client->request('GET', '/api/feedentries/news', $options); return json_decode($response->getContent(), false, 512, JSON_THROW_ON_ERROR); - } catch (ColiboException $exception) { - return []; } catch (\Throwable $throwable) { $this->logger->error('{code}: {message}', [ 'code' => $throwable->getCode(), @@ -94,47 +101,6 @@ public function getSearchGroups(FeedSource $feedSource, string $type = 'WorkGrou } return $groups; - } catch (ColiboException $exception) { - return []; - } catch (\Throwable $throwable) { - $this->logger->error('{code}: {message}', [ - 'code' => $throwable->getCode(), - 'message' => $throwable->getMessage(), - ]); - - return []; - } - } - - /** - * @param FeedSource $feedSource - * - * @return array - */ - public function getFeedEntryPublishersGroups(FeedSource $feedSource): array - { - try { - $client = $this->getApiClient($feedSource); - - $response = $client->request('GET', '/api/feedentries/publishers/groups', [ - 'query' => ['groupType' => 'Department'], - ]); - - $groups = []; - $childGroupIds = []; - foreach ($response->toArray() as $group) { - $groups[] = $group; - - if (isset($group['hasChildren']) && $group['hasChildren']) { - $childGroupIds[] = $group['id']; - } - } - - $this->getFeedEntryPublishersGroupsChildren($feedSource, $childGroupIds, $groups); - - return $groups; - } catch (ColiboException $exception) { - return []; } catch (\Throwable $throwable) { $this->logger->error('{code}: {message}', [ 'code' => $throwable->getCode(), @@ -170,64 +136,6 @@ private function getSearchGroupsPage(FeedSource $feedSource, string $type, int $ } catch (ColiboException $exception) { throw $exception; } catch (\Throwable $throwable) { - $this->logger->error('{code}: {message}', [ - 'code' => $throwable->getCode(), - 'message' => $throwable->getMessage(), - ]); - - throw new ColiboException($throwable->getMessage(), $throwable->getCode(), $throwable); - } - } - - /** - * Get - * - * @param FeedSource $feedSource - * @param array $childGroupIds - * @param array $groups - * - * @return void - * - * @throws ColiboException - */ - private function getFeedEntryPublishersGroupsChildren(FeedSource $feedSource, array $childGroupIds, array &$groups): void - { - try { - $client = $this->getApiClient($feedSource); - - $batches = array_chunk($childGroupIds, self::BATCH_SIZE); - - foreach ($batches as $batch) { - // @see https://symfony.com/doc/current/http_client.html#concurrent-requests - $responses = []; - foreach ($batch as $childGroupId) { - $uri = sprintf('/api/feedentries/publishers/groups/%d/children', $childGroupId); - $responses[] = $client->request('GET', $uri, []); - } - - $childGroupIds = []; - foreach ($responses as $response) { - foreach ($response->toArray() as $group) { - $groups[] = $group; - - if (isset($group['hasChildren']) && $group['hasChildren']) { - $childGroupIds[] = $group['id']; - } - } - } - } - - if (!empty($childGroupIds)) { - $this->getFeedEntryPublishersGroupsChildren($feedSource, $childGroupIds, $groups); - } - } catch (ColiboException $exception) { - throw $exception; - } catch (\Throwable $throwable) { - $this->logger->error('{code}: {message}', [ - 'code' => $throwable->getCode(), - 'message' => $throwable->getMessage(), - ]); - throw new ColiboException($throwable->getMessage(), $throwable->getCode(), $throwable); } } @@ -252,7 +160,7 @@ private function getApiClient(FeedSource $feedSource): HttpClientInterface $secrets = new SecretsDTO($feedSource); $this->apiClients[$id] = HttpClient::createForBaseUri($secrets->apiBaseUri)->withOptions([ 'headers' => [ - 'Authorization' => 'Bearer ' . $this->fetchColiboToken($feedSource), + 'Authorization' => 'Bearer ' . $this->fetchToken($feedSource), 'Accept' => 'application/json', ], ]); @@ -261,7 +169,7 @@ private function getApiClient(FeedSource $feedSource): HttpClientInterface } /** - * Get Colibo auth token for the given FeedSource + * Get the auth token for the given FeedSource * * @param FeedSource $feedSource * @@ -269,7 +177,7 @@ private function getApiClient(FeedSource $feedSource): HttpClientInterface * * @throws ColiboException */ - private function fetchColiboToken(FeedSource $feedSource): string + private function fetchToken(FeedSource $feedSource): string { $id = ColiboFeedType::getIdKey($feedSource); @@ -287,6 +195,7 @@ private function fetchColiboToken(FeedSource $feedSource): string $response = $client->request('POST', '/auth/oauth2/connect/token', [ 'headers' => [ 'Content-Type' => 'application/x-www-form-urlencoded', + 'Accept' => 'application/json', ], 'body' => [ 'grant_type' => self::GRANT_TYPE, @@ -308,15 +217,10 @@ private function fetchColiboToken(FeedSource $feedSource): string $cacheItem->expiresAfter($expireSeconds); $this->feedsCache->save($cacheItem); } catch (\Throwable $throwable) { - $this->logger->error('{code}: {message}', [ - 'code' => $throwable->getCode(), - 'message' => $throwable->getMessage(), - ]); - throw new ColiboException($throwable->getMessage(), $throwable->getCode(), $throwable); } } return $token; } -} \ No newline at end of file +} diff --git a/src/Feed/SourceType/Colibo/ColiboFeedType.php b/src/Feed/SourceType/Colibo/ColiboFeedType.php index 771fd641..e72afe79 100644 --- a/src/Feed/SourceType/Colibo/ColiboFeedType.php +++ b/src/Feed/SourceType/Colibo/ColiboFeedType.php @@ -8,9 +8,11 @@ use App\Entity\Tenant\FeedSource; use App\Feed\FeedOutputModels; use App\Feed\FeedTypeInterface; +use App\Feed\OutputModel\ConfigOption; use App\Service\FeedService; use FeedIo\Feed\Item; use FeedIo\Feed\Node\Category; +use Psr\Cache\CacheItemInterface; use Symfony\Component\DomCrawler\Crawler; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Uid\Ulid; @@ -36,29 +38,28 @@ public function __construct( public function getAdminFormOptions(FeedSource $feedSource): array { - $feedEntryPublishers = $this->feedService->getFeedSourceConfigUrl($feedSource, 'FeedEntryPublishers'); - $feedEntryRecipients = $this->feedService->getFeedSourceConfigUrl($feedSource, 'FeedEntryRecipients'); + $feedEntryRecipients = $this->feedService->getFeedSourceConfigUrl($feedSource, 'recipients'); + $feedEntryPublishers = $this->feedService->getFeedSourceConfigUrl($feedSource, 'publishers'); - // @TODO: Translation. return [ [ - 'key' => 'colibo-feed-entry-publishers-selector', + 'key' => 'colibo-feed-type-publishers-selector', 'input' => 'multiselect-from-endpoint', - 'endpoint' => $feedEntryPublishers, - 'name' => 'colibo-feed-entry-publishers', - 'label' => 'Vælg afsender grupper for de nyheder du ønsker at vise', - 'helpText' => 'Her vælger du hvilke afsender grupper der skal hentes nyheder fra.', - 'formGroupClasses' => 'col-md-6 mb-3', + 'endpoint' => $feedEntryRecipients, + 'name' => 'recipients', + 'label' => 'Modtagergrupper', + 'helpText' => 'Vælg hvilke grupper, der skal hentes nyheder fra.', + 'formGroupClasses' => 'mb-3', ], [ - 'key' => 'colibo-feed-entry-recipients-selector', + 'key' => 'colibo-feed-type-publishers-selector', 'input' => 'multiselect-from-endpoint', - 'endpoint' => $feedEntryRecipients, - 'name' => 'colibo-feed-entry-recipients', - 'label' => 'Vælg modtager grupper for de nyheder du ønsker at vise', - 'helpText' => 'Her vælger du hvilke afsender grupper der skal hentes nyheder fra.', - 'formGroupClasses' => 'col-md-6 mb-3', - ], + 'endpoint' => $feedEntryPublishers, + 'name' => 'publishers', + 'label' => 'Afsendergrupper', + 'helpText' => 'Vælg afsendergrupper for at begrænse hvilke afsenderes nyheder, der vises fra modtagergruppen. Hvis den ikke er valgt vises alle nyheder fra modtagergruppen.', + 'formGroupClasses' => 'mb-3', + ] ]; } @@ -67,13 +68,21 @@ public function getData(Feed $feed): array $configuration = $feed->getConfiguration(); $baseUri = $feed->getFeedSource()->getSecrets()['api_base_uri']; - $entries = $this->apiClient->getFeedEntriesNews($feed->getFeedSource(), $configuration['colibo-feed-entry-recipients'], $configuration['colibo-feed-entry-publishers']); - $result = [ - 'title' => 'Colibo Feed', + 'title' => 'Intranet', 'entries' => [], ]; + $recipients = $configuration['recipients'] ?? null; + $publishers = $configuration['publishers'] ?? []; + + if (null === $recipients) { + return $result; + } + + $entries = $this->apiClient->getFeedEntriesNews($feed->getFeedSource(), $recipients, $publishers); + + foreach ($entries as $entry) { $item = new Item(); $item->setTitle($entry->fields->title); @@ -139,25 +148,26 @@ public function getData(Feed $feed): array public function getConfigOptions(Request $request, FeedSource $feedSource, string $name): ?array { switch ($name) { - case 'FeedEntryPublishers': - case 'FeedEntryRecipients': + case 'recipients': + case 'publishers': $id = self::getIdKey($feedSource); /** @var CacheItemInterface $cacheItem */ - $cacheItem = $this->feedsCache->getItem('colibo_feed_entry_publishers_groups_'.$id); + $cacheItem = $this->feedsCache->getItem('colibo_feed_entry_groups_'.$id); if ($cacheItem->isHit()) { $groups = $cacheItem->get(); } else { $groups = $this->apiClient->getSearchGroups($feedSource); - $groups = array_map(fn (array $item) => [ - 'id' => Ulid::generate(), - 'title' => sprintf('%s (%d)', $item['model']['title'], $item['model']['id']), - 'value' => (string) $item['model']['id'], - ], $groups); + $groups = array_map(fn (array $item) => + new ConfigOption( + Ulid::generate(), + sprintf('%s (%d)', $item['model']['title'], $item['model']['id']), + (string) $item['model']['id'] + ), $groups); - usort($groups, fn ($a, $b) => strcmp($a['title'], $b['title'])); + usort($groups, fn ($a, $b) => strcmp($a->title, $b->title)); $cacheItem->set($groups); $cacheItem->expiresAfter(self::CACHE_TTL); @@ -173,12 +183,23 @@ public function getConfigOptions(Request $request, FeedSource $feedSource, strin public function getRequiredSecrets(): array { - return ['client_id', 'client_secret']; + return [ + 'api_base_uri' => [ + 'type' => 'string', + 'exposeValue' => true, + ], + 'client_id' => [ + 'type' => 'string' + ], + 'client_secret' => [ + 'type' => 'string' + ], + ]; } public function getRequiredConfiguration(): array { - return ['api_base_uri']; + return ['recipients', 'publishers']; } public function getSupportedFeedOutputType(): string @@ -188,7 +209,23 @@ public function getSupportedFeedOutputType(): string public function getSchema(): array { - return []; + return [ + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'type' => 'object', + 'properties' => [ + 'api_base_uri' => [ + 'type' => 'string', + 'format' => 'uri', + ], + 'client_id' => [ + 'type' => 'string', + ], + 'client_secret' => [ + 'type' => 'string', + ], + ], + 'required' => ['api_base_uri', 'client_id', 'client_secret'], + ]; } public static function getIdKey(FeedSource $feedSource): string diff --git a/src/Feed/SourceType/Rss/RssFeedType.php b/src/Feed/SourceType/Rss/RssFeedType.php index 078f3127..90abf690 100644 --- a/src/Feed/SourceType/Rss/RssFeedType.php +++ b/src/Feed/SourceType/Rss/RssFeedType.php @@ -56,7 +56,15 @@ public function getData(Feed $feed): array /** @var Item $item */ foreach ($feedResult->getFeed() as $item) { - $result['entries'][] = $item->toArray(); + $entry = $item->toArray(); + + if (empty($entry['author'])) { + $entry['author'] = [ + 'name' => $feedResult->getFeed()->getTitle(), + ]; + } + + $result['entries'][] = $entry; if (!is_null($numberOfEntries) && count($result['entries']) >= $numberOfEntries) { break; From e05ab3fd29ab3fe3083f7f6907b05c0ff5b8b00a Mon Sep 17 00:00:00 2001 From: Troels Ugilt Jensen <6103205+tuj@users.noreply.github.com> Date: Thu, 19 Dec 2024 14:19:34 +0100 Subject: [PATCH 5/9] 3208: Changed to only show allowed recipients in slide creation flow --- src/Feed/SourceType/Colibo/ApiClient.php | 5 +- src/Feed/SourceType/Colibo/ColiboFeedType.php | 59 ++++++++++++------- 2 files changed, 43 insertions(+), 21 deletions(-) diff --git a/src/Feed/SourceType/Colibo/ApiClient.php b/src/Feed/SourceType/Colibo/ApiClient.php index 1ad09ac2..0c08d96b 100644 --- a/src/Feed/SourceType/Colibo/ApiClient.php +++ b/src/Feed/SourceType/Colibo/ApiClient.php @@ -34,10 +34,12 @@ public function __construct( * An array of recipient ID's to filter by * @param array $publishers * An array of publisher ID's to filter by + * @param int $pageSize + * Number of elements to retrieve * * @return mixed */ - public function getFeedEntriesNews(FeedSource $feedSource, array $recipients = [], array $publishers = []): mixed + public function getFeedEntriesNews(FeedSource $feedSource, array $recipients = [], array $publishers = [], int $pageSize = 10): mixed { try { $client = $this->getApiClient($feedSource); @@ -55,6 +57,7 @@ public function getFeedEntriesNews(FeedSource $feedSource, array $recipients = [ 'Id' => $publisher, 'Type' => 'Group' ], $publishers), + 'pageSize' => $pageSize, ], ]; diff --git a/src/Feed/SourceType/Colibo/ColiboFeedType.php b/src/Feed/SourceType/Colibo/ColiboFeedType.php index e72afe79..fb376c0e 100644 --- a/src/Feed/SourceType/Colibo/ColiboFeedType.php +++ b/src/Feed/SourceType/Colibo/ColiboFeedType.php @@ -13,6 +13,7 @@ use FeedIo\Feed\Item; use FeedIo\Feed\Node\Category; use Psr\Cache\CacheItemInterface; +use Symfony\Component\BrowserKit\Exception\JsonException; use Symfony\Component\DomCrawler\Crawler; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Uid\Ulid; @@ -38,28 +39,28 @@ public function __construct( public function getAdminFormOptions(FeedSource $feedSource): array { - $feedEntryRecipients = $this->feedService->getFeedSourceConfigUrl($feedSource, 'recipients'); - $feedEntryPublishers = $this->feedService->getFeedSourceConfigUrl($feedSource, 'publishers'); + $feedEntryRecipients = $this->feedService->getFeedSourceConfigUrl($feedSource, 'allowed-recipients'); return [ [ - 'key' => 'colibo-feed-type-publishers-selector', + 'key' => 'colibo-feed-type-recipient-selector', 'input' => 'multiselect-from-endpoint', 'endpoint' => $feedEntryRecipients, 'name' => 'recipients', - 'label' => 'Modtagergrupper', + 'label' => 'Grupper', 'helpText' => 'Vælg hvilke grupper, der skal hentes nyheder fra.', 'formGroupClasses' => 'mb-3', ], [ - 'key' => 'colibo-feed-type-publishers-selector', - 'input' => 'multiselect-from-endpoint', - 'endpoint' => $feedEntryPublishers, - 'name' => 'publishers', - 'label' => 'Afsendergrupper', - 'helpText' => 'Vælg afsendergrupper for at begrænse hvilke afsenderes nyheder, der vises fra modtagergruppen. Hvis den ikke er valgt vises alle nyheder fra modtagergruppen.', + 'key' => 'colibo-feed-type-page-size', + 'input' => 'input', + 'type' => 'number', + 'name' => 'page_size', + 'label' => 'Antal nyheder', + 'defaultValue' => '5', + 'helpText' => 'Vælg hvor mange nyheder der maksimalt skal hentes.', 'formGroupClasses' => 'mb-3', - ] + ], ]; } @@ -73,15 +74,15 @@ public function getData(Feed $feed): array 'entries' => [], ]; - $recipients = $configuration['recipients'] ?? null; + $recipients = $configuration['recipients'] ?? []; $publishers = $configuration['publishers'] ?? []; + $pageSize = isset($configuration['page_size']) ? (int) $configuration['page_size'] : 10; - if (null === $recipients) { + if (empty($recipients)) { return $result; } - $entries = $this->apiClient->getFeedEntriesNews($feed->getFeedSource(), $recipients, $publishers); - + $entries = $this->apiClient->getFeedEntriesNews($feed->getFeedSource(), $recipients, $publishers, $pageSize); foreach ($entries as $entry) { $item = new Item(); @@ -118,7 +119,12 @@ public function getData(Feed $feed): array $item->setAuthor($author); if ($entry->fields->galleryItems !== null) { - $galleryItems = json_decode($entry->fields->galleryItems, true, 512, JSON_THROW_ON_ERROR); + try { + $galleryItems = json_decode($entry->fields->galleryItems, true, 512, JSON_THROW_ON_ERROR); + } catch (\JsonException) { + $galleryItems = []; + } + foreach ($galleryItems as $galleryItem) { $media = new Item\Media(); @@ -148,8 +154,12 @@ public function getData(Feed $feed): array public function getConfigOptions(Request $request, FeedSource $feedSource, string $name): ?array { switch ($name) { + case 'allowed-recipients': + $allowedIds = $feedSource->getSecrets()['allowed_recipients'] ?? []; + $allGroupOptions = $this->getConfigOptions($request, $feedSource, 'recipients'); + + return array_values(array_filter($allGroupOptions, fn(ConfigOption $group) => in_array($group->value, $allowedIds))); case 'recipients': - case 'publishers': $id = self::getIdKey($feedSource); /** @var CacheItemInterface $cacheItem */ @@ -175,7 +185,6 @@ public function getConfigOptions(Request $request, FeedSource $feedSource, strin } return $groups; - default: return null; } @@ -194,12 +203,16 @@ public function getRequiredSecrets(): array 'client_secret' => [ 'type' => 'string' ], + 'allowed_recipients' => [ + 'type' => 'string_array', + 'exposeValue' => true, + ] ]; } public function getRequiredConfiguration(): array { - return ['recipients', 'publishers']; + return ['recipients', 'page_size']; } public function getSupportedFeedOutputType(): string @@ -223,8 +236,14 @@ public function getSchema(): array 'client_secret' => [ 'type' => 'string', ], + 'allowed_recipients' => [ + 'type' => 'array', + 'items' => [ + 'type' => 'string', + ], + ], ], - 'required' => ['api_base_uri', 'client_id', 'client_secret'], + 'required' => ['api_base_uri', 'client_id', 'client_secret', 'allowed_recipients'], ]; } From 9148f421885f9609e219cbe4199b84cece2a624a Mon Sep 17 00:00:00 2001 From: Troels Ugilt Jensen <6103205+tuj@users.noreply.github.com> Date: Thu, 19 Dec 2024 15:31:28 +0100 Subject: [PATCH 6/9] 3208: Fixed coding standards issues --- fixtures/feed_source.yaml | 4 +- psalm-baseline.xml | 27 ---------- src/Feed/FeedException.php | 5 +- src/Feed/OutputModel/ConfigOption.php | 7 ++- src/Feed/SourceType/Colibo/ApiClient.php | 33 +++++++------ .../SourceType/Colibo/ColiboException.php | 5 +- src/Feed/SourceType/Colibo/ColiboFeedType.php | 49 +++++++++++-------- src/Feed/SourceType/Colibo/SecretsDTO.php | 2 +- .../EventDatabaseApiFeedType.php | 26 +++++++--- src/Feed/SourceType/Koba/KobaFeedType.php | 6 +-- .../SourceType/Notified/NotifiedFeedType.php | 2 +- .../SparkleIO/SparkleIOFeedType.php | 6 +-- src/Service/FeedService.php | 2 +- tests/Service/FeedServiceTest.php | 2 +- 14 files changed, 87 insertions(+), 89 deletions(-) diff --git a/fixtures/feed_source.yaml b/fixtures/feed_source.yaml index 989d60c7..4c3180b6 100644 --- a/fixtures/feed_source.yaml +++ b/fixtures/feed_source.yaml @@ -2,7 +2,7 @@ App\Entity\Tenant\FeedSource: feed (template): description: - feedType: "App\\Feed\\RssFeedType" + feedType: "App\\Feed\\SourceType\\Rss\\RssFeedType" secrets: [ ] supportedFeedOutputType: 'rss' createdAt (unique): '' @@ -16,7 +16,7 @@ App\Entity\Tenant\FeedSource: tenant: '@tenant_xyz' feed_source_abc_notified (extends feed): title: 'feed_source_abc_notified' - feedType: "App\\Feed\\RssFeedType" + feedType: "App\\Feed\\SourceType\\Rss\\RssFeedType" secrets: token: '1234567890' supportedFeedOutputType: 'instagram' diff --git a/psalm-baseline.xml b/psalm-baseline.xml index ef8d120f..e2174f7b 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -285,33 +285,6 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/Feed/FeedException.php b/src/Feed/FeedException.php index 448794a1..e164db7b 100644 --- a/src/Feed/FeedException.php +++ b/src/Feed/FeedException.php @@ -1,8 +1,9 @@ 'application/json', ], 'query' => [ - 'recipients' => array_map(fn($recipient) => (object) [ + 'recipients' => array_map(fn ($recipient) => (object) [ 'Id' => $recipient, - 'Type' => 'Group' + 'Type' => 'Group', ], $recipients), - 'publishers' => array_map(fn($publisher) => (object) [ + 'publishers' => array_map(fn ($publisher) => (object) [ 'Id' => $publisher, - 'Type' => 'Group' + 'Type' => 'Group', ], $publishers), 'pageSize' => $pageSize, ], @@ -90,7 +91,7 @@ public function getSearchGroups(FeedSource $feedSource, string $type = 'WorkGrou $groups = $responseData['results']; $total = $responseData['total']; - $pages = (int)ceil($total / self::BATCH_SIZE); + $pages = (int) ceil($total / self::BATCH_SIZE); /** @var ResponseInterface[] $responses */ $responses = []; @@ -139,12 +140,12 @@ private function getSearchGroupsPage(FeedSource $feedSource, string $type, int $ } catch (ColiboException $exception) { throw $exception; } catch (\Throwable $throwable) { - throw new ColiboException($throwable->getMessage(), $throwable->getCode(), $throwable); + throw new ColiboException($throwable->getMessage(), (int) $throwable->getCode(), $throwable); } } /** - * Get an authenticated scoped API client for the given FeedSource + * Get an authenticated scoped API client for the given FeedSource. * * @param FeedSource $feedSource * @@ -163,7 +164,7 @@ private function getApiClient(FeedSource $feedSource): HttpClientInterface $secrets = new SecretsDTO($feedSource); $this->apiClients[$id] = HttpClient::createForBaseUri($secrets->apiBaseUri)->withOptions([ 'headers' => [ - 'Authorization' => 'Bearer ' . $this->fetchToken($feedSource), + 'Authorization' => 'Bearer '.$this->fetchToken($feedSource), 'Accept' => 'application/json', ], ]); @@ -172,7 +173,7 @@ private function getApiClient(FeedSource $feedSource): HttpClientInterface } /** - * Get the auth token for the given FeedSource + * Get the auth token for the given FeedSource. * * @param FeedSource $feedSource * @@ -185,7 +186,7 @@ private function fetchToken(FeedSource $feedSource): string $id = ColiboFeedType::getIdKey($feedSource); /** @var CacheItemInterface $cacheItem */ - $cacheItem = $this->feedsCache->getItem('colibo_token_' . $id); + $cacheItem = $this->feedsCache->getItem('colibo_token_'.$id); if ($cacheItem->isHit()) { /** @var string $token */ @@ -220,7 +221,7 @@ private function fetchToken(FeedSource $feedSource): string $cacheItem->expiresAfter($expireSeconds); $this->feedsCache->save($cacheItem); } catch (\Throwable $throwable) { - throw new ColiboException($throwable->getMessage(), $throwable->getCode(), $throwable); + throw new ColiboException($throwable->getMessage(), (int) $throwable->getCode(), $throwable); } } diff --git a/src/Feed/SourceType/Colibo/ColiboException.php b/src/Feed/SourceType/Colibo/ColiboException.php index c78fe300..6ca7f2c5 100644 --- a/src/Feed/SourceType/Colibo/ColiboException.php +++ b/src/Feed/SourceType/Colibo/ColiboException.php @@ -1,10 +1,11 @@ getConfiguration(); - $baseUri = $feed->getFeedSource()->getSecrets()['api_base_uri']; + $secrets = $feed->getFeedSource()?->getSecrets() ?? []; $result = [ 'title' => 'Intranet', 'entries' => [], ]; + $baseUri = $secrets['api_base_uri']; $recipients = $configuration['recipients'] ?? []; $publishers = $configuration['publishers'] ?? []; $pageSize = isset($configuration['page_size']) ? (int) $configuration['page_size'] : 10; - if (empty($recipients)) { + if (empty($baseUri) || 0 === count($recipients)) { return $result; } - $entries = $this->apiClient->getFeedEntriesNews($feed->getFeedSource(), $recipients, $publishers, $pageSize); + $feedSource = $feed->getFeedSource(); + + if (null === $feedSource) { + return $result; + } + + $entries = $this->apiClient->getFeedEntriesNews($feedSource, $recipients, $publishers, $pageSize); foreach ($entries as $entry) { $item = new Item(); @@ -111,14 +117,14 @@ public function getData(Feed $feed): array } $item->setContent($content); - $updated = null === $entry->updated ? $entry->publishDate : $entry->updated; + $updated = $entry->updated ?? $entry->publishDate; $item->setLastModified(new \DateTime($updated)); $author = new Item\Author(); $author->setName($entry->publisher->name); $item->setAuthor($author); - if ($entry->fields->galleryItems !== null) { + if (null !== $entry->fields->galleryItems) { try { $galleryItems = json_decode($entry->fields->galleryItems, true, 512, JSON_THROW_ON_ERROR); } catch (\JsonException) { @@ -155,10 +161,14 @@ public function getConfigOptions(Request $request, FeedSource $feedSource, strin { switch ($name) { case 'allowed-recipients': - $allowedIds = $feedSource->getSecrets()['allowed_recipients'] ?? []; + $allowedIds = $feedSource->getSecrets()['allowed_recipients'] ?? []; $allGroupOptions = $this->getConfigOptions($request, $feedSource, 'recipients'); - return array_values(array_filter($allGroupOptions, fn(ConfigOption $group) => in_array($group->value, $allowedIds))); + if (null === $allGroupOptions) { + return []; + } + + return array_values(array_filter($allGroupOptions, fn (ConfigOption $group) => in_array($group->value, $allowedIds))); case 'recipients': $id = self::getIdKey($feedSource); @@ -170,12 +180,11 @@ public function getConfigOptions(Request $request, FeedSource $feedSource, strin } else { $groups = $this->apiClient->getSearchGroups($feedSource); - $groups = array_map(fn (array $item) => - new ConfigOption( - Ulid::generate(), - sprintf('%s (%d)', $item['model']['title'], $item['model']['id']), - (string) $item['model']['id'] - ), $groups); + $groups = array_map(fn (array $item) => new ConfigOption( + Ulid::generate(), + sprintf('%s (%d)', $item['model']['title'], $item['model']['id']), + (string) $item['model']['id'] + ), $groups); usort($groups, fn ($a, $b) => strcmp($a->title, $b->title)); @@ -198,15 +207,15 @@ public function getRequiredSecrets(): array 'exposeValue' => true, ], 'client_id' => [ - 'type' => 'string' + 'type' => 'string', ], 'client_secret' => [ - 'type' => 'string' + 'type' => 'string', ], 'allowed_recipients' => [ 'type' => 'string_array', 'exposeValue' => true, - ] + ], ]; } @@ -243,7 +252,7 @@ public function getSchema(): array ], ], ], - 'required' => ['api_base_uri', 'client_id', 'client_secret', 'allowed_recipients'], + 'required' => ['api_base_uri', 'client_id', 'client_secret'], ]; } diff --git a/src/Feed/SourceType/Colibo/SecretsDTO.php b/src/Feed/SourceType/Colibo/SecretsDTO.php index 101ebcb8..761e86ff 100644 --- a/src/Feed/SourceType/Colibo/SecretsDTO.php +++ b/src/Feed/SourceType/Colibo/SecretsDTO.php @@ -28,7 +28,7 @@ public function __construct(FeedSource $feedSource) throw new \RuntimeException('Invalid api_endpoint.'); } - $this->apiBaseUri = rtrim($secrets['api_base_uri'], '/'); + $this->apiBaseUri = rtrim((string) $secrets['api_base_uri'], '/'); $this->clientId = $secrets['client_id']; $this->clientSecret = $secrets['client_secret']; } diff --git a/src/Feed/SourceType/EventDatabase/EventDatabaseApiFeedType.php b/src/Feed/SourceType/EventDatabase/EventDatabaseApiFeedType.php index 358548b9..ce8cf13c 100644 --- a/src/Feed/SourceType/EventDatabase/EventDatabaseApiFeedType.php +++ b/src/Feed/SourceType/EventDatabase/EventDatabaseApiFeedType.php @@ -6,8 +6,8 @@ use App\Entity\Tenant\Feed; use App\Entity\Tenant\FeedSource; -use App\Feed\FeedTypeInterface; use App\Feed\FeedOutputModels; +use App\Feed\FeedTypeInterface; use App\Service\FeedService; use Doctrine\ORM\EntityManagerInterface; use Psr\Log\LoggerInterface; @@ -52,12 +52,21 @@ public function getData(Feed $feed): array $tags = $configuration['subscriptionTagValue'] ?? null; $numberOfItems = $configuration['subscriptionNumberValue'] ?? 5; - $queryParams = array_filter([ + $queryParams = [ 'items_per_page' => $numberOfItems, - 'occurrences.place.id' => array_map(static fn ($place) => str_replace('/api/places/', '', (string) $place['value']), $places), - 'organizer.id' => array_map(static fn ($organizer) => str_replace('/api/organizers/', '', (string) $organizer['value']), $organizers), - 'tags' => array_map(static fn ($tag) => str_replace('/api/tags/', '', (string) $tag['value']), $tags), - ]); + ]; + + if (null !== $places) { + $queryParams['occurrences.place.id'] = array_map(static fn (array $place) => str_replace('/api/places/', '', (string) $place['value']), $places); + } + + if (null !== $organizers) { + $queryParams['organizer.id'] = array_map(static fn (array $organizer) => str_replace('/api/organizers/', '', (string) $organizer['value']), $organizers); + } + + if (null !== $tags) { + $queryParams['tags'] = array_map(static fn (array $tag) => str_replace('/api/tags/', '', (string) $tag['value']), $tags); + } $response = $this->client->request( 'GET', @@ -124,12 +133,13 @@ public function getData(Feed $feed): array if ($throwable instanceof ClientException && Response::HTTP_NOT_FOUND == $throwable->getCode()) { try { // Slide publishedTo is set to now. This will make the slide unpublished from this point on. - $feed->getSlide()->setPublishedTo(new \DateTime('now', new \DateTimeZone('UTC'))); + $slide = $feed->getSlide()?->setPublishedTo(new \DateTime('now', new \DateTimeZone('UTC'))); + $this->entityManager->flush(); $this->logger->info('Feed with id: {feedId} depends on an item that does not exist in Event Database. Unpublished slide with id: {slideId}', [ 'feedId' => $feed->getId(), - 'slideId' => $feed->getSlide()->getId(), + 'slideId' => $slide?->getId(), ]); } catch (\Exception $exception) { $this->logger->error('{code}: {message}', [ diff --git a/src/Feed/SourceType/Koba/KobaFeedType.php b/src/Feed/SourceType/Koba/KobaFeedType.php index 37091092..b214b99a 100644 --- a/src/Feed/SourceType/Koba/KobaFeedType.php +++ b/src/Feed/SourceType/Koba/KobaFeedType.php @@ -6,8 +6,8 @@ use App\Entity\Tenant\Feed; use App\Entity\Tenant\FeedSource; -use App\Feed\FeedTypeInterface; use App\Feed\FeedOutputModels; +use App\Feed\FeedTypeInterface; use App\Service\FeedService; use Psr\Log\LoggerInterface; use Symfony\Component\HttpFoundation\Request; @@ -87,7 +87,7 @@ public function getData(Feed $feed): array } // Apply list filter. If enabled it removes all events that do not have (liste) in title. - if ($filterList) { + if (true === $filterList) { if (!str_contains($title, '(liste)')) { continue; } else { @@ -96,7 +96,7 @@ public function getData(Feed $feed): array } // Apply booked title override. If enabled it changes the title to Optaget if it contains (optaget). - if ($rewriteBookedTitles) { + if (true === $rewriteBookedTitles) { if (str_contains($title, '(optaget)')) { $title = 'Optaget'; } diff --git a/src/Feed/SourceType/Notified/NotifiedFeedType.php b/src/Feed/SourceType/Notified/NotifiedFeedType.php index 5995920e..ce378079 100644 --- a/src/Feed/SourceType/Notified/NotifiedFeedType.php +++ b/src/Feed/SourceType/Notified/NotifiedFeedType.php @@ -6,8 +6,8 @@ use App\Entity\Tenant\Feed; use App\Entity\Tenant\FeedSource; -use App\Feed\FeedTypeInterface; use App\Feed\FeedOutputModels; +use App\Feed\FeedTypeInterface; use App\Service\FeedService; use Psr\Log\LoggerInterface; use Symfony\Component\HttpFoundation\Request; diff --git a/src/Feed/SourceType/SparkleIO/SparkleIOFeedType.php b/src/Feed/SourceType/SparkleIO/SparkleIOFeedType.php index 692b5947..4a4329cf 100644 --- a/src/Feed/SourceType/SparkleIO/SparkleIOFeedType.php +++ b/src/Feed/SourceType/SparkleIO/SparkleIOFeedType.php @@ -6,15 +6,15 @@ use App\Entity\Tenant\Feed; use App\Entity\Tenant\FeedSource; -use App\Feed\FeedTypeInterface; use App\Feed\FeedOutputModels; +use App\Feed\FeedTypeInterface; use App\Service\FeedService; use Psr\Cache\CacheItemInterface; +use Psr\Cache\CacheItemPoolInterface; use Psr\Cache\InvalidArgumentException; use Psr\Log\LoggerInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Uid\Ulid; -use Symfony\Contracts\Cache\CacheInterface; use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface; use Symfony\Contracts\HttpClient\Exception\RedirectionExceptionInterface; use Symfony\Contracts\HttpClient\Exception\ServerExceptionInterface; @@ -31,7 +31,7 @@ class SparkleIOFeedType implements FeedTypeInterface public function __construct( private readonly FeedService $feedService, private readonly HttpClientInterface $client, - private readonly CacheInterface $feedsCache, + private readonly CacheItemPoolInterface $feedsCache, private readonly LoggerInterface $logger, ) {} diff --git a/src/Service/FeedService.php b/src/Service/FeedService.php index 71eff7e8..fdd17039 100644 --- a/src/Service/FeedService.php +++ b/src/Service/FeedService.php @@ -100,7 +100,7 @@ public function getData(Feed $feed): ?array /** @var CacheItemInterface $cacheItem */ $cacheItem = $this->feedsCache->getItem($feedId); - if (false && $cacheItem->isHit()) { + if ($cacheItem->isHit()) { /** @var array $data */ $data = $cacheItem->get(); } else { diff --git a/tests/Service/FeedServiceTest.php b/tests/Service/FeedServiceTest.php index c28c9ca2..8a95bb55 100644 --- a/tests/Service/FeedServiceTest.php +++ b/tests/Service/FeedServiceTest.php @@ -11,7 +11,7 @@ use App\Feed\SourceType\EventDatabase\EventDatabaseApiFeedType; use App\Feed\SourceType\Koba\KobaFeedType; use App\Feed\SourceType\Notified\NotifiedFeedType; -use App\Feed\Type\Rss\RssFeedType; +use App\Feed\SourceType\Rss\RssFeedType; use App\Feed\SourceType\SparkleIO\SparkleIOFeedType; use App\Service\FeedService; use Doctrine\ORM\EntityManagerInterface; From 2be4536e9a4f24971b824123832d5fce9b70feac Mon Sep 17 00:00:00 2001 From: Troels Ugilt Jensen <6103205+tuj@users.noreply.github.com> Date: Thu, 19 Dec 2024 15:43:39 +0100 Subject: [PATCH 7/9] 3208: Fixed coding standards issues --- CHANGELOG.md | 2 ++ docs/feed/feed-overview.md | 20 ++++++++++---------- src/Feed/OutputModel/Rss/Readme.md | 2 +- 3 files changed, 13 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1b0eac97..c845041a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ All notable changes to this project will be documented in this file. ## [Unreleased] +- [#226](https://github.com/os2display/display-api-service/pull/226) + - Added Colibo feed type. - [#215](https://github.com/os2display/display-api-service/pull/215) - Added calendar api feed type. - [#223](https://github.com/os2display/display-api-service/pull/223) diff --git a/docs/feed/feed-overview.md b/docs/feed/feed-overview.md index d0f0322d..12b159b6 100644 --- a/docs/feed/feed-overview.md +++ b/docs/feed/feed-overview.md @@ -1,33 +1,33 @@ # Feed Overview -"Feeds" in OS2display are external data sources that can provide up-to-data to slides. The idea is that if you can set -up slide based on a feed and publish it. The Screen Client will then fetch new data from the feed whenever the Slide is +"Feeds" in OS2display are external data sources that can provide up-to-data to slides. The idea is that if you can set +up slide based on a feed and publish it. The Screen Client will then fetch new data from the feed whenever the Slide is shown on screen. -The simplest example is a classic RSS news feed. You can set up a slide based on the RSS slide template, configure the +The simplest example is a classic RSS news feed. You can set up a slide based on the RSS slide template, configure the RSS source URL, and whenever the slide is on screen it will show the latest entries from the RSS feed. This means that administrators can set up slides and playlists that stays up to date automatically. ## Architecture -The "Feed" architecture is designed to enable both generic and custom feed types. To enable this all feed based screen +The "Feed" architecture is designed to enable both generic and custom feed types. To enable this all feed based screen templates are designed to support a given "feed output model". These are normalized data sets from a given feed type. -Each feed implementation defines which output model it supports. Thereby multiple feed implementations can support the -same output model. This is done to enable decoupling of the screen templates from the feed implementation. +Each feed implementation defines which output model it supports. Thereby multiple feed implementations can support the +same output model. This is done to enable decoupling of the screen templates from the feed implementation. For example: * If you have a news source that is not a RSS feed you can implement a "FeedSource" that fetches data from your source - then normalizes the data and outputs it as the RSS output model. When setting up RSS slides this feed source can then + then normalizes the data and outputs it as the RSS output model. When setting up RSS slides this feed source can then be selected as the source for the slide. -* OS2display has calendar templates that can show bookings or meetings. To show data from your specific calendar or - booking system you can implement a "FeedSource" that fetches booking data from your source and normalizes it to match +* OS2display has calendar templates that can show bookings or meetings. To show data from your specific calendar or + booking system you can implement a "FeedSource" that fetches booking data from your source and normalizes it to match the calendar output model. @todo Slide -> Feed -> FeedSource Auth -Caching \ No newline at end of file +Caching diff --git a/src/Feed/OutputModel/Rss/Readme.md b/src/Feed/OutputModel/Rss/Readme.md index eb23bf1e..12fcd589 100644 --- a/src/Feed/OutputModel/Rss/Readme.md +++ b/src/Feed/OutputModel/Rss/Readme.md @@ -4,4 +4,4 @@ The output model for RSS is defined as `FeedIo\Reader\Result` and `FeedIo\Feed\ItemInterface` -@see https://alexdebril.github.io/feed-io/ +@see [https://alexdebril.github.io/feed-io/](https://alexdebril.github.io/feed-io/). From 7c2e546053b7f129add86addb274a5dddd9073ca Mon Sep 17 00:00:00 2001 From: Troels Ugilt Jensen <6103205+tuj@users.noreply.github.com> Date: Fri, 20 Dec 2024 10:57:55 +0100 Subject: [PATCH 8/9] 3208: Removed refactorings from feature --- config/packages/nelmio_cors.yaml | 2 +- config/services.yaml | 2 +- fixtures/feed_source.yaml | 4 +- migrations/Version20241125085559.php | 39 ------------------- .../Calendar => }/CalendarApiFeedType.php | 4 +- .../Colibo => }/ColiboFeedType.php | 5 +-- .../EventDatabaseApiFeedType.php | 4 +- .../{SourceType/Koba => }/KobaFeedType.php | 4 +- .../Notified => }/NotifiedFeedType.php | 4 +- src/Feed/{SourceType/Rss => }/RssFeedType.php | 4 +- src/Feed/SourceType/Colibo/ApiClient.php | 1 + .../SparkleIO => }/SparkleIOFeedType.php | 4 +- tests/Api/FeedSourceTest.php | 10 ++--- tests/Feed/NotifiedFeedTypeTest.php | 2 +- tests/Service/FeedServiceTest.php | 12 +++--- 15 files changed, 25 insertions(+), 76 deletions(-) delete mode 100644 migrations/Version20241125085559.php rename src/Feed/{SourceType/Calendar => }/CalendarApiFeedType.php (99%) rename src/Feed/{SourceType/Colibo => }/ColiboFeedType.php (98%) rename src/Feed/{SourceType/EventDatabase => }/EventDatabaseApiFeedType.php (99%) rename src/Feed/{SourceType/Koba => }/KobaFeedType.php (98%) rename src/Feed/{SourceType/Notified => }/NotifiedFeedType.php (98%) rename src/Feed/{SourceType/Rss => }/RssFeedType.php (97%) rename src/Feed/{SourceType/SparkleIO => }/SparkleIOFeedType.php (98%) diff --git a/config/packages/nelmio_cors.yaml b/config/packages/nelmio_cors.yaml index 7b39d06d..f815f4e3 100644 --- a/config/packages/nelmio_cors.yaml +++ b/config/packages/nelmio_cors.yaml @@ -1,7 +1,7 @@ nelmio_cors: defaults: origin_regex: true - allow_credentials: true + allow_credentials: false allow_origin: ['%env(CORS_ALLOW_ORIGIN)%'] allow_methods: ['GET', 'OPTIONS', 'POST', 'PUT', 'PATCH', 'DELETE'] allow_headers: ['Content-Type', 'Authorization', 'Authorization-Tenant-Key'] diff --git a/config/services.yaml b/config/services.yaml index 2cf4cf64..6f1a0b8c 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -51,7 +51,7 @@ services: Symfony\Component\Security\Http\Authentication\AuthenticationFailureHandlerInterface: '@Lexik\Bundle\JWTAuthenticationBundle\Security\Http\Authentication\AuthenticationFailureHandler' Symfony\Component\Security\Http\Authentication\AuthenticationSuccessHandlerInterface: '@Lexik\Bundle\JWTAuthenticationBundle\Security\Http\Authentication\AuthenticationSuccessHandler' - App\Feed\SourceType\Calendar\CalendarApiFeedType: + App\Feed\CalendarApiFeedType: arguments: $locationEndpoint: '%env(string:CALENDAR_API_FEED_SOURCE_LOCATION_ENDPOINT)%' $resourceEndpoint: '%env(string:CALENDAR_API_FEED_SOURCE_RESOURCE_ENDPOINT)%' diff --git a/fixtures/feed_source.yaml b/fixtures/feed_source.yaml index 4c3180b6..989d60c7 100644 --- a/fixtures/feed_source.yaml +++ b/fixtures/feed_source.yaml @@ -2,7 +2,7 @@ App\Entity\Tenant\FeedSource: feed (template): description: - feedType: "App\\Feed\\SourceType\\Rss\\RssFeedType" + feedType: "App\\Feed\\RssFeedType" secrets: [ ] supportedFeedOutputType: 'rss' createdAt (unique): '' @@ -16,7 +16,7 @@ App\Entity\Tenant\FeedSource: tenant: '@tenant_xyz' feed_source_abc_notified (extends feed): title: 'feed_source_abc_notified' - feedType: "App\\Feed\\SourceType\\Rss\\RssFeedType" + feedType: "App\\Feed\\RssFeedType" secrets: token: '1234567890' supportedFeedOutputType: 'instagram' diff --git a/migrations/Version20241125085559.php b/migrations/Version20241125085559.php deleted file mode 100644 index 56b233be..00000000 --- a/migrations/Version20241125085559.php +++ /dev/null @@ -1,39 +0,0 @@ -addSql('UPDATE feed_source SET feed_type = "App\\\\Feed\\\\SourceType\\\\Calendar\\\\CalendarApiFeedType" WHERE feed_type = "App\\\\Feed\\\\CalendarApiFeedType"'); - $this->addSql('UPDATE feed_source SET feed_type = "App\\\\Feed\\\\SourceType\\\\EventDatabase\\\\EventDatabaseApiFeedType" WHERE feed_type = "App\\\\Feed\\\\EventDatabaseApiFeedType"'); - $this->addSql('UPDATE feed_source SET feed_type = "App\\\\Feed\\\\SourceType\\\\Koba\\\\KobaFeedType" WHERE feed_type = "App\\\\Feed\\\\KobaFeedType"'); - $this->addSql('UPDATE feed_source SET feed_type = "App\\\\Feed\\\\SourceType\\\\Notified\\\\NotifiedFeedType" WHERE feed_type = "App\\\\Feed\\\\NotifiedFeedType"'); - $this->addSql('UPDATE feed_source SET feed_type = "App\\\\Feed\\\\SourceType\\\\Rss\\\\RssFeedType" WHERE feed_type = "App\\\\Feed\\\\RssFeedType"'); - $this->addSql('UPDATE feed_source SET feed_type = "App\\\\Feed\\\\SourceType\\\\SparkleIO\\\\SparkleIOFeedType" WHERE feed_type = "App\\\\Feed\\\\SparkleIOFeedType"'); - } - - public function down(Schema $schema): void - { - $this->addSql('UPDATE feed_source SET feed_type = "App\\\\Feed\\\\CalendarApiFeedType" WHERE feed_type LIKE "App\\\\Feed\\\\SourceType\\\\Calendar\\\\CalendarApiFeedType"'); - $this->addSql('UPDATE feed_source SET feed_type = "App\\\\Feed\\\\EventDatabaseApiFeedType" WHERE feed_type LIKE "App\\\\Feed\\\\SourceType\\\\EventDatabase\\\\EventDatabaseApiFeedType"'); - $this->addSql('UPDATE feed_source SET feed_type = "App\\\\Feed\\\\KobaFeedType" WHERE feed_type LIKE "App\\\\Feed\\\\SourceType\\\\Koba\\\\KobaFeedType"'); - $this->addSql('UPDATE feed_source SET feed_type = "App\\\\Feed\\\\NotifiedFeedType" WHERE feed_type LIKE "App\\\\Feed\\\\SourceType\\\\Notified\\\\NotifiedFeedType"'); - $this->addSql('UPDATE feed_source SET feed_type = "App\\\\Feed\\\\RssFeedType" WHERE feed_type LIKE "App\\\\Feed\\\\SourceType\\\\Rss\\\\RssFeedType"'); - $this->addSql('UPDATE feed_source SET feed_type = "App\\\\Feed\\\\SparkleIOFeedType" WHERE feed_type LIKE "App\\\\Feed\\\\SourceType\\\\SparkleIO\\\\SparkleIOFeedType"'); - } -} diff --git a/src/Feed/SourceType/Calendar/CalendarApiFeedType.php b/src/Feed/CalendarApiFeedType.php similarity index 99% rename from src/Feed/SourceType/Calendar/CalendarApiFeedType.php rename to src/Feed/CalendarApiFeedType.php index b3ebd57c..7f4c820d 100644 --- a/src/Feed/SourceType/Calendar/CalendarApiFeedType.php +++ b/src/Feed/CalendarApiFeedType.php @@ -2,12 +2,10 @@ declare(strict_types=1); -namespace App\Feed\SourceType\Calendar; +namespace App\Feed; use App\Entity\Tenant\Feed; use App\Entity\Tenant\FeedSource; -use App\Feed\FeedOutputModels; -use App\Feed\FeedTypeInterface; use App\Feed\OutputModel\Calendar\Event; use App\Feed\OutputModel\Calendar\Location; use App\Feed\OutputModel\Calendar\Resource; diff --git a/src/Feed/SourceType/Colibo/ColiboFeedType.php b/src/Feed/ColiboFeedType.php similarity index 98% rename from src/Feed/SourceType/Colibo/ColiboFeedType.php rename to src/Feed/ColiboFeedType.php index abb422d9..65526ab6 100644 --- a/src/Feed/SourceType/Colibo/ColiboFeedType.php +++ b/src/Feed/ColiboFeedType.php @@ -2,13 +2,12 @@ declare(strict_types=1); -namespace App\Feed\SourceType\Colibo; +namespace App\Feed; use App\Entity\Tenant\Feed; use App\Entity\Tenant\FeedSource; -use App\Feed\FeedOutputModels; -use App\Feed\FeedTypeInterface; use App\Feed\OutputModel\ConfigOption; +use App\Feed\SourceType\Colibo\ApiClient; use App\Service\FeedService; use FeedIo\Feed\Item; use FeedIo\Feed\Node\Category; diff --git a/src/Feed/SourceType/EventDatabase/EventDatabaseApiFeedType.php b/src/Feed/EventDatabaseApiFeedType.php similarity index 99% rename from src/Feed/SourceType/EventDatabase/EventDatabaseApiFeedType.php rename to src/Feed/EventDatabaseApiFeedType.php index ce8cf13c..250c2e40 100644 --- a/src/Feed/SourceType/EventDatabase/EventDatabaseApiFeedType.php +++ b/src/Feed/EventDatabaseApiFeedType.php @@ -2,12 +2,10 @@ declare(strict_types=1); -namespace App\Feed\SourceType\EventDatabase; +namespace App\Feed; use App\Entity\Tenant\Feed; use App\Entity\Tenant\FeedSource; -use App\Feed\FeedOutputModels; -use App\Feed\FeedTypeInterface; use App\Service\FeedService; use Doctrine\ORM\EntityManagerInterface; use Psr\Log\LoggerInterface; diff --git a/src/Feed/SourceType/Koba/KobaFeedType.php b/src/Feed/KobaFeedType.php similarity index 98% rename from src/Feed/SourceType/Koba/KobaFeedType.php rename to src/Feed/KobaFeedType.php index b214b99a..aab32608 100644 --- a/src/Feed/SourceType/Koba/KobaFeedType.php +++ b/src/Feed/KobaFeedType.php @@ -2,12 +2,10 @@ declare(strict_types=1); -namespace App\Feed\SourceType\Koba; +namespace App\Feed; use App\Entity\Tenant\Feed; use App\Entity\Tenant\FeedSource; -use App\Feed\FeedOutputModels; -use App\Feed\FeedTypeInterface; use App\Service\FeedService; use Psr\Log\LoggerInterface; use Symfony\Component\HttpFoundation\Request; diff --git a/src/Feed/SourceType/Notified/NotifiedFeedType.php b/src/Feed/NotifiedFeedType.php similarity index 98% rename from src/Feed/SourceType/Notified/NotifiedFeedType.php rename to src/Feed/NotifiedFeedType.php index ce378079..8de891b5 100644 --- a/src/Feed/SourceType/Notified/NotifiedFeedType.php +++ b/src/Feed/NotifiedFeedType.php @@ -2,12 +2,10 @@ declare(strict_types=1); -namespace App\Feed\SourceType\Notified; +namespace App\Feed; use App\Entity\Tenant\Feed; use App\Entity\Tenant\FeedSource; -use App\Feed\FeedOutputModels; -use App\Feed\FeedTypeInterface; use App\Service\FeedService; use Psr\Log\LoggerInterface; use Symfony\Component\HttpFoundation\Request; diff --git a/src/Feed/SourceType/Rss/RssFeedType.php b/src/Feed/RssFeedType.php similarity index 97% rename from src/Feed/SourceType/Rss/RssFeedType.php rename to src/Feed/RssFeedType.php index 90abf690..a1e51912 100644 --- a/src/Feed/SourceType/Rss/RssFeedType.php +++ b/src/Feed/RssFeedType.php @@ -2,12 +2,10 @@ declare(strict_types=1); -namespace App\Feed\SourceType\Rss; +namespace App\Feed; use App\Entity\Tenant\Feed; use App\Entity\Tenant\FeedSource; -use App\Feed\FeedOutputModels; -use App\Feed\FeedTypeInterface; use FeedIo\Adapter\Http\Client; use FeedIo\Feed\Item; use FeedIo\FeedIo; diff --git a/src/Feed/SourceType/Colibo/ApiClient.php b/src/Feed/SourceType/Colibo/ApiClient.php index 48b59942..ae98dab1 100644 --- a/src/Feed/SourceType/Colibo/ApiClient.php +++ b/src/Feed/SourceType/Colibo/ApiClient.php @@ -5,6 +5,7 @@ namespace App\Feed\SourceType\Colibo; use App\Entity\Tenant\FeedSource; +use App\Feed\ColiboFeedType; use Psr\Cache\CacheItemInterface; use Psr\Cache\CacheItemPoolInterface; use Psr\Log\LoggerInterface; diff --git a/src/Feed/SourceType/SparkleIO/SparkleIOFeedType.php b/src/Feed/SparkleIOFeedType.php similarity index 98% rename from src/Feed/SourceType/SparkleIO/SparkleIOFeedType.php rename to src/Feed/SparkleIOFeedType.php index 4a4329cf..67787a84 100644 --- a/src/Feed/SourceType/SparkleIO/SparkleIOFeedType.php +++ b/src/Feed/SparkleIOFeedType.php @@ -2,12 +2,10 @@ declare(strict_types=1); -namespace App\Feed\SourceType\SparkleIO; +namespace App\Feed; use App\Entity\Tenant\Feed; use App\Entity\Tenant\FeedSource; -use App\Feed\FeedOutputModels; -use App\Feed\FeedTypeInterface; use App\Service\FeedService; use Psr\Cache\CacheItemInterface; use Psr\Cache\CacheItemPoolInterface; diff --git a/tests/Api/FeedSourceTest.php b/tests/Api/FeedSourceTest.php index 6bf70f28..f72e3261 100644 --- a/tests/Api/FeedSourceTest.php +++ b/tests/Api/FeedSourceTest.php @@ -60,7 +60,7 @@ public function testCreateFeedSource(): void 'json' => [ 'title' => 'Test feed source', 'description' => 'This is a test feed source', - 'feedType' => \App\Feed\SourceType\EventDatabase\EventDatabaseApiFeedType::class, + 'feedType' => \App\Feed\EventDatabaseApiFeedType::class, 'secrets' => [ 'host' => 'https://www.test.dk', ], @@ -77,7 +77,7 @@ public function testCreateFeedSource(): void '@type' => 'FeedSource', 'title' => 'Test feed source', 'description' => 'This is a test feed source', - 'feedType' => \App\Feed\SourceType\EventDatabase\EventDatabaseApiFeedType::class, + 'feedType' => \App\Feed\EventDatabaseApiFeedType::class, 'secrets' => [ 'host' => 'https://www.test.dk', ], @@ -146,7 +146,7 @@ public function testCreateFeedSourceWithEventDatabaseFeedTypeWithoutRequiredSecr 'json' => [ 'title' => 'Test feed source', 'outputType' => 'This is a test output type', - 'feedType' => \App\Feed\SourceType\EventDatabase\EventDatabaseApiFeedType::class, + 'feedType' => \App\Feed\EventDatabaseApiFeedType::class, 'secrets' => [ 'test secret', ], @@ -171,7 +171,7 @@ public function testUpdateFeedSource(): void 'title' => 'Updated title', 'description' => 'Updated description', 'outputType' => 'This is a test output type', - 'feedType' => \App\Feed\SourceType\EventDatabase\EventDatabaseApiFeedType::class, + 'feedType' => \App\Feed\EventDatabaseApiFeedType::class, 'secrets' => [ ], ], @@ -198,7 +198,7 @@ public function testDeleteFeedSource(): void 'title' => 'Test feed source', 'description' => 'This is a test feed source', 'outputType' => 'This is a test output type', - 'feedType' => \App\Feed\SourceType\EventDatabase\EventDatabaseApiFeedType::class, + 'feedType' => \App\Feed\EventDatabaseApiFeedType::class, 'secrets' => [ 'host' => 'https://www.test.dk', ], diff --git a/tests/Feed/NotifiedFeedTypeTest.php b/tests/Feed/NotifiedFeedTypeTest.php index ec8203bb..5a7ef76c 100644 --- a/tests/Feed/NotifiedFeedTypeTest.php +++ b/tests/Feed/NotifiedFeedTypeTest.php @@ -4,7 +4,7 @@ namespace App\Tests\Feed; -use App\Feed\SourceType\Notified\NotifiedFeedType; +use App\Feed\NotifiedFeedType; use App\Repository\FeedSourceRepository; use App\Repository\SlideRepository; use App\Service\FeedService; diff --git a/tests/Service/FeedServiceTest.php b/tests/Service/FeedServiceTest.php index 8a95bb55..9cfd24b2 100644 --- a/tests/Service/FeedServiceTest.php +++ b/tests/Service/FeedServiceTest.php @@ -6,13 +6,13 @@ use App\Entity\Tenant\Feed; use App\Entity\Tenant\FeedSource; +use App\Feed\CalendarApiFeedType; +use App\Feed\EventDatabaseApiFeedType; use App\Feed\FeedTypeInterface; -use App\Feed\SourceType\Calendar\CalendarApiFeedType; -use App\Feed\SourceType\EventDatabase\EventDatabaseApiFeedType; -use App\Feed\SourceType\Koba\KobaFeedType; -use App\Feed\SourceType\Notified\NotifiedFeedType; -use App\Feed\SourceType\Rss\RssFeedType; -use App\Feed\SourceType\SparkleIO\SparkleIOFeedType; +use App\Feed\KobaFeedType; +use App\Feed\NotifiedFeedType; +use App\Feed\RssFeedType; +use App\Feed\SparkleIOFeedType; use App\Service\FeedService; use Doctrine\ORM\EntityManagerInterface; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; From 8c380bf08bbaca7a72b4b5dec37be0e85ce026cd Mon Sep 17 00:00:00 2001 From: Troels Ugilt Jensen <6103205+tuj@users.noreply.github.com> Date: Fri, 20 Dec 2024 11:03:45 +0100 Subject: [PATCH 9/9] 3208: Removed todos --- docs/feed/feed-overview.md | 6 ------ 1 file changed, 6 deletions(-) diff --git a/docs/feed/feed-overview.md b/docs/feed/feed-overview.md index 12b159b6..adb2c0af 100644 --- a/docs/feed/feed-overview.md +++ b/docs/feed/feed-overview.md @@ -25,9 +25,3 @@ For example: * OS2display has calendar templates that can show bookings or meetings. To show data from your specific calendar or booking system you can implement a "FeedSource" that fetches booking data from your source and normalizes it to match the calendar output model. - -@todo - -Slide -> Feed -> FeedSource -Auth -Caching