mirror of
https://code.mensbeam.com/MensBeam/Arsse.git
synced 2024-12-22 21:22:40 +00:00
Prototype Miniflux user querying
This commit is contained in:
parent
2eedf7d38c
commit
d85988f09d
3 changed files with 85 additions and 13 deletions
|
@ -6,7 +6,7 @@
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
namespace JKingWeb\Arsse\Misc;
|
namespace JKingWeb\Arsse\Misc;
|
||||||
|
|
||||||
class Date {
|
abstract class Date {
|
||||||
public static function transform($date, string $outFormat = null, string $inFormat = null) {
|
public static function transform($date, string $outFormat = null, string $inFormat = null) {
|
||||||
$date = ValueInfo::normalize($date, ValueInfo::T_DATE, $inFormat);
|
$date = ValueInfo::normalize($date, ValueInfo::T_DATE, $inFormat);
|
||||||
if (!$date) {
|
if (!$date) {
|
||||||
|
|
|
@ -12,6 +12,7 @@ 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\Date;
|
||||||
use JKingWeb\Arsse\Misc\ValueInfo as V;
|
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;
|
||||||
|
@ -32,8 +33,7 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler {
|
||||||
'password' => "string",
|
'password' => "string",
|
||||||
'user_agent' => "string",
|
'user_agent' => "string",
|
||||||
];
|
];
|
||||||
|
protected const PATHS = [
|
||||||
protected $paths = [
|
|
||||||
'/categories' => ['GET' => "getCategories", 'POST' => "createCategory"],
|
'/categories' => ['GET' => "getCategories", 'POST' => "createCategory"],
|
||||||
'/categories/1' => ['PUT' => "updateCategory", 'DELETE' => "deleteCategory"],
|
'/categories/1' => ['PUT' => "updateCategory", 'DELETE' => "deleteCategory"],
|
||||||
'/discover' => ['POST' => "discoverSubscriptions"],
|
'/discover' => ['POST' => "discoverSubscriptions"],
|
||||||
|
@ -42,7 +42,7 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler {
|
||||||
'/entries/1/bookmark' => ['PUT' => "toggleEntryBookmark"],
|
'/entries/1/bookmark' => ['PUT' => "toggleEntryBookmark"],
|
||||||
'/export' => ['GET' => "opmlExport"],
|
'/export' => ['GET' => "opmlExport"],
|
||||||
'/feeds' => ['GET' => "getFeeds", 'POST' => "createFeed"],
|
'/feeds' => ['GET' => "getFeeds", 'POST' => "createFeed"],
|
||||||
'/feeds/1' => ['GET' => "getFeed", 'PUT' => "updateFeed", 'DELETE' => "removeFeed"],
|
'/feeds/1' => ['GET' => "getFeed", 'PUT' => "updateFeed", 'DELETE' => "removeFeed"],
|
||||||
'/feeds/1/entries/1' => ['GET' => "getFeedEntry"],
|
'/feeds/1/entries/1' => ['GET' => "getFeedEntry"],
|
||||||
'/feeds/1/entries' => ['GET' => "getFeedEntries"],
|
'/feeds/1/entries' => ['GET' => "getFeedEntries"],
|
||||||
'/feeds/1/icon' => ['GET' => "getFeedIcon"],
|
'/feeds/1/icon' => ['GET' => "getFeedIcon"],
|
||||||
|
@ -51,8 +51,16 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler {
|
||||||
'/import' => ['POST' => "opmlImport"],
|
'/import' => ['POST' => "opmlImport"],
|
||||||
'/me' => ['GET' => "getCurrentUser"],
|
'/me' => ['GET' => "getCurrentUser"],
|
||||||
'/users' => ['GET' => "getUsers", 'POST' => "createUser"],
|
'/users' => ['GET' => "getUsers", 'POST' => "createUser"],
|
||||||
'/users/1' => ['GET' => "getUser", 'PUT' => "updateUser", 'DELETE' => "deleteUser"],
|
'/users/1' => ['GET' => "getUserByNum", 'PUT' => "updateUserByNum", 'DELETE' => "deleteUser"],
|
||||||
'/users/*' => ['GET' => "getUser"],
|
'/users/*' => ['GET' => "getUserById"],
|
||||||
|
];
|
||||||
|
protected const ADMIN_FUNCTIONS = [
|
||||||
|
'getUsers' => true,
|
||||||
|
'getUserByNum' => true,
|
||||||
|
'getUserById' => true,
|
||||||
|
'createUser' => true,
|
||||||
|
'updateUserByNum' => true,
|
||||||
|
'deleteUser' => true,
|
||||||
];
|
];
|
||||||
|
|
||||||
public function __construct() {
|
public function __construct() {
|
||||||
|
@ -80,6 +88,11 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected function isAdmin(): bool {
|
||||||
|
return (bool) Arsse::$user->propertiesGet(Arsse::$user->id, false)['admin'];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
public function dispatch(ServerRequestInterface $req): ResponseInterface {
|
public function dispatch(ServerRequestInterface $req): ResponseInterface {
|
||||||
// try to authenticate
|
// try to authenticate
|
||||||
if (!$this->authenticate($req)) {
|
if (!$this->authenticate($req)) {
|
||||||
|
@ -96,6 +109,9 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler {
|
||||||
if ($func instanceof ResponseInterface) {
|
if ($func instanceof ResponseInterface) {
|
||||||
return $func;
|
return $func;
|
||||||
}
|
}
|
||||||
|
if ((self::ADMIN_FUNCTIONS[$func] ?? false) && !$this->isAdmin()) {
|
||||||
|
return new ErrorResponse("403", 403);
|
||||||
|
}
|
||||||
$data = [];
|
$data = [];
|
||||||
$query = [];
|
$query = [];
|
||||||
if ($func === "opmlImport") {
|
if ($func === "opmlImport") {
|
||||||
|
@ -148,9 +164,9 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler {
|
||||||
protected function handleHTTPOptions(string $url): ResponseInterface {
|
protected function handleHTTPOptions(string $url): ResponseInterface {
|
||||||
// normalize the URL path: change any IDs to 1 for easier comparison
|
// normalize the URL path: change any IDs to 1 for easier comparison
|
||||||
$url = $this->normalizePathIDs($url);
|
$url = $this->normalizePathIDs($url);
|
||||||
if (isset($this->paths[$url])) {
|
if (isset(self::PATHS[$url])) {
|
||||||
// if the path is supported, respond with the allowed methods and other metadata
|
// if the path is supported, respond with the allowed methods and other metadata
|
||||||
$allowed = array_keys($this->paths[$url]);
|
$allowed = array_keys(self::PATHS[$url]);
|
||||||
// if GET is allowed, so is HEAD
|
// if GET is allowed, so is HEAD
|
||||||
if (in_array("GET", $allowed)) {
|
if (in_array("GET", $allowed)) {
|
||||||
array_unshift($allowed, "HEAD");
|
array_unshift($allowed, "HEAD");
|
||||||
|
@ -172,15 +188,15 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler {
|
||||||
$method = strtoupper($method);
|
$method = strtoupper($method);
|
||||||
// we now evaluate the supplied URL against every supported path for the selected scope
|
// we now evaluate the supplied URL against every supported path for the selected scope
|
||||||
// the URL is evaluated as an array so as to avoid decoded escapes turning invalid URLs into valid ones
|
// the URL is evaluated as an array so as to avoid decoded escapes turning invalid URLs into valid ones
|
||||||
if (isset($this->paths[$url])) {
|
if (isset(self::PATHS[$url])) {
|
||||||
// if the path is supported, make sure the method is allowed
|
// if the path is supported, make sure the method is allowed
|
||||||
if (isset($this->paths[$url][$method])) {
|
if (isset(self::PATHS[$url][$method])) {
|
||||||
// if it is allowed, return the object method to run, assuming the method exists
|
// if it is allowed, return the object method to run, assuming the method exists
|
||||||
assert(method_exists($this, $this->paths[$url][$method]), new \Exception("Method is not implemented"));
|
assert(method_exists($this, self::PATHS[$url][$method]), new \Exception("Method is not implemented"));
|
||||||
return $this->paths[$url][$method];
|
return self::PATHS[$url][$method];
|
||||||
} else {
|
} else {
|
||||||
// otherwise return 405
|
// otherwise return 405
|
||||||
return new EmptyResponse(405, ['Allow' => implode(", ", array_keys($this->paths[$url]))]);
|
return new EmptyResponse(405, ['Allow' => implode(", ", array_keys(self::PATHS[$url]))]);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// if the path is not supported, return 404
|
// if the path is not supported, return 404
|
||||||
|
@ -200,6 +216,40 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler {
|
||||||
return $body;
|
return $body;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected function listUsers(array $users, bool $reportMissing): array {
|
||||||
|
$out = [];
|
||||||
|
$now = Date::transform("now", "iso8601m");
|
||||||
|
foreach ($users as $u) {
|
||||||
|
try {
|
||||||
|
$info = Arsse::$user->propertiesGet($u, true);
|
||||||
|
} catch (UserException $e) {
|
||||||
|
if ($reportMissing) {
|
||||||
|
throw $e;
|
||||||
|
} else {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$out[] = [
|
||||||
|
'id' => $info['num'],
|
||||||
|
'username' => $u,
|
||||||
|
'is_admin' => $info['admin'] ?? false,
|
||||||
|
'theme' => $info['theme'] ?? "light_serif",
|
||||||
|
'language' => $info['lang'] ?? "en_US",
|
||||||
|
'timezone' => $info['tz'] ?? "UTC",
|
||||||
|
'entry_sorting_direction' => ($info['sort_asc'] ?? false) ? "asc" : "desc",
|
||||||
|
'entries_per_page' => $info['page_size'] ?? 100,
|
||||||
|
'keyboard_shortcuts' => $info['shortcuts'] ?? true,
|
||||||
|
'show_reading_time' => $info['reading_time'] ?? true,
|
||||||
|
'last_login_at' => $now,
|
||||||
|
'entry_swipe' => $info['swipe'] ?? true,
|
||||||
|
'extra' => [
|
||||||
|
'custom_css' => $info['stylesheet'] ?? "",
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
return $out;
|
||||||
|
}
|
||||||
|
|
||||||
protected function discoverSubscriptions(array $path, array $query, array $data) {
|
protected function discoverSubscriptions(array $path, array $query, array $data) {
|
||||||
try {
|
try {
|
||||||
$list = Feed::discoverAll((string) $data['url'], (string) $data['username'], (string) $data['password']);
|
$list = Feed::discoverAll((string) $data['url'], (string) $data['username'], (string) $data['password']);
|
||||||
|
@ -219,6 +269,26 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler {
|
||||||
return new Response($out);
|
return new Response($out);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected function getUsers(array $path, array $query, array $data) {
|
||||||
|
return new Response($this->listUsers(Arsse::$user->list(), false));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getUserById(array $path, array $query, array $data) {
|
||||||
|
try {
|
||||||
|
return $this->listUsers([$path[1]], true)[0] ?? [];
|
||||||
|
} catch (UserException $e) {
|
||||||
|
return new ErrorResponse("404", 404);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getUserByNum(array $path, array $query, array $data) {
|
||||||
|
return $this->listUsers([Arsse::$user->id], false)[0] ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getCurrentUser(array $path, array $query, array $data) {
|
||||||
|
return new Response($this->listUsers([Arsse::$user->id], false)[0] ?? new \stdClass);
|
||||||
|
}
|
||||||
|
|
||||||
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)));
|
||||||
|
|
|
@ -8,6 +8,8 @@ return [
|
||||||
'CLI.Auth.Failure' => 'Authentication failed',
|
'CLI.Auth.Failure' => 'Authentication failed',
|
||||||
|
|
||||||
'API.Miniflux.Error.401' => 'Access Unauthorized',
|
'API.Miniflux.Error.401' => 'Access Unauthorized',
|
||||||
|
'API.Miniflux.Error.403' => 'Access Forbidden',
|
||||||
|
'API.Miniflux.Error.404' => 'Resource Not Found',
|
||||||
'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.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.fetch404' => 'Resource not found (404), this feed doesn\'t exists anymore, check the feed URL',
|
||||||
|
|
Loading…
Reference in a new issue