Skip to content

Instantly share code, notes, and snippets.

@ricardoboss
Last active November 21, 2022 11:23
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save ricardoboss/fc4d0b69275f9c4754560362e9df07c8 to your computer and use it in GitHub Desktop.
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.
#!/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);
@ricardoboss
Copy link
Author

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 install sudo apt install update-motd.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment