Last active
November 21, 2022 11:23
-
-
Save ricardoboss/fc4d0b69275f9c4754560362e9df07c8 to your computer and use it in GitHub Desktop.
A shell script to fetch rss feeds in php and print them in a neat way with colors and formatting.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/php | |
<?php | |
declare(strict_types=1); | |
/** | |
* Shell script to fetch rss feeds. | |
* | |
* @author Ricardo Boss <mail@ricardoboss.de> | |
*/ | |
// feed configuration | |
const FEEDS = [ | |
// 'Laravel News' => [ | |
// 'url' => 'https://feed.laravel-news.com/', | |
// 'show_item_description' => true, | |
// 'description_max_length' => 50, | |
// 'max_items' => 3, | |
// 'reverse_items' => false, | |
// ], | |
'php.net' => [ | |
'url' => 'https://news-web.php.net/group.php?group=php.announce&format=rss', | |
'show_item_description' => false, | |
'description_max_length' => 50, | |
'max_items' => 3, | |
'reverse_items' => true, | |
], | |
'JetBrains PhpStorm' => [ | |
'url' => 'https://blog.jetbrains.com/phpstorm/feed/', | |
'show_item_description' => false, | |
'description_max_length' => 50, | |
'max_items' => 3, | |
'reverse_items' => true, | |
], | |
]; | |
if (!extension_loaded('simplexml') || !extension_loaded('curl') || !extension_loaded('openssl')) { | |
echo "One of these extensions is missing: simplexml, curl, openssl"; | |
exit; | |
} | |
if (PHP_MAJOR_VERSION < 8) { | |
echo "At least PHP 8 is required, you are running " . PHP_VERSION; | |
exit; | |
} | |
const FORMAT_PREFIX = "\e["; | |
const FORMAT_SUFFIX = "m"; | |
const FORMAT_RESET = "0"; | |
const FORMAT_BOLD = "1"; | |
const FORMAT_DIM = "2"; | |
const FORMAT_UNDERLINE = "4"; | |
const FORMAT_BLINK = "5"; | |
const FORMAT_INVERT = "7"; | |
const FORMAT_HIDDEN = "8"; | |
const FORMAT_TOKEN_SUFFIX = "\e[0m"; | |
const FOREGROUND_BLACK = "0;30"; | |
const FOREGROUND_RED = "0;31"; | |
const FOREGROUND_GREEN = "0;32"; | |
const FOREGROUND_BROWN = "0;33"; | |
const FOREGROUND_BLUE = "0;34"; | |
const FOREGROUND_PURPLE = "0;35"; | |
const FOREGROUND_CYAN = "0;36"; | |
const FOREGROUND_LIGHT_GRAY = "0;37"; | |
const FOREGROUND_DARK_GRAY = "1;30"; | |
const FOREGROUND_LIGHT_RED = "1;31"; | |
const FOREGROUND_LIGHT_GREEN = "1;32"; | |
const FOREGROUND_YELLOW = "1;33"; | |
const FOREGROUND_LIGHT_BLUE = "1;34"; | |
const FOREGROUND_LIGHT_PURPLE = "1;35"; | |
const FOREGROUND_LIGHT_CYAN = "1;36"; | |
const FOREGROUND_WHITE = "1;37"; | |
const BACKGROUND_BLACK = "40"; | |
const BACKGROUND_RED = "41"; | |
const BACKGROUND_GREEN = "42"; | |
const BACKGROUND_YELLOW = "43"; | |
const BACKGROUND_BLUE = "44"; | |
const BACKGROUND_MAGENTA = "45"; | |
const BACKGROUND_CYAN = "46"; | |
const BACKGROUND_LIGHT_GRAY = "47"; | |
/** | |
* Formats a token. | |
* | |
* @param string|int|float|bool|null|Stringable $token The token to format. | |
* @param string ...$options (Optional) The formatting options to use. | |
* | |
* @return string The formatted token. | |
*/ | |
function tokf(string|int|float|bool|null|Stringable $token, string ...$options): string { | |
$token = (string)$token; | |
if (count($options) == 0) { | |
return $token; | |
} | |
$format = FORMAT_PREFIX; | |
$format .= implode(';', $options); | |
$format .= FORMAT_SUFFIX; | |
return $format . $token . FORMAT_TOKEN_SUFFIX; | |
} | |
/** | |
* Formats a message (an array of tokens). | |
* Available options for tokens are the same as for tokf. | |
* | |
* @param array<array-key, array> $tokens The tokens to format whereas the key of this array is the token itself and the value must be an array of options. | |
* @param null|string $separator (Optional) null to concatenate the tokens directly. | |
* | |
* @return string The formatted message. | |
* | |
* @see tokf | |
*/ | |
function messagef(array $tokens, ?string $separator = "\n"): string { | |
$message = ''; | |
$first = true; | |
foreach ($tokens as $tokenArray) { | |
$token = array_key_exists('token', $tokenArray) ? $tokenArray['token'] : $tokenArray[0]; | |
$options = array_key_exists('format', $tokenArray) ? $tokenArray['format'] : []; | |
if (!is_array($options)) { | |
$options = [$options]; | |
} | |
$formatted = tokf($token, ...$options); | |
if ($first) { | |
$first = false; | |
} else { | |
$message .= ($separator ?? ''); | |
} | |
$message .= $formatted; | |
} | |
return $message; | |
} | |
/** | |
* Gets an rss feed as a stdClass object. | |
* | |
* @param string $url Which url to request to get the rss feed. | |
* | |
* @return stdClass An object, which contains the data. The success attribute will be false if the request failed, true otherwise. | |
*/ | |
function getRssFeed(string $url): stdClass { | |
// open handle and set settings | |
$ch = curl_init(); | |
curl_setopt($ch, CURLOPT_URL, $url); | |
curl_setopt($ch, CURLOPT_HEADER, 0); | |
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); | |
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0); | |
curl_setopt($ch, CURLOPT_ENCODING , "gzip, deflate"); | |
curl_setopt( | |
$ch, | |
CURLOPT_HTTPHEADER, | |
[ | |
'User-Agent: PHP-News-Fetcher/v1.0', | |
'Accept: application/rss+xml,application/xhtml+xml,application/xml', | |
] | |
); | |
// execute request | |
$xml = curl_exec($ch); | |
// create response | |
$response = new stdClass(); | |
$response->success = false; | |
// check if request was successful | |
if ($xml === false) { | |
$response->error = curl_error($ch); | |
} else { | |
$err = []; | |
set_error_handler( | |
static function (int $code, string $message) use (&$err) { | |
$err[] = $message; | |
} | |
); | |
// convert xml to rss | |
$rss = simplexml_load_string($xml); | |
restore_error_handler(); | |
$response->error = implode("\n", $err); | |
if ($rss !== false) { | |
// convert rss to json | |
$json = json_encode($rss); | |
// strip null and empty values | |
$json = preg_replace('/,\s*"[^"]+":null|"[^"]+":null,?/', '', $json); | |
$json = preg_replace('/,\s*"[^"]+":{}|"[^"]+":{},?/', '', $json); | |
// convert json to stdClass | |
$class = json_decode($json, false); | |
if ($class === false) { | |
$response->error = json_last_error_msg(); | |
} else { | |
// save channel data to response | |
$response->success = true; | |
$response->data = $class->channel; | |
} | |
} | |
} | |
// close handle | |
curl_close($ch); | |
return $response; | |
} | |
$lines = [ | |
["\n~= N E W S =~\n", 'format' => [FORMAT_BOLD, FOREGROUND_LIGHT_CYAN]] | |
]; | |
foreach (FEEDS as $name => $feed) { | |
$response = getRssFeed($feed['url']); | |
if (!$response->success) { | |
$lines[] = [' ' . $name . ' ', 'format' => [FOREGROUND_GREEN, FORMAT_INVERT]]; | |
$lines[] = ["\nRequest to {$feed['url']} failed!", 'format' => FOREGROUND_LIGHT_RED]; | |
foreach (explode("\n", $response->error) as $line) { | |
$lines[] = [$line, 'format' => FOREGROUND_LIGHT_RED, FORMAT_INVERT]; | |
} | |
continue; | |
} | |
$data = $response->data; | |
$title = [' ' . $data->title . ' ', 'format' => [FOREGROUND_GREEN, FORMAT_INVERT]]; | |
if (isset($data->description)) { | |
$lines[] = [ | |
messagef( | |
[ | |
$title, | |
[' - ' . $data->description, 'format' => [FORMAT_INVERT]], | |
], | |
null, | |
), | |
]; | |
} else { | |
$lines[] = $title; | |
} | |
$lines[] = [""]; | |
$dataItemCount = count($data->item); | |
$itemCount = min($dataItemCount, $feed['max_items']); | |
if ($feed['reverse_items']) { | |
$start = $dataItemCount - 1; | |
$end = $start - $itemCount; | |
$step = -1; | |
} else { | |
$start = 0; | |
$end = $itemCount; | |
$step = 1; | |
} | |
for ($i = $start; $i !== $end; $i += $step) { | |
$item = $data->item[$i]; | |
$itemLines = [ | |
[$item->pubDate, 'format' => FOREGROUND_PURPLE], | |
[$item->title, 'format' => [FORMAT_BOLD, FOREGROUND_YELLOW]], | |
]; | |
if (isset($item->description) && $feed['show_item_description']) { | |
$itemLines[] = [ | |
strlen($item->description) > $feed['description_max_length'] | |
? | |
substr($item->description, 0, $feed['description_max_length']) . "..." | |
: | |
$item->description, | |
'format' => FOREGROUND_CYAN | |
]; | |
} | |
$itemLines[] = [$item->link, 'format' => FORMAT_UNDERLINE]; | |
$lines[] = [messagef($itemLines, " - ")]; | |
} | |
$lines[] = [""]; | |
} | |
echo messagef($lines); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
To include this script in your servers welcome message (Message Of The Day; MOTD), put the file contents in
/etc/update-motd.d/11-news
(no extension!) and installsudo apt install update-motd
.