mirror of
https://code.mensbeam.com/MensBeam/Arsse.git
synced 2024-12-22 13:12:41 +00:00
Minimally functional, highly experimental, working server
- Basic update service handles only one feed at a time and possibly leaks memory - Output for REST requests is still very basic - No avatar support - No reporting of whether cron works - No cleanup before or after feed updates
This commit is contained in:
parent
d4674c61b2
commit
6d4aa4db6e
9 changed files with 194 additions and 32 deletions
10
arsse.php
Normal file
10
arsse.php
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
<?php
|
||||||
|
namespace JKingWeb\Arsse;
|
||||||
|
require_once __DIR__.DIRECTORY_SEPARATOR."bootstrap.php";
|
||||||
|
Data::load(new Conf());
|
||||||
|
|
||||||
|
if(\PHP_SAPI=="cli") {
|
||||||
|
(new Service)->watch();
|
||||||
|
} else {
|
||||||
|
(new REST)->dispatch();
|
||||||
|
}
|
|
@ -1,3 +0,0 @@
|
||||||
<?php
|
|
||||||
require_once __DIR__.DIRECTORY_SEPARATOR."bootstrap.php";
|
|
||||||
Data::load(new Conf());
|
|
22
lib/REST.php
22
lib/REST.php
|
@ -23,6 +23,8 @@ class REST {
|
||||||
// Tiny Tiny RSS https://tt-rss.org/gitlab/fox/tt-rss/wikis/ApiReference
|
// Tiny Tiny RSS https://tt-rss.org/gitlab/fox/tt-rss/wikis/ApiReference
|
||||||
// Fever https://feedafever.com/api
|
// Fever https://feedafever.com/api
|
||||||
// NewsBlur http://www.newsblur.com/api
|
// NewsBlur http://www.newsblur.com/api
|
||||||
|
// Miniflux https://github.com/miniflux/miniflux/blob/master/docs/json-rpc-api.markdown
|
||||||
|
// CommaFeed https://www.commafeed.com/api/
|
||||||
];
|
];
|
||||||
|
|
||||||
function __construct() {
|
function __construct() {
|
||||||
|
@ -36,8 +38,22 @@ class REST {
|
||||||
$class = $this->apis[$api]['class'];
|
$class = $this->apis[$api]['class'];
|
||||||
$drv = new $class();
|
$drv = new $class();
|
||||||
$out = $drv->dispatch($req);
|
$out = $drv->dispatch($req);
|
||||||
echo "Status: ".$out->code."\n";
|
header("Status: ".$out->code." ".Data::$lang->msg("HTTP.Status.".$out->code));
|
||||||
echo json_encode($out->payload,\JSON_PRETTY_PRINT);
|
if(!is_null($out->payload)) {
|
||||||
|
header("Content-Type: ".$out->type);
|
||||||
|
switch($out->type) {
|
||||||
|
case REST\Response::T_JSON:
|
||||||
|
$body = json_encode($out->payload,\JSON_PRETTY_PRINT);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
$body = (string) $out->payload;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
foreach($out->fields as $field) {
|
||||||
|
header($field);
|
||||||
|
}
|
||||||
|
echo $body;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -49,6 +65,6 @@ class REST {
|
||||||
if(strpos($url, $api['match'])===0) return $id;
|
if(strpos($url, $api['match'])===0) return $id;
|
||||||
}
|
}
|
||||||
// or throw an exception otherwise
|
// or throw an exception otherwise
|
||||||
throw new REST\ExceptionURL("apiNotSupported", $url);
|
throw new REST\Exception501();
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -13,6 +13,7 @@ use JKingWeb\Arsse\REST\Exception405;
|
||||||
|
|
||||||
class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler {
|
class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler {
|
||||||
const REALM = "NextCloud News API v1-2";
|
const REALM = "NextCloud News API v1-2";
|
||||||
|
const VERSION = "11.0.5";
|
||||||
|
|
||||||
protected $dateFormat = "unix";
|
protected $dateFormat = "unix";
|
||||||
|
|
||||||
|
@ -110,12 +111,19 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler {
|
||||||
'star/multiple' => ['PUT' => "articleMarkStarredMulti"],
|
'star/multiple' => ['PUT' => "articleMarkStarredMulti"],
|
||||||
'unstar/multiple' => ['PUT' => "articleMarkStarredMulti"],
|
'unstar/multiple' => ['PUT' => "articleMarkStarredMulti"],
|
||||||
],
|
],
|
||||||
'cleanup' => [],
|
'cleanup' => [
|
||||||
'version' => [
|
'before-update' => ['GET' => "cleanupBefore"],
|
||||||
'' => ['GET' => "versionReport"],
|
'after-update' => ['GET' => "cleanupAfter"],
|
||||||
|
],
|
||||||
|
'version' => [
|
||||||
|
'' => ['GET' => "serverVersion"],
|
||||||
|
],
|
||||||
|
'status' => [
|
||||||
|
'' => ['GET' => "serverStatus"],
|
||||||
|
],
|
||||||
|
'user' => [
|
||||||
|
'' => ['GET' => "userStatus"],
|
||||||
],
|
],
|
||||||
'status' => [],
|
|
||||||
'user' => [],
|
|
||||||
];
|
];
|
||||||
// the first path element is the overall scope of the request
|
// the first path element is the overall scope of the request
|
||||||
$scope = $url[0];
|
$scope = $url[0];
|
||||||
|
@ -284,11 +292,6 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler {
|
||||||
}
|
}
|
||||||
return new Response(204);
|
return new Response(204);
|
||||||
}
|
}
|
||||||
|
|
||||||
// return the server version
|
|
||||||
protected function versionReport(array $url, array $data): Response {
|
|
||||||
return new Response(200, ['version' => \JKingWeb\Arsse\VERSION]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// return list of feeds which should be refreshed
|
// return list of feeds which should be refreshed
|
||||||
protected function feedListStale(array $url, array $data): Response {
|
protected function feedListStale(array $url, array $data): Response {
|
||||||
|
@ -461,7 +464,7 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler {
|
||||||
$c->reverse(true);
|
$c->reverse(true);
|
||||||
}
|
}
|
||||||
// set the edition mark-off; the database uses an or-equal comparison for internal consistency, but the protocol does not, so we must adjust by one
|
// set the edition mark-off; the database uses an or-equal comparison for internal consistency, but the protocol does not, so we must adjust by one
|
||||||
if(isset($data['offset'])) {
|
if(isset($data['offset']) && $data['offset'] > 0) {
|
||||||
if($c->reverse) {
|
if($c->reverse) {
|
||||||
$c->latestEdition($data['offset'] - 1);
|
$c->latestEdition($data['offset'] - 1);
|
||||||
} else {
|
} else {
|
||||||
|
@ -590,4 +593,49 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler {
|
||||||
$t->commit();
|
$t->commit();
|
||||||
return new Response(204);
|
return new Response(204);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected function userStatus(array $url, array $data): Response {
|
||||||
|
// FIXME: stub
|
||||||
|
$data = Data::$db->userPropertiesGet(Data::$user->id);
|
||||||
|
$out = [
|
||||||
|
'userId' => Data::$user->id,
|
||||||
|
'displayName' => $data['name'] ?? Data::$user->id,
|
||||||
|
'lastLoginTimestamp' => time(),
|
||||||
|
'avatar' => null,
|
||||||
|
];
|
||||||
|
return new Response(200, $out);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function cleanupBefore(array $url, array $data): Response {
|
||||||
|
// function requires admin rights per spec
|
||||||
|
if(Data::$user->rightsGet(Data::$user->id)==User::RIGHTS_NONE) return new Response(403);
|
||||||
|
// FIXME: stub
|
||||||
|
return new Response(204);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function cleanupAfter(array $url, array $data): Response {
|
||||||
|
// function requires admin rights per spec
|
||||||
|
if(Data::$user->rightsGet(Data::$user->id)==User::RIGHTS_NONE) return new Response(403);
|
||||||
|
// FIXME: stub
|
||||||
|
return new Response(204);
|
||||||
|
}
|
||||||
|
|
||||||
|
// return the server version
|
||||||
|
protected function serverVersion(array $url, array $data): Response {
|
||||||
|
return new Response(200, [
|
||||||
|
'version' => self::VERSION,
|
||||||
|
'arsse_version' => \JKingWeb\Arsse\VERSION,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function serverStatus(array $url, array $data): Response {
|
||||||
|
// FIXME: stub
|
||||||
|
return new Response(200, [
|
||||||
|
'version' => self::VERSION,
|
||||||
|
'arsse_version' => \JKingWeb\Arsse\VERSION,
|
||||||
|
'warnings' => [
|
||||||
|
'improperlyConfiguredCron' => false,
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
}
|
||||||
}
|
}
|
42
lib/Service.php
Normal file
42
lib/Service.php
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
namespace JKingWeb\Arsse;
|
||||||
|
|
||||||
|
class Service {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var Service\Driver
|
||||||
|
*/
|
||||||
|
protected $drv;
|
||||||
|
/**
|
||||||
|
* @var \DateInterval
|
||||||
|
*/
|
||||||
|
protected $interval;
|
||||||
|
|
||||||
|
function __construct() {
|
||||||
|
$driver = Data::$conf->serviceDriver;
|
||||||
|
$this->drv = new $driver();
|
||||||
|
$this->interval = new \DateInterval(Data::$conf->serviceFrequency); // FIXME: this needs to fall back in case of incorrect input
|
||||||
|
}
|
||||||
|
|
||||||
|
function watch() {
|
||||||
|
while(true) {
|
||||||
|
$t = new \DateTime();
|
||||||
|
$list = Data::$db->feedListStale();
|
||||||
|
if($list) {
|
||||||
|
echo date("H:i:s")." Updating feeds ".json_encode($list)."\n";
|
||||||
|
// TODO: pre-cleanup
|
||||||
|
$this->drv->queue(...$list);
|
||||||
|
$this->drv->exec();
|
||||||
|
$this->drv->clean();
|
||||||
|
// TODO: post-cleanup
|
||||||
|
} else {
|
||||||
|
echo date("H:i:s")." No feeds to update; sleeping\n";
|
||||||
|
}
|
||||||
|
$t->add($this->interval);
|
||||||
|
do {
|
||||||
|
@time_sleep_until($t->getTimestamp());
|
||||||
|
} while($t->getTimestamp() > time());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
11
lib/Service/Driver.php
Normal file
11
lib/Service/Driver.php
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
namespace JKingWeb\Arsse\Service;
|
||||||
|
|
||||||
|
interface Driver {
|
||||||
|
static function driverName(): string;
|
||||||
|
static function requirementsMet(): bool;
|
||||||
|
function queue(int ...$feeds): int;
|
||||||
|
function exec(): int;
|
||||||
|
function clean(): bool;
|
||||||
|
}
|
38
lib/Service/Internal/Driver.php
Normal file
38
lib/Service/Internal/Driver.php
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
namespace JKingWeb\Arsse\Service\Internal;
|
||||||
|
use JKingWeb\Arsse\Data;
|
||||||
|
|
||||||
|
class Driver implements \JKingWeb\Arsse\Service\Driver {
|
||||||
|
protected $queue = [];
|
||||||
|
|
||||||
|
static function driverName(): string {
|
||||||
|
return Data::$lang->msg("Driver.Service.Internal.Name");
|
||||||
|
}
|
||||||
|
|
||||||
|
static function requirementsMet(): bool {
|
||||||
|
// this driver has no requirements
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function __construct() {
|
||||||
|
}
|
||||||
|
|
||||||
|
function queue(int ...$feeds): int {
|
||||||
|
$this->queue = array_merge($this->queue, $feeds);
|
||||||
|
return sizeof($this->queue);
|
||||||
|
}
|
||||||
|
|
||||||
|
function exec(): int {
|
||||||
|
while(sizeof($this->queue)) {
|
||||||
|
$id = array_shift($this->queue);
|
||||||
|
Data::$db->feedUpdate($id);
|
||||||
|
}
|
||||||
|
return Data::$conf->serviceQueueWidth - sizeof($this->queue);
|
||||||
|
}
|
||||||
|
|
||||||
|
function clean(): bool {
|
||||||
|
$this->queue = [];
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,23 +1,22 @@
|
||||||
<?php
|
<?php
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
namespace JKingWeb\Arsse\User\Internal;
|
namespace JKingWeb\Arsse\User\Internal;
|
||||||
use JKingWeb\Arsse\User\Driver as Iface;
|
|
||||||
|
|
||||||
final class Driver implements Iface {
|
final class Driver implements \JKingWeb\Arsse\User\Driver {
|
||||||
use InternalFunctions;
|
use InternalFunctions;
|
||||||
|
|
||||||
protected $db;
|
protected $db;
|
||||||
protected $functions = [
|
protected $functions = [
|
||||||
"auth" => Iface::FUNC_INTERNAL,
|
"auth" => self::FUNC_INTERNAL,
|
||||||
"userList" => Iface::FUNC_INTERNAL,
|
"userList" => self::FUNC_INTERNAL,
|
||||||
"userExists" => Iface::FUNC_INTERNAL,
|
"userExists" => self::FUNC_INTERNAL,
|
||||||
"userAdd" => Iface::FUNC_INTERNAL,
|
"userAdd" => self::FUNC_INTERNAL,
|
||||||
"userRemove" => Iface::FUNC_INTERNAL,
|
"userRemove" => self::FUNC_INTERNAL,
|
||||||
"userPasswordSet" => Iface::FUNC_INTERNAL,
|
"userPasswordSet" => self::FUNC_INTERNAL,
|
||||||
"userPropertiesGet" => Iface::FUNC_INTERNAL,
|
"userPropertiesGet" => self::FUNC_INTERNAL,
|
||||||
"userPropertiesSet" => Iface::FUNC_INTERNAL,
|
"userPropertiesSet" => self::FUNC_INTERNAL,
|
||||||
"userRightsGet" => Iface::FUNC_INTERNAL,
|
"userRightsGet" => self::FUNC_INTERNAL,
|
||||||
"userRightsSet" => Iface::FUNC_INTERNAL,
|
"userRightsSet" => self::FUNC_INTERNAL,
|
||||||
];
|
];
|
||||||
|
|
||||||
static public function driverName(): string {
|
static public function driverName(): string {
|
||||||
|
@ -29,7 +28,7 @@ final class Driver implements Iface {
|
||||||
if(array_key_exists($function, $this->functions)) {
|
if(array_key_exists($function, $this->functions)) {
|
||||||
return $this->functions[$function];
|
return $this->functions[$function];
|
||||||
} else {
|
} else {
|
||||||
return Iface::FUNC_NOT_IMPLEMENTED;
|
return self::FUNC_NOT_IMPLEMENTED;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
<?php
|
<?php
|
||||||
return [
|
return [
|
||||||
'Driver.User.Internal.Name' => 'Internal',
|
|
||||||
|
|
||||||
'Driver.Db.SQLite3.Name' => 'SQLite 3',
|
'Driver.Db.SQLite3.Name' => 'SQLite 3',
|
||||||
|
'Driver.Service.Curl.Name' => 'HTTP (curl)',
|
||||||
|
'Driver.Service.Internal.Name' => 'Internal',
|
||||||
|
'Driver.User.Internal.Name' => 'Internal',
|
||||||
|
|
||||||
'HTTP.Status.200' => 'OK',
|
'HTTP.Status.200' => 'OK',
|
||||||
'HTTP.Status.204' => 'No Content',
|
'HTTP.Status.204' => 'No Content',
|
||||||
|
|
Loading…
Reference in a new issue