mirror of
https://code.mensbeam.com/MensBeam/Arsse.git
synced 2024-12-22 21:22:40 +00:00
Implement Miniflux feed discovery
This commit is contained in:
parent
669e17a1f6
commit
94154d4354
4 changed files with 75 additions and 7 deletions
|
@ -7,21 +7,31 @@ declare(strict_types=1);
|
||||||
namespace JKingWeb\Arsse\REST\Miniflux;
|
namespace JKingWeb\Arsse\REST\Miniflux;
|
||||||
|
|
||||||
use JKingWeb\Arsse\Arsse;
|
use JKingWeb\Arsse\Arsse;
|
||||||
|
use JKingWeb\Arsse\Feed;
|
||||||
|
use JKingWeb\Arsse\Feed\Exception as FeedException;
|
||||||
use JKingWeb\Arsse\AbstractException;
|
use JKingWeb\Arsse\AbstractException;
|
||||||
use JKingWeb\Arsse\Db\ExceptionInput;
|
use JKingWeb\Arsse\Db\ExceptionInput;
|
||||||
use JKingWeb\Arsse\Misc\HTTP;
|
use JKingWeb\Arsse\Misc\HTTP;
|
||||||
use JKingWeb\Arsse\Misc\ValueInfo;
|
use JKingWeb\Arsse\Misc\ValueInfo as V;
|
||||||
use JKingWeb\Arsse\REST\Exception;
|
use JKingWeb\Arsse\REST\Exception;
|
||||||
use JKingWeb\Arsse\User\ExceptionConflict as UserException;
|
use JKingWeb\Arsse\User\ExceptionConflict as UserException;
|
||||||
use Psr\Http\Message\ServerRequestInterface;
|
use Psr\Http\Message\ServerRequestInterface;
|
||||||
use Psr\Http\Message\ResponseInterface;
|
use Psr\Http\Message\ResponseInterface;
|
||||||
use Laminas\Diactoros\Response\EmptyResponse;
|
use Laminas\Diactoros\Response\EmptyResponse;
|
||||||
|
use Laminas\Diactoros\Response\JsonResponse as Response;
|
||||||
|
|
||||||
class V1 extends \JKingWeb\Arsse\REST\AbstractHandler {
|
class V1 extends \JKingWeb\Arsse\REST\AbstractHandler {
|
||||||
|
public const VERSION = "2.0.25";
|
||||||
|
|
||||||
protected const ACCEPTED_TYPES_OPML = ["application/xml", "text/xml", "text/x-opml"];
|
protected const ACCEPTED_TYPES_OPML = ["application/xml", "text/xml", "text/x-opml"];
|
||||||
protected const ACCEPTED_TYPES_JSON = ["application/json"];
|
protected const ACCEPTED_TYPES_JSON = ["application/json"];
|
||||||
protected const TOKEN_LENGTH = 32;
|
protected const TOKEN_LENGTH = 32;
|
||||||
public const VERSION = "2.0.25";
|
protected const VALID_JSON = [
|
||||||
|
'url' => "string",
|
||||||
|
'username' => "string",
|
||||||
|
'password' => "string",
|
||||||
|
'user_agent' => "string",
|
||||||
|
];
|
||||||
|
|
||||||
protected $paths = [
|
protected $paths = [
|
||||||
'/categories' => ['GET' => "getCategories", 'POST' => "createCategory"],
|
'/categories' => ['GET' => "getCategories", 'POST' => "createCategory"],
|
||||||
|
@ -86,6 +96,8 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler {
|
||||||
if ($func instanceof ResponseInterface) {
|
if ($func instanceof ResponseInterface) {
|
||||||
return $func;
|
return $func;
|
||||||
}
|
}
|
||||||
|
$data = [];
|
||||||
|
$query = [];
|
||||||
if ($func === "opmlImport") {
|
if ($func === "opmlImport") {
|
||||||
if (!HTTP::matchType($req, "", ...[self::ACCEPTED_TYPES_OPML])) {
|
if (!HTTP::matchType($req, "", ...[self::ACCEPTED_TYPES_OPML])) {
|
||||||
return new ErrorResponse("", 415, ['Accept' => implode(", ", self::ACCEPTED_TYPES_OPML)]);
|
return new ErrorResponse("", 415, ['Accept' => implode(", ", self::ACCEPTED_TYPES_OPML)]);
|
||||||
|
@ -97,12 +109,16 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler {
|
||||||
// if the body could not be parsed as JSON, return "400 Bad Request"
|
// if the body could not be parsed as JSON, return "400 Bad Request"
|
||||||
return new ErrorResponse(["invalidBodyJSON", json_last_error_msg()], 400);
|
return new ErrorResponse(["invalidBodyJSON", json_last_error_msg()], 400);
|
||||||
}
|
}
|
||||||
} else {
|
$data = $this->normalizeBody((array) $data);
|
||||||
$data = null;
|
if ($data instanceof ResponseInterface) {
|
||||||
|
return $data;
|
||||||
|
}
|
||||||
|
} elseif ($method === "GET") {
|
||||||
|
$query = $req->getQueryParams();
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
$path = explode("/", ltrim($target, "/"));
|
$path = explode("/", ltrim($target, "/"));
|
||||||
return $this->$func($path, $req->getQueryParams(), $data);
|
return $this->$func($path, $query, $data);
|
||||||
// @codeCoverageIgnoreStart
|
// @codeCoverageIgnoreStart
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
// if there was a REST exception return 400
|
// if there was a REST exception return 400
|
||||||
|
@ -118,7 +134,7 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler {
|
||||||
$path = explode("/", $url);
|
$path = explode("/", $url);
|
||||||
// any path components which are database IDs (integers greater than zero) should be replaced with "1", for easier comparison (we don't care about the specific ID)
|
// any path components which are database IDs (integers greater than zero) should be replaced with "1", for easier comparison (we don't care about the specific ID)
|
||||||
for ($a = 0; $a < sizeof($path); $a++) {
|
for ($a = 0; $a < sizeof($path); $a++) {
|
||||||
if (ValueInfo::id($path[$a])) {
|
if (V::id($path[$a])) {
|
||||||
$path[$a] = "1";
|
$path[$a] = "1";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -172,6 +188,37 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected function normalizeBody(array $body) {
|
||||||
|
// Miniflux does not attempt to coerce values into different types
|
||||||
|
foreach (self::VALID_JSON as $k => $t) {
|
||||||
|
if (!isset($body[$k])) {
|
||||||
|
$body[$k] = null;
|
||||||
|
} elseif (gettype($body[$k]) !== $t) {
|
||||||
|
return new ErrorResponse(["invalidInputType", 'field' => $k, 'expected' => $t, 'actual' => gettype($body[$k])]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $body;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function discoverSubscriptions(array $path, array $query, array $data) {
|
||||||
|
try {
|
||||||
|
$list = Feed::discoverAll((string) $data['url'], (string) $data['username'], (string) $data['password']);
|
||||||
|
} catch (FeedException $e) {
|
||||||
|
$msg = [
|
||||||
|
10502 => "fetch404",
|
||||||
|
10506 => "fetch403",
|
||||||
|
10507 => "fetch401",
|
||||||
|
][$e->getCode()] ?? "fetchOther";
|
||||||
|
return new ErrorResponse($msg, 500);
|
||||||
|
}
|
||||||
|
$out = [];
|
||||||
|
foreach($list as $url) {
|
||||||
|
// TODO: This needs to be refined once PicoFeed is replaced
|
||||||
|
$out[] = ['title' => "Feed", 'type' => "rss", 'url' => $url];
|
||||||
|
}
|
||||||
|
return new Response($out);
|
||||||
|
}
|
||||||
|
|
||||||
public static function tokenGenerate(string $user, string $label): string {
|
public static function tokenGenerate(string $user, string $label): string {
|
||||||
// Miniflux produces tokens in base64url alphabet
|
// Miniflux produces tokens in base64url alphabet
|
||||||
$t = str_replace(["+", "/"], ["-", "_"], base64_encode(random_bytes(self::TOKEN_LENGTH)));
|
$t = str_replace(["+", "/"], ["-", "_"], base64_encode(random_bytes(self::TOKEN_LENGTH)));
|
||||||
|
|
|
@ -22,7 +22,6 @@ use Laminas\Diactoros\Response\EmptyResponse;
|
||||||
|
|
||||||
class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler {
|
class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler {
|
||||||
public const VERSION = "11.0.5";
|
public const VERSION = "11.0.5";
|
||||||
protected const REALM = "Nextcloud News API v1-2";
|
|
||||||
protected const ACCEPTED_TYPE = "application/json";
|
protected const ACCEPTED_TYPE = "application/json";
|
||||||
|
|
||||||
protected $dateFormat = "unix";
|
protected $dateFormat = "unix";
|
||||||
|
|
|
@ -9,6 +9,11 @@ return [
|
||||||
|
|
||||||
'API.Miniflux.Error.401' => 'Access Unauthorized',
|
'API.Miniflux.Error.401' => 'Access Unauthorized',
|
||||||
'API.Miniflux.Error.invalidBodyJSON' => 'Invalid JSON payload: {0}',
|
'API.Miniflux.Error.invalidBodyJSON' => 'Invalid JSON payload: {0}',
|
||||||
|
'API.Miniflux.Error.invalidInputType' => 'Input key "{field}" of type {actual} was expected as {expected}',
|
||||||
|
'API.Miniflux.Error.fetch404' => 'Resource not found (404), this feed doesn\'t exists anymore, check the feed URL',
|
||||||
|
'API.Miniflux.Error.fetch401' => 'You are not authorized to access this resource (invalid username/password)',
|
||||||
|
'API.Miniflux.Error.fetch403' => 'Unable to fetch this resource (Status Code = 403)',
|
||||||
|
'API.Miniflux.Error.fetchOther' => 'Unable to fetch this resource',
|
||||||
|
|
||||||
'API.TTRSS.Category.Uncategorized' => 'Uncategorized',
|
'API.TTRSS.Category.Uncategorized' => 'Uncategorized',
|
||||||
'API.TTRSS.Category.Special' => 'Special',
|
'API.TTRSS.Category.Special' => 'Special',
|
||||||
|
|
|
@ -122,4 +122,21 @@ class TestV1 extends \JKingWeb\Arsse\Test\AbstractTest {
|
||||||
["/import", "POST", "application/xml, text/xml, text/x-opml"],
|
["/import", "POST", "application/xml, text/xml, text/x-opml"],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testRejectBadlyTypedData(): void {
|
||||||
|
$exp = new ErrorResponse(["invalidInputType", 'field' => "url", 'expected' => "string", 'actual' => "integer"], 400);
|
||||||
|
$this->assertMessage($exp, $this->req("POST", "/discover", ['url' => 2112]));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testDiscoverFeeds(): void {
|
||||||
|
$exp = new Response([
|
||||||
|
['title' => "Feed", 'type' => "rss", 'url' => "http://localhost:8000/Feed/Discovery/Feed"],
|
||||||
|
['title' => "Feed", 'type' => "rss", 'url' => "http://localhost:8000/Feed/Discovery/Missing"],
|
||||||
|
]);
|
||||||
|
$this->assertMessage($exp, $this->req("POST", "/discover", ['url' => "http://localhost:8000/Feed/Discovery/Valid"]));
|
||||||
|
$exp = new Response([]);
|
||||||
|
$this->assertMessage($exp, $this->req("POST", "/discover", ['url' => "http://localhost:8000/Feed/Discovery/Invalid"]));
|
||||||
|
$exp = new ErrorResponse("fetch404", 500);
|
||||||
|
$this->assertMessage($exp, $this->req("POST", "/discover", ['url' => "http://localhost:8000/Feed/Discovery/Missing"]));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue