mirror of
https://code.mensbeam.com/MensBeam/Arsse.git
synced 2024-12-22 21:22:40 +00:00
Implement TT-RSS API level 15
This commit is contained in:
parent
f2e5d567ec
commit
211cea648e
2 changed files with 53 additions and 50 deletions
|
@ -24,7 +24,7 @@ use Laminas\Diactoros\Response\JsonResponse as Response;
|
||||||
use Laminas\Diactoros\Response\EmptyResponse;
|
use Laminas\Diactoros\Response\EmptyResponse;
|
||||||
|
|
||||||
class API extends \JKingWeb\Arsse\REST\AbstractHandler {
|
class API extends \JKingWeb\Arsse\REST\AbstractHandler {
|
||||||
public const LEVEL = 14; // emulated API level
|
public const LEVEL = 15; // emulated API level
|
||||||
public const VERSION = "17.4"; // emulated TT-RSS version
|
public const VERSION = "17.4"; // emulated TT-RSS version
|
||||||
|
|
||||||
protected const LABEL_OFFSET = 1024; // offset below zero at which labels begin, counting down
|
protected const LABEL_OFFSET = 1024; // offset below zero at which labels begin, counting down
|
||||||
|
@ -79,7 +79,7 @@ class API extends \JKingWeb\Arsse\REST\AbstractHandler {
|
||||||
'include_header' => ValueInfo::T_BOOL | ValueInfo::M_DROP, // whether to attach a header to the results of `getHeadlines`
|
'include_header' => ValueInfo::T_BOOL | ValueInfo::M_DROP, // whether to attach a header to the results of `getHeadlines`
|
||||||
'search' => ValueInfo::T_STRING, // search string for `getHeadlines`
|
'search' => ValueInfo::T_STRING, // search string for `getHeadlines`
|
||||||
'field' => ValueInfo::T_INT, // which state to change in `updateArticle`
|
'field' => ValueInfo::T_INT, // which state to change in `updateArticle`
|
||||||
'mode' => ValueInfo::T_INT, // whether to set, clear, or toggle the selected state in `updateArticle`
|
'mode' => ValueInfo::T_MIXED, // whether to set, clear, or toggle the selected state in `updateArticle` (integer), or whether to ignore a certain recent timeframe in `catchupFeed` (string)
|
||||||
'data' => ValueInfo::T_STRING, // note text in `updateArticle` if setting a note
|
'data' => ValueInfo::T_STRING, // note text in `updateArticle` if setting a note
|
||||||
];
|
];
|
||||||
protected const VIEW_MODES = ["all_articles", "adaptive", "unread", "marked", "has_note", "published"];
|
protected const VIEW_MODES = ["all_articles", "adaptive", "unread", "marked", "has_note", "published"];
|
||||||
|
@ -1037,6 +1037,7 @@ class API extends \JKingWeb\Arsse\REST\AbstractHandler {
|
||||||
public function opCatchUpFeed(array $data): array {
|
public function opCatchUpFeed(array $data): array {
|
||||||
$id = $data['feed_id'] ?? self::FEED_ARCHIVED;
|
$id = $data['feed_id'] ?? self::FEED_ARCHIVED;
|
||||||
$cat = $data['is_cat'] ?? false;
|
$cat = $data['is_cat'] ?? false;
|
||||||
|
$mode = $data['mode'] ?? "all";
|
||||||
$out = ['status' => "OK"];
|
$out = ['status' => "OK"];
|
||||||
// first prepare the context; unsupported contexts simply return early
|
// first prepare the context; unsupported contexts simply return early
|
||||||
$c = (new Context)->hidden(false);
|
$c = (new Context)->hidden(false);
|
||||||
|
@ -1089,6 +1090,16 @@ class API extends \JKingWeb\Arsse\REST\AbstractHandler {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
switch ($mode) {
|
||||||
|
case "2week":
|
||||||
|
$c->notModifiedSince(Date::sub("P2W", $this->now()));
|
||||||
|
break;
|
||||||
|
case "1week":
|
||||||
|
$c->notModifiedSince(Date::sub("P1W", $this->now()));
|
||||||
|
break;
|
||||||
|
case "1day":
|
||||||
|
$c->notModifiedSince(Date::sub("PT24H", $this->now()));
|
||||||
|
}
|
||||||
// perform the marking
|
// perform the marking
|
||||||
try {
|
try {
|
||||||
Arsse::$db->articleMark(Arsse::$user->id, ['read' => true], $c);
|
Arsse::$db->articleMark(Arsse::$user->id, ['read' => true], $c);
|
||||||
|
@ -1102,6 +1113,7 @@ class API extends \JKingWeb\Arsse\REST\AbstractHandler {
|
||||||
public function opUpdateArticle(array $data): array {
|
public function opUpdateArticle(array $data): array {
|
||||||
// normalize input
|
// normalize input
|
||||||
$articles = array_filter(ValueInfo::normalize(explode(",", (string) $data['article_ids']), ValueInfo::T_INT | ValueInfo::M_ARRAY), [ValueInfo::class, "id"]);
|
$articles = array_filter(ValueInfo::normalize(explode(",", (string) $data['article_ids']), ValueInfo::T_INT | ValueInfo::M_ARRAY), [ValueInfo::class, "id"]);
|
||||||
|
$data['mode'] = ValueInfo::normalize($data['mode'], ValueInfo::T_INT);
|
||||||
if (!$articles) {
|
if (!$articles) {
|
||||||
// if there are no valid articles this is an error
|
// if there are no valid articles this is an error
|
||||||
throw new Exception("INCORRECT_USAGE");
|
throw new Exception("INCORRECT_USAGE");
|
||||||
|
|
|
@ -19,8 +19,8 @@ use JKingWeb\Arsse\REST\TinyTinyRSS\API;
|
||||||
use Psr\Http\Message\ResponseInterface;
|
use Psr\Http\Message\ResponseInterface;
|
||||||
use Laminas\Diactoros\Response\JsonResponse as Response;
|
use Laminas\Diactoros\Response\JsonResponse as Response;
|
||||||
use Laminas\Diactoros\Response\EmptyResponse;
|
use Laminas\Diactoros\Response\EmptyResponse;
|
||||||
|
|
||||||
/** @covers \JKingWeb\Arsse\REST\TinyTinyRSS\API<extended>
|
/** @covers \JKingWeb\Arsse\REST\TinyTinyRSS\API<extended>
|
||||||
|
|
||||||
* @covers \JKingWeb\Arsse\REST\TinyTinyRSS\Exception */
|
* @covers \JKingWeb\Arsse\REST\TinyTinyRSS\Exception */
|
||||||
class TestAPI extends \JKingWeb\Arsse\Test\AbstractTest {
|
class TestAPI extends \JKingWeb\Arsse\Test\AbstractTest {
|
||||||
protected const NOW = "2020-12-21T23:09:17.189065Z";
|
protected const NOW = "2020-12-21T23:09:17.189065Z";
|
||||||
|
@ -1309,55 +1309,46 @@ LONG_STRING;
|
||||||
$this->assertMessage($this->respGood($exp), $this->req($in[1]));
|
$this->assertMessage($this->respGood($exp), $this->req($in[1]));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testMarkFeedsAsRead(): void {
|
/** @dataProvider provideMassMarkings */
|
||||||
$in1 = [
|
public function testMarkFeedsAsRead(array $in, ?Context $c): void {
|
||||||
// no-ops
|
$base = ['op' => "catchupFeed", 'sid' => "PriestsOfSyrinx"];
|
||||||
['op' => "catchupFeed", 'sid' => "PriestsOfSyrinx"],
|
$in = array_merge($base, $in);
|
||||||
['op' => "catchupFeed", 'sid' => "PriestsOfSyrinx", 'feed_id' => 0],
|
|
||||||
['op' => "catchupFeed", 'sid' => "PriestsOfSyrinx", 'feed_id' => -2],
|
|
||||||
['op' => "catchupFeed", 'sid' => "PriestsOfSyrinx", 'feed_id' => -6],
|
|
||||||
['op' => "catchupFeed", 'sid' => "PriestsOfSyrinx", 'feed_id' => -1, 'is_cat' => true],
|
|
||||||
['op' => "catchupFeed", 'sid' => "PriestsOfSyrinx", 'feed_id' => -3, 'is_cat' => true],
|
|
||||||
['op' => "catchupFeed", 'sid' => "PriestsOfSyrinx", 'feed_id' => -4, 'is_cat' => true],
|
|
||||||
];
|
|
||||||
$in2 = [
|
|
||||||
// simple contexts
|
|
||||||
['op' => "catchupFeed", 'sid' => "PriestsOfSyrinx", 'feed_id' => -1],
|
|
||||||
['op' => "catchupFeed", 'sid' => "PriestsOfSyrinx", 'feed_id' => -4],
|
|
||||||
['op' => "catchupFeed", 'sid' => "PriestsOfSyrinx", 'feed_id' => -2112],
|
|
||||||
['op' => "catchupFeed", 'sid' => "PriestsOfSyrinx", 'feed_id' => 2112],
|
|
||||||
['op' => "catchupFeed", 'sid' => "PriestsOfSyrinx", 'feed_id' => 42, 'is_cat' => true],
|
|
||||||
['op' => "catchupFeed", 'sid' => "PriestsOfSyrinx", 'feed_id' => 0, 'is_cat' => true],
|
|
||||||
['op' => "catchupFeed", 'sid' => "PriestsOfSyrinx", 'feed_id' => -2, 'is_cat' => true],
|
|
||||||
];
|
|
||||||
$in3 = [
|
|
||||||
// this one has a tricky time-based context
|
|
||||||
['op' => "catchupFeed", 'sid' => "PriestsOfSyrinx", 'feed_id' => -3],
|
|
||||||
];
|
|
||||||
\Phake::when(Arsse::$db)->articleMark->thenThrow(new ExceptionInput("typeViolation"));
|
\Phake::when(Arsse::$db)->articleMark->thenThrow(new ExceptionInput("typeViolation"));
|
||||||
$exp = $this->respGood(['status' => "OK"]);
|
// create a mock-current time
|
||||||
// verify the above are in fact no-ops
|
\Phake::when(Arsse::$obj)->get(\DateTimeImmutable::class)->thenReturn(new \DateTimeImmutable(self::NOW));
|
||||||
for ($a = 0; $a < sizeof($in1); $a++) {
|
// TT-RSS always responds the same regardless of success or failure
|
||||||
$this->assertMessage($exp, $this->req($in1[$a]), "Test $a failed");
|
$this->assertMessage($this->respGood(['status' => "OK"]), $this->req($in));
|
||||||
}
|
if (isset($c)) {
|
||||||
|
\Phake::verify(Arsse::$db)->articleMark(Arsse::$user->id, ['read' => true], $c);
|
||||||
|
} else {
|
||||||
\Phake::verify(Arsse::$db, \Phake::times(0))->articleMark;
|
\Phake::verify(Arsse::$db, \Phake::times(0))->articleMark;
|
||||||
// verify the simple contexts
|
|
||||||
for ($a = 0; $a < sizeof($in2); $a++) {
|
|
||||||
$this->assertMessage($exp, $this->req($in2[$a]), "Test $a failed");
|
|
||||||
}
|
}
|
||||||
\Phake::verify(Arsse::$db)->articleMark($this->anything(), ['read' => true], (new Context)->hidden(false));
|
|
||||||
\Phake::verify(Arsse::$db)->articleMark($this->anything(), ['read' => true], (new Context)->starred(true)->hidden(false));
|
|
||||||
\Phake::verify(Arsse::$db)->articleMark($this->anything(), ['read' => true], (new Context)->label(1088)->hidden(false));
|
|
||||||
\Phake::verify(Arsse::$db)->articleMark($this->anything(), ['read' => true], (new Context)->subscription(2112)->hidden(false));
|
|
||||||
\Phake::verify(Arsse::$db)->articleMark($this->anything(), ['read' => true], (new Context)->folder(42)->hidden(false));
|
|
||||||
\Phake::verify(Arsse::$db)->articleMark($this->anything(), ['read' => true], (new Context)->folderShallow(0)->hidden(false));
|
|
||||||
\Phake::verify(Arsse::$db)->articleMark($this->anything(), ['read' => true], (new Context)->labelled(true)->hidden(false));
|
|
||||||
// verify the time-based mock
|
|
||||||
$t = Date::sub("PT24H");
|
|
||||||
for ($a = 0; $a < sizeof($in3); $a++) {
|
|
||||||
$this->assertMessage($exp, $this->req($in3[$a]), "Test $a failed");
|
|
||||||
}
|
}
|
||||||
\Phake::verify(Arsse::$db)->articleMark($this->anything(), ['read' => true], $this->equalTo((new Context)->hidden(false)->modifiedSince($t), 2)); // within two seconds
|
|
||||||
|
public function provideMassMarkings(): iterable {
|
||||||
|
$c = (new Context)->hidden(false);
|
||||||
|
return [
|
||||||
|
[[], null],
|
||||||
|
[['feed_id' => 0], null],
|
||||||
|
[['feed_id' => 0, 'is_cat' => true], (clone $c)->folderShallow(0)],
|
||||||
|
[['feed_id' => 0, 'is_cat' => true, 'mode' => "bogus"], (clone $c)->folderShallow(0)],
|
||||||
|
[['feed_id' => -1], (clone $c)->starred(true)],
|
||||||
|
[['feed_id' => -1, 'is_cat' => true], null],
|
||||||
|
[['feed_id' => -3], (clone $c)->modifiedSince(Date::sub("PT24H", self::NOW))],
|
||||||
|
[['feed_id' => -3, 'mode' => "1day"], (clone $c)->modifiedSince(Date::sub("PT24H", self::NOW))->notModifiedSince(Date::sub("PT24H", self::NOW))], // this is a nonsense query, but it's what TT-RSS appearsto do
|
||||||
|
[['feed_id' => -3, 'is_cat' => true], null],
|
||||||
|
[['feed_id' => -2], null],
|
||||||
|
[['feed_id' => -2, 'is_cat' => true], (clone $c)->labelled(true)],
|
||||||
|
[['feed_id' => -2, 'is_cat' => true, 'mode' => "all"], (clone $c)->labelled(true)],
|
||||||
|
[['feed_id' => -4], $c],
|
||||||
|
[['feed_id' => -4, 'is_cat' => true], null],
|
||||||
|
[['feed_id' => -6], null],
|
||||||
|
[['feed_id' => -2112], (clone $c)->label(1088)],
|
||||||
|
[['feed_id' => 42, 'is_cat' => true], (clone $c)->folder(42)],
|
||||||
|
[['feed_id' => 42, 'is_cat' => true, 'mode' => "1week"], (clone $c)->folder(42)->notModifiedSince(Date::sub("P1W", self::NOW))],
|
||||||
|
[['feed_id' => 2112], (clone $c)->subscription(2112)],
|
||||||
|
[['feed_id' => 2112, 'mode' => "2week"], (clone $c)->subscription(2112)->notModifiedSince(Date::sub("P2W", self::NOW))],
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testRetrieveFeedList(): void {
|
public function testRetrieveFeedList(): void {
|
||||||
|
|
Loading…
Reference in a new issue