diff --git a/docs/feeds.md b/docs/feeds.md index de9b53f..a5ca8a6 100644 --- a/docs/feeds.md +++ b/docs/feeds.md @@ -15,6 +15,8 @@ feeds: - name: GitHub Status url: https://www.githubstatus.com/history.rss interval: 600 + truncate: true + truncate_length: 250 active_hours: start_time: '09:30' end_time: '17:00' @@ -24,22 +26,24 @@ feeds: ### Standard -| Name | Required | Description | -| ----------------- | -------- | ----------------------------------------------------------------------------- | -| `name` | **Yes** | Feed name | -| `url` | **Yes** | Feed URL | -| `interval` | **Yes** | Interval between feed checks in seconds. Minimum value is `300` (5 minutes). | -| `title_prefix` | No | Text to prepend to notification titles. | +| Name | Required | Type | Description | +| ----------------- | -------- | ------- | ----------------------------------------------------------------------------------------------- | +| `name` | **Yes** | string | Feed name | +| `url` | **Yes** | string | Feed URL | +| `interval` | **Yes** | integer | Interval between feed checks in seconds. Minimum value is `300` (5 minutes). | +| `title_prefix` | No | string | Text to prepend to notification titles. | +| `truncate` | No | boolean | Truncate notification text. Disabled by default. Use `truncate_length` to set custom length. | +| `truncate_length` | No | integer | Number of characters to truncate notification text to. Minimum is `0` and the default is `200`. | ### Notification services -| Name | Required | Description | -| ----------------- | -------- | ----------------------------------------------------------------------------- | -| `gotify_token` | No | Gotify application token. Overrides token set with an environment variable. | -| `gotify_priority` | No | Gotify message priority. Overrides default value and/or environment variable. | -| `ntfy_topic` | No | Ntfy topic. Overrides topic set with an environment variable. | -| `ntfy_token` | No | Ntfy access token. Overrides token set with an environment variable. | -| `ntfy_priority` | No | Ntfy message priority. Overrides default value and/or environment variable. | +| Name | Required | Type | Description | +| ----------------- | -------- | ------- | ----------------------------------------------------------------------------- | +| `gotify_token` | No | string | Gotify application token. Overrides token set with an environment variable. | +| `gotify_priority` | No | integer | Gotify message priority. Overrides default value and/or environment variable. | +| `ntfy_topic` | No | string | Ntfy topic. Overrides topic set with an environment variable. | +| `ntfy_token` | No | string | Ntfy access token. Overrides token set with an environment variable. | +| `ntfy_priority` | No | integer | Ntfy message priority. Overrides default value and/or environment variable. | ### Active hours @@ -47,10 +51,10 @@ Use active hours to restrict feed monitoring to a specific time window. Both parameters are required when configuring active hours. -| Name | Description | -| ------------------------ | ------------------------------------------------------------ | -| `active_hours.start_time`| Start time for active hours in 24-hour format. e.g: `09:00` | -| `active_hours.end_time` | End time for active hours in 24-hour format. e.g: `16:30` | +| Name | Type | Description | +| ------------------------ | ------ | ------------------------------------------------------------ | +| `active_hours.start_time`| string | Start time for active hours in 24-hour format. e.g: `09:00` | +| `active_hours.end_time` | string | End time for active hours in 24-hour format. e.g: `16:30` | #### Limitations diff --git a/feeds.example.yaml b/feeds.example.yaml index 320110b..e6f4d63 100644 --- a/feeds.example.yaml +++ b/feeds.example.yaml @@ -6,6 +6,8 @@ feeds: - name: GitHub status url: https://www.githubstatus.com/history.rss interval: 600 + truncate: true + truncate_length: 250 active_hours: start_time: 09:30 end_time: 17:00 diff --git a/src/Check.php b/src/Check.php index a09b1e6..2f42644 100644 --- a/src/Check.php +++ b/src/Check.php @@ -149,7 +149,9 @@ private function process(\FeedIo\Reader\Result $result): void $title, $body, $item->getLink(), - $this->details->getTitlePrefix() + $this->details->getTitlePrefix(), + $this->details->getTruncateStatus(), + $this->details->getTruncateLength() ); } } diff --git a/src/Feed/Details.php b/src/Feed/Details.php index a83ab14..0b5399b 100644 --- a/src/Feed/Details.php +++ b/src/Feed/Details.php @@ -115,6 +115,26 @@ public function getNtfyPriority(): ?int return $this->details['ntfy_priority'] ?? null; } + /** + * Returns text truncation status + * + * @return bool + */ + public function getTruncateStatus(): bool + { + return $this->details['truncate'] ?? false; + } + + /** + * Returns text truncation length + * + * @return ?int + */ + public function getTruncateLength(): ?int + { + return $this->details['truncate_length'] ?? null; + } + /** * Returns active hours start time * diff --git a/src/Feed/Validate.php b/src/Feed/Validate.php index 89da44e..b385371 100644 --- a/src/Feed/Validate.php +++ b/src/Feed/Validate.php @@ -27,6 +27,7 @@ public function __construct(array $feed, int $minCheckInterval) $this->url(); $this->interval($minCheckInterval); $this->titlePrefix(); + $this->messageTruncation(); $this->gotifyToken(); $this->gotifyPriority(); @@ -96,6 +97,39 @@ private function titlePrefix(): void } } + /** + * Validate entry message truncation + * + * @throws FeedsException if truncate value is invalid + * @throws FeedsException if truncate length value is invalid + * @throws FeedsException if truncate length value is less than zeo + */ + private function messageTruncation(): void + { + if (array_key_exists('truncate', $this->details) === true) { + $truncate = filter_var($this->details['truncate'], FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE); + + if ($truncate === null) { + throw new FeedsException(sprintf('Invalid truncate value given for feed: %s', $this->details['name'])); + } + } + + if (array_key_exists('truncate_length', $this->details) === true) { + $length = filter_var($this->details['truncate_length'], FILTER_VALIDATE_INT, FILTER_NULL_ON_FAILURE); + + if ($length === null) { + throw new FeedsException(sprintf('Invalid truncate length given for feed: %s', $this->details['name'])); + } + + if ($length < 0) { + throw new FeedsException(sprintf( + 'Truncate length less than zero given for feed: %s', + $this->details['name'] + )); + } + } + } + /** * Validate entry gotify token * diff --git a/src/Message.php b/src/Message.php index 6ff9ffc..45c3ee5 100644 --- a/src/Message.php +++ b/src/Message.php @@ -8,19 +8,34 @@ class Message private string $title; private string $body; private string $url; + private bool $truncate = false; + private int $truncateLength = 200; /** * @param string $title Message title * @param string $body Message body * @param string $url Message URL * @param ?string $prefix Message title prefix + * @param bool $truncate Truncate message status + * @param ?int $truncateLength Number of characters to truncate message to. */ - public function __construct(string $title, string $body, string $url = '', ?string $prefix = null) - { + public function __construct( + string $title, + string $body, + string $url = '', + ?string $prefix = null, + bool $truncate = false, + ?int $truncateLength = null + ) { $this->title = $title; $this->body = $body; $this->url = $url; $this->prefix = $prefix; + $this->truncate = $truncate; + + if (is_null($truncateLength) === false && $truncateLength >= 0) { + $this->truncateLength = $truncateLength; + } } /** @@ -46,6 +61,10 @@ public function getTitle(): string */ public function getBody(): string { + if ($this->truncate === true) { + return $this->truncate($this->body); + } + return $this->body; } @@ -57,4 +76,25 @@ public function getUrl(): string { return $this->url; } + + /** + * Truncate message body + * @param string $text + * @return string + */ + private function truncate(string $text): string + { + if (strlen($text) <= $this->truncateLength) { + return $text; + } + + $text = substr($text, 0, $this->truncateLength); + $breakpoint = strrpos($text, '.'); + + if ($breakpoint !== false) { + $text = substr($text, 0, $breakpoint); + } + + return $text . '...'; + } } diff --git a/tests/Feed/DetailsTest.php b/tests/Feed/DetailsTest.php index 8cf602f..5f9aa49 100644 --- a/tests/Feed/DetailsTest.php +++ b/tests/Feed/DetailsTest.php @@ -179,6 +179,30 @@ public function testGetNtfyPriority(): void ); } + public function testGetTruncateStatus(): void + { + $this->assertEquals( + self::$feeds[1]['truncate'], + self::$details[1]->getTruncateStatus() + ); + + $this->assertFalse( + self::$details[0]->getTruncateStatus() + ); + } + + public function testGetTruncateLength(): void + { + $this->assertEquals( + self::$feeds[1]['truncate_length'], + self::$details[1]->getTruncateLength() + ); + + $this->assertNull( + self::$details[0]->getTruncateLength() + ); + } + /** * Test getActiveHoursStartTime() */ diff --git a/tests/MessageTest.php b/tests/MessageTest.php index 9a9a397..c3f7a27 100644 --- a/tests/MessageTest.php +++ b/tests/MessageTest.php @@ -2,10 +2,21 @@ use PHPUnit\Framework\Attributes\CoversClass; use Vigilant\Message; +use Vigilant\Helper\Json; #[CoversClass(Message::class)] class MessageTest extends TestCase { + /** + * @var array $samples Test sample + */ + private static array $samples = []; + + public static function setUpBeforeClass(): void + { + self::$samples = Json::decode(self::loadSample('message-truncation.json')); + } + public function testClass(): void { $title = 'Hello World'; @@ -35,4 +46,48 @@ public function testTitlePrefix(): void $this->assertEquals($fullTitle, $message->getTitle()); } + + /** + * Test message truncation with the default truncation length + */ + public function testTruncation(): void + { + $message = new Message( + title: '', + body: self::$samples['default']['input'], + truncate: true + ); + + $this->assertEquals(self::$samples['default']['output'], $message->getBody()); + } + + /** + * Test message truncation with a custom truncation length + */ + public function testCustomLengthTruncation(): void + { + $message = new Message( + title: '', + body: self::$samples['custom']['input'], + truncate: true, + truncateLength: self::$samples['custom']['length'] + ); + + $this->assertEquals(self::$samples['custom']['output'], $message->getBody()); + } + + /** + * Test message truncation with text that is shorter than given truncation length + */ + public function testTruncationWithTextShorterThatTruncationLength(): void + { + $message = new Message( + title: '', + body: self::$samples['short']['input'], + truncate: true, + truncateLength: self::$samples['short']['length'] + ); + + $this->assertEquals(self::$samples['short']['output'], $message->getBody()); + } } diff --git a/tests/TestCase.php b/tests/TestCase.php index 8ae9fbf..957f09a 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -13,7 +13,7 @@ abstract class TestCase extends BaseTestCase protected static function loadSample(string $name): string { $path = __DIR__ . '/files/' . $name; - $contents = file_get_contents($path); + $contents = @file_get_contents($path); if ($contents === false) { throw new RuntimeException(sprintf('Unable to load sample file: %s', $path)); diff --git a/tests/files/feeds-invalid.yaml b/tests/files/feeds-invalid.yaml index 596e8a2..7b8920e 100644 --- a/tests/files/feeds-invalid.yaml +++ b/tests/files/feeds-invalid.yaml @@ -169,3 +169,27 @@ feeds: active_hours: start_time: 06:00 end_time: 05:00 + + truncateValueInvalid: + exception: Invalid truncate value given + data: + name: Example.com Feed + url: https://www.example.com/feed.rss + interval: 300 + truncate: string + + truncateLengthValueInvalid: + exception: Invalid truncate length + data: + name: Example.com Feed + url: https://www.example.com/feed.rss + interval: 300 + truncate_length: string + + truncateLengthLessThanZero: + exception: Truncate length less than zero + data: + name: Example.com Feed + url: https://www.example.com/feed.rss + interval: 300 + truncate_length: -1 diff --git a/tests/files/feeds.yaml b/tests/files/feeds.yaml index 1d144b3..e3e262b 100644 --- a/tests/files/feeds.yaml +++ b/tests/files/feeds.yaml @@ -13,6 +13,8 @@ feeds: ntfy_topic: GitHub_status ntfy_priority: 1 ntfy_token: HQKPyHCODeoMFuN + truncate: true + truncate_length: 150 active_hours: start_time: 06:00 end_time: 21:00 diff --git a/tests/files/message-truncation.json b/tests/files/message-truncation.json new file mode 100644 index 0000000..eb5f65e --- /dev/null +++ b/tests/files/message-truncation.json @@ -0,0 +1,17 @@ +{ + "default": { + "length": 200, + "input": "Lorem ipsum odor amet, consectetuer adipiscing elit. Accumsan phasellus hac nascetur torquent aliquet purus lacinia. Curae sagittis commodo lacinia, tortor fermentum condimentum. Finibus ut lacinia vivamus, convallis vestibulum mus nibh rutrum proin.", + "output": "Lorem ipsum odor amet, consectetuer adipiscing elit. Accumsan phasellus hac nascetur torquent aliquet purus lacinia. Curae sagittis commodo lacinia, tortor fermentum condimentum..." + }, + "custom": { + "length": 50, + "input": "Lorem ipsum odor amet, consectetuer adipiscing elit. Accumsan phasellus hac nascetur torquent aliquet purus lacinia. Curae sagittis commodo lacinia, tortor fermentum condimentum. Finibus ut lacinia vivamus, convallis vestibulum mus nibh rutrum proin.", + "output": "Lorem ipsum odor amet, consectetuer adipiscing eli..." + }, + "short": { + "length": 100, + "input": "Lorem ipsum odor amet.", + "output": "Lorem ipsum odor amet." + } +}