Fix generation commands and make them available in ./occ (#402)

This commit is contained in:
Sean Molenaar 2019-03-06 13:10:37 +01:00 committed by Benjamin Brahmer
parent 6a4e56e727
commit 71ba5a3ad1
12 changed files with 398 additions and 70 deletions

View File

@ -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:

View File

@ -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

View File

@ -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));

10
bin/tools/generate_authors.php Normal file → Executable file
View File

@ -1,3 +1,4 @@
#!/usr/bin/env php
<?php
/**
* Nextcloud - News
@ -15,13 +16,20 @@ exec($cmd, $contributors);
// extract data from git output into an array
$regex = '/^\s*(?P<commit_count>\d+)\s*(?P<name>.*\w)\s*<(?P<email>[^\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

126
bin/tools/generate_explore.php Normal file → Executable file
View File

@ -1,3 +1,4 @@
#!/usr/bin/env php
<?php
/**
* Nextcloud - News
@ -8,58 +9,89 @@
* @author Bernhard Posselt <dev@bernhard-posselt.com>
* @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';
/**
* Argument data
*/
protected $url;
protected $votes;
/**
* Set up class.
*/
public function __construct()
{
$app = new Application();
$container = $app->getContainer();
$this->reader = $container->query(FeedIo::class);
$this->favicon = new Favicon();
}
/**
* 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);
}
$this->votes = (count($argv) === 3) ? $argv[2] : 100;
$this->url = $argv[1];
}
/**
* 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,
];
return $result;
} catch (\Throwable $ex) {
return [ 'error' => $ex->getMessage() ];
}
}
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);
$location = $resource->getUrl();
$content = $resource->getContent();
$encoding = $resource->getEncoding();
$parser = $reader->getParser($location, $content, $encoding);
$feed = $parser->execute();
$favicon = new PicoFeed\Reader\Favicon($config);
$result = [
"title" => $feed->getTitle(),
"favicon" => $favicon->find($url),
"url" => $feed->getSiteUrl(),
"feed" => $feed->getFeedUrl(),
"description" => $feed->getDescription(),
"votes" => $votes
];
if ($feed->getLogo()) {
$result["image"] = $feed->getLogo();
}
print(json_encode($result, JSON_PRETTY_PRINT));
} catch (\Exception $ex) {
print($ex->getMessage());
}
print("\n");

View File

@ -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",

View File

@ -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 :)

View File

@ -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

View File

@ -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.

View File

@ -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'),

View File

@ -0,0 +1,94 @@
<?php
/**
* Nextcloud - News
*
* This file is licensed under the Affero General Public License version 3 or
* later. See the COPYING file.
*
* @author Sean Molenaar <sean@seanmolenaar.eu>
* @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('<error>Failed to fetch feed info:</error>');
$output->writeln($ex->getMessage());
return 1;
}
}
}

View File

@ -0,0 +1,200 @@
<?php
/**
* @author Sean Molenaar <sean@seanmolenaar.eu>
*
* @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 <http://www.gnu.org/licenses/>
*
*/
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('<error>'));
$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]);
}
}