@ -16,6 +17,7 @@ use JKingWeb\Arsse\Misc\ValueInfo;
use JKingWeb\Arsse\AbstractException;
use JKingWeb\Arsse\ExceptionType;
use JKingWeb\Arsse\Db\ExceptionInput;
use JKingWeb\Arsse\Db\ResultEmpty;
use JKingWeb\Arsse\Feed\Exception as FeedException;
use JKingWeb\Arsse\REST\Response;
@ -32,10 +34,13 @@ Protocol difference so far:
- The "Published" virtual feed is non-functional (this will not be implemented in the near term)
- setArticleLabel responds with errors for invalid labels where TT-RSS simply returns a zero result
- The result of setArticleLabel counts only records which actually changed rather than all entries attempted
- Using both limit/skip and unread_only in getFeeds produces reliable results, unlike in TT-RSS
- Top-level categories in getFeedTree have a 'parent_id' property (set to null); in TT-RSS the property is absent
- Article hashes are SHA-256 rather than SHA-1.
- Articles have at most one attachment (enclosure), whereas TTRSS allows for several; there is also significantly less detail. These are limitations of picoFeed which should be addressed
- IDs for enclosures are ommitted as we don't give them IDs
- Searching in getHeadlines is not yet implemented
- Category -3 (all non-special feeds) is handled correctly in getHeadlines; TT-RSS returns results for feed -3 (Fresh)
*/
@ -59,35 +64,34 @@ class API extends \JKingWeb\Arsse\REST\AbstractHandler {
'op' => ValueInfo::T_STRING, // the function ("operation") to perform
'sid' => ValueInfo::T_STRING, // session ID
'seq' => ValueInfo::T_INT, // request number from client
'user' => ValueInfo::T_STRING | ValueInfo::M_STRICT, // user name for `login`
'password' => ValueInfo::T_STRING | ValueInfo::M_STRICT, // password for `login` and `subscribeToFeed`
'include_empty' => ValueInfo::T_BOOL | ValueInfo::M_DROP, // whether to include empty items in `getFeedTree` and `getCategories`
'unread_only' => ValueInfo::T_BOOL | ValueInfo::M_DROP, // whether to exclude items without unread articles in `getCategories` and `getFeeds`
'enable_nested' => ValueInfo::T_BOOL | ValueInfo::M_DROP, // whether to NOT show subcategories in `getCategories
'include_nested' => ValueInfo::T_BOOL | ValueInfo::M_DROP, // whether to include subcategories in `getFeeds` and the articles thereof in `getHeadlines`
'caption' => ValueInfo::T_STRING | ValueInfo::M_STRICT, // name for categories, feed, and labels
'parent_id' => ValueInfo::T_INT, // parent category for `addCategory` and `moveCategory`
'category_id' => ValueInfo::T_INT, // parent category for `subscribeToFeed` and `moveFeed`, and subject for category-modification functions
'cat_id' => ValueInfo::T_INT, // parent category for `getFeeds`
'label_id' => ValueInfo::T_INT, // label ID in label-related functions
'feed_url' => ValueInfo::T_STRING | ValueInfo::M_STRICT, // URL of feed in `subscribeToFeed`
'login' => ValueInfo::T_STRING | ValueInfo::M_STRICT, // remote user name in `subscribeToFeed`
'feed_id' => ValueInfo::T_INT, // feed, label, or category ID for various functions
'is_cat' => ValueInfo::T_BOOL | ValueInfo::M_DROP, // whether 'feed_id' refers to a category
'article_id' => ValueInfo::T_MIXED, // single article ID in `getLabels`; one or more (comma-separated) article IDs in `getArticle`
'article_ids' => ValueInfo::T_STRING, // one or more (comma-separated) article IDs in `updateArticle` and `setArticleLabel`
'assign' => ValueInfo::T_BOOL | ValueInfo::M_DROP, // whether to assign or clear (false) a label in `setArticleLabel`
'limit' => ValueInfo::T_INT, // maximum number of records returned in `getFeeds`, `getHeadlines`, and `getCompactHeadlines`
'offset' => ValueInfo::T_INT, // number of records to skip in `getFeeds`, for pagination
'skip' => ValueInfo::T_INT, // number of records to skip in `getHeadlines` and `getCompactHeadlines`, for pagination
'show_excerpt' => ValueInfo::T_BOOL | ValueInfo::M_DROP, // whether to include article excerpts in `getHeadlines`
'show_content' => ValueInfo::T_BOOL | ValueInfo::M_DROP, // whether to include article content in `getHeadlines`
'include_attachments' => ValueInfo::T_BOOL | ValueInfo::M_DROP, // whether to include article enclosures in `getHeadlines`
$c->markedSince(Date::sub("PT24H"))->unread(false); // FIXME: this selects any recently touched article which is read, not necessarily a recently read one
break;
default:
// any actual feed
$c->subscription($id);
break;
}
}
}
// next handle the view mode
switch ($viewMode) {
case "all_articles":
// no context needed here
break;
case "adaptive":
// adaptive means "return only unread unless there are none, in which case return all articles"
if ($c->unread !== false && Arsse::$db->articleCount(Arsse::$user->id, (clone $c)->unread(true))) {
$c->unread(true);
}
break;
case "unread":
if ($c->unread !== false) {
$c->unread(true);
} else {
// unread mode in the "Recently Read" feed is a no-op
return new ResultEmpty;
}
break;
case "marked":
$c->starred(true);
break;
case "has_note":
$c->annotated(true);
break;
case "published":
// not implemented
// TODO: if the Published feed is implemented, the headline function needs to be modified accordingly
return new ResultEmpty;
default:
throw new \JKingWeb\Arsse\Exception("constantUnknown", $viewMode); // @codeCoverageIgnore
Phake::when(Arsse::$db)->articleCount($this->anything(), (new Context)->unread(true))->thenReturn(1);
Phake::when(Arsse::$db)->articleList($this->anything(), (new Context)->subscription(2112), Database::LIST_MINIMAL)->thenThrow(new ExceptionInput("subjectMissing"));
Phake::when(Arsse::$db)->articleList($this->anything(), new Context, Database::LIST_MINIMAL)->thenReturn(new Result($this->articles));
Phake::when(Arsse::$db)->articleList($this->anything(), (new Context)->starred(true), Database::LIST_MINIMAL)->thenReturn(new Result([['id' => 1]]));
Phake::when(Arsse::$db)->articleList($this->anything(), (new Context)->label(1088), Database::LIST_MINIMAL)->thenReturn(new Result([['id' => 2]]));
Phake::when(Arsse::$db)->articleList($this->anything(), (new Context)->unread(true), Database::LIST_MINIMAL)->thenReturn(new Result([['id' => 3]]));
Phake::when(Arsse::$db)->articleList($this->anything(), (new Context)->label(1088)->unread(true), Database::LIST_MINIMAL)->thenReturn(new Result([['id' => 4]]));
Phake::when(Arsse::$db)->articleList($this->anything(), (new Context)->subscription(42)->starred(true), Database::LIST_MINIMAL)->thenReturn(new Result([['id' => 5]]));
Phake::when(Arsse::$db)->articleList($this->anything(), (new Context)->subscription(42)->annotated(true), Database::LIST_MINIMAL)->thenReturn(new Result([['id' => 6]]));
Phake::when(Arsse::$db)->articleList($this->anything(), (new Context)->limit(5), Database::LIST_MINIMAL)->thenReturn(new Result([['id' => 7]]));
Phake::when(Arsse::$db)->articleList($this->anything(), (new Context)->offset(2), Database::LIST_MINIMAL)->thenReturn(new Result([['id' => 8]]));
Phake::when(Arsse::$db)->articleList($this->anything(), (new Context)->limit(5)->offset(2), Database::LIST_MINIMAL)->thenReturn(new Result([['id' => 9]]));
Phake::when(Arsse::$db)->articleList($this->anything(), (new Context)->oldestArticle(48), Database::LIST_MINIMAL)->thenReturn(new Result([['id' => 10]]));
$out1 = [
$this->respErr("INCORRECT_USAGE"),
$this->respGood([]),
$this->respGood([]),
$this->respGood([]),
$this->respGood([]),
$this->respGood([]),
$this->respGood([]),
$this->respGood([['id' => 101],['id' => 102]]),
$this->respGood([['id' => 1]]),
$this->respGood([['id' => 2]]),
$this->respGood([['id' => 3]]),
$this->respGood([['id' => 2]]), // the result is 2 rather than 4 because there are no unread, so the unread context is not used