From 71ba5a3ad1a1c9d867af68e72a4a19acd9ffe08d Mon Sep 17 00:00:00 2001 From: Sean Molenaar Date: Wed, 6 Mar 2019 13:10:37 +0100 Subject: Fix generation commands and make them available in ./occ (#402) --- .travis.yml | 19 +-- Makefile | 2 + appinfo/register_command.php | 2 + bin/tools/generate_authors.php | 10 +- bin/tools/generate_explore.php | 114 ++++++++++------ composer.json | 3 +- docs/README.md | 2 +- docs/explore/README.md | 6 +- docs/plugins/README.md | 2 +- lib/AppInfo/Application.php | 2 - lib/Command/ExploreGenerator.php | 94 +++++++++++++ tests/Unit/Command/ExploreGeneratorTest.php | 200 ++++++++++++++++++++++++++++ 12 files changed, 392 insertions(+), 64 deletions(-) mode change 100644 => 100755 bin/tools/generate_authors.php mode change 100644 => 100755 bin/tools/generate_explore.php create mode 100644 lib/Command/ExploreGenerator.php create mode 100644 tests/Unit/Command/ExploreGeneratorTest.php diff --git a/.travis.yml b/.travis.yml index 4f1030a3c..597298a04 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,15 +2,15 @@ sudo: false dist: trusty language: php php: - - 7.0 - - 7.1 + - 7.0.33 + - 7.1.26 - 7.2 - 7.3 - nightly env: global: - - CORE_BRANCH=stable14 + - CORE_BRANCH=stable15 - MOZ_HEADLESS=1 matrix: - DB=pgsql @@ -20,16 +20,6 @@ matrix: - env: DB=pgsql CORE_BRANCH=master - php: nightly include: - - php: 7.1 - env: DB=sqlite - - php: 7.2 - env: DB=sqlite - - php: 7.1 - env: DB=mysql - - php: 7.2 - env: DB=mysql - - php: 7.2 - env: DB=pgsql CORE_BRANCH=master - php: 7.3 env: DB=sqlite - php: 7.3 @@ -59,9 +49,10 @@ before_script: - ./occ app:check-code news - ./occ background:cron # enable default cron - php -S localhost:8080 & - - cd apps/news script: + - ./occ news:generate-explore --votes 100 "https://nextcloud.com/blogfeed" + - cd apps/news - make test after_failure: diff --git a/Makefile b/Makefile index e9aa3c1e2..f3c9b602c 100644 --- a/Makefile +++ b/Makefile @@ -150,6 +150,7 @@ endif appstore: rm -rf $(appstore_build_directory) $(appstore_artifact_directory) mkdir -p $(appstore_build_directory) $(appstore_artifact_directory) + ./bin/tools/generate_authors.php cp -r \ "appinfo" \ "css" \ @@ -189,3 +190,4 @@ test: # \Test\TestCase is only allowed to access the db if TRAVIS environment variable is set env TRAVIS=1 ./vendor/phpunit/phpunit/phpunit -c phpunit.integration.xml --coverage-clover build/php-unit.clover $(MAKE) phpcs + ./bin/tools/generate_authors.php diff --git a/appinfo/register_command.php b/appinfo/register_command.php index 723733bf5..3ce99b2a1 100644 --- a/appinfo/register_command.php +++ b/appinfo/register_command.php @@ -15,6 +15,7 @@ use OCA\News\Command\Updater\UpdateFeed; use OCA\News\Command\Updater\AllFeeds; use OCA\News\Command\Updater\BeforeUpdate; use OCA\News\Command\Updater\AfterUpdate; +use OCA\News\Command\ExploreGenerator; $app = new Application(); $container = $app->getContainer(); @@ -22,3 +23,4 @@ $application->add($container->query(AllFeeds::class)); $application->add($container->query(UpdateFeed::class)); $application->add($container->query(BeforeUpdate::class)); $application->add($container->query(AfterUpdate::class)); +$application->add($container->query(ExploreGenerator::class)); diff --git a/bin/tools/generate_authors.php b/bin/tools/generate_authors.php old mode 100644 new mode 100755 index 5c181b031..53d2c8892 --- a/bin/tools/generate_authors.php +++ b/bin/tools/generate_authors.php @@ -1,3 +1,4 @@ +#!/usr/bin/env php \d+)\s*(?P.*\w)\s*<(?P[^\s]+)>$/'; $contributors = array_map(function ($contributor) use ($regex) { + $result = []; preg_match($regex, $contributor, $result); return $result; }, $contributors); // filter out bots $contributors = array_filter($contributors, function ($contributor) { - return strpos($contributor['name'], 'Jenkins') !== 0; + if (empty($contributor['name']) || empty($contributor['email'])) { + return false; + } + if (strpos($contributor['email'], 'bot') || strpos($contributor['name'], 'bot')) { + return false; + } + return true; }); // turn tuples into markdown diff --git a/bin/tools/generate_explore.php b/bin/tools/generate_explore.php old mode 100644 new mode 100755 index 3ad19cb81..766e82db7 --- a/bin/tools/generate_explore.php +++ b/bin/tools/generate_explore.php @@ -1,3 +1,4 @@ +#!/usr/bin/env php * @copyright Bernhard Posselt 2016 */ +require_once __DIR__ . '/../../vendor/autoload.php'; +require_once __DIR__ . '/../../../../lib/base.php'; + +use FeedIo\FeedIo; +use Favicon\Favicon; +use OCA\News\AppInfo\Application; + +$generator = new ExploreGenerator(); +$generator->parse_argv($argv); +print(json_encode($generator->read(), JSON_PRETTY_PRINT)); +print("\n"); /** * This is used for generating a JSON config section for a feed by executing: * php -f generate_authors.php www.feed.com + * @deprecated Use ./occ news:generate-explore instead. */ +class ExploreGenerator +{ + /** + * Feed and favicon fetcher. + */ + protected $reader; + protected $favicon; -require_once __DIR__ . '/../../vendor/autoload.php'; - -if (count($argv) < 2 || count($argv) > 3) { - print('Usage: php -f generate_explore http://path.com/feed [vote_count]'); - print("\n"); - exit(); -} elseif (count($argv) === 3) { - $votes = $argv[2]; -} else { - $votes = 100; -} - -$url = $argv[1]; - -try { - $config = new PicoFeed\Config\Config(); - $reader = new PicoFeed\Reader\Reader($config); - $resource = $reader->discover($url); + /** + * Argument data + */ + protected $url; + protected $votes; - $location = $resource->getUrl(); - $content = $resource->getContent(); - $encoding = $resource->getEncoding(); + /** + * Set up class. + */ + public function __construct() + { + $app = new Application(); + $container = $app->getContainer(); - $parser = $reader->getParser($location, $content, $encoding); + $this->reader = $container->query(FeedIo::class); + $this->favicon = new Favicon(); + } - $feed = $parser->execute(); + /** + * Parse required arguments. + * @param array $argv Arguments to the script. + * @return void + */ + public function parse_argv($argv = []) + { + if (count($argv) < 2 || count($argv) > 3) + { + print('Usage: php -f generate_explore http://path.com/feed [vote_count]'); + print("\n"); + exit(1); + } - $favicon = new PicoFeed\Reader\Favicon($config); + $this->votes = (count($argv) === 3) ? $argv[2] : 100; + $this->url = $argv[1]; + } - $result = [ - "title" => $feed->getTitle(), - "favicon" => $favicon->find($url), - "url" => $feed->getSiteUrl(), - "feed" => $feed->getFeedUrl(), - "description" => $feed->getDescription(), - "votes" => $votes - ]; + /** + * Read the provided feed and return the important data. + * @return array Object representation of the feed + */ + public function read() + { + try { + $resource = $this->reader->read($this->url); + $feed = $resource->getFeed(); + $result = [ + 'title' => $feed->getTitle(), + 'favicon' => $this->favicon->get($feed->getLink()), + 'url' => $feed->getLink(), + 'feed' => $this->url, + 'description' => $feed->getDescription(), + 'votes' => $this->votes, + ]; - if ($feed->getLogo()) { - $result["image"] = $feed->getLogo(); - } + return $result; + } catch (\Throwable $ex) { + return [ 'error' => $ex->getMessage() ]; + } + } - print(json_encode($result, JSON_PRETTY_PRINT)); - -} catch (\Exception $ex) { - print($ex->getMessage()); } - -print("\n"); \ No newline at end of file diff --git a/composer.json b/composer.json index de3b7a028..4f1e1690f 100644 --- a/composer.json +++ b/composer.json @@ -40,7 +40,8 @@ "pear/net_url2": "2.2.2", "riimu/kit-pathjoin": "1.2.0", "debril/feed-io": "^3.0", - "arthurhoaro/favicon": "^1.2" + "arthurhoaro/favicon": "^1.2", + "ext-json": "*" }, "require-dev": { "phpunit/phpunit": "^6.5", diff --git a/docs/README.md b/docs/README.md index 63837701a..62c2dac07 100644 --- a/docs/README.md +++ b/docs/README.md @@ -9,5 +9,5 @@ As a developer you can interact with the News app in the following ways: * [Customize the explore section](explore/) -The News app uses [picoFeed](https://github.com/miniflux/picoFeed) for parsing feeds and full text feeds. picoFeed is a fantastic library so if you [add custom full text configurations](https://github.com/miniflux/picoFeed/blob/master/docs/grabber.markdown#how-to-write-a-grabber-rules-file) or fix bugs, please consider **contributing your changes** back to the library to help others :) +The News app uses [FeedIO](https://github.com/alexdebril/feed-io) for parsing feeds and full text feeds. FeedIO is a fantastic library so if you contribute or fix bugs, please consider **contributing your changes** back to the library to help others :) diff --git a/docs/explore/README.md b/docs/explore/README.md index c377e343b..be77a2a85 100644 --- a/docs/explore/README.md +++ b/docs/explore/README.md @@ -22,13 +22,13 @@ The file has the following format: } ``` -To ease the pain of constructing the JSON object, you can use a small script to automatically create it: +To ease the pain of constructing the JSON object, you can use a nextcloud command to automatically create it: - php -f bin/tools/generate_explore.php https://path.com/to/feed.rss + php ./occ news:generate-explore https://path.com/to/feed.rss By passing a second parameter you can set the vote count which determines the sorting on the explore page: - php -f bin/tools/generate_explore.php https://path.com/to/feed.rss 1000 + php ./occ news:generate-explore https://path.com/to/feed.rss 1000 You can paste the output directly into the appropriate json file but you may need to add additional categories and commas diff --git a/docs/plugins/README.md b/docs/plugins/README.md index bc9e110d0..3d7f968cb 100644 --- a/docs/plugins/README.md +++ b/docs/plugins/README.md @@ -8,7 +8,7 @@ There are essentially three different use cases for plugins: * Dropping in additional CSS or JavaScript ## The Basics -Whatever plugin you want to create, you first need to create a basic structure. A plugin is basically just an app so you can take advantage of the full [Nextcloud app API](https://docs.nextcloud.org/server/9/developer_manual/app/index.html). If you want you can [take a look at the developer docs](https://docs.nextcloud.org/server/9/developer_manual/app/index.html) or [dig into the tutorial](https://docs.nextcloud.org/server/9/developer_manual/app/tutorial.html). +Whatever plugin you want to create, you first need to create a basic structure. A plugin is basically just an app so you can take advantage of the full [Nextcloud app API](https://docs.nextcloud.org/server/latest/developer_manual/app/index.html). If you want you can [take a look at the developer docs](https://docs.nextcloud.org/server/latest/developer_manual/app/index.html) or [dig into the tutorial](https://docs.nextcloud.org/server/latest/developer_manual/app/tutorial.html). However if you just want to start slow, the full process is described below. diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php index c70a2fb6f..755f3ea70 100644 --- a/lib/AppInfo/Application.php +++ b/lib/AppInfo/Application.php @@ -76,7 +76,6 @@ class Application extends App return $c->query(MapperFactory::class)->build(); }); - /** * App config parser. */ @@ -123,7 +122,6 @@ class Application extends App ); }); - $container->registerService(Config::class, function (IContainer $c): Config { $config = new Config( $c->query('ConfigView'), diff --git a/lib/Command/ExploreGenerator.php b/lib/Command/ExploreGenerator.php new file mode 100644 index 000000000..2e1b38e91 --- /dev/null +++ b/lib/Command/ExploreGenerator.php @@ -0,0 +1,94 @@ + + * @copyright Sean Molenaar 2019 + */ +namespace OCA\News\Command; + +use FeedIo\FeedIo; +use Favicon\Favicon; + +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; + +/** + * This is used for generating a JSON config section for a feed by executing: + * ./occ news:generate-explore www.feed.com + */ +class ExploreGenerator extends Command +{ + /** + * Feed and favicon fetcher. + */ + protected $reader; + protected $favicon; + + /** + * Set up class. + * + * @param FeedIo $reader Feed reader + * @param Favicon $favicon Favicon fetcher + */ + public function __construct(FeedIo $reader, Favicon $favicon) + { + $this->reader = $reader; + $this->favicon = $favicon; + parent::__construct(); + } + + protected function configure() + { + $result = [ + 'title' => 'Feed - Title', + 'favicon' => 'www.web.com/favicon.ico', + 'url' => 'www.web.com', + 'feed' => 'www.web.com/rss.xml', + 'description' => 'description is here', + 'votes' => 100, + ]; + + $this->setName('news:generate-explore') + ->setDescription( + 'Prints a JSON string which represents the given ' . + 'feed URL and votes, e.g.: ' . json_encode($result) + ) + ->addArgument('feed', InputArgument::REQUIRED, 'Feed to parse') + ->addOption('votes', null, InputOption::VALUE_OPTIONAL, 'Votes for the feed, defaults to 100'); + } + + protected function execute(InputInterface $input, OutputInterface $output) + { + $url = $input->getArgument('feed'); + $votes = $input->getOption('votes'); + if (!$votes) { + $votes = 100; + } + + try { + $resource = $this->reader->read($url); + $feed = $resource->getFeed(); + $result = [ + 'title' => $feed->getTitle(), + 'favicon' => $this->favicon->get($feed->getLink()), + 'url' => $feed->getLink(), + 'feed' => $url, + 'description' => $feed->getDescription(), + 'votes' => $votes, + ]; + + $output->writeln(json_encode($result, JSON_PRETTY_PRINT)); + } catch (\Throwable $ex) { + $output->writeln('Failed to fetch feed info:'); + $output->writeln($ex->getMessage()); + return 1; + } + } +} diff --git a/tests/Unit/Command/ExploreGeneratorTest.php b/tests/Unit/Command/ExploreGeneratorTest.php new file mode 100644 index 000000000..ac1f2c3c8 --- /dev/null +++ b/tests/Unit/Command/ExploreGeneratorTest.php @@ -0,0 +1,200 @@ + + * + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see + * + */ + +namespace OCA\News\Tests\Unit\Command; + +use FeedIo\Feed; +use FeedIo\FeedIo; +use Favicon\Favicon; +use FeedIo\Reader\Result; +use OCA\News\Command\ExploreGenerator; + +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; +use Test\TestCase; + +class ExploreGeneratorTest extends TestCase { + /** @var \PHPUnit_Framework_MockObject_MockObject */ + protected $favicon; + /** @var \PHPUnit_Framework_MockObject_MockObject */ + protected $feedio; + /** @var \PHPUnit_Framework_MockObject_MockObject */ + protected $consoleInput; + /** @var \PHPUnit_Framework_MockObject_MockObject */ + protected $consoleOutput; + + /** @var \Symfony\Component\Console\Command\Command */ + protected $command; + + protected function setUp() + { + parent::setUp(); + + $feedio = $this->feedio = $this->getMockBuilder(FeedIo::class) + ->disableOriginalConstructor() + ->getMock(); + + $favicon = $this->favicon = $this->getMockBuilder(Favicon::class) + ->disableOriginalConstructor() + ->getMock(); + $this->consoleInput = $this->getMockBuilder(InputInterface::class)->getMock(); + $this->consoleOutput = $this->getMockBuilder(OutputInterface::class)->getMock(); + + /** @var \FeedIo\FeedIo $feedio, \Favicon\Favicon $favicon */ + $this->command = new ExploreGenerator($feedio, $favicon); + } + + /** + * Test a valid feed will write the data needed. + */ + public function testValidFeed() + { + $result = $this->getMockBuilder(Result::class) + ->disableOriginalConstructor() + ->getMock(); + $feed = $this->getMockBuilder(Feed::class) + ->disableOriginalConstructor() + ->getMock(); + $feed->expects($this->once()) + ->method('getTitle') + ->willReturn('Title'); + $feed->expects($this->exactly(2)) + ->method('getLink') + ->willReturn('Link'); + $feed->expects($this->once()) + ->method('getDescription') + ->willReturn('Description'); + + $result->expects($this->once()) + ->method('getFeed') + ->willReturn($feed); + + $this->favicon->expects($this->once()) + ->method('get') + ->willReturn('https://feed.io/favicon.ico'); + + $this->feedio->expects($this->once()) + ->method('read') + ->with('https://feed.io/rss.xml') + ->willReturn($result); + + $this->consoleInput->expects($this->once()) + ->method('getArgument') + ->with('feed') + ->willReturn('https://feed.io/rss.xml'); + + $this->consoleInput->expects($this->once()) + ->method('getOption') + ->with('votes') + ->willReturn(100); + + $this->consoleOutput->expects($this->once()) + ->method('writeln') + ->with($this->stringContains('https:\/\/feed.io\/rss.xml')); + + self::invokePrivate($this->command, 'execute', [$this->consoleInput, $this->consoleOutput]); + } + + /** + * Test a valid feed will write the data needed. + */ + public function testFailingFeed() + { + + $this->favicon->expects($this->never()) + ->method('get'); + + $this->feedio->expects($this->once()) + ->method('read') + ->with('https://feed.io/rss.xml') + ->will($this->throwException(new \Exception('Failure'))); + + $this->consoleInput->expects($this->once()) + ->method('getArgument') + ->with('feed') + ->willReturn('https://feed.io/rss.xml'); + + $this->consoleInput->expects($this->once()) + ->method('getOption') + ->with('votes') + ->willReturn(100); + + $this->consoleOutput->expects($this->at(0)) + ->method('writeln') + ->with($this->stringContains('')); + + $this->consoleOutput->expects($this->at(1)) + ->method('writeln') + ->with($this->stringContains('Failure')); + + self::invokePrivate($this->command, 'execute', [$this->consoleInput, $this->consoleOutput]); + } + + /** + * Test a valid feed and votes will write the data needed. + */ + public function testFeedWithVotes() + { + $result = $this->getMockBuilder(Result::class) + ->disableOriginalConstructor() + ->getMock(); + $feed = $this->getMockBuilder(Feed::class) + ->disableOriginalConstructor() + ->getMock(); + $feed->expects($this->once()) + ->method('getTitle') + ->willReturn('Title'); + $feed->expects($this->exactly(2)) + ->method('getLink') + ->willReturn('Link'); + $feed->expects($this->once()) + ->method('getDescription') + ->willReturn('Description'); + + $result->expects($this->once()) + ->method('getFeed') + ->willReturn($feed); + + $this->favicon->expects($this->once()) + ->method('get') + ->willReturn('https://feed.io/favicon.ico'); + + $this->feedio->expects($this->once()) + ->method('read') + ->with('https://feed.io/rss.xml') + ->willReturn($result); + + $this->consoleInput->expects($this->once()) + ->method('getArgument') + ->with('feed') + ->willReturn('https://feed.io/rss.xml'); + + $this->consoleInput->expects($this->once()) + ->method('getOption') + ->with('votes') + ->willReturn(200); + + $this->consoleOutput->expects($this->once()) + ->method('writeln') + ->with($this->stringContains('200')); + + self::invokePrivate($this->command, 'execute', [$this->consoleInput, $this->consoleOutput]); + } +} -- cgit v1.2.3