Skip to content

Commit

Permalink
Merge pull request #206 from aroskanalen/feature/1203-notified
Browse files Browse the repository at this point in the history
Adds support for NotifiedFeedType as replacement for SparkleIOFeedType.
  • Loading branch information
tuj authored May 3, 2024
2 parents fcf4364 + e579ef7 commit 12f61bb
Show file tree
Hide file tree
Showing 13 changed files with 437 additions and 11 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file.

## [Unreleased]

- [#206](https://github.com/os2display/display-api-service/pull/206)
- Added support for Notified (Instagram) feed as replacement for SparkleIOFeedType.
- Deprecated SparkleIOFeedType. (getsparkle.io has shut down)

## [2.0.4] - 2024-04-25

- [#204](https://github.com/os2display/display-api-service/pull/204)
Expand Down
9 changes: 9 additions & 0 deletions fixtures/feed.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,12 @@ App\Entity\Tenant\Feed:
createdAt (unique): '<dateTimeBetween("-2 years", "-2 days")>'
modifiedAt: '<dateTimeBetween($createdAt, "-1 days")>'
id: '<ulid($createdAt)>'
feed_abc_notified:
feedSource: '@feed_source_abc_notified'
slide: '@slide_abc_notified'
tenant: '@tenant_abc'
createdAt (unique): '<dateTimeBetween("-2 years", "-2 days")>'
modifiedAt: '<dateTimeBetween($createdAt, "-1 days")>'
id: '<ulid($createdAt)>'
configuration:
feeds: [12345]
9 changes: 8 additions & 1 deletion fixtures/feed_source.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ App\Entity\Tenant\FeedSource:
feed (template):
description: <text()>
feedType: "App\\Feed\\RssFeedType"
secrets: []
secrets: [ ]
supportedFeedOutputType: 'rss'
createdAt (unique): '<dateTimeBetween("-2 years", "-2 days")>'
modifiedAt: '<dateTimeBetween($createdAt, "-1 days")>'
Expand All @@ -14,3 +14,10 @@ App\Entity\Tenant\FeedSource:
feed_source_xyz_2 (extends feed):
title: 'feed_source_xyz_2'
tenant: '@tenant_xyz'
feed_source_abc_notified (extends feed):
title: 'feed_source_abc_notified'
feedType: "App\\Feed\\RssFeedType"
secrets:
token: '1234567890'
supportedFeedOutputType: 'instagram'
tenant: '@tenant_abc'
10 changes: 8 additions & 2 deletions fixtures/slide.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,12 @@ App\Entity\Tenant\Slide:
theme: '@theme_abc_1'
feed: '@feed_abc_1'
tenant: '@tenant_abc'
media: [ '@media_abc_*', '@media_abc_*', '@media_abc_*']
media: [ '@media_abc_*', '@media_abc_*', '@media_abc_*' ]
slide_abc_{2..60} (extends slide):
title: 'slide_abc_<current()>'
theme: '@theme_abc_*'
tenant: '@tenant_abc'
media: [ '@media_abc_*', '@media_abc_*', '@media_abc_*']
media: [ '@media_abc_*', '@media_abc_*', '@media_abc_*' ]
slide_def_shared_to_abc (extends slide):
title: 'slide_def_shared_to_abc'
theme: '@theme_def'
Expand All @@ -28,3 +28,9 @@ App\Entity\Tenant\Slide:
title: 'slide_xyz_<current()>'
theme: '@theme_xyz'
tenant: '@tenant_xyz'
slide_abc_notified (extends slide):
title: 'slide_abc_notified'
template: '@template_notified'
content:
maxEntries: 6
tenant: '@tenant_abc'
7 changes: 7 additions & 0 deletions fixtures/template.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,10 @@ App\Entity\Template:
createdAt (unique): '<dateTimeBetween("-2 years", "-2 days")>'
modifiedAt: '<dateTimeBetween($createdAt, "-1 days")>'
id: '<ulid($createdAt)>'
template_notified:
title: 'template_notified'
description: A template with different that serves notified data
resources: <templateResources()>
createdAt (unique): '<dateTimeBetween("-2 years", "-2 days")>'
modifiedAt: '<dateTimeBetween($createdAt, "-1 days")>'
id: '<ulid($createdAt)>'
236 changes: 236 additions & 0 deletions src/Feed/NotifiedFeedType.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,236 @@
<?php

declare(strict_types=1);

namespace App\Feed;

use App\Entity\Tenant\Feed;
use App\Entity\Tenant\FeedSource;
use App\Service\FeedService;
use Psr\Log\LoggerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Uid\Ulid;
use Symfony\Contracts\HttpClient\HttpClientInterface;

/**
* @see https://api.listen.notified.com/docs/index.html
*/
class NotifiedFeedType implements FeedTypeInterface
{
final public const string SUPPORTED_FEED_TYPE = 'instagram';
final public const int REQUEST_TIMEOUT = 10;

private const string BASE_URL = 'https://api.listen.notified.com';

public function __construct(
private readonly FeedService $feedService,
private readonly HttpClientInterface $client,
private readonly LoggerInterface $logger
) {}

public function getData(Feed $feed): array
{
try {
$secrets = $feed->getFeedSource()?->getSecrets();
if (!isset($secrets['token'])) {
return [];
}

$configuration = $feed->getConfiguration();
if (!isset($configuration['feeds']) || 0 === count($configuration['feeds'])) {
return [];
}

$slide = $feed->getSlide();
$slideContent = $slide?->getContent();

$pageSize = $slideContent['maxEntries'] ?? 10;

$token = $secrets['token'];

$data = $this->getMentions($token, $pageSize, $configuration['feeds']);

return array_map(fn (array $item) => $this->getFeedItemObject($item), $data);
} catch (\Throwable $throwable) {
$this->logger->error('{code}: {message}', [
'code' => $throwable->getCode(),
'message' => $throwable->getMessage(),
]);
}

return [];
}

/**
* {@inheritDoc}
*/
public function getAdminFormOptions(FeedSource $feedSource): array
{
$endpoint = $this->feedService->getFeedSourceConfigUrl($feedSource, 'feeds');

// @TODO: Translation.
return [
[
'key' => 'notified-selector',
'input' => 'multiselect-from-endpoint',
'endpoint' => $endpoint,
'name' => 'feeds',
'label' => 'Vælg feed',
'helpText' => 'Her vælger du hvilket feed der skal hentes indgange fra.',
'formGroupClasses' => 'col-md-6 mb-3',
],
];
}

/**
* {@inheritDoc}
*/
public function getConfigOptions(Request $request, FeedSource $feedSource, string $name): ?array
{
try {
if ('feeds' === $name) {
$secrets = $feedSource->getSecrets();

if (!isset($secrets['token'])) {
return [];
}

$token = $secrets['token'];

$data = $this->getSearchProfiles($token);

return array_map(fn (array $item) => [
'id' => Ulid::generate(),
'title' => $item['name'] ?? '',
'value' => $item['id'] ?? '',
], $data);
}
} catch (\Throwable $throwable) {
$this->logger->error('{code}: {message}', [
'code' => $throwable->getCode(),
'message' => $throwable->getMessage(),
]);
}

return null;
}

public function getMentions(string $token, int $pageSize = 10, array $searchProfileIds = []): array
{
$body = [
'pageSize' => $pageSize,
'page' => 1,
'searchProfileIds' => $searchProfileIds,
];

$res = $this->client->request(
'POST',
self::BASE_URL.'/api/listen/mentions',
[
'timeout' => self::REQUEST_TIMEOUT,
'headers' => [
'Accept' => 'application/json',
'Content-Type' => 'application/json',
'Notified-Custom-Token' => $token,
],
'body' => json_encode($body),
]
);

return $res->toArray();
}

public function getSearchProfiles(string $token): array
{
$response = $this->client->request(
'GET',
self::BASE_URL.'/api/listen/searchprofiles',
[
'timeout' => self::REQUEST_TIMEOUT,
'headers' => [
'Accept' => 'application/json',
'Content-Type' => 'application/json',
'Notified-Custom-Token' => $token,
],
]
);

return $response->toArray();
}

/**
* {@inheritDoc}
*/
public function getRequiredSecrets(): array
{
return ['token'];
}

/**
* {@inheritDoc}
*/
public function getRequiredConfiguration(): array
{
return ['feeds'];
}

/**
* {@inheritDoc}
*/
public function getSupportedFeedOutputType(): string
{
return self::SUPPORTED_FEED_TYPE;
}

/**
* Parse feed item into object.
*/
private function getFeedItemObject(array $item): array
{
$description = $item['description'] ?? null;

return [
'text' => $description,
'textMarkup' => null !== $description ? $this->wrapTags($description) : null,
'mediaUrl' => $item['mediaUrl'] ?? null,
// Video is not supported by the Notified Listen API.
'videoUrl' => null,
'username' => $item['sourceName'] ?? null,
'createdTime' => $item['published'] ?? null,
];
}

private function wrapTags(string $input): string
{
$text = trim($input);

// Strip unicode zero-width-space.
$text = str_replace("\xE2\x80\x8B", '', $text);

// Collects trailing tags one by one.
$trailingTags = [];
$pattern = "/\s*#(?<tag>[^\s#]+)\n?$/u";
while (preg_match($pattern, (string) $text, $matches)) {
// We're getting tags in reverse order.
array_unshift($trailingTags, $matches['tag']);
$text = preg_replace($pattern, '', (string) $text);
}

// Wrap sections in p tags.
$text = preg_replace("/(.+)\n?/u", '<p>\1</p>', (string) $text);

// Wrap inline tags.
$pattern = '/(#(?<tag>[^\s#]+))/';

return implode('', [
'<div class="text">',
preg_replace($pattern, '<span class="tag">\1</span>', (string) $text),
'</div>',
'<div class="tags">',
implode(' ',
array_map(fn ($tag) => '<span class="tag">#'.$tag.'</span>', $trailingTags)
),
'</div>',
]);
}
}
1 change: 1 addition & 0 deletions src/Feed/SparkleIOFeedType.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;

/** @deprecated The SparkleIO service is discontinued. */
class SparkleIOFeedType implements FeedTypeInterface
{
final public const SUPPORTED_FEED_TYPE = 'instagram';
Expand Down
4 changes: 2 additions & 2 deletions tests/Api/FeedSourceTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,14 @@ public function testGetCollection(): void
'@context' => '/contexts/FeedSource',
'@id' => '/v2/feed-sources',
'@type' => 'hydra:Collection',
'hydra:totalItems' => 1,
'hydra:totalItems' => 2,
'hydra:view' => [
'@id' => '/v2/feed-sources?itemsPerPage=10',
'@type' => 'hydra:PartialCollectionView',
],
]);

$this->assertCount(1, $response->toArray()['hydra:member']);
$this->assertCount(2, $response->toArray()['hydra:member']);
}

public function testGetItem(): void
Expand Down
4 changes: 2 additions & 2 deletions tests/Api/SlidesTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,12 @@ public function testGetCollection(): void
'@context' => '/contexts/Slide',
'@id' => '/v2/slides',
'@type' => 'hydra:Collection',
'hydra:totalItems' => 60,
'hydra:totalItems' => 61,
'hydra:view' => [
'@id' => '/v2/slides?itemsPerPage=10&page=1',
'@type' => 'hydra:PartialCollectionView',
'hydra:first' => '/v2/slides?itemsPerPage=10&page=1',
'hydra:last' => '/v2/slides?itemsPerPage=10&page=6',
'hydra:last' => '/v2/slides?itemsPerPage=10&page=7',
],
]);

Expand Down
4 changes: 2 additions & 2 deletions tests/Api/TemplatesTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,14 @@ public function testGetCollection(): void
'@context' => '/contexts/Template',
'@id' => '/v2/templates',
'@type' => 'hydra:Collection',
'hydra:totalItems' => 1,
'hydra:totalItems' => 2,
'hydra:view' => [
'@id' => '/v2/templates?itemsPerPage=5',
'@type' => 'hydra:PartialCollectionView',
],
]);

$this->assertCount(1, $response->toArray()['hydra:member']);
$this->assertCount(2, $response->toArray()['hydra:member']);

// @TODO: resources: Object value found, but an array is required. In JSON it's an object but in the entity
// it's an key array? So this test will fail.
Expand Down
Loading

0 comments on commit 12f61bb

Please sign in to comment.