1
1
Fork 0
mirror of https://code.mensbeam.com/MensBeam/Arsse.git synced 2024-12-22 21:22:40 +00:00

Implement CLI for user metadata

This commit is contained in:
J. King 2021-02-10 11:24:01 -05:00
parent b7c7915a65
commit 68422390da
5 changed files with 172 additions and 7 deletions

View file

@ -5,6 +5,7 @@ New features:
- Support for the Miniflux protocol (see manual for details) - Support for the Miniflux protocol (see manual for details)
- Support for API level 15 of Tiny Tiny RSS - Support for API level 15 of Tiny Tiny RSS
- Support for feed icons in Fever - Support for feed icons in Fever
- Command-line functionality for managing user metadata
Bug fixes: Bug fixes:
- Use icons specified in Atom feeds when available - Use icons specified in Atom feeds when available

View file

@ -17,8 +17,11 @@ Usage:
arsse.php feed refresh <n> arsse.php feed refresh <n>
arsse.php conf save-defaults [<file>] arsse.php conf save-defaults [<file>]
arsse.php user [list] arsse.php user [list]
arsse.php user add <username> [<password>] arsse.php user add <username> [<password>] [--admin]
arsse.php user remove <username> arsse.php user remove <username>
arsse.php user show <username>
arsse.php user set <username> <property> <value>
arsse.php user unset <username> <property>
arsse.php user set-pass <username> [<password>] arsse.php user set-pass <username> [<password>]
[--oldpass=<pass>] [--fever] [--oldpass=<pass>] [--fever]
arsse.php user unset-pass <username> arsse.php user unset-pass <username>
@ -63,11 +66,13 @@ Commands:
Prints a list of all existing users, one per line. Prints a list of all existing users, one per line.
user add <username> [<password>] user add <username> [<password>] [--admin]
Adds the user specified by <username>, with the provided password Adds the user specified by <username>, with the provided password
<password>. If no password is specified, a random password will be <password>. If no password is specified, a random password will be
generated and printed to standard output. generated and printed to standard output. The --admin option will make
the user an administrator, which allows them to manage users via the
Miniflux protocol, among other things.
user remove <username> user remove <username>
@ -76,6 +81,22 @@ Commands:
which the user was subscribed will be retained and refreshed until the which the user was subscribed will be retained and refreshed until the
configured retention time elapses. configured retention time elapses.
user show <username>
Displays the metadata of a user in a basic tabular format. See below for
details on the various properties displayed.
user set <username> <property> <value>
Sets a user's metadata proprty to the supplied value. See below for
details on the various properties available.
user unset <username> <property>
Sets a user's metadata proprty to its default value. See below for
details on the various properties available. What the default value
for a property evaluates to depends on which protocol is used.
user set-pass <username> [<password>] user set-pass <username> [<password>]
Changes <username>'s password to <password>. If no password is specified, Changes <username>'s password to <password>. If no password is specified,
@ -128,6 +149,65 @@ Commands:
The --flat option can be used to omit folders from the export. Some OPML The --flat option can be used to omit folders from the export. Some OPML
implementations may not support folders, or arbitrary nesting; this option implementations may not support folders, or arbitrary nesting; this option
may be used when planning to import into such software. may be used when planning to import into such software.
User metadata:
User metadata is primary used by the Miniflux protocol, and most
properties have identical or similar names to those used by Miniflux.
Properties may also affect other protocols, or conversely may have no
effect even when using the Miniflux protocol; this is noted below when
appropriate.
Booleans accept any of the values true/false, 1/0, yes/no, on/off.
The following metadata properties exist for each user:
num
Integer. The numeric identifier of the user. This is assigned at user
creation and is read-only.
admin
Boolean. Whether the user is an administrator. Administrators may
manage other users via the Miniflux protocol, and also may trigger
feed updates manually via the Nextcloud News protocol.
lang
String. The preferred language of the user, as a BCP 47 language tag
e.g. "en-ca". Note that since The Arsse currently only includes
English text it is not used by The Arsse itself, but clients may
use this metadata in protocols which expose it.
tz
String. The time zone of the user, as a tzdata identifier e.g.
"America/Los_Angeles".
root_folder_name
String. The name of the root folder, in protocols which allow it to
be renamed.
sort_asc
Boolean. Whether the user prefers ascending sort order for articles.
Descending order is usually the default, but explicitly setting this
property false will also make a preference for descending order
explicit.
theme
String. The user's preferred theme. This is not used by The Arsse
itself, but clients may use this metadata in protocols which expose
it.
page_size
Integer. The user's preferred pge size when listing articles. This is
not used by The Arsse itself, but clients may use this metadata in
protocols which expose it.
shortcuts
Boolean. Whether to enable keyboard shortcuts. This is not used by
The Arsse itself, but clients may use this metadata in protocols which
expose it.
gestures
Boolean. Whether to enable touch gestures. This is not used by
The Arsse itself, but clients may use this metadata in protocols which
expose it.
reading_time
Boolean. Whether to calculate and display the estimated reading time
for articles. Currently The Arsse does not calculate reading time, so
changing this will likely have no effect.
stylesheet
String. A user CSS stylesheet. This is not used by The Arsse itself,
but clients may use this metadata in protocols which expose it.
USAGE_TEXT; USAGE_TEXT;
protected function usage($prog): string { protected function usage($prog): string {
@ -215,10 +295,14 @@ USAGE_TEXT;
} }
protected function userManage($args): int { protected function userManage($args): int {
$cmd = $this->command(["add", "remove", "set-pass", "unset-pass", "list", "auth"], $args); $cmd = $this->command(["add", "remove", "show", "set", "unset", "set-pass", "unset-pass", "list", "auth"], $args);
switch ($cmd) { switch ($cmd) {
case "add": case "add":
return $this->userAddOrSetPassword("add", $args["<username>"], $args["<password>"]); $out = $this->userAddOrSetPassword("add", $args["<username>"], $args["<password>"]);
if ($args['--admin']) {
Arsse::$user->propertiesSet($args["<username>"], ['admin' => true]);
}
return $out;
case "set-pass": case "set-pass":
if ($args['--fever']) { if ($args['--fever']) {
$passwd = Arsse::$obj->get(Fever::class)->register($args["<username>"], $args["<password>"]); $passwd = Arsse::$obj->get(Fever::class)->register($args["<username>"], $args["<password>"]);
@ -239,6 +323,12 @@ USAGE_TEXT;
return 0; return 0;
case "remove": case "remove":
return (int) !Arsse::$user->remove($args["<username>"]); return (int) !Arsse::$user->remove($args["<username>"]);
case "show":
return $this->userShowProperties($args["<username>"]);
case "set":
return (int) !Arsse::$user->propertiesSet($args["<username>"], [$args["<property>"] => $args["<value>"]]);
case "unset":
return (int) !Arsse::$user->propertiesSet($args["<username>"], [$args["<property>"] => null]);
case "auth": case "auth":
return $this->userAuthenticate($args["<username>"], $args["<password>"], $args["--fever"]); return $this->userAuthenticate($args["<username>"], $args["<password>"], $args["--fever"]);
case "list": case "list":
@ -275,4 +365,16 @@ USAGE_TEXT;
return 1; return 1;
} }
} }
protected function userShowProperties(string $user): int {
$data = Arsse::$user->propertiesGet($user);
$len = array_reduce(array_keys($data), function($carry, $item) {
return max($carry, strlen($item));
}, 0) + 2;
foreach ($data as $k => $v) {
echo str_pad($k, $len, " ");
echo var_export($v, true).\PHP_EOL;
}
return 0;
}
} }

View file

@ -18,14 +18,14 @@ class User {
'admin' => V::T_BOOL, 'admin' => V::T_BOOL,
'lang' => V::T_STRING, 'lang' => V::T_STRING,
'tz' => V::T_STRING, 'tz' => V::T_STRING,
'root_folder_name' => V::T_STRING,
'sort_asc' => V::T_BOOL, 'sort_asc' => V::T_BOOL,
'theme' => V::T_STRING, 'theme' => V::T_STRING,
'page_size' => V::T_INT, // greater than zero 'page_size' => V::T_INT, // greater than zero
'shortcuts' => V::T_BOOL, 'shortcuts' => V::T_BOOL,
'gestures' => V::T_BOOL, 'gestures' => V::T_BOOL,
'stylesheet' => V::T_STRING,
'reading_time' => V::T_BOOL, 'reading_time' => V::T_BOOL,
'root_folder_name' => V::T_STRING, 'stylesheet' => V::T_STRING,
]; ];
public const PROPERTIES_LARGE = ["stylesheet"]; public const PROPERTIES_LARGE = ["stylesheet"];

View file

@ -156,6 +156,15 @@ class TestCLI extends \JKingWeb\Arsse\Test\AbstractTest {
]; ];
} }
public function testAddAUserAsAdministrator(): void {
Arsse::$user = $this->createMock(User::class);
Arsse::$user->method("add")->willReturn("random password");
Arsse::$user->method("propertiesSet")->willReturn([]);
Arsse::$user->expects($this->exactly(1))->method("add")->with("jane.doe@example.com", null);
Arsse::$user->expects($this->exactly(1))->method("propertiesSet")->with("jane.doe@example.com", ['admin' => true]);
$this->assertConsole($this->cli, "arsse.php user add jane.doe@example.com --admin", 0, "random password");
}
/** @dataProvider provideUserAuthentication */ /** @dataProvider provideUserAuthentication */
public function testAuthenticateAUser(string $cmd, int $exitStatus, string $output): void { public function testAuthenticateAUser(string $cmd, int $exitStatus, string $output): void {
// FIXME: Phake is somehow unable to mock the User class correctly, so we use PHPUnit's mocks instead // FIXME: Phake is somehow unable to mock the User class correctly, so we use PHPUnit's mocks instead
@ -357,4 +366,56 @@ class TestCLI extends \JKingWeb\Arsse\Test\AbstractTest {
["arsse.php import jane.doe@example.com bad.opml --replace --flat", 10603, "bad.opml", "jane.doe@example.com", true, true], ["arsse.php import jane.doe@example.com bad.opml --replace --flat", 10603, "bad.opml", "jane.doe@example.com", true, true],
]; ];
} }
public function testShowMetadataOfAUser(): void {
$data = [
'num' => 42,
'admin' => false,
'lang' => "en-ca",
'tz' => "America/Toronto",
'root_folder_name' => null,
'sort_asc' => true,
'theme' => null,
'page_size' => 50,
'shortcuts' => true,
'gestures' => null,
'reading_time' => false,
'stylesheet' => "body {color:gray}",
];
$exp = implode(\PHP_EOL, [
"num 42",
"admin false",
"lang 'en-ca'",
"tz 'America/Toronto'",
"root_folder_name NULL",
"sort_asc true",
"theme NULL",
"page_size 50",
"shortcuts true",
"gestures NULL",
"reading_time false",
"stylesheet 'body {color:gray}'",
]);
Arsse::$user = $this->createMock(User::class);
Arsse::$user->method("propertiesGet")->willReturn($data);
Arsse::$user->expects($this->once())->method("propertiesGet")->with("john.doe@example.com", true);
$this->assertConsole($this->cli, "arsse.php user show john.doe@example.com", 0, $exp);
}
/** @dataProvider provideMetadataChanges */
public function testSetMetadataOfAUser(string $cmd, string $user, array $in, array $out, int $exp): void {
Arsse::$user = $this->createMock(User::class);
Arsse::$user->method("propertiesSet")->willReturn($out);
Arsse::$user->expects($this->once())->method("propertiesSet")->with($user, $in);
$this->assertConsole($this->cli, $cmd, $exp, "");
}
public function provideMetadataChanges(): iterable {
return [
["arsse.php user set john admin true", "john", ['admin' => "true"], ['admin' => "true"], 0],
["arsse.php user set john bogus 1", "john", ['bogus' => "1"], [], 1],
["arsse.php user unset john admin", "john", ['admin' => null], ['admin' => null], 0],
["arsse.php user unset john bogus", "john", ['bogus' => null], [], 1],
];
}
} }

View file

@ -88,6 +88,7 @@ abstract class AbstractTest extends \PHPUnit\Framework\TestCase {
}, $params, array_keys($params))); }, $params, array_keys($params)));
} }
$url = URL::queryAppend($url, (string) $params); $url = URL::queryAppend($url, (string) $params);
$params = null;
} }
$q = parse_url($url, \PHP_URL_QUERY); $q = parse_url($url, \PHP_URL_QUERY);
if (strlen($q ?? "")) { if (strlen($q ?? "")) {