Browse Source

Implement TTRSS opera getArticle; fixes #84

microsub
J. King 7 years ago
parent
commit
de92fb514b
  1. 5
      lib/Database.php
  2. 67
      lib/REST/TinyTinyRSS/API.php
  3. 132
      tests/REST/TinyTinyRSS/TestTinyTinyAPI.php
  4. 72
      tests/lib/Database/SeriesArticle.php

5
lib/Database.php

@ -962,8 +962,10 @@ class Database {
return new Db\ResultAggregate(...$out);
} else {
$columns = [
// (id, subscription, feed, modified, unread, starred, edition): always included
"arsse_articles.url as url",
"title",
"arsse_articles.title as title",
"(select coalesce(arsse_subscriptions.title,arsse_feeds.title) from arsse_feeds join arsse_subscriptions on arsse_subscriptions.feed is arsse_feeds.id where arsse_feeds.id is arsse_articles.feed) as subscription_title",
"author",
"content",
"guid",
@ -972,6 +974,7 @@ class Database {
"url_title_hash||':'||url_content_hash||':'||title_content_hash as fingerprint",
"arsse_enclosures.url as media_url",
"arsse_enclosures.type as media_type",
"(select note from arsse_marks where article is arsse_articles.id and subscription in (select sub from subscribed_feeds)) as note"
];
$q = $this->articleQuery($user, $context, $columns);
$q->setJoin("left join arsse_enclosures on arsse_enclosures.article is arsse_articles.id");

67
lib/REST/TinyTinyRSS/API.php

@ -29,6 +29,9 @@ Protocol difference so far:
- 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
- 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
*/
@ -1169,4 +1172,68 @@ class API extends \JKingWeb\Arsse\REST\AbstractHandler {
$tr->commit();
return ['status' => "OK", 'updated' => $out];
}
public function opGetArticle(array $data): array {
// normalize input
$articles = array_filter(ValueInfo::normalize(explode(",", (string) $data['article_id']), ValueInfo::T_INT | ValueInfo::M_ARRAY), [ValueInfo::class, "id"]);
if (!$articles) {
// if there are no valid articles this is an error
throw new Exception("INCORRECT_USAGE");
}
$tr = Arsse::$db->begin();
// retrieve the list of label names for the user
$labels = [];
foreach (Arsse::$db->labelList(Arsse::$user->id, false) as $label) {
$labels[$label['id']] = $label['name'];
}
// retrieve the requested articles
$out = [];
foreach (Arsse::$db->articleList(Arsse::$user->id, (new Context)->articles($articles)) as $article) {
$out[] = [
'id' => $article['id'],
'guid' => $article['guid'] ? "SHA256:".$article['guid'] : null,
'title' => $article['title'],
'link' => $article['url'],
'labels' => $this->articleLabelList($labels, $article['id']),
'unread' => (bool) $article['unread'],
'marked' => (bool) $article['starred'],
'published' => false, // TODO: if the Published feed is implemented, the getArticle operation should be amended accordingly
'comments' => "", // FIXME: What is this?
'author' => $article['author'],
'updated' => Date::transform($article['edited_date'], "unix", "sql"),
'feed_id' => $article['subscription'],
'feed_title' => $article['subscription_title'],
'attachments' => $article['media_url'] ? [[
'content_url' => $article['media_url'],
'content_type' => $article['media_type'],
'title' => "",
'duration' => "",
'width' => "",
'height' => "",
'post_id' => $article['id'],
]] : [], // TODO: We need to support multiple enclosures
'score' => 0, // score is not implemented as it is not modifiable from the TTRSS API
'note' => strlen($article['note']) ? $article['note'] : null,
'lang' => "", // FIXME: picoFeed should be able to retrieve this information
'content' => $article['content'],
];
}
return $out;
}
protected function articleLabelList(array $labels, int $id): array {
$out = [];
if (!$labels) {
return $out;
}
foreach (Arsse::$db->articleLabelsGet(Arsse::$user->id, $id) as $label) {
$out[] = [
$this->labelOut($label), // ID
$labels[$label], // name
"", // foreground colour
"", // background colour
];
}
return $out;
}
}

132
tests/REST/TinyTinyRSS/TestTinyTinyAPI.php

@ -47,6 +47,48 @@ class TestTinyTinyAPI extends Test\AbstractTest {
['id' => 1, 'articles' => 2, 'read' => 2, 'unread' => 0, 'name' => "Logical"],
];
protected $starred = ['total' => 10, 'unread' => 4, 'read' => 6];
protected $articles = [
[
'id' => 101,
'url' => 'http://example.com/1',
'title' => 'Article title 1',
'subscription_title' => "Feed 11",
'author' => '',
'content' => '<p>Article content 1</p>',
'guid' => '',
'published_date' => '2000-01-01 00:00:00',
'edited_date' => '2000-01-01 00:00:01',
'modified_date' => '2000-01-01 01:00:00',
'unread' => 1,
'starred' => 0,
'edition' => 101,
'subscription' => 8,
'fingerprint' => 'f5cb8bfc1c7396dc9816af212a3e2ac5221585c2a00bf7ccb6aabd95dcfcd6a6:fb0bc8f8cb08913dc5a497db700e327f1d34e4987402687d494a5891f24714d4:18fdd4fa93d693128c43b004399e5c9cea6c261ddfa002518d3669f55d8c2207',
'media_url' => null,
'media_type' => null,
'note' => "",
],
[
'id' => 102,
'url' => 'http://example.com/2',
'title' => 'Article title 2',
'subscription_title' => "Feed 11",
'author' => 'J. King',
'content' => '<p>Article content 2</p>',
'guid' => '5be8a5a46ecd52ed132191c8d27fb1af6b3d4edc00234c5d9f8f0e10562ed3b7',
'published_date' => '2000-01-02 00:00:00',
'edited_date' => '2000-01-02 00:00:02',
'modified_date' => '2000-01-02 02:00:00',
'unread' => 0,
'starred' => 0,
'edition' => 202,
'subscription' => 8,
'fingerprint' => '0e86d2de822a174fe3c44a466953e63ca1f1a58a19cbf475fce0855d4e3d5153:13075894189c47ffcfafd1dfe7fbb539f7c74a69d35a399b3abf8518952714f9:2abd0a8cba83b8214a66c8f0293ba63e467d720540e29ff8ddcdab069d4f1c9e',
'media_url' => "http://example.com/text",
'media_type' => "text/plain",
'note' => "Note 2",
],
];
protected function respGood($content = null, $seq = 0): Response {
return new Response(200, [
@ -1133,7 +1175,7 @@ class TestTinyTinyAPI extends Test\AbstractTest {
['op' => "updateArticle", 'sid' => "PriestsOfSyrinx", 'article_ids' => "42, 2112, -1", 'field' => 3, 'data' => "eh"],
['op' => "updateArticle", 'sid' => "PriestsOfSyrinx", 'article_ids' => "42, 2112, -1", 'field' => 4], // invalid field
['op' => "updateArticle", 'sid' => "PriestsOfSyrinx", 'article_ids' => "0, -1", 'field' => 4], // no valid IDs
['op' => "updateArticle", 'sid' => "PriestsOfSyrinx", 'article_ids' => "0, -1", 'field' => 3], // no valid IDs
];
Phake::when(Arsse::$db)->articleMark->thenReturn(1);
Phake::when(Arsse::$db)->articleMark($this->anything(), ['starred' => false], (new Context)->articles([42, 2112]))->thenReturn(2);
@ -1182,4 +1224,92 @@ class TestTinyTinyAPI extends Test\AbstractTest {
$this->assertEquals($out[$a], $this->h->dispatch(new Request("POST", "", json_encode($in[$a]))), "Test $a failed");
}
}
public function testListArticles() {
$in = [
// error conditions
['op' => "getArticle", 'sid' => "PriestsOfSyrinx"],
['op' => "getArticle", 'sid' => "PriestsOfSyrinx", 'article_id' => 0],
['op' => "getArticle", 'sid' => "PriestsOfSyrinx", 'article_id' => -1],
['op' => "getArticle", 'sid' => "PriestsOfSyrinx", 'article_id' => "0,-1"],
// acceptable input
['op' => "getArticle", 'sid' => "PriestsOfSyrinx", 'article_id' => "101,102"],
['op' => "getArticle", 'sid' => "PriestsOfSyrinx", 'article_id' => "101"],
['op' => "getArticle", 'sid' => "PriestsOfSyrinx", 'article_id' => "102"],
];
Phake::when(Arsse::$db)->labelList($this->anything())->thenReturn(new Result($this->labels));
Phake::when(Arsse::$db)->labelList($this->anything(), false)->thenReturn(new Result($this->usedLabels));
Phake::when(Arsse::$db)->articleLabelsGet($this->anything(), 101)->thenReturn([]);
Phake::when(Arsse::$db)->articleLabelsGet($this->anything(), 102)->thenReturn([1,3]);
Phake::when(Arsse::$db)->articleList($this->anything(), (new Context)->articles([101, 102]))->thenReturn(new Result($this->articles));
Phake::when(Arsse::$db)->articleList($this->anything(), (new Context)->articles([101]))->thenReturn(new Result([$this->articles[0]]));
Phake::when(Arsse::$db)->articleList($this->anything(), (new Context)->articles([102]))->thenReturn(new Result([$this->articles[1]]));
$exp = $this->respErr("INCORRECT_USAGE");
$this->assertEquals($exp, $this->h->dispatch(new Request("POST", "", json_encode($in[0]))));
$this->assertEquals($exp, $this->h->dispatch(new Request("POST", "", json_encode($in[1]))));
$this->assertEquals($exp, $this->h->dispatch(new Request("POST", "", json_encode($in[2]))));
$this->assertEquals($exp, $this->h->dispatch(new Request("POST", "", json_encode($in[3]))));
$exp = [
[
'id' => 101,
'guid' => null,
'title' => 'Article title 1',
'link' => 'http://example.com/1',
'labels' => [],
'unread' => true,
'marked' => false,
'published' => false,
'comments' => "",
'author' => '',
'updated' => strtotime('2000-01-01 00:00:01'),
'feed_id' => 8,
'feed_title' => "Feed 11",
'attachments' => [],
'score' => 0,
'note' => null,
'lang' => "",
'content' => '<p>Article content 1</p>',
],
[
'id' => 102,
'guid' => "SHA256:5be8a5a46ecd52ed132191c8d27fb1af6b3d4edc00234c5d9f8f0e10562ed3b7",
'title' => 'Article title 2',
'link' => 'http://example.com/2',
'labels' => [
[-1025, "Logical", "", ""],
[-1027, "Fascinating", "", ""],
],
'unread' => false,
'marked' => false,
'published' => false,
'comments' => "",
'author' => "J. King",
'updated' => strtotime('2000-01-02 00:00:02'),
'feed_id' => 8,
'feed_title' => "Feed 11",
'attachments' => [
[
'content_url' => "http://example.com/text",
'content_type' => "text/plain",
'title' => "",
'duration' => "",
'width' => "",
'height' => "",
'post_id' => 102,
],
],
'score' => 0,
'note' => "Note 2",
'lang' => "",
'content' => '<p>Article content 2</p>',
],
];
$this->assertEquals($this->respGood($exp), $this->h->dispatch(new Request("POST", "", json_encode($in[4]))));
$this->assertEquals($this->respGood([$exp[0]]), $this->h->dispatch(new Request("POST", "", json_encode($in[5]))));
$this->assertEquals($this->respGood([$exp[1]]), $this->h->dispatch(new Request("POST", "", json_encode($in[6]))));
// test the special case when labels are not used
Phake::when(Arsse::$db)->labelList($this->anything())->thenReturn(new Result([]));
Phake::when(Arsse::$db)->labelList($this->anything(), false)->thenReturn(new Result([]));
$this->assertEquals($this->respGood([$exp[0]]), $this->h->dispatch(new Request("POST", "", json_encode($in[5]))));
}
}

72
tests/lib/Database/SeriesArticle.php

@ -46,21 +46,22 @@ trait SeriesArticle {
'columns' => [
'id' => "int",
'url' => "str",
'title' => "str",
],
'rows' => [
[1,"http://example.com/1"],
[2,"http://example.com/2"],
[3,"http://example.com/3"],
[4,"http://example.com/4"],
[5,"http://example.com/5"],
[6,"http://example.com/6"],
[7,"http://example.com/7"],
[8,"http://example.com/8"],
[9,"http://example.com/9"],
[10,"http://example.com/10"],
[11,"http://example.com/11"],
[12,"http://example.com/12"],
[13,"http://example.com/13"],
[1,"http://example.com/1", "Feed 1"],
[2,"http://example.com/2", "Feed 2"],
[3,"http://example.com/3", "Feed 3"],
[4,"http://example.com/4", "Feed 4"],
[5,"http://example.com/5", "Feed 5"],
[6,"http://example.com/6", "Feed 6"],
[7,"http://example.com/7", "Feed 7"],
[8,"http://example.com/8", "Feed 8"],
[9,"http://example.com/9", "Feed 9"],
[10,"http://example.com/10", "Feed 10"],
[11,"http://example.com/11", "Feed 11"],
[12,"http://example.com/12", "Feed 12"],
[13,"http://example.com/13", "Feed 13"],
]
],
'arsse_subscriptions' => [
@ -69,22 +70,23 @@ trait SeriesArticle {
'owner' => "str",
'feed' => "int",
'folder' => "int",
'title' => "str",
],
'rows' => [
[1,"john.doe@example.com",1,null],
[2,"john.doe@example.com",2,null],
[3,"john.doe@example.com",3,1],
[4,"john.doe@example.com",4,6],
[5,"john.doe@example.com",10,5],
[6,"jane.doe@example.com",1,null],
[7,"jane.doe@example.com",10,null],
[8,"john.doe@example.org",11,null],
[9,"john.doe@example.org",12,null],
[10,"john.doe@example.org",13,null],
[11,"john.doe@example.net",10,null],
[12,"john.doe@example.net",2,9],
[13,"john.doe@example.net",3,8],
[14,"john.doe@example.net",4,7],
[1, "john.doe@example.com",1, null,"Subscription 1"],
[2, "john.doe@example.com",2, null,null],
[3, "john.doe@example.com",3, 1,"Subscription 3"],
[4, "john.doe@example.com",4, 6,null],
[5, "john.doe@example.com",10, 5,"Subscription 5"],
[6, "jane.doe@example.com",1, null,null],
[7, "jane.doe@example.com",10,null,"Subscription 7"],
[8, "john.doe@example.org",11,null,null],
[9, "john.doe@example.org",12,null,"Subscription 9"],
[10,"john.doe@example.org",13,null,null],
[11,"john.doe@example.net",10,null,"Subscription 11"],
[12,"john.doe@example.net",2, 9,null],
[13,"john.doe@example.net",3, 8,"Subscription 13"],
[14,"john.doe@example.net",4, 7,null],
]
],
'arsse_articles' => [
@ -198,9 +200,9 @@ trait SeriesArticle {
[5, 19,1,0,'2000-01-01 00:00:00',''],
[5, 20,0,1,'2010-01-01 00:00:00',''],
[7, 20,1,0,'2010-01-01 00:00:00',''],
[8, 102,1,0,'2000-01-02 02:00:00',''],
[9, 103,0,1,'2000-01-03 03:00:00',''],
[9, 104,1,1,'2000-01-04 04:00:00',''],
[8, 102,1,0,'2000-01-02 02:00:00','Note 2'],
[9, 103,0,1,'2000-01-03 03:00:00','Note 3'],
[9, 104,1,1,'2000-01-04 04:00:00','Note 4'],
[10,105,0,0,'2000-01-05 05:00:00',''],
[11, 19,0,0,'2017-01-01 00:00:00','ook'],
[11, 20,1,0,'2017-01-01 00:00:00','eek'],
@ -243,6 +245,7 @@ trait SeriesArticle {
'id' => 101,
'url' => 'http://example.com/1',
'title' => 'Article title 1',
'subscription_title' => "Feed 11",
'author' => '',
'content' => '<p>Article content 1</p>',
'guid' => 'e433653cef2e572eee4215fa299a4a5af9137b2cefd6283c85bd69a32915beda',
@ -256,11 +259,13 @@ trait SeriesArticle {
'fingerprint' => 'f5cb8bfc1c7396dc9816af212a3e2ac5221585c2a00bf7ccb6aabd95dcfcd6a6:fb0bc8f8cb08913dc5a497db700e327f1d34e4987402687d494a5891f24714d4:18fdd4fa93d693128c43b004399e5c9cea6c261ddfa002518d3669f55d8c2207',
'media_url' => null,
'media_type' => null,
'note' => "",
],
[
'id' => 102,
'url' => 'http://example.com/2',
'title' => 'Article title 2',
'subscription_title' => "Feed 11",
'author' => '',
'content' => '<p>Article content 2</p>',
'guid' => '5be8a5a46ecd52ed132191c8d27fb1af6b3d4edc00234c5d9f8f0e10562ed3b7',
@ -274,11 +279,13 @@ trait SeriesArticle {
'fingerprint' => '0e86d2de822a174fe3c44a466953e63ca1f1a58a19cbf475fce0855d4e3d5153:13075894189c47ffcfafd1dfe7fbb539f7c74a69d35a399b3abf8518952714f9:2abd0a8cba83b8214a66c8f0293ba63e467d720540e29ff8ddcdab069d4f1c9e',
'media_url' => "http://example.com/text",
'media_type' => "text/plain",
'note' => "Note 2",
],
[
'id' => 103,
'url' => 'http://example.com/3',
'title' => 'Article title 3',
'subscription_title' => "Subscription 9",
'author' => '',
'content' => '<p>Article content 3</p>',
'guid' => '31a6594500a48b59fcc8a075ce82b946c9c3c782460d088bd7b8ef3ede97ad92',
@ -292,11 +299,13 @@ trait SeriesArticle {
'fingerprint' => 'f74b06b240bd08abf4d3fdfc20dba6a6f6eb8b4f1a00e9a617efd63a87180a4b:b278380e984cefe63f0e412b88ffc9cb0befdfa06fdc00bace1da99a8daff406:ad622b31e739cd3a3f3c788991082cf4d2f7a8773773008e75f0572e58cd373b',
'media_url' => "http://example.com/video",
'media_type' => "video/webm",
'note' => "Note 3",
],
[
'id' => 104,
'url' => 'http://example.com/4',
'title' => 'Article title 4',
'subscription_title' => "Subscription 9",
'author' => '',
'content' => '<p>Article content 4</p>',
'guid' => '804e517d623390e71497982c77cf6823180342ebcd2e7d5e32da1e55b09dd180',
@ -310,11 +319,13 @@ trait SeriesArticle {
'fingerprint' => 'f3615c7f16336d3ea242d35cf3fc17dbc4ee3afb78376bf49da2dd7a5a25dec8:f11c2b4046f207579aeb9c69a8c20ca5461cef49756ccfa5ba5e2344266da3b3:ab2da63276acce431250b18d3d49b988b226a99c7faadf275c90b751aee05be9',
'media_url' => "http://example.com/image",
'media_type' => "image/svg+xml",
'note' => "Note 4",
],
[
'id' => 105,
'url' => 'http://example.com/5',
'title' => 'Article title 5',
'subscription_title' => "Feed 13",
'author' => '',
'content' => '<p>Article content 5</p>',
'guid' => 'db3e736c2c492f5def5c5da33ddcbea1824040e9ced2142069276b0a6e291a41',
@ -328,6 +339,7 @@ trait SeriesArticle {
'fingerprint' => 'd40da96e39eea6c55948ccbe9b3d275b5f931298288dbe953990c5f496097022:834240f84501b5341d375414718204ec421561f3825d34c22bf9182203e42900:43b970ac6ec5f8a9647b2c7e4eed8b1d7f62e154a95eed748b0294c1256764ba',
'media_url' => "http://example.com/audio",
'media_type' => "audio/ogg",
'note' => "",
],
];

Loading…
Cancel
Save