diff --git a/docs/en/030_Supported_Protocols/005_Miniflux.md b/docs/en/030_Supported_Protocols/005_Miniflux.md index 6a063bfa..a85a61ea 100644 --- a/docs/en/030_Supported_Protocols/005_Miniflux.md +++ b/docs/en/030_Supported_Protocols/005_Miniflux.md @@ -34,6 +34,7 @@ Miniflux version 2.0.27 is emulated, though not all features are implemented - The "All" category is treated specially (see below for details) - Category names consisting only of whitespace are rejected along with the empty string - Filtering rules may not function identically (see below for details) +- The `checked_at` field of feeds indicates when the feed was last updated rather than when it was last checked # Behaviour of filtering (block and keep) rules diff --git a/lib/Database.php b/lib/Database.php index 77af92ca..3866ddaf 100644 --- a/lib/Database.php +++ b/lib/Database.php @@ -777,8 +777,9 @@ class Database { * - "edited": The date and time at which the newsfeed was last modified by its authors * - "modified": The date and time at which the subscription properties were last changed by the user * - "next_fetch": The date and time and which the feed will next be fetched - * - "unread": The number of unread articles associated with the subscription * - "etag": The ETag header-field in the last fetch response + * - "scrape": Whether the user wants scrape full-article content + * - "unread": The number of unread articles associated with the subscription * * @param string $user The user whose subscriptions are to be listed * @param integer|null $folder The identifier of the folder under which to list subscriptions; by default the root folder is used diff --git a/lib/REST/Miniflux/V1.php b/lib/REST/Miniflux/V1.php index c1bc8e57..43d9e36c 100644 --- a/lib/REST/Miniflux/V1.php +++ b/lib/REST/Miniflux/V1.php @@ -591,50 +591,59 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { return new EmptyResponse(204); } - protected function getFeeds(): ResponseInterface { + protected function mapFolders(): array { $meta = Arsse::$user->propertiesGet(Arsse::$user->id, false); - $tr = Arsse::$db->begin(); - $out = []; - // compile the list of folders; the feed list includes folder names $folders = [0 => ['id' => 1, 'title' => $meta['root_folder_name'] ?? Arsse::$lang->msg("API.Miniflux.DefaultCategoryName"), 'user_id' => $meta['num']]]; - foreach(Arsse::$db->folderList(Arsse::$user->id, null, false) as $r) { + foreach (Arsse::$db->folderList(Arsse::$user->id, null, false) as $r) { $folders[(int) $r['id']] = [ 'id' => ((int) $r['id']) + 1, 'title' => $r['name'], 'user_id' => $meta['num'], ]; } + return $folders; + } + + protected function transformFeed(array $sub, array $folders): array { + $url = new Uri($sub['url']); + return [ + 'id' => (int) $sub['id'], + 'user_id' => $folders[0]['user_id'], + 'feed_url' => (string) $url->withUserInfo(""), + 'site_url' => (string) $sub['source'], + 'title' => (string) $sub['title'], + 'checked_at' => Date::transform($sub['updated'], "iso8601m", "sql"), + 'next_check_at' => Date::transform($sub['next_fetch'], "iso8601m", "sql") ?? "0001-01-01T00:00:00.000000Z", + 'etag_header' => (string) $sub['etag'], + 'last_modified_header' => (string) Date::transform($sub['edited'], "http", "sql"), + 'parsing_error_message' => (string) $sub['err_msg'], + 'parsing_error_count' => (int) $sub['err_count'], + 'scraper_rules' => "", + 'rewrite_rules' => "", + 'crawler' => (bool) $sub['scrape'], + 'blocklist_rules' => (string) $sub['block_rule'], + 'keeplist_rules' => (string) $sub['keep_rule'], + 'user_agent' => "", + 'username' => rawurldecode(explode(":", $url->getUserInfo(), 2)[0] ?? ""), + 'password' => rawurldecode(explode(":", $url->getUserInfo(), 2)[1] ?? ""), + 'disabled' => false, + 'ignore_http_cache' => false, + 'fetch_via_proxy' => false, + 'category' => $folders[(int) $sub['top_folder']], + 'icon' => $sub['icon_id'] ? ['feed_id' => (int) $sub['id'], 'icon_id' => (int) $sub['icon_id']] : null, + ]; + } + + protected function getFeeds(): ResponseInterface { + $tr = Arsse::$db->begin(); + // compile the list of folders; the feed list includes folder names + $folders = $this->mapFolders(); // next compile the list of feeds + $out = []; foreach (Arsse::$db->subscriptionList(Arsse::$user->id) as $r) { - $url = new Uri($r['url']); - $out = [ - 'id' => (int) $r['id'], - 'user_id' => $meta['num'], - 'feed_url' => $url->withUserInfo(""), - 'site_url' => $r['source'], - 'title' => $r['title'], - 'checked_at' => Date::transform($r['updated'], "iso8601", "sql"), - 'next_check_at' => Date::transform($r['next_fetch'], "iso8601", "sql") ?? "0001-01-01T00:00:00Z", - 'etag_header' => $r['etag'] ?? "", - 'last_modified_header' => (string) Date::transform($r['edited'], "http", "sql"), - 'parsing_error_message' => (string) $r['err_msg'], - 'parsing_error_count' => (int) $r['err_count'], - 'scraper_rules' => "", - 'rewrite_rules' => "", - 'crawler' => (bool) $r['scrape'], - 'blocklist_rules' => (string) $r['block_rule'], - 'keeplist_rules' => (string) $r['keep_rule'], - 'user_agent' => "", - 'username' => explode(":", $url->getUserInfo(), 2)[0] ?? "", - 'password' => explode(":", $url->getUserInfo(), 2)[1] ?? "", - 'disabled' => false, - 'ignore_http_cache' => false, - 'fetch_via_proxy' => false, - 'category' => $folders[$r['top_folder']], - 'icon' => $r['icon_id'] ? ['feed_id' => (int) $r['id'], 'icon_id' => (int) $r['icon_id']] : null, - ]; - return new Response($out); + $out[] = $this->transformFeed($r, $folders); } + return new Response($out); } public static function tokenGenerate(string $user, string $label): string { diff --git a/tests/cases/REST/Miniflux/TestV1.php b/tests/cases/REST/Miniflux/TestV1.php index 496df378..54e03435 100644 --- a/tests/cases/REST/Miniflux/TestV1.php +++ b/tests/cases/REST/Miniflux/TestV1.php @@ -525,4 +525,80 @@ class TestV1 extends \JKingWeb\Arsse\Test\AbstractTest { \Phake::verify(Arsse::$db)->articleMark("john.doe@example.com", ['read' => true], (new Context)->folder(2111)) ); } + + public function testListReeds(): void { + \Phake::when(Arsse::$db)->folderList->thenReturn(new Result([ + ['id' => 5, 'name' => "Cat Ook"], + ])); + \Phake::when(Arsse::$db)->subscriptionList->thenReturn(new Result([ + ['id' => 1, 'feed' => 12, 'url' => "http://example.com/ook", 'title' => "Ook", 'source' => "http://example.com/", 'icon_id' => 47, 'icon_url' => "http://example.com/icon", 'folder' => 2112, 'top_folder' => 5, 'pinned' => 0, 'err_count' => 1, 'err_msg' => "Oopsie", 'order_type' => 0, 'keep_rule' => "this|that", 'block_rule' => "both", 'added' => "2020-12-21 21:12:00", 'updated' => "2021-01-05 13:51:32", 'edited' => "2021-01-01 00:00:00", 'modified' => "2020-11-30 04:08:52", 'next_fetch' => "2021-01-20 00:00:00", 'etag' => "OOKEEK", 'scrape' => 0, 'unread' => 42], + ['id' => 55, 'feed' => 12, 'url' => "http://j%20k:super%20secret@example.com/eek", 'title' => "Eek", 'source' => "http://example.com/", 'icon_id' => null, 'icon_url' => null, 'folder' => null, 'top_folder' => null, 'pinned' => 0, 'err_count' => 0, 'err_msg' => null, 'order_type' => 0, 'keep_rule' => null, 'block_rule' => null, 'added' => "2020-12-21 21:12:00", 'updated' => "2021-01-05 13:51:32", 'edited' => null, 'modified' => "2020-11-30 04:08:52", 'next_fetch' => null, 'etag' => null, 'scrape' => 1, 'unread' => 0], + ])); + $exp = new Response([ + [ + 'id' => 1, + 'user_id' => 42, + 'feed_url' => "http://example.com/ook", + 'site_url' => "http://example.com/", + 'title' => "Ook", + 'checked_at' => "2021-01-05T13:51:32.000000Z", + 'next_check_at' => "2021-01-20T00:00:00.000000Z", + 'etag_header' => "OOKEEK", + 'last_modified_header' => "Fri, 01 Jan 2021 00:00:00 GMT", + 'parsing_error_message' => "Oopsie", + 'parsing_error_count' => 1, + 'scraper_rules' => "", + 'rewrite_rules' => "", + 'crawler' => false, + 'blocklist_rules' => "both", + 'keeplist_rules' => "this|that", + 'user_agent' => "", + 'username' => "", + 'password' => "", + 'disabled' => false, + 'ignore_http_cache' => false, + 'fetch_via_proxy' => false, + 'category' => [ + 'id' => 6, + 'title' => "Cat Ook", + 'user_id' => 42 + ], + 'icon' => [ + 'feed_id' => 1, + 'icon_id' => 47 + ], + ], + [ + 'id' => 55, + 'user_id' => 42, + 'feed_url' => "http://example.com/eek", + 'site_url' => "http://example.com/", + 'title' => "Eek", + 'checked_at' => "2021-01-05T13:51:32.000000Z", + 'next_check_at' => "0001-01-01T00:00:00.000000Z", + 'etag_header' => "", + 'last_modified_header' => "", + 'parsing_error_message' => "", + 'parsing_error_count' => 0, + 'scraper_rules' => "", + 'rewrite_rules' => "", + 'crawler' => true, + 'blocklist_rules' => "", + 'keeplist_rules' => "", + 'user_agent' => "", + 'username' => "j k", + 'password' => "super secret", + 'disabled' => false, + 'ignore_http_cache' => false, + 'fetch_via_proxy' => false, + 'category' => [ + 'id' => 1, + 'title' => "All", + 'user_id' => 42 + ], + 'icon' => null, + ], + ]); + $this->assertMessage($exp, $this->req("GET", "/feeds")); + } }